Skip to content

Use absolute imports for generated local type support#261

Open
Lidang-Jiang wants to merge 2 commits into
ros2:rollingfrom
Lidang-Jiang:fix/absolute-local-imports
Open

Use absolute imports for generated local type support#261
Lidang-Jiang wants to merge 2 commits into
ros2:rollingfrom
Lidang-Jiang:fix/absolute-local-imports

Conversation

@Lidang-Jiang
Copy link
Copy Markdown

@Lidang-Jiang Lidang-Jiang commented Apr 29, 2026

Summary

Fixes #257.

  • Generate absolute module imports for namespaced message field types, e.g. import builtin_interfaces.msg.
  • Refer to namespaced types by their fully qualified module path in type support imports, default construction, annotations, and check_fields assertions.
  • Add template-level regression coverage for scalar, fixed-size array, and unbounded sequence fields using builtin_interfaces/Duration, without adding a package-level test dependency on builtin_interfaces.

Did you use Generative AI?

Yes. I used OpenAI Codex (GPT-5) to help investigate the issue, prepare the code and test changes, and draft/update this PR description. I reviewed the changes and ran the local tests listed below before opening the PR.

Before / After

Before

Command:

git show upstream/rolling:rosidl_generator_py/resource/_msg.py.em | sed -n '178,207p'

Output:

        type_ = type_.value_type
    if isinstance(type_, NamespacedType):
        if (
            type_.name.endswith(SERVICE_RESPONSE_MESSAGE_SUFFIX) or
            type_.name.endswith(SERVICE_REQUEST_MESSAGE_SUFFIX)
        ):
            continue
        if (
            type_.name.endswith(ACTION_GOAL_SUFFIX) or
            type_.name.endswith(ACTION_RESULT_SUFFIX) or
            type_.name.endswith(ACTION_FEEDBACK_SUFFIX)
        ):
            action_name, suffix = type_.name.rsplit('_', 1)
            typename = (*type_.namespaces, action_name, action_name + '.' + suffix)
        else:
            typename = (*type_.namespaces, type_.name, type_.name)
        importable_typesupports.add(typename)
}@
@[for typename in sorted(importable_typesupports)]@

            from @('.'.join(typename[:-2])) import @(typename[-2])
            if @(typename[-1])._TYPE_SUPPORT is None:
                @(typename[-1]).__import_type_support__()
@[end for]@
After

Command:

rg -n "builtin_interfaces\\.msg|from builtin_interfaces\\.msg" \
  build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py \
  build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py

Output:

build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:27:    import builtin_interfaces.msg  # noqa: E402, I100, I201, I300
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:72:            import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:73:            if builtin_interfaces.msg.Duration._TYPE_SUPPORT is None:
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:74:                builtin_interfaces.msg.Duration.__import_type_support__()
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:107:                 array_data: typing.Optional[collections.abc.Sequence[builtin_interfaces.msg.Duration]] = None,  # noqa: E501
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:108:                 sequence_data: typing.Optional[collections.abc.Sequence[builtin_interfaces.msg.Duration]] = None,  # noqa: E501
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:114:        import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:115:        self.array_data = array_data if array_data is not None else [builtin_interfaces.msg.Duration() for x in range(2)]
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:162:    def array_data(self) -> typing.Annotated[typing.Any, list[builtin_interfaces.msg.Duration]]:   # typing.Annotated can be remove after mypy 1.16+ see mypy#3004
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:167:    def array_data(self, value: collections.abc.Sequence[builtin_interfaces.msg.Duration]) -> None:
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:174:        import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:186:                     all(isinstance(v, builtin_interfaces.msg.Duration) for v in value) and
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:188:                    "The 'array_data' field must be sequence with length 2 and each value of type 'builtin_interfaces.msg.Duration'"
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:196:    def sequence_data(self) -> typing.Annotated[typing.Any, list[builtin_interfaces.msg.Duration]]:   # typing.Annotated can be remove after mypy 1.16+ see mypy#3004
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:201:    def sequence_data(self, value: collections.abc.Sequence[builtin_interfaces.msg.Duration]) -> None:
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:208:        import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:219:                     all(isinstance(v, builtin_interfaces.msg.Duration) for v in value) and
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration_array_sequence.py:221:                    "The 'sequence_data' field must be sequence and each value of type 'builtin_interfaces.msg.Duration'"
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:27:    import builtin_interfaces.msg  # noqa: E402, I100, I201, I300
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:72:            import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:73:            if builtin_interfaces.msg.Duration._TYPE_SUPPORT is None:
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:74:                builtin_interfaces.msg.Duration.__import_type_support__()
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:104:                 data: typing.Optional[builtin_interfaces.msg.Duration] = None,  # noqa: E501
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:110:        import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:111:        self.data = data if data is not None else builtin_interfaces.msg.Duration()
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:155:    def data(self) -> builtin_interfaces.msg.Duration:
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:160:    def data(self, value: builtin_interfaces.msg.Duration) -> None:
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:161:        import builtin_interfaces.msg
build_pr/rosidl_generator_py/rosidl_generator_py/rosidl_generator_py/msg/_duration.py:168:                    isinstance(value, builtin_interfaces.msg.Duration), \

Tests

Command:

PYTHONPATH=rosidl_generator_py:$WORKSPACE/rosidl/rosidl_parser:$WORKSPACE/rosidl/rosidl_pycommon python3 -m pytest rosidl_generator_py/test/test_template_imports.py -q

Output:

..                                                                       [100%]
2 passed in 0.12s

Command:

python3 -m flake8 rosidl_generator_py/test/test_template_imports.py
git diff --check

Output:

<no output>

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
@christophebedard
Copy link
Copy Markdown
Member

@Lidang-Jiang did you use AI to write all/some of this code or open this PR?

@Lidang-Jiang
Copy link
Copy Markdown
Author

Thanks for checking. Yes, I used OpenAI Codex (GPT-5) to help investigate the issue, prepare the code and test changes, and draft/update this PR description. I updated the PR body with a Generative AI disclosure section as well.

@christophebedard
Copy link
Copy Markdown
Member

@InvincibleRMC could you review this?

Comment thread rosidl_generator_py/CMakeLists.txt Outdated
if(BUILD_TESTING)
find_package(ament_cmake_pytest REQUIRED)

find_package(builtin_interfaces REQUIRED)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will run CI to double check. But I'm pretty sure this won't work since this would cause a circular package order. We might need to make another pr before this splitting the tests into there own rosidl_generator_py_tests or something to avoid this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah CI complains about being unable to topologically order the package after adding builtin_interfaces.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in 3dd9104. I removed the builtin_interfaces test dependency and moved the regression coverage to a template-level pytest so this no longer affects package ordering.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, fixed in 3dd9104 by removing the generated builtin_interfaces test interfaces and the CMake/package.xml dependency that caused the topological cycle.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also addressed in 3dd9104: the package-order failure is gone because the PR no longer adds builtin_interfaces to rosidl_generator_py CMake/package test dependencies.

@InvincibleRMC
Copy link
Copy Markdown
Contributor

Pulls: #261
Gist: https://gist.githubusercontent.com/InvincibleRMC/958b7530d78835ed1ae7c544e9336c58/raw/bb0c723b0335bf0d3247b31f5bfa66a8298a8111/ros2.repos
BUILD args: --continue-on-error --packages-above-and-dependencies rosidl_generator_py
TEST args: --packages-above rosidl_generator_py
ROS Distro: rolling
Job: ci_launcher
ci_launcher ran: https://ci.ros2.org/job/ci_launcher/19266

  • Linux Build Status
  • Linux-aarch64 Build Status
  • Linux-rhel Build Status
  • Windows Build Status

Move the absolute import regression coverage from generated test interfaces to a template-level pytest so rosidl_generator_py does not need to depend on builtin_interfaces during testing.

Signed-off-by: Lidang Jiang <lidangjiang@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Message named pkg/Duration with field builtin_interfaces/Duration is invalid

3 participants