diff --git a/rclpy/CMakeLists.txt b/rclpy/CMakeLists.txt index 4c81ea76f..14f8c89d2 100644 --- a/rclpy/CMakeLists.txt +++ b/rclpy/CMakeLists.txt @@ -157,8 +157,12 @@ if(BUILD_TESTING) ) endif() + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_mypy) ament_lint_auto_find_test_dependencies() + find_package(ament_cmake_mypy REQUIRED) + ament_mypy(AMENT_STRICT) + find_package(ament_cmake_cppcheck REQUIRED) ament_cppcheck() set_tests_properties(cppcheck PROPERTIES TIMEOUT 420) diff --git a/rclpy/package.xml b/rclpy/package.xml index 68de5ddc2..78a51bf74 100644 --- a/rclpy/package.xml +++ b/rclpy/package.xml @@ -62,6 +62,7 @@ ament_cmake_pytest ament_lint_auto ament_lint_common + ament_cmake_mypy python3-pytest rosidl_generator_py test_msgs diff --git a/rclpy/rclpy/endpoint_info.py b/rclpy/rclpy/endpoint_info.py index 90ec30aeb..38ecf5e33 100644 --- a/rclpy/rclpy/endpoint_info.py +++ b/rclpy/rclpy/endpoint_info.py @@ -234,7 +234,7 @@ def __init__( node_namespace: str = '', service_type: str = '', service_type_hash: Union[TypeHash, TypeHashDictionary] = TypeHash(), - qos_profiles: Sequence[Union[QoSProfile, '_rclpy._rmw_qos_profile_dict']] = [], + qos_profiles: list[QoSProfile] | list['_rclpy._rmw_qos_profile_dict'] = [], endpoint_gids: list[list[int]] = [], endpoint_type: Union[EndpointTypeEnum, int] = EndpointTypeEnum.INVALID, endpoint_count: int = 0 @@ -368,8 +368,8 @@ def qos_profiles(self) -> Annotated[Any, list[QoSProfile]]: return self._qos_profiles @qos_profiles.setter - def qos_profiles(self, value: list[Union[QoSProfile, '_rclpy._rmw_qos_profile_dict']]) -> None: - assert isinstance(value, list) + def qos_profiles(self, value: Sequence[Union[QoSProfile, + '_rclpy._rmw_qos_profile_dict']]) -> None: self._qos_profiles = [v if isinstance(v, QoSProfile) else QoSProfile(**v) for v in value] def __eq__(self, other: object) -> bool: diff --git a/rclpy/rclpy/executors.py b/rclpy/rclpy/executors.py index 2983b27d1..0307831fe 100644 --- a/rclpy/rclpy/executors.py +++ b/rclpy/rclpy/executors.py @@ -560,7 +560,7 @@ def _take_subscription(self, sub: Subscription[Any] msg_tuple = msg_info async def _execute() -> None: - await await_or_execute(sub.callback, *msg_tuple) + await await_or_execute(sub.callback, *msg_tuple) # type: ignore [arg-type] return _execute except InvalidHandle: diff --git a/rclpy/rclpy/experimental/async_node.py b/rclpy/rclpy/experimental/async_node.py index 58acb3741..0dce63ec5 100644 --- a/rclpy/rclpy/experimental/async_node.py +++ b/rclpy/rclpy/experimental/async_node.py @@ -140,7 +140,7 @@ async def __aenter__(self) -> 'AsyncNode': self._tg = await tg.__aenter__() for entity in self._entities: if hasattr(entity, '_run'): - entity._task = self._tg.create_task(entity._run()) + entity._task = self._tg.create_task(entity._run()) # type: ignore[union-attr] return self async def __aexit__( @@ -221,16 +221,16 @@ def create_publisher( """ if self._destroyed.is_set(): raise RuntimeError('Cannot create publisher on a destroyed node') - qos_profile = self._validate_qos_or_depth_parameter(qos_profile) + qos_profile_validated = self._validate_qos_or_depth_parameter(qos_profile) publisher_handle = self._create_publisher_handle( - msg_type, topic, qos_profile) + msg_type, topic, qos_profile_validated) pub = AsyncPublisher( publisher_handle, msg_type, topic, - qos_profile, + qos_profile_validated, on_destroy=self._entities.discard, ) self._entities.add(pub) diff --git a/rclpy/rclpy/impl/_rclpy_pybind11.pyi b/rclpy/rclpy/impl/_rclpy_pybind11.pyi index 6ea0a9a43..cce8f5395 100644 --- a/rclpy/rclpy/impl/_rclpy_pybind11.pyi +++ b/rclpy/rclpy/impl/_rclpy_pybind11.pyi @@ -618,7 +618,8 @@ class Subscription(Destroyable, Generic[MsgT]): def __init__(self, node: Node, pymsg_type: type[MsgT], topic: str, pyqos_profile: rmw_qos_profile_t, - content_filter_options: Optional[ContentFilterOptions] = None) -> None: ... + content_filter_options: Optional[ContentFilterOptions] = None, + acceptable_buffer_backends: str | None = None) -> None: ... @property def pointer(self) -> int: @@ -1077,6 +1078,13 @@ class rmw_incompatible_type_status_t: def total_count_change(self) -> int: ... +def publisher_event_type_is_supported(event_type: rcl_publisher_event_type_t) -> bool: + """Check if a publisher event type is supported by the active RMW implementation.""" + +def subscription_event_type_is_supported(event_type: rcl_subscription_event_type_t) -> bool: + """Check if a subscription event type is supported by the active RMW implementation.""" + + def rclpy_get_rmw_implementation_identifier() -> str: """Retrieve the identifier for the active RMW implementation.""" diff --git a/rclpy/rclpy/node.py b/rclpy/rclpy/node.py index 8d558b828..15a88eb2a 100644 --- a/rclpy/rclpy/node.py +++ b/rclpy/rclpy/node.py @@ -494,8 +494,6 @@ def declare_parameters( 'being included in self._parameter_overrides, and ', 'ignore_override=False') - from typing import cast - value = cast(AllowableParameterValue, value) parameter_list.append(Parameter(name, value=value)) descriptors.update({name: descriptor}) @@ -1927,7 +1925,7 @@ def _create_publisher_handle( self, msg_type: Type[MsgT], topic: str, - qos_profile: Union[QoSProfile, int], + qos_profile: QoSProfile, *, qos_overriding_options: Optional[QoSOverridingOptions] = None, ) -> '_rclpy.Publisher[MsgT]': @@ -2254,20 +2252,20 @@ def create_publisher( :param event_callbacks: User-defined callbacks for middleware events. :return: The new publisher. """ - qos_profile = self._validate_qos_or_depth_parameter(qos_profile) + qos_profile_validated = self._validate_qos_or_depth_parameter(qos_profile) callback_group = callback_group or self.default_callback_group publisher_object = self._create_publisher_handle( msg_type, topic, - qos_profile, + qos_profile_validated, qos_overriding_options=qos_overriding_options ) try: publisher = publisher_class( - publisher_object, msg_type, topic, qos_profile, + publisher_object, msg_type, topic, qos_profile_validated, on_destroy=self._on_destroy_publisher, event_callbacks=event_callbacks or PublisherEventCallbacks(), callback_group=callback_group) diff --git a/rclpy/rclpy/parameter_service.py b/rclpy/rclpy/parameter_service.py index e7fc7432f..b205cf3ca 100644 --- a/rclpy/rclpy/parameter_service.py +++ b/rclpy/rclpy/parameter_service.py @@ -97,7 +97,7 @@ def _get_parameters_callback( param = node.get_parameter(name) except (ParameterNotDeclaredException, ParameterUninitializedException) as ex: node.get_logger().warning(f'Failed to get parameters: {ex}') - response.values = node.get_parameters([]) + response.values = [] return response response.values.append(param.get_parameter_value()) return response diff --git a/rclpy/rclpy/task.py b/rclpy/rclpy/task.py index ff49d2afa..6c5df7f8f 100644 --- a/rclpy/rclpy/task.py +++ b/rclpy/rclpy/task.py @@ -12,12 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from collections.abc import Callable +from collections.abc import Generator from enum import Enum import inspect import sys import threading -from typing import (Any, Callable, cast, Coroutine, Dict, Generator, Generic, List, - Optional, overload, Tuple, TYPE_CHECKING, TypeVar, Union) +from typing import Any +from typing import Coroutine +from typing import Generic +from typing import Optional +from typing import overload +from typing import TYPE_CHECKING +from typing import TypeVar +from typing import Union + import warnings import weakref @@ -51,7 +60,7 @@ def __init__(self, *, executor: Optional['Executor'] = None) -> None: self._exception: Optional[Exception] = None self._exception_fetched = False # callbacks or tasks to be scheduled after this task completes - self._callbacks: List[Union[Callable[['Future[T]'], None], 'Task[Any]']] = [] + self._callbacks: list[Union[Callable[['Future[T]'], None], 'Task[Any]']] = [] # Lock for threadsafety self._lock = threading.Lock() # An executor to use when scheduling done callbacks @@ -260,42 +269,41 @@ class Task(Future[T]): @overload def __init__(self, handler: Callable[..., Coroutine[Any, Any, T]], - args: Optional[Tuple[Any, ...]] = None, - kwargs: Optional[Dict[str, Any]] = None, + args: Optional[tuple[Any, ...]] = None, + kwargs: Optional[dict[str, Any]] = None, executor: Optional['Executor'] = None) -> None: ... @overload def __init__(self, handler: Callable[..., T], - args: Optional[Tuple[Any, ...]] = None, - kwargs: Optional[Dict[str, Any]] = None, + args: Optional[tuple[Any, ...]] = None, + kwargs: Optional[dict[str, Any]] = None, executor: Optional['Executor'] = None) -> None: ... def __init__(self, handler: Callable[..., Any], - args: Optional[Tuple[Any, ...]] = None, - kwargs: Optional[Dict[str, Any]] = None, + args: Optional[tuple[Any, ...]] = None, + kwargs: Optional[dict[str, Any]] = None, executor: Optional['Executor'] = None) -> None: super().__init__(executor=executor) # Arguments passed into the function if args is None: args = () - self._args: Optional[Tuple[Any, ...]] = args + self._args: Optional[tuple[Any, ...]] = args if kwargs is None: kwargs = {} - self._kwargs: Optional[Dict[str, Any]] = kwargs + self._kwargs: Optional[dict[str, Any]] = kwargs # _handler is either a normal function or a coroutine if inspect.iscoroutinefunction(handler): self._handler: Union[ Coroutine[Any, Any, T], - Callable[[], T], + Callable[..., T], None - ] = cast(Coroutine[Any, Any, T], handler(*args, **kwargs)) + ] = handler(*args, **kwargs) self._args = None self._kwargs = None else: - handler = cast(Callable[[], T], handler) self._handler = handler # True while the task is being executed self._executing = False @@ -328,6 +336,7 @@ def __call__(self) -> None: # Execute a normal function try: assert self._handler is not None and callable(self._handler) + assert self._args is not None and self._kwargs is not None self.set_result(self._handler(*self._args, **self._kwargs)) except Exception as e: self.set_exception(e) diff --git a/rclpy/test/test_events_executor.py b/rclpy/test/test_events_executor.py index e31edffbc..cd8e37d7f 100644 --- a/rclpy/test/test_events_executor.py +++ b/rclpy/test/test_events_executor.py @@ -697,7 +697,7 @@ def test_timers(self) -> None: # Only one of the callbacks should be delivered, though we can't necessarily predict which # one. def handler() -> None: - nonlocal count + nonlocal count # type: ignore[misc] count += 1 timer1.cancel() timer2.cancel()