From 63d018da215985992171f488bcf571d8419e10c4 Mon Sep 17 00:00:00 2001 From: Michel Hidalgo Date: Fri, 4 Jun 2021 11:18:24 -0300 Subject: [PATCH 1/3] Support passing keyword arguments to rosidl CLI extensions Signed-off-by: Michel Hidalgo --- rosidl_cli/rosidl_cli/extensions.py | 58 ++++++++++++++++--- rosidl_cli/test/rosidl_cli/test_extensions.py | 36 ++++++++++++ 2 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 rosidl_cli/test/rosidl_cli/test_extensions.py diff --git a/rosidl_cli/rosidl_cli/extensions.py b/rosidl_cli/rosidl_cli/extensions.py index 14488df66..935d12c94 100644 --- a/rosidl_cli/rosidl_cli/extensions.py +++ b/rosidl_cli/rosidl_cli/extensions.py @@ -13,6 +13,9 @@ # limitations under the License. import logging +import re + +import yaml from rosidl_cli.entry_points import load_entry_points @@ -23,7 +26,7 @@ class Extension: """A generic extension point.""" - def __init__(self, name): + def __init__(self, name, **kwargs): self.__name = name @property @@ -31,26 +34,65 @@ def name(self): return self.__name -def load_extensions(group_name, *, strict=False, **kwargs): +SPECS_PATTERN = re.compile(r'^(\w+)(?:\[(.+)\])?$') + + +def parse_extension_specification(spec): """ - Load extensions for a specific group. + Parse extension specification. - See :py:function:`load_entry_points` for further reference on - additional keyword arguments. + :param str spec: specification string in + 'name[key0: value0, ...]' or 'name' format. + Key-value pairs are parsed as YAML dictionaries. + :returns: a tuple of specification name and + keyword arguments, if any, as a dict. + """ + match = SPECS_PATTERN.match(spec) + if not match: + raise ValueError(f'{spec} is not a valid spec') + name = match.group(1) + kwargs = match.group(2) + if kwargs is not None: + try: + kwargs = yaml.safe_load('{' + kwargs + '}') + except Exception as e: + raise ValueError( + f'{spec} is not a valid spec' + ) from e + else: + kwargs = {} + return name, kwargs + + +def load_extensions(group_name, *, specs=None, strict=False): + """ + Load extensions for a specific group. :param str group_name: the name of the extension group + :param list specs: an optional collection of extension specs + (see :py:function:`parse_extension_specification` for spec format) :param bool strict: whether to raise or warn on error :returns: a list of :py:class:`Extension` instances :rtype: list """ extensions = [] + + if specs is not None: + kwargs = dict(map( + parse_extension_specification, specs)) + specs = list(kwargs.keys()) + else: + kwargs = {} + for name, factory in load_entry_points( - group_name, strict=strict, **kwargs + group_name, specs=specs, strict=strict ).items(): try: - extensions.append(factory(name)) + extensions.append(factory(name, **kwargs.get(name, {}))) except Exception as e: - msg = f"Failed to instantiate extension '{name}': {e}" + msg = f"Failed to instantiate extension '{name}' " + which = kwargs.get(name, 'default') + msg += f"with '{which}' arguments: {e}" if strict: raise RuntimeError(msg) logger.warning(msg) diff --git a/rosidl_cli/test/rosidl_cli/test_extensions.py b/rosidl_cli/test/rosidl_cli/test_extensions.py new file mode 100644 index 000000000..13ec432df --- /dev/null +++ b/rosidl_cli/test/rosidl_cli/test_extensions.py @@ -0,0 +1,36 @@ +# 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 parse_extension_specification + +import pytest + + +def test_extension_specification_parsing(): + with pytest.raises(ValueError): + parse_extension_specification("bad[") + + with pytest.raises(ValueError): + parse_extension_specification("bad[]") + + with pytest.raises(ValueError): + parse_extension_specification("bad[:]") + + name, kwargs = parse_extension_specification("no_args") + assert name == 'no_args' + assert kwargs == {} + + name, kwargs = parse_extension_specification("with_args[key: value]") + assert name == 'with_args' + assert kwargs == {'key': 'value'} From e0377559167eb0824514cdb3baf2d37e738d85ba Mon Sep 17 00:00:00 2001 From: Michel Hidalgo Date: Fri, 4 Jun 2021 12:08:17 -0300 Subject: [PATCH 2/3] Please flake8 Signed-off-by: Michel Hidalgo --- rosidl_cli/rosidl_cli/extensions.py | 4 ++-- rosidl_cli/test/rosidl_cli/test_extensions.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rosidl_cli/rosidl_cli/extensions.py b/rosidl_cli/rosidl_cli/extensions.py index 935d12c94..b25d17e34 100644 --- a/rosidl_cli/rosidl_cli/extensions.py +++ b/rosidl_cli/rosidl_cli/extensions.py @@ -15,10 +15,10 @@ import logging import re -import yaml - from rosidl_cli.entry_points import load_entry_points +import yaml + logger = logging.getLogger(__name__) diff --git a/rosidl_cli/test/rosidl_cli/test_extensions.py b/rosidl_cli/test/rosidl_cli/test_extensions.py index 13ec432df..7e3dd2158 100644 --- a/rosidl_cli/test/rosidl_cli/test_extensions.py +++ b/rosidl_cli/test/rosidl_cli/test_extensions.py @@ -12,25 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -from rosidl_cli.extensions import parse_extension_specification - import pytest +from rosidl_cli.extensions import parse_extension_specification + def test_extension_specification_parsing(): with pytest.raises(ValueError): - parse_extension_specification("bad[") + parse_extension_specification('bad[') with pytest.raises(ValueError): - parse_extension_specification("bad[]") + parse_extension_specification('bad[]') with pytest.raises(ValueError): - parse_extension_specification("bad[:]") + parse_extension_specification('bad[:]') - name, kwargs = parse_extension_specification("no_args") + name, kwargs = parse_extension_specification('no_args') assert name == 'no_args' assert kwargs == {} - name, kwargs = parse_extension_specification("with_args[key: value]") + name, kwargs = parse_extension_specification('with_args[key: value]') assert name == 'with_args' assert kwargs == {'key': 'value'} From db920d1e98bcfd899ea0d5a96845f4d03ae275e4 Mon Sep 17 00:00:00 2001 From: Michel Hidalgo Date: Mon, 12 Jul 2021 10:45:19 -0300 Subject: [PATCH 3/3] Drop kwargs from Extension base class Signed-off-by: Michel Hidalgo --- rosidl_cli/rosidl_cli/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rosidl_cli/rosidl_cli/extensions.py b/rosidl_cli/rosidl_cli/extensions.py index b25d17e34..dac8400ab 100644 --- a/rosidl_cli/rosidl_cli/extensions.py +++ b/rosidl_cli/rosidl_cli/extensions.py @@ -26,7 +26,7 @@ class Extension: """A generic extension point.""" - def __init__(self, name, **kwargs): + def __init__(self, name): self.__name = name @property