diff --git a/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake b/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake index cfc424a7..b1539c6c 100644 --- a/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake +++ b/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake @@ -289,5 +289,10 @@ if(BUILD_TESTING AND rosidl_generate_interfaces_ADD_LINTER_TESTS) # a value of zero tells uncrustify to ignore line length MAX_LINE_LENGTH 0 "${_output_path}") + + find_package(ament_cmake_mypy REQUIRED) + ament_mypy( + TESTNAME "mypy_rosidl_generate_py" + "${_output_path}") endif() endif() diff --git a/rosidl_generator_py/package.xml b/rosidl_generator_py/package.xml index 3aab7c2a..7b0a053d 100644 --- a/rosidl_generator_py/package.xml +++ b/rosidl_generator_py/package.xml @@ -36,6 +36,7 @@ ament_cmake_cppcheck ament_cmake_cpplint ament_cmake_flake8 + ament_cmake_mypy ament_cmake_pep257 ament_cmake_uncrustify diff --git a/rosidl_generator_py/py.typed b/rosidl_generator_py/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/rosidl_generator_py/resource/_action.py.em b/rosidl_generator_py/resource/_action.py.em index 77fbf875..50b8ef08 100644 --- a/rosidl_generator_py/resource/_action.py.em +++ b/rosidl_generator_py/resource/_action.py.em @@ -38,7 +38,7 @@ class Metaclass_@(action.namespaced_type.name)(type): _TYPE_SUPPORT = None @@classmethod - def __import_type_support__(cls): + def __import_type_support__(cls) -> None: try: from rosidl_generator_py import import_type_support module = import_type_support('@(package_name)') @@ -92,5 +92,5 @@ class @(action.namespaced_type.name)(metaclass=Metaclass_@(action.namespaced_typ # The generic message for get the status of a goal. from action_msgs.msg._goal_status_array import GoalStatusArray as GoalStatusMessage - def __init__(self): + def __init__(self) -> None: raise NotImplementedError('Action classes can not be instantiated') diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 3f49224a..6a876379 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -5,7 +5,9 @@ # This is being done at the module level and not on the instance level to avoid looking # for the same variable multiple times on each instance. This variable is not supposed to # change during runtime so it makes sense to only look for it once. +from collections.abc import MutableMapping from os import getenv +from typing import Any, ClassVar, Dict, Optional, Tuple, Type, TYPE_CHECKING, TypedDict ros_python_check_fields = getenv('ROS_PYTHON_CHECK_FIELDS', default='') @ diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 8c21e552..7d43d782 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -29,6 +29,156 @@ from rosidl_parser.definition import SIGNED_INTEGER_TYPES from rosidl_parser.definition import UnboundedSequence from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ +@{ +from typing import Set +import_type_checking = False +type_annotations_setter = {} +type_annotations_getter = {} +type_imports: Set[str] = set() + +for member in message.structure.members: + type_ = member.type + + if isinstance(type_, AbstractNestedType): + type_ = type_.value_type + + python_type = get_python_type(type_) + + ANY = 'Any' # Done because of mypy#3004 + + type_annotation = '' + type_annotations_getter[member.name] = '' + + if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: + + type_annotations_getter[member.name] = ANY + + if isinstance(member.type, Array): + dtype = SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'] + type_annotation = f'NDArray[{dtype}], ' + type_imports.add('from numpy.typing import NDArray') + elif isinstance(member.type, AbstractSequence): + # Uses MutableSequence because array does not support subscripting + type_annotation = f'MutableSequence[{python_type}], ' + type_imports.add('from collections.abc import MutableSequence') + + if isinstance(member.type, AbstractNestedType): + type_annotations_getter[member.name] = ANY + + type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' + f'Set[{python_type}], UserList[{python_type}]]') + + type_imports.add('from typing import Union') + type_imports.add('from collections.abc import Sequence') + type_imports.add('from collections.abc import Set') + type_imports.add('from collections import UserList') + elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size(): + type_annotation = 'Union[str, UserString]' + + type_imports.add('from typing import Union') + type_imports.add('from collections import UserString') + elif isinstance(type_, BasicType) and type_.typename == 'char': + type_annotation = 'Union[str, UserString]' + + type_imports.add('from typing import Union') + type_imports.add('from collections import UserString') + elif isinstance(type_, BasicType) and type_.typename == 'octet': + type_annotation = 'Union[bytes, ByteString]' + + type_imports.add('from typing import Union') + type_imports.add('from collections.abc import ByteString') + else: + type_annotation = python_type + + if isinstance(type_, NamespacedType): + + joined_type_namespaces = '.'.join(type_.namespaces) + if(type_.name.endswith(ACTION_GOAL_SUFFIX) or type_.name.endswith(ACTION_RESULT_SUFFIX) or type_.name.endswith(ACTION_FEEDBACK_SUFFIX)): + type_name_rsplit = type_.name.rsplit('_', 1) + type_imports.add(f'from {joined_type_namespaces}._{convert_camel_case_to_lower_case_underscore(type_name_rsplit[0])} import {type_.name}') + else: + type_imports.add(f'from {joined_type_namespaces} import {type_.name}') + + type_annotations_setter[member.name] = f'\'{type_annotation}\'' + + if type_annotations_getter[member.name] == '': + type_annotations_getter[member.name] = type_annotations_setter[member.name] + + + +def get_type_annotation_constant_default(constant, value, type_imports) -> str: + from rosidl_parser.definition import AbstractNestedType, BasicType, NamespacedType, AbstractSequence, Array + from rosidl_generator_py.generate_py_impl import SPECIAL_NESTED_BASIC_TYPES, get_python_type, constant_value_to_py + + type_ = constant.type + + if isinstance(type_, AbstractNestedType): + type_ = type_.value_type + + python_type = get_python_type(type_) + + type_annotation = '' + + if isinstance(constant.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: + if isinstance(constant.type, Array): + dtype = SPECIAL_NESTED_BASIC_TYPES[constant.type.value_type.typename]['dtype'] + type_annotation = f'NDArray[{dtype}]' + type_imports.add('from numpy.typing import NDArray') + elif isinstance(constant.type, AbstractSequence): + # Uses MutableSequence because array does not support subscripting + type_annotation = f'MutableSequence[{python_type}]' + type_imports.add('from collections.abc import MutableSequence') + elif isinstance(constant.type, AbstractNestedType): + type_imports.add('from typing import List') + type_annotation = f'List[{python_type}]' + elif isinstance(type_, NamespacedType): + type_annotation = python_type + elif isinstance(type_, float): + return 'float' + else: + if isinstance(value, str): + if "'" in value or '"' in value: + return 'str' + else: + type_imports.add('from typing import Literal') + type_annotation = f'Literal["{value}"]' + elif isinstance(value, float): + return 'float' + elif type_.typename == 'octet': + return 'bytes' + else: + type_imports.add('from typing import Literal') + type_annotation = f'Literal[{value}]' + + type_annotation = f'\'{type_annotation}\'' + return type_annotation + +custom_type_annotations = {} + +for constant in message.constants: + value = constant.value + custom_type_annotations[constant.name] = get_type_annotation_constant_default(constant, value, type_imports) + +default_type_annotations = {} + +for member in message.structure.members: + if member.has_annotation('default'): + constant = member + value = constant.get_annotation_value('default')['value'] + default_type_annotations[constant.name] = get_type_annotation_constant_default(constant, value, type_imports) +}@ +@{ +suffix = '__'.join(message.structure.namespaced_type.namespaces[1:]) + '__' + convert_camel_case_to_lower_case_underscore(message.structure.namespaced_type.name) +}@ +if TYPE_CHECKING: + from ctypes import Structure + + class PyCapsule(Structure): + pass # don't need to define the full structure + +@[for type_import in type_imports]@ + @(type_import) +@[end for] @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @# Collect necessary import statements for all members @{ @@ -66,8 +216,6 @@ for member in message.structure.members: @ @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @[if imports]@ - - # Import statements for member types @[ for import_statement, member_names in sorted(imports.items())]@ @@ -91,22 +239,40 @@ for member in message.structure.members: class Metaclass_@(message.structure.namespaced_type.name)(type): """Metaclass of message '@(message.structure.namespaced_type.name)'.""" - _CREATE_ROS_MESSAGE = None - _CONVERT_FROM_PY = None - _CONVERT_TO_PY = None - _DESTROY_ROS_MESSAGE = None - _TYPE_SUPPORT = None + _CREATE_ROS_MESSAGE: ClassVar[Optional['PyCapsule']] = None + _CONVERT_FROM_PY: ClassVar[Optional['PyCapsule']] = None + _CONVERT_TO_PY: ClassVar[Optional['PyCapsule']] = None + _DESTROY_ROS_MESSAGE: ClassVar[Optional['PyCapsule']] = None + _TYPE_SUPPORT: ClassVar[Optional['PyCapsule']] = None + + class @(message.structure.namespaced_type.name)Constants(TypedDict): +@[if not custom_type_annotations]@ + pass +@[else]@ +@[for constant in message.constants]@ + @(constant.name): @(custom_type_annotations[constant.name]) +@[ end for]@ +@[end if]@ - __constants = { + __constants: @(message.structure.namespaced_type.name)Constants = { @[for constant in message.constants]@ '@(constant.name)': @constant_value_to_py(constant.type, constant.value), @[end for]@ } + class @(message.structure.namespaced_type.name)Default(@(message.structure.namespaced_type.name)Constants): +@[if not default_type_annotations]@ + pass +@[else]@ +@[ for name, type in default_type_annotations.items()]@ + @(name.upper())__DEFAULT: @(type) +@[ end for]@ +@[end if]@ + @@classmethod - def __import_type_support__(cls): + def __import_type_support__(cls) -> None: try: - from rosidl_generator_py import import_type_support + from rosidl_generator_py.rosidl_generator_py import import_type_support module = import_type_support('@(package_name)') except ImportError: import logging @@ -117,9 +283,6 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): 'Failed to import needed modules for type support:\n' + traceback.format_exc()) else: -@{ -suffix = '__'.join(message.structure.namespaced_type.namespaces[1:]) + '__' + convert_camel_case_to_lower_case_underscore(message.structure.namespaced_type.name) -}@ cls._CREATE_ROS_MESSAGE = module.create_ros_message_msg__@(suffix) cls._CONVERT_FROM_PY = module.convert_from_py_msg__@(suffix) cls._CONVERT_TO_PY = module.convert_to_py_msg__@(suffix) @@ -151,18 +314,18 @@ for member in message.structure.members: @[for typename in sorted(importable_typesupports)]@ from @('.'.join(typename[:-2])) import @(typename[-2]) - if @(typename[-1]).__class__._TYPE_SUPPORT is None: - @(typename[-1]).__class__.__import_type_support__() + if @(typename[-1])._TYPE_SUPPORT is None: + @(typename[-1]).__import_type_support__() @[end for]@ @@classmethod - def __prepare__(cls, name, bases, **kwargs): + def __prepare__(metacls, name: str, bases: Tuple[Type[Any], ...], /, **kwds: Any) -> MutableMapping[str, object]: # list constant names here so that they appear in the help text of # the message class under "Data and other attributes defined here:" # as well as populate each message instance return { @[for constant in message.constants]@ - '@(constant.name)': cls.__constants['@(constant.name)'], + '@(constant.name)': metacls.__constants['@(constant.name)'], @[end for]@ @[for member in message.structure.members]@ @[ if member.has_annotation('default')]@ @@ -173,7 +336,7 @@ for member in message.structure.members: @[for constant in message.constants]@ @@property - def @(constant.name)(self): + def @(constant.name)(self) -> @(custom_type_annotations[constant.name]): """Message constant '@(constant.name)'.""" return Metaclass_@(message.structure.namespaced_type.name).__constants['@(constant.name)'] @[end for]@ @@ -181,7 +344,7 @@ for member in message.structure.members: @[ if member.has_annotation('default')]@ @@property - def @(member.name.upper())__DEFAULT(cls): + def @(member.name.upper())__DEFAULT(cls) -> @(default_type_annotations[member.name]): """Return default value for message field '@(member.name)'.""" return @(value_to_py(member.type, member.get_annotation_value('default')['value'])) @[ end if]@ @@ -212,7 +375,7 @@ class @(message.structure.namespaced_type.name)(metaclass=Metaclass_@(message.st '_check_fields', ] - _fields_and_field_types = { + _fields_and_field_types: Dict[str, str] = { @[for member in message.structure.members]@ @[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ @[ continue]@ @@ -257,7 +420,7 @@ string@ # This attribute is used to store an rosidl_parser.definition variable # related to the data type of each of the components the message. - SLOT_TYPES = ( + SLOT_TYPES: Tuple[rosidl_parser.definition.AbstractType, ...] = ( @[for member in message.structure.members]@ @[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ @[ continue]@ @@ -292,16 +455,33 @@ if isinstance(type_, AbstractNestedType): , # noqa: E501 @[end for]@ ) +@{ +# Taken from flake8-builtins +# https://github.com/gforcada/flake8-builtins/blob/689ad01c03cb52ae73be23d19706e6a4a491f4e9/flake8_builtins.py +import inspect +import builtins +BUILTINS = [ + a[0] + for a in inspect.getmembers(builtins) +] +}@ - def __init__(self, **kwargs): - if 'check_fields' in kwargs: - self._check_fields = kwargs['check_fields'] + def __init__(self, +@[for member in message.structure.members]@ +@[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ +@[ continue]@ +@[ end if]@ +@[ if member.name in BUILTINS]@ + @(member.name): Optional[@(type_annotations_setter[member.name])] = None, # noqa: E501, A002 +@[ else]@ + @(member.name): Optional[@(type_annotations_setter[member.name])] = None, # noqa: E501 +@[ end if]@ +@[end for]@ + check_fields: Optional[bool] = None) -> None: + if check_fields is not None: + self._check_fields = check_fields else: self._check_fields = ros_python_check_fields == '1' - if self._check_fields: - assert all('_' + key in self.__slots__ for key in kwargs.keys()), \ - 'Invalid arguments passed to constructor: %s' % \ - ', '.join(sorted(k for k in kwargs.keys() if '_' + k not in self.__slots__)) @[for member in message.structure.members]@ @[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ @[ continue]@ @@ -312,8 +492,7 @@ if isinstance(type_, AbstractNestedType): type_ = type_.value_type }@ @[ if member.has_annotation('default')]@ - self.@(member.name) = kwargs.get( - '@(member.name)', @(message.structure.namespaced_type.name).@(member.name.upper())__DEFAULT) + self.@(member.name) = @(member.name) or @(message.structure.namespaced_type.name).@(member.name.upper())__DEFAULT @[ else]@ @[ if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence)]@ @[ if ( @@ -328,46 +507,37 @@ if isinstance(type_, AbstractNestedType): @[ end if]@ @[ if isinstance(member.type, Array)]@ @[ if isinstance(type_, BasicType) and type_.typename == 'octet']@ - self.@(member.name) = kwargs.get( - '@(member.name)', - [bytes([0]) for x in range(@(member.type.size))] - ) + self.@(member.name) = @(member.name) or [bytes([0]) for x in range(@(member.type.size))] @[ elif isinstance(type_, BasicType) and type_.typename in CHARACTER_TYPES]@ - self.@(member.name) = kwargs.get( - '@(member.name)', - [chr(0) for x in range(@(member.type.size))] - ) + self.@(member.name) = @(member.name) or [chr(0) for x in range(@(member.type.size))] @[ else]@ @[ if isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@ - if '@(member.name)' not in kwargs: + if @(member.name) is None: self.@(member.name) = numpy.zeros(@(member.type.size), dtype=@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'])) else: - self.@(member.name) = numpy.array(kwargs.get('@(member.name)'), dtype=@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'])) + self.@(member.name) = numpy.array(@(member.name), dtype=@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'])) assert self.@(member.name).shape == (@(member.type.size), ) @[ else]@ - self.@(member.name) = kwargs.get( - '@(member.name)', - [@(get_python_type(type_))() for x in range(@(member.type.size))] - ) + self.@(member.name) = @(member.name) or [@(get_python_type(type_))() for x in range(@(member.type.size))] @[ end if]@ @[ end if]@ @[ elif isinstance(member.type, AbstractSequence)]@ @[ if isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@ - self.@(member.name) = array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', kwargs.get('@(member.name)', [])) + self.@(member.name) = @(member.name) or array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) @[ else]@ - self.@(member.name) = kwargs.get('@(member.name)', []) + self.@(member.name) = @(member.name) or [] @[ end if]@ @[ elif isinstance(type_, BasicType) and type_.typename == 'octet']@ - self.@(member.name) = kwargs.get('@(member.name)', bytes([0])) + self.@(member.name) = @(member.name) or bytes([0]) @[ elif isinstance(type_, BasicType) and type_.typename in CHARACTER_TYPES]@ - self.@(member.name) = kwargs.get('@(member.name)', chr(0)) + self.@(member.name) = @(member.name) or chr(0) @[ else]@ - self.@(member.name) = kwargs.get('@(member.name)', @(get_python_type(type_))()) + self.@(member.name) = @(member.name) or @(get_python_type(type_))() @[ end if]@ @[ end if]@ @[end for]@ - def __repr__(self): + def __repr__(self) -> str: typename = self.__class__.__module__.split('.') typename.pop() typename.append(self.__class__.__name__) @@ -394,15 +564,15 @@ if isinstance(type_, AbstractNestedType): args.append(s + '=' + fieldstr) return '%s(%s)' % ('.'.join(typename), ', '.join(args)) - def __eq__(self, other): - if not isinstance(other, self.__class__): + def __eq__(self, other: object) -> bool: + if not isinstance(other, @(message.structure.namespaced_type.name)): return False @[for member in message.structure.members]@ @[ if len(message.structure.members) == 1 and member.name == EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@ @[ continue]@ @[ end if]@ @[ if isinstance(member.type, Array) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@ - if all(self.@(member.name) != other.@(member.name)): + if all(self.@(member.name) != other.@(member.name)): # type: ignore[arg-type] @[ else]@ if self.@(member.name) != other.@(member.name): @[ end if]@ @@ -411,7 +581,7 @@ if isinstance(type_, AbstractNestedType): return True @@classmethod - def get_fields_and_field_types(cls): + def get_fields_and_field_types(cls) -> Dict[str, str]: from copy import copy return copy(cls._fields_and_field_types) @[for member in message.structure.members]@ @@ -429,14 +599,15 @@ import builtins noqa_string = '' if member.name in dict(inspect.getmembers(builtins)).keys(): noqa_string = ' # noqa: A003' + }@ @@builtins.property@(noqa_string) - def @(member.name)(self):@(noqa_string) + def @(member.name)(self) -> @(type_annotations_getter[member.name]):@(noqa_string) """Message field '@(member.name)'.""" return self._@(member.name) @@@(member.name).setter@(noqa_string) - def @(member.name)(self, value):@(noqa_string) + def @(member.name)(self, value: @(type_annotations_setter[member.name])) -> None:@(noqa_string) if self._check_fields: @[ if isinstance(member.type, AbstractNestedType) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES]@ @[ if isinstance(member.type, Array)]@ @@ -477,8 +648,6 @@ if member.name in dict(inspect.getmembers(builtins)).keys(): from collections import UserString @[ elif isinstance(type_, AbstractGenericString) and type_.has_maximum_size()]@ from collections import UserString -@[ elif isinstance(type_, BasicType) and type_.typename == 'octet']@ - from collections.abc import ByteString @[ elif isinstance(type_, BasicType) and type_.typename in CHARACTER_TYPES]@ from collections import UserString @[ end if]@ @@ -551,7 +720,7 @@ bound = 1.7976931348623157e+308 isinstance(value, @(type_.name)), \ "The '@(member.name)' field must be a sub message of type '@(type_.name)'" @[ elif isinstance(type_, BasicType) and type_.typename == 'octet']@ - (isinstance(value, (bytes, ByteString)) and + (isinstance(value, (bytes, bytearray, memoryview)) and len(value) == 1), \ "The '@(member.name)' field must be of type 'bytes' or 'ByteString' with length 1" @[ elif isinstance(type_, BasicType) and type_.typename == 'char']@ @@ -601,7 +770,11 @@ bound = 1.7976931348623157e+308 @[ if isinstance(member.type, Array)]@ self._@(member.name) = numpy.array(value, dtype=@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'])) @[ elif isinstance(member.type, AbstractSequence)]@ +@[ if SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'] in ('f', 'd')]@ + self._@(member.name) = array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', value) # type: ignore[assignment] +@[ else]@ self._@(member.name) = array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', value) +@[ end if]@ @[ end if]@ @[ else]@ self._@(member.name) = value diff --git a/rosidl_generator_py/resource/_srv.py.em b/rosidl_generator_py/resource/_srv.py.em index 24d4548f..504ce70b 100644 --- a/rosidl_generator_py/resource/_srv.py.em +++ b/rosidl_generator_py/resource/_srv.py.em @@ -19,14 +19,13 @@ TEMPLATE( message=service.event_message, import_statements=import_statements) }@ - class Metaclass_@(service.namespaced_type.name)(type): """Metaclass of service '@(service.namespaced_type.name)'.""" _TYPE_SUPPORT = None @@classmethod - def __import_type_support__(cls): + def __import_type_support__(cls) -> None: try: from rosidl_generator_py import import_type_support module = import_type_support('@(package_name)') @@ -55,5 +54,5 @@ class @(service.namespaced_type.name)(metaclass=Metaclass_@(service.namespaced_t from @('.'.join(service.namespaced_type.namespaces)).@(module_name) import @(service.response_message.structure.namespaced_type.name) as Response from @('.'.join(service.namespaced_type.namespaces)).@(module_name) import @(service.event_message.structure.namespaced_type.name) as Event - def __init__(self): + def __init__(self) -> None: raise NotImplementedError('Service classes can not be instantiated') diff --git a/rosidl_generator_py/rosidl_generator_py/__init__.py b/rosidl_generator_py/rosidl_generator_py/__init__.py index de932669..3e0b1d41 100644 --- a/rosidl_generator_py/rosidl_generator_py/__init__.py +++ b/rosidl_generator_py/rosidl_generator_py/__init__.py @@ -21,7 +21,7 @@ try: from .generate_py_impl import generate_py - assert generate_py + generate_py __all__.append('generate_py') except ImportError: logger = logging.getLogger('rosidl_generator_py') diff --git a/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py b/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py index 0cbaaa10..980917b6 100644 --- a/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py +++ b/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py @@ -130,36 +130,42 @@ def print_warning_if_reserved_keyword(member_name, interface_type, interface_nam sorted((value, key) for (key, value) in module_names.items()): f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem} # noqa: F401\n') + f'{idl_stem} as {idl_stem} # noqa: F401\n') if subfolder == 'srv': f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_Event # noqa: F401\n') + f'{idl_stem}_Event as {idl_stem}_Event # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_Request # noqa: F401\n') + f'{idl_stem}_Request as {idl_stem}_Request # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_Response # noqa: F401\n') + f'{idl_stem}_Response as {idl_stem}_Response # noqa: F401\n') elif subfolder == 'action': f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_GetResult_Event # noqa: F401\n') + f'{idl_stem}_GetResult_Event as {idl_stem}_GetResult_Event' + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_GetResult_Request # noqa: F401\n') + f'{idl_stem}_GetResult_Request as {idl_stem}_GetResult_Request' + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_GetResult_Response # noqa: F401\n') + f'{idl_stem}_GetResult_Response as {idl_stem}_GetResult_Response' + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_SendGoal_Event # noqa: F401\n') + f'{idl_stem}_SendGoal_Event as {idl_stem}_SendGoal_Event' + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_SendGoal_Request # noqa: F401\n') + f'{idl_stem}_SendGoal_Request as {idl_stem}_SendGoal_Request' + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_SendGoal_Response # noqa: F401\n') + f'{idl_stem}_SendGoal_Response as {idl_stem}_SendGoal_Response' + ' # noqa: F401\n') # expand templates per available typesupport implementation template_dir = args['template_dir'] @@ -192,6 +198,10 @@ def print_warning_if_reserved_keyword(member_name, interface_type, interface_nam minimum_timestamp=latest_target_timestamp) generated_files.append(generated_file) + # Generate py.typed to mark the generate files as having type support as according to PEP561. + with open(os.path.join(args['output_dir'], 'py.typed'), 'w', encoding='utf-8'): + pass + return generated_files diff --git a/rosidl_generator_py/rosidl_generator_py/import_type_support_impl.py b/rosidl_generator_py/rosidl_generator_py/import_type_support_impl.py index c16b0fb2..bea8a93e 100644 --- a/rosidl_generator_py/rosidl_generator_py/import_type_support_impl.py +++ b/rosidl_generator_py/rosidl_generator_py/import_type_support_impl.py @@ -13,6 +13,7 @@ # limitations under the License. import importlib +from types import ModuleType from rpyutils import add_dll_directories_from_env @@ -20,13 +21,13 @@ class UnsupportedTypeSupport(Exception): """Raised when typesupport couldn't be imported.""" - def __init__(self, pkg_name): + def __init__(self, pkg_name: str) -> None: message = "Could not import 'rosidl_typesupport_c' for package '{0}'".format(pkg_name) super(UnsupportedTypeSupport, self).__init__(message) self.pkg_name = pkg_name -def import_type_support(pkg_name): +def import_type_support(pkg_name: str) -> ModuleType: """ Import the rosidl_typesupport_c module of a package. diff --git a/rosidl_generator_py/test/test_cli_extension.py b/rosidl_generator_py/test/test_cli_extension.py index bcc5fbb6..238dee4c 100644 --- a/rosidl_generator_py/test/test_cli_extension.py +++ b/rosidl_generator_py/test/test_cli_extension.py @@ -15,12 +15,13 @@ import pathlib from ament_index_python import get_resources +from pytest import CaptureFixture from rosidl_cli.command.generate.api import generate PACKAGE_DIR = str(pathlib.Path(__file__).parent.parent) -def test_cli_extension_for_smoke(tmp_path, capsys): +def test_cli_extension_for_smoke(tmp_path: pathlib.Path, capsys: CaptureFixture[str]) -> None: # NOTE(hidmic): pytest and empy do not play along, # the latter expects some proxy will stay in sys.stdout # and the former insists in overwriting it diff --git a/rosidl_generator_py/test/test_interfaces.py b/rosidl_generator_py/test/test_interfaces.py index 100beed9..e7ff858d 100644 --- a/rosidl_generator_py/test/test_interfaces.py +++ b/rosidl_generator_py/test/test_interfaces.py @@ -39,7 +39,7 @@ from rosidl_parser.definition import UnboundedString -def test_basic_types(): +def test_basic_types() -> None: msg = BasicTypes(check_fields=True) # types @@ -148,7 +148,7 @@ def test_basic_types(): assert math.isinf(msg.float64_value) -def test_strings(): +def test_strings() -> None: msg = Strings(check_fields=True) # types @@ -202,7 +202,7 @@ def test_strings(): setattr(msg, 'bounded_string_value_default1', 'a' * 23) -def test_wstrings(): +def test_wstrings() -> None: msg = WStrings(check_fields=True) # types @@ -216,7 +216,7 @@ def test_wstrings(): assert 'ハローワールド' == msg.wstring_value_default3 -def test_arrays_of_bounded_strings(): +def test_arrays_of_bounded_strings() -> None: msg = StringArrays(check_fields=True) array_valid_string_length = ['a' * 2, 'b' * 3, 'c' * 4] array_too_long_strings = ['a' * 2, 'b' * 3, 'c' * 6] @@ -259,16 +259,16 @@ def test_arrays_of_bounded_strings(): assert array10strings == msg.ub_string_dynamic_array_value -def test_constructor(): +def test_constructor() -> None: msg = Strings(string_value='foo', check_fields=True) assert 'foo' == msg.string_value - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Strings(unknown_field='test', check_fields=True) -def test_constants(): +def test_constants() -> None: assert Constants.BOOL_CONST is True assert bytes([50]) == Constants.BYTE_CONST assert 100 == Constants.CHAR_CONST @@ -288,7 +288,7 @@ def test_constants(): setattr(Constants, 'INT32_CONST', 42) -def test_default_values(): +def test_default_values() -> None: msg = Defaults(check_fields=True) assert msg.bool_value is True @@ -315,7 +315,7 @@ def test_default_values(): setattr(Defaults, 'INT32_VALUE__DEFAULT', 24) -def test_arrays(): +def test_arrays() -> None: msg = Arrays(check_fields=True) # types @@ -524,7 +524,7 @@ def test_arrays(): assert numpy.array_equal(arr_of_float64_with_inf, msg.float64_values) -def test_bounded_sequences(): +def test_bounded_sequences() -> None: msg = BoundedSequences(check_fields=True) # types @@ -748,7 +748,7 @@ def test_bounded_sequences(): setattr(msg, 'float64_values', [-float64_ieee_max_next, 0.0, float64_ieee_max_next]) -def test_unbounded_sequences(): +def test_unbounded_sequences() -> None: msg = UnboundedSequences(check_fields=True) # types @@ -897,7 +897,7 @@ def test_unbounded_sequences(): setattr(msg, 'float64_values', [-float64_ieee_max_next, 0.0, float64_ieee_max_next]) -def test_slot_attributes(): +def test_slot_attributes() -> None: msg = Nested(check_fields=True) assert hasattr(msg, 'get_fields_and_field_types') assert hasattr(msg, '__slots__') @@ -912,7 +912,7 @@ def test_slot_attributes(): assert expected_slot_type == nested_slot_types_dict[expected_field] -def test_string_slot_attributes(): +def test_string_slot_attributes() -> None: msg = StringArrays(check_fields=True) assert hasattr(msg, 'get_fields_and_field_types') assert hasattr(msg, '__slots__') @@ -938,7 +938,7 @@ def test_string_slot_attributes(): assert expected_slot_type == string_slot_types_dict[expected_field] -def test_modifying_slot_fields_and_types(): +def test_modifying_slot_fields_and_types() -> None: msg = StringArrays(check_fields=True) assert hasattr(msg, 'get_fields_and_field_types') string_slot_types_dict = getattr(msg, 'get_fields_and_field_types')() @@ -947,7 +947,7 @@ def test_modifying_slot_fields_and_types(): assert len(getattr(msg, 'get_fields_and_field_types')()) == string_slot_types_dict_len -def test_slot_types(): +def test_slot_types() -> None: msg = Nested(check_fields=True) assert hasattr(msg, 'SLOT_TYPES') assert hasattr(msg, '__slots__') @@ -957,7 +957,7 @@ def test_slot_types(): assert nested_slot_types[0].name == 'BasicTypes' -def test_string_slot_types(): +def test_string_slot_types() -> None: msg = StringArrays(check_fields=True) assert hasattr(msg, 'SLOT_TYPES') assert hasattr(msg, '__slots__') @@ -985,7 +985,7 @@ def test_string_slot_types(): assert string_slot_types[4].size == 3 -def test_builtin_sequence_slot_attributes(): +def test_builtin_sequence_slot_attributes() -> None: msg = BuiltinTypeSequencesIdl(check_fields=True) assert hasattr(msg, 'get_fields_and_field_types') assert hasattr(msg, '__slots__') diff --git a/rosidl_generator_py/test/test_property.py b/rosidl_generator_py/test/test_property.py index 75f3739f..6f389f0b 100644 --- a/rosidl_generator_py/test/test_property.py +++ b/rosidl_generator_py/test/test_property.py @@ -15,7 +15,7 @@ from rosidl_generator_py.msg import Property -def test_msg_property(): +def test_msg_property() -> None: msg = Property() # types