diff --git a/rosidl_cli/package.xml b/rosidl_cli/package.xml
index 46f5c7ee5..e45804c7a 100644
--- a/rosidl_cli/package.xml
+++ b/rosidl_cli/package.xml
@@ -22,6 +22,7 @@
ament_copyright
ament_flake8
+ ament_mypy
ament_pep257
ament_xmllint
python3-pytest
diff --git a/rosidl_cli/py.typed b/rosidl_cli/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/rosidl_cli/rosidl_cli/cli.py b/rosidl_cli/rosidl_cli/cli.py
index 9c3f8ac28..ce729a816 100644
--- a/rosidl_cli/rosidl_cli/cli.py
+++ b/rosidl_cli/rosidl_cli/cli.py
@@ -14,13 +14,18 @@
import argparse
import signal
+from typing import Any, List, Union
from rosidl_cli.command.generate import GenerateCommand
from rosidl_cli.command.translate import TranslateCommand
from rosidl_cli.common import get_first_line_doc
-def add_subparsers(parser, cli_name, commands):
+def add_subparsers(
+ parser: argparse.ArgumentParser,
+ cli_name: str,
+ commands: List[Union[GenerateCommand, TranslateCommand]]
+) -> argparse._SubParsersAction[argparse.ArgumentParser]:
"""
Create argparse subparser for each command.
@@ -63,7 +68,7 @@ def add_subparsers(parser, cli_name, commands):
return subparser
-def main():
+def main() -> Union[str, signal.Signals, Any]:
script_name = 'rosidl'
description = f'{script_name} is an extensible command-line tool ' \
'for ROS interface generation.'
@@ -74,7 +79,8 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter
)
- commands = [GenerateCommand(), TranslateCommand()]
+ commands: List[Union[GenerateCommand, TranslateCommand]] = \
+ [GenerateCommand(), TranslateCommand()]
# add arguments for command extension(s)
add_subparsers(
diff --git a/rosidl_cli/rosidl_cli/command/__init__.py b/rosidl_cli/rosidl_cli/command/__init__.py
index 22d035bb5..187a6c187 100644
--- a/rosidl_cli/rosidl_cli/command/__init__.py
+++ b/rosidl_cli/rosidl_cli/command/__init__.py
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import argparse
+
class Command:
"""
@@ -22,8 +24,8 @@ class Command:
* `add_arguments`
"""
- def add_arguments(self, parser):
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
pass
- def main(self, *, parser, args):
+ def main(self, *, args: argparse.Namespace) -> None:
raise NotImplementedError()
diff --git a/rosidl_cli/rosidl_cli/command/generate/__init__.py b/rosidl_cli/rosidl_cli/command/generate/__init__.py
index ee46a937a..b12b946c1 100644
--- a/rosidl_cli/rosidl_cli/command/generate/__init__.py
+++ b/rosidl_cli/rosidl_cli/command/generate/__init__.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import argparse
import pathlib
from rosidl_cli.command import Command
@@ -24,7 +25,7 @@ class GenerateCommand(Command):
name = 'generate'
- def add_arguments(self, parser):
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-o', '--output-path', metavar='PATH',
type=pathlib.Path, default=None,
@@ -50,7 +51,7 @@ def add_arguments(self, parser):
"If prefixed by another path followed by a colon ':', "
'path resolution is performed against such path.'))
- def main(self, *, args):
+ def main(self, *, args: argparse.Namespace) -> None:
generate(
package_name=args.package_name,
interface_files=args.interface_files,
diff --git a/rosidl_cli/rosidl_cli/command/generate/api.py b/rosidl_cli/rosidl_cli/command/generate/api.py
index ebec89144..ff7edbbcb 100644
--- a/rosidl_cli/rosidl_cli/command/generate/api.py
+++ b/rosidl_cli/rosidl_cli/command/generate/api.py
@@ -14,20 +14,22 @@
import os
import pathlib
+from typing import List, Optional
+from .extensions import GenerateCommandExtension
from .extensions import load_type_extensions
from .extensions import load_typesupport_extensions
def generate(
*,
- package_name,
- interface_files,
- include_paths=None,
- output_path=None,
- types=None,
- typesupports=None
-):
+ package_name: str,
+ interface_files: List[str],
+ include_paths: Optional[List[str]] = None,
+ output_path: Optional[pathlib.Path] = None,
+ types: Optional[List[str]] = None,
+ typesupports: Optional[List[str]] = None
+) -> List[List[str]]:
"""
Generate source code from interface definition files.
@@ -60,7 +62,7 @@ def generate(
:returns: list of lists of paths to generated source code files,
one group per type or type support extension invoked
"""
- extensions = []
+ extensions: List[GenerateCommandExtension] = []
unspecific_generation = not types and not typesupports
diff --git a/rosidl_cli/rosidl_cli/command/generate/extensions.py b/rosidl_cli/rosidl_cli/command/generate/extensions.py
index fde1fb6b0..a89630d71 100644
--- a/rosidl_cli/rosidl_cli/command/generate/extensions.py
+++ b/rosidl_cli/rosidl_cli/command/generate/extensions.py
@@ -12,6 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from pathlib import Path
+from typing import cast, List, Optional
+
from rosidl_cli.extensions import Extension
from rosidl_cli.extensions import load_extensions
@@ -26,11 +29,11 @@ class GenerateCommandExtension(Extension):
def generate(
self,
- package_name,
- interface_files,
- include_paths,
- output_path
- ):
+ package_name: str,
+ interface_files: List[str],
+ include_paths: List[str],
+ output_path: Path
+ ) -> List[str]:
"""
Generate source code.
@@ -48,11 +51,17 @@ def generate(
raise NotImplementedError()
-def load_type_extensions(**kwargs):
+def load_type_extensions(*, specs: Optional[List[str]],
+ strict: bool) -> List[GenerateCommandExtension]:
"""Load extensions for type representation source code generation."""
- return load_extensions('rosidl_cli.command.generate.type_extensions', **kwargs)
+ extensions = load_extensions('rosidl_cli.command.generate.type_extensions', specs=specs,
+ strict=strict)
+ return cast(List[GenerateCommandExtension], extensions)
-def load_typesupport_extensions(**kwargs):
+def load_typesupport_extensions(*, specs: Optional[List[str]], strict: bool
+ ) -> List[GenerateCommandExtension]:
"""Load extensions for type support source code generation."""
- return load_extensions('rosidl_cli.command.generate.typesupport_extensions', **kwargs)
+ extensions = load_extensions('rosidl_cli.command.generate.typesupport_extensions',
+ specs=specs, strict=strict)
+ return cast(List[GenerateCommandExtension], extensions)
diff --git a/rosidl_cli/rosidl_cli/command/helpers.py b/rosidl_cli/rosidl_cli/command/helpers.py
index f23cc9a88..d81b0cdc3 100644
--- a/rosidl_cli/rosidl_cli/command/helpers.py
+++ b/rosidl_cli/rosidl_cli/command/helpers.py
@@ -17,9 +17,10 @@
import os
import pathlib
import tempfile
+from typing import Generator, List, Tuple
-def package_name_from_interface_file_path(path):
+def package_name_from_interface_file_path(path: pathlib.Path) -> str:
"""
Derive ROS package name from a ROS interface definition file path.
@@ -29,7 +30,7 @@ def package_name_from_interface_file_path(path):
return pathlib.Path(os.path.abspath(path)).parents[1].name
-def dependencies_from_include_paths(include_paths):
+def dependencies_from_include_paths(include_paths: List[str]) -> List[str]:
"""
Collect dependencies' ROS interface definition files from include paths.
@@ -45,7 +46,7 @@ def dependencies_from_include_paths(include_paths):
})
-def interface_path_as_tuple(path):
+def interface_path_as_tuple(path: str) -> Tuple[pathlib.Path, pathlib.Path]:
"""
Express interface definition file path as an (absolute prefix, relative path) tuple.
@@ -61,18 +62,20 @@ def interface_path_as_tuple(path):
"""
path_as_string = str(path)
if ':' not in path_as_string:
- prefix = pathlib.Path.cwd()
+ prefix_path = pathlib.Path.cwd()
else:
prefix, _, path = path_as_string.rpartition(':')
- prefix = pathlib.Path(os.path.abspath(prefix))
- path = pathlib.Path(path)
- if path.is_absolute():
+ prefix_path = pathlib.Path(os.path.abspath(prefix))
+ path_as_path = pathlib.Path(path)
+ if path_as_path.is_absolute():
raise ValueError('Interface definition file path '
- f"'{path}' cannot be absolute")
- return prefix, path
+ f"'{path_as_path}' cannot be absolute")
+ return prefix_path, path_as_path
-def idl_tuples_from_interface_files(interface_files):
+def idl_tuples_from_interface_files(
+ interface_files: List[str]
+) -> List[str]:
"""
Express ROS interface definition file paths as IDL tuples.
@@ -80,9 +83,9 @@ def idl_tuples_from_interface_files(interface_files):
which to resolve it followed by a colon ':'. This function then applies
the same logic as `interface_path_as_tuple`.
"""
- idl_tuples = []
- for path in interface_files:
- prefix, path = interface_path_as_tuple(path)
+ idl_tuples: List[str] = []
+ for interface_path in interface_files:
+ prefix, path = interface_path_as_tuple(interface_path)
idl_tuples.append(f'{prefix}:{path.as_posix()}')
return idl_tuples
@@ -90,12 +93,12 @@ def idl_tuples_from_interface_files(interface_files):
@contextlib.contextmanager
def legacy_generator_arguments_file(
*,
- package_name,
- interface_files,
- include_paths,
- templates_path,
- output_path
-):
+ package_name: str,
+ interface_files: List[str],
+ include_paths: List[str],
+ templates_path: str,
+ output_path: str
+) -> Generator[str, None, None]:
"""
Generate a temporary rosidl generator arguments file.
@@ -138,10 +141,10 @@ def legacy_generator_arguments_file(
def generate_visibility_control_file(
*,
- package_name,
- template_path,
- output_path
-):
+ package_name: str,
+ template_path: str,
+ output_path: str
+) -> None:
"""
Generate a visibility control file from a template.
diff --git a/rosidl_cli/rosidl_cli/command/translate/__init__.py b/rosidl_cli/rosidl_cli/command/translate/__init__.py
index 03798db3e..f5b6368f0 100644
--- a/rosidl_cli/rosidl_cli/command/translate/__init__.py
+++ b/rosidl_cli/rosidl_cli/command/translate/__init__.py
@@ -12,8 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import argparse
import pathlib
+
from rosidl_cli.command import Command
from .api import translate
@@ -24,7 +26,7 @@ class TranslateCommand(Command):
name = 'translate'
- def add_arguments(self, parser):
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-o', '--output-path', metavar='PATH',
type=pathlib.Path, default=None,
@@ -64,7 +66,7 @@ def add_arguments(self, parser):
'path resolution is performed against such path.')
)
- def main(self, *, args):
+ def main(self, *, args: argparse.Namespace) -> None:
translate(
package_name=args.package_name,
interface_files=args.interface_files,
diff --git a/rosidl_cli/rosidl_cli/command/translate/api.py b/rosidl_cli/rosidl_cli/command/translate/api.py
index b63db278e..a7d18bd45 100644
--- a/rosidl_cli/rosidl_cli/command/translate/api.py
+++ b/rosidl_cli/rosidl_cli/command/translate/api.py
@@ -15,20 +15,21 @@
import collections
import os
import pathlib
+from typing import DefaultDict, Dict, List, Optional, Union
from .extensions import load_translate_extensions
def translate(
*,
- package_name,
- interface_files,
- output_format,
- input_format=None,
- include_paths=None,
- output_path=None,
- translators=None
-):
+ package_name: str,
+ interface_files: List[str],
+ output_format: str,
+ input_format: Optional[str] = None,
+ include_paths: Optional[List[str]] = None,
+ output_path: Optional[pathlib.Path] = None,
+ translators: Optional[List[str]] = None
+) -> List[str]:
"""
Translate interface definition files from one format to another.
@@ -64,7 +65,8 @@ def translate(
raise RuntimeError('No translate extensions found')
if not input_format:
- interface_files_per_format = collections.defaultdict(list)
+ interface_files_per_format: Union[DefaultDict[str, List[str]],
+ Dict[str, List[str]]] = collections.defaultdict(list)
for interface_file in interface_files:
input_format = os.path.splitext(interface_file)[-1][1:]
interface_files_per_format[input_format].append(interface_file)
diff --git a/rosidl_cli/rosidl_cli/command/translate/extensions.py b/rosidl_cli/rosidl_cli/command/translate/extensions.py
index f193a4804..10c3ba194 100644
--- a/rosidl_cli/rosidl_cli/command/translate/extensions.py
+++ b/rosidl_cli/rosidl_cli/command/translate/extensions.py
@@ -11,6 +11,8 @@
# 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 cast, ClassVar, List, Optional
from rosidl_cli.extensions import Extension
from rosidl_cli.extensions import load_extensions
@@ -28,13 +30,16 @@ class TranslateCommandExtension(Extension):
* `translate`
"""
+ input_format: ClassVar[str]
+ output_format: ClassVar[str]
+
def translate(
self,
- package_name,
- interface_files,
- include_paths,
- output_path
- ):
+ package_name: str,
+ interface_files: List[str],
+ include_paths: List[str],
+ output_path: Path
+ ) -> List[str]:
"""
Translate interface definition files.
@@ -57,8 +62,10 @@ def translate(
raise NotImplementedError()
-def load_translate_extensions(**kwargs):
+def load_translate_extensions(*, specs: Optional[List[str]], strict: bool
+ ) -> List[TranslateCommandExtension]:
"""Load extensions for interface definition translation."""
- return load_extensions(
- 'rosidl_cli.command.translate.extensions', **kwargs
+ extensions = load_extensions(
+ 'rosidl_cli.command.translate.extensions', specs=specs, strict=strict
)
+ return cast(List[TranslateCommandExtension], extensions)
diff --git a/rosidl_cli/rosidl_cli/common.py b/rosidl_cli/rosidl_cli/common.py
index 1f94c2c63..c0e8c9d42 100644
--- a/rosidl_cli/rosidl_cli/common.py
+++ b/rosidl_cli/rosidl_cli/common.py
@@ -13,7 +13,7 @@
# limitations under the License.
-def get_first_line_doc(any_type):
+def get_first_line_doc(any_type: object) -> str:
if any_type.__doc__:
for line in any_type.__doc__.splitlines():
line = line.strip()
diff --git a/rosidl_cli/rosidl_cli/entry_points.py b/rosidl_cli/rosidl_cli/entry_points.py
index edab63729..6a79673f9 100644
--- a/rosidl_cli/rosidl_cli/entry_points.py
+++ b/rosidl_cli/rosidl_cli/entry_points.py
@@ -12,38 +12,41 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import importlib.metadata as importlib_metadata
import logging
-
-try:
- import importlib.metadata as importlib_metadata
-except ModuleNotFoundError:
- import importlib_metadata
+import sys
+from typing import Any, Dict, List, Optional, Tuple, Union
logger = logging.getLogger(__name__)
-def get_entry_points(group_name, *, specs=None, strict=False):
+def get_entry_points(group_name: str, *, specs: Optional[List[str]] = None, strict: bool = False
+ ) -> Dict[str, importlib_metadata.EntryPoint]:
"""
Get entry points from a specific group.
- :param str group_name: the name of the entry point group
- :param list specs: an optional collection of entry point names to retrieve
- :param bool strict: whether to raise or warn on error
+ :param group_name: the name of the entry point group
+ :param specs: an optional collection of entry point names to retrieve
+ :param strict: whether to raise or warn on error
:returns: mapping from entry point names to ``EntryPoint`` instances
- :rtype: dict
"""
if specs is not None:
- specs = set(specs)
+ specs_set = set(specs)
+ else:
+ specs_set = None
entry_points_impl = importlib_metadata.entry_points()
- if hasattr(entry_points_impl, 'select'):
+ # Select does not exist until python 3.10
+ if sys.version_info >= (3, 10):
groups = entry_points_impl.select(group=group_name)
else:
- groups = entry_points_impl.get(group_name, [])
- entry_points = {}
+ groups: Union[Tuple[importlib_metadata.EntryPoint, ...],
+ List[importlib_metadata.EntryPoint]] = entry_points_impl.get(group_name, [])
+
+ entry_points: Dict[str, importlib_metadata.EntryPoint] = {}
for entry_point in groups:
name = entry_point.name
- if specs and name not in specs:
+ if specs_set and name not in specs_set:
continue
if name in entry_points:
msg = (f"Found duplicate entry point '{name}': "
@@ -53,8 +56,8 @@ def get_entry_points(group_name, *, specs=None, strict=False):
logger.warning(msg)
continue
entry_points[name] = entry_point
- if specs:
- pending = specs - set(entry_points)
+ if specs_set:
+ pending = specs_set - set(entry_points)
if pending:
msg = 'Some specs could not be met: '
msg += ', '.join(map(str, pending))
@@ -64,21 +67,22 @@ def get_entry_points(group_name, *, specs=None, strict=False):
return entry_points
-def load_entry_points(group_name, *, strict=False, **kwargs):
+def load_entry_points(group_name: str, *, specs: Optional[List[str]],
+ strict: bool = False,
+ ) -> Dict[str, Any]:
"""
Load entry points for a specific group.
See :py:meth:`get_entry_points` for further reference on
additional keyword arguments.
- :param str group_name: the name of the entry point group
- :param bool strict: whether to raise or warn on error
+ :param group_name: the name of the entry point group
+ :param strict: whether to raise or warn on error
:returns: mapping from entry point name to loaded entry point
- :rtype: dict
"""
- loaded_entry_points = {}
+ loaded_entry_points: Dict[str, Any] = {}
for name, entry_point in get_entry_points(
- group_name, strict=strict, **kwargs
+ group_name, strict=strict, specs=specs
).items():
try:
loaded_entry_points[name] = entry_point.load()
diff --git a/rosidl_cli/rosidl_cli/extensions.py b/rosidl_cli/rosidl_cli/extensions.py
index 7cc31afa2..bbc28c9e7 100644
--- a/rosidl_cli/rosidl_cli/extensions.py
+++ b/rosidl_cli/rosidl_cli/extensions.py
@@ -14,10 +14,19 @@
import logging
import re
+from typing import Any, Dict, Final, List, Optional, Tuple, TYPE_CHECKING, Union
from rosidl_cli.entry_points import load_entry_points
-import yaml
+import yaml # type: ignore[import]
+
+if TYPE_CHECKING:
+ from typing import TypedDict
+ from typing_extensions import NotRequired
+
+ class LoadExtensionsArg(TypedDict):
+ specs: NotRequired[Optional[List[str]]]
+ strict: NotRequired[bool]
logger = logging.getLogger(__name__)
@@ -26,18 +35,18 @@
class Extension:
"""A generic extension point."""
- def __init__(self, name):
+ def __init__(self, name: str) -> None:
self.__name = name
@property
- def name(self):
+ def name(self) -> str:
return self.__name
-SPECS_PATTERN = re.compile(r'^(\w+)(?:\[(.+)\])?$')
+SPECS_PATTERN: Final = re.compile(r'^(\w+)(?:\[(.+)\])?$')
-def parse_extension_specification(spec):
+def parse_extension_specification(spec: str) -> Tuple[Union[str, Any], Union[Dict[Any, Any], Any]]:
"""
Parse extension specification.
@@ -64,18 +73,18 @@ def parse_extension_specification(spec):
return name, kwargs
-def load_extensions(group_name, *, specs=None, strict=False):
+def load_extensions(group_name: str, *, specs: Optional[List[str]] = None,
+ strict: bool = False) -> List[Extension]:
"""
Load extensions for a specific group.
- :param str group_name: the name of the extension group
- :param list specs: an optional collection of extension specs
+ :param group_name: the name of the extension group
+ :param specs: an optional collection of extension specs
(see :py:meth:`parse_extension_specification` for spec format)
- :param bool strict: whether to raise or warn on error
+ :param strict: whether to raise or warn on error
:returns: a list of :py:class:`Extension` instances
- :rtype: list
"""
- extensions = []
+ extensions: List[Extension] = []
if specs is not None:
kwargs = dict(map(
diff --git a/rosidl_cli/test/rosidl_cli/test_common.py b/rosidl_cli/test/rosidl_cli/test_common.py
index 166f8bf0f..ccc4a6cb7 100644
--- a/rosidl_cli/test/rosidl_cli/test_common.py
+++ b/rosidl_cli/test/rosidl_cli/test_common.py
@@ -15,20 +15,20 @@
from rosidl_cli.common import get_first_line_doc
-def test_getting_first_line_from_no_docstring():
+def test_getting_first_line_from_no_docstring() -> None:
func = test_getting_first_line_from_no_docstring
line = get_first_line_doc(func)
assert line == ''
-def test_getting_first_line_from_docstring():
+def test_getting_first_line_from_docstring() -> None:
"""Check it gets the first line."""
func = test_getting_first_line_from_docstring
line = get_first_line_doc(func)
assert line == 'Check it gets the first line'
-def test_getting_first_line_from_multiline_docstring():
+def test_getting_first_line_from_multiline_docstring() -> None:
"""
Check it really gets the first non-empty line.
diff --git a/rosidl_cli/test/rosidl_cli/test_extensions.py b/rosidl_cli/test/rosidl_cli/test_extensions.py
index 7e3dd2158..94af493af 100644
--- a/rosidl_cli/test/rosidl_cli/test_extensions.py
+++ b/rosidl_cli/test/rosidl_cli/test_extensions.py
@@ -17,7 +17,7 @@
from rosidl_cli.extensions import parse_extension_specification
-def test_extension_specification_parsing():
+def test_extension_specification_parsing() -> None:
with pytest.raises(ValueError):
parse_extension_specification('bad[')
diff --git a/rosidl_cli/test/rosidl_cli/test_helpers.py b/rosidl_cli/test/rosidl_cli/test_helpers.py
index 5a16f684f..bba66d4e9 100644
--- a/rosidl_cli/test/rosidl_cli/test_helpers.py
+++ b/rosidl_cli/test/rosidl_cli/test_helpers.py
@@ -15,6 +15,7 @@
import json
import os
import pathlib
+from typing import Iterable
import pytest
@@ -22,7 +23,7 @@
from rosidl_cli.command.helpers import legacy_generator_arguments_file
-def test_interface_path_as_tuple():
+def test_interface_path_as_tuple() -> None:
prefix, path = interface_path_as_tuple('/tmp:msg/Empty.idl')
assert pathlib.Path('msg/Empty.idl') == path
assert pathlib.Path(os.path.abspath('/tmp')) == prefix
@@ -37,7 +38,7 @@ def test_interface_path_as_tuple():
@pytest.fixture
-def current_path(request):
+def current_path(request: pytest.FixtureRequest) -> Iterable[pathlib.Path]:
path = pathlib.Path(request.module.__file__)
path = path.resolve()
path = path.parent
@@ -49,7 +50,7 @@ def current_path(request):
os.chdir(str(cwd))
-def test_legacy_generator_arguments_file(current_path):
+def test_legacy_generator_arguments_file(current_path: pathlib.Path) -> None:
with legacy_generator_arguments_file(
package_name='foo',
interface_files=['msg/Foo.idl'],
diff --git a/rosidl_cli/test/test_copyright.py b/rosidl_cli/test/test_copyright.py
index cf0fae31f..66a7d63eb 100644
--- a/rosidl_cli/test/test_copyright.py
+++ b/rosidl_cli/test/test_copyright.py
@@ -18,6 +18,6 @@
@pytest.mark.copyright
@pytest.mark.linter
-def test_copyright():
+def test_copyright() -> None:
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found errors'
diff --git a/rosidl_cli/test/test_flake8.py b/rosidl_cli/test/test_flake8.py
index 27ee1078f..eac16eef9 100644
--- a/rosidl_cli/test/test_flake8.py
+++ b/rosidl_cli/test/test_flake8.py
@@ -18,7 +18,7 @@
@pytest.mark.flake8
@pytest.mark.linter
-def test_flake8():
+def test_flake8() -> None:
rc, errors = main_with_errors(argv=[])
assert rc == 0, \
'Found %d code style errors / warnings:\n' % len(errors) + \
diff --git a/rosidl_cli/test/test_mypy.py b/rosidl_cli/test/test_mypy.py
new file mode 100644
index 000000000..97e4f502a
--- /dev/null
+++ b/rosidl_cli/test/test_mypy.py
@@ -0,0 +1,23 @@
+# Copyright 2024 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_mypy.main import main
+import pytest
+
+
+@pytest.mark.mypy
+@pytest.mark.linter
+def test_mypy() -> None:
+ rc = main(argv=[])
+ assert rc == 0, 'Found type errors!'
diff --git a/rosidl_cli/test/test_pep257.py b/rosidl_cli/test/test_pep257.py
index 0e38a6c60..4ae521a5a 100644
--- a/rosidl_cli/test/test_pep257.py
+++ b/rosidl_cli/test/test_pep257.py
@@ -18,6 +18,6 @@
@pytest.mark.linter
@pytest.mark.pep257
-def test_pep257():
+def test_pep257() -> None:
rc = main(argv=[])
assert rc == 0, 'Found code style errors / warnings'
diff --git a/rosidl_cli/test/test_xmllint.py b/rosidl_cli/test/test_xmllint.py
index f46285e71..08bf7fd78 100644
--- a/rosidl_cli/test/test_xmllint.py
+++ b/rosidl_cli/test/test_xmllint.py
@@ -18,6 +18,6 @@
@pytest.mark.linter
@pytest.mark.xmllint
-def test_xmllint():
+def test_xmllint() -> None:
rc = main(argv=[])
assert rc == 0, 'Found errors'