Skip to content

Commit c54b80a

Browse files
committed
Add rosidl translate CLI.
Signed-off-by: Michel Hidalgo <[email protected]>
1 parent 76bd5cc commit c54b80a

File tree

4 files changed

+194
-20
lines changed

4 files changed

+194
-20
lines changed

rosidl_cli/rosidl_cli/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import signal
1717

1818
from rosidl_cli.command.generate import GenerateCommand
19+
from rosidl_cli.command.translate import TranslateCommand
1920
from rosidl_cli.common import get_first_line_doc
2021

2122

@@ -73,7 +74,7 @@ def main():
7374
formatter_class=argparse.RawDescriptionHelpFormatter
7475
)
7576

76-
commands = [GenerateCommand()]
77+
commands = [GenerateCommand(), TranslateCommand()]
7778

7879
# add arguments for command extension(s)
7980
add_subparsers(

rosidl_cli/rosidl_cli/command/generate/helpers.py renamed to rosidl_cli/rosidl_cli/command/helpers.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,45 @@ def dependencies_from_include_paths(include_paths):
4343
})
4444

4545

46-
def idl_tuples_from_interface_files(interface_files):
46+
def interface_path_as_tuple(path):
4747
"""
48-
Express ROS interface definition file paths as IDL tuples.
48+
Express interface definition file path as an
49+
(absolute prefix, relative path) tuple.
4950
50-
An IDL tuple is a relative path prefixed by by an absolute path against
51-
which to resolve it followed by a colon ':'. This function then applies
52-
the following logic:
53-
- If a given path follows this pattern, it is passed through.
51+
An interface definition file path is a relative path, optionally prefixed
52+
by a path against which to resolve the former followed by a colon ':'.
53+
Thus, this function applies following logic:
54+
- If a given path follows this pattern, it is split at the colon ':'
5455
- If a given path is prefixed by a relative path, it is resolved
5556
relative to the current working directory.
56-
- If a given path has no prefixes, the current working directory is
57+
- If a given path has no prefix, the current working directory is
5758
used as prefix.
5859
"""
60+
path_as_string = str(path)
61+
if ':' not in path_as_string:
62+
prefix = pathlib.Path.cwd()
63+
else:
64+
prefix, _, path = path_as_string.rpartition(':')
65+
prefix = pathlib.Path(prefix).resolve()
66+
path = pathlib.Path(path)
67+
if path.is_absolute():
68+
raise ValueError('Interface definition file path '
69+
f"'{path}' cannot be absolute")
70+
return prefix, path
71+
72+
73+
def idl_tuples_from_interface_files(interface_files):
74+
"""
75+
Express ROS interface definition file paths as IDL tuples.
76+
77+
An IDL tuple is a relative path prefixed by an absolute path against
78+
which to resolve it followed by a colon ':'. This function then applies
79+
the same logic as `interface_path_as_tuple`.
80+
"""
5981
idl_tuples = []
6082
for path in interface_files:
61-
path_as_string = str(path)
62-
if ':' not in path_as_string:
63-
prefix = pathlib.Path.cwd()
64-
stem = path
65-
else:
66-
prefix, _, stem = path_as_string.rpartition(':')
67-
prefix = pathlib.Path(prefix).resolve()
68-
stem = pathlib.Path(stem)
69-
if stem.is_absolute():
70-
raise ValueError('Interface definition file path '
71-
f'{stem} cannot be absolute')
72-
idl_tuples.append(f'{prefix}:{stem.as_posix()}')
83+
prefix, path = interface_path_as_tuple(path)
84+
idl_tuples.append(f'{prefix}:{path.as_posix()}')
7385
return idl_tuples
7486

7587

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright 2021 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import collections
16+
import os
17+
import pathlib
18+
19+
from rosidl_cli.command import Command
20+
21+
from .extensions import load_translate_extensions
22+
23+
24+
class TranslateCommand(Command):
25+
"""Translate interface definition files from one format to another."""
26+
27+
name = 'translate'
28+
29+
def add_arguments(self, parser):
30+
parser.add_argument(
31+
'-o', '--output-path', metavar='PATH',
32+
type=pathlib.Path, default=pathlib.Path.cwd(),
33+
help=('Path to directory to hold translated interface definition'
34+
"files. Defaults to '.'."))
35+
parser.add_argument(
36+
'--use', '--translator', metavar='TRANSLATOR_SPEC',
37+
dest='translator_specs', action='append', default=[],
38+
help=('Translators to be used. If none is given, '
39+
'suitable available ones will be used.')
40+
)
41+
parser.add_argument(
42+
'--to', '--output-format', required=True,
43+
metavar='FORMAT', dest='output_format',
44+
help='Output format for translate interface definition files.'
45+
)
46+
parser.add_argument(
47+
'--from', '--input-format', default=None,
48+
metavar='FORMAT', dest='input_format',
49+
help=('Input format for all source interface definition files. '
50+
'If not given, file extensions will be used to deduce '
51+
'the format of each interface definition file.')
52+
)
53+
parser.add_argument(
54+
'-I', '--include-path', metavar='PATH', type=pathlib.Path,
55+
dest='include_paths', action='append', default=[],
56+
help='Paths to include dependency interface definition files from.'
57+
)
58+
parser.add_argument(
59+
'package_name',
60+
help='Name of the package all interface files belong to')
61+
parser.add_argument(
62+
'interface_files', metavar='interface_file', nargs='+',
63+
help=('Normalized relative path to an interface definition file. '
64+
"If prefixed by another path followed by a colon ':', "
65+
'path resolution is performed against such path.')
66+
)
67+
68+
def main(self, *, args):
69+
extensions = load_translate_extensions(
70+
specs=args.translator_specs,
71+
strict=any(args.translator_specs)
72+
)
73+
if not extensions:
74+
return 'No translate extensions found'
75+
76+
if not args.input_format:
77+
interface_files_per_format = collections.defaultdict(list)
78+
for interface_file in args.interface_files:
79+
input_format = os.path.splitext(interface_file)[-1][1:]
80+
interface_files_per_format[input_format].append(interface_file)
81+
else:
82+
interface_files_per_format = {
83+
args.input_format: args.interface_files}
84+
85+
for input_format, interface_files in interface_files_per_format.items():
86+
extension = next((
87+
extension for extension in extensions
88+
if extension.input_format == input_format and \
89+
extension.output_format == args.output_format
90+
), None)
91+
92+
if not extension:
93+
return (f"Translation from '{input_format}' to "
94+
f"'{args.output_format}' is not supported")
95+
96+
extension.translate(
97+
args.package_name, interface_files,
98+
args.include_paths, args.output_path)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2021 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from rosidl_cli.extensions import Extension
16+
from rosidl_cli.extensions import load_extensions
17+
18+
19+
class TranslateCommandExtension(Extension):
20+
"""
21+
The extension point for interface definition translation.
22+
23+
The following attributes must be defined
24+
* `input_format`
25+
* `output_format`
26+
27+
The following methods must be defined:
28+
* `translate`
29+
"""
30+
31+
def translate(
32+
self,
33+
package_name,
34+
interface_files,
35+
include_paths,
36+
output_path
37+
):
38+
"""
39+
Translate interface definition files.
40+
41+
The path to an interface definition file is a relative path optionally
42+
prefixed by an absolute path followed by a colon ':', in which case
43+
path resolution is to be performed against that absolute path.
44+
45+
On output, the directory structure specified by this relative path
46+
will be replicated e.g. an ``msg/Empty.foo`` file will result in a
47+
``msg/Empty.bar`` file under `output_path`.
48+
49+
:param package_name: name of the package `interface_file` belongs to
50+
:param interface_files: list of paths to interface definition files
51+
:param include_paths: list of paths to include dependency interface
52+
definition files from
53+
:param output_path: path to directory to hold translated interface
54+
definition files
55+
"""
56+
raise NotImplementedError()
57+
58+
59+
def load_translate_extensions(**kwargs):
60+
"""Load extensions for interface definition translation."""
61+
return load_extensions(
62+
'rosidl_cli.command.translate.extensions', **kwargs
63+
)

0 commit comments

Comments
 (0)