From 24fbba076d0a358e3185934e608caf7b64805c2b Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sat, 24 Jan 2026 11:10:30 -0800 Subject: [PATCH 01/21] init Signed-off-by: Michael Carlstrom --- ament_clang_format/setup.py | 3 + ament_clang_tidy/setup.py | 3 + ament_copyright/setup.py | 3 + ament_cppcheck/setup.py | 3 + ament_cpplint/setup.py | 3 + ament_flake8/setup.py | 3 + ament_lint_auto/ament_lint_auto/__init__.py | 0 ament_lint_auto/ament_lint_auto/main.py | 21 ++ ament_lint_auto/ament_lint_auto/py.typed | 0 ament_lint_common_py/CHANGELOG.rst | 279 ++++++++++++++++++++ ament_lint_common_py/CMakeLists.txt | 10 + ament_lint_common_py/doc/index.rst | 22 ++ ament_lint_common_py/package.xml | 38 +++ ament_mypy/setup.py | 3 + ament_pclint/setup.py | 3 + ament_pep257/setup.py | 3 + ament_pycodestyle/setup.py | 3 + ament_pyflakes/setup.py | 3 + ament_uncrustify/setup.py | 3 + ament_xmllint/setup.py | 3 + 20 files changed, 409 insertions(+) create mode 100644 ament_lint_auto/ament_lint_auto/__init__.py create mode 100644 ament_lint_auto/ament_lint_auto/main.py create mode 100644 ament_lint_auto/ament_lint_auto/py.typed create mode 100644 ament_lint_common_py/CHANGELOG.rst create mode 100644 ament_lint_common_py/CMakeLists.txt create mode 100644 ament_lint_common_py/doc/index.rst create mode 100644 ament_lint_common_py/package.xml diff --git a/ament_clang_format/setup.py b/ament_clang_format/setup.py index 1510ea0be..8bd7de942 100644 --- a/ament_clang_format/setup.py +++ b/ament_clang_format/setup.py @@ -44,5 +44,8 @@ 'console_scripts': [ 'ament_clang_format = ament_clang_format.main:main', ], + 'ament_lint': [ + 'ament_clang_format = ament_clang_format.main:main', + ], }, ) diff --git a/ament_clang_tidy/setup.py b/ament_clang_tidy/setup.py index 65a7d9697..dfe106627 100644 --- a/ament_clang_tidy/setup.py +++ b/ament_clang_tidy/setup.py @@ -44,5 +44,8 @@ 'console_scripts': [ 'ament_clang_tidy = ament_clang_tidy.main:main', ], + 'ament_lint': [ + 'ament_clang_tidy = ament_clang_tidy.main:main', + ], }, ) diff --git a/ament_copyright/setup.py b/ament_copyright/setup.py index 702b24e8e..bca42b282 100644 --- a/ament_copyright/setup.py +++ b/ament_copyright/setup.py @@ -58,5 +58,8 @@ 'pytest11': [ 'ament_copyright = ament_copyright.pytest_marker', ], + 'ament_lint': [ + 'ament_copyright = ament_copyright.main:main', + ], }, ) diff --git a/ament_cppcheck/setup.py b/ament_cppcheck/setup.py index 3b5986709..be1a8e04c 100644 --- a/ament_cppcheck/setup.py +++ b/ament_cppcheck/setup.py @@ -40,5 +40,8 @@ 'console_scripts': [ 'ament_cppcheck = ament_cppcheck.main:main', ], + 'ament_lint': [ + 'ament_cppcheck = ament_cppcheck.main:main', + ], }, ) diff --git a/ament_cpplint/setup.py b/ament_cpplint/setup.py index 53e3534a7..0926fee58 100644 --- a/ament_cpplint/setup.py +++ b/ament_cpplint/setup.py @@ -40,5 +40,8 @@ 'console_scripts': [ 'ament_cpplint = ament_cpplint.main:main', ], + 'ament_lint': [ + 'ament_cpplint = ament_cpplint.main:main', + ], }, ) diff --git a/ament_flake8/setup.py b/ament_flake8/setup.py index b928050a5..df8175659 100644 --- a/ament_flake8/setup.py +++ b/ament_flake8/setup.py @@ -45,5 +45,8 @@ 'pytest11': [ 'ament_flake8 = ament_flake8.pytest_marker', ], + 'ament_lint': [ + 'ament_flake8 = ament_flake8.main:main', + ], }, ) diff --git a/ament_lint_auto/ament_lint_auto/__init__.py b/ament_lint_auto/ament_lint_auto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ament_lint_auto/ament_lint_auto/main.py b/ament_lint_auto/ament_lint_auto/main.py new file mode 100644 index 000000000..be45e4c9b --- /dev/null +++ b/ament_lint_auto/ament_lint_auto/main.py @@ -0,0 +1,21 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from importlib.metadata import entry_points + +linters = entry_points(group="ament_lint") + +for ep in linters: + rc = ep.load()() + print(rc) diff --git a/ament_lint_auto/ament_lint_auto/py.typed b/ament_lint_auto/ament_lint_auto/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/ament_lint_common_py/CHANGELOG.rst b/ament_lint_common_py/CHANGELOG.rst new file mode 100644 index 000000000..b47523654 --- /dev/null +++ b/ament_lint_common_py/CHANGELOG.rst @@ -0,0 +1,279 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package ament_lint_common_py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +0.20.3 (2025-11-24) +------------------- + +0.20.2 (2025-07-30) +------------------- + +0.20.1 (2025-06-19) +------------------- +* Fix cmake deprecation (`#539 `_) +* Contributors: mosfet80 + +0.20.0 (2025-04-24) +------------------- + +0.19.2 (2025-04-17) +------------------- + +0.19.1 (2025-02-02) +------------------- + +0.19.0 (2024-11-20) +------------------- + +0.18.1 (2024-06-17) +------------------- + +0.18.0 (2024-04-26) +------------------- + +0.17.0 (2024-04-16) +------------------- + +0.16.4 (2024-03-28) +------------------- +* Update maintainer list in package.xml files (`#474 `_) +* Contributors: Michael Jeronimo + +0.16.3 (2024-02-07) +------------------- + +0.16.2 (2023-12-26) +------------------- + +0.16.1 (2023-11-06) +------------------- + +0.16.0 (2023-10-04) +------------------- + +0.15.2 (2023-07-11) +------------------- + +0.15.1 (2023-06-07) +------------------- + +0.15.0 (2023-04-27) +------------------- + +0.14.1 (2023-04-11) +------------------- + +0.14.0 (2023-02-14) +------------------- +* [rolling] Update maintainers - 2022-11-07 (`#421 `_) +* Contributors: Audrow Nash + +0.13.2 (2022-09-13) +------------------- +* Update maintainers (`#379 `_) +* Contributors: methylDragon + +0.13.1 (2022-05-09) +------------------- + +0.13.0 (2022-04-29) +------------------- + +0.12.3 (2022-04-08) +------------------- + +0.12.2 (2022-03-28) +------------------- + +0.12.1 (2022-03-01) +------------------- + +0.12.0 (2022-02-18) +------------------- + +0.11.4 (2022-01-14) +------------------- +* Update forthcoming version in changelogs +* Contributors: Audrow Nash + +0.11.3 (2022-01-14) +------------------- +* Update maintainers to Michael Jeronimo and Michel Hidalgo (`#340 `_) +* Fix typo in ament_lint_common/package.xml (`#336 `_) +* Contributors: Audrow Nash, Kenji Miyake + +0.11.2 (2021-06-18) +------------------- + +0.11.1 (2021-06-18) +------------------- + +0.11.0 (2021-06-18) +------------------- + +0.10.6 (2021-05-06) +------------------- + +0.10.5 (2021-04-14) +------------------- +* Remove Claire as a maintainer. (`#312 `_) + * Remove Claire as a maintainer. + * Remove dead email addresses. + * Remove more dead email addresses. + * Switch setup.py maintainer to Audrow. +* Contributors: Chris Lalancette + +0.10.4 (2021-03-18) +------------------- + +0.10.3 (2021-03-03) +------------------- +* 0.10.3 +* Contributors: Audrow Nash + +0.10.2 (2021-02-12) +------------------- +* Add Audrow as a maintainer (`#294 `_) +* Drop trailing tab from package manifests (`#291 `_) + Follow-up to 8bf194aa1ac282db5483dd0d3fefff8f325b0db8 +* Contributors: Audrow Nash, Scott K Logan + +0.10.1 (2021-01-25) +------------------- +* Update maintainer (`#274 `_) + * update maintainer + * add authors +* Contributors: Claire Wang + +0.10.0 (2020-09-18) +------------------- + +0.9.4 (2020-05-26) +------------------ + +0.9.3 (2020-05-18) +------------------ + +0.9.2 (2020-05-08) +------------------ + +0.9.1 (2020-04-10) +------------------ +* 0.9.0 +* add ament_lint_common doc (`#218 `_) + * adding ament_lint_common doc + * add links to common linters + * change ament_uncrustify link +* Contributors: Chris Lalancette, Marya Belanger + +0.8.1 (2019-10-23) +------------------ + +0.8.0 (2019-09-17) +------------------ + +0.7.4 (2019-07-31) +------------------ +* Add clang tidy to ament linters (`#155 `_) + * Basic clang-lint functionality + * Add clang tidy cmake dir + * Add Google/default config file + * Add xunit output + * Add lint tests + * Update copyright, maintainer, verbiage +* Contributors: John + +0.7.3 (2019-05-09 14:08) +------------------------ + +0.7.2 (2019-05-09 09:30) +------------------------ + +0.7.1 (2019-05-07) +------------------ + +0.7.0 (2019-04-11) +------------------ + +0.6.3 (2019-01-14) +------------------ + +0.6.2 (2018-12-06) +------------------ + +0.6.1 (2018-11-15) +------------------ + +0.6.0 (2018-11-14) +------------------ +* add ament\_(cmake\_)xmllint packages (`#104 `_) + * add ament\_(cmake\_)xmllint packages + * consider xsi:noNamespaceSchemaLocation of root tag +* Contributors: Dirk Thomas + +0.5.2 (2018-06-27) +------------------ + +0.5.1 (2018-06-18 13:47) +------------------------ + +0.5.0 (2018-06-18 10:09) +------------------------ + +0.4.0 (2017-12-08) +------------------ +* 0.0.3 +* 0.0.2 +* Test python module import order using flake8 (`#63 `_) + * Add flake8 linter + * Don't deal with flake8-import-order just yet + * Debugging prints + * Reinstate import order rule + * Fix reporting bug by using the inner flake8 style guide + * Fixup + * Add comment on wrapper StyleGuide use + * use flake8 v3 (`#1 `_) + * Reorder package.xml + * Get the filenames from the file checkers because input_file isn't called by flake8 anymore + * Output count of all error types + * Get flake8 to use the config file + The current implementation of get_style_guide does not process the config file correctly. + * Error when flake8 v2 found + * Print errors like pep8 + * remove __future_\_ imports + * add schema to manifest files + * Support flake8 v2 as well as v3 + * Output checked files + otherwise it's not present in xunit files for tests run directly with nose (not ament_cmake_flake8) + * Prevent v2 imports from happening on systems with v3 + * Flake8 replaces pep8+pyflakes +* update schema url +* add schema to manifest files +* Merge pull request `#56 `_ from ament/cmake35 + require CMake 3.5 +* require CMake 3.5 +* Merge pull request `#39 `_ from ament/cpplint_python3 + use Python 3 for cpplint and split into Python and CMake package +* refactor ament_cpplint into Python and CMake package +* Merge pull request `#36 `_ from ament/enable_cpplint + add ament_cpplint as default linter +* add ament_cpplint as default linter +* Merge pull request `#28 `_ from ament/pep257 + add packages to check pep257 compliance +* use ament_cmake_pep257 as a common linter +* add explicit build type +* Merge pull request `#19 `_ from ament/split_linter_packages_in_python_and_cmake + split linter packages in python and cmake +* move cmake part of ament_uncrustify to ament_cmake_uncrustify +* move cmake part of ament_pyflakes to ament_cmake_pyflakes +* move cmake part of ament_pep8 to ament_cmake_pep8 +* move cmake part of ament_lint_cmake to ament_cmake_lint_cmake +* move cmake part of ament_cppcheck to ament_cmake_cppcheck +* Merge pull request `#8 `_ from ament/ament_copyright + add more options to ament_copyright +* add more options to ament_copyright +* disable clang_format and cpplint for now (fix `ros2/ros2#15 `_) +* Merge pull request `#2 `_ from ament/ament_lint_auto + allow linting based on test dependencies only +* add ament_lint_auto and ament_lint_common, update all linter packages to implement extension point of ament_lint_auto +* Contributors: Dirk Thomas, dhood diff --git a/ament_lint_common_py/CMakeLists.txt b/ament_lint_common_py/CMakeLists.txt new file mode 100644 index 000000000..44cd31e1a --- /dev/null +++ b/ament_lint_common_py/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) + +project(ament_lint_common_py NONE) + +find_package(ament_cmake_core REQUIRED) +find_package(ament_cmake_export_dependencies REQUIRED) + +ament_package_xml() +ament_export_dependencies(${${PROJECT_NAME}_EXEC_DEPENDS}) +ament_package() diff --git a/ament_lint_common_py/doc/index.rst b/ament_lint_common_py/doc/index.rst new file mode 100644 index 000000000..aa6e506b0 --- /dev/null +++ b/ament_lint_common_py/doc/index.rst @@ -0,0 +1,22 @@ +ament_lint_common_py +================= + +A mechanism for running the following set of common linters: + +* `ament_copyright `_ : a copyright linter which checks that copyright statements and license headers are present and correct + +* `ament_cppcheck `_ : a C++ checker which can also find some logic tests + +* `ament_cpplint `_ : a C++ style checker (e.g. comment style) + +* `ament_flake8 `_ : a style checker for Python files + +* `ament_lint `_ : a cmake linter + +* `ament_pep257 `_ : a style checker for Python docstrings + +* `ament_uncrustify `_ : a C++ style checker + +* `ament_xmllint `_ : an xml linter + +The `ament_lint_auto `_ documentation provides information on using ament_lint_common. diff --git a/ament_lint_common_py/package.xml b/ament_lint_common_py/package.xml new file mode 100644 index 000000000..666ad19d7 --- /dev/null +++ b/ament_lint_common_py/package.xml @@ -0,0 +1,38 @@ + + + + ament_lint_common_py + 0.20.3 + The list of commonly used linters in the ament python build system. + + + Chris Lalancette + + Apache License 2.0 + + Audrow Nash + Brandon Ong + Claire Wang + Dirk Thomas + Michel Hidalgo + + ament_cmake_core + ament_cmake_export_dependencies + + ament_cmake_core + + + + ament_copyright + ament_cppcheck + ament_cpplint + ament_flake8 + ament_lint_cmake + ament_pep257 + ament_uncrustify + ament_xmllint + + + ament_cmake + + diff --git a/ament_mypy/setup.py b/ament_mypy/setup.py index 4bcdc3ea7..c67a9234f 100644 --- a/ament_mypy/setup.py +++ b/ament_mypy/setup.py @@ -45,5 +45,8 @@ 'pytest11': [ 'ament_mypy = ament_mypy.pytest_marker', ], + 'ament_lint': [ + 'ament_mypy = ament_mypy.main:main', + ], }, ) diff --git a/ament_pclint/setup.py b/ament_pclint/setup.py index f43d546ec..7a19c0883 100644 --- a/ament_pclint/setup.py +++ b/ament_pclint/setup.py @@ -61,5 +61,8 @@ 'console_scripts': [ 'ament_pclint = ament_pclint.main:main', ], + 'ament_lint': [ + 'ament_pclint = ament_pclint.main:main', + ], }, ) diff --git a/ament_pep257/setup.py b/ament_pep257/setup.py index 4a8f190ba..3181eb5bb 100644 --- a/ament_pep257/setup.py +++ b/ament_pep257/setup.py @@ -46,5 +46,8 @@ 'pytest11': [ 'ament_pep257 = ament_pep257.pytest_marker', ], + 'ament_lint': [ + 'ament_pep257 = ament_pep257.main:main', + ], }, ) diff --git a/ament_pycodestyle/setup.py b/ament_pycodestyle/setup.py index 14e351146..c7c35fe2a 100644 --- a/ament_pycodestyle/setup.py +++ b/ament_pycodestyle/setup.py @@ -43,5 +43,8 @@ 'console_scripts': [ 'ament_pycodestyle = ament_pycodestyle.main:main', ], + 'ament_lint': [ + 'ament_pycodestyle = ament_pycodestyle.main:main', + ], }, ) diff --git a/ament_pyflakes/setup.py b/ament_pyflakes/setup.py index cec869891..77d625708 100644 --- a/ament_pyflakes/setup.py +++ b/ament_pyflakes/setup.py @@ -40,5 +40,8 @@ 'console_scripts': [ 'ament_pyflakes = ament_pyflakes.main:main', ], + 'ament_lint': [ + 'ament_pyflakes = ament_pyflakes.main:main', + ], }, ) diff --git a/ament_uncrustify/setup.py b/ament_uncrustify/setup.py index 039322af4..0ac219792 100644 --- a/ament_uncrustify/setup.py +++ b/ament_uncrustify/setup.py @@ -44,5 +44,8 @@ 'console_scripts': [ 'ament_uncrustify = ament_uncrustify.main:main', ], + 'ament_lint': [ + 'ament_uncrustify = ament_uncrustify.main:main', + ], }, ) diff --git a/ament_xmllint/setup.py b/ament_xmllint/setup.py index e8dbcf37a..4d64f91c4 100644 --- a/ament_xmllint/setup.py +++ b/ament_xmllint/setup.py @@ -43,5 +43,8 @@ 'pytest11': [ 'ament_xmllint = ament_xmllint.pytest_marker', ], + 'ament_lint': [ + 'ament_xmllint = ament_xmllint.main:main', + ], }, ) From 5feeaf87eb5bf9ad7544663a5568087acfd68dd9 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sat, 24 Jan 2026 18:27:28 -0800 Subject: [PATCH 02/21] Register linters Signed-off-by: Michael Carlstrom --- ament_clang_format/ament_clang_format/main.py | 20 ++++ ament_clang_format/setup.py | 2 +- ament_clang_tidy/ament_clang_tidy/main.py | 20 ++++ ament_clang_tidy/setup.py | 2 +- ament_copyright/ament_copyright/main.py | 13 +++ ament_copyright/setup.py | 2 +- ament_cppcheck/ament_cppcheck/main.py | 20 ++++ ament_cppcheck/setup.py | 2 +- ament_cpplint/ament_cpplint/main.py | 20 ++++ ament_cpplint/setup.py | 2 +- ament_flake8/ament_flake8/main.py | 11 ++ ament_flake8/setup.py | 2 +- ament_lint_auto_py/CHANGELOG.rst | 3 + .../ament_lint_auto_py}/__init__.py | 0 .../ament_lint_auto_py}/py.typed | 0 .../ament_lint_auto_py/pytest_plugin.py | 61 ++++++++++ ament_lint_auto_py/doc/index.rst | 104 ++++++++++++++++++ ament_lint_auto_py/package.xml | 23 ++++ .../resource/ament_lint_auto_py | 0 ament_lint_auto_py/setup.py | 44 ++++++++ .../test/test_ament_lint_auto.py | 13 ++- ament_lint_cmake/ament_lint_cmake/main.py | 11 ++ ament_lint_cmake/setup.py | 3 + ament_mypy/ament_mypy/main.py | 12 +- ament_mypy/setup.py | 2 +- ament_mypy/test/test_flake8.py | 26 ----- ament_mypy/test/test_xmllint.py | 23 ---- ament_pclint/ament_pclint/main.py | 17 +++ ament_pclint/setup.py | 2 +- ament_pep257/ament_pep257/main.py | 11 ++ ament_pep257/setup.py | 2 +- ament_pycodestyle/ament_pycodestyle/main.py | 11 ++ ament_pycodestyle/setup.py | 2 +- ament_pyflakes/ament_pyflakes/main.py | 11 ++ ament_pyflakes/setup.py | 2 +- ament_uncrustify/ament_uncrustify/main.py | 20 ++++ ament_uncrustify/setup.py | 2 +- ament_xmllint/ament_xmllint/main.py | 16 ++- ament_xmllint/setup.py | 2 +- 39 files changed, 469 insertions(+), 70 deletions(-) create mode 100644 ament_lint_auto_py/CHANGELOG.rst rename {ament_lint_auto/ament_lint_auto => ament_lint_auto_py/ament_lint_auto_py}/__init__.py (100%) rename {ament_lint_auto/ament_lint_auto => ament_lint_auto_py/ament_lint_auto_py}/py.typed (100%) create mode 100644 ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py create mode 100644 ament_lint_auto_py/doc/index.rst create mode 100644 ament_lint_auto_py/package.xml create mode 100644 ament_lint_auto_py/resource/ament_lint_auto_py create mode 100644 ament_lint_auto_py/setup.py rename ament_lint_auto/ament_lint_auto/main.py => ament_lint_auto_py/test/test_ament_lint_auto.py (65%) delete mode 100644 ament_mypy/test/test_flake8.py delete mode 100644 ament_mypy/test/test_xmllint.py diff --git a/ament_clang_format/ament_clang_format/main.py b/ament_clang_format/ament_clang_format/main.py index 871206289..fd245537a 100755 --- a/ament_clang_format/ament_clang_format/main.py +++ b/ament_clang_format/ament_clang_format/main.py @@ -19,6 +19,7 @@ import subprocess import sys import time +from typing import Literal from xml.etree import ElementTree from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -26,6 +27,25 @@ import yaml +class ClangFormatRunner: + + NAME = 'ament_clang_format' + FILE_TYPES = ( + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.h', + '*.hh', + '*.hpp', + '*.hxx', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): config_file = os.path.join( os.path.dirname(__file__), 'configuration', '.clang-format') diff --git a/ament_clang_format/setup.py b/ament_clang_format/setup.py index 8bd7de942..d22109ec1 100644 --- a/ament_clang_format/setup.py +++ b/ament_clang_format/setup.py @@ -45,7 +45,7 @@ 'ament_clang_format = ament_clang_format.main:main', ], 'ament_lint': [ - 'ament_clang_format = ament_clang_format.main:main', + 'ament_clang_format = ament_clang_format.main:ClangFormatRunner', ], }, ) diff --git a/ament_clang_tidy/ament_clang_tidy/main.py b/ament_clang_tidy/ament_clang_tidy/main.py index 5faa81425..a264e4b76 100755 --- a/ament_clang_tidy/ament_clang_tidy/main.py +++ b/ament_clang_tidy/ament_clang_tidy/main.py @@ -24,6 +24,7 @@ import subprocess import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -31,6 +32,25 @@ import yaml +class ClangTidyRunner: + + NAME = 'ament_clang_tidy' + FILE_TYPES = ( + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.h', + '*.hh', + '*.hpp', + '*.hxx', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): extensions = ['c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx'] diff --git a/ament_clang_tidy/setup.py b/ament_clang_tidy/setup.py index dfe106627..90be749ea 100644 --- a/ament_clang_tidy/setup.py +++ b/ament_clang_tidy/setup.py @@ -45,7 +45,7 @@ 'ament_clang_tidy = ament_clang_tidy.main:main', ], 'ament_lint': [ - 'ament_clang_tidy = ament_clang_tidy.main:main', + 'ament_clang_tidy = ament_clang_tidy.main:ClangTidyRunner', ], }, ) diff --git a/ament_copyright/ament_copyright/main.py b/ament_copyright/ament_copyright/main.py index 361f3b107..138b1d03e 100644 --- a/ament_copyright/ament_copyright/main.py +++ b/ament_copyright/ament_copyright/main.py @@ -18,6 +18,7 @@ import re import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -36,6 +37,18 @@ from ament_copyright.parser import search_copyright_information +class CopyrightRunner: + + NAME = 'ament_copyright' + FILE_TYPES = ( + '*', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): extensions = [ 'c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx', diff --git a/ament_copyright/setup.py b/ament_copyright/setup.py index bca42b282..59751e237 100644 --- a/ament_copyright/setup.py +++ b/ament_copyright/setup.py @@ -59,7 +59,7 @@ 'ament_copyright = ament_copyright.pytest_marker', ], 'ament_lint': [ - 'ament_copyright = ament_copyright.main:main', + 'ament_copyright = ament_copyright.main:CopyrightRunner', ], }, ) diff --git a/ament_cppcheck/ament_cppcheck/main.py b/ament_cppcheck/ament_cppcheck/main.py index 450179926..b8f8c0383 100755 --- a/ament_cppcheck/ament_cppcheck/main.py +++ b/ament_cppcheck/ament_cppcheck/main.py @@ -22,6 +22,7 @@ import subprocess import sys import time +from typing import Literal from xml.etree import ElementTree from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -51,6 +52,25 @@ def get_cppcheck_version(cppcheck_bin): return tokens[1] +class CPPCheckRunner: + + NAME = 'ament_cppcheck' + FILE_TYPES = ( + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.h', + '*.hh', + '*.hpp', + '*.hxx', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): extensions = ['c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx'] diff --git a/ament_cppcheck/setup.py b/ament_cppcheck/setup.py index be1a8e04c..be3caed4e 100644 --- a/ament_cppcheck/setup.py +++ b/ament_cppcheck/setup.py @@ -41,7 +41,7 @@ 'ament_cppcheck = ament_cppcheck.main:main', ], 'ament_lint': [ - 'ament_cppcheck = ament_cppcheck.main:main', + 'ament_cppcheck = ament_cppcheck.main:CPPCheckRunner', ], }, ) diff --git a/ament_cpplint/ament_cpplint/main.py b/ament_cpplint/ament_cpplint/main.py index fd8769b7b..04860bdd7 100755 --- a/ament_cpplint/ament_cpplint/main.py +++ b/ament_cpplint/ament_cpplint/main.py @@ -21,6 +21,7 @@ import re import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -69,6 +70,25 @@ def custom_get_header_guard_cpp_variable(filename): cpplint.GetHeaderGuardCPPVariable = custom_get_header_guard_cpp_variable +class CPPLintRunner: + + NAME = 'ament_cpplint' + FILE_TYPES = ( + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.h', + '*.hh', + '*.hpp', + '*.hxx', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): extensions = ['c', 'cc', 'cpp', 'cxx'] headers = ['h', 'hh', 'hpp', 'hxx'] diff --git a/ament_cpplint/setup.py b/ament_cpplint/setup.py index 0926fee58..aa056b1b1 100644 --- a/ament_cpplint/setup.py +++ b/ament_cpplint/setup.py @@ -41,7 +41,7 @@ 'ament_cpplint = ament_cpplint.main:main', ], 'ament_lint': [ - 'ament_cpplint = ament_cpplint.main:main', + 'ament_cpplint = ament_cpplint.main:CPPLintRunner', ], }, ) diff --git a/ament_flake8/ament_flake8/main.py b/ament_flake8/ament_flake8/main.py index ed22bdde1..b5f457923 100755 --- a/ament_flake8/ament_flake8/main.py +++ b/ament_flake8/ament_flake8/main.py @@ -18,6 +18,7 @@ import os import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -27,6 +28,16 @@ from flake8.main import options as flake8_options +class Flake8Runner: + + NAME = 'ament_flake8' + FILE_TYPES = ('*.py', ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): rc, _ = main_with_errors(argv=argv) return rc diff --git a/ament_flake8/setup.py b/ament_flake8/setup.py index df8175659..44fd7e281 100644 --- a/ament_flake8/setup.py +++ b/ament_flake8/setup.py @@ -46,7 +46,7 @@ 'ament_flake8 = ament_flake8.pytest_marker', ], 'ament_lint': [ - 'ament_flake8 = ament_flake8.main:main', + 'ament_flake8 = ament_flake8.main:Flake8Runner', ], }, ) diff --git a/ament_lint_auto_py/CHANGELOG.rst b/ament_lint_auto_py/CHANGELOG.rst new file mode 100644 index 000000000..b20c0edbc --- /dev/null +++ b/ament_lint_auto_py/CHANGELOG.rst @@ -0,0 +1,3 @@ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Changelog for package ament_lint_auto_py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/ament_lint_auto/ament_lint_auto/__init__.py b/ament_lint_auto_py/ament_lint_auto_py/__init__.py similarity index 100% rename from ament_lint_auto/ament_lint_auto/__init__.py rename to ament_lint_auto_py/ament_lint_auto_py/__init__.py diff --git a/ament_lint_auto/ament_lint_auto/py.typed b/ament_lint_auto_py/ament_lint_auto_py/py.typed similarity index 100% rename from ament_lint_auto/ament_lint_auto/py.typed rename to ament_lint_auto_py/ament_lint_auto_py/py.typed diff --git a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py new file mode 100644 index 000000000..14e1337a9 --- /dev/null +++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py @@ -0,0 +1,61 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections.abc import Callable +from importlib.metadata import entry_points +from importlib.metadata import EntryPoint +from pathlib import Path +import sys + +import pytest +from pytest import Config +from pytest import Metafunc + + +def pytest_configure(config: Config) -> None: + config.addinivalue_line('markers', + 'ament_lint_auto_py: marks tests as running all linter checks') + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + if 'ament_lint_ep' in metafunc.fixturenames: + linters = entry_points(group='ament_lint') + + filtered_linters: list[EntryPoint] = [] + ids: list[int] = [] + + for ep in linters: + func = ep.load() + name = func.NAME + file_types = func.FILE_TYPES + + paths: list[Path] = [] + for ext in file_types: + paths.extend(Path('.').rglob(f'{ext}')) + + if not paths: + print(f'Skipping {name} because no files of type {file_types} exist.') + continue + + filtered_linters.append(ep) + ids.append(name) + + metafunc.parametrize('ament_lint_ep', filtered_linters, ids=ids) + + +@pytest.fixture +def run_entry_point(ament_lint_ep: EntryPoint) -> Callable[[], int]: + func = ament_lint_ep.load() + sys.argv = [func.NAME] + return func() diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst new file mode 100644 index 000000000..c1e17e737 --- /dev/null +++ b/ament_lint_auto_py/doc/index.rst @@ -0,0 +1,104 @@ +ament_lint_auto_py +=============== + +.. TODO +The package simplifies using multiple linters as part of the CMake tests. +It reduces the amount of CMake code to a bare minimum. + +``CMakeLists.txt``: + +.. code:: cmake + + # this must happen before the invocation of ament_package() + if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() + endif() + +The set of linters to be used is then only specified in the package manifest as +test dependencies. + +``package.xml``: + +.. code:: xml + + ament_lint_auto + + + ament_cmake_clang_format + ament_cmake_cppcheck + ament_cmake_pycodestyle + +Since recursive dependencies are also being used a single test dependency is +sufficient to test with a set of common linters. + +``package.xml``: + +.. code:: xml + + ament_lint_auto + + + ament_lint_common + +The documentation of the package `ament_cmake_test +`_ provides more information on testing +in CMake ament packages. + + +How to exclude linter modules with ament_lint_auto? +--------------------------------------------------- + +Linter modules can be excluded via the CMake list variable `AMENT_LINT_AUTO_EXCLUDE`. + +As an example to exclude the `copyright` linter: + +``CMakeLists.txt``: + +.. code:: cmake + + # this must happen before the invocation of ament_package() + if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_copyright) + ament_lint_auto_find_test_dependencies() + endif() + + +How to exclude files with ament_lint_auto? +------------------------------------------ + +Linter hooks shall conform to the ament_lint_auto convention of excluding files +specified in the CMake list variable `AMENT_LINT_AUTO_FILE_EXCLUDE`. +As such, the CMake snippet from above can be modified to exclude files across +all linters with one addition. + +``CMakeLists.txt``: + +.. code:: cmake + + # this must happen before the invocation of ament_package() + if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + set(AMENT_LINT_AUTO_FILE_EXCLUDE /path/to/ignored_file ...) + ament_lint_auto_find_test_dependencies() + endif() + +For a more specific example, this excludes all python files matching a pattern using globbing. +Multiple expressions can be combined on multiple lines. +It might be a good idea to issue a warning to developers that linting is disabled +if you plan to enable it at some point. + +.. code:: cmake + + file(GLOB_RECURSE AMENT_LINT_AUTO_FILE_EXCLUDE + # Exclude all the python files in src directory + src/*.py + # Exclude all the c++ implementation files in test directory + test/*.cpp + ) + message(AUTHOR_WARNING + "Ament lint auto tests are disabled on the following: " + ${AMENT_LINT_AUTO_FILE_EXCLUDE} + ) + diff --git a/ament_lint_auto_py/package.xml b/ament_lint_auto_py/package.xml new file mode 100644 index 000000000..fd2e3013c --- /dev/null +++ b/ament_lint_auto_py/package.xml @@ -0,0 +1,23 @@ + + + + ament_lint_auto_py + 0.20.3 + The auto-magic functions for ease to use of the ament linters in Python. + + + Chris Lalancette + + Apache License 2.0 + + Audrow Nash + + python3-pytest + + ament_lint_common_py + ament_mypy + + + ament_python + + diff --git a/ament_lint_auto_py/resource/ament_lint_auto_py b/ament_lint_auto_py/resource/ament_lint_auto_py new file mode 100644 index 000000000..e69de29bb diff --git a/ament_lint_auto_py/setup.py b/ament_lint_auto_py/setup.py new file mode 100644 index 000000000..ec8fb54ba --- /dev/null +++ b/ament_lint_auto_py/setup.py @@ -0,0 +1,44 @@ +from setuptools import find_packages +from setuptools import setup + +package_name = 'ament_lint_auto_py' + +setup( + name=package_name, + version='0.20.3', + packages=find_packages(exclude=['test']), + data_files=[ + ('share/' + package_name, ['package.xml']), + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ], + package_data={'': [ + 'py.typed' + ]}, + zip_safe=False, + author='Ted Kern', + author_email='ted.kern@canonical.com', + maintainer='Michael Jeronimo', + maintainer_email='michael.jeronimo@openrobotics.org', + url='https://github.com/ament/ament_lint', + download_url='https://github.com/ament/ament_lint/releases', + keywords=['ROS'], + classifiers=[ + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Topic :: Software Development', + ], + description='Run ament linters', + long_description='The auto-magic functions for ease to use of the ament linters in Python.', + license='Apache License, Version 2.0', + extras_require={ + 'test': [ + 'pytest', + ], + }, + entry_points={ + 'pytest11': [ + 'ament_lint_auto_py = ament_lint_auto_py.pytest_plugin', + ], + }, +) diff --git a/ament_lint_auto/ament_lint_auto/main.py b/ament_lint_auto_py/test/test_ament_lint_auto.py similarity index 65% rename from ament_lint_auto/ament_lint_auto/main.py rename to ament_lint_auto_py/test/test_ament_lint_auto.py index be45e4c9b..1bab52439 100644 --- a/ament_lint_auto/ament_lint_auto/main.py +++ b/ament_lint_auto_py/test/test_ament_lint_auto.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from importlib.metadata import entry_points +import pytest -linters = entry_points(group="ament_lint") -for ep in linters: - rc = ep.load()() - print(rc) +# TODO: AMENT_LINT_AUTO_EXCLUDE suppor +@pytest.mark.ament_lint_auto_py +@pytest.mark.linter +def test_ament_lint_auto(run_entry_point) -> None: + """Run all installed linter dynamically.""" + rc = run_entry_point() + assert rc == 0, f'Linter[{run_entry_point.NAME}] failed with exit code {rc}' diff --git a/ament_lint_cmake/ament_lint_cmake/main.py b/ament_lint_cmake/ament_lint_cmake/main.py index aa76fcbd0..dddd644d3 100755 --- a/ament_lint_cmake/ament_lint_cmake/main.py +++ b/ament_lint_cmake/ament_lint_cmake/main.py @@ -18,6 +18,7 @@ import os import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -32,6 +33,16 @@ def is_valid_file(filename): cmakelint.IsValidFile = is_valid_file +class LintCMakeRunner: + + NAME = 'ament_lint_cmake' + FILE_TYPES = ('CMakeLists.txt', '*.cmake') + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): parser = argparse.ArgumentParser( description='Check CMake code against the style conventions.', diff --git a/ament_lint_cmake/setup.py b/ament_lint_cmake/setup.py index 9d93996f0..39e1bf7d0 100644 --- a/ament_lint_cmake/setup.py +++ b/ament_lint_cmake/setup.py @@ -40,5 +40,8 @@ 'console_scripts': [ 'ament_lint_cmake = ament_lint_cmake.main:main', ], + 'ament_lint': [ + 'ament_lint_cmake = ament_lint_cmake.main:LintCMakeRunner' + ] }, ) diff --git a/ament_mypy/ament_mypy/main.py b/ament_mypy/ament_mypy/main.py index 082d5943a..f3c9e3e16 100755 --- a/ament_mypy/ament_mypy/main.py +++ b/ament_mypy/ament_mypy/main.py @@ -16,8 +16,8 @@ import argparse import os -import re import sys +import re import textwrap import time from typing import List, Match, Optional, Tuple @@ -27,6 +27,16 @@ import mypy.api +class MypyRunner: + + NAME = 'ament_mypy' + FILE_TYPES = ('*.py', '*.pyi') + + @staticmethod + def __call__() -> int: + return main() + + def main(argv: List[str] = sys.argv[1:]) -> int: """Command line tool for static type analysis with mypy.""" parser = argparse.ArgumentParser( diff --git a/ament_mypy/setup.py b/ament_mypy/setup.py index c67a9234f..42b8ee88c 100644 --- a/ament_mypy/setup.py +++ b/ament_mypy/setup.py @@ -46,7 +46,7 @@ 'ament_mypy = ament_mypy.pytest_marker', ], 'ament_lint': [ - 'ament_mypy = ament_mypy.main:main', + 'ament_mypy = ament_mypy.main:MypyRunner', ], }, ) diff --git a/ament_mypy/test/test_flake8.py b/ament_mypy/test/test_flake8.py deleted file mode 100644 index 83ea1c099..000000000 --- a/ament_mypy/test/test_flake8.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2019 Canonical, Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_flake8.main import main_with_errors - -import pytest - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc, errors = main_with_errors(argv=[]) - assert rc == 0, \ - 'Found %d code style errors / warnings:\n' % len(errors) + \ - '\n'.join(errors) diff --git a/ament_mypy/test/test_xmllint.py b/ament_mypy/test/test_xmllint.py deleted file mode 100644 index f46285e71..000000000 --- a/ament_mypy/test/test_xmllint.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2019 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ament_xmllint.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.xmllint -def test_xmllint(): - rc = main(argv=[]) - assert rc == 0, 'Found errors' diff --git a/ament_pclint/ament_pclint/main.py b/ament_pclint/ament_pclint/main.py index 960c58d63..846b97bb5 100755 --- a/ament_pclint/ament_pclint/main.py +++ b/ament_pclint/ament_pclint/main.py @@ -23,11 +23,28 @@ import subprocess import sys import time +from typing import Literal from xml.etree import ElementTree from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr +class PCLintRunner: + + NAME = 'ament_pclint' + FILE_TYPES = ( + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.c++', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): extensions = ['c', 'cc', 'cpp', 'cxx', 'c++'] diff --git a/ament_pclint/setup.py b/ament_pclint/setup.py index 7a19c0883..b9dfe53be 100644 --- a/ament_pclint/setup.py +++ b/ament_pclint/setup.py @@ -62,7 +62,7 @@ 'ament_pclint = ament_pclint.main:main', ], 'ament_lint': [ - 'ament_pclint = ament_pclint.main:main', + 'ament_pclint = ament_pclint.main:PCLintRunner', ], }, ) diff --git a/ament_pep257/ament_pep257/main.py b/ament_pep257/ament_pep257/main.py index a8f43800d..b4e4d2190 100755 --- a/ament_pep257/ament_pep257/main.py +++ b/ament_pep257/ament_pep257/main.py @@ -19,6 +19,7 @@ import os import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -55,6 +56,16 @@ ] +class Pep257Runner: + + NAME = 'ament_pep257' + FILE_TYPES = ('*.py') + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): parser = argparse.ArgumentParser( description='Check docstrings against the style conventions in PEP 257.', diff --git a/ament_pep257/setup.py b/ament_pep257/setup.py index 3181eb5bb..87fe4cbad 100644 --- a/ament_pep257/setup.py +++ b/ament_pep257/setup.py @@ -47,7 +47,7 @@ 'ament_pep257 = ament_pep257.pytest_marker', ], 'ament_lint': [ - 'ament_pep257 = ament_pep257.main:main', + 'ament_pep257 = ament_pep257.main:Pep257Runner', ], }, ) diff --git a/ament_pycodestyle/ament_pycodestyle/main.py b/ament_pycodestyle/ament_pycodestyle/main.py index 57b28a475..e9ff4b680 100755 --- a/ament_pycodestyle/ament_pycodestyle/main.py +++ b/ament_pycodestyle/ament_pycodestyle/main.py @@ -17,12 +17,23 @@ import argparse import os import sys +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr import pycodestyle +class PycodestyleRunner: + + NAME = 'ament_pycodestyle' + FILE_TYPES = ('*.py') + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): config_file = os.path.join( os.path.dirname(__file__), 'configuration', 'ament_pycodestyle.ini') diff --git a/ament_pycodestyle/setup.py b/ament_pycodestyle/setup.py index c7c35fe2a..6ad5789c6 100644 --- a/ament_pycodestyle/setup.py +++ b/ament_pycodestyle/setup.py @@ -44,7 +44,7 @@ 'ament_pycodestyle = ament_pycodestyle.main:main', ], 'ament_lint': [ - 'ament_pycodestyle = ament_pycodestyle.main:main', + 'ament_pycodestyle = ament_pycodestyle.main:PycodestyleRunner', ], }, ) diff --git a/ament_pyflakes/ament_pyflakes/main.py b/ament_pyflakes/ament_pyflakes/main.py index 1545c9c35..1a02d02ae 100755 --- a/ament_pyflakes/ament_pyflakes/main.py +++ b/ament_pyflakes/ament_pyflakes/main.py @@ -18,6 +18,7 @@ import os import sys import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr @@ -26,6 +27,16 @@ from pyflakes.reporter import Reporter +class PyflakesRunner: + + NAME = 'ament_pyflakes' + FILE_TYPES = ('*.py') + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): parser = argparse.ArgumentParser( description='Check code using pyflakes.', diff --git a/ament_pyflakes/setup.py b/ament_pyflakes/setup.py index 77d625708..94ea9547d 100644 --- a/ament_pyflakes/setup.py +++ b/ament_pyflakes/setup.py @@ -41,7 +41,7 @@ 'ament_pyflakes = ament_pyflakes.main:main', ], 'ament_lint': [ - 'ament_pyflakes = ament_pyflakes.main:main', + 'ament_pyflakes = ament_pyflakes.main:PyflakesRunner', ], }, ) diff --git a/ament_uncrustify/ament_uncrustify/main.py b/ament_uncrustify/ament_uncrustify/main.py index a3c3bf14e..a0274a3cc 100755 --- a/ament_uncrustify/ament_uncrustify/main.py +++ b/ament_uncrustify/ament_uncrustify/main.py @@ -27,10 +27,30 @@ import sys import tempfile import time +from typing import Literal from xml.sax.saxutils import escape from xml.sax.saxutils import quoteattr +class UncrustifyRunner: + + NAME = 'ament_uncrustify' + FILE_TYPES = ( + '*.c', + '*.cc', + '*.cpp', + '*.cxx', + '*.h', + '*.hh', + '*.hpp', + '*.hxx', + ) + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + def main(argv=sys.argv[1:]): uncrustify_bin = find_executable('uncrustify') if not uncrustify_bin: diff --git a/ament_uncrustify/setup.py b/ament_uncrustify/setup.py index 0ac219792..f730a3f6f 100644 --- a/ament_uncrustify/setup.py +++ b/ament_uncrustify/setup.py @@ -45,7 +45,7 @@ 'ament_uncrustify = ament_uncrustify.main:main', ], 'ament_lint': [ - 'ament_uncrustify = ament_uncrustify.main:main', + 'ament_uncrustify = ament_uncrustify.main:UncrustifyRunner', ], }, ) diff --git a/ament_xmllint/ament_xmllint/main.py b/ament_xmllint/ament_xmllint/main.py index e1a27a7a0..614074302 100755 --- a/ament_xmllint/ament_xmllint/main.py +++ b/ament_xmllint/ament_xmllint/main.py @@ -20,6 +20,7 @@ import subprocess import sys import time +from typing import Literal from xml.etree import ElementTree from xml.sax import make_parser from xml.sax import SAXParseException @@ -28,7 +29,17 @@ from xml.sax.saxutils import quoteattr -def main(argv=sys.argv[1:]): +class XmlLintRunner: + + NAME = 'ament_xmllint' + FILE_TYPES = ('*.xml') + + @staticmethod + def __call__() -> Literal[0, 1]: + return main() + + +def main(argv=sys.argv[1:]) -> Literal[0, 1]: default_extensions = ['xml'] parser = argparse.ArgumentParser( @@ -70,7 +81,8 @@ def main(argv=sys.argv[1:]): xmllint_bin = shutil.which('xmllint') if not xmllint_bin: - return "Could not find 'xmllint' executable" + print("Could not find 'xmllint' executable", file=sys.stderr) + return 1 report = [] diff --git a/ament_xmllint/setup.py b/ament_xmllint/setup.py index 4d64f91c4..5f5fb3ee2 100644 --- a/ament_xmllint/setup.py +++ b/ament_xmllint/setup.py @@ -44,7 +44,7 @@ 'ament_xmllint = ament_xmllint.pytest_marker', ], 'ament_lint': [ - 'ament_xmllint = ament_xmllint.main:main', + 'ament_xmllint = ament_xmllint.main:XmlLintRunner', ], }, ) From a2005082a59a9326cf4e434ce960438f3b3ac7b8 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sat, 24 Jan 2026 18:33:43 -0800 Subject: [PATCH 03/21] undo more accidentally changes Signed-off-by: Michael Carlstrom --- ament_mypy/ament_mypy/main.py | 2 +- ament_mypy/test/test_flake8.py | 26 ++++++++++++++++++++++++++ ament_mypy/test/test_xmllint.py | 23 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 ament_mypy/test/test_flake8.py create mode 100644 ament_mypy/test/test_xmllint.py diff --git a/ament_mypy/ament_mypy/main.py b/ament_mypy/ament_mypy/main.py index f3c9e3e16..a379caf7e 100755 --- a/ament_mypy/ament_mypy/main.py +++ b/ament_mypy/ament_mypy/main.py @@ -16,8 +16,8 @@ import argparse import os -import sys import re +import sys import textwrap import time from typing import List, Match, Optional, Tuple diff --git a/ament_mypy/test/test_flake8.py b/ament_mypy/test/test_flake8.py new file mode 100644 index 000000000..83ea1c099 --- /dev/null +++ b/ament_mypy/test/test_flake8.py @@ -0,0 +1,26 @@ +# Copyright 2019 Canonical, Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors + +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/ament_mypy/test/test_xmllint.py b/ament_mypy/test/test_xmllint.py new file mode 100644 index 000000000..f46285e71 --- /dev/null +++ b/ament_mypy/test/test_xmllint.py @@ -0,0 +1,23 @@ +# Copyright 2019 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_xmllint.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.xmllint +def test_xmllint(): + rc = main(argv=[]) + assert rc == 0, 'Found errors' From 0890137b11f59602b9303b00ca7ab795252f9e4e Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sat, 24 Jan 2026 18:43:25 -0800 Subject: [PATCH 04/21] remove changelog Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/doc/index.rst | 40 +-- .../test/test_ament_lint_auto.py | 1 - ament_lint_common_py/CHANGELOG.rst | 272 ------------------ ament_lint_common_py/doc/index.rst | 2 +- 4 files changed, 21 insertions(+), 294 deletions(-) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index c1e17e737..5fea0b310 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -2,18 +2,22 @@ ament_lint_auto_py =============== .. TODO -The package simplifies using multiple linters as part of the CMake tests. -It reduces the amount of CMake code to a bare minimum. +The package simplifies using multiple linters as part of pytest tests. +It reduces the amount of Python code to a bare minimum. -``CMakeLists.txt``: +``test_ament_lint_auto`: -.. code:: cmake +.. code:: python - # this must happen before the invocation of ament_package() - if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - ament_lint_auto_find_test_dependencies() - endif() + import pytest + + + @pytest.mark.ament_lint_auto_py + @pytest.mark.linter + def test_ament_lint_auto(run_entry_point) -> None: + """Run all installed linter dynamically.""" + rc = run_entry_point() + assert rc == 0, f'Linter[{run_entry_point.NAME}] failed with exit code {rc}' The set of linters to be used is then only specified in the package manifest as test dependencies. @@ -22,12 +26,12 @@ test dependencies. .. code:: xml - ament_lint_auto + ament_lint_auto_py - ament_cmake_clang_format - ament_cmake_cppcheck - ament_cmake_pycodestyle + ament_clang_format + ament_cppcheck + ament_pycodestyle Since recursive dependencies are also being used a single test dependency is sufficient to test with a set of common linters. @@ -36,16 +40,12 @@ sufficient to test with a set of common linters. .. code:: xml - ament_lint_auto + ament_lint_auto_py - ament_lint_common - -The documentation of the package `ament_cmake_test -`_ provides more information on testing -in CMake ament packages. - + ament_lint_common_py +.. TODO How to exclude linter modules with ament_lint_auto? --------------------------------------------------- diff --git a/ament_lint_auto_py/test/test_ament_lint_auto.py b/ament_lint_auto_py/test/test_ament_lint_auto.py index 1bab52439..cf78e939e 100644 --- a/ament_lint_auto_py/test/test_ament_lint_auto.py +++ b/ament_lint_auto_py/test/test_ament_lint_auto.py @@ -15,7 +15,6 @@ import pytest -# TODO: AMENT_LINT_AUTO_EXCLUDE suppor @pytest.mark.ament_lint_auto_py @pytest.mark.linter def test_ament_lint_auto(run_entry_point) -> None: diff --git a/ament_lint_common_py/CHANGELOG.rst b/ament_lint_common_py/CHANGELOG.rst index b47523654..b33076449 100644 --- a/ament_lint_common_py/CHANGELOG.rst +++ b/ament_lint_common_py/CHANGELOG.rst @@ -5,275 +5,3 @@ Changelog for package ament_lint_common_py 0.20.3 (2025-11-24) ------------------- -0.20.2 (2025-07-30) -------------------- - -0.20.1 (2025-06-19) -------------------- -* Fix cmake deprecation (`#539 `_) -* Contributors: mosfet80 - -0.20.0 (2025-04-24) -------------------- - -0.19.2 (2025-04-17) -------------------- - -0.19.1 (2025-02-02) -------------------- - -0.19.0 (2024-11-20) -------------------- - -0.18.1 (2024-06-17) -------------------- - -0.18.0 (2024-04-26) -------------------- - -0.17.0 (2024-04-16) -------------------- - -0.16.4 (2024-03-28) -------------------- -* Update maintainer list in package.xml files (`#474 `_) -* Contributors: Michael Jeronimo - -0.16.3 (2024-02-07) -------------------- - -0.16.2 (2023-12-26) -------------------- - -0.16.1 (2023-11-06) -------------------- - -0.16.0 (2023-10-04) -------------------- - -0.15.2 (2023-07-11) -------------------- - -0.15.1 (2023-06-07) -------------------- - -0.15.0 (2023-04-27) -------------------- - -0.14.1 (2023-04-11) -------------------- - -0.14.0 (2023-02-14) -------------------- -* [rolling] Update maintainers - 2022-11-07 (`#421 `_) -* Contributors: Audrow Nash - -0.13.2 (2022-09-13) -------------------- -* Update maintainers (`#379 `_) -* Contributors: methylDragon - -0.13.1 (2022-05-09) -------------------- - -0.13.0 (2022-04-29) -------------------- - -0.12.3 (2022-04-08) -------------------- - -0.12.2 (2022-03-28) -------------------- - -0.12.1 (2022-03-01) -------------------- - -0.12.0 (2022-02-18) -------------------- - -0.11.4 (2022-01-14) -------------------- -* Update forthcoming version in changelogs -* Contributors: Audrow Nash - -0.11.3 (2022-01-14) -------------------- -* Update maintainers to Michael Jeronimo and Michel Hidalgo (`#340 `_) -* Fix typo in ament_lint_common/package.xml (`#336 `_) -* Contributors: Audrow Nash, Kenji Miyake - -0.11.2 (2021-06-18) -------------------- - -0.11.1 (2021-06-18) -------------------- - -0.11.0 (2021-06-18) -------------------- - -0.10.6 (2021-05-06) -------------------- - -0.10.5 (2021-04-14) -------------------- -* Remove Claire as a maintainer. (`#312 `_) - * Remove Claire as a maintainer. - * Remove dead email addresses. - * Remove more dead email addresses. - * Switch setup.py maintainer to Audrow. -* Contributors: Chris Lalancette - -0.10.4 (2021-03-18) -------------------- - -0.10.3 (2021-03-03) -------------------- -* 0.10.3 -* Contributors: Audrow Nash - -0.10.2 (2021-02-12) -------------------- -* Add Audrow as a maintainer (`#294 `_) -* Drop trailing tab from package manifests (`#291 `_) - Follow-up to 8bf194aa1ac282db5483dd0d3fefff8f325b0db8 -* Contributors: Audrow Nash, Scott K Logan - -0.10.1 (2021-01-25) -------------------- -* Update maintainer (`#274 `_) - * update maintainer - * add authors -* Contributors: Claire Wang - -0.10.0 (2020-09-18) -------------------- - -0.9.4 (2020-05-26) ------------------- - -0.9.3 (2020-05-18) ------------------- - -0.9.2 (2020-05-08) ------------------- - -0.9.1 (2020-04-10) ------------------- -* 0.9.0 -* add ament_lint_common doc (`#218 `_) - * adding ament_lint_common doc - * add links to common linters - * change ament_uncrustify link -* Contributors: Chris Lalancette, Marya Belanger - -0.8.1 (2019-10-23) ------------------- - -0.8.0 (2019-09-17) ------------------- - -0.7.4 (2019-07-31) ------------------- -* Add clang tidy to ament linters (`#155 `_) - * Basic clang-lint functionality - * Add clang tidy cmake dir - * Add Google/default config file - * Add xunit output - * Add lint tests - * Update copyright, maintainer, verbiage -* Contributors: John - -0.7.3 (2019-05-09 14:08) ------------------------- - -0.7.2 (2019-05-09 09:30) ------------------------- - -0.7.1 (2019-05-07) ------------------- - -0.7.0 (2019-04-11) ------------------- - -0.6.3 (2019-01-14) ------------------- - -0.6.2 (2018-12-06) ------------------- - -0.6.1 (2018-11-15) ------------------- - -0.6.0 (2018-11-14) ------------------- -* add ament\_(cmake\_)xmllint packages (`#104 `_) - * add ament\_(cmake\_)xmllint packages - * consider xsi:noNamespaceSchemaLocation of root tag -* Contributors: Dirk Thomas - -0.5.2 (2018-06-27) ------------------- - -0.5.1 (2018-06-18 13:47) ------------------------- - -0.5.0 (2018-06-18 10:09) ------------------------- - -0.4.0 (2017-12-08) ------------------- -* 0.0.3 -* 0.0.2 -* Test python module import order using flake8 (`#63 `_) - * Add flake8 linter - * Don't deal with flake8-import-order just yet - * Debugging prints - * Reinstate import order rule - * Fix reporting bug by using the inner flake8 style guide - * Fixup - * Add comment on wrapper StyleGuide use - * use flake8 v3 (`#1 `_) - * Reorder package.xml - * Get the filenames from the file checkers because input_file isn't called by flake8 anymore - * Output count of all error types - * Get flake8 to use the config file - The current implementation of get_style_guide does not process the config file correctly. - * Error when flake8 v2 found - * Print errors like pep8 - * remove __future_\_ imports - * add schema to manifest files - * Support flake8 v2 as well as v3 - * Output checked files - otherwise it's not present in xunit files for tests run directly with nose (not ament_cmake_flake8) - * Prevent v2 imports from happening on systems with v3 - * Flake8 replaces pep8+pyflakes -* update schema url -* add schema to manifest files -* Merge pull request `#56 `_ from ament/cmake35 - require CMake 3.5 -* require CMake 3.5 -* Merge pull request `#39 `_ from ament/cpplint_python3 - use Python 3 for cpplint and split into Python and CMake package -* refactor ament_cpplint into Python and CMake package -* Merge pull request `#36 `_ from ament/enable_cpplint - add ament_cpplint as default linter -* add ament_cpplint as default linter -* Merge pull request `#28 `_ from ament/pep257 - add packages to check pep257 compliance -* use ament_cmake_pep257 as a common linter -* add explicit build type -* Merge pull request `#19 `_ from ament/split_linter_packages_in_python_and_cmake - split linter packages in python and cmake -* move cmake part of ament_uncrustify to ament_cmake_uncrustify -* move cmake part of ament_pyflakes to ament_cmake_pyflakes -* move cmake part of ament_pep8 to ament_cmake_pep8 -* move cmake part of ament_lint_cmake to ament_cmake_lint_cmake -* move cmake part of ament_cppcheck to ament_cmake_cppcheck -* Merge pull request `#8 `_ from ament/ament_copyright - add more options to ament_copyright -* add more options to ament_copyright -* disable clang_format and cpplint for now (fix `ros2/ros2#15 `_) -* Merge pull request `#2 `_ from ament/ament_lint_auto - allow linting based on test dependencies only -* add ament_lint_auto and ament_lint_common, update all linter packages to implement extension point of ament_lint_auto -* Contributors: Dirk Thomas, dhood diff --git a/ament_lint_common_py/doc/index.rst b/ament_lint_common_py/doc/index.rst index aa6e506b0..c7c9d218b 100644 --- a/ament_lint_common_py/doc/index.rst +++ b/ament_lint_common_py/doc/index.rst @@ -11,7 +11,7 @@ A mechanism for running the following set of common linters: * `ament_flake8 `_ : a style checker for Python files -* `ament_lint `_ : a cmake linter +* `ament_cmake_lint `_ : a cmake linter * `ament_pep257 `_ : a style checker for Python docstrings From 28d55ab4755937eee5f8705f13294609c28bf023 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sat, 24 Jan 2026 20:05:26 -0800 Subject: [PATCH 05/21] make runner stateful for AMENT_LINT_AUTO_FILE_EXCLUDE Signed-off-by: Michael Carlstrom --- ament_clang_format/ament_clang_format/main.py | 8 +-- ament_clang_tidy/ament_clang_tidy/main.py | 8 +-- ament_copyright/ament_copyright/main.py | 8 +-- ament_cppcheck/ament_cppcheck/main.py | 8 +-- ament_cpplint/ament_cpplint/main.py | 8 +-- ament_flake8/ament_flake8/main.py | 8 +-- .../ament_lint_auto_py/pytest_plugin.py | 21 +++++++- ament_lint_auto_py/doc/index.rst | 53 ++++--------------- ament_lint_cmake/ament_lint_cmake/main.py | 9 ++-- ament_lint_common_py/doc/index.rst | 2 +- ament_mypy/ament_mypy/main.py | 8 +-- ament_pclint/ament_pclint/main.py | 9 ++-- ament_pep257/ament_pep257/main.py | 8 +-- ament_pycodestyle/ament_pycodestyle/main.py | 9 ++-- ament_pyflakes/ament_pyflakes/main.py | 9 ++-- ament_uncrustify/ament_uncrustify/main.py | 9 ++-- ament_xmllint/ament_xmllint/main.py | 9 ++-- 17 files changed, 107 insertions(+), 87 deletions(-) diff --git a/ament_clang_format/ament_clang_format/main.py b/ament_clang_format/ament_clang_format/main.py index fd245537a..b3d57ee98 100755 --- a/ament_clang_format/ament_clang_format/main.py +++ b/ament_clang_format/ament_clang_format/main.py @@ -41,9 +41,11 @@ class ClangFormatRunner: '*.hxx', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_clang_tidy/ament_clang_tidy/main.py b/ament_clang_tidy/ament_clang_tidy/main.py index a264e4b76..bd928c2a4 100755 --- a/ament_clang_tidy/ament_clang_tidy/main.py +++ b/ament_clang_tidy/ament_clang_tidy/main.py @@ -46,9 +46,11 @@ class ClangTidyRunner: '*.hxx', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_copyright/ament_copyright/main.py b/ament_copyright/ament_copyright/main.py index 138b1d03e..7a14cc85b 100644 --- a/ament_copyright/ament_copyright/main.py +++ b/ament_copyright/ament_copyright/main.py @@ -44,9 +44,11 @@ class CopyrightRunner: '*', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_cppcheck/ament_cppcheck/main.py b/ament_cppcheck/ament_cppcheck/main.py index b8f8c0383..f83bc762e 100755 --- a/ament_cppcheck/ament_cppcheck/main.py +++ b/ament_cppcheck/ament_cppcheck/main.py @@ -66,9 +66,11 @@ class CPPCheckRunner: '*.hxx', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_cpplint/ament_cpplint/main.py b/ament_cpplint/ament_cpplint/main.py index 04860bdd7..38afcd3f9 100755 --- a/ament_cpplint/ament_cpplint/main.py +++ b/ament_cpplint/ament_cpplint/main.py @@ -84,9 +84,11 @@ class CPPLintRunner: '*.hxx', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_flake8/ament_flake8/main.py b/ament_flake8/ament_flake8/main.py index b5f457923..b72f800ce 100755 --- a/ament_flake8/ament_flake8/main.py +++ b/ament_flake8/ament_flake8/main.py @@ -33,9 +33,11 @@ class Flake8Runner: NAME = 'ament_flake8' FILE_TYPES = ('*.py', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py index 14e1337a9..e698d8a92 100644 --- a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py +++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py @@ -15,6 +15,7 @@ from collections.abc import Callable from importlib.metadata import entry_points from importlib.metadata import EntryPoint +import os from pathlib import Path import sys @@ -29,6 +30,9 @@ def pytest_configure(config: Config) -> None: def pytest_generate_tests(metafunc: Metafunc) -> None: + AMENT_LINT_AUTO_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_EXCLUDE', '') + EXCLUDED_LINTERS = {name.strip() for name in AMENT_LINT_AUTO_EXCLUDE.split(';') if name} + if 'ament_lint_ep' in metafunc.fixturenames: linters = entry_points(group='ament_lint') @@ -40,6 +44,11 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: name = func.NAME file_types = func.FILE_TYPES + if name in EXCLUDED_LINTERS: + print(f'Skipping {name} because it is in ' + f'AMENT_LINT_AUTO_EXCLUDE:={AMENT_LINT_AUTO_EXCLUDE}.') + continue + paths: list[Path] = [] for ext in file_types: paths.extend(Path('.').rglob(f'{ext}')) @@ -57,5 +66,13 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: @pytest.fixture def run_entry_point(ament_lint_ep: EntryPoint) -> Callable[[], int]: func = ament_lint_ep.load() - sys.argv = [func.NAME] - return func() + + AMENT_LINT_AUTO_FILE_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_FILE_EXCLUDE', '') + EXCLUDED_FILE_GLOBS = {name.strip() for + name in AMENT_LINT_AUTO_FILE_EXCLUDE.split(';') if name} + if EXCLUDED_FILE_GLOBS: + args = ['--exclude'] + sys.argv.extend(EXCLUDED_FILE_GLOBS) + return func(args) + + return func([]) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index 5fea0b310..58d67f60c 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -1,11 +1,10 @@ ament_lint_auto_py =============== -.. TODO The package simplifies using multiple linters as part of pytest tests. It reduces the amount of Python code to a bare minimum. -``test_ament_lint_auto`: +``test_ament_lint_auto.py`: .. code:: python @@ -45,60 +44,30 @@ sufficient to test with a set of common linters. ament_lint_common_py -.. TODO + How to exclude linter modules with ament_lint_auto? --------------------------------------------------- -Linter modules can be excluded via the CMake list variable `AMENT_LINT_AUTO_EXCLUDE`. +Linter modules can be excluded via the environment variable `AMENT_LINT_AUTO_EXCLUDE`. As an example to exclude the `copyright` linter: -``CMakeLists.txt``: - -.. code:: cmake - - # this must happen before the invocation of ament_package() - if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_copyright) - ament_lint_auto_find_test_dependencies() - endif() +.. code:: bash + export AMENT_LINT_EXCLUDE="ament_copyright;" How to exclude files with ament_lint_auto? ------------------------------------------ Linter hooks shall conform to the ament_lint_auto convention of excluding files -specified in the CMake list variable `AMENT_LINT_AUTO_FILE_EXCLUDE`. -As such, the CMake snippet from above can be modified to exclude files across -all linters with one addition. - -``CMakeLists.txt``: - -.. code:: cmake +specified in the environment list variable `AMENT_LINT_AUTO_FILE_EXCLUDE`. - # this must happen before the invocation of ament_package() - if(BUILD_TESTING) - find_package(ament_lint_auto REQUIRED) - set(AMENT_LINT_AUTO_FILE_EXCLUDE /path/to/ignored_file ...) - ament_lint_auto_find_test_dependencies() - endif() +.. code:: bash + export AMENT_LINT_EXCLUDE="/path/to/ignored_file" For a more specific example, this excludes all python files matching a pattern using globbing. Multiple expressions can be combined on multiple lines. -It might be a good idea to issue a warning to developers that linting is disabled -if you plan to enable it at some point. - -.. code:: cmake - - file(GLOB_RECURSE AMENT_LINT_AUTO_FILE_EXCLUDE - # Exclude all the python files in src directory - src/*.py - # Exclude all the c++ implementation files in test directory - test/*.cpp - ) - message(AUTHOR_WARNING - "Ament lint auto tests are disabled on the following: " - ${AMENT_LINT_AUTO_FILE_EXCLUDE} - ) +.. code:: bash + .. code:: bash + export AMENT_LINT_EXCLUDE="src/*.py;test/*.cpp" diff --git a/ament_lint_cmake/ament_lint_cmake/main.py b/ament_lint_cmake/ament_lint_cmake/main.py index dddd644d3..cbf8f1860 100755 --- a/ament_lint_cmake/ament_lint_cmake/main.py +++ b/ament_lint_cmake/ament_lint_cmake/main.py @@ -38,9 +38,12 @@ class LintCMakeRunner: NAME = 'ament_lint_cmake' FILE_TYPES = ('CMakeLists.txt', '*.cmake') - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_lint_common_py/doc/index.rst b/ament_lint_common_py/doc/index.rst index c7c9d218b..a64cc97d0 100644 --- a/ament_lint_common_py/doc/index.rst +++ b/ament_lint_common_py/doc/index.rst @@ -19,4 +19,4 @@ A mechanism for running the following set of common linters: * `ament_xmllint `_ : an xml linter -The `ament_lint_auto `_ documentation provides information on using ament_lint_common. +The `ament_lint_auto_py `_ documentation provides information on using ament_lint_common_py. diff --git a/ament_mypy/ament_mypy/main.py b/ament_mypy/ament_mypy/main.py index a379caf7e..7d4f61841 100755 --- a/ament_mypy/ament_mypy/main.py +++ b/ament_mypy/ament_mypy/main.py @@ -32,9 +32,11 @@ class MypyRunner: NAME = 'ament_mypy' FILE_TYPES = ('*.py', '*.pyi') - @staticmethod - def __call__() -> int: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> int: + return main(self.args) def main(argv: List[str] = sys.argv[1:]) -> int: diff --git a/ament_pclint/ament_pclint/main.py b/ament_pclint/ament_pclint/main.py index 846b97bb5..3c1e23008 100755 --- a/ament_pclint/ament_pclint/main.py +++ b/ament_pclint/ament_pclint/main.py @@ -40,9 +40,12 @@ class PCLintRunner: '*.c++', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_pep257/ament_pep257/main.py b/ament_pep257/ament_pep257/main.py index b4e4d2190..4bc4818ec 100755 --- a/ament_pep257/ament_pep257/main.py +++ b/ament_pep257/ament_pep257/main.py @@ -61,9 +61,11 @@ class Pep257Runner: NAME = 'ament_pep257' FILE_TYPES = ('*.py') - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_pycodestyle/ament_pycodestyle/main.py b/ament_pycodestyle/ament_pycodestyle/main.py index e9ff4b680..80db0d8f6 100755 --- a/ament_pycodestyle/ament_pycodestyle/main.py +++ b/ament_pycodestyle/ament_pycodestyle/main.py @@ -29,9 +29,12 @@ class PycodestyleRunner: NAME = 'ament_pycodestyle' FILE_TYPES = ('*.py') - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_pyflakes/ament_pyflakes/main.py b/ament_pyflakes/ament_pyflakes/main.py index 1a02d02ae..87a7b4114 100755 --- a/ament_pyflakes/ament_pyflakes/main.py +++ b/ament_pyflakes/ament_pyflakes/main.py @@ -32,9 +32,12 @@ class PyflakesRunner: NAME = 'ament_pyflakes' FILE_TYPES = ('*.py') - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_uncrustify/ament_uncrustify/main.py b/ament_uncrustify/ament_uncrustify/main.py index a0274a3cc..41047095a 100755 --- a/ament_uncrustify/ament_uncrustify/main.py +++ b/ament_uncrustify/ament_uncrustify/main.py @@ -46,9 +46,12 @@ class UncrustifyRunner: '*.hxx', ) - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]): diff --git a/ament_xmllint/ament_xmllint/main.py b/ament_xmllint/ament_xmllint/main.py index 614074302..2c378bc48 100755 --- a/ament_xmllint/ament_xmllint/main.py +++ b/ament_xmllint/ament_xmllint/main.py @@ -34,9 +34,12 @@ class XmlLintRunner: NAME = 'ament_xmllint' FILE_TYPES = ('*.xml') - @staticmethod - def __call__() -> Literal[0, 1]: - return main() + def __init__(self, args: list[str]) -> None: + self.args = args + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) def main(argv=sys.argv[1:]) -> Literal[0, 1]: From e03c19f0a37adfdf95e23abecf3949960d1550e8 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 25 Jan 2026 08:54:03 -0800 Subject: [PATCH 06/21] Add docs about 3rd party linters Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/doc/index.rst | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index 58d67f60c..8c146bcc9 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -45,7 +45,7 @@ sufficient to test with a set of common linters. ament_lint_common_py -How to exclude linter modules with ament_lint_auto? +How to exclude linter modules with ament_lint_auto_py? --------------------------------------------------- Linter modules can be excluded via the environment variable `AMENT_LINT_AUTO_EXCLUDE`. @@ -56,7 +56,7 @@ As an example to exclude the `copyright` linter: export AMENT_LINT_EXCLUDE="ament_copyright;" -How to exclude files with ament_lint_auto? +How to exclude files with ament_lint_auto_py? ------------------------------------------ Linter hooks shall conform to the ament_lint_auto convention of excluding files @@ -71,3 +71,36 @@ Multiple expressions can be combined on multiple lines. .. code:: bash .. code:: bash export AMENT_LINT_EXCLUDE="src/*.py;test/*.cpp" + + +How to register 3rd party linters with ament_lint_auto_py? +--------------------------------------------------------- + +To register a third party linter implement class like the following. + +.. code:: python + + class CustomRunner: + + NAME = 'ament_custom' + FILE_TYPES = ('*.py') + + def __init__(self, args: list[str]) -> None: + self.args = args + + def __call__(self) -> Literal[0, 1]: + return main(self.args) + + def main(): + # Custom linting + pass + +Then in a `setup.py` register an entry point for the `CustomRunner`. + +.. code:: python + + entry_points={ + 'ament_lint': [ + 'ament_lint_cmake = ament_lint_cmake.main:LintCMakeRunner' + ] + }, From 53bf4f4d2a3a5c04dbdd626f2c956af7a7c8ccfa Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 25 Jan 2026 10:02:26 -0800 Subject: [PATCH 07/21] allow for ament_lint* metapackages Signed-off-by: Michael Carlstrom --- .../ament_lint_auto_py/pytest_plugin.py | 90 +++++++++++++++++-- ament_lint_auto_py/package.xml | 1 + 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py index e698d8a92..356ff9fcf 100644 --- a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py +++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py @@ -18,31 +18,103 @@ import os from pathlib import Path import sys +from typing import Final +import xml.etree.ElementTree as ET + +from ament_index_python.packages import get_package_share_directory import pytest from pytest import Config from pytest import Metafunc +DEPEND_TAGS: Final = ( + 'depend', + 'build_depend', + 'test_depend', + 'exec_depend', +) + +PACKAGE_XML: Final = 'package.xml' + + +def should_expand_package(name: str) -> bool: + return name.startswith('ament_lint') + + +def get_package_xml_for_package(pkg_name: str) -> Path: + return Path(get_package_share_directory(pkg_name)) / PACKAGE_XML + + +def get_depends_recursive( + pkg_xml: Path, + seen: set[str] | None = None, +) -> set[str]: + seen = seen or set() + + tree = ET.parse(pkg_xml) + root = tree.getroot() + + deps: set[str] = set() + + for tag in DEPEND_TAGS: + for dep in root.findall(tag): + if not dep.text: + continue + + name = dep.text.strip() + if name in seen: + continue + + seen.add(name) + deps.add(name) + + if should_expand_package(name): + dep_xml = get_package_xml_for_package(name) + deps |= get_depends_recursive(dep_xml, seen) + + return deps + + +def find_package_xml(start: Path) -> Path | None: + for parent in [start, *start.parents]: + pkg_xml = parent / PACKAGE_XML + if pkg_xml.is_file(): + return pkg_xml + return None + + def pytest_configure(config: Config) -> None: config.addinivalue_line('markers', 'ament_lint_auto_py: marks tests as running all linter checks') def pytest_generate_tests(metafunc: Metafunc) -> None: - AMENT_LINT_AUTO_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_EXCLUDE', '') - EXCLUDED_LINTERS = {name.strip() for name in AMENT_LINT_AUTO_EXCLUDE.split(';') if name} - if 'ament_lint_ep' in metafunc.fixturenames: + AMENT_LINT_AUTO_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_EXCLUDE', '') + EXCLUDED_LINTERS = {name.strip() for name in AMENT_LINT_AUTO_EXCLUDE.split(';') if name} + + test_file = Path(metafunc.definition.path) + pkg_xml = find_package_xml(test_file) + + if pkg_xml is None: + print('No package.xml found. Is this a ROS package?') + return + + effective_depends = get_depends_recursive(pkg_xml) linters = entry_points(group='ament_lint') filtered_linters: list[EntryPoint] = [] ids: list[int] = [] for ep in linters: - func = ep.load() - name = func.NAME - file_types = func.FILE_TYPES + runner = ep.load() + name = runner.NAME + file_types = runner.FILE_TYPES + + if name not in effective_depends: + print(f'Skipping {name} because it was not found in the package.xml') + continue if name in EXCLUDED_LINTERS: print(f'Skipping {name} because it is in ' @@ -65,7 +137,7 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: @pytest.fixture def run_entry_point(ament_lint_ep: EntryPoint) -> Callable[[], int]: - func = ament_lint_ep.load() + runner = ament_lint_ep.load() AMENT_LINT_AUTO_FILE_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_FILE_EXCLUDE', '') EXCLUDED_FILE_GLOBS = {name.strip() for @@ -73,6 +145,6 @@ def run_entry_point(ament_lint_ep: EntryPoint) -> Callable[[], int]: if EXCLUDED_FILE_GLOBS: args = ['--exclude'] sys.argv.extend(EXCLUDED_FILE_GLOBS) - return func(args) + return runner(args) - return func([]) + return runner([]) diff --git a/ament_lint_auto_py/package.xml b/ament_lint_auto_py/package.xml index fd2e3013c..136d51c9f 100644 --- a/ament_lint_auto_py/package.xml +++ b/ament_lint_auto_py/package.xml @@ -12,6 +12,7 @@ Audrow Nash + ament_index_python python3-pytest ament_lint_common_py From 8b282298fb590336300c452461d8c68f99d1e76f Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 25 Jan 2026 15:50:03 -0800 Subject: [PATCH 08/21] add pytest config options, and auto test detection Signed-off-by: Michael Carlstrom --- .../ament_lint_auto_py/pytest_plugin.py | 226 ++++++++---------- .../ament_lint_auto_py/xml_helpers.py | 86 +++++++ ament_lint_auto_py/doc/index.rst | 43 ++-- ament_lint_auto_py/package.xml | 5 +- .../test/test_ament_lint_auto.py | 23 -- 5 files changed, 210 insertions(+), 173 deletions(-) create mode 100644 ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py delete mode 100644 ament_lint_auto_py/test/test_ament_lint_auto.py diff --git a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py index 356ff9fcf..cba1766ca 100644 --- a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py +++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py @@ -12,139 +12,109 @@ # See the License for the specific language governing permissions and # limitations under the License. -from collections.abc import Callable from importlib.metadata import entry_points from importlib.metadata import EntryPoint -import os from pathlib import Path -import sys -from typing import Final -import xml.etree.ElementTree as ET +from typing import Literal - -from ament_index_python.packages import get_package_share_directory -import pytest +from ament_lint_auto_py.xml_helpers import find_package_xml +from ament_lint_auto_py.xml_helpers import get_depends_recursive +from ament_lint_auto_py.xml_helpers import package_has_ament_lint_auto_py from pytest import Config -from pytest import Metafunc - - -DEPEND_TAGS: Final = ( - 'depend', - 'build_depend', - 'test_depend', - 'exec_depend', -) - -PACKAGE_XML: Final = 'package.xml' - - -def should_expand_package(name: str) -> bool: - return name.startswith('ament_lint') - - -def get_package_xml_for_package(pkg_name: str) -> Path: - return Path(get_package_share_directory(pkg_name)) / PACKAGE_XML - - -def get_depends_recursive( - pkg_xml: Path, - seen: set[str] | None = None, -) -> set[str]: - seen = seen or set() - - tree = ET.parse(pkg_xml) - root = tree.getroot() - - deps: set[str] = set() - - for tag in DEPEND_TAGS: - for dep in root.findall(tag): - if not dep.text: - continue - - name = dep.text.strip() - if name in seen: - continue - - seen.add(name) - deps.add(name) - - if should_expand_package(name): - dep_xml = get_package_xml_for_package(name) - deps |= get_depends_recursive(dep_xml, seen) - - return deps - - -def find_package_xml(start: Path) -> Path | None: - for parent in [start, *start.parents]: - pkg_xml = parent / PACKAGE_XML - if pkg_xml.is_file(): - return pkg_xml - return None +from pytest import Item +from pytest import Parser +from pytest import Session def pytest_configure(config: Config) -> None: - config.addinivalue_line('markers', - 'ament_lint_auto_py: marks tests as running all linter checks') - - -def pytest_generate_tests(metafunc: Metafunc) -> None: - if 'ament_lint_ep' in metafunc.fixturenames: - AMENT_LINT_AUTO_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_EXCLUDE', '') - EXCLUDED_LINTERS = {name.strip() for name in AMENT_LINT_AUTO_EXCLUDE.split(';') if name} - - test_file = Path(metafunc.definition.path) - pkg_xml = find_package_xml(test_file) - - if pkg_xml is None: - print('No package.xml found. Is this a ROS package?') - return - - effective_depends = get_depends_recursive(pkg_xml) - linters = entry_points(group='ament_lint') - - filtered_linters: list[EntryPoint] = [] - ids: list[int] = [] - - for ep in linters: - runner = ep.load() - name = runner.NAME - file_types = runner.FILE_TYPES - - if name not in effective_depends: - print(f'Skipping {name} because it was not found in the package.xml') - continue - - if name in EXCLUDED_LINTERS: - print(f'Skipping {name} because it is in ' - f'AMENT_LINT_AUTO_EXCLUDE:={AMENT_LINT_AUTO_EXCLUDE}.') - continue - - paths: list[Path] = [] - for ext in file_types: - paths.extend(Path('.').rglob(f'{ext}')) - - if not paths: - print(f'Skipping {name} because no files of type {file_types} exist.') - continue - - filtered_linters.append(ep) - ids.append(name) - - metafunc.parametrize('ament_lint_ep', filtered_linters, ids=ids) - - -@pytest.fixture -def run_entry_point(ament_lint_ep: EntryPoint) -> Callable[[], int]: - runner = ament_lint_ep.load() - - AMENT_LINT_AUTO_FILE_EXCLUDE = os.environ.get('AMENT_LINT_AUTO_FILE_EXCLUDE', '') - EXCLUDED_FILE_GLOBS = {name.strip() for - name in AMENT_LINT_AUTO_FILE_EXCLUDE.split(';') if name} - if EXCLUDED_FILE_GLOBS: - args = ['--exclude'] - sys.argv.extend(EXCLUDED_FILE_GLOBS) - return runner(args) - - return runner([]) + config.addinivalue_line( + 'markers', 'ament_lint_auto_py: marks tests as running all linter checks' + ) + + +class AmentLintItem(Item): + def __init__( + self, + *, + entry_point: EntryPoint, + file_excludes: list[str], + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.entry_point = entry_point + self.file_excludes = file_excludes + + def runtest(self) -> None: + runner = self.entry_point.load() + + args: list[str] = [] + if self.file_excludes: + args.append('--exclude') + args.extend(self.file_excludes) + + rc = runner(args)() + if rc != 0: + raise AssertionError( + f'Linter[{runner.NAME}] failed with exit code {rc}' + ) + + def reportinfo(self) -> tuple[Path, Literal[0], str]: + return self.path, 0, f'ament_lint: {self.name}' + + +def pytest_collection_modifyitems(session: Session, config: Config, items: list[Item]) -> None: + excluded = set(config.getini('ament_lint_auto_exclude')) + file_excludes = config.getini('ament_lint_auto_file_exclude') + + pkg_xml = find_package_xml(config.rootpath) + if pkg_xml is None: + return # Not in a ROS package + + if not package_has_ament_lint_auto_py(pkg_xml): + return # Package does not opt-in + + effective_depends = get_depends_recursive(pkg_xml) + linters = entry_points(group='ament_lint') + + for ep in linters: + runner = ep.load() + + if runner.NAME not in effective_depends: + continue # skipping linter if not declared in depends of a package.xmk + + if runner.NAME in excluded: + continue # skipping linter if declared in ament_lint_auto_exclude + + # skip linters if no matching files exist + found_file = False + for pattern in runner.FILE_TYPES: + if any(Path('.').rglob(pattern)): + found_file = True + break + if not found_file: + continue + + items.append( + AmentLintItem.from_parent( + parent=session, + name=runner.NAME, + entry_point=ep, + file_excludes=file_excludes, + path=config.rootpath, + ) + ) + + +def pytest_addoption(parser: Parser): + parser.addini( + 'ament_lint_auto_exclude', + 'Linters to exclude from ament_lint_auto_py', + type='linelist', + default=[], + ) + parser.addini( + 'ament_lint_auto_file_exclude', + 'File globs to exclude from ament linters', + type='linelist', + default=[], + ) diff --git a/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py b/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py new file mode 100644 index 000000000..02534f3d2 --- /dev/null +++ b/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py @@ -0,0 +1,86 @@ +# Copyright 2026 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from typing import Final +import xml.etree.ElementTree as ET + + +from ament_index_python.packages import get_package_share_directory + + +DEPEND_TAGS: Final = ( + 'depend', + 'build_depend', + 'test_depend', + 'exec_depend', +) + +PACKAGE_XML: Final = 'package.xml' + + +def should_expand_package(name: str) -> bool: + return name.startswith('ament_lint') + + +def get_package_xml_for_package(pkg_name: str) -> Path: + return Path(get_package_share_directory(pkg_name)) / PACKAGE_XML + + +def get_depends_recursive( + pkg_xml: Path, + seen: set[str] | None = None, +) -> set[str]: + seen = seen or set() + + tree = ET.parse(pkg_xml) + root = tree.getroot() + + deps: set[str] = set() + + for tag in DEPEND_TAGS: + for dep in root.findall(tag): + if not dep.text: + continue + + name = dep.text.strip() + if name in seen: + continue + + seen.add(name) + deps.add(name) + + if should_expand_package(name): + dep_xml = get_package_xml_for_package(name) + deps |= get_depends_recursive(dep_xml, seen) + + return deps + + +def find_package_xml(start: Path) -> Path | None: + for parent in [start, *start.parents]: + pkg_xml = parent / PACKAGE_XML + if pkg_xml.is_file(): + return pkg_xml + return None + + +def package_has_ament_lint_auto_py(pkg_xml: Path) -> bool: + tree = ET.parse(pkg_xml) + root = tree.getroot() + for tag in DEPEND_TAGS: + for dep in root.findall(tag): + if dep.text and dep.text.strip() == 'ament_lint_auto_py': + return True + return False diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index 8c146bcc9..a1eb41886 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -2,21 +2,15 @@ ament_lint_auto_py =============== The package simplifies using multiple linters as part of pytest tests. -It reduces the amount of Python code to a bare minimum. -``test_ament_lint_auto.py`: +To have `ament_lint_auto_py` collect and run the linters include it in the `package.xml` -.. code:: python +``package.xml``: - import pytest +.. code:: xml + ament_lint_auto_py - @pytest.mark.ament_lint_auto_py - @pytest.mark.linter - def test_ament_lint_auto(run_entry_point) -> None: - """Run all installed linter dynamically.""" - rc = run_entry_point() - assert rc == 0, f'Linter[{run_entry_point.NAME}] failed with exit code {rc}' The set of linters to be used is then only specified in the package manifest as test dependencies. @@ -32,7 +26,7 @@ test dependencies. ament_cppcheck ament_pycodestyle -Since recursive dependencies are also being used a single test dependency is +Since recursive dependencies are also being used a for packages starting with `ament_lint_*` a single test dependency is sufficient to test with a set of common linters. ``package.xml``: @@ -48,30 +42,37 @@ sufficient to test with a set of common linters. How to exclude linter modules with ament_lint_auto_py? --------------------------------------------------- -Linter modules can be excluded via the environment variable `AMENT_LINT_AUTO_EXCLUDE`. +Linter modules can be excluded via the pytest configurable variables `ament_lint_auto_exclude` in pytest config files like `pytest.ini` or `pyproject.toml`. As an example to exclude the `copyright` linter: -.. code:: bash - export AMENT_LINT_EXCLUDE="ament_copyright;" +.. code:: + [pytest] + + ament_lint_auto_exclude = ament_copyright How to exclude files with ament_lint_auto_py? ------------------------------------------ -Linter hooks shall conform to the ament_lint_auto convention of excluding files -specified in the environment list variable `AMENT_LINT_AUTO_FILE_EXCLUDE`. +Linter hooks shall conform to the ament_lint_auto_py convention of excluding files +specified in the environment list variable `ament_lint_auto_file_exclude`. + +.. code:: + [pytest] -.. code:: bash - export AMENT_LINT_EXCLUDE="/path/to/ignored_file" + ament_lint_auto_file_exclude = /path/to/ignored_file For a more specific example, this excludes all python files matching a pattern using globbing. Multiple expressions can be combined on multiple lines. -.. code:: bash - .. code:: bash - export AMENT_LINT_EXCLUDE="src/*.py;test/*.cpp" +.. code:: + [pytest] + + ament_lint_auto_file_exclude = + src/* + test/*.cpp How to register 3rd party linters with ament_lint_auto_py? --------------------------------------------------------- diff --git a/ament_lint_auto_py/package.xml b/ament_lint_auto_py/package.xml index 136d51c9f..b8482fdc0 100644 --- a/ament_lint_auto_py/package.xml +++ b/ament_lint_auto_py/package.xml @@ -5,7 +5,7 @@ 0.20.3 The auto-magic functions for ease to use of the ament linters in Python. - + Chris Lalancette Apache License 2.0 @@ -15,6 +15,9 @@ ament_index_python python3-pytest + + ament_lint_auto_py + ament_lint_common_py ament_mypy diff --git a/ament_lint_auto_py/test/test_ament_lint_auto.py b/ament_lint_auto_py/test/test_ament_lint_auto.py deleted file mode 100644 index cf78e939e..000000000 --- a/ament_lint_auto_py/test/test_ament_lint_auto.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2026 Open Source Robotics Foundation, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - - -@pytest.mark.ament_lint_auto_py -@pytest.mark.linter -def test_ament_lint_auto(run_entry_point) -> None: - """Run all installed linter dynamically.""" - rc = run_entry_point() - assert rc == 0, f'Linter[{run_entry_point.NAME}] failed with exit code {rc}' From 6d44ffb3968c40783007c5c15fa9bd2fce22a4c7 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 25 Jan 2026 15:57:40 -0800 Subject: [PATCH 09/21] update logic around ament_lint_auto_py for linting Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py | 5 ++++- ament_lint_auto_py/package.xml | 3 --- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py b/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py index 02534f3d2..db1bc09f5 100644 --- a/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py +++ b/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py @@ -79,7 +79,10 @@ def find_package_xml(start: Path) -> Path | None: def package_has_ament_lint_auto_py(pkg_xml: Path) -> bool: tree = ET.parse(pkg_xml) root = tree.getroot() - for tag in DEPEND_TAGS: + + # Adds name so this package can self lint + OPT_IN_TAGS = (*DEPEND_TAGS, 'name') + for tag in OPT_IN_TAGS: for dep in root.findall(tag): if dep.text and dep.text.strip() == 'ament_lint_auto_py': return True diff --git a/ament_lint_auto_py/package.xml b/ament_lint_auto_py/package.xml index b8482fdc0..0cae56756 100644 --- a/ament_lint_auto_py/package.xml +++ b/ament_lint_auto_py/package.xml @@ -15,9 +15,6 @@ ament_index_python python3-pytest - - ament_lint_auto_py - ament_lint_common_py ament_mypy From edfb5bad562b927a494929328e1350936a426047 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Sun, 25 Jan 2026 16:05:27 -0800 Subject: [PATCH 10/21] add note about parity Signed-off-by: Michael Carlstrom --- ament_lint_common_py/doc/index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ament_lint_common_py/doc/index.rst b/ament_lint_common_py/doc/index.rst index a64cc97d0..a31f054f9 100644 --- a/ament_lint_common_py/doc/index.rst +++ b/ament_lint_common_py/doc/index.rst @@ -1,6 +1,8 @@ ament_lint_common_py ================= +The pytest variant of ament_lint_common. The dependencies match to keep parity between them. + A mechanism for running the following set of common linters: * `ament_copyright `_ : a copyright linter which checks that copyright statements and license headers are present and correct From 53f38cf9ca82517ade361023ddeaff46fb8e68c9 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:41:55 -0800 Subject: [PATCH 11/21] Update ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py index cba1766ca..79b81eadd 100644 --- a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py +++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py @@ -80,7 +80,7 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[ runner = ep.load() if runner.NAME not in effective_depends: - continue # skipping linter if not declared in depends of a package.xmk + continue # skipping linter if not declared in depends of a package.xml if runner.NAME in excluded: continue # skipping linter if declared in ament_lint_auto_exclude From 307057d57e38c186e882a290fba453de6da40c69 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:03 -0800 Subject: [PATCH 12/21] Update ament_lint_auto_py/doc/index.rst Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index a1eb41886..f9f1f0ac4 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -84,7 +84,7 @@ To register a third party linter implement class like the following. class CustomRunner: NAME = 'ament_custom' - FILE_TYPES = ('*.py') + FILE_TYPES = ('*.py',) def __init__(self, args: list[str]) -> None: self.args = args From a7d6f04b63e04fb3e861a91ef7ecc7f37e3ef025 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:10 -0800 Subject: [PATCH 13/21] Update ament_pep257/ament_pep257/main.py Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_pep257/ament_pep257/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_pep257/ament_pep257/main.py b/ament_pep257/ament_pep257/main.py index 4bc4818ec..307f592fa 100755 --- a/ament_pep257/ament_pep257/main.py +++ b/ament_pep257/ament_pep257/main.py @@ -59,7 +59,7 @@ class Pep257Runner: NAME = 'ament_pep257' - FILE_TYPES = ('*.py') + FILE_TYPES = ('*.py',) def __init__(self, args: list[str]) -> None: self.args = args From 53cefad83bb952ff5554ebfab470ddb23ba131e7 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:16 -0800 Subject: [PATCH 14/21] Update ament_pycodestyle/ament_pycodestyle/main.py Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_pycodestyle/ament_pycodestyle/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_pycodestyle/ament_pycodestyle/main.py b/ament_pycodestyle/ament_pycodestyle/main.py index 80db0d8f6..ce0caf931 100755 --- a/ament_pycodestyle/ament_pycodestyle/main.py +++ b/ament_pycodestyle/ament_pycodestyle/main.py @@ -27,7 +27,7 @@ class PycodestyleRunner: NAME = 'ament_pycodestyle' - FILE_TYPES = ('*.py') + FILE_TYPES = ('*.py',) def __init__(self, args: list[str]) -> None: self.args = args From 9d4b926a4d94a22c08e26ae0473a133810e944a9 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:23 -0800 Subject: [PATCH 15/21] Update ament_pyflakes/ament_pyflakes/main.py Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_pyflakes/ament_pyflakes/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_pyflakes/ament_pyflakes/main.py b/ament_pyflakes/ament_pyflakes/main.py index 87a7b4114..322f80daf 100755 --- a/ament_pyflakes/ament_pyflakes/main.py +++ b/ament_pyflakes/ament_pyflakes/main.py @@ -30,7 +30,7 @@ class PyflakesRunner: NAME = 'ament_pyflakes' - FILE_TYPES = ('*.py') + FILE_TYPES = ('*.py',) def __init__(self, args: list[str]) -> None: self.args = args From f7e74d5c356241037adf33b60873ee22ef3ad739 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:33 -0800 Subject: [PATCH 16/21] Update ament_lint_auto_py/doc/index.rst Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index f9f1f0ac4..e6b85bdc1 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -1,5 +1,5 @@ ament_lint_auto_py -=============== +================== The package simplifies using multiple linters as part of pytest tests. From ec4052f122404b77ac864e9fe3fa8dae9d7dffb2 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:41 -0800 Subject: [PATCH 17/21] Update ament_lint_auto_py/doc/index.rst Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index e6b85bdc1..1cf8c8909 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -40,7 +40,7 @@ sufficient to test with a set of common linters. How to exclude linter modules with ament_lint_auto_py? ---------------------------------------------------- +------------------------------------------------------ Linter modules can be excluded via the pytest configurable variables `ament_lint_auto_exclude` in pytest config files like `pytest.ini` or `pyproject.toml`. From f352eb409c00adda2eca8e315329b8fbd68b35c0 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:42:53 -0800 Subject: [PATCH 18/21] Update ament_lint_auto_py/doc/index.rst Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst index 1cf8c8909..ffea95e42 100644 --- a/ament_lint_auto_py/doc/index.rst +++ b/ament_lint_auto_py/doc/index.rst @@ -53,7 +53,7 @@ As an example to exclude the `copyright` linter: How to exclude files with ament_lint_auto_py? ------------------------------------------- +--------------------------------------------- Linter hooks shall conform to the ament_lint_auto_py convention of excluding files specified in the environment list variable `ament_lint_auto_file_exclude`. From 8d4a55ad68895b93af02f1c57a0d49c1e8a9e892 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:43:01 -0800 Subject: [PATCH 19/21] Update ament_lint_common_py/doc/index.rst Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_common_py/doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_common_py/doc/index.rst b/ament_lint_common_py/doc/index.rst index a31f054f9..4c7a0f64f 100644 --- a/ament_lint_common_py/doc/index.rst +++ b/ament_lint_common_py/doc/index.rst @@ -1,5 +1,5 @@ ament_lint_common_py -================= +==================== The pytest variant of ament_lint_common. The dependencies match to keep parity between them. From 99209d7554ae54a6bbfadf8cc3fc6f1bfbbc3cf3 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 16:43:10 -0800 Subject: [PATCH 20/21] Update ament_xmllint/ament_xmllint/main.py Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_xmllint/ament_xmllint/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_xmllint/ament_xmllint/main.py b/ament_xmllint/ament_xmllint/main.py index 2c378bc48..ea4efebe7 100755 --- a/ament_xmllint/ament_xmllint/main.py +++ b/ament_xmllint/ament_xmllint/main.py @@ -32,7 +32,7 @@ class XmlLintRunner: NAME = 'ament_xmllint' - FILE_TYPES = ('*.xml') + FILE_TYPES = ('*.xml',) def __init__(self, args: list[str]) -> None: self.args = args From 44a3fba7e3b8b3c72dc4a49ad21b59a91152bde7 Mon Sep 17 00:00:00 2001 From: Michael Carlstrom Date: Fri, 6 Feb 2026 19:09:01 -0800 Subject: [PATCH 21/21] Update ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py Co-authored-by: Tomoya Fujita Signed-off-by: Michael Carlstrom --- ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py index 79b81eadd..589955dcf 100644 --- a/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py +++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py @@ -88,7 +88,7 @@ def pytest_collection_modifyitems(session: Session, config: Config, items: list[ # skip linters if no matching files exist found_file = False for pattern in runner.FILE_TYPES: - if any(Path('.').rglob(pattern)): + if any(config.rootpath.rglob(pattern)): found_file = True break if not found_file: