Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
24fbba0
init
InvincibleRMC Jan 24, 2026
5feeaf8
Register linters
InvincibleRMC Jan 25, 2026
a200508
undo more accidentally changes
InvincibleRMC Jan 25, 2026
0890137
remove changelog
InvincibleRMC Jan 25, 2026
28d55ab
make runner stateful for AMENT_LINT_AUTO_FILE_EXCLUDE
InvincibleRMC Jan 25, 2026
e03c19f
Add docs about 3rd party linters
InvincibleRMC Jan 25, 2026
53bf4f4
allow for ament_lint* metapackages
InvincibleRMC Jan 25, 2026
8b28229
add pytest config options, and auto test detection
InvincibleRMC Jan 25, 2026
6d44ffb
update logic around ament_lint_auto_py for linting
InvincibleRMC Jan 25, 2026
edfb5ba
add note about parity
InvincibleRMC Jan 26, 2026
53f38cf
Update ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py
InvincibleRMC Feb 7, 2026
307057d
Update ament_lint_auto_py/doc/index.rst
InvincibleRMC Feb 7, 2026
a7d6f04
Update ament_pep257/ament_pep257/main.py
InvincibleRMC Feb 7, 2026
53cefad
Update ament_pycodestyle/ament_pycodestyle/main.py
InvincibleRMC Feb 7, 2026
9d4b926
Update ament_pyflakes/ament_pyflakes/main.py
InvincibleRMC Feb 7, 2026
f7e74d5
Update ament_lint_auto_py/doc/index.rst
InvincibleRMC Feb 7, 2026
ec4052f
Update ament_lint_auto_py/doc/index.rst
InvincibleRMC Feb 7, 2026
f352eb4
Update ament_lint_auto_py/doc/index.rst
InvincibleRMC Feb 7, 2026
8d4a55a
Update ament_lint_common_py/doc/index.rst
InvincibleRMC Feb 7, 2026
99209d7
Update ament_xmllint/ament_xmllint/main.py
InvincibleRMC Feb 7, 2026
44a3fba
Update ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py
InvincibleRMC Feb 7, 2026
5aa6c76
Merge branch 'rolling' into ament_lint_auto-for-python
InvincibleRMC Mar 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions ament_clang_format/ament_clang_format/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,35 @@
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

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')
Expand Down
3 changes: 3 additions & 0 deletions ament_clang_format/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@
'console_scripts': [
'ament_clang_format = ament_clang_format.main:main',
],
'ament_lint': [
'ament_clang_format = ament_clang_format.main:ClangFormatRunner',
],
},
)
22 changes: 22 additions & 0 deletions ament_clang_tidy/ament_clang_tidy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,35 @@
import subprocess
import sys
import time
from typing import Literal

from xml.sax.saxutils import escape
from xml.sax.saxutils import quoteattr

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']

Expand Down
3 changes: 3 additions & 0 deletions ament_clang_tidy/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@
'console_scripts': [
'ament_clang_tidy = ament_clang_tidy.main:main',
],
'ament_lint': [
'ament_clang_tidy = ament_clang_tidy.main:ClangTidyRunner',
],
},
)
15 changes: 15 additions & 0 deletions ament_copyright/ament_copyright/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions ament_copyright/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@
'pytest11': [
'ament_copyright = ament_copyright.pytest_marker',
],
'ament_lint': [
'ament_copyright = ament_copyright.main:CopyrightRunner',
],
},
)
22 changes: 22 additions & 0 deletions ament_cppcheck/ament_cppcheck/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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']

Expand Down
3 changes: 3 additions & 0 deletions ament_cppcheck/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@
'console_scripts': [
'ament_cppcheck = ament_cppcheck.main:main',
],
'ament_lint': [
'ament_cppcheck = ament_cppcheck.main:CPPCheckRunner',
],
},
)
22 changes: 22 additions & 0 deletions ament_cpplint/ament_cpplint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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']
Expand Down
3 changes: 3 additions & 0 deletions ament_cpplint/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@
'console_scripts': [
'ament_cpplint = ament_cpplint.main:main',
],
'ament_lint': [
'ament_cpplint = ament_cpplint.main:CPPLintRunner',
],
},
)
13 changes: 13 additions & 0 deletions ament_flake8/ament_flake8/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ament_flake8/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@
'pytest11': [
'ament_flake8 = ament_flake8.pytest_marker',
],
'ament_lint': [
'ament_flake8 = ament_flake8.main:Flake8Runner',
],
},
)
3 changes: 3 additions & 0 deletions ament_lint_auto_py/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Changelog for package ament_lint_auto_py
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Empty file.
Empty file.
120 changes: 120 additions & 0 deletions ament_lint_auto_py/ament_lint_auto_py/pytest_plugin.py
Original file line number Diff line number Diff line change
@@ -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.xmk
Comment thread
InvincibleRMC marked this conversation as resolved.
Outdated

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)):
Comment thread
InvincibleRMC marked this conversation as resolved.
Outdated
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=[],
)
Loading