From f81e0233c0b25230b8977079b6682d3520d7ffb6 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 1 Mar 2024 18:18:08 -0500 Subject: [PATCH 01/54] explicit export and py.typed Signed-off-by: Michael Carlstrom --- .../rosidl_generator_py/generate_py_impl.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) 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..82711cf4 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,36 @@ 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 +192,11 @@ 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 From 2f993516395ba082c31c94cb9af4e25baae705d4 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 1 Mar 2024 18:26:01 -0500 Subject: [PATCH 02/54] Fix line lengths Signed-off-by: Michael Carlstrom --- .../rosidl_generator_py/generate_py_impl.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 82711cf4..0445581f 100644 --- a/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py +++ b/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py @@ -144,22 +144,28 @@ def print_warning_if_reserved_keyword(member_name, interface_type, interface_nam elif subfolder == 'action': f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_GetResult_Event as {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 as {idl_stem}_GetResult_Request # noqa: F401\n') + f'{idl_stem}_GetResult_Request as {idl_stem}_GetResult_Request' + 'W # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' - f'{idl_stem}_GetResult_Response as {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 as {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 as {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 as {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'] From 05761d84245ad2aa8b3f1fd574158846239d4001 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 1 Mar 2024 18:31:46 -0500 Subject: [PATCH 03/54] Fix hanging indent Signed-off-by: Michael Carlstrom --- .../rosidl_generator_py/generate_py_impl.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 0445581f..980917b6 100644 --- a/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py +++ b/rosidl_generator_py/rosidl_generator_py/generate_py_impl.py @@ -145,15 +145,15 @@ def print_warning_if_reserved_keyword(member_name, interface_type, interface_nam f.write( f'from {package_name}.{subfolder}.{module_name} import ' f'{idl_stem}_GetResult_Event as {idl_stem}_GetResult_Event' - ' # noqa: F401\n') + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' f'{idl_stem}_GetResult_Request as {idl_stem}_GetResult_Request' - 'W # noqa: F401\n') + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' f'{idl_stem}_GetResult_Response as {idl_stem}_GetResult_Response' - ' # noqa: F401\n') + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' f'{idl_stem}_SendGoal_Event as {idl_stem}_SendGoal_Event' @@ -161,11 +161,11 @@ def print_warning_if_reserved_keyword(member_name, interface_type, interface_nam f.write( f'from {package_name}.{subfolder}.{module_name} import ' f'{idl_stem}_SendGoal_Request as {idl_stem}_SendGoal_Request' - ' # noqa: F401\n') + ' # noqa: F401\n') f.write( f'from {package_name}.{subfolder}.{module_name} import ' f'{idl_stem}_SendGoal_Response as {idl_stem}_SendGoal_Response' - ' # noqa: F401\n') + ' # noqa: F401\n') # expand templates per available typesupport implementation template_dir = args['template_dir'] @@ -198,9 +198,8 @@ 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'): + # 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 From 9807b07512626e8b716651ce6050904a9ccebb6b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 15 Mar 2024 02:34:17 -0400 Subject: [PATCH 04/54] Improve typing Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 67 ++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 8c21e552..f57de6e0 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -293,7 +293,25 @@ if isinstance(type_, AbstractNestedType): @[end for]@ ) - def __init__(self, **kwargs): +@[for member in message.structure.members]@ +@{ +type_ = member.type +if isinstance(type_, AbstractNestedType): + type_ = type_.value_type +}@ +@[ if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence)]@ +@[ if ( + type_.name.endswith(ACTION_GOAL_SUFFIX) or + type_.name.endswith(ACTION_RESULT_SUFFIX) or + type_.name.endswith(ACTION_FEEDBACK_SUFFIX))]@ + from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name) +@[ else]@ + from @('.'.join(type_.namespaces)) import @(type_.name) +@[ end if]@ +@[ end if]@ +@[end for]@ + + def __init__(self, **kwargs) -> None: if 'check_fields' in kwargs: self._check_fields = kwargs['check_fields'] else: @@ -367,7 +385,7 @@ if isinstance(type_, AbstractNestedType): @[ end if]@ @[end for]@ - def __repr__(self): + def __repr__(self) -> str: typename = self.__class__.__module__.split('.') typename.pop() typename.append(self.__class__.__name__) @@ -394,7 +412,7 @@ if isinstance(type_, AbstractNestedType): args.append(s + '=' + fieldstr) return '%s(%s)' % ('.'.join(typename), ', '.join(args)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if not isinstance(other, self.__class__): return False @[for member in message.structure.members]@ @@ -410,8 +428,10 @@ if isinstance(type_, AbstractNestedType): @[end for]@ return True + from typing import Dict + @@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 +449,49 @@ import builtins noqa_string = '' if member.name in dict(inspect.getmembers(builtins)).keys(): noqa_string = ' # noqa: A003' + +import numpy +from rosidl_generator_py.generate_py_impl import get_python_type + +python_type = get_python_type(type_) + +type_annotation = '' +type_imports = None + +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): + type_annotation = f'numpy.ndarray[{python_type}], ' + elif isinstance(member.type, AbstractSequence): + type_annotation = f'array.array[{python_type}], ' + +if isinstance(member.type, AbstractNestedType): + type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' + f'Set[{python_type}], UserList[{python_type}]]') + + type_imports = 'from typing import Union\n from collections.abc import Sequence, Set\n from collections import UserList\n' +elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size(): + type_annotation = 'Union[str, UserString]' + type_imports = 'from typing import Union\n from collections import UserString' +elif isinstance(type_, BasicType) and type_.typename == 'char': + type_annotation = 'Union[str, UserString]' + type_imports = 'from typing import Union\n from collections import UserString' +elif isinstance(type_, BasicType) and type_.typename == 'octet': + type_annotation = 'Union[bytes, ByteString]' + type_imports = 'from typing import Union\n from collections.abc import ByteString' +else: + type_annotation = python_type + }@ +@[ if type_imports]@ + @(type_imports) +@[ end if]@ @@builtins.property@(noqa_string) - def @(member.name)(self):@(noqa_string) + def @(member.name)(self) -> @(type_annotation):@(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_annotation)) -> 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)]@ From 288ba5ddf339a75ae4f6bd65a3dcdb6368f1cd98 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 15 Mar 2024 03:06:56 -0400 Subject: [PATCH 05/54] Fix subscription error Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index f57de6e0..eedde27d 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -481,6 +481,9 @@ elif isinstance(type_, BasicType) and type_.typename == 'octet': else: type_annotation = python_type + +type_annotation = f'\'{type_annotation}\'' + }@ @[ if type_imports]@ @(type_imports) From 9aad27a89e1eb18341f183119b15e06b0e0fcfa0 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 15 Mar 2024 03:49:20 -0400 Subject: [PATCH 06/54] Tests complete Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 1 + rosidl_generator_py/resource/_msg.py.em | 48 ++++++++++++------------- rosidl_generator_py/resource/_srv.py.em | 2 +- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 3f49224a..b6f90fc1 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -6,6 +6,7 @@ # 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 os import getenv +from typing import Dict, TYPE_CHECKING 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 eedde27d..ae4d2c62 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -293,24 +293,6 @@ if isinstance(type_, AbstractNestedType): @[end for]@ ) -@[for member in message.structure.members]@ -@{ -type_ = member.type -if isinstance(type_, AbstractNestedType): - type_ = type_.value_type -}@ -@[ if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence)]@ -@[ if ( - type_.name.endswith(ACTION_GOAL_SUFFIX) or - type_.name.endswith(ACTION_RESULT_SUFFIX) or - type_.name.endswith(ACTION_FEEDBACK_SUFFIX))]@ - from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name) -@[ else]@ - from @('.'.join(type_.namespaces)) import @(type_.name) -@[ end if]@ -@[ end if]@ -@[end for]@ - def __init__(self, **kwargs) -> None: if 'check_fields' in kwargs: self._check_fields = kwargs['check_fields'] @@ -428,8 +410,6 @@ if isinstance(type_, AbstractNestedType): @[end for]@ return True - from typing import Dict - @@classmethod def get_fields_and_field_types(cls) -> Dict[str, str]: from copy import copy @@ -456,7 +436,8 @@ from rosidl_generator_py.generate_py_impl import get_python_type python_type = get_python_type(type_) type_annotation = '' -type_imports = None +type_imports = '' + 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): @@ -468,25 +449,40 @@ if isinstance(member.type, AbstractNestedType): type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' f'Set[{python_type}], UserList[{python_type}]]') - type_imports = 'from typing import Union\n from collections.abc import Sequence, Set\n from collections import UserList\n' + type_imports = 'from typing import Union\n from collections.abc import Sequence, Set\n from collections import UserList\n' elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size(): type_annotation = 'Union[str, UserString]' - type_imports = 'from typing import Union\n from collections import UserString' + type_imports = 'from typing import Union\n from collections import UserString\n' elif isinstance(type_, BasicType) and type_.typename == 'char': type_annotation = 'Union[str, UserString]' - type_imports = 'from typing import Union\n from collections import UserString' + type_imports = 'from typing import Union\n from collections import UserString\n' elif isinstance(type_, BasicType) and type_.typename == 'octet': type_annotation = 'Union[bytes, ByteString]' - type_imports = 'from typing import Union\n from collections.abc import ByteString' + type_imports = 'from typing import Union\n from collections.abc import ByteString\n' else: type_annotation = python_type +if type_imports != '': + type_imports = f'{type_imports} ' + +if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence): + 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 = f'{type_imports}from {joined_type_namespaces}._{convert_camel_case_to_lower_case_underscore(type_name_rsplit[0])} import {type_.name}\n' + else: + type_imports = f'{type_imports}from {joined_type_namespaces} import {type_.name}\n' + +if type_imports == '': + type_imports = None type_annotation = f'\'{type_annotation}\'' }@ @[ if type_imports]@ - @(type_imports) + # Done to avoid circular imports for type checking. + if TYPE_CHECKING: + @(type_imports) @[ end if]@ @@builtins.property@(noqa_string) def @(member.name)(self) -> @(type_annotation):@(noqa_string) diff --git a/rosidl_generator_py/resource/_srv.py.em b/rosidl_generator_py/resource/_srv.py.em index 24d4548f..86d71a54 100644 --- a/rosidl_generator_py/resource/_srv.py.em +++ b/rosidl_generator_py/resource/_srv.py.em @@ -55,5 +55,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') From 2822852d0934a2b741b3d593d9291a943041a32a Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 15 Mar 2024 03:56:30 -0400 Subject: [PATCH 07/54] Fewer flake8 Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index ae4d2c62..992e1821 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -462,10 +462,11 @@ elif isinstance(type_, BasicType) and type_.typename == 'octet': else: type_annotation = python_type -if type_imports != '': - type_imports = f'{type_imports} ' - if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence): + + if type_imports != '': + type_imports = f'{type_imports} ' + 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) From 118b9ef26ab07346bfcfda2020b2aca372eee11b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 15 Mar 2024 13:36:53 -0400 Subject: [PATCH 08/54] Fix typing import error Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_action.py.em | 2 +- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 38 ++++++++++++++++++---- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/rosidl_generator_py/resource/_action.py.em b/rosidl_generator_py/resource/_action.py.em index 77fbf875..d591f2fb 100644 --- a/rosidl_generator_py/resource/_action.py.em +++ b/rosidl_generator_py/resource/_action.py.em @@ -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 b6f90fc1..ef877643 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -6,7 +6,7 @@ # 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 os import getenv -from typing import Dict, TYPE_CHECKING +from typing import Dict 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 992e1821..348c9e39 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -29,6 +29,30 @@ from rosidl_parser.definition import SIGNED_INTEGER_TYPES from rosidl_parser.definition import UnboundedSequence from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ +@{ +import_type_checking = False +for member in message.structure.members: + if isinstance(member.type, AbstractNestedType): + import_type_checking = True + break + elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size(): + import_type_checking = True + break + elif isinstance(member.type, BasicType) and member.type.typename == 'char': + import_type_checking = True + break + elif isinstance(member.type, BasicType) and member.type.typename == 'octet': + import_type_checking = True + break + + if isinstance(member.type, NamespacedType): + import_type_checking = True + break +}@ +@[if import_type_checking]@ + +from typing import TYPE_CHECKING # noqa: E402, I100 +@[end if]@ @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @# Collect necessary import statements for all members @{ @@ -449,20 +473,20 @@ if isinstance(member.type, AbstractNestedType): type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' f'Set[{python_type}], UserList[{python_type}]]') - type_imports = 'from typing import Union\n from collections.abc import Sequence, Set\n from collections import UserList\n' + type_imports = 'from typing import Union # noqa: F811\n from collections.abc import Sequence, Set # noqa: F811\n from collections import UserList # noqa: F811\n' elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size(): type_annotation = 'Union[str, UserString]' - type_imports = 'from typing import Union\n from collections import UserString\n' + type_imports = 'from typing import Union # noqa: F811\n from collections import UserString # noqa: F811\n' elif isinstance(type_, BasicType) and type_.typename == 'char': type_annotation = 'Union[str, UserString]' - type_imports = 'from typing import Union\n from collections import UserString\n' + type_imports = 'from typing import Union # noqa: F811\n from collections import UserString # noqa: F811\n' elif isinstance(type_, BasicType) and type_.typename == 'octet': type_annotation = 'Union[bytes, ByteString]' - type_imports = 'from typing import Union\n from collections.abc import ByteString\n' + type_imports = 'from typing import Union # noqa: F811\n from collections.abc import ByteString # noqa: F811\n' else: type_annotation = python_type -if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence): +if isinstance(type_, NamespacedType): if type_imports != '': type_imports = f'{type_imports} ' @@ -470,9 +494,9 @@ if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSeq 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 = f'{type_imports}from {joined_type_namespaces}._{convert_camel_case_to_lower_case_underscore(type_name_rsplit[0])} import {type_.name}\n' + type_imports = f'{type_imports}from {joined_type_namespaces}._{convert_camel_case_to_lower_case_underscore(type_name_rsplit[0])} import {type_.name} # noqa: F811\n' else: - type_imports = f'{type_imports}from {joined_type_namespaces} import {type_.name}\n' + type_imports = f'{type_imports}from {joined_type_namespaces} import {type_.name} # noqa: F811\n' if type_imports == '': type_imports = None From af91451115803cb60e068f4524538651371e5689 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom <36806982+InvincibleRMC@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:34:24 -0700 Subject: [PATCH 09/54] Update _msg.py.em Signed-off-by: Michael Carlstrom <36806982+InvincibleRMC@users.noreply.github.com> --- rosidl_generator_py/resource/_msg.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 348c9e39..b839fbe4 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -317,7 +317,7 @@ if isinstance(type_, AbstractNestedType): @[end for]@ ) - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: Dict[str, object]) -> None: if 'check_fields' in kwargs: self._check_fields = kwargs['check_fields'] else: From 153a6d7905288812869cbc167929d50fc172a317 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 17 Mar 2024 15:32:01 -0400 Subject: [PATCH 10/54] Constructor typing refactor Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 204 ++++++++++---------- rosidl_generator_py/test/test_interfaces.py | 2 +- 3 files changed, 104 insertions(+), 104 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index ef877643..90a65078 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -6,7 +6,7 @@ # 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 os import getenv -from typing import Dict +from typing import Dict, Optional 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 b839fbe4..1c266642 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -31,27 +31,74 @@ from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ @{ import_type_checking = False +type_annotations = {} +type_imports = set() + for member in message.structure.members: + type_ = member.type + + if isinstance(type_, AbstractNestedType): + type_ = type_.value_type + + python_type = get_python_type(type_) + + type_annotation = '' + + if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: + if isinstance(member.type, Array): + type_annotation = f'numpy.ndarray[{python_type}], ' + elif isinstance(member.type, AbstractSequence): + type_annotation = f'array.array[{python_type}], ' + if isinstance(member.type, AbstractNestedType): - import_type_checking = True - break + 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(): - import_type_checking = True - break - elif isinstance(member.type, BasicType) and member.type.typename == 'char': - import_type_checking = True - break - elif isinstance(member.type, BasicType) and member.type.typename == 'octet': - import_type_checking = True - break - - if isinstance(member.type, NamespacedType): - import_type_checking = True - break + 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[member.name] = f'\'{type_annotation}\'' + +if len(type_imports) > 0: + import_type_checking = True }@ @[if import_type_checking]@ from typing import TYPE_CHECKING # noqa: E402, I100 + +if TYPE_CHECKING: +@[ for type_import in type_imports]@ + @(type_import) +@[ end for] @[end if]@ @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @# Collect necessary import statements for all members @@ -90,8 +137,6 @@ for member in message.structure.members: @ @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @[if imports]@ - - # Import statements for member types @[ for import_statement, member_names in sorted(imports.items())]@ @@ -316,16 +361,30 @@ 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: Dict[str, object]) -> None: - if 'check_fields' in kwargs: - self._check_fields = kwargs['check_fields'] + def __init__(self, +@[for member in message.structure.members]@ +@[ if member.name in BUILTINS]@ + @(member.name): Optional[@(type_annotations[member.name])] = None, # noqa: E501, A002 +@[ else]@ + @(member.name): Optional[@(type_annotations[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]@ @@ -335,9 +394,13 @@ type_ = member.type if isinstance(type_, AbstractNestedType): type_ = type_.value_type }@ +@[ if not (not member.has_annotation('default') and isinstance(member.type, Array) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES)]@ + if @(member.name): + self.@(member.name) = @(member.name) + else: +@[ end if]@ @[ if member.has_annotation('default')]@ - self.@(member.name) = kwargs.get( - '@(member.name)', @(message.structure.namespaced_type.name).@(member.name.upper())__DEFAULT) + self.@(member.name) = @(message.structure.namespaced_type.name).@(member.name.upper())__DEFAULT @[ else]@ @[ if isinstance(type_, NamespacedType) and not isinstance(member.type, AbstractSequence)]@ @[ if ( @@ -345,48 +408,39 @@ if isinstance(type_, AbstractNestedType): type_.name.endswith(ACTION_RESULT_SUFFIX) or type_.name.endswith(ACTION_FEEDBACK_SUFFIX) )]@ - from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name) + from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name) @[ else]@ - from @('.'.join(type_.namespaces)) import @(type_.name) + from @('.'.join(type_.namespaces)) import @(type_.name) @[ end if]@ @[ 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) = [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) = [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) = [@(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) = array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) @[ else]@ - self.@(member.name) = kwargs.get('@(member.name)', []) + self.@(member.name) = [] @[ end if]@ @[ elif isinstance(type_, BasicType) and type_.typename == 'octet']@ - self.@(member.name) = kwargs.get('@(member.name)', bytes([0])) + self.@(member.name) = bytes([0]) @[ elif isinstance(type_, BasicType) and type_.typename in CHARACTER_TYPES]@ - self.@(member.name) = kwargs.get('@(member.name)', chr(0)) + self.@(member.name) = chr(0) @[ else]@ - self.@(member.name) = kwargs.get('@(member.name)', @(get_python_type(type_))()) + self.@(member.name) = @(get_python_type(type_))() @[ end if]@ @[ end if]@ @[end for]@ @@ -454,68 +508,14 @@ noqa_string = '' if member.name in dict(inspect.getmembers(builtins)).keys(): noqa_string = ' # noqa: A003' -import numpy -from rosidl_generator_py.generate_py_impl import get_python_type - -python_type = get_python_type(type_) - -type_annotation = '' -type_imports = '' - - -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): - type_annotation = f'numpy.ndarray[{python_type}], ' - elif isinstance(member.type, AbstractSequence): - type_annotation = f'array.array[{python_type}], ' - -if isinstance(member.type, AbstractNestedType): - type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' - f'Set[{python_type}], UserList[{python_type}]]') - - type_imports = 'from typing import Union # noqa: F811\n from collections.abc import Sequence, Set # noqa: F811\n from collections import UserList # noqa: F811\n' -elif isinstance(member.type, AbstractGenericString) and member.type.has_maximum_size(): - type_annotation = 'Union[str, UserString]' - type_imports = 'from typing import Union # noqa: F811\n from collections import UserString # noqa: F811\n' -elif isinstance(type_, BasicType) and type_.typename == 'char': - type_annotation = 'Union[str, UserString]' - type_imports = 'from typing import Union # noqa: F811\n from collections import UserString # noqa: F811\n' -elif isinstance(type_, BasicType) and type_.typename == 'octet': - type_annotation = 'Union[bytes, ByteString]' - type_imports = 'from typing import Union # noqa: F811\n from collections.abc import ByteString # noqa: F811\n' -else: - type_annotation = python_type - -if isinstance(type_, NamespacedType): - - if type_imports != '': - type_imports = f'{type_imports} ' - - 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 = f'{type_imports}from {joined_type_namespaces}._{convert_camel_case_to_lower_case_underscore(type_name_rsplit[0])} import {type_.name} # noqa: F811\n' - else: - type_imports = f'{type_imports}from {joined_type_namespaces} import {type_.name} # noqa: F811\n' - -if type_imports == '': - type_imports = None - -type_annotation = f'\'{type_annotation}\'' - }@ -@[ if type_imports]@ - # Done to avoid circular imports for type checking. - if TYPE_CHECKING: - @(type_imports) -@[ end if]@ @@builtins.property@(noqa_string) - def @(member.name)(self) -> @(type_annotation):@(noqa_string) + def @(member.name)(self) -> @(type_annotations[member.name]):@(noqa_string) """Message field '@(member.name)'.""" return self._@(member.name) @@@(member.name).setter@(noqa_string) - def @(member.name)(self, value: @(type_annotation)) -> None:@(noqa_string) + def @(member.name)(self, value: @(type_annotations[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)]@ diff --git a/rosidl_generator_py/test/test_interfaces.py b/rosidl_generator_py/test/test_interfaces.py index 100beed9..ba6cadd8 100644 --- a/rosidl_generator_py/test/test_interfaces.py +++ b/rosidl_generator_py/test/test_interfaces.py @@ -264,7 +264,7 @@ def test_constructor(): assert 'foo' == msg.string_value - with pytest.raises(AssertionError): + with pytest.raises(TypeError): Strings(unknown_field='test', check_fields=True) From 6a147a73634fb9e54bf280c3ac707d6bd5e21c01 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom <36806982+InvincibleRMC@users.noreply.github.com> Date: Sun, 17 Mar 2024 15:54:27 -0400 Subject: [PATCH 11/54] Update _msg.py.em Signed-off-by: Michael Carlstrom <36806982+InvincibleRMC@users.noreply.github.com> --- rosidl_generator_py/resource/_msg.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 1c266642..8d2dde50 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -173,7 +173,7 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): } @@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)') From 946381a73ee852b46d583abd5d9491550f0fc873 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 17 Mar 2024 23:14:17 -0400 Subject: [PATCH 12/54] remove structure_needs_at_least_one_member Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 8d2dde50..873d0979 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -374,6 +374,9 @@ BUILTINS = [ 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[member.name])] = None, # noqa: E501, A002 @[ else]@ From 956fe0ae02911c038206736932e7315ed3763ebe Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 17 Mar 2024 23:36:07 -0400 Subject: [PATCH 13/54] Add actual numpy type annotations Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 873d0979..ac628cbf 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -46,7 +46,9 @@ for member in message.structure.members: if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: if isinstance(member.type, Array): - type_annotation = f'numpy.ndarray[{python_type}], ' + 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): type_annotation = f'array.array[{python_type}], ' From ae98901b167666d237f32d700b7aba16c95ca703 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Mon, 18 Mar 2024 16:30:55 -0400 Subject: [PATCH 14/54] Add None Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_action.py.em | 2 +- rosidl_generator_py/resource/_srv.py.em | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rosidl_generator_py/resource/_action.py.em b/rosidl_generator_py/resource/_action.py.em index d591f2fb..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)') diff --git a/rosidl_generator_py/resource/_srv.py.em b/rosidl_generator_py/resource/_srv.py.em index 86d71a54..ae622b3b 100644 --- a/rosidl_generator_py/resource/_srv.py.em +++ b/rosidl_generator_py/resource/_srv.py.em @@ -26,7 +26,7 @@ class Metaclass_@(service.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)') From 8d06b8c90f95001172ddd27d500e700dafaf59f2 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Mon, 18 Mar 2024 17:50:47 -0400 Subject: [PATCH 15/54] Add types to __prepare__ args Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 90a65078..1351ad2b 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -6,7 +6,7 @@ # 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 os import getenv -from typing import Dict, Optional +from typing import Any, Dict, Optional, Tuple 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 ac628cbf..753e8038 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -90,18 +90,15 @@ for member in message.structure.members: type_annotations[member.name] = f'\'{type_annotation}\'' -if len(type_imports) > 0: - import_type_checking = True +type_imports.add('from typing import Literal') }@ -@[if import_type_checking]@ from typing import TYPE_CHECKING # noqa: E402, I100 if TYPE_CHECKING: -@[ for type_import in type_imports]@ +@[for type_import in type_imports]@ @(type_import) -@[ end for] -@[end if]@ +@[end for] @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @# Collect necessary import statements for all members @{ @@ -227,7 +224,7 @@ for member in message.structure.members: @[end for]@ @@classmethod - def __prepare__(cls, name, bases, **kwargs): + def __prepare__(cls, name: 'Literal["@(message.structure.namespaced_type.name)"]', bases: Tuple[()], **kwargs: Any): # 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 From ad14966eda65d38b5a0bf1beb413cde6d518093b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Mon, 18 Mar 2024 23:46:59 -0400 Subject: [PATCH 16/54] Default type annotations Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 4 +- rosidl_generator_py/resource/_msg.py.em | 74 ++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 1351ad2b..daeae10a 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -5,8 +5,10 @@ # 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 __future__ import annotations + from os import getenv -from typing import Any, Dict, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple, 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 753e8038..d09ce199 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -88,12 +88,70 @@ for member in message.structure.members: type_imports.add(f'from {joined_type_namespaces} import {type_.name}') - type_annotations[member.name] = f'\'{type_annotation}\'' + type_annotations[member.name] = type_annotation type_imports.add('from typing import Literal') + +constant_type_annotations = {} + +for member in message.structure.members: + + if member.has_annotation('default'): + constant = member + 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): + type_annotation = f'array.array[{python_type}]' + 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): + type_annotation = 'float' + else: + value = constant.get_annotation_value('default')['value'] + if isinstance(value, str): + value = value.replace("\"", "\\\"") + value = value.replace("\'", "\\\'") + if '\'' in value: + value = f'\"{value}\"' + else: + value = f'\'{value}\'' + + type_annotation = f'Literal[{value}]' + elif isinstance(value, float): + type_annotation = 'float' + elif type_.typename == 'octet': + const_value = constant_value_to_py(type_, value) + type_annotation = f'Literal[{const_value}]' + else: + type_annotation = f'Literal[{value}]' + + constant_type_annotations[constant.name] = type_annotation }@ from typing import TYPE_CHECKING # noqa: E402, I100 +import sys # noqa: E402, I100 + +if sys.version_info >= (3, 12): + from typing import override +else: + # Definition taking from typing_extension. + def override(func: Callable[..., Any]) -> Callable[..., Any]: + return func if TYPE_CHECKING: @[for type_import in type_imports]@ @@ -165,6 +223,15 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): _DESTROY_ROS_MESSAGE = None _TYPE_SUPPORT = None + class @(message.structure.namespaced_type.name)Constants(TypedDict): +@[if not constant_type_annotations]@ + pass +@[else]@ +@[ for name, type in constant_type_annotations.items()]@ + @(name.upper())__DEFAULT: @(type) +@[ end for]@ +@[end if]@ + __constants = { @[for constant in message.constants]@ '@(constant.name)': @constant_value_to_py(constant.type, constant.value), @@ -223,8 +290,9 @@ for member in message.structure.members: @(typename[-1]).__class__.__import_type_support__() @[end for]@ + @@override @@classmethod - def __prepare__(cls, name: 'Literal["@(message.structure.namespaced_type.name)"]', bases: Tuple[()], **kwargs: Any): + def __prepare__(cls, name: 'Literal["@(message.structure.namespaced_type.name)"]', bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Constants: # 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 @@ -249,7 +317,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) -> @(constant_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]@ From 48def6b3f32e5bdbd99a3bb5fef1298694a078e4 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 09:00:53 -0400 Subject: [PATCH 17/54] Constant type annotations Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 74 ++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index d09ce199..a814fece 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -92,7 +92,54 @@ for member in message.structure.members: type_imports.add('from typing import Literal') -constant_type_annotations = {} +custom_type_annotations = {} + +for constant in message.constants: + 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): + type_annotation = f'array.array[{python_type}]' + 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): + type_annotation = 'float' + else: + value = constant.value + if isinstance(value, str): + value = value.replace("\"", "\\\"") + value = value.replace("\'", "\\\'") + if '\'' in value: + value = f'\"{value}\"' + else: + value = f'\'{value}\'' + + type_annotation = f'Literal[{value}]' + elif isinstance(value, float): + type_annotation = 'float' + elif type_.typename == 'octet': + const_value = constant_value_to_py(type_, value) + type_annotation = f'Literal[{const_value}]' + else: + type_annotation = f'Literal[{value}]' + + custom_type_annotations[constant.name] = type_annotation + +default_type_annotations = {} for member in message.structure.members: @@ -140,7 +187,7 @@ for member in message.structure.members: else: type_annotation = f'Literal[{value}]' - constant_type_annotations[constant.name] = type_annotation + default_type_annotations[constant.name] = type_annotation }@ from typing import TYPE_CHECKING # noqa: E402, I100 @@ -224,20 +271,29 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): _TYPE_SUPPORT = None class @(message.structure.namespaced_type.name)Constants(TypedDict): -@[if not constant_type_annotations]@ +@[if not custom_type_annotations]@ pass @[else]@ -@[ for name, type in constant_type_annotations.items()]@ - @(name.upper())__DEFAULT: @(type) +@[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) -> None: try: @@ -292,7 +348,7 @@ for member in message.structure.members: @@override @@classmethod - def __prepare__(cls, name: 'Literal["@(message.structure.namespaced_type.name)"]', bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Constants: + def __prepare__(cls, name: 'Literal["@(message.structure.namespaced_type.name)"]', bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: # 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 @@ -309,7 +365,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]@ @@ -317,7 +373,7 @@ for member in message.structure.members: @[ if member.has_annotation('default')]@ @@property - def @(member.name.upper())__DEFAULT(cls) -> @(constant_type_annotations[member.name]): + 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]@ From 8458b389bc1b57b5e7620d7aaadeacedc4278001 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 09:16:39 -0400 Subject: [PATCH 18/54] Type constants Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index a814fece..b8fcfba3 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -342,8 +342,8 @@ 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]@ @@override @@ -404,7 +404,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]@ From 9f80bcc325060ffd72fbb13756ebbe86665db133 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 09:21:55 -0400 Subject: [PATCH 19/54] SLOT_TYPES type Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index b8fcfba3..ddc74dc9 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -449,7 +449,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]@ From 4b2f72c3ebc90d816b321aebc45f7ef56c3da673 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 10:47:46 -0400 Subject: [PATCH 20/54] small refactor Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 1 + rosidl_generator_py/resource/_msg.py.em | 61 +++++-------------------- 2 files changed, 13 insertions(+), 49 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index daeae10a..6a5e1964 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -8,6 +8,7 @@ from __future__ import annotations from os import getenv +import sys from typing import Any, Callable, Dict, Optional, Tuple, 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 ddc74dc9..6d59d9c9 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -92,9 +92,10 @@ for member in message.structure.members: type_imports.add('from typing import Literal') -custom_type_annotations = {} +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 -for constant in message.constants: type_ = constant.type if isinstance(type_, AbstractNestedType): @@ -119,7 +120,6 @@ for constant in message.constants: elif isinstance(type_, float): type_annotation = 'float' else: - value = constant.value if isinstance(value, str): value = value.replace("\"", "\\\"") value = value.replace("\'", "\\\'") @@ -137,61 +137,24 @@ for constant in message.constants: else: type_annotation = f'Literal[{value}]' - custom_type_annotations[constant.name] = 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 - 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): - type_annotation = f'array.array[{python_type}]' - 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): - type_annotation = 'float' - else: - value = constant.get_annotation_value('default')['value'] - if isinstance(value, str): - value = value.replace("\"", "\\\"") - value = value.replace("\'", "\\\'") - if '\'' in value: - value = f'\"{value}\"' - else: - value = f'\'{value}\'' - - type_annotation = f'Literal[{value}]' - elif isinstance(value, float): - type_annotation = 'float' - elif type_.typename == 'octet': - const_value = constant_value_to_py(type_, value) - type_annotation = f'Literal[{const_value}]' - else: - type_annotation = f'Literal[{value}]' - - default_type_annotations[constant.name] = type_annotation + value = constant.get_annotation_value('default')['value'] + default_type_annotations[constant.name] = get_type_annotation_constant_default(constant, value, type_imports) }@ from typing import TYPE_CHECKING # noqa: E402, I100 -import sys # noqa: E402, I100 if sys.version_info >= (3, 12): from typing import override From a412da2e57dbe12f9359623dc41e322c290583c4 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 11:24:30 -0400 Subject: [PATCH 21/54] Add Protocol Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 30 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 6d59d9c9..7f4eb2dd 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -163,7 +163,22 @@ else: def override(func: Callable[..., Any]) -> Callable[..., Any]: return func +@{ +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 + + from typing import Protocol + class TypeSupportProtocol(Protocol): + create_ros_message_msg__@(suffix): PyCapsule + convert_from_py_msg__@(suffix): PyCapsule + convert_to_py_msg__@(suffix): PyCapsule + type_support_msg__@(suffix): PyCapsule + destroy_ros_message_msg__@(suffix): PyCapsule + @[for type_import in type_imports]@ @(type_import) @[end for] @@ -227,11 +242,11 @@ 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: Optional[PyCapsule] = None + _CONVERT_FROM_PY: Optional[PyCapsule] = None + _CONVERT_TO_PY: Optional[PyCapsule] = None + _DESTROY_ROS_MESSAGE: Optional[PyCapsule] = None + _TYPE_SUPPORT: Optional[PyCapsule] = None class @(message.structure.namespaced_type.name)Constants(TypedDict): @[if not custom_type_annotations]@ @@ -261,7 +276,7 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): def __import_type_support__(cls) -> None: try: from rosidl_generator_py import import_type_support - module = import_type_support('@(package_name)') + module: TypeSupportProtocol = import_type_support('@(package_name)') except ImportError: import logging import traceback @@ -271,9 +286,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) From 86d8a971e3aa332d61ef53a3d24c130624da81fc Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 11:55:00 -0400 Subject: [PATCH 22/54] Blank lines for flake8 Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 7f4eb2dd..f2e8dbf8 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -168,10 +168,12 @@ suffix = '__'.join(message.structure.namespaced_type.namespaces[1:]) + '__' + co }@ if TYPE_CHECKING: from ctypes import Structure + class PyCapsule(Structure): pass # don't need to define the full structure from typing import Protocol + class TypeSupportProtocol(Protocol): create_ros_message_msg__@(suffix): PyCapsule convert_from_py_msg__@(suffix): PyCapsule From 5d839f4481e03320cc1517f774ea1014ced40739 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 13:38:02 -0400 Subject: [PATCH 23/54] Remove Literal for better 3.6 support Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 -- rosidl_generator_py/resource/_msg.py.em | 38 ++++++++++++++----------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 6a5e1964..daa3ad73 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -5,8 +5,6 @@ # 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 __future__ import annotations - from os import getenv import sys from typing import Any, Callable, Dict, Optional, Tuple, TypedDict diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index f2e8dbf8..ca8713ca 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -50,7 +50,9 @@ for member in message.structure.members: type_annotation = f'NDArray[{dtype}], ' type_imports.add('from numpy.typing import NDArray') elif isinstance(member.type, AbstractSequence): - type_annotation = f'array.array[{python_type}], ' + # 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_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' @@ -88,7 +90,7 @@ for member in message.structure.members: type_imports.add(f'from {joined_type_namespaces} import {type_.name}') - type_annotations[member.name] = type_annotation + type_annotations[member.name] = f'\'{type_annotation}\'' type_imports.add('from typing import Literal') @@ -111,7 +113,9 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: type_annotation = f'NDArray[{dtype}]' type_imports.add('from numpy.typing import NDArray') elif isinstance(constant.type, AbstractSequence): - type_annotation = f'array.array[{python_type}]' + # 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}]' @@ -121,22 +125,22 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: type_annotation = 'float' else: if isinstance(value, str): - value = value.replace("\"", "\\\"") - value = value.replace("\'", "\\\'") - if '\'' in value: - value = f'\"{value}\"' - else: - value = f'\'{value}\'' + # Literal type annotations cannot handle strings with ' or " until python3.7 + # Using from __future__ import annotations or Just importing Literal in python3.8 + if '\'' in value or '\"' in value: + return 'str' - type_annotation = f'Literal[{value}]' + type_annotation = f'Literal[\"{value}\"]' elif isinstance(value, float): type_annotation = 'float' elif type_.typename == 'octet': - const_value = constant_value_to_py(type_, value) - type_annotation = f'Literal[{const_value}]' + # Literal type annotations cannot handle bytes with ' or " until python3.7 + # Using from __future__ import annotations or Just importing Literal in python3.8 + type_annotation = 'bytes' else: type_annotation = f'Literal[{value}]' + type_annotation = f'\'{type_annotation}\'' return type_annotation custom_type_annotations = {} @@ -244,11 +248,11 @@ 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: Optional[PyCapsule] = None - _CONVERT_FROM_PY: Optional[PyCapsule] = None - _CONVERT_TO_PY: Optional[PyCapsule] = None - _DESTROY_ROS_MESSAGE: Optional[PyCapsule] = None - _TYPE_SUPPORT: Optional[PyCapsule] = None + _CREATE_ROS_MESSAGE: Optional['PyCapsule'] = None + _CONVERT_FROM_PY: Optional['PyCapsule'] = None + _CONVERT_TO_PY: Optional['PyCapsule'] = None + _DESTROY_ROS_MESSAGE: Optional['PyCapsule'] = None + _TYPE_SUPPORT: Optional['PyCapsule'] = None class @(message.structure.namespaced_type.name)Constants(TypedDict): @[if not custom_type_annotations]@ From 1aef88a635f569ac8e357a764c0b9ba611829b36 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 18:30:07 -0400 Subject: [PATCH 24/54] Better return types Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index ca8713ca..9ec1af19 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -31,7 +31,8 @@ from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ @{ import_type_checking = False -type_annotations = {} +type_annotations_getter = {} +type_annotations_setter = {} type_imports = set() for member in message.structure.members: @@ -43,18 +44,21 @@ for member in message.structure.members: python_type = get_python_type(type_) type_annotation = '' + type_annotation_getter = '' if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: if isinstance(member.type, Array): dtype = SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'] - type_annotation = f'NDArray[{dtype}], ' + type_annotation_getter = 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_annotation_getter = f'MutableSequence[{python_type}]' type_imports.add('from collections.abc import MutableSequence') if isinstance(member.type, AbstractNestedType): + if type_annotation_getter != '': + type_annotation = f'{type_annotation_getter}, ' type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' f'Set[{python_type}], UserList[{python_type}]]') @@ -90,7 +94,12 @@ for member in message.structure.members: type_imports.add(f'from {joined_type_namespaces} import {type_.name}') - type_annotations[member.name] = f'\'{type_annotation}\'' + type_annotations_setter[member.name] = f'\'{type_annotation}\'' + + if type_annotation_getter == '': + type_annotation_getter = type_annotation + + type_annotations_getter[member.name] = f'\'{type_annotation_getter}\'' type_imports.add('from typing import Literal') @@ -482,9 +491,9 @@ BUILTINS = [ @[ continue]@ @[ end if]@ @[ if member.name in BUILTINS]@ - @(member.name): Optional[@(type_annotations[member.name])] = None, # noqa: E501, A002 + @(member.name): Optional[@(type_annotations_setter[member.name])] = None, # noqa: E501, A002 @[ else]@ - @(member.name): Optional[@(type_annotations[member.name])] = None, # noqa: E501 + @(member.name): Optional[@(type_annotations_setter[member.name])] = None, # noqa: E501 @[ end if]@ @[end for]@ check_fields: Optional[bool] = None) -> None: @@ -617,12 +626,12 @@ if member.name in dict(inspect.getmembers(builtins)).keys(): }@ @@builtins.property@(noqa_string) - def @(member.name)(self) -> @(type_annotations[member.name]):@(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: @(type_annotations[member.name])) -> None:@(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)]@ From 468c8f5689096d9430c79931856ca3d27b35fd36 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 19:01:39 -0400 Subject: [PATCH 25/54] Move TYPE_CHECKING import Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index daa3ad73..a2b6ecdf 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -7,7 +7,7 @@ # change during runtime so it makes sense to only look for it once. from os import getenv import sys -from typing import Any, Callable, Dict, Optional, Tuple, TypedDict +from typing import Any, Callable, Dict, Optional, Tuple, 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 9ec1af19..d3d4dc67 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -167,8 +167,6 @@ for member in message.structure.members: default_type_annotations[constant.name] = get_type_annotation_constant_default(constant, value, type_imports) }@ -from typing import TYPE_CHECKING # noqa: E402, I100 - if sys.version_info >= (3, 12): from typing import override else: From 432a9dc42c5e506463799bde53ebf43311ae0d3c Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Tue, 19 Mar 2024 19:17:36 -0400 Subject: [PATCH 26/54] or syntax Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 27 ++++++++++--------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index d3d4dc67..0a0ce9ed 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -508,13 +508,8 @@ type_ = member.type if isinstance(type_, AbstractNestedType): type_ = type_.value_type }@ -@[ if not (not member.has_annotation('default') and isinstance(member.type, Array) and isinstance(member.type.value_type, BasicType) and member.type.value_type.typename in SPECIAL_NESTED_BASIC_TYPES)]@ - if @(member.name): - self.@(member.name) = @(member.name) - else: -@[ end if]@ @[ if member.has_annotation('default')]@ - self.@(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 ( @@ -522,16 +517,16 @@ if isinstance(type_, AbstractNestedType): type_.name.endswith(ACTION_RESULT_SUFFIX) or type_.name.endswith(ACTION_FEEDBACK_SUFFIX) )]@ - from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name) + from @('.'.join(type_.namespaces))._@(convert_camel_case_to_lower_case_underscore(type_.name.rsplit('_', 1)[0])) import @(type_.name) @[ else]@ - from @('.'.join(type_.namespaces)) import @(type_.name) + from @('.'.join(type_.namespaces)) import @(type_.name) @[ end if]@ @[ end if]@ @[ if isinstance(member.type, Array)]@ @[ if isinstance(type_, BasicType) and type_.typename == 'octet']@ - self.@(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) = [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) is None: @@ -540,21 +535,21 @@ if isinstance(type_, AbstractNestedType): 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) = [@(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'])', []) + self.@(member.name) = @(member.name) or array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) @[ else]@ - self.@(member.name) = [] + self.@(member.name) = @(member.name) or [] @[ end if]@ @[ elif isinstance(type_, BasicType) and type_.typename == 'octet']@ - self.@(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) = chr(0) + self.@(member.name) = @(member.name) or chr(0) @[ else]@ - self.@(member.name) = @(get_python_type(type_))() + self.@(member.name) = @(member.name) or @(get_python_type(type_))() @[ end if]@ @[ end if]@ @[end for]@ From af7463ad74872e493bdb05dc89cecfdac22ad336 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 18:48:01 -0400 Subject: [PATCH 27/54] Literal Clean up Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_action.py.em | 2 ++ rosidl_generator_py/resource/_msg.py.em | 17 ++-------- rosidl_generator_py/resource/_srv.py.em | 5 +-- .../test/test_cli_extension.py | 3 +- rosidl_generator_py/test/test_interfaces.py | 32 +++++++++---------- rosidl_generator_py/test/test_property.py | 2 +- 6 files changed, 26 insertions(+), 35 deletions(-) diff --git a/rosidl_generator_py/resource/_action.py.em b/rosidl_generator_py/resource/_action.py.em index 50b8ef08..05382d03 100644 --- a/rosidl_generator_py/resource/_action.py.em +++ b/rosidl_generator_py/resource/_action.py.em @@ -29,6 +29,8 @@ TEMPLATE( '_msg.py.em', package_name=package_name, interface_path=interface_path, message=action.feedback_message, import_statements=import_statements) + +type_imports.add('from typing import NoReturn') }@ diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 0a0ce9ed..4f53c25a 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -133,21 +133,8 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: elif isinstance(type_, float): type_annotation = 'float' else: - if isinstance(value, str): - # Literal type annotations cannot handle strings with ' or " until python3.7 - # Using from __future__ import annotations or Just importing Literal in python3.8 - if '\'' in value or '\"' in value: - return 'str' - - type_annotation = f'Literal[\"{value}\"]' - elif isinstance(value, float): - type_annotation = 'float' - elif type_.typename == 'octet': - # Literal type annotations cannot handle bytes with ' or " until python3.7 - # Using from __future__ import annotations or Just importing Literal in python3.8 - type_annotation = 'bytes' - else: - type_annotation = f'Literal[{value}]' + type_imports.add('from typing import Literal') + type_annotation = f'Literal[{value}]' type_annotation = f'\'{type_annotation}\'' return type_annotation diff --git a/rosidl_generator_py/resource/_srv.py.em b/rosidl_generator_py/resource/_srv.py.em index ae622b3b..a9bab136 100644 --- a/rosidl_generator_py/resource/_srv.py.em +++ b/rosidl_generator_py/resource/_srv.py.em @@ -17,8 +17,9 @@ TEMPLATE( '_msg.py.em', package_name=package_name, interface_path=interface_path, message=service.event_message, import_statements=import_statements) -}@ +type_imports.add('from typing import NoReturn') +}@ class Metaclass_@(service.namespaced_type.name)(type): """Metaclass of service '@(service.namespaced_type.name)'.""" @@ -55,5 +56,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) -> None: + def __init__(self) -> NoReturn: raise NotImplementedError('Service classes can not be instantiated') 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 ba6cadd8..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,7 +259,7 @@ 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 @@ -268,7 +268,7 @@ def test_constructor(): 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 From bb4ca22db15a57deb7eceba8a791b41d0e17949f Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 18:54:22 -0400 Subject: [PATCH 28/54] remove forward reference Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 4f53c25a..298301a2 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -133,7 +133,6 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: elif isinstance(type_, float): type_annotation = 'float' else: - type_imports.add('from typing import Literal') type_annotation = f'Literal[{value}]' type_annotation = f'\'{type_annotation}\'' @@ -323,7 +322,7 @@ for member in message.structure.members: @@override @@classmethod - def __prepare__(cls, name: 'Literal["@(message.structure.namespaced_type.name)"]', bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: + def __prepare__(cls, name: Literal["@(message.structure.namespaced_type.name)"], bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: # 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 From b0667d2e205ecb192797908dec17ae9f28bdd252 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 18:56:35 -0400 Subject: [PATCH 29/54] remove string wrapper Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 298301a2..e7e25dab 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -133,7 +133,7 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: elif isinstance(type_, float): type_annotation = 'float' else: - type_annotation = f'Literal[{value}]' + return f'Literal[{value}]' type_annotation = f'\'{type_annotation}\'' return type_annotation From 3fca5b1f7864ca374a23fe33a29a6a022f17e570 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 18:58:44 -0400 Subject: [PATCH 30/54] replace quotes with ' Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index e7e25dab..e1f250e3 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -322,7 +322,7 @@ for member in message.structure.members: @@override @@classmethod - def __prepare__(cls, name: Literal["@(message.structure.namespaced_type.name)"], bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: + def __prepare__(cls, name: Literal['@(message.structure.namespaced_type.name)'], bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: # 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 From 4a8fdf317a0a252c005635077547d3d45fb7b41c Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 19:04:24 -0400 Subject: [PATCH 31/54] add mypy test Signed-off-by: Michael Carlstrom --- rosidl_generator_py/package.xml | 1 + 1 file changed, 1 insertion(+) 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 From 68629fa8555db6aa2f14819e8f9126aa00d45b7b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 19:04:30 -0400 Subject: [PATCH 32/54] add mypy test Signed-off-by: Michael Carlstrom --- .../cmake/rosidl_generator_py_generate_interfaces.cmake | 5 +++++ 1 file changed, 5 insertions(+) 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() From f1d38a7288baec2c8dd6ccb691b4559f3677fd78 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 19:32:33 -0400 Subject: [PATCH 33/54] resolve truthy assert Signed-off-by: Michael Carlstrom --- rosidl_generator_py/rosidl_generator_py/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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') From a5b8d4161acf95797a1690c3c9fdab5bbbd17436 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 19:36:07 -0400 Subject: [PATCH 34/54] move Literal import Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index a2b6ecdf..e33f81c3 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -7,7 +7,7 @@ # change during runtime so it makes sense to only look for it once. from os import getenv import sys -from typing import Any, Callable, Dict, Optional, Tuple, TYPE_CHECKING, TypedDict +from typing import Any, Callable, Dict, Literal, Optional, Tuple, 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 e1f250e3..8820ca27 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -101,8 +101,6 @@ for member in message.structure.members: type_annotations_getter[member.name] = f'\'{type_annotation_getter}\'' -type_imports.add('from typing import Literal') - 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 From 18869552cf67937c274ff63c4bc86024b12259ef Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:00:33 -0400 Subject: [PATCH 35/54] update string literals Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 8820ca27..add0b6e4 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -129,7 +129,12 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: elif isinstance(type_, NamespacedType): type_annotation = python_type elif isinstance(type_, float): - type_annotation = 'float' + return 'float' + elif isinstance(value, str): + if "'" in value or '"' in value: + return 'str' + else: + return f'Literal[\'{value}\']' else: return f'Literal[{value}]' From d3acf35ae86b9dafaebfdaa1a62ef8c4a53f230f Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:01:39 -0400 Subject: [PATCH 36/54] fix Literal float Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index add0b6e4..cd106925 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -133,6 +133,8 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: elif isinstance(value, str): if "'" in value or '"' in value: return 'str' + elif isinstance(value, float): + return 'float' else: return f'Literal[\'{value}\']' else: From 9d10ab57ba02eed872c2320b9973295b48c2cd91 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:12:02 -0400 Subject: [PATCH 37/54] Add some types into rosidl_generator_py Signed-off-by: Michael Carlstrom --- rosidl_generator_py/py.typed | 0 rosidl_generator_py/resource/_msg.py.em | 11 +---------- .../rosidl_generator_py/import_type_support_impl.py | 5 +++-- 3 files changed, 4 insertions(+), 12 deletions(-) create mode 100644 rosidl_generator_py/py.typed 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/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index cd106925..9553e4c9 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -174,15 +174,6 @@ if TYPE_CHECKING: class PyCapsule(Structure): pass # don't need to define the full structure - from typing import Protocol - - class TypeSupportProtocol(Protocol): - create_ros_message_msg__@(suffix): PyCapsule - convert_from_py_msg__@(suffix): PyCapsule - convert_to_py_msg__@(suffix): PyCapsule - type_support_msg__@(suffix): PyCapsule - destroy_ros_message_msg__@(suffix): PyCapsule - @[for type_import in type_imports]@ @(type_import) @[end for] @@ -280,7 +271,7 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): def __import_type_support__(cls) -> None: try: from rosidl_generator_py import import_type_support - module: TypeSupportProtocol = import_type_support('@(package_name)') + module = import_type_support('@(package_name)') except ImportError: import logging import traceback 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..342eb86f 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 @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from types import ModuleType import importlib 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. From 33d126aa56acc68da9033e7d1307ebce22603d6b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:23:58 -0400 Subject: [PATCH 38/54] type_support import Signed-off-by: Michael Carlstrom --- .../rosidl_generator_py/msg/_constants.py | 258 ++++++++++++++++++ rosidl_generator_py/resource/_msg.py.em | 2 +- .../import_type_support_impl.py | 2 +- 3 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py diff --git a/build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py b/build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py new file mode 100644 index 00000000..52345b75 --- /dev/null +++ b/build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py @@ -0,0 +1,258 @@ +# generated from rosidl_generator_py/resource/_idl.py.em +# with input from rosidl_generator_py:msg/Constants.idl +# generated code does not contain a copyright notice + +# 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 os import getenv +import sys +from typing import Any, Callable, Dict, Literal, Optional, Tuple, TYPE_CHECKING, TypedDict + +ros_python_check_fields = getenv('ROS_PYTHON_CHECK_FIELDS', default='') + +if sys.version_info >= (3, 12): + from typing import override +else: + # Definition taking from typing_extension. + def override(func: Callable[..., Any]) -> Callable[..., Any]: + return func + +if TYPE_CHECKING: + from ctypes import Structure + + class PyCapsule(Structure): + pass # don't need to define the full structure + + +# Import statements for member types + +import rosidl_parser.definition # noqa: E402, I100 + + +class Metaclass_Constants(type): + """Metaclass of message 'Constants'.""" + + _CREATE_ROS_MESSAGE: Optional['PyCapsule'] = None + _CONVERT_FROM_PY: Optional['PyCapsule'] = None + _CONVERT_TO_PY: Optional['PyCapsule'] = None + _DESTROY_ROS_MESSAGE: Optional['PyCapsule'] = None + _TYPE_SUPPORT: Optional['PyCapsule'] = None + + class ConstantsConstants(TypedDict): + BOOL_CONST: Literal[True] + BYTE_CONST: Literal[b'2'] + CHAR_CONST: Literal[100] + FLOAT32_CONST: Literal[1.125] + FLOAT64_CONST: Literal[1.125] + INT8_CONST: Literal[-50] + UINT8_CONST: Literal[200] + INT16_CONST: Literal[-1000] + UINT16_CONST: Literal[2000] + INT32_CONST: Literal[-30000] + UINT32_CONST: Literal[60000] + INT64_CONST: Literal[-40000000] + UINT64_CONST: Literal[50000000] + + __constants: ConstantsConstants = { + 'BOOL_CONST': True, + 'BYTE_CONST': b'2', + 'CHAR_CONST': 100, + 'FLOAT32_CONST': 1.125, + 'FLOAT64_CONST': 1.125, + 'INT8_CONST': -50, + 'UINT8_CONST': 200, + 'INT16_CONST': -1000, + 'UINT16_CONST': 2000, + 'INT32_CONST': -30000, + 'UINT32_CONST': 60000, + 'INT64_CONST': -40000000, + 'UINT64_CONST': 50000000, + } + + class ConstantsDefault(ConstantsConstants): + pass + + @classmethod + def __import_type_support__(cls) -> None: + try: + from rosidl_generator_py import import_type_support + module = import_type_support('rosidl_generator_py') + except ImportError: + import logging + import traceback + logger = logging.getLogger( + 'rosidl_generator_py.msg.Constants') + logger.debug( + 'Failed to import needed modules for type support:\n' + + traceback.format_exc()) + else: + cls._CREATE_ROS_MESSAGE = module.create_ros_message_msg__msg__constants + cls._CONVERT_FROM_PY = module.convert_from_py_msg__msg__constants + cls._CONVERT_TO_PY = module.convert_to_py_msg__msg__constants + cls._TYPE_SUPPORT = module.type_support_msg__msg__constants + cls._DESTROY_ROS_MESSAGE = module.destroy_ros_message_msg__msg__constants + + @override + @classmethod + def __prepare__(cls, name: Literal['Constants'], bases: Tuple[()], **kwargs: Any) -> ConstantsDefault: + # 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 { + 'BOOL_CONST': cls.__constants['BOOL_CONST'], + 'BYTE_CONST': cls.__constants['BYTE_CONST'], + 'CHAR_CONST': cls.__constants['CHAR_CONST'], + 'FLOAT32_CONST': cls.__constants['FLOAT32_CONST'], + 'FLOAT64_CONST': cls.__constants['FLOAT64_CONST'], + 'INT8_CONST': cls.__constants['INT8_CONST'], + 'UINT8_CONST': cls.__constants['UINT8_CONST'], + 'INT16_CONST': cls.__constants['INT16_CONST'], + 'UINT16_CONST': cls.__constants['UINT16_CONST'], + 'INT32_CONST': cls.__constants['INT32_CONST'], + 'UINT32_CONST': cls.__constants['UINT32_CONST'], + 'INT64_CONST': cls.__constants['INT64_CONST'], + 'UINT64_CONST': cls.__constants['UINT64_CONST'], + } + + @property + def BOOL_CONST(self) -> Literal[True]: + """Message constant 'BOOL_CONST'.""" + return Metaclass_Constants.__constants['BOOL_CONST'] + + @property + def BYTE_CONST(self) -> Literal[50]: + """Message constant 'BYTE_CONST'.""" + return Metaclass_Constants.__constants['BYTE_CONST'] + + @property + def CHAR_CONST(self) -> Literal[100]: + """Message constant 'CHAR_CONST'.""" + return Metaclass_Constants.__constants['CHAR_CONST'] + + @property + def FLOAT32_CONST(self) -> Literal[1.125]: + """Message constant 'FLOAT32_CONST'.""" + return Metaclass_Constants.__constants['FLOAT32_CONST'] + + @property + def FLOAT64_CONST(self) -> Literal[1.125]: + """Message constant 'FLOAT64_CONST'.""" + return Metaclass_Constants.__constants['FLOAT64_CONST'] + + @property + def INT8_CONST(self) -> Literal[-50]: + """Message constant 'INT8_CONST'.""" + return Metaclass_Constants.__constants['INT8_CONST'] + + @property + def UINT8_CONST(self) -> Literal[200]: + """Message constant 'UINT8_CONST'.""" + return Metaclass_Constants.__constants['UINT8_CONST'] + + @property + def INT16_CONST(self) -> Literal[-1000]: + """Message constant 'INT16_CONST'.""" + return Metaclass_Constants.__constants['INT16_CONST'] + + @property + def UINT16_CONST(self) -> Literal[2000]: + """Message constant 'UINT16_CONST'.""" + return Metaclass_Constants.__constants['UINT16_CONST'] + + @property + def INT32_CONST(self) -> Literal[-30000]: + """Message constant 'INT32_CONST'.""" + return Metaclass_Constants.__constants['INT32_CONST'] + + @property + def UINT32_CONST(self) -> Literal[60000]: + """Message constant 'UINT32_CONST'.""" + return Metaclass_Constants.__constants['UINT32_CONST'] + + @property + def INT64_CONST(self) -> Literal[-40000000]: + """Message constant 'INT64_CONST'.""" + return Metaclass_Constants.__constants['INT64_CONST'] + + @property + def UINT64_CONST(self) -> Literal[50000000]: + """Message constant 'UINT64_CONST'.""" + return Metaclass_Constants.__constants['UINT64_CONST'] + + +class Constants(metaclass=Metaclass_Constants): + """ + Message class 'Constants'. + + Constants: + BOOL_CONST + BYTE_CONST + CHAR_CONST + FLOAT32_CONST + FLOAT64_CONST + INT8_CONST + UINT8_CONST + INT16_CONST + UINT16_CONST + INT32_CONST + UINT32_CONST + INT64_CONST + UINT64_CONST + """ + + __slots__ = [ + '_check_fields', + ] + + _fields_and_field_types: Dict[str, str] = { + } + + # 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: Tuple[rosidl_parser.definition.AbstractType, ...] = ( + ) + + def __init__(self, + 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' + + def __repr__(self) -> str: + typename = self.__class__.__module__.split('.') + typename.pop() + typename.append(self.__class__.__name__) + args = [] + for s, t in zip(self.get_fields_and_field_types().keys(), self.SLOT_TYPES): + field = getattr(self, s) + fieldstr = repr(field) + # We use Python array type for fields that can be directly stored + # in them, and "normal" sequences for everything else. If it is + # a type that we store in an array, strip off the 'array' portion. + if ( + isinstance(t, rosidl_parser.definition.AbstractSequence) and + isinstance(t.value_type, rosidl_parser.definition.BasicType) and + t.value_type.typename in ['float', 'double', 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'] + ): + if len(field) == 0: + fieldstr = '[]' + else: + if self._check_fields: + assert fieldstr.startswith('array(') + prefix = "array('X', " + suffix = ')' + fieldstr = fieldstr[len(prefix):-len(suffix)] + args.append(s + '=' + fieldstr) + return '%s(%s)' % ('.'.join(typename), ', '.join(args)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return False + return True + + @classmethod + def get_fields_and_field_types(cls) -> Dict[str, str]: + from copy import copy + return copy(cls._fields_and_field_types) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 9553e4c9..7689ce4b 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -270,7 +270,7 @@ class Metaclass_@(message.structure.namespaced_type.name)(type): @@classmethod 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 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 342eb86f..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 @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from types import ModuleType import importlib +from types import ModuleType from rpyutils import add_dll_directories_from_env From 829c25b6eecb43dfb4b859661a0223466d33d02b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:24:19 -0400 Subject: [PATCH 39/54] remove file Signed-off-by: Michael Carlstrom --- .../rosidl_generator_py/msg/_constants.py | 258 ------------------ 1 file changed, 258 deletions(-) delete mode 100644 build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py diff --git a/build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py b/build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py deleted file mode 100644 index 52345b75..00000000 --- a/build/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_constants.py +++ /dev/null @@ -1,258 +0,0 @@ -# generated from rosidl_generator_py/resource/_idl.py.em -# with input from rosidl_generator_py:msg/Constants.idl -# generated code does not contain a copyright notice - -# 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 os import getenv -import sys -from typing import Any, Callable, Dict, Literal, Optional, Tuple, TYPE_CHECKING, TypedDict - -ros_python_check_fields = getenv('ROS_PYTHON_CHECK_FIELDS', default='') - -if sys.version_info >= (3, 12): - from typing import override -else: - # Definition taking from typing_extension. - def override(func: Callable[..., Any]) -> Callable[..., Any]: - return func - -if TYPE_CHECKING: - from ctypes import Structure - - class PyCapsule(Structure): - pass # don't need to define the full structure - - -# Import statements for member types - -import rosidl_parser.definition # noqa: E402, I100 - - -class Metaclass_Constants(type): - """Metaclass of message 'Constants'.""" - - _CREATE_ROS_MESSAGE: Optional['PyCapsule'] = None - _CONVERT_FROM_PY: Optional['PyCapsule'] = None - _CONVERT_TO_PY: Optional['PyCapsule'] = None - _DESTROY_ROS_MESSAGE: Optional['PyCapsule'] = None - _TYPE_SUPPORT: Optional['PyCapsule'] = None - - class ConstantsConstants(TypedDict): - BOOL_CONST: Literal[True] - BYTE_CONST: Literal[b'2'] - CHAR_CONST: Literal[100] - FLOAT32_CONST: Literal[1.125] - FLOAT64_CONST: Literal[1.125] - INT8_CONST: Literal[-50] - UINT8_CONST: Literal[200] - INT16_CONST: Literal[-1000] - UINT16_CONST: Literal[2000] - INT32_CONST: Literal[-30000] - UINT32_CONST: Literal[60000] - INT64_CONST: Literal[-40000000] - UINT64_CONST: Literal[50000000] - - __constants: ConstantsConstants = { - 'BOOL_CONST': True, - 'BYTE_CONST': b'2', - 'CHAR_CONST': 100, - 'FLOAT32_CONST': 1.125, - 'FLOAT64_CONST': 1.125, - 'INT8_CONST': -50, - 'UINT8_CONST': 200, - 'INT16_CONST': -1000, - 'UINT16_CONST': 2000, - 'INT32_CONST': -30000, - 'UINT32_CONST': 60000, - 'INT64_CONST': -40000000, - 'UINT64_CONST': 50000000, - } - - class ConstantsDefault(ConstantsConstants): - pass - - @classmethod - def __import_type_support__(cls) -> None: - try: - from rosidl_generator_py import import_type_support - module = import_type_support('rosidl_generator_py') - except ImportError: - import logging - import traceback - logger = logging.getLogger( - 'rosidl_generator_py.msg.Constants') - logger.debug( - 'Failed to import needed modules for type support:\n' + - traceback.format_exc()) - else: - cls._CREATE_ROS_MESSAGE = module.create_ros_message_msg__msg__constants - cls._CONVERT_FROM_PY = module.convert_from_py_msg__msg__constants - cls._CONVERT_TO_PY = module.convert_to_py_msg__msg__constants - cls._TYPE_SUPPORT = module.type_support_msg__msg__constants - cls._DESTROY_ROS_MESSAGE = module.destroy_ros_message_msg__msg__constants - - @override - @classmethod - def __prepare__(cls, name: Literal['Constants'], bases: Tuple[()], **kwargs: Any) -> ConstantsDefault: - # 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 { - 'BOOL_CONST': cls.__constants['BOOL_CONST'], - 'BYTE_CONST': cls.__constants['BYTE_CONST'], - 'CHAR_CONST': cls.__constants['CHAR_CONST'], - 'FLOAT32_CONST': cls.__constants['FLOAT32_CONST'], - 'FLOAT64_CONST': cls.__constants['FLOAT64_CONST'], - 'INT8_CONST': cls.__constants['INT8_CONST'], - 'UINT8_CONST': cls.__constants['UINT8_CONST'], - 'INT16_CONST': cls.__constants['INT16_CONST'], - 'UINT16_CONST': cls.__constants['UINT16_CONST'], - 'INT32_CONST': cls.__constants['INT32_CONST'], - 'UINT32_CONST': cls.__constants['UINT32_CONST'], - 'INT64_CONST': cls.__constants['INT64_CONST'], - 'UINT64_CONST': cls.__constants['UINT64_CONST'], - } - - @property - def BOOL_CONST(self) -> Literal[True]: - """Message constant 'BOOL_CONST'.""" - return Metaclass_Constants.__constants['BOOL_CONST'] - - @property - def BYTE_CONST(self) -> Literal[50]: - """Message constant 'BYTE_CONST'.""" - return Metaclass_Constants.__constants['BYTE_CONST'] - - @property - def CHAR_CONST(self) -> Literal[100]: - """Message constant 'CHAR_CONST'.""" - return Metaclass_Constants.__constants['CHAR_CONST'] - - @property - def FLOAT32_CONST(self) -> Literal[1.125]: - """Message constant 'FLOAT32_CONST'.""" - return Metaclass_Constants.__constants['FLOAT32_CONST'] - - @property - def FLOAT64_CONST(self) -> Literal[1.125]: - """Message constant 'FLOAT64_CONST'.""" - return Metaclass_Constants.__constants['FLOAT64_CONST'] - - @property - def INT8_CONST(self) -> Literal[-50]: - """Message constant 'INT8_CONST'.""" - return Metaclass_Constants.__constants['INT8_CONST'] - - @property - def UINT8_CONST(self) -> Literal[200]: - """Message constant 'UINT8_CONST'.""" - return Metaclass_Constants.__constants['UINT8_CONST'] - - @property - def INT16_CONST(self) -> Literal[-1000]: - """Message constant 'INT16_CONST'.""" - return Metaclass_Constants.__constants['INT16_CONST'] - - @property - def UINT16_CONST(self) -> Literal[2000]: - """Message constant 'UINT16_CONST'.""" - return Metaclass_Constants.__constants['UINT16_CONST'] - - @property - def INT32_CONST(self) -> Literal[-30000]: - """Message constant 'INT32_CONST'.""" - return Metaclass_Constants.__constants['INT32_CONST'] - - @property - def UINT32_CONST(self) -> Literal[60000]: - """Message constant 'UINT32_CONST'.""" - return Metaclass_Constants.__constants['UINT32_CONST'] - - @property - def INT64_CONST(self) -> Literal[-40000000]: - """Message constant 'INT64_CONST'.""" - return Metaclass_Constants.__constants['INT64_CONST'] - - @property - def UINT64_CONST(self) -> Literal[50000000]: - """Message constant 'UINT64_CONST'.""" - return Metaclass_Constants.__constants['UINT64_CONST'] - - -class Constants(metaclass=Metaclass_Constants): - """ - Message class 'Constants'. - - Constants: - BOOL_CONST - BYTE_CONST - CHAR_CONST - FLOAT32_CONST - FLOAT64_CONST - INT8_CONST - UINT8_CONST - INT16_CONST - UINT16_CONST - INT32_CONST - UINT32_CONST - INT64_CONST - UINT64_CONST - """ - - __slots__ = [ - '_check_fields', - ] - - _fields_and_field_types: Dict[str, str] = { - } - - # 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: Tuple[rosidl_parser.definition.AbstractType, ...] = ( - ) - - def __init__(self, - 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' - - def __repr__(self) -> str: - typename = self.__class__.__module__.split('.') - typename.pop() - typename.append(self.__class__.__name__) - args = [] - for s, t in zip(self.get_fields_and_field_types().keys(), self.SLOT_TYPES): - field = getattr(self, s) - fieldstr = repr(field) - # We use Python array type for fields that can be directly stored - # in them, and "normal" sequences for everything else. If it is - # a type that we store in an array, strip off the 'array' portion. - if ( - isinstance(t, rosidl_parser.definition.AbstractSequence) and - isinstance(t.value_type, rosidl_parser.definition.BasicType) and - t.value_type.typename in ['float', 'double', 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'] - ): - if len(field) == 0: - fieldstr = '[]' - else: - if self._check_fields: - assert fieldstr.startswith('array(') - prefix = "array('X', " - suffix = ')' - fieldstr = fieldstr[len(prefix):-len(suffix)] - args.append(s + '=' + fieldstr) - return '%s(%s)' % ('.'.join(typename), ', '.join(args)) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, self.__class__): - return False - return True - - @classmethod - def get_fields_and_field_types(cls) -> Dict[str, str]: - from copy import copy - return copy(cls._fields_and_field_types) From 9b95b026d7f7b8b3fd1a13fc4575ec5152257ceb Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:35:44 -0400 Subject: [PATCH 40/54] fix byte litearls Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 7689ce4b..c013c36e 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -130,14 +130,16 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: type_annotation = python_type elif isinstance(type_, float): return 'float' - elif isinstance(value, str): - if "'" in value or '"' in value: - return 'str' + else: + if isinstance(value, str): + if "'" in value or '"' in value: + return 'str' + else: + return f'Literal[\'{value}\']' elif isinstance(value, float): return 'float' - else: - return f'Literal[\'{value}\']' - else: + elif type_.typename == 'octet': + return 'bytes' return f'Literal[{value}]' type_annotation = f'\'{type_annotation}\'' From 010db3f487ceb3ae105d964b1c6bbe11d66785dd Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 20:54:48 -0400 Subject: [PATCH 41/54] remove ovveride Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 3 +-- rosidl_generator_py/resource/_msg.py.em | 11 +---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index e33f81c3..8b61d874 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -6,8 +6,7 @@ # 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 os import getenv -import sys -from typing import Any, Callable, Dict, Literal, Optional, Tuple, TYPE_CHECKING, TypedDict +from typing import Any, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Type, 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 c013c36e..6a0daae8 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -159,14 +159,6 @@ for member in message.structure.members: value = constant.get_annotation_value('default')['value'] default_type_annotations[constant.name] = get_type_annotation_constant_default(constant, value, type_imports) }@ - -if sys.version_info >= (3, 12): - from typing import override -else: - # Definition taking from typing_extension. - def override(func: Callable[..., Any]) -> Callable[..., Any]: - return func - @{ suffix = '__'.join(message.structure.namespaced_type.namespaces[1:]) + '__' + convert_camel_case_to_lower_case_underscore(message.structure.namespaced_type.name) }@ @@ -318,9 +310,8 @@ for member in message.structure.members: @(typename[-1]).__import_type_support__() @[end for]@ - @@override @@classmethod - def __prepare__(cls, name: Literal['@(message.structure.namespaced_type.name)'], bases: Tuple[()], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: + def __prepare__(cls, name: str, bases: Tuple[Type, ...], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: # 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 From ffc1b375c6b6ca2c3d72d824a13d432f624ac893 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:01:27 -0400 Subject: [PATCH 42/54] fix __prepare__ Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 1 + rosidl_generator_py/resource/_msg.py.em | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 8b61d874..8f92b3bb 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -7,6 +7,7 @@ # change during runtime so it makes sense to only look for it once. from os import getenv from typing import Any, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Type, TypedDict +from collections.abc import MutableMapping 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 6a0daae8..23b7a811 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -311,7 +311,7 @@ for member in message.structure.members: @[end for]@ @@classmethod - def __prepare__(cls, name: str, bases: Tuple[Type, ...], **kwargs: Any) -> @(message.structure.namespaced_type.name)Default: + 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 From 5752835ce25ed60fa2faf5355648e01dfe2d6928 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:12:05 -0400 Subject: [PATCH 43/54] fix metacls Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_action.py.em | 2 +- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 12 ++++++------ rosidl_generator_py/resource/_srv.py.em | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rosidl_generator_py/resource/_action.py.em b/rosidl_generator_py/resource/_action.py.em index 05382d03..db0283aa 100644 --- a/rosidl_generator_py/resource/_action.py.em +++ b/rosidl_generator_py/resource/_action.py.em @@ -94,5 +94,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) -> None: + def __init__(self) -> 'NoReturn': 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 8f92b3bb..be2dc19b 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -6,7 +6,7 @@ # 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 os import getenv -from typing import Any, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Type, TypedDict +from typing import Any, ClassVar, Dict, Literal, Optional, Tuple, TYPE_CHECKING, Type, TypedDict from collections.abc import MutableMapping 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 23b7a811..0b360957 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -231,11 +231,11 @@ 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: Optional['PyCapsule'] = None - _CONVERT_FROM_PY: Optional['PyCapsule'] = None - _CONVERT_TO_PY: Optional['PyCapsule'] = None - _DESTROY_ROS_MESSAGE: Optional['PyCapsule'] = None - _TYPE_SUPPORT: Optional['PyCapsule'] = 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]@ @@ -317,7 +317,7 @@ for member in message.structure.members: # 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')]@ diff --git a/rosidl_generator_py/resource/_srv.py.em b/rosidl_generator_py/resource/_srv.py.em index a9bab136..6aacf893 100644 --- a/rosidl_generator_py/resource/_srv.py.em +++ b/rosidl_generator_py/resource/_srv.py.em @@ -56,5 +56,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) -> NoReturn: + def __init__(self) -> 'NoReturn': raise NotImplementedError('Service classes can not be instantiated') From ad8fd40e6cc273d9dd9a2bc7b186bf2286682524 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:17:06 -0400 Subject: [PATCH 44/54] remove ByteString from isinstance Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 0b360957..54b8a91b 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -640,8 +640,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]@ @@ -714,7 +712,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']@ From e7a2ed600e973a1a1f891459dc005b700bad3403 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:21:25 -0400 Subject: [PATCH 45/54] import error Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index be2dc19b..6255eb49 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -5,9 +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, Literal, Optional, Tuple, TYPE_CHECKING, Type, TypedDict -from collections.abc import MutableMapping ros_python_check_fields = getenv('ROS_PYTHON_CHECK_FIELDS', default='') @ From f1966c7918b4503c4b36ff178e67383461370ed8 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:22:46 -0400 Subject: [PATCH 46/54] switch typing improt Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 6255eb49..104a9eb8 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -7,7 +7,7 @@ # 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, Literal, Optional, Tuple, TYPE_CHECKING, Type, TypedDict +from typing import Any, ClassVar, Dict, Literal, Optional, Tuple, Type, TYPE_CHECKING, TypedDict ros_python_check_fields = getenv('ROS_PYTHON_CHECK_FIELDS', default='') @ From 95c8e52f6bc801059e559c244b38626316fb0a4a Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:31:11 -0400 Subject: [PATCH 47/54] move Literal import Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_idl.py.em | 2 +- rosidl_generator_py/resource/_msg.py.em | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rosidl_generator_py/resource/_idl.py.em b/rosidl_generator_py/resource/_idl.py.em index 104a9eb8..6a876379 100644 --- a/rosidl_generator_py/resource/_idl.py.em +++ b/rosidl_generator_py/resource/_idl.py.em @@ -7,7 +7,7 @@ # 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, Literal, Optional, Tuple, Type, TYPE_CHECKING, TypedDict +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 54b8a91b..b68b9e76 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -135,12 +135,15 @@ def get_type_annotation_constant_default(constant, value, type_imports) -> str: if "'" in value or '"' in value: return 'str' else: - return f'Literal[\'{value}\']' + type_imports.add('from typing import Literal') + type_annotation = f'Literal["{value}"]' elif isinstance(value, float): return 'float' elif type_.typename == 'octet': return 'bytes' - return f'Literal[{value}]' + else: + type_imports.add('from typing import Literal') + type_annotation = f'Literal[{value}]' type_annotation = f'\'{type_annotation}\'' return type_annotation From 375cc73730bb05503b41b27fca08665ee79f20d0 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:43:50 -0400 Subject: [PATCH 48/54] match getter and setter Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_action.py.em | 4 +--- rosidl_generator_py/resource/_msg.py.em | 19 ++++--------------- rosidl_generator_py/resource/_srv.py.em | 4 +--- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/rosidl_generator_py/resource/_action.py.em b/rosidl_generator_py/resource/_action.py.em index db0283aa..50b8ef08 100644 --- a/rosidl_generator_py/resource/_action.py.em +++ b/rosidl_generator_py/resource/_action.py.em @@ -29,8 +29,6 @@ TEMPLATE( '_msg.py.em', package_name=package_name, interface_path=interface_path, message=action.feedback_message, import_statements=import_statements) - -type_imports.add('from typing import NoReturn') }@ @@ -94,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) -> 'NoReturn': + def __init__(self) -> None: raise NotImplementedError('Action classes can not be instantiated') diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index b68b9e76..d8dc9929 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -31,7 +31,7 @@ from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ @{ import_type_checking = False -type_annotations_getter = {} +# type_annotations_getter = {} type_annotations_setter = {} type_imports = set() @@ -44,23 +44,17 @@ for member in message.structure.members: python_type = get_python_type(type_) type_annotation = '' - type_annotation_getter = '' - + if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: if isinstance(member.type, Array): - dtype = SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'] - type_annotation_getter = 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_getter = f'MutableSequence[{python_type}]' type_imports.add('from collections.abc import MutableSequence') if isinstance(member.type, AbstractNestedType): - if type_annotation_getter != '': - type_annotation = f'{type_annotation_getter}, ' type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' - f'Set[{python_type}], UserList[{python_type}]]') + f'Set[{python_type}], UserList[{python_type}]]') type_imports.add('from typing import Union') type_imports.add('from collections.abc import Sequence') @@ -93,13 +87,8 @@ for member in message.structure.members: else: type_imports.add(f'from {joined_type_namespaces} import {type_.name}') - type_annotations_setter[member.name] = f'\'{type_annotation}\'' - if type_annotation_getter == '': - type_annotation_getter = type_annotation - - type_annotations_getter[member.name] = f'\'{type_annotation_getter}\'' def get_type_annotation_constant_default(constant, value, type_imports) -> str: from rosidl_parser.definition import AbstractNestedType, BasicType, NamespacedType, AbstractSequence, Array @@ -597,7 +586,7 @@ if member.name in dict(inspect.getmembers(builtins)).keys(): }@ @@builtins.property@(noqa_string) - def @(member.name)(self) -> @(type_annotations_getter[member.name]):@(noqa_string) + def @(member.name)(self) -> @(type_annotations_setter[member.name]):@(noqa_string) """Message field '@(member.name)'.""" return self._@(member.name) diff --git a/rosidl_generator_py/resource/_srv.py.em b/rosidl_generator_py/resource/_srv.py.em index 6aacf893..504ce70b 100644 --- a/rosidl_generator_py/resource/_srv.py.em +++ b/rosidl_generator_py/resource/_srv.py.em @@ -17,8 +17,6 @@ TEMPLATE( '_msg.py.em', package_name=package_name, interface_path=interface_path, message=service.event_message, import_statements=import_statements) - -type_imports.add('from typing import NoReturn') }@ class Metaclass_@(service.namespaced_type.name)(type): @@ -56,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) -> 'NoReturn': + def __init__(self) -> None: raise NotImplementedError('Service classes can not be instantiated') From 576fe2489bfe1583b26c0895c6c444fa6b28c3b3 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 21:53:01 -0400 Subject: [PATCH 49/54] fix numpy types Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index d8dc9929..14a218ed 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -47,9 +47,12 @@ for member in message.structure.members: if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: 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): From 381d1e11bcc6d535f4597e529e4a785f4f32e272 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 22:02:03 -0400 Subject: [PATCH 50/54] resolve all() Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 14a218ed..b23d560f 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -552,14 +552,14 @@ if isinstance(type_, AbstractNestedType): return '%s(%s)' % ('.'.join(typename), ', '.join(args)) def __eq__(self, other: object) -> bool: - if not isinstance(other, self.__class__): + 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]@ From 41cc5618de196c5ad159063617a3ebc850a4b682 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Wed, 4 Sep 2024 22:27:08 -0400 Subject: [PATCH 51/54] fix mypy error Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index b23d560f..3ef627d1 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -559,7 +559,7 @@ if isinstance(type_, AbstractNestedType): @[ 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)): # type: ignore[arg-type] + if all(self.@(member.name) != other.@(member.name)): # type: ignore[arg-type] @[ else]@ if self.@(member.name) != other.@(member.name): @[ end if]@ @@ -757,7 +757,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 From 6e357c3a12f296917faec62eb62e51024dc0673d Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Sep 2024 13:35:26 -0400 Subject: [PATCH 52/54] type ignore around mypy#3004 Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 3ef627d1..68fb2004 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -30,10 +30,12 @@ from rosidl_parser.definition import UnboundedSequence from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ @{ +from typing import Set + import_type_checking = False -# type_annotations_getter = {} +type_annotations_getter = {} type_annotations_setter = {} -type_imports = set() +type_imports: Set[str] = set() for member in message.structure.members: type_ = member.type @@ -44,16 +46,19 @@ for member in message.structure.members: python_type = get_python_type(type_) type_annotation = '' + type_annotation_getter = '' if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: if isinstance(member.type, Array): dtype = SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'] - type_annotation = f'NDArray[{dtype}], ' + type_annotation_getter = 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_annotation_getter = f'MutableSequence[{python_type}]' type_imports.add('from collections.abc import MutableSequence') + + type_annotation = f'{type_annotation_getter}, ' if isinstance(member.type, AbstractNestedType): type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' @@ -92,6 +97,11 @@ for member in message.structure.members: type_annotations_setter[member.name] = f'\'{type_annotation}\'' + if type_annotation_getter == '': + type_annotation_getter = type_annotation + + type_annotations_getter[member.name] = f'\'{type_annotation_getter}\'' + def get_type_annotation_constant_default(constant, value, type_imports) -> str: from rosidl_parser.definition import AbstractNestedType, BasicType, NamespacedType, AbstractSequence, Array @@ -478,7 +488,10 @@ type_ = member.type if isinstance(type_, AbstractNestedType): type_ = type_.value_type }@ -@[ if member.has_annotation('default')]@ +@[ if member.has_annotation('default') and isinstance(member.type, (AbstractSequence, Array))]@ + # type ignore Due to mypy#3004 + self.@(member.name) = @(member.name) or @(message.structure.namespaced_type.name).@(member.name.upper())__DEFAULT # type: ignore[assignment] +@[ elif member.has_annotation('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)]@ @@ -510,7 +523,8 @@ if isinstance(type_, AbstractNestedType): @[ 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) = @(member.name) or array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) + # type ignore Due to mypy#3004 + self.@(member.name) = @(member.name) or array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) # type: ignore[assignment] @[ else]@ self.@(member.name) = @(member.name) or [] @[ end if]@ @@ -589,9 +603,14 @@ if member.name in dict(inspect.getmembers(builtins)).keys(): }@ @@builtins.property@(noqa_string) - def @(member.name)(self) -> @(type_annotations_setter[member.name]):@(noqa_string) + def @(member.name)(self) -> @(type_annotations_getter[member.name]):@(noqa_string) """Message field '@(member.name)'.""" +@[ if isinstance(member.type, AbstractSequence)]@ + # type ignore Due to mypy#3004 + return self._@(member.name) # type: ignore[return-value] +@[ else]@ return self._@(member.name) +@[ end if]@ @@@(member.name).setter@(noqa_string) def @(member.name)(self, value: @(type_annotations_setter[member.name])) -> None:@(noqa_string) From dd5c3e902e5792b4c8c0f4ba010b898ce8234574 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sat, 7 Sep 2024 16:23:03 -0400 Subject: [PATCH 53/54] revert specifc getter type Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 29 +++++-------------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index 68fb2004..f0c5b95e 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -31,9 +31,7 @@ from rosidl_parser.definition import UNSIGNED_INTEGER_TYPES }@ @{ from typing import Set - import_type_checking = False -type_annotations_getter = {} type_annotations_setter = {} type_imports: Set[str] = set() @@ -46,19 +44,16 @@ for member in message.structure.members: python_type = get_python_type(type_) type_annotation = '' - type_annotation_getter = '' if isinstance(member.type, AbstractNestedType) and isinstance(type_, BasicType) and type_.typename in SPECIAL_NESTED_BASIC_TYPES: if isinstance(member.type, Array): dtype = SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['dtype'] - type_annotation_getter = f'NDArray[{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_getter = f'MutableSequence[{python_type}]' + type_annotation = f'MutableSequence[{python_type}], ' type_imports.add('from collections.abc import MutableSequence') - - type_annotation = f'{type_annotation_getter}, ' if isinstance(member.type, AbstractNestedType): type_annotation = (f'Union[{type_annotation}Sequence[{python_type}], ' @@ -97,11 +92,6 @@ for member in message.structure.members: type_annotations_setter[member.name] = f'\'{type_annotation}\'' - if type_annotation_getter == '': - type_annotation_getter = type_annotation - - type_annotations_getter[member.name] = f'\'{type_annotation_getter}\'' - def get_type_annotation_constant_default(constant, value, type_imports) -> str: from rosidl_parser.definition import AbstractNestedType, BasicType, NamespacedType, AbstractSequence, Array @@ -488,10 +478,7 @@ type_ = member.type if isinstance(type_, AbstractNestedType): type_ = type_.value_type }@ -@[ if member.has_annotation('default') and isinstance(member.type, (AbstractSequence, Array))]@ - # type ignore Due to mypy#3004 - self.@(member.name) = @(member.name) or @(message.structure.namespaced_type.name).@(member.name.upper())__DEFAULT # type: ignore[assignment] -@[ elif member.has_annotation('default')]@ +@[ if member.has_annotation('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)]@ @@ -523,8 +510,7 @@ if isinstance(type_, AbstractNestedType): @[ 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]@ - # type ignore Due to mypy#3004 - self.@(member.name) = @(member.name) or array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) # type: ignore[assignment] + self.@(member.name) = @(member.name) or array.array('@(SPECIAL_NESTED_BASIC_TYPES[member.type.value_type.typename]['type_code'])', []) @[ else]@ self.@(member.name) = @(member.name) or [] @[ end if]@ @@ -603,14 +589,9 @@ if member.name in dict(inspect.getmembers(builtins)).keys(): }@ @@builtins.property@(noqa_string) - def @(member.name)(self) -> @(type_annotations_getter[member.name]):@(noqa_string) + def @(member.name)(self) -> @(type_annotations_setter[member.name]):@(noqa_string) """Message field '@(member.name)'.""" -@[ if isinstance(member.type, AbstractSequence)]@ - # type ignore Due to mypy#3004 - return self._@(member.name) # type: ignore[return-value] -@[ else]@ return self._@(member.name) -@[ end if]@ @@@(member.name).setter@(noqa_string) def @(member.name)(self, value: @(type_annotations_setter[member.name])) -> None:@(noqa_string) From b6bc52c8c393dd5b9b2610fdc988b3980d04413a Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 27 Sep 2024 12:37:59 -0400 Subject: [PATCH 54/54] Use Any to avoid mypy#3004 Signed-off-by: Michael Carlstrom --- rosidl_generator_py/resource/_msg.py.em | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/rosidl_generator_py/resource/_msg.py.em b/rosidl_generator_py/resource/_msg.py.em index f0c5b95e..7d43d782 100644 --- a/rosidl_generator_py/resource/_msg.py.em +++ b/rosidl_generator_py/resource/_msg.py.em @@ -33,6 +33,7 @@ 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: @@ -43,9 +44,15 @@ for member in message.structure.members: 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}], ' @@ -56,6 +63,8 @@ for member in message.structure.members: 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}]]') @@ -92,6 +101,10 @@ for member in message.structure.members: 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 @@ -589,7 +602,7 @@ if member.name in dict(inspect.getmembers(builtins)).keys(): }@ @@builtins.property@(noqa_string) - def @(member.name)(self) -> @(type_annotations_setter[member.name]):@(noqa_string) + def @(member.name)(self) -> @(type_annotations_getter[member.name]):@(noqa_string) """Message field '@(member.name)'.""" return self._@(member.name)