diff --git a/rosidl_cli/rosidl_cli/command/generate/__init__.py b/rosidl_cli/rosidl_cli/command/generate/__init__.py index b65b95f57..ee46a937a 100644 --- a/rosidl_cli/rosidl_cli/command/generate/__init__.py +++ b/rosidl_cli/rosidl_cli/command/generate/__init__.py @@ -16,8 +16,7 @@ from rosidl_cli.command import Command -from .extensions import load_type_extensions -from .extensions import load_typesupport_extensions +from .api import generate class GenerateCommand(Command): @@ -27,17 +26,17 @@ class GenerateCommand(Command): def add_arguments(self, parser): parser.add_argument( - '-o', '--output-path', type=pathlib.Path, - metavar='PATH', default=pathlib.Path.cwd(), - help=('Path to directory to hold generated source code files. ' - "Defaults to '.'.")) + '-o', '--output-path', metavar='PATH', + type=pathlib.Path, default=None, + help=('Path to directory to hold generated ' + "source code files. Defaults to '.'.")) parser.add_argument( - '-t', '--type', metavar='TYPE_SPEC', - dest='type_specs', action='append', default=[], + '-t', '--type', metavar='TYPE', + dest='types', action='append', default=[], help='Target type representations for generation.') parser.add_argument( - '-ts', '--type-support', metavar='TYPESUPPORT_SPEC', - dest='typesupport_specs', action='append', default=[], + '-ts', '--type-support', metavar='TYPESUPPORT', + dest='typesupports', action='append', default=[], help='Target type supports for generation.') parser.add_argument( '-I', '--include-path', type=pathlib.Path, metavar='PATH', @@ -47,36 +46,16 @@ def add_arguments(self, parser): 'package_name', help='Name of the package to generate code for') parser.add_argument( 'interface_files', metavar='interface_file', nargs='+', - help=('Normalized relative path to interface definition file. ' + help=('Relative path to an interface definition file. ' "If prefixed by another path followed by a colon ':', " 'path resolution is performed against such path.')) def main(self, *, args): - extensions = [] - - unspecific_generation = \ - not args.type_specs and not args.typesupport_specs - - if args.type_specs or unspecific_generation: - extensions.extend(load_type_extensions( - specs=args.type_specs, - strict=not unspecific_generation)) - - if args.typesupport_specs or unspecific_generation: - extensions.extend(load_typesupport_extensions( - specs=args.typesupport_specs, - strict=not unspecific_generation)) - - if unspecific_generation and not extensions: - return 'No type nor typesupport extensions were found' - - if len(extensions) > 1: - for extension in extensions: - extension.generate( - args.package_name, args.interface_files, args.include_paths, - output_path=args.output_path / extension.name) - else: - extensions[0].generate( - args.package_name, args.interface_files, - args.include_paths, args.output_path - ) + generate( + package_name=args.package_name, + interface_files=args.interface_files, + include_paths=args.include_paths, + output_path=args.output_path, + types=args.types, + typesupports=args.typesupports + ) diff --git a/rosidl_cli/rosidl_cli/command/generate/api.py b/rosidl_cli/rosidl_cli/command/generate/api.py new file mode 100644 index 000000000..ebec89144 --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/generate/api.py @@ -0,0 +1,99 @@ +# Copyright 2021 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 os +import pathlib + +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 +): + """ + Generate source code from interface definition files. + + To do so, this function leverages type representation and type + support generation support as provided by third-party package + extensions. + + Each path to an interface definition file is a relative path optionally + prefixed by another path followed by a colon ':', against which the first + relative path is to be resolved. + + The directory structure that these relative paths exhibit will be replicated + on output (as opposed to the prefix path, which will be ignored). + + If no type representation nor type support is specified, all available ones + will be generated. + + If more than one type representation or type support is generated, the + name of each will be appended to the given `output_path` to preclude + name clashes upon writing source code files. + + :param package_name: name of the package to generate source code for + :param interface_files: list of paths to interface definition files + :param include_paths: optional list of paths to include dependency + interface definition files from + :param output_path: optional path to directory to hold generated + source code files, defaults to the current working directory + :param types: optional list of type representations to generate + :param typesupports: optional list of type supports to generate + :returns: list of lists of paths to generated source code files, + one group per type or type support extension invoked + """ + extensions = [] + + unspecific_generation = not types and not typesupports + + if types or unspecific_generation: + extensions.extend(load_type_extensions( + specs=types, + strict=not unspecific_generation)) + + if typesupports or unspecific_generation: + extensions.extend(load_typesupport_extensions( + specs=typesupports, + strict=not unspecific_generation)) + + if unspecific_generation and not extensions: + raise RuntimeError('No type nor typesupport extensions were found') + + if include_paths is None: + include_paths = [] + + if output_path is None: + output_path = pathlib.Path.cwd() + else: + os.makedirs(output_path, exist_ok=True) + + if len(extensions) > 1: + return [ + extension.generate( + package_name, interface_files, include_paths, + output_path=output_path / extension.name) + for extension in extensions + ] + + return [extensions[0].generate( + package_name, interface_files, + include_paths, output_path + )] diff --git a/rosidl_cli/rosidl_cli/command/generate/extensions.py b/rosidl_cli/rosidl_cli/command/generate/extensions.py index 4c976d7e7..fde1fb6b0 100644 --- a/rosidl_cli/rosidl_cli/command/generate/extensions.py +++ b/rosidl_cli/rosidl_cli/command/generate/extensions.py @@ -43,6 +43,7 @@ def generate( :param include_paths: list of paths to include dependency interface definition files from. :param output_path: path to directory to hold generated source code files + :returns: list of paths to generated source files """ raise NotImplementedError() diff --git a/rosidl_cli/rosidl_cli/command/translate/__init__.py b/rosidl_cli/rosidl_cli/command/translate/__init__.py index 20607d25b..03798db3e 100644 --- a/rosidl_cli/rosidl_cli/command/translate/__init__.py +++ b/rosidl_cli/rosidl_cli/command/translate/__init__.py @@ -12,13 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import collections -import os import pathlib from rosidl_cli.command import Command -from .extensions import load_translate_extensions +from .api import translate class TranslateCommand(Command): @@ -29,14 +27,15 @@ class TranslateCommand(Command): def add_arguments(self, parser): parser.add_argument( '-o', '--output-path', metavar='PATH', - type=pathlib.Path, default=pathlib.Path.cwd(), - help=('Path to directory to hold translated interface definition' - "files. Defaults to '.'.")) + type=pathlib.Path, default=None, + help=('Path to directory to hold translated interface ' + "definition files. Defaults to '.'.") + ) parser.add_argument( - '--use', '--translator', metavar='TRANSLATOR_SPEC', - dest='translator_specs', action='append', default=[], - help=('Translators to be used. If none is given, ' - 'suitable available ones will be used.') + '--use', '--translator', metavar='TRANSLATOR', + dest='translators', action='append', default=[], + help=('Translator to be used. If none is specified, ' + 'all available ones will be considered.') ) parser.add_argument( '--to', '--output-format', required=True, @@ -60,39 +59,18 @@ def add_arguments(self, parser): help='Name of the package all interface files belong to') parser.add_argument( 'interface_files', metavar='interface_file', nargs='+', - help=('Normalized relative path to an interface definition file. ' + help=('Relative path to an interface definition file. ' "If prefixed by another path followed by a colon ':', " 'path resolution is performed against such path.') ) def main(self, *, args): - extensions = load_translate_extensions( - specs=args.translator_specs, - strict=any(args.translator_specs) + translate( + package_name=args.package_name, + interface_files=args.interface_files, + output_format=args.output_format, + input_format=args.input_format, + include_paths=args.include_paths, + output_path=args.output_path, + translators=args.translators ) - if not extensions: - return 'No translate extensions found' - - if not args.input_format: - interface_files_per_format = collections.defaultdict(list) - for interface_file in args.interface_files: - input_format = os.path.splitext(interface_file)[-1][1:] - interface_files_per_format[input_format].append(interface_file) - else: - interface_files_per_format = { - args.input_format: args.interface_files} - - for input_format, interface_files in interface_files_per_format.items(): - extension = next(( - extension for extension in extensions - if extension.input_format == input_format and - extension.output_format == args.output_format - ), None) - - if not extension: - return (f"Translation from '{input_format}' to " - f"'{args.output_format}' is not supported") - - extension.translate( - args.package_name, interface_files, - args.include_paths, args.output_path) diff --git a/rosidl_cli/rosidl_cli/command/translate/api.py b/rosidl_cli/rosidl_cli/command/translate/api.py new file mode 100644 index 000000000..b63db278e --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/translate/api.py @@ -0,0 +1,100 @@ +# Copyright 2021 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 collections +import os +import pathlib + +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 +): + """ + Translate interface definition files from one format to another. + + To do so, this function leverages translation support as provided + by third-party package extensions. + + Each path to an interface definition file is a relative path optionally + prefixed by another path followed by a colon ':', against which the first + relative path is to be resolved. + + The directory structure that these relative paths exhibit will be + replicated on output (as opposed to the prefix path, which will be + ignored). + + If no translators are specified, all available ones will be considered. + + :param package_name: name of the package all interface files belong to + :param interface_files: list of paths to interface definition files + :param output_format: format to translate interface definition files to + :param input_format: optional format to assume for all interface + definition files, deduced from file extensions if not given + :param include_paths: optional list of paths to include dependency + interface definition files from + :param output_path: optional path to directory to hold translated + interface definition files, defaults to the current working directory + :param translators: optional list of translators to use + :returns: list of paths to translated interface definition files + """ + extensions = load_translate_extensions( + specs=translators, strict=bool(translators) + ) + if not extensions: + raise RuntimeError('No translate extensions found') + + if not input_format: + interface_files_per_format = 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) + else: + interface_files_per_format = {input_format: interface_files} + + if include_paths is None: + include_paths = [] + + if output_path is None: + output_path = pathlib.Path.cwd() + else: + os.makedirs(output_path, exist_ok=True) + + translated_interface_files = [] + for input_format, interface_files in interface_files_per_format.items(): + extension = next(( + extension for extension in extensions + if extension.input_format == input_format and + extension.output_format == output_format + ), None) + + if not extension: + raise RuntimeError('\n'.join([ + f"Cannot translate the following files to '{output_format}' format:", + *[f'- {path}' for path in interface_files], + 'No translator found' + ])) + + translated_interface_files.extend(extension.translate( + package_name, interface_files, include_paths, output_path)) + + return translated_interface_files diff --git a/rosidl_cli/rosidl_cli/command/translate/extensions.py b/rosidl_cli/rosidl_cli/command/translate/extensions.py index 1cb97b5d3..f193a4804 100644 --- a/rosidl_cli/rosidl_cli/command/translate/extensions.py +++ b/rosidl_cli/rosidl_cli/command/translate/extensions.py @@ -52,6 +52,7 @@ def translate( definition files from :param output_path: path to directory to hold translated interface definition files + :returns: list of paths to translated interface definition files """ raise NotImplementedError()