Skip to content

Commit

Permalink
Add rosidl generate CLI. (#567)
Browse files Browse the repository at this point in the history
Signed-off-by: Michel Hidalgo <[email protected]>
Co-authored-by: Shane Loretz <[email protected]>
  • Loading branch information
hidmic and sloretz authored Mar 3, 2021
1 parent 57befb8 commit f0b42cf
Show file tree
Hide file tree
Showing 23 changed files with 896 additions and 0 deletions.
19 changes: 19 additions & 0 deletions rosidl_cli/completion/rosidl-argcomplete.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 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.

if type register-python-argcomplete3 > /dev/null 2>&1; then
eval "$(register-python-argcomplete3 rosidl)"
elif type register-python-argcomplete > /dev/null 2>&1; then
eval "$(register-python-argcomplete rosidl)"
fi
23 changes: 23 additions & 0 deletions rosidl_cli/completion/rosidl-argcomplete.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 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.

autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit

# Get this scripts directory
__rosidl_cli_completion_dir=${0:a:h}
# Just source the bash version, it works in zsh too
source "$__rosidl_cli_completion_dir/rosidl-argcomplete.bash"
# Cleanup
unset __rosidl_cli_completion_dir
27 changes: 27 additions & 0 deletions rosidl_cli/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
<name>rosidl_cli</name>
<version>0.1.0</version>
<description>
Command line tools for ROS interface generation.
</description>
<maintainer email="[email protected]">Chris Lalancette</maintainer>
<maintainer email="[email protected]">Shane Loretz</maintainer>
<license>Apache License 2.0</license>

<author email="[email protected]">Michel Hidalgo</author>

<exec_depend>python3-argcomplete</exec_depend>
<exec_depend>python3-importlib-metadata</exec_depend>

<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>ament_xmllint</test_depend>
<test_depend>python3-pytest</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>
2 changes: 2 additions & 0 deletions rosidl_cli/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
junit_family=xunit2
2 changes: 2 additions & 0 deletions rosidl_cli/resource/package.dsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
source;share/rosidl_cli/environment/rosidl-argcomplete.bash
source;share/rosidl_cli/environment/rosidl-argcomplete.zsh
Empty file added rosidl_cli/resource/rosidl_cli
Empty file.
Empty file.
103 changes: 103 additions & 0 deletions rosidl_cli/rosidl_cli/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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 argparse
import signal

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


def add_subparsers(parser, cli_name, commands):
"""
Create argparse subparser for each command.
The ``cli_name`` is used for the title and description of the
``add_subparsers`` function call.
For each command a subparser is created.
:param parser: the parent argument parser
:type parser: :py:class:`argparse.ArgumentParser`
:param str cli_name: name of the command line command to which the
subparsers are being added
:param commands: each of the commands contributing specific arguments
:type commands: :py:class:`List[Command]`
"""
# add subparser with description of available subparsers
description = ''

commands = sorted(commands, key=lambda command: command.name)
max_length = max(len(command.name) for command in commands)
for command in commands:
description += '%s %s\n' % (
command.name.ljust(max_length),
get_first_line_doc(command))
subparser = parser.add_subparsers(
title='Commands', description=description,
metavar=f'Call `{cli_name} <command> -h` for more detailed usage.')
subparser.dest = '_command'
subparser.required = True

# add extension specific sub-sub-parser with its arguments
for command in commands:
command_parser = subparser.add_parser(
command.name,
description=get_first_line_doc(command),
formatter_class=argparse.RawDescriptionHelpFormatter)
command_parser.set_defaults(_command=command)
command.add_arguments(command_parser)

return subparser


def main():
script_name = 'rosidl'
description = f'{script_name} is an extensible command-line tool ' \
'for ROS interface generation.'

# top level parser
parser = argparse.ArgumentParser(
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter
)

commands = [GenerateCommand()]

# add arguments for command extension(s)
add_subparsers(
parser,
script_name,
commands
)

# register argcomplete hook if available
try:
from argcomplete import autocomplete
except ImportError:
pass
else:
autocomplete(parser, exclude=['-h', '--help'])

# parse the command line arguments
args = parser.parse_args()

# call the main method of the command
try:
rc = args._command.main(args=args)
except KeyboardInterrupt:
rc = signal.SIGINT
except (ValueError, RuntimeError) as e:
rc = str(e)
return rc
29 changes: 29 additions & 0 deletions rosidl_cli/rosidl_cli/command/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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.


class Command:
"""
The extension point for 'command' extensions.
The following methods must be defined:
* `main`
* `add_arguments`
"""

def add_arguments(self, parser):
pass

def main(self, *, parser, args):
raise NotImplementedError()
82 changes: 82 additions & 0 deletions rosidl_cli/rosidl_cli/command/generate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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 pathlib

from rosidl_cli.command import Command

from .extensions import load_type_extensions
from .extensions import load_typesupport_extensions


class GenerateCommand(Command):
"""Generate source code from interface definition files."""

name = 'generate'

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 '.'."))
parser.add_argument(
'-t', '--type', metavar='TYPE_SPEC',
dest='type_specs', action='append', default=[],
help='Target type representations for generation.')
parser.add_argument(
'-ts', '--type-support', metavar='TYPESUPPORT_SPEC',
dest='typesupport_specs', action='append', default=[],
help='Target type supports for generation.')
parser.add_argument(
'-I', '--include-path', type=pathlib.Path, metavar='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 to generate code for')
parser.add_argument(
'interface_files', metavar='interface_file', nargs='+',
help=('Normalized relative path to 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
)
57 changes: 57 additions & 0 deletions rosidl_cli/rosidl_cli/command/generate/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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 GenerateCommandExtension(Extension):
"""
The extension point for source code generation.
The following methods must be defined:
* `generate`
"""

def generate(
self,
package_name,
interface_files,
include_paths,
output_path
):
"""
Generate source code.
Paths to interface definition files are relative paths optionally
prefixed by an absolute path followed by a colon ':', in which case
path resolution is to be performed against that absolute path.
: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: list of paths to include dependency interface
definition files from.
:param output_path: path to directory to hold generated source code files
"""
raise NotImplementedError()


def load_type_extensions(**kwargs):
"""Load extensions for type representation source code generation."""
return load_extensions('rosidl_cli.command.generate.type_extensions', **kwargs)


def load_typesupport_extensions(**kwargs):
"""Load extensions for type support source code generation."""
return load_extensions('rosidl_cli.command.generate.typesupport_extensions', **kwargs)
Loading

0 comments on commit f0b42cf

Please sign in to comment.