Skip to content

Commit

Permalink
Add rosidl translate CLI.
Browse files Browse the repository at this point in the history
Signed-off-by: Michel Hidalgo <[email protected]>
  • Loading branch information
hidmic committed Feb 26, 2021
1 parent f7a9d3a commit 68d9210
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 20 deletions.
3 changes: 2 additions & 1 deletion rosidl_cli/rosidl_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import signal

from rosidl_cli.command.generate import GenerateCommand
from rosidl_cli.command.translate import TranslateCommand
from rosidl_cli.common import get_first_line_doc


Expand Down Expand Up @@ -81,7 +82,7 @@ def main(*, script_name='rosidl', argv=None, description=None, commands=None):
)

if commands is None:
commands = [GenerateCommand()]
commands = [GenerateCommand(), TranslateCommand()]

# add arguments for command extension(s)
add_subparsers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,33 +43,45 @@ def dependencies_from_include_paths(include_paths):
})


def idl_tuples_from_interface_files(interface_files):
def interface_path_as_tuple(path):
"""
Express ROS interface definition file paths as IDL tuples.
Express interface definition file path as an
(absolute prefix, relative path) tuple.
An IDL tuple is a relative path prefixed by by an absolute path against
which to resolve it followed by a colon ':'. This function then applies
the following logic:
- If a given path follows this pattern, it is passed through.
An interface definition file path is a relative path, optionally prefixed
by a path against which to resolve the former followed by a colon ':'.
Thus, this function applies following logic:
- If a given path follows this pattern, it is split at the colon ':'
- If a given path is prefixed by a relative path, it is resolved
relative to the current working directory.
- If a given path has no prefixes, the current working directory is
- If a given path has no prefix, the current working directory is
used as prefix.
"""
path_as_string = str(path)
if ':' not in path_as_string:
prefix = pathlib.Path.cwd()
else:
prefix, _, path = path_as_string.rpartition(':')
prefix = pathlib.Path(prefix).resolve()
path = pathlib.Path(path)
if path.is_absolute():
raise ValueError('Interface definition file path '
f"'{path}' cannot be absolute")
return prefix, path


def idl_tuples_from_interface_files(interface_files):
"""
Express ROS interface definition file paths as IDL tuples.
An IDL tuple is a relative path prefixed by an absolute path against
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:
path_as_string = str(path)
if ':' not in path_as_string:
prefix = pathlib.Path.cwd()
stem = path
else:
prefix, _, stem = path_as_string.rpartition(':')
prefix = pathlib.Path(prefix).resolve()
stem = pathlib.Path(stem)
if stem.is_absolute():
raise ValueError('Interface definition file path '
f'{stem} cannot be absolute')
idl_tuples.append(f'{prefix}:{stem.as_posix()}')
prefix, path = interface_path_as_tuple(path)
idl_tuples.append(f'{prefix}:{path.as_posix()}')
return idl_tuples


Expand Down
102 changes: 102 additions & 0 deletions rosidl_cli/rosidl_cli/command/translate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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 rosidl_cli.command import Command

from .extensions import load_translate_extensions


class TranslateCommand(Command):
"""Translate interface definition files from one format to another."""

name = 'translate'

def add_arguments(self, parser, cli_name):
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 '.'."))
parser.add_argument(
'--use', '--translator', metavar='TRANSLATOR_SPEC',
dest='translator_specs', action='append', default=[],
help=('Target translators, backed by tool extensions. '
'Specified by name plus an optional PEP440 version '
'specifier. If none is given, suitable ones among '
'all available translators will be chosen.')
)
parser.add_argument(
'--to', '--output-format', metavar='FORMAT',
dest='output_format', required=True,
help=('Output format for translate interface definition files. '
'Specified by name plus an optional PEP440 version.')
)
parser.add_argument(
'--from', '--input-format', metavar='FORMAT',
dest='input_format', default=None,
help=('Input format for all source interface definition files. '
'Specified by name plus an optional PEP440 version. '
'If not given, file extensions will be used to deduce '
'the format of each interface definition file.')
)
parser.add_argument(
'-I', '--include-path', metavar='PATH', type=pathlib.Path,
dest='include_paths', action='append', default=[],
help='Paths to include dependency interface definition files from.'
)
parser.add_argument(
'package_name',
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. '
"If prefixed by another path followed by a colon ':', "
'path resolution is performed against such path.')
)

def main(self, *, parser, args):
extensions = load_translate_extensions(
specs=args.translator_specs,
strict=any(args.translator_specs)
)
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)
63 changes: 63 additions & 0 deletions rosidl_cli/rosidl_cli/command/translate/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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.

from rosidl_cli.extensions import Extension
from rosidl_cli.extensions import load_extensions


class TranslateCommandExtension(Extension):
"""
The extension point for interface definition translation.
The following attributes must be defined
* `input_format`
* `output_format`
The following methods must be defined:
* `translate`
"""

def translate(
self,
package_name,
interface_file,
include_paths,
output_path
):
"""
Translate interface definition file.
The path to an interface definition file is a relative path optionally
prefixed by an absolute path followed by a colon ':', in which case
path resolution is to be performed against that absolute path.
On output, the directory structure specified by this relative path
will be replicated e.g. an ``msg/Empty.foo`` file will result in a
``msg/Empty.bar`` file under `output_path`.
:param package_name: name of the package `interface_file` belongs to
:param interface_file: path to the interface definition file
:param include_paths: list of paths to include dependency interface
definition files from
:param output_path: path to directory to hold the translated interface
definition file
"""
raise NotImplementedError()


def load_translate_extensions(**kwargs):
"""Load extensions for interface definition translation."""
return load_extensions(
'rosidl_cli.command.translate.extensions', **kwargs
)

0 comments on commit 68d9210

Please sign in to comment.