diff --git a/ament_clang_format/ament_clang_format/main.py b/ament_clang_format/ament_clang_format/main.py
index 87120628..b3d57ee9 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,27 @@
import yaml
+class ClangFormatRunner:
+
+ NAME = 'ament_clang_format'
+ FILE_TYPES = (
+ '*.c',
+ '*.cc',
+ '*.cpp',
+ '*.cxx',
+ '*.h',
+ '*.hh',
+ '*.hpp',
+ '*.hxx',
+ )
+
+ 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:]):
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 1510ea0b..d22109ec 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:ClangFormatRunner',
+ ],
},
)
diff --git a/ament_clang_tidy/ament_clang_tidy/main.py b/ament_clang_tidy/ament_clang_tidy/main.py
index 5faa8142..bd928c2a 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,27 @@
import yaml
+class ClangTidyRunner:
+
+ NAME = 'ament_clang_tidy'
+ FILE_TYPES = (
+ '*.c',
+ '*.cc',
+ '*.cpp',
+ '*.cxx',
+ '*.h',
+ '*.hh',
+ '*.hpp',
+ '*.hxx',
+ )
+
+ 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:]):
extensions = ['c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx']
diff --git a/ament_clang_tidy/setup.py b/ament_clang_tidy/setup.py
index 65a7d969..90be749e 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:ClangTidyRunner',
+ ],
},
)
diff --git a/ament_copyright/ament_copyright/main.py b/ament_copyright/ament_copyright/main.py
index 361f3b10..7a14cc85 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,20 @@
from ament_copyright.parser import search_copyright_information
+class CopyrightRunner:
+
+ NAME = 'ament_copyright'
+ FILE_TYPES = (
+ '*',
+ )
+
+ 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:]):
extensions = [
'c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx',
diff --git a/ament_copyright/setup.py b/ament_copyright/setup.py
index 702b24e8..59751e23 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:CopyrightRunner',
+ ],
},
)
diff --git a/ament_cppcheck/ament_cppcheck/main.py b/ament_cppcheck/ament_cppcheck/main.py
index 45017992..f83bc762 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,27 @@ def get_cppcheck_version(cppcheck_bin):
return tokens[1]
+class CPPCheckRunner:
+
+ NAME = 'ament_cppcheck'
+ FILE_TYPES = (
+ '*.c',
+ '*.cc',
+ '*.cpp',
+ '*.cxx',
+ '*.h',
+ '*.hh',
+ '*.hpp',
+ '*.hxx',
+ )
+
+ 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:]):
extensions = ['c', 'cc', 'cpp', 'cxx', 'h', 'hh', 'hpp', 'hxx']
diff --git a/ament_cppcheck/setup.py b/ament_cppcheck/setup.py
index 3b598670..be3caed4 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:CPPCheckRunner',
+ ],
},
)
diff --git a/ament_cpplint/ament_cpplint/main.py b/ament_cpplint/ament_cpplint/main.py
index fd8769b7..38afcd3f 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,27 @@ 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',
+ )
+
+ 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:]):
extensions = ['c', 'cc', 'cpp', 'cxx']
headers = ['h', 'hh', 'hpp', 'hxx']
diff --git a/ament_cpplint/setup.py b/ament_cpplint/setup.py
index 53e3534a..aa056b1b 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:CPPLintRunner',
+ ],
},
)
diff --git a/ament_flake8/ament_flake8/main.py b/ament_flake8/ament_flake8/main.py
index ed22bdde..b72f800c 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,18 @@
from flake8.main import options as flake8_options
+class Flake8Runner:
+
+ NAME = 'ament_flake8'
+ 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(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 b928050a..44fd7e28 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:Flake8Runner',
+ ],
},
)
diff --git a/ament_lint_auto_py/CHANGELOG.rst b/ament_lint_auto_py/CHANGELOG.rst
new file mode 100644
index 00000000..b20c0edb
--- /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_py/ament_lint_auto_py/__init__.py b/ament_lint_auto_py/ament_lint_auto_py/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ament_lint_auto_py/ament_lint_auto_py/py.typed b/ament_lint_auto_py/ament_lint_auto_py/py.typed
new file mode 100644
index 00000000..e69de29b
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 00000000..589955dc
--- /dev/null
+++ b/ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py
@@ -0,0 +1,120 @@
+# 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
+from importlib.metadata import EntryPoint
+from pathlib import Path
+from typing import Literal
+
+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 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'
+ )
+
+
+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.xml
+
+ 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(config.rootpath.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 00000000..db1bc09f
--- /dev/null
+++ b/ament_lint_auto_py/ament_lint_auto_py/xml_helpers.py
@@ -0,0 +1,89 @@
+# 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()
+
+ # 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
+ return False
diff --git a/ament_lint_auto_py/doc/index.rst b/ament_lint_auto_py/doc/index.rst
new file mode 100644
index 00000000..ffea95e4
--- /dev/null
+++ b/ament_lint_auto_py/doc/index.rst
@@ -0,0 +1,107 @@
+ament_lint_auto_py
+==================
+
+The package simplifies using multiple linters as part of pytest tests.
+
+To have `ament_lint_auto_py` collect and run the linters include it in the `package.xml`
+
+``package.xml``:
+
+.. code:: xml
+
+ ament_lint_auto_py
+
+
+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_py
+
+
+ ament_clang_format
+ ament_cppcheck
+ ament_pycodestyle
+
+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``:
+
+.. code:: xml
+
+ ament_lint_auto_py
+
+
+ ament_lint_common_py
+
+
+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`.
+
+As an example to exclude the `copyright` linter:
+
+.. 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_py convention of excluding files
+specified in the environment list variable `ament_lint_auto_file_exclude`.
+
+.. code::
+ [pytest]
+
+ 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::
+ [pytest]
+
+ ament_lint_auto_file_exclude =
+ src/*
+ 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'
+ ]
+ },
diff --git a/ament_lint_auto_py/package.xml b/ament_lint_auto_py/package.xml
new file mode 100644
index 00000000..0cae5675
--- /dev/null
+++ b/ament_lint_auto_py/package.xml
@@ -0,0 +1,24 @@
+
+
+
+ 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
+
+ ament_index_python
+ 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 00000000..e69de29b
diff --git a/ament_lint_auto_py/setup.py b/ament_lint_auto_py/setup.py
new file mode 100644
index 00000000..ec8fb54b
--- /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_cmake/ament_lint_cmake/main.py b/ament_lint_cmake/ament_lint_cmake/main.py
index aa76fcbd..cbf8f186 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,19 @@ def is_valid_file(filename):
cmakelint.IsValidFile = is_valid_file
+class LintCMakeRunner:
+
+ NAME = 'ament_lint_cmake'
+ FILE_TYPES = ('CMakeLists.txt', '*.cmake')
+
+ 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:]):
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 9d93996f..39e1bf7d 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_lint_common_py/CHANGELOG.rst b/ament_lint_common_py/CHANGELOG.rst
new file mode 100644
index 00000000..b3307644
--- /dev/null
+++ b/ament_lint_common_py/CHANGELOG.rst
@@ -0,0 +1,7 @@
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Changelog for package ament_lint_common_py
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+0.20.3 (2025-11-24)
+-------------------
+
diff --git a/ament_lint_common_py/CMakeLists.txt b/ament_lint_common_py/CMakeLists.txt
new file mode 100644
index 00000000..44cd31e1
--- /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 00000000..4c7a0f64
--- /dev/null
+++ b/ament_lint_common_py/doc/index.rst
@@ -0,0 +1,24 @@
+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
+
+* `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_cmake_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_py `_ documentation provides information on using ament_lint_common_py.
diff --git a/ament_lint_common_py/package.xml b/ament_lint_common_py/package.xml
new file mode 100644
index 00000000..666ad19d
--- /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/ament_mypy/main.py b/ament_mypy/ament_mypy/main.py
index 082d5943..7d4f6184 100755
--- a/ament_mypy/ament_mypy/main.py
+++ b/ament_mypy/ament_mypy/main.py
@@ -27,6 +27,18 @@
import mypy.api
+class MypyRunner:
+
+ NAME = 'ament_mypy'
+ FILE_TYPES = ('*.py', '*.pyi')
+
+ 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:
"""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 4bcdc3ea..42b8ee88 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:MypyRunner',
+ ],
},
)
diff --git a/ament_pclint/ament_pclint/main.py b/ament_pclint/ament_pclint/main.py
index 960c58d6..3c1e2300 100755
--- a/ament_pclint/ament_pclint/main.py
+++ b/ament_pclint/ament_pclint/main.py
@@ -23,11 +23,31 @@
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++',
+ )
+
+ 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:]):
extensions = ['c', 'cc', 'cpp', 'cxx', 'c++']
diff --git a/ament_pclint/setup.py b/ament_pclint/setup.py
index f43d546e..b9dfe53b 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:PCLintRunner',
+ ],
},
)
diff --git a/ament_pep257/ament_pep257/main.py b/ament_pep257/ament_pep257/main.py
index a8f43800..307f592f 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,18 @@
]
+class Pep257Runner:
+
+ NAME = 'ament_pep257'
+ 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(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 4a8f190b..87fe4cba 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:Pep257Runner',
+ ],
},
)
diff --git a/ament_pycodestyle/ament_pycodestyle/main.py b/ament_pycodestyle/ament_pycodestyle/main.py
index 57b28a47..ce0caf93 100755
--- a/ament_pycodestyle/ament_pycodestyle/main.py
+++ b/ament_pycodestyle/ament_pycodestyle/main.py
@@ -17,12 +17,26 @@
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',)
+
+ 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:]):
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 14e35114..6ad5789c 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:PycodestyleRunner',
+ ],
},
)
diff --git a/ament_pyflakes/ament_pyflakes/main.py b/ament_pyflakes/ament_pyflakes/main.py
index 1545c9c3..322f80da 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,19 @@
from pyflakes.reporter import Reporter
+class PyflakesRunner:
+
+ NAME = 'ament_pyflakes'
+ FILE_TYPES = ('*.py',)
+
+ 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:]):
parser = argparse.ArgumentParser(
description='Check code using pyflakes.',
diff --git a/ament_pyflakes/setup.py b/ament_pyflakes/setup.py
index cec86989..94ea9547 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:PyflakesRunner',
+ ],
},
)
diff --git a/ament_uncrustify/ament_uncrustify/main.py b/ament_uncrustify/ament_uncrustify/main.py
index a3c3bf14..41047095 100755
--- a/ament_uncrustify/ament_uncrustify/main.py
+++ b/ament_uncrustify/ament_uncrustify/main.py
@@ -27,10 +27,33 @@
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',
+ )
+
+ 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:]):
uncrustify_bin = find_executable('uncrustify')
if not uncrustify_bin:
diff --git a/ament_uncrustify/setup.py b/ament_uncrustify/setup.py
index 039322af..f730a3f6 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:UncrustifyRunner',
+ ],
},
)
diff --git a/ament_xmllint/ament_xmllint/main.py b/ament_xmllint/ament_xmllint/main.py
index 261de0d6..870fb07d 100755
--- a/ament_xmllint/ament_xmllint/main.py
+++ b/ament_xmllint/ament_xmllint/main.py
@@ -22,6 +22,7 @@
import sys
import tempfile
import time
+from typing import Literal
import urllib.request
from xml.etree import ElementTree
from xml.sax import make_parser
@@ -31,7 +32,20 @@
from xml.sax.saxutils import quoteattr
-def main(argv=sys.argv[1:]):
+class XmlLintRunner:
+
+ NAME = 'ament_xmllint'
+ FILE_TYPES = ('*.xml',)
+
+ 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]:
default_extensions = ['xml']
parser = argparse.ArgumentParser(
@@ -73,7 +87,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 e8dbcf37..5f5fb3ee 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:XmlLintRunner',
+ ],
},
)