Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rosidl translate CLI. #575

Merged
merged 2 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -73,7 +74,7 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter
)

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 @@ -45,33 +45,44 @@ 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(os.path.abspath(prefix))
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 = os.path.abspath(prefix)
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
98 changes: 98 additions & 0 deletions rosidl_cli/rosidl_cli/command/translate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 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):
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=('Translators to be used. If none is given, '
'suitable available ones will be used.')
)
parser.add_argument(
'--to', '--output-format', required=True,
metavar='FORMAT', dest='output_format',
help='Output format for translate interface definition files.'
)
parser.add_argument(
'--from', '--input-format', default=None,
metavar='FORMAT', dest='input_format',
help=('Input format for all source interface definition files. '
'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, *, 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_files,
include_paths,
output_path
):
"""
Translate interface definition files.

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_files: list of paths to interface definition files
:param include_paths: list of paths to include dependency interface
definition files from
:param output_path: path to directory to hold translated interface
definition files
"""
raise NotImplementedError()


def load_translate_extensions(**kwargs):
"""Load extensions for interface definition translation."""
return load_extensions(
'rosidl_cli.command.translate.extensions', **kwargs
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,22 @@

import pytest

from rosidl_cli.command.generate.helpers import legacy_generator_arguments_file
from rosidl_cli.command.helpers import interface_path_as_tuple
from rosidl_cli.command.helpers import legacy_generator_arguments_file


def test_interface_path_as_tuple():
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

prefix, path = interface_path_as_tuple('tmp:msg/Empty.idl')
assert pathlib.Path('msg/Empty.idl') == path
assert pathlib.Path.cwd() / 'tmp' == prefix

prefix, path = interface_path_as_tuple('msg/Empty.idl')
assert pathlib.Path('msg/Empty.idl') == path
assert pathlib.Path.cwd() == prefix


@pytest.fixture
Expand Down