From 1090efa8e6e917fdea760f5d90057dbf95caef92 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 15 Apr 2019 16:57:28 -0300 Subject: [PATCH 01/75] Added launch_fronted abstract entity Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/__init__.py | 24 ++++++++++++ launch_frontend/launch_frontend/entity.py | 43 +++++++++++++++++++++ launch_frontend/package.xml | 22 +++++++++++ launch_frontend/setup.py | 29 ++++++++++++++ launch_frontend/test/test_copyright.py | 23 +++++++++++ launch_frontend/test/test_flake8.py | 23 +++++++++++ launch_frontend/test/test_pep257.py | 23 +++++++++++ 7 files changed, 187 insertions(+) create mode 100644 launch_frontend/launch_frontend/__init__.py create mode 100644 launch_frontend/launch_frontend/entity.py create mode 100644 launch_frontend/package.xml create mode 100644 launch_frontend/setup.py create mode 100644 launch_frontend/test/test_copyright.py create mode 100644 launch_frontend/test/test_flake8.py create mode 100644 launch_frontend/test/test_pep257.py diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py new file mode 100644 index 000000000..8ea926305 --- /dev/null +++ b/launch_frontend/launch_frontend/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2019 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. + +"""Main entry point for the `launch_frontend` package.""" + +from .entity import Entity +from .parser import parse_executable + + +__all__ = [ + 'Entity', + 'parse_executable' +] diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py new file mode 100644 index 000000000..b7453fd9b --- /dev/null +++ b/launch_frontend/launch_frontend/entity.py @@ -0,0 +1,43 @@ +# Copyright 2019 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. + +"""Module for Entity class.""" + +from typing import Any +from typing import List +from typing import Optional +from typing import Text + + +class Entity: + """Single item in the intermediate front_end representation.""" + + @property + def type_name(self) -> Text: + """Get Entity type.""" + raise NotImplementedError() + + @property + def parent(self) -> Optional['Entity']: + """Get Entity parent.""" + raise NotImplementedError() + + @property + def children(self) -> Optional[List['Entity']]: + """Get Entity children.""" + raise NotImplementedError() + + def __getattr__(self, name: Text) -> Optional[Any]: + """Get attribute.""" + raise NotImplementedError() diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml new file mode 100644 index 000000000..ef81f7d4f --- /dev/null +++ b/launch_frontend/package.xml @@ -0,0 +1,22 @@ + + + + launch_frontend + 0.7.3 + The ROS launch frontend. + Ivan Paunovic + Apache License 2.0 + + launch + launch_ros + launch_testing + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/launch_frontend/setup.py b/launch_frontend/setup.py new file mode 100644 index 000000000..9916f3732 --- /dev/null +++ b/launch_frontend/setup.py @@ -0,0 +1,29 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='launch_frontend', + version='0.7.3', + packages=find_packages(exclude=['test']), + install_requires=['setuptools'], + zip_safe=True, + author='Ivan Paunovic', + author_email='ivanpauno@ekumenlabs.com', + maintainer='Ivan Paunovic', + maintainer_email='ivanpauno@ekumenlabs.com', + url='https://github.com/ros2/launch', + download_url='https://github.com/ros2/launch/releases', + keywords=['ROS'], + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Topic :: Software Development', + ], + description='Launch process from different front-ends.', + long_description=( + 'This package provides the to parse different front-ends ' + 'to a programmatic launch process.'), + license='Apache License, Version 2.0', + tests_require=['pytest'], +) diff --git a/launch_frontend/test/test_copyright.py b/launch_frontend/test/test_copyright.py new file mode 100644 index 000000000..cf0fae31f --- /dev/null +++ b/launch_frontend/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2017 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 ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/launch_frontend/test/test_flake8.py b/launch_frontend/test/test_flake8.py new file mode 100644 index 000000000..eff829969 --- /dev/null +++ b/launch_frontend/test/test_flake8.py @@ -0,0 +1,23 @@ +# Copyright 2017 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 ament_flake8.main import main +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc = main(argv=[]) + assert rc == 0, 'Found errors' diff --git a/launch_frontend/test/test_pep257.py b/launch_frontend/test/test_pep257.py new file mode 100644 index 000000000..3aeb4d348 --- /dev/null +++ b/launch_frontend/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 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 ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[]) + assert rc == 0, 'Found code style errors / warnings' From 5a894f25b8d6e3ac2990ca919bf78ddd183b3f36 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 15 Apr 2019 17:04:12 -0300 Subject: [PATCH 02/75] Add launch_xml entity implementation Signed-off-by: ivanpauno --- launch_xml/README.md | 67 +++++++++++++++++++++++++++++++ launch_xml/launch_xml/__init__.py | 19 +++++++++ launch_xml/launch_xml/entity.py | 59 +++++++++++++++++++++++++++ launch_xml/package.xml | 23 +++++++++++ launch_xml/setup.py | 29 +++++++++++++ launch_xml/test/test_copyright.py | 23 +++++++++++ launch_xml/test/test_flake8.py | 23 +++++++++++ launch_xml/test/test_pep257.py | 23 +++++++++++ 8 files changed, 266 insertions(+) create mode 100644 launch_xml/README.md create mode 100644 launch_xml/launch_xml/__init__.py create mode 100644 launch_xml/launch_xml/entity.py create mode 100644 launch_xml/package.xml create mode 100644 launch_xml/setup.py create mode 100644 launch_xml/test/test_copyright.py create mode 100644 launch_xml/test/test_flake8.py create mode 100644 launch_xml/test/test_pep257.py diff --git a/launch_xml/README.md b/launch_xml/README.md new file mode 100644 index 000000000..66231a0fc --- /dev/null +++ b/launch_xml/README.md @@ -0,0 +1,67 @@ +# launch_xml + +This package provides an abstraction of the XML tree. + +## XML front-end mapping rules + +### Accessing xml attributes + +When having an xml tag like: + +```xml + +``` + +If the entity `e` is wrapping it, the following two statements would be true: +```python +hasattr(e, 'attr') == True +e.attr == '2' +``` + +As a general rule, the value of the attribute is returned as an string. +Conversion to `float` or `int` should be explicitly done in the parser method. +For handling lists, see `Built-in Substitutions` section. + +### Accessing XML children as parameters: + +In this xml: + +```xml + + + + +``` + +The `env` children could be accessed like: + +```python +len(e.env) == 2 +e.env[0].name == 'a' +e.env[0].value == '100' +e.env[1].name == 'b' +e.env[1].value == 'stuff' +``` + +In these cases, `e.env` is a list of entities, which could be accessed in the same abstract way. + +### Accessing all the XML children: + +All the children can be directly accessed: + +```python +e.children +``` + +It returns a list of launch_xml.Entity wrapping each of the xml children. + +### Built-in substitutions + +See [this](https://github.com/ros2/design/blob/d3a35d7ea201721892993e85e28a5a223cdaa001/articles/151_roslaunch_xml.md) document. + +Additional substitution, for handling lists: + +`$(list [sep=,] a,b,c,d)` +: Substituted by a python list, splited by the separator that follows `sep=`. + Default separator is `,`. + This substitution is evaluated instantaneously, and not in a lazy way. diff --git a/launch_xml/launch_xml/__init__.py b/launch_xml/launch_xml/__init__.py new file mode 100644 index 000000000..b079be908 --- /dev/null +++ b/launch_xml/launch_xml/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2019 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. + +"""Main entry point for the `launch_xml` package.""" + +from .entity import Entity + +__all__ = ['Entity'] diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py new file mode 100644 index 000000000..b65d98dcc --- /dev/null +++ b/launch_xml/launch_xml/entity.py @@ -0,0 +1,59 @@ +# Copyright 2019 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. + +"""Module for Entity class.""" + +from typing import Optional +from typing import Text +from xml.etree.ElementTree import Element + +import launch_frontend + + +class Entity(launch_frontend.Entity): + """Single item in the intermediate XML front_end representation.""" + + def __init__(self, xml_element: Element, + *, parent: 'Entity' = None) -> Text: + """Construnctor.""" + self.__xml_element = xml_element + self.__parent = parent + + @property + def type_name(self) -> Text: + """Get Entity type.""" + return self.__xml_element.tag + + @property + def parent(self) -> Optional['Entity']: + """Get Entity parent.""" + return self.__parent + + @property + def children(self): + """Get Entity children.""" + return [Entity(child) for child in self.__xml_element] + + def __getattr__(self, name): + """Abstraction of how to access the xml tree.""" + if name in self.__xml_element.attrib: + return self.__xml_element.attrib[name] + return_list = filter(lambda x: x.tag == name, + self.__xml_element) + return_list = [Entity(item) for item in return_list] + if not return_list: + raise AttributeError( + 'Can not find attribute {} in Entity {}'.format( + name, self.type_name)) + return return_list diff --git a/launch_xml/package.xml b/launch_xml/package.xml new file mode 100644 index 000000000..bae4a58ea --- /dev/null +++ b/launch_xml/package.xml @@ -0,0 +1,23 @@ + + + + launch_xml + 0.7.3 + The ROS launch XML frontend. + Ivan Paunovic + Apache License 2.0 + + launch + launch_frontend + launch_ros + launch_testing + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/launch_xml/setup.py b/launch_xml/setup.py new file mode 100644 index 000000000..2b8c0359d --- /dev/null +++ b/launch_xml/setup.py @@ -0,0 +1,29 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='launch_xml', + version='0.7.3', + packages=find_packages(exclude=['test']), + install_requires=['setuptools'], + zip_safe=True, + author='Ivan Paunovic', + author_email='ivanpauno@ekumenlabs.com', + maintainer='Ivan Paunovic', + maintainer_email='ivanpauno@ekumenlabs.com', + url='https://github.com/ros2/launch', + download_url='https://github.com/ros2/launch/releases', + keywords=['ROS'], + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Topic :: Software Development', + ], + description='Launch process from different front-ends.', + long_description=( + 'This package provides the to parse different front-ends ' + 'to a programmatic launch process.'), + license='Apache License, Version 2.0', + tests_require=['pytest'], +) diff --git a/launch_xml/test/test_copyright.py b/launch_xml/test/test_copyright.py new file mode 100644 index 000000000..cf0fae31f --- /dev/null +++ b/launch_xml/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2017 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 ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/launch_xml/test/test_flake8.py b/launch_xml/test/test_flake8.py new file mode 100644 index 000000000..eff829969 --- /dev/null +++ b/launch_xml/test/test_flake8.py @@ -0,0 +1,23 @@ +# Copyright 2017 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 ament_flake8.main import main +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc = main(argv=[]) + assert rc == 0, 'Found errors' diff --git a/launch_xml/test/test_pep257.py b/launch_xml/test/test_pep257.py new file mode 100644 index 000000000..3aeb4d348 --- /dev/null +++ b/launch_xml/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 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 ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[]) + assert rc == 0, 'Found code style errors / warnings' From 39584303cdb36128d87e94dd7a300e31e8a104c0 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 15 Apr 2019 17:04:51 -0300 Subject: [PATCH 03/75] Add parse_executable function and test Signed-off-by: ivanpauno --- launch/launch/actions/execute_process.py | 5 ++ launch_frontend/launch_frontend/parser.py | 64 +++++++++++++++++++ launch_xml/test/launch_xml/executable.xml | 3 + launch_xml/test/launch_xml/test_executable.py | 55 ++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 launch_frontend/launch_frontend/parser.py create mode 100644 launch_xml/test/launch_xml/executable.xml create mode 100644 launch_xml/test/launch_xml/test_executable.py diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 8c0d620b6..52f0400c1 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -637,3 +637,8 @@ def additional_env(self): def shell(self): """Getter for shell.""" return self.__shell + + @property + def prefix(self): + """Getter for shell.""" + return self.__prefix diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py new file mode 100644 index 000000000..1376b5f7f --- /dev/null +++ b/launch_frontend/launch_frontend/parser.py @@ -0,0 +1,64 @@ +# Copyright 2019 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. + +"""Module for Parser methods.""" + +import launch + +from .entity import Entity + + +def str_to_bool(string): + """Convert xs::boolean to python bool.""" + if not string or string.lower() in ('0', 'false'): + return False + if string.lower() in ('1', 'true'): + return True + raise RuntimeError('Expected "true" or "false", got {}'.format(string)) + + +def get_dictionary_from_key_value_pairs(pairs): + """Get dictionary from key-value pairs.""" + if not pairs: + return None + return {pair.name: pair.value for pair in pairs} + + +def parse_executable(entity: Entity): + """Parse executable tag.""" + cmd = entity.cmd + cwd = getattr(entity, 'cwd', None) + name = getattr(entity, 'name', None) + shell = str_to_bool(getattr(entity, 'shell', None)) + prefix = getattr(entity, 'launch-prefix', None) + output = getattr(entity, 'output', None) + args = getattr(entity, 'args', None) + args = args.split(' ') if args else [] + if not type(args) == list: + args = [args] + # TODO(ivanpauno): How will predicates be handle in env? + # Substitutions aren't allowing conditions now. + env = get_dictionary_from_key_value_pairs(getattr(entity, 'env', None)) + + cmd_list = [cmd] + cmd_list.extend(args) + # TODO(ivanpauno): Handle predicate conditions + return launch.actions.ExecuteProcess( + cmd=cmd_list, + cwd=cwd, + env=env, + name=name, + shell=shell, + prefix=prefix, + output=output) diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml new file mode 100644 index 000000000..66854ce37 --- /dev/null +++ b/launch_xml/test/launch_xml/executable.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py new file mode 100644 index 000000000..9848cdd3e --- /dev/null +++ b/launch_xml/test/launch_xml/test_executable.py @@ -0,0 +1,55 @@ +# Copyright 2019 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. + +"""Example of how to parse an xml.""" + +from pathlib import Path +import xml.etree.ElementTree as ET + +from launch import LaunchDescription +from launch import LaunchService + +from launch_frontend import parse_executable + +from launch_xml import Entity + + +def test_executable(): + """Parse node xml example.""" + tree = ET.parse(str(Path(__file__).parent / 'executable.xml')) + root = tree.getroot() + root_entity = Entity(root) + executable = parse_executable(root_entity) + cmd = [i[0].perform(None) for i in executable.cmd] + assert(cmd == + ['ls', '-l', '-a', '-s']) + assert(executable.cwd[0].perform(None) == '/') + assert(executable.name[0].perform(None) == 'my_ls') + assert(executable.shell is True) + assert(executable.output == 'log') + key, value = executable.env[0] + key = key[0].perform(None) + value = value[0].perform(None) + assert(key == 'var') + assert(value == '1') + assert(executable.prefix[0].perform(None) == 'time') + ld = LaunchDescription() + ld.add_action(executable) + ls = LaunchService() + ls.include_launch_description(ld) + assert(0 == ls.run()) + + +if __name__ == '__main__': + test_executable() From f23772dca225ddd38e0c6f195371c300aac00ffb Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 22 Apr 2019 10:08:46 -0300 Subject: [PATCH 04/75] Corrected with PR comments Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/parser.py | 19 +++++++------------ launch_frontend/package.xml | 2 -- launch_frontend/setup.py | 6 +++--- launch_xml/README.md | 16 ++++++++++++---- launch_xml/package.xml | 2 -- launch_xml/setup.py | 6 +++--- launch_xml/test/launch_xml/executable.xml | 2 +- launch_xml/test/launch_xml/test_executable.py | 2 +- 8 files changed, 27 insertions(+), 28 deletions(-) diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 1376b5f7f..a2785162d 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -28,28 +28,23 @@ def str_to_bool(string): raise RuntimeError('Expected "true" or "false", got {}'.format(string)) -def get_dictionary_from_key_value_pairs(pairs): - """Get dictionary from key-value pairs.""" - if not pairs: - return None - return {pair.name: pair.value for pair in pairs} - - def parse_executable(entity: Entity): """Parse executable tag.""" cmd = entity.cmd cwd = getattr(entity, 'cwd', None) name = getattr(entity, 'name', None) - shell = str_to_bool(getattr(entity, 'shell', None)) + shell = str_to_bool(getattr(entity, 'shell', 'false')) prefix = getattr(entity, 'launch-prefix', None) - output = getattr(entity, 'output', None) + output = getattr(entity, 'output', 'log') args = getattr(entity, 'args', None) args = args.split(' ') if args else [] - if not type(args) == list: + if not isinstance(args, list): args = [args] # TODO(ivanpauno): How will predicates be handle in env? # Substitutions aren't allowing conditions now. - env = get_dictionary_from_key_value_pairs(getattr(entity, 'env', None)) + env = getattr(entity, 'env', None) + if env is not None: + env = {e.name: e.value for e in env} cmd_list = [cmd] cmd_list.extend(args) @@ -57,7 +52,7 @@ def parse_executable(entity: Entity): return launch.actions.ExecuteProcess( cmd=cmd_list, cwd=cwd, - env=env, + additional_env=env, name=name, shell=shell, prefix=prefix, diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml index ef81f7d4f..75bd88cea 100644 --- a/launch_frontend/package.xml +++ b/launch_frontend/package.xml @@ -8,8 +8,6 @@ Apache License 2.0 launch - launch_ros - launch_testing ament_copyright ament_flake8 diff --git a/launch_frontend/setup.py b/launch_frontend/setup.py index 9916f3732..181d7cd2b 100644 --- a/launch_frontend/setup.py +++ b/launch_frontend/setup.py @@ -20,10 +20,10 @@ 'Programming Language :: Python', 'Topic :: Software Development', ], - description='Launch process from different front-ends.', + description='Front-end extensions to `launch`.', long_description=( - 'This package provides the to parse different front-ends ' - 'to a programmatic launch process.'), + 'This package provides front-end extensions to the `launch` package.' + ), license='Apache License, Version 2.0', tests_require=['pytest'], ) diff --git a/launch_xml/README.md b/launch_xml/README.md index 66231a0fc..91757bd55 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -12,17 +12,17 @@ When having an xml tag like: ``` -If the entity `e` is wrapping it, the following two statements would be true: +If the entity `e` is wrapping it, the following two statements will be true: ```python hasattr(e, 'attr') == True e.attr == '2' ``` -As a general rule, the value of the attribute is returned as an string. +As a general rule, the value of the attribute is returned as a string. Conversion to `float` or `int` should be explicitly done in the parser method. For handling lists, see `Built-in Substitutions` section. -### Accessing XML children as parameters: +### Accessing XML children as attributes: In this xml: @@ -55,7 +55,15 @@ e.children It returns a list of launch_xml.Entity wrapping each of the xml children. -### Built-in substitutions +### Attribute lookup order + +The attributes are check in the following order: + +- Is tried to be accessed like a XML attribute. +- Is tried to be accessed like XML children. +- `AttributeError` is raised. + +## Built-in substitutions See [this](https://github.com/ros2/design/blob/d3a35d7ea201721892993e85e28a5a223cdaa001/articles/151_roslaunch_xml.md) document. diff --git a/launch_xml/package.xml b/launch_xml/package.xml index bae4a58ea..7e8d6b695 100644 --- a/launch_xml/package.xml +++ b/launch_xml/package.xml @@ -9,8 +9,6 @@ launch launch_frontend - launch_ros - launch_testing ament_copyright ament_flake8 diff --git a/launch_xml/setup.py b/launch_xml/setup.py index 2b8c0359d..4f2cac54c 100644 --- a/launch_xml/setup.py +++ b/launch_xml/setup.py @@ -20,10 +20,10 @@ 'Programming Language :: Python', 'Topic :: Software Development', ], - description='Launch process from different front-ends.', + description='XML `launch` front-end extension.', long_description=( - 'This package provides the to parse different front-ends ' - 'to a programmatic launch process.'), + 'This package provides XML parsing ability to `launch-frontend` package.' + ), license='Apache License, Version 2.0', tests_require=['pytest'], ) diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml index 66854ce37..d5707c262 100644 --- a/launch_xml/test/launch_xml/executable.xml +++ b/launch_xml/test/launch_xml/executable.xml @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 9848cdd3e..589985a9a 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -38,7 +38,7 @@ def test_executable(): assert(executable.name[0].perform(None) == 'my_ls') assert(executable.shell is True) assert(executable.output == 'log') - key, value = executable.env[0] + key, value = executable.additional_env[0] key = key[0].perform(None) value = value[0].perform(None) assert(key == 'var') From df8c439dae8077aacc9a4d687f5519680fea9149 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 25 Apr 2019 09:23:23 -0300 Subject: [PATCH 05/75] Corrected with PR comments Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/parser.py | 48 ++++++++++++------- launch_xml/README.md | 23 +++++---- launch_xml/launch_xml/entity.py | 7 ++- launch_xml/test/launch_xml/list.xml | 5 ++ launch_xml/test/launch_xml/test_executable.py | 2 +- launch_xml/test/launch_xml/test_list.py | 34 +++++++++++++ 6 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 launch_xml/test/launch_xml/list.xml create mode 100644 launch_xml/test/launch_xml/test_list.py diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index a2785162d..3e1464288 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -14,6 +14,9 @@ """Module for Parser methods.""" +from typing import Optional +from typing import Text + import launch from .entity import Entity @@ -28,32 +31,45 @@ def str_to_bool(string): raise RuntimeError('Expected "true" or "false", got {}'.format(string)) +def load_optional_attribute( + kwargs: dict, + entity: Entity, + name: Text, + constructor_name: Optional[Text] = None +): + """Load an optional attribute of `entity` named `name` in `kwargs`.""" + attr = getattr(entity, name, None) + key = name + if constructor_name is not None: + key = constructor_name + if attr is not None: + kwargs[key] = attr + + def parse_executable(entity: Entity): """Parse executable tag.""" cmd = entity.cmd - cwd = getattr(entity, 'cwd', None) - name = getattr(entity, 'name', None) - shell = str_to_bool(getattr(entity, 'shell', 'false')) - prefix = getattr(entity, 'launch-prefix', None) - output = getattr(entity, 'output', 'log') - args = getattr(entity, 'args', None) - args = args.split(' ') if args else [] - if not isinstance(args, list): - args = [args] + kwargs = {} + load_optional_attribute(kwargs, entity, 'cwd') + load_optional_attribute(kwargs, entity, 'name') + load_optional_attribute(kwargs, entity, 'launch-prefix', 'prefix') + load_optional_attribute(kwargs, entity, 'output') + shell = getattr(entity, 'shell', None) + if shell is not None: + kwargs['shell'] = str_to_bool(shell) # TODO(ivanpauno): How will predicates be handle in env? # Substitutions aren't allowing conditions now. env = getattr(entity, 'env', None) if env is not None: env = {e.name: e.value for e in env} - + kwargs['additional_env'] = env + args = getattr(entity, 'args', None) + args = args.split(' ') if args else [] + if not isinstance(args, list): + args = [args] cmd_list = [cmd] cmd_list.extend(args) # TODO(ivanpauno): Handle predicate conditions return launch.actions.ExecuteProcess( cmd=cmd_list, - cwd=cwd, - additional_env=env, - name=name, - shell=shell, - prefix=prefix, - output=output) + **kwargs) diff --git a/launch_xml/README.md b/launch_xml/README.md index 91757bd55..fadb41389 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -9,7 +9,7 @@ This package provides an abstraction of the XML tree. When having an xml tag like: ```xml - + ``` If the entity `e` is wrapping it, the following two statements will be true: @@ -20,7 +20,19 @@ e.attr == '2' As a general rule, the value of the attribute is returned as a string. Conversion to `float` or `int` should be explicitly done in the parser method. -For handling lists, see `Built-in Substitutions` section. +For handling lists, the `*-sep` attribute is used. e.g.: + +```xml + + + +``` + +```python +e.tag.attr == [2, 3, 4] +e.tag2.attr == [2, 3, 4] +e.tag3.attr == [2, 3, 4] +``` ### Accessing XML children as attributes: @@ -66,10 +78,3 @@ The attributes are check in the following order: ## Built-in substitutions See [this](https://github.com/ros2/design/blob/d3a35d7ea201721892993e85e28a5a223cdaa001/articles/151_roslaunch_xml.md) document. - -Additional substitution, for handling lists: - -`$(list [sep=,] a,b,c,d)` -: Substituted by a python list, splited by the separator that follows `sep=`. - Default separator is `,`. - This substitution is evaluated instantaneously, and not in a lazy way. diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index b65d98dcc..9de27e60e 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -48,7 +48,12 @@ def children(self): def __getattr__(self, name): """Abstraction of how to access the xml tree.""" if name in self.__xml_element.attrib: - return self.__xml_element.attrib[name] + name_sep = name + '-sep' + if name_sep not in self.__xml_element.attrib: + return self.__xml_element.attrib[name] + else: + sep = self.__xml_element.attrib[name_sep] + return self.__xml_element.attrib[name].split(sep) return_list = filter(lambda x: x.tag == name, self.__xml_element) return_list = [Entity(item) for item in return_list] diff --git a/launch_xml/test/launch_xml/list.xml b/launch_xml/test/launch_xml/list.xml new file mode 100644 index 000000000..6a950a0a6 --- /dev/null +++ b/launch_xml/test/launch_xml/list.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 589985a9a..6e4530291 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Example of how to parse an xml.""" +"""Test parsing an executable action.""" from pathlib import Path import xml.etree.ElementTree as ET diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py new file mode 100644 index 000000000..88dc27a31 --- /dev/null +++ b/launch_xml/test/launch_xml/test_list.py @@ -0,0 +1,34 @@ +# Copyright 2019 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. + +"""Test parsing list attributes.""" + +from pathlib import Path +import xml.etree.ElementTree as ET + +from launch_xml import Entity + + +def test_list(): + """Parse tags with list attributes.""" + tree = ET.parse(str(Path(__file__).parent / 'list.xml')) + root = tree.getroot() + root_entity = Entity(root) + assert root_entity.tag1[0].attr == ['1', '2', '3'] + assert root_entity.tag2[0].attr == ['1', '2', '3'] + assert root_entity.tag3[0].attr == ['1', '2', '3'] + + +if __name__ == '__main__': + test_list() From 471e406dd14a16e3cc5a41a2145b3dd46aea7507 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 16 Apr 2019 18:31:31 -0300 Subject: [PATCH 06/75] Add expose_action and expose_substitution functions Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/__init__.py | 5 ++ launch_frontend/launch_frontend/expose.py | 56 +++++++++++++++++++ .../launch_frontend/test_expose_decorators.py | 53 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 launch_frontend/launch_frontend/expose.py create mode 100644 launch_frontend/test/launch_frontend/test_expose_decorators.py diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index 8ea926305..21f9b0362 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -15,10 +15,15 @@ """Main entry point for the `launch_frontend` package.""" from .entity import Entity +from .expose import __expose_impl, expose_action, expose_substitution from .parser import parse_executable __all__ = [ 'Entity', + # Implementation function, should only be imported in test_expose_decorators. + '__expose_impl', + 'expose_action', + 'expose_substitution', 'parse_executable' ] diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py new file mode 100644 index 000000000..21579c629 --- /dev/null +++ b/launch_frontend/launch_frontend/expose.py @@ -0,0 +1,56 @@ +# Copyright 2019 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. + +"""Module which adds methods for exposing parsing methods.""" + +import inspect +from typing import Text + +substitution_parse_methods = dict({}) +action_parse_methods = dict({}) + + +def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): + """Implementats expose decorators.""" + def expose_substitution_decorator(exposed): + if name in parse_methods_map: + raise RuntimeError( + 'Two parsing methods exposed with same name.' + ) + found_parse_method = None + if inspect.isclass(exposed): + if 'parse' in dir(exposed): + print('class with parse method') + found_parse_method = exposed.parse + elif callable(exposed): + print('callable') + found_parse_method = exposed + if not found_parse_method: + raise RuntimeError( + 'Exposed {} parser for {} is not a callable or a class' + ' containg a parse method'.format(exposed_type, name) + ) + parse_methods_map[name] = found_parse_method + return exposed + return expose_substitution_decorator + + +def expose_substitution(name: Text): + """Exposes a substitution.""" + return __expose_impl(name, substitution_parse_methods, 'substitution') + + +def expose_action(name: Text): + """Exposes an action.""" + return __expose_impl(name, action_parse_methods, 'action') diff --git a/launch_frontend/test/launch_frontend/test_expose_decorators.py b/launch_frontend/test/launch_frontend/test_expose_decorators.py new file mode 100644 index 000000000..40364d59c --- /dev/null +++ b/launch_frontend/test/launch_frontend/test_expose_decorators.py @@ -0,0 +1,53 @@ +# Copyright 2019 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 launch_frontend import __expose_impl + +import pytest + + +class ToBeExposed: + + @staticmethod + def parse(entity): + return ToBeExposed() + + +def to_be_exposed(entity): + return ToBeExposed() + + +register = dict({}) + + +def expose_test(name): + return __expose_impl(name, register, 'test') + + +def test_expose_decorators(): + expose_test('ToBeExposed')(ToBeExposed) + assert 'ToBeExposed' in register + if 'ToBeExposed' in register: + assert register['ToBeExposed'] is ToBeExposed.parse + expose_test('to_be_exposed')(to_be_exposed) + assert 'to_be_exposed' in register + if 'to_be_exposed' in register: + assert register['to_be_exposed'] is to_be_exposed + NotACallable = 5 + with pytest.raises( + RuntimeError, + match='Exposed test parser for NotACallable is not a callable or a class' + ' containg a parse method' + ): + expose_test('NotACallable')(NotACallable) From 9807d4d1276b336d7f78c5eadec9c4c7ac48f0c3 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 22 Apr 2019 09:38:29 -0300 Subject: [PATCH 07/75] Add parser Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 40 +++++++++++++++++++++ launch_frontend/launch_frontend/expose.py | 34 +++++++++++++++--- launch_frontend/launch_frontend/parser.py | 42 +++++++++++++++++++++++ launch_xml/launch_xml/entity.py | 23 +++++++++++-- launch_xml/setup.py | 5 +++ 5 files changed, 137 insertions(+), 7 deletions(-) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index b7453fd9b..f8cff18bc 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -14,15 +14,55 @@ """Module for Entity class.""" +import io from typing import Any from typing import List from typing import Optional from typing import Text +from typing import Union + +from launch.utilities import is_a + +from pkg_resources import iter_entry_points + +frontend_entities = { + entry_point.name: entry_point.load() + for entry_point in iter_entry_points('launch_frontend.entity') +} class Entity: """Single item in the intermediate front_end representation.""" + @staticmethod + def load( + file: Union[str, io.TextIOBase], + parent: 'Entity' + ) -> 'Entity': + """Return an entity loaded with a markup file.""" + if is_a(file, str): + # This automatically recognizes 'file.xml' or 'file.launch.xml' + # as a launch file using the xml frontend. + frontend_name = file.rsplit('.', 1)[1] + if frontend_name in frontend_entities: + return frontend_entities.load(file) + # If not, apply brute force. + # TODO(ivanpauno): Maybe, we want to force correct file naming. + # In that case, we should raise an error here. + # Note(ivanpauno): It's impossible to recognize a wrong formatted + # file from a non-recognized front-end implementation. + for implementation in frontend_entities: + try: + return implementation.load(file) + except Exception: + pass + raise RuntimeError('Not recognized front-end implementation.') + + @staticmethod + def frontend() -> Text: + """Get which frontend is wrapping.""" + raise NotImplementedError() + @property def type_name(self) -> Text: """Get Entity type.""" diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index 21579c629..9ec901f66 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -17,13 +17,17 @@ import inspect from typing import Text -substitution_parse_methods = dict({}) -action_parse_methods = dict({}) +# from .entity import Entity + +substitution_parse_methods = {} +action_parse_methods = {} +# frontend_entities = {} +# frontend_interpolate_function = {} def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): """Implementats expose decorators.""" - def expose_substitution_decorator(exposed): + def expose_impl_decorator(exposed): if name in parse_methods_map: raise RuntimeError( 'Two parsing methods exposed with same name.' @@ -43,7 +47,7 @@ def expose_substitution_decorator(exposed): ) parse_methods_map[name] = found_parse_method return exposed - return expose_substitution_decorator + return expose_impl_decorator def expose_substitution(name: Text): @@ -54,3 +58,25 @@ def expose_substitution(name: Text): def expose_action(name: Text): """Exposes an action.""" return __expose_impl(name, action_parse_methods, 'action') + + +# def expose_entity(name: Text): +# """Exposes a frontend.""" +# def expose_entity_decorator(exposed): +# if name in frontend_entities and exposed is not frontend_entities[name]: +# raise RuntimeError('Two frontends exposed with the same name.') +# if not issubclass(exposed, Entity): +# raise RuntimeError('expose_frontend expects a launch_frontend.Entity subclass.') +# frontend_entities[name] = exposed +# return exposed +# return expose_entity_decorator + + +# def expose_substitution_interpolation(name: Text): +# """Exposes a substitution interpolation function.""" +# def expose_substitution_interpolation_decorator(exposed): +# if not callable(exposed): +# raise RuntimeError('Expected a callable.') +# frontend_interpolate_function[name] = exposed +# return exposed +# return expose_substitution_interpolation_decorator diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 3e1464288..129e4f398 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -19,7 +19,41 @@ import launch +from pkg_resources import iter_entry_points + from .entity import Entity +from .expose import action_parse_methods + +interpolation_fuctions = { + entry_point.name: entry_point.load() + for entry_point in iter_entry_points('launch_frontend.interpolate_substitution') +} + + +class Parser: + """Parse an Entity, and creates a launch description from it.""" + + # TODO(ivanpauno): Why don't use free methods? Instead of a class. + + def parse_action(self, entity: Entity) -> launch.Action: + """Parse an action, using its registered parsing method.""" + if entity.type_name not in action_parse_methods: + raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) + return action_parse_methods[entity.type_name](entity) + + def parse_substitution(self, value: Text, frontend: Text) -> launch.SomeSubstitutionsType: + """Parse a substitution, using its registered parsing method.""" + if frontend in interpolation_fuctions: + return interpolation_fuctions[frontend](value) + # else: + # return default_substitution_interpolation(value) + + def parse_description(self, entity: Entity) -> launch.LaunchDescription: + """Parse a launch description.""" + if entity.type_name != 'launch': + raise RuntimeError('Expected \'launch\' as root tag') + actions = [self.parse_action(child) for child in self.__root_entity.children] + return launch.LaunchDescription(actions) def str_to_bool(string): @@ -73,3 +107,11 @@ def parse_executable(entity: Entity): return launch.actions.ExecuteProcess( cmd=cmd_list, **kwargs) + +def parse_include(entity: Entity): + """Parse a launch file to be included.""" + # TODO(ivanpauno): Should be allow to include a programmatic launch file? How? + # TODO(ivanpauno): Create launch_ros.actions.IncludeAction, supporting namespacing. + # TODO(ivanpauno): Handle if and unless conditions. + loaded_entity = Entity.load(entity.file, entity.parent) + Parser.parse_description(loaded_entity) diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index 9de27e60e..ffdf0bd00 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -14,9 +14,11 @@ """Module for Entity class.""" +import io +import xml.etree.ElementTree as ET from typing import Optional from typing import Text -from xml.etree.ElementTree import Element +from typing import Union import launch_frontend @@ -24,12 +26,27 @@ class Entity(launch_frontend.Entity): """Single item in the intermediate XML front_end representation.""" - def __init__(self, xml_element: Element, - *, parent: 'Entity' = None) -> Text: + @staticmethod + def load( + stream: Union[str, io.TextIOBase], + parent: 'Entity' = None + ) -> 'Entity': + """Return entity loaded with markup file.""" + return Entity(ET.parse(stream).getroot()) + + def __init__(self, + xml_element: ET.Element = None, + *, + parent: 'Entity' = None) -> Text: """Construnctor.""" self.__xml_element = xml_element self.__parent = parent + @staticmethod + def frontend() -> Text: + """Get which frontend is wrapping.""" + return 'xml' + @property def type_name(self) -> Text: """Get Entity type.""" diff --git a/launch_xml/setup.py b/launch_xml/setup.py index 4f2cac54c..5625bce7a 100644 --- a/launch_xml/setup.py +++ b/launch_xml/setup.py @@ -26,4 +26,9 @@ ), license='Apache License, Version 2.0', tests_require=['pytest'], + entry_points={ + 'launch_frontend.entity': [ + 'xml = launch_xml:Entity', + ], + } ) From 60e28d07e6db012d90c34019fb5f25e4831273c3 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 22 Apr 2019 11:56:45 -0300 Subject: [PATCH 08/75] Add interpolate substitution function Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 13 +- launch_frontend/launch_frontend/parser.py | 5 +- .../launch_frontend/substitutions.py | 190 ++++++++++++++++++ launch_xml/launch_xml/entity.py | 2 +- launch_xml/test/launch_xml/test_executable.py | 8 +- 5 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 launch_frontend/launch_frontend/substitutions.py diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index f8cff18bc..a9a784dfe 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -25,11 +25,6 @@ from pkg_resources import iter_entry_points -frontend_entities = { - entry_point.name: entry_point.load() - for entry_point in iter_entry_points('launch_frontend.entity') -} - class Entity: """Single item in the intermediate front_end representation.""" @@ -37,15 +32,19 @@ class Entity: @staticmethod def load( file: Union[str, io.TextIOBase], - parent: 'Entity' + parent: 'Entity' = None ) -> 'Entity': """Return an entity loaded with a markup file.""" + frontend_entities = { + entry_point.name: entry_point.load() + for entry_point in iter_entry_points('launch_frontend.entity') + } if is_a(file, str): # This automatically recognizes 'file.xml' or 'file.launch.xml' # as a launch file using the xml frontend. frontend_name = file.rsplit('.', 1)[1] if frontend_name in frontend_entities: - return frontend_entities.load(file) + return frontend_entities[frontend_name].load(file) # If not, apply brute force. # TODO(ivanpauno): Maybe, we want to force correct file naming. # In that case, we should raise an error here. diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 129e4f398..f94ecb4d6 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -23,6 +23,7 @@ from .entity import Entity from .expose import action_parse_methods +from .substitutions import default_substitution_interpolation interpolation_fuctions = { entry_point.name: entry_point.load() @@ -45,8 +46,8 @@ def parse_substitution(self, value: Text, frontend: Text) -> launch.SomeSubstitu """Parse a substitution, using its registered parsing method.""" if frontend in interpolation_fuctions: return interpolation_fuctions[frontend](value) - # else: - # return default_substitution_interpolation(value) + else: + return default_substitution_interpolation(value) def parse_description(self, entity: Entity) -> launch.LaunchDescription: """Parse a launch description.""" diff --git a/launch_frontend/launch_frontend/substitutions.py b/launch_frontend/launch_frontend/substitutions.py new file mode 100644 index 000000000..12a39c34a --- /dev/null +++ b/launch_frontend/launch_frontend/substitutions.py @@ -0,0 +1,190 @@ +# Copyright 2019 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. + +"""Module for built-in front-end substitutions.""" + +from typing import Text + +from launch import SomeSubstitutionsType +from launch.substitutions import EnvironmentVariable +from launch.substitutions import FindExecutable + +from .expose import expose_substitution +from .expose import substitution_parse_methods + + +@expose_substitution('test') +def test(data): + """Delete me please.""" + if len(data) > 1: + raise AttributeError('Expected a len 1 list') + return data[0] + + +@expose_substitution('list') +def parse_list(string: Text): + """Parse a list substitution.""" + if len(string) > 1: + raise AttributeError('Expected a len 1 list.') + string = string[0] + pos = string.find('sep=') + sep = ',' + if pos == 0: + sep, string = string[4:].split(' ', 1) + return string.split(sep) + + +def parse_find_executable(executable_name: SomeSubstitutionsType): + """Return FindExecutable substitution.""" + return FindExecutable(executable_name) + + +@expose_substitution('env') +def parse_env(data): + """Return FindExecutable substitution.""" + if not data or len(data) > 2: + raise AttributeError('env substitution expected 1 or 2 arguments') + name = data[0] + default = data[1] if len(data) == 2 else '' + # print(name) + # print(default) + return EnvironmentVariable(name, default_value=default) + + +def find_no_scaped(string, substr, pos): + """Find the first non scaped 'substr' substring in 'string', starting from 'pos'.""" + scaped_substr = '\\' + substr + dscaped_substr = '\\' + scaped_substr + while True: + pos_found = string.find(substr, pos) + pos_found_scaped = string.find(scaped_substr, pos) + pos_found_dscaped = string.find(dscaped_substr, pos) + is_double_scaped = pos_found_dscaped >= 0 and pos_found == pos_found_dscaped + 2 + is_scaped = pos_found_scaped >= 0 and pos_found == pos_found_scaped + 1 + is_scaped = not is_double_scaped and is_scaped + if pos_found < 0 or not is_scaped: + return pos_found + pos = pos_found + 1 + + +def replace_scapes(string): + ret = '' + pos = 0 + while True: + last_pos = pos + pos = string.find('\\', pos) + if pos < 0: + return ret + string[last_pos:] + if string[pos+1] == '\\': + ret = ret + string[last_pos:pos] + '\\' + pos = pos + 2 + continue + if string[pos+1:pos+3] == '$(': + ret = ret + string[last_pos:pos] + '$(' + pos = pos + 3 + continue + if string[pos+1] == ')': + ret = ret + string[last_pos:pos] + ')' + pos = pos + 2 + continue + raise RuntimeError('Wrong backslash usage in string') + + +def default_substitution_interpolation(string): + """Interpolate substitutions in a string.""" + # TODO(ivanpauno): Use 're' package to do this in a cleaner way. + # This scans from left to right. It pushes the position + # of the opening brackets. When it finds a closing bracket + # it pops and substitute. + # The output is a list of substitutions and strings. + subst_list = [] # Substitutions list to be returned. + pos = 0 # Position of the string when we should continue parsing. + opening_brackets_pile = [] # Pile containing opening brackets. + # A dict, containing the nested substitutions that have been done. + # The key is the nesting level. + # Each item is a list, in order to handle substitutions that takes + # more than one argument, like $(env var default). + nested_substitutions = dict({}) + while 1: + ob = find_no_scaped(string, '$(', pos) + cb = find_no_scaped(string, ')', pos) + if ob >= 0 and ob < cb: + # New opening bracket found + if opening_brackets_pile: + middle_string = None + if pos > opening_brackets_pile[-1] + 2: + middle_string = string[pos:ob] + else: + # Skip the key + _, middle_string = string[ + opening_brackets_pile[-1]:ob].split(' ', 1) + if middle_string: + try: + nested_substitutions[len(opening_brackets_pile)-1].append( + middle_string) + except (TypeError, KeyError): + nested_substitutions[len(opening_brackets_pile)-1] = \ + [middle_string] + opening_brackets_pile.append(ob) + pos = ob + 2 + continue + if cb >= 0: + # New closing bracket found + ob_pop = opening_brackets_pile.pop() + subst_key, subst_value = string[ob_pop+2:cb].split(' ', 1) + if subst_key not in substitution_parse_methods: + # Unknown substitution + raise RuntimeError( + 'Invalid substitution type: {}'.format(subst_key)) + if len(opening_brackets_pile)+1 not in nested_substitutions: + # Doesn't have a nested substitution inside. + try: + nested_substitutions[len(opening_brackets_pile)].append( + substitution_parse_methods[subst_key]([replace_scapes(subst_value)])) + except (TypeError, KeyError): + nested_substitutions[len(opening_brackets_pile)] = \ + [substitution_parse_methods[subst_key]([replace_scapes(subst_value)])] + else: + # Have a nested substitution inside + try: + nested_substitutions[len(opening_brackets_pile)].append( + substitution_parse_methods[subst_key]( + nested_substitutions[len(opening_brackets_pile)+1]) + ) + except (TypeError, KeyError): + nested_substitutions[len(opening_brackets_pile)] = [ + substitution_parse_methods[subst_key]( + nested_substitutions[len(opening_brackets_pile)+1]) + ] + del nested_substitutions[len(opening_brackets_pile)+1] + if not opening_brackets_pile: + # Is not nested inside other substitution + subst_list.append(replace_scapes(string[:ob_pop])) + subst_list.extend(nested_substitutions[0]) + string = string[cb+1:] + pos = 0 + nested_substitutions = dict({}) + else: + # Is still nested in other substitution + pos = cb + 1 + continue + if opening_brackets_pile: + raise RuntimeError('Non matching substitution brackets.') + return subst_list + + +if __name__ == '__main__': + print(default_substitution_interpolation( + r'hola $(test \)como\$(\\) $(test $(list 1,2,3))' + ' $(env $(env jkl $(test msj)) bsd)')) diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index ffdf0bd00..c887d1ba7 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -15,10 +15,10 @@ """Module for Entity class.""" import io -import xml.etree.ElementTree as ET from typing import Optional from typing import Text from typing import Union +import xml.etree.ElementTree as ET import launch_frontend diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 6e4530291..f5ab0e8c2 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -15,21 +15,17 @@ """Test parsing an executable action.""" from pathlib import Path -import xml.etree.ElementTree as ET from launch import LaunchDescription from launch import LaunchService +from launch_frontend import Entity from launch_frontend import parse_executable -from launch_xml import Entity - def test_executable(): """Parse node xml example.""" - tree = ET.parse(str(Path(__file__).parent / 'executable.xml')) - root = tree.getroot() - root_entity = Entity(root) + root_entity = Entity.load(str(Path(__file__).parent / 'executable.xml')) executable = parse_executable(root_entity) cmd = [i[0].perform(None) for i in executable.cmd] assert(cmd == From 9e8fac7af7d43529180cdac4aba3a03565756405 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 22 Apr 2019 12:39:31 -0300 Subject: [PATCH 09/75] Updated xml 'executable' action test. Corrected parser module Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/__init__.py | 8 ++-- launch_frontend/launch_frontend/entity.py | 1 + launch_frontend/launch_frontend/expose.py | 28 -------------- launch_frontend/launch_frontend/parser.py | 42 ++++++++++----------- 4 files changed, 27 insertions(+), 52 deletions(-) diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index 21f9b0362..e3126e578 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -16,14 +16,16 @@ from .entity import Entity from .expose import __expose_impl, expose_action, expose_substitution -from .parser import parse_executable +from .parser import parse_action, parse_description, parse_substitution __all__ = [ 'Entity', - # Implementation function, should only be imported in test_expose_decorators. + # Implementation detail, should only be imported in test_expose_decorators. '__expose_impl', 'expose_action', 'expose_substitution', - 'parse_executable' + 'parse_action', + 'parse_description', + 'parse_substitution', ] diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index a9a784dfe..6a15c8b48 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -35,6 +35,7 @@ def load( parent: 'Entity' = None ) -> 'Entity': """Return an entity loaded with a markup file.""" + # frontend_entities is not global to avoid a recursive import frontend_entities = { entry_point.name: entry_point.load() for entry_point in iter_entry_points('launch_frontend.entity') diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index 9ec901f66..b3d6b5e06 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -17,12 +17,8 @@ import inspect from typing import Text -# from .entity import Entity - substitution_parse_methods = {} action_parse_methods = {} -# frontend_entities = {} -# frontend_interpolate_function = {} def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): @@ -35,10 +31,8 @@ def expose_impl_decorator(exposed): found_parse_method = None if inspect.isclass(exposed): if 'parse' in dir(exposed): - print('class with parse method') found_parse_method = exposed.parse elif callable(exposed): - print('callable') found_parse_method = exposed if not found_parse_method: raise RuntimeError( @@ -58,25 +52,3 @@ def expose_substitution(name: Text): def expose_action(name: Text): """Exposes an action.""" return __expose_impl(name, action_parse_methods, 'action') - - -# def expose_entity(name: Text): -# """Exposes a frontend.""" -# def expose_entity_decorator(exposed): -# if name in frontend_entities and exposed is not frontend_entities[name]: -# raise RuntimeError('Two frontends exposed with the same name.') -# if not issubclass(exposed, Entity): -# raise RuntimeError('expose_frontend expects a launch_frontend.Entity subclass.') -# frontend_entities[name] = exposed -# return exposed -# return expose_entity_decorator - - -# def expose_substitution_interpolation(name: Text): -# """Exposes a substitution interpolation function.""" -# def expose_substitution_interpolation_decorator(exposed): -# if not callable(exposed): -# raise RuntimeError('Expected a callable.') -# frontend_interpolate_function[name] = exposed -# return exposed -# return expose_substitution_interpolation_decorator diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index f94ecb4d6..406ed4b51 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -23,6 +23,7 @@ from .entity import Entity from .expose import action_parse_methods +from .expose import expose_action from .substitutions import default_substitution_interpolation interpolation_fuctions = { @@ -31,30 +32,27 @@ } -class Parser: - """Parse an Entity, and creates a launch description from it.""" +def parse_action(entity: Entity) -> launch.Action: + """Parse an action, using its registered parsing method.""" + if entity.type_name not in action_parse_methods: + raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) + return action_parse_methods[entity.type_name](entity) - # TODO(ivanpauno): Why don't use free methods? Instead of a class. - def parse_action(self, entity: Entity) -> launch.Action: - """Parse an action, using its registered parsing method.""" - if entity.type_name not in action_parse_methods: - raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) - return action_parse_methods[entity.type_name](entity) +def parse_substitution(value: Text, frontend: Text) -> launch.SomeSubstitutionsType: + """Parse a substitution, using its registered parsing method.""" + if frontend in interpolation_fuctions: + return interpolation_fuctions[frontend](value) + else: + return default_substitution_interpolation(value) - def parse_substitution(self, value: Text, frontend: Text) -> launch.SomeSubstitutionsType: - """Parse a substitution, using its registered parsing method.""" - if frontend in interpolation_fuctions: - return interpolation_fuctions[frontend](value) - else: - return default_substitution_interpolation(value) - def parse_description(self, entity: Entity) -> launch.LaunchDescription: - """Parse a launch description.""" - if entity.type_name != 'launch': - raise RuntimeError('Expected \'launch\' as root tag') - actions = [self.parse_action(child) for child in self.__root_entity.children] - return launch.LaunchDescription(actions) +def parse_description(entity: Entity) -> launch.LaunchDescription: + """Parse a launch description.""" + if entity.type_name != 'launch': + raise RuntimeError('Expected \'launch\' as root tag') + actions = [parse_action(child) for child in entity.children] + return launch.LaunchDescription(actions) def str_to_bool(string): @@ -81,6 +79,7 @@ def load_optional_attribute( kwargs[key] = attr +@expose_action('executable') def parse_executable(entity: Entity): """Parse executable tag.""" cmd = entity.cmd @@ -109,10 +108,11 @@ def parse_executable(entity: Entity): cmd=cmd_list, **kwargs) + def parse_include(entity: Entity): """Parse a launch file to be included.""" # TODO(ivanpauno): Should be allow to include a programmatic launch file? How? # TODO(ivanpauno): Create launch_ros.actions.IncludeAction, supporting namespacing. # TODO(ivanpauno): Handle if and unless conditions. loaded_entity = Entity.load(entity.file, entity.parent) - Parser.parse_description(loaded_entity) + parse_description(loaded_entity) From 43d87c7554d48d7fbc85f800a5e932b63da49d1b Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 22 Apr 2019 12:41:50 -0300 Subject: [PATCH 10/75] Corrected error checking in decorators Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/expose.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index b3d6b5e06..f7eb55035 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -24,10 +24,6 @@ def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): """Implementats expose decorators.""" def expose_impl_decorator(exposed): - if name in parse_methods_map: - raise RuntimeError( - 'Two parsing methods exposed with same name.' - ) found_parse_method = None if inspect.isclass(exposed): if 'parse' in dir(exposed): @@ -39,6 +35,10 @@ def expose_impl_decorator(exposed): 'Exposed {} parser for {} is not a callable or a class' ' containg a parse method'.format(exposed_type, name) ) + if name in parse_methods_map and found_parse_method is not parse_methods_map[name]: + raise RuntimeError( + 'Two parsing methods exposed with same name.' + ) parse_methods_map[name] = found_parse_method return exposed return expose_impl_decorator From 7a031219ec93aeed7366aa558463effab2d28b66 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 23 Apr 2019 15:41:09 -0300 Subject: [PATCH 11/75] Correct launch_frontend.Entity Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index 6a15c8b48..0ed6d7014 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -25,6 +25,8 @@ from pkg_resources import iter_entry_points +frontend_entities = None + class Entity: """Single item in the intermediate front_end representation.""" @@ -36,10 +38,12 @@ def load( ) -> 'Entity': """Return an entity loaded with a markup file.""" # frontend_entities is not global to avoid a recursive import - frontend_entities = { - entry_point.name: entry_point.load() - for entry_point in iter_entry_points('launch_frontend.entity') - } + global frontend_entities + if frontend_entities is None: + frontend_entities = { + entry_point.name: entry_point.load() + for entry_point in iter_entry_points('launch_frontend.entity') + } if is_a(file, str): # This automatically recognizes 'file.xml' or 'file.launch.xml' # as a launch file using the xml frontend. From e2c50dd8d1aae20dfd4b905b12a4cc64beb02fc1 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 23 Apr 2019 15:43:45 -0300 Subject: [PATCH 12/75] Load entry_points which add parsing methods Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/expose.py | 2 +- launch_frontend/launch_frontend/parser.py | 14 ++++++++++++-- launch_frontend/launch_frontend/substitutions.py | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index f7eb55035..d81624f33 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -17,8 +17,8 @@ import inspect from typing import Text -substitution_parse_methods = {} action_parse_methods = {} +substitution_parse_methods = {} def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 406ed4b51..1acc7c7d1 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -22,8 +22,7 @@ from pkg_resources import iter_entry_points from .entity import Entity -from .expose import action_parse_methods -from .expose import expose_action +from .expose import action_parse_methods, expose_action from .substitutions import default_substitution_interpolation interpolation_fuctions = { @@ -31,9 +30,18 @@ for entry_point in iter_entry_points('launch_frontend.interpolate_substitution') } +extensions_loaded = False + + +def load_parser_extensions(): + for entry_point in iter_entry_points('launch_frontend.parser_extension'): + entry_point.load() + def parse_action(entity: Entity) -> launch.Action: """Parse an action, using its registered parsing method.""" + if not extensions_loaded: + load_parser_extensions() if entity.type_name not in action_parse_methods: raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) return action_parse_methods[entity.type_name](entity) @@ -41,6 +49,8 @@ def parse_action(entity: Entity) -> launch.Action: def parse_substitution(value: Text, frontend: Text) -> launch.SomeSubstitutionsType: """Parse a substitution, using its registered parsing method.""" + if not extensions_loaded: + load_parser_extensions() if frontend in interpolation_fuctions: return interpolation_fuctions[frontend](value) else: diff --git a/launch_frontend/launch_frontend/substitutions.py b/launch_frontend/launch_frontend/substitutions.py index 12a39c34a..6f4ddc3eb 100644 --- a/launch_frontend/launch_frontend/substitutions.py +++ b/launch_frontend/launch_frontend/substitutions.py @@ -20,8 +20,8 @@ from launch.substitutions import EnvironmentVariable from launch.substitutions import FindExecutable -from .expose import expose_substitution -from .expose import substitution_parse_methods + +from .expose import expose_substitution, substitution_parse_methods @expose_substitution('test') From 68cba5aefe227876db1f91471795b738af70118d Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 23 Apr 2019 15:45:16 -0300 Subject: [PATCH 13/75] Modified executable example Signed-off-by: ivanpauno --- launch_xml/test/launch_xml/executable.xml | 8 +++++--- launch_xml/test/launch_xml/test_executable.py | 11 ++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml index d5707c262..437c7a1da 100644 --- a/launch_xml/test/launch_xml/executable.xml +++ b/launch_xml/test/launch_xml/executable.xml @@ -1,3 +1,5 @@ - - - + + + + + diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index f5ab0e8c2..1d71cca60 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -16,17 +16,16 @@ from pathlib import Path -from launch import LaunchDescription from launch import LaunchService -from launch_frontend import Entity -from launch_frontend import parse_executable +from launch_frontend import Entity, parse_description def test_executable(): """Parse node xml example.""" root_entity = Entity.load(str(Path(__file__).parent / 'executable.xml')) - executable = parse_executable(root_entity) + ld = parse_description(root_entity) + executable = ld.entities[0] cmd = [i[0].perform(None) for i in executable.cmd] assert(cmd == ['ls', '-l', '-a', '-s']) @@ -39,9 +38,7 @@ def test_executable(): value = value[0].perform(None) assert(key == 'var') assert(value == '1') - assert(executable.prefix[0].perform(None) == 'time') - ld = LaunchDescription() - ld.add_action(executable) + # assert(executable.prefix[0].perform(None) == 'time') ls = LaunchService() ls.include_launch_description(ld) assert(0 == ls.run()) From 4450dd5a3d5f36224c037878ded55418a9238194 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 23 Apr 2019 16:57:23 -0300 Subject: [PATCH 14/75] Used parse_substitution in parse_executable Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/parser.py | 38 +++++++++++++++---- .../launch_frontend/substitutions.py | 1 + 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 1acc7c7d1..f13c62af4 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -78,6 +78,8 @@ def load_optional_attribute( kwargs: dict, entity: Entity, name: Text, + *, + subst: bool = True, constructor_name: Optional[Text] = None ): """Load an optional attribute of `entity` named `name` in `kwargs`.""" @@ -86,18 +88,30 @@ def load_optional_attribute( if constructor_name is not None: key = constructor_name if attr is not None: - kwargs[key] = attr + if subst: + kwargs[key] = parse_substitution(attr, entity.frontend) + else: + kwargs[key] = attr @expose_action('executable') def parse_executable(entity: Entity): """Parse executable tag.""" - cmd = entity.cmd + cmd = parse_substitution(entity.cmd, entity.frontend) kwargs = {} load_optional_attribute(kwargs, entity, 'cwd') load_optional_attribute(kwargs, entity, 'name') - load_optional_attribute(kwargs, entity, 'launch-prefix', 'prefix') - load_optional_attribute(kwargs, entity, 'output') + load_optional_attribute( + kwargs, + entity, + 'launch-prefix', + constructor_name='prefix' + ) + load_optional_attribute( + kwargs, + entity, + 'output', + subst=False) shell = getattr(entity, 'shell', None) if shell is not None: kwargs['shell'] = str_to_bool(shell) @@ -105,12 +119,20 @@ def parse_executable(entity: Entity): # Substitutions aren't allowing conditions now. env = getattr(entity, 'env', None) if env is not None: - env = {e.name: e.value for e in env} + env = {e.name: parse_substitution(e.value, e.frontend) for e in env} kwargs['additional_env'] = env args = getattr(entity, 'args', None) - args = args.split(' ') if args else [] - if not isinstance(args, list): - args = [args] + if args is not None: + args = parse_substitution(args, entity.frontend) + new_args = [] + for arg in args: + if isinstance(arg, str): + new_args.extend(arg.split(' ')) + else: + new_args.append(arg) + args = new_args + else: + args = [] cmd_list = [cmd] cmd_list.extend(args) # TODO(ivanpauno): Handle predicate conditions diff --git a/launch_frontend/launch_frontend/substitutions.py b/launch_frontend/launch_frontend/substitutions.py index 6f4ddc3eb..24e1822b3 100644 --- a/launch_frontend/launch_frontend/substitutions.py +++ b/launch_frontend/launch_frontend/substitutions.py @@ -179,6 +179,7 @@ def default_substitution_interpolation(string): # Is still nested in other substitution pos = cb + 1 continue + subst_list.append(replace_scapes(string[pos:])) if opening_brackets_pile: raise RuntimeError('Non matching substitution brackets.') return subst_list From cc0af6e13387f6ea9a0c20b9571f702e3d709d09 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 25 Apr 2019 13:43:24 -0300 Subject: [PATCH 15/75] Changed entity 'frontend' staticmethod to a property Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 2 +- launch_xml/launch_xml/entity.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index 0ed6d7014..e47bc27ad 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -62,7 +62,7 @@ def load( pass raise RuntimeError('Not recognized front-end implementation.') - @staticmethod + @property def frontend() -> Text: """Get which frontend is wrapping.""" raise NotImplementedError() diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index c887d1ba7..cf936610d 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -42,8 +42,8 @@ def __init__(self, self.__xml_element = xml_element self.__parent = parent - @staticmethod - def frontend() -> Text: + @property + def frontend(self) -> Text: """Get which frontend is wrapping.""" return 'xml' From 3ddbe059f98538fa74410f12f83c2c07f27ee6b1 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 25 Apr 2019 13:45:30 -0300 Subject: [PATCH 16/75] Deleted list substitution Signed-off-by: ivanpauno --- .../launch_frontend/substitutions.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/launch_frontend/launch_frontend/substitutions.py b/launch_frontend/launch_frontend/substitutions.py index 24e1822b3..64606fd3e 100644 --- a/launch_frontend/launch_frontend/substitutions.py +++ b/launch_frontend/launch_frontend/substitutions.py @@ -14,8 +14,6 @@ """Module for built-in front-end substitutions.""" -from typing import Text - from launch import SomeSubstitutionsType from launch.substitutions import EnvironmentVariable from launch.substitutions import FindExecutable @@ -32,19 +30,6 @@ def test(data): return data[0] -@expose_substitution('list') -def parse_list(string: Text): - """Parse a list substitution.""" - if len(string) > 1: - raise AttributeError('Expected a len 1 list.') - string = string[0] - pos = string.find('sep=') - sep = ',' - if pos == 0: - sep, string = string[4:].split(' ', 1) - return string.split(sep) - - def parse_find_executable(executable_name: SomeSubstitutionsType): """Return FindExecutable substitution.""" return FindExecutable(executable_name) @@ -187,5 +172,5 @@ def default_substitution_interpolation(string): if __name__ == '__main__': print(default_substitution_interpolation( - r'hola $(test \)como\$(\\) $(test $(list 1,2,3))' + r'hola $(test \)como\$(\\) $(test asd)' ' $(env $(env jkl $(test msj)) bsd)')) From 4a347febcc3c8814372747c52c0fa2730da9b6d2 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 25 Apr 2019 13:51:00 -0300 Subject: [PATCH 17/75] Updated cleaner executable args handling using list Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/parser.py | 10 ++-------- launch_xml/test/launch_xml/executable.xml | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index f13c62af4..c472f5c1c 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -123,16 +123,10 @@ def parse_executable(entity: Entity): kwargs['additional_env'] = env args = getattr(entity, 'args', None) if args is not None: - args = parse_substitution(args, entity.frontend) - new_args = [] - for arg in args: - if isinstance(arg, str): - new_args.extend(arg.split(' ')) - else: - new_args.append(arg) - args = new_args + args = [parse_substitution(arg, entity.frontend) for arg in args] else: args = [] + print(args) cmd_list = [cmd] cmd_list.extend(args) # TODO(ivanpauno): Handle predicate conditions diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml index 437c7a1da..55a52684b 100644 --- a/launch_xml/test/launch_xml/executable.xml +++ b/launch_xml/test/launch_xml/executable.xml @@ -1,5 +1,5 @@ - + From 3dd5c3107b5568fbd27acdcb032a353eab586052 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 9 May 2019 09:55:26 -0300 Subject: [PATCH 18/75] Using a grammar for parsing substitutions. Moved to frontend specific parsers. Signed-off-by: ivanpauno --- launch/launch/__init__.py | 2 + launch_frontend/launch_frontend/__init__.py | 10 +- .../launch_frontend/action_parse_methods.py | 118 +++++++++++ .../launch_frontend/convert_text_to.py | 68 ++++++ launch_frontend/launch_frontend/entity.py | 44 ---- launch_frontend/launch_frontend/expose.py | 41 +++- launch_frontend/launch_frontend/grammar.lark | 61 ++++++ .../launch_frontend/parse_substitution.py | 108 ++++++++++ launch_frontend/launch_frontend/parser.py | 193 ++++++++---------- .../substitution_parse_methods.py | 60 ++++++ .../launch_frontend/substitutions.py | 176 ---------------- .../launch_frontend/test_substitutions.py | 112 ++++++++++ launch_xml/launch_xml/__init__.py | 6 +- launch_xml/launch_xml/entity.py | 15 -- launch_xml/launch_xml/parser.py | 35 ++++ launch_xml/setup.py | 4 +- launch_xml/test/launch_xml/executable.xml | 2 +- launch_xml/test/launch_xml/test_executable.py | 6 +- 18 files changed, 698 insertions(+), 363 deletions(-) create mode 100644 launch_frontend/launch_frontend/action_parse_methods.py create mode 100644 launch_frontend/launch_frontend/convert_text_to.py create mode 100644 launch_frontend/launch_frontend/grammar.lark create mode 100644 launch_frontend/launch_frontend/parse_substitution.py create mode 100644 launch_frontend/launch_frontend/substitution_parse_methods.py delete mode 100644 launch_frontend/launch_frontend/substitutions.py create mode 100644 launch_frontend/test/launch_frontend/test_substitutions.py create mode 100644 launch_xml/launch_xml/parser.py diff --git a/launch/launch/__init__.py b/launch/launch/__init__.py index 1cdc897b7..72aeeecd6 100644 --- a/launch/launch/__init__.py +++ b/launch/launch/__init__.py @@ -18,6 +18,7 @@ from . import conditions from . import events from . import logging +from . import substitutions from .action import Action from .condition import Condition from .event import Event @@ -39,6 +40,7 @@ 'conditions', 'events', 'logging', + 'substitutions', 'Action', 'Condition', 'Event', diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index e3126e578..6e4c11ad1 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -14,9 +14,13 @@ """Main entry point for the `launch_frontend` package.""" +# All files containing parsing methods should be imported here. +# If not, the action or substitution isn't going to be exposed. +from . import action_parse_methods # noqa: F401 +from . import substitution_parse_methods # noqa: F401 from .entity import Entity from .expose import __expose_impl, expose_action, expose_substitution -from .parser import parse_action, parse_description, parse_substitution +from .parser import Parser __all__ = [ @@ -25,7 +29,5 @@ '__expose_impl', 'expose_action', 'expose_substitution', - 'parse_action', - 'parse_description', - 'parse_substitution', + 'Parser', ] diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py new file mode 100644 index 000000000..987a6de3c --- /dev/null +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -0,0 +1,118 @@ +# Copyright 2019 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. + +"""Module for launch action parsing methods.""" + +import shlex + +import launch +from launch.conditions import IfCondition +from launch.conditions import UnlessCondition +from launch.substitutions import TextSubstitution + +from .convert_text_to import str_to_bool +from .entity import Entity +from .expose import expose_action +from .parser import Parser + + +@expose_action('executable') +def parse_executable(entity: Entity, parser: Parser): + """Parse executable tag.""" + cmd = parser.parse_substitution(entity.cmd) + kwargs = {} + cwd = getattr(entity, 'cwd', None) + if cwd is not None: + kwargs['cwd'] = parser.parse_substitution(cwd) + name = getattr(entity, 'name', None) + if name is not None: + kwargs['name'] = parser.parse_substitution(name) + prefix = getattr(entity, 'launch-prefix', None) + if prefix is not None: + kwargs['prefix'] = parser.parse_substitution(prefix) + output = getattr(entity, 'output', None) + if output is not None: + kwargs['output'] = output + shell = getattr(entity, 'shell', None) + if shell is not None: + kwargs['shell'] = str_to_bool(shell) + # Conditions won't be allowed in the `env` tag. + # If that feature is needed, `set_enviroment_variable` and + # `unset_enviroment_variable` actions should be used. + env = getattr(entity, 'env', None) + if env is not None: + env = {e.name: parser.parse_substitution(e.value) for e in env} + kwargs['additional_env'] = env + args = getattr(entity, 'args', None) + # `args` is supposed to be a list separated with ' '. + # All the found `TextSubstitution` items are split and + # added to the list again as a `TextSubstitution`. + # Another option: Enforce to explicetly write a list in + # the launch file (if that's wanted) + # In xml 'args' and 'args-sep' tags should be used. + if args is not None: + args = parser.parse_substitution(args) + new_args = [] + for arg in args: + if isinstance(arg, TextSubstitution): + text = arg.text + text = shlex.split(text) + text = [TextSubstitution(text=item) for item in text] + new_args.extend(text) + else: + new_args.append(arg) + args = new_args + else: + args = [] + # Option 2: + # if args is not None: + # if isinstance(args, Text): + # args = [args] + # args = [parser.parse_substitution(arg) for arg in args] + # else: + # args = [] + cmd_list = [cmd] + cmd_list.extend(args) + if_cond = getattr(entity, 'if', None) + unless_cond = getattr(entity, 'unless', None) + if if_cond is not None and unless_cond is not None: + raise RuntimeError("if and unless conditions can't be usede simultaneously") + if if_cond is not None: + kwargs['condition'] = IfCondition(predicate_expression=if_cond) + if unless_cond is not None: + kwargs['condition'] = UnlessCondition(predicate_expression=unless_cond) + + return launch.actions.ExecuteProcess( + cmd=cmd_list, + **kwargs + ) + + +@expose_action('let') +def parse_let(entity: Entity, parser: Parser): + """Parse let tag.""" + name = parser.parse_substitution(entity.var) + value = parser.parse_substitution(entity.value) + return launch.actions.SetLaunchConfiguration( + name, + value + ) + +# def parse_include(entity: Entity): +# """Parse a launch file to be included.""" +# # TODO(ivanpauno): Should be allow to include a programmatic launch file? How? +# # TODO(ivanpauno): Create launch_ros.actions.IncludeAction, supporting namespacing. +# # TODO(ivanpauno): Handle if and unless conditions. +# loaded_entity = Entity.load(entity.file, entity.parent) +# parse_description(loaded_entity) diff --git a/launch_frontend/launch_frontend/convert_text_to.py b/launch_frontend/launch_frontend/convert_text_to.py new file mode 100644 index 000000000..614be2c42 --- /dev/null +++ b/launch_frontend/launch_frontend/convert_text_to.py @@ -0,0 +1,68 @@ +# Copyright 2019 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. + +"""Module that provides methods for converting string to other data types.""" + +from typing import Iterable +from typing import Text +from typing import Union + + +def str_to_bool(string: Text): + """Convert text to python bool.""" + if string.lower() in ('0', 'false'): + return False + if string.lower() in ('1', 'true'): + return True + raise RuntimeError('Expected "true" or "false", got {}'.format(string)) + + +def guess_type_from_string(value: Union[Text, Iterable[Text]]): + """Guess the desired type of the parameter based on the string value.""" + if not isinstance(value, Text): + return [__guess_type_from_string(item) for item in value] + return __guess_type_from_string(value) + + +def __guess_type_from_string(string_value: Text): + if __is_bool(string_value): + return string_value.lower() == 'true' + if __is_integer(string_value): + return int(string_value) + if __is_float(string_value): + return float(string_value) + else: + return string_value + + +def __is_bool(string_value: Text): + if string_value.lower() in ('false', 'true'): + return True + return False + + +def __is_integer(string_value: Text): + try: + int(string_value) + except ValueError: + return False + return True + + +def __is_float(string_value: Text): + try: + float(string_value) + except ValueError: + return False + return True diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index e47bc27ad..b7453fd9b 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -14,59 +14,15 @@ """Module for Entity class.""" -import io from typing import Any from typing import List from typing import Optional from typing import Text -from typing import Union - -from launch.utilities import is_a - -from pkg_resources import iter_entry_points - -frontend_entities = None class Entity: """Single item in the intermediate front_end representation.""" - @staticmethod - def load( - file: Union[str, io.TextIOBase], - parent: 'Entity' = None - ) -> 'Entity': - """Return an entity loaded with a markup file.""" - # frontend_entities is not global to avoid a recursive import - global frontend_entities - if frontend_entities is None: - frontend_entities = { - entry_point.name: entry_point.load() - for entry_point in iter_entry_points('launch_frontend.entity') - } - if is_a(file, str): - # This automatically recognizes 'file.xml' or 'file.launch.xml' - # as a launch file using the xml frontend. - frontend_name = file.rsplit('.', 1)[1] - if frontend_name in frontend_entities: - return frontend_entities[frontend_name].load(file) - # If not, apply brute force. - # TODO(ivanpauno): Maybe, we want to force correct file naming. - # In that case, we should raise an error here. - # Note(ivanpauno): It's impossible to recognize a wrong formatted - # file from a non-recognized front-end implementation. - for implementation in frontend_entities: - try: - return implementation.load(file) - except Exception: - pass - raise RuntimeError('Not recognized front-end implementation.') - - @property - def frontend() -> Text: - """Get which frontend is wrapping.""" - raise NotImplementedError() - @property def type_name(self) -> Text: """Get Entity type.""" diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index d81624f33..eb387a479 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -22,12 +22,37 @@ def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): - """Implementats expose decorators.""" + """ + Return a decorator for exposing a parsing method in a dictionary. + + The returned decorator will check the following things in order: + - If it is decorating a class, it will look for a method called `parse` and store it + as the parsing method. The `parse` method is supposed to be static. If the class + don't have a `parse` method, it will raise a `RuntimeError`. + - If it is decorating a callable, it will store it as the parsing method. + - If not, it will raise a `RuntimeError`. + + If two different parsing methods are exposed using the same `name`, a `RuntimeError` + will be raised. + + :param: name a string which specifies the key used for storing the parsing + method in the dictionary. + :param: parse_methods_map a dict where the parsing method will be stored. + :exposed_type: A string specifing the parsing function type. + Only used for having clearer error log messages. + """ + # TODO(ivanpauno): Check signature of the registered method/parsing function. + # TODO(ivanpauno): Infer a parsing function from the constructor annotations. + # That should be done in case a method called 'parse' is not found in the decorated class. def expose_impl_decorator(exposed): found_parse_method = None if inspect.isclass(exposed): if 'parse' in dir(exposed): found_parse_method = exposed.parse + else: + raise RuntimeError( + "Did not found a method called 'parse' in the class being decorated." + ) elif callable(exposed): found_parse_method = exposed if not found_parse_method: @@ -37,7 +62,7 @@ def expose_impl_decorator(exposed): ) if name in parse_methods_map and found_parse_method is not parse_methods_map[name]: raise RuntimeError( - 'Two parsing methods exposed with same name.' + 'Two {} parsing methods exposed with same name.'.format(exposed_type) ) parse_methods_map[name] = found_parse_method return exposed @@ -45,10 +70,18 @@ def expose_impl_decorator(exposed): def expose_substitution(name: Text): - """Exposes a substitution.""" + """ + Return a decorator for exposing a substitution. + + Read __expose_impl documentation. + """ return __expose_impl(name, substitution_parse_methods, 'substitution') def expose_action(name: Text): - """Exposes an action.""" + """ + Return a decorator for exposing an action. + + Read __expose_impl documentation. + """ return __expose_impl(name, action_parse_methods, 'action') diff --git a/launch_frontend/launch_frontend/grammar.lark b/launch_frontend/launch_frontend/grammar.lark new file mode 100644 index 000000000..ffe854d59 --- /dev/null +++ b/launch_frontend/launch_frontend/grammar.lark @@ -0,0 +1,61 @@ +%import common.DIGIT +%import common.FLOAT +%import common.INT +%import common.LETTER +%import common.WS + +IDENTIFIER: LETTER (LETTER | DIGIT | "_" | "-")* + +UNQUOTED_STRING: (/[^'"$]|\$(?=!\()|(?<=\\)\$/ | "\"" | "\'")+ +UNQUOTED_RSTRING: (/[^ '"$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\"" | "\'")+ + +SINGLE_QUOTED_STRING: (/[^'$]|\$(?=!\()|(?<=\\)\$/ | "\'")+ +SINGLE_QUOTED_RSTRING: (/[^ '$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\'")+ + +DOUBLE_QUOTED_STRING: (/[^"$]|\$(?=!\()|(?<=\\)\$/ | "\"")+ +DOUBLE_QUOTED_RSTRING: (/[^ "$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\"")+ + +single_quoted_part: single_quoted_substitution + | SINGLE_QUOTED_RSTRING + +single_quoted_value: single_quoted_part+ + +single_quoted_arguments: single_quoted_value + | single_quoted_arguments " " single_quoted_value + +single_quoted_substitution: "$" "(" IDENTIFIER (" " single_quoted_arguments)? ")" + +single_quoted_fragment: single_quoted_substitution | SINGLE_QUOTED_STRING + +single_quoted_template: "'" single_quoted_fragment* "'" + +double_quoted_part: double_quoted_substitution + | DOUBLE_QUOTED_RSTRING + +double_quoted_value: double_quoted_part+ + +double_quoted_arguments: double_quoted_value + | double_quoted_arguments " " double_quoted_value + +double_quoted_substitution: "$" "(" IDENTIFIER (" " double_quoted_arguments)? ")" + +double_quoted_fragment: double_quoted_substitution | DOUBLE_QUOTED_STRING + +double_quoted_template: "\"" double_quoted_fragment* "\"" + +part: substitution + | UNQUOTED_RSTRING + +value: part+ + | single_quoted_template + | double_quoted_template + +arguments: value + | arguments " " value + +substitution: "$" "(" IDENTIFIER (" " arguments)? ")" + +fragment: substitution + | UNQUOTED_STRING + +template: fragment+ diff --git a/launch_frontend/launch_frontend/parse_substitution.py b/launch_frontend/launch_frontend/parse_substitution.py new file mode 100644 index 000000000..8c517a242 --- /dev/null +++ b/launch_frontend/launch_frontend/parse_substitution.py @@ -0,0 +1,108 @@ +# Copyright 2019 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. + +"""Module for parsing substitutions.""" + +import os +from typing import Text + +from lark import Lark +from lark import Token +from lark import Transformer + +from launch.substitutions import TextSubstitution + +from launch_frontend.expose import substitution_parse_methods + + +def replace_escaped_characters(data: Text, beg: int = 0) -> Text: + """Search escaped characters and replace them.""" + pos = data.find('\\', beg) + if pos == -1: + return data[beg:] + return data[beg:pos] + data[pos+1] + replace_escaped_characters(data, pos+2) + + +class ExtractSubstitution(Transformer): + """Extract a substitution.""" + + def part(self, content): + assert(len(content) == 1) + content = content[0] + if isinstance(content, Token): + assert content.type.endswith('_RSTRING') + return TextSubstitution(text=replace_escaped_characters(content.value)) + return content + + single_quoted_part = part + double_quoted_part = part + + def value(self, parts): + if len(parts) == 1 and isinstance(parts[0], list): + # Deal with single and double quoted templates + return parts[0] + return parts + + single_quoted_value = value + double_quoted_value = value + + def arguments(self, values): + if len(values) > 1: + # Deal with tail recursive argument parsing + return [*values[0], values[1]] + return values + + single_quoted_arguments = arguments + double_quoted_arguments = arguments + + def substitution(self, args): + assert len(args) >= 1 + name = args[0] + assert isinstance(name, Token) + assert name.type == 'IDENTIFIER' + # TODO(hidmic): Lookup and instantiate Substitution + if name.value not in substitution_parse_methods: + raise RuntimeError( + 'Unknown substitution: {}'.format(name.value)) + return substitution_parse_methods[name.value](*args[1:]) + + single_quoted_substitution = substitution + double_quoted_substitution = substitution + + def fragment(self, content): + assert len(content) == 1 + content = content[0] + if isinstance(content, Token): + assert content.type.endswith('_STRING') + return TextSubstitution(text=replace_escaped_characters(content.value)) + return content + + single_quoted_fragment = fragment + double_quoted_fragment = fragment + + def template(self, fragments): + return fragments + + single_quoted_template = template + double_quoted_template = template + + +grammar_file = os.path.join(os.path.dirname(__file__), 'grammar.lark') +parser = Lark.open(grammar_file, start='template') +transformer = ExtractSubstitution() + + +def default_parse_substitution(string_value): + tree = parser.parse(string_value) + return transformer.transform(tree) diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index c472f5c1c..27db1324e 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -12,18 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Module for Parser methods.""" +"""Module for Parser class and parsing methods.""" -from typing import Optional +import io from typing import Text +from typing import Union import launch +from launch.utilities import is_a from pkg_resources import iter_entry_points from .entity import Entity -from .expose import action_parse_methods, expose_action -from .substitutions import default_substitution_interpolation +from .expose import action_parse_methods +from .parse_substitution import default_parse_substitution interpolation_fuctions = { entry_point.name: entry_point.load() @@ -33,112 +35,77 @@ extensions_loaded = False -def load_parser_extensions(): - for entry_point in iter_entry_points('launch_frontend.parser_extension'): - entry_point.load() - - -def parse_action(entity: Entity) -> launch.Action: - """Parse an action, using its registered parsing method.""" - if not extensions_loaded: - load_parser_extensions() - if entity.type_name not in action_parse_methods: - raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) - return action_parse_methods[entity.type_name](entity) - - -def parse_substitution(value: Text, frontend: Text) -> launch.SomeSubstitutionsType: - """Parse a substitution, using its registered parsing method.""" - if not extensions_loaded: - load_parser_extensions() - if frontend in interpolation_fuctions: - return interpolation_fuctions[frontend](value) - else: - return default_substitution_interpolation(value) - - -def parse_description(entity: Entity) -> launch.LaunchDescription: - """Parse a launch description.""" - if entity.type_name != 'launch': - raise RuntimeError('Expected \'launch\' as root tag') - actions = [parse_action(child) for child in entity.children] - return launch.LaunchDescription(actions) - - -def str_to_bool(string): - """Convert xs::boolean to python bool.""" - if not string or string.lower() in ('0', 'false'): - return False - if string.lower() in ('1', 'true'): - return True - raise RuntimeError('Expected "true" or "false", got {}'.format(string)) - - -def load_optional_attribute( - kwargs: dict, - entity: Entity, - name: Text, - *, - subst: bool = True, - constructor_name: Optional[Text] = None -): - """Load an optional attribute of `entity` named `name` in `kwargs`.""" - attr = getattr(entity, name, None) - key = name - if constructor_name is not None: - key = constructor_name - if attr is not None: - if subst: - kwargs[key] = parse_substitution(attr, entity.frontend) - else: - kwargs[key] = attr - - -@expose_action('executable') -def parse_executable(entity: Entity): - """Parse executable tag.""" - cmd = parse_substitution(entity.cmd, entity.frontend) - kwargs = {} - load_optional_attribute(kwargs, entity, 'cwd') - load_optional_attribute(kwargs, entity, 'name') - load_optional_attribute( - kwargs, - entity, - 'launch-prefix', - constructor_name='prefix' - ) - load_optional_attribute( - kwargs, - entity, - 'output', - subst=False) - shell = getattr(entity, 'shell', None) - if shell is not None: - kwargs['shell'] = str_to_bool(shell) - # TODO(ivanpauno): How will predicates be handle in env? - # Substitutions aren't allowing conditions now. - env = getattr(entity, 'env', None) - if env is not None: - env = {e.name: parse_substitution(e.value, e.frontend) for e in env} - kwargs['additional_env'] = env - args = getattr(entity, 'args', None) - if args is not None: - args = [parse_substitution(arg, entity.frontend) for arg in args] - else: - args = [] - print(args) - cmd_list = [cmd] - cmd_list.extend(args) - # TODO(ivanpauno): Handle predicate conditions - return launch.actions.ExecuteProcess( - cmd=cmd_list, - **kwargs) - - -def parse_include(entity: Entity): - """Parse a launch file to be included.""" - # TODO(ivanpauno): Should be allow to include a programmatic launch file? How? - # TODO(ivanpauno): Create launch_ros.actions.IncludeAction, supporting namespacing. - # TODO(ivanpauno): Handle if and unless conditions. - loaded_entity = Entity.load(entity.file, entity.parent) - parse_description(loaded_entity) +class Parser: + """ + Abstract class for parsing actions, substitutions and descriptions. + + Implementations of the parser class, should override the load method. + They could also override the parse_substitution method, or not. + load_parser_extensions, parse_action and parse_description are not suposed to be overrided. + """ + + extensions_loaded = False + frontend_parsers = None + + @classmethod + def load_parser_extensions(cls): + """Load parser extension, in order to get all the exposed substitutions and actions.""" + if cls.extensions_loaded is False: + for entry_point in iter_entry_points('launch_frontend.parser_extension'): + entry_point.load() + cls.extensions_loaded = True + + @classmethod + def load_parser_implementations(cls): + """Load all the available frontend entities.""" + if cls.frontend_parsers is None: + cls.frontend_parsers = { + entry_point.name: entry_point.load() + for entry_point in iter_entry_points('launch_frontend.parser') + } + + @classmethod + def parse_action(cls, entity: Entity) -> launch.Action: + """Parse an action, using its registered parsing method.""" + cls.load_parser_extensions() + if entity.type_name not in action_parse_methods: + raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) + return action_parse_methods[entity.type_name](entity, cls) + + @classmethod + def parse_substitution(cls, value: Text) -> launch.SomeSubstitutionsType: + """Parse a substitution.""" + return default_parse_substitution(value) + + @classmethod + def parse_description(cls, entity: Entity) -> launch.LaunchDescription: + """Parse a launch description.""" + if entity.type_name != 'launch': + raise RuntimeError('Expected \'launch\' as root tag') + actions = [cls.parse_action(child) for child in entity.children] + return launch.LaunchDescription(actions) + + @classmethod + def load( + cls, + file: Union[str, io.TextIOBase], + ) -> (Entity, 'Parser'): + """Return an entity loaded with a markup file.""" + cls.load_parser_implementations() + if is_a(file, str): + # This automatically recognizes 'file.xml' or 'file.launch.xml' + # as a launch file using the xml frontend. + frontend_name = file.rsplit('.', 1)[1] + if frontend_name in cls.frontend_parsers: + return cls.frontend_parsers[frontend_name].load(file) + # If not, apply brute force. + # TODO(ivanpauno): Maybe, we want to force correct file naming. + # In that case, we should raise an error here. + # TODO(ivanpauno): Recognize a wrong formatted file error from + # unknown front-end implementation error. + for implementation in cls.frontend_parsers.values(): + try: + return implementation.load(file) + except Exception: + pass + raise RuntimeError('Not recognized front-end implementation.') diff --git a/launch_frontend/launch_frontend/substitution_parse_methods.py b/launch_frontend/launch_frontend/substitution_parse_methods.py new file mode 100644 index 000000000..4c9ba8a92 --- /dev/null +++ b/launch_frontend/launch_frontend/substitution_parse_methods.py @@ -0,0 +1,60 @@ +# Copyright 2019 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. + +"""Module for launch substitution parsing methods.""" + +import launch + +from .expose import expose_substitution + + +# @expose_substitution('test') +# def test(data): +# """Delete me please.""" +# if len(data) > 1: +# raise AttributeError('Expected a len 1 list') +# return data[0] + + +@expose_substitution('env') +def parse_env(data: launch.SomeSubstitutionsType): + """Parse EnviromentVariable substitution.""" + if not data or len(data) > 2: + raise AttributeError('env substitution expects 1 or 2 arguments') + name = data[0] + kwargs = {} + if len(data) == 2: + kwargs['default_value'] = data[1] + return launch.substitutions.EnvironmentVariable(name, **kwargs) + + +@expose_substitution('var') +def parse_var(data: launch.SomeSubstitutionsType): + """Parse FindExecutable substitution.""" + if not data or len(data) > 2: + raise AttributeError('var substitution expects 1 or 2 arguments') + name = data[0] + kwargs = {} + if len(data) == 2: + kwargs['default'] = data[1] + return launch.substitutions.LaunchConfiguration(name, **kwargs) + + +@expose_substitution('find-exec') +def parse_find_exec(data: launch.SomeSubstitutionsType): + """Parse FindExecutable substitution.""" + if not data or len(data) > 1: + raise AttributeError('find-exec substitution expects 1 argument') + name = data[0] + return launch.substitutions.FindExecutable(name=name) diff --git a/launch_frontend/launch_frontend/substitutions.py b/launch_frontend/launch_frontend/substitutions.py deleted file mode 100644 index 64606fd3e..000000000 --- a/launch_frontend/launch_frontend/substitutions.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2019 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. - -"""Module for built-in front-end substitutions.""" - -from launch import SomeSubstitutionsType -from launch.substitutions import EnvironmentVariable -from launch.substitutions import FindExecutable - - -from .expose import expose_substitution, substitution_parse_methods - - -@expose_substitution('test') -def test(data): - """Delete me please.""" - if len(data) > 1: - raise AttributeError('Expected a len 1 list') - return data[0] - - -def parse_find_executable(executable_name: SomeSubstitutionsType): - """Return FindExecutable substitution.""" - return FindExecutable(executable_name) - - -@expose_substitution('env') -def parse_env(data): - """Return FindExecutable substitution.""" - if not data or len(data) > 2: - raise AttributeError('env substitution expected 1 or 2 arguments') - name = data[0] - default = data[1] if len(data) == 2 else '' - # print(name) - # print(default) - return EnvironmentVariable(name, default_value=default) - - -def find_no_scaped(string, substr, pos): - """Find the first non scaped 'substr' substring in 'string', starting from 'pos'.""" - scaped_substr = '\\' + substr - dscaped_substr = '\\' + scaped_substr - while True: - pos_found = string.find(substr, pos) - pos_found_scaped = string.find(scaped_substr, pos) - pos_found_dscaped = string.find(dscaped_substr, pos) - is_double_scaped = pos_found_dscaped >= 0 and pos_found == pos_found_dscaped + 2 - is_scaped = pos_found_scaped >= 0 and pos_found == pos_found_scaped + 1 - is_scaped = not is_double_scaped and is_scaped - if pos_found < 0 or not is_scaped: - return pos_found - pos = pos_found + 1 - - -def replace_scapes(string): - ret = '' - pos = 0 - while True: - last_pos = pos - pos = string.find('\\', pos) - if pos < 0: - return ret + string[last_pos:] - if string[pos+1] == '\\': - ret = ret + string[last_pos:pos] + '\\' - pos = pos + 2 - continue - if string[pos+1:pos+3] == '$(': - ret = ret + string[last_pos:pos] + '$(' - pos = pos + 3 - continue - if string[pos+1] == ')': - ret = ret + string[last_pos:pos] + ')' - pos = pos + 2 - continue - raise RuntimeError('Wrong backslash usage in string') - - -def default_substitution_interpolation(string): - """Interpolate substitutions in a string.""" - # TODO(ivanpauno): Use 're' package to do this in a cleaner way. - # This scans from left to right. It pushes the position - # of the opening brackets. When it finds a closing bracket - # it pops and substitute. - # The output is a list of substitutions and strings. - subst_list = [] # Substitutions list to be returned. - pos = 0 # Position of the string when we should continue parsing. - opening_brackets_pile = [] # Pile containing opening brackets. - # A dict, containing the nested substitutions that have been done. - # The key is the nesting level. - # Each item is a list, in order to handle substitutions that takes - # more than one argument, like $(env var default). - nested_substitutions = dict({}) - while 1: - ob = find_no_scaped(string, '$(', pos) - cb = find_no_scaped(string, ')', pos) - if ob >= 0 and ob < cb: - # New opening bracket found - if opening_brackets_pile: - middle_string = None - if pos > opening_brackets_pile[-1] + 2: - middle_string = string[pos:ob] - else: - # Skip the key - _, middle_string = string[ - opening_brackets_pile[-1]:ob].split(' ', 1) - if middle_string: - try: - nested_substitutions[len(opening_brackets_pile)-1].append( - middle_string) - except (TypeError, KeyError): - nested_substitutions[len(opening_brackets_pile)-1] = \ - [middle_string] - opening_brackets_pile.append(ob) - pos = ob + 2 - continue - if cb >= 0: - # New closing bracket found - ob_pop = opening_brackets_pile.pop() - subst_key, subst_value = string[ob_pop+2:cb].split(' ', 1) - if subst_key not in substitution_parse_methods: - # Unknown substitution - raise RuntimeError( - 'Invalid substitution type: {}'.format(subst_key)) - if len(opening_brackets_pile)+1 not in nested_substitutions: - # Doesn't have a nested substitution inside. - try: - nested_substitutions[len(opening_brackets_pile)].append( - substitution_parse_methods[subst_key]([replace_scapes(subst_value)])) - except (TypeError, KeyError): - nested_substitutions[len(opening_brackets_pile)] = \ - [substitution_parse_methods[subst_key]([replace_scapes(subst_value)])] - else: - # Have a nested substitution inside - try: - nested_substitutions[len(opening_brackets_pile)].append( - substitution_parse_methods[subst_key]( - nested_substitutions[len(opening_brackets_pile)+1]) - ) - except (TypeError, KeyError): - nested_substitutions[len(opening_brackets_pile)] = [ - substitution_parse_methods[subst_key]( - nested_substitutions[len(opening_brackets_pile)+1]) - ] - del nested_substitutions[len(opening_brackets_pile)+1] - if not opening_brackets_pile: - # Is not nested inside other substitution - subst_list.append(replace_scapes(string[:ob_pop])) - subst_list.extend(nested_substitutions[0]) - string = string[cb+1:] - pos = 0 - nested_substitutions = dict({}) - else: - # Is still nested in other substitution - pos = cb + 1 - continue - subst_list.append(replace_scapes(string[pos:])) - if opening_brackets_pile: - raise RuntimeError('Non matching substitution brackets.') - return subst_list - - -if __name__ == '__main__': - print(default_substitution_interpolation( - r'hola $(test \)como\$(\\) $(test asd)' - ' $(env $(env jkl $(test msj)) bsd)')) diff --git a/launch_frontend/test/launch_frontend/test_substitutions.py b/launch_frontend/test/launch_frontend/test_substitutions.py new file mode 100644 index 000000000..276d706a2 --- /dev/null +++ b/launch_frontend/test/launch_frontend/test_substitutions.py @@ -0,0 +1,112 @@ +# Copyright 2019 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. + +"""Test the default substitution interpolator.""" + +from launch.substitutions import TextSubstitution + +from launch_frontend.expose import expose_substitution +from launch_frontend.parse_substitution import default_parse_substitution + + +def test_text_only(): + subst = default_parse_substitution('yes') + assert len(subst) == 1 + assert subst[0].perform(None) == 'yes' + subst = default_parse_substitution('10') + assert len(subst) == 1 + assert subst[0].perform(None) == '10' + subst = default_parse_substitution('10e4') + assert len(subst) == 1 + assert subst[0].perform(None) == '10e4' + subst = default_parse_substitution('10e4') + assert len(subst) == 1 + assert subst[0].perform(None) == '10e4' + + +@expose_substitution('test') +def parse_test_substitution(data): + if not data or len(data) > 1: + raise RuntimeError() + return TextSubstitution(text=''.join([i.perform(None) for i in data[0]])) + + +def test_text_with_embedded_substitutions(): + subst = default_parse_substitution('why_$(test asd)_asdasd_$(test bsd)') + assert len(subst) == 4 + assert subst[0].perform(None) == 'why_' + assert subst[1].perform(None) == 'asd' + assert subst[2].perform(None) == '_asdasd_' + assert subst[3].perform(None) == 'bsd' + +# TODO(ivanpauno): Don't deppend on substitution parsing methods for testing the interpolator. +# Write some dummy substitutions and parsing methods instead. + + +def test_substitution_with_multiple_arguments(): + subst = default_parse_substitution('$(env what heck)') + assert len(subst) == 1 + subst = subst[0] + assert subst.name[0].perform(None) == 'what' + assert subst.default_value[0].perform(None) == 'heck' + + +def test_escaped_characters(): + subst = default_parse_substitution(r'$(env what/\$\(test asd\\\)) 10 10)') + assert len(subst) == 2 + assert subst[0].name[0].perform(None) == 'what/$(test' + assert subst[0].default_value[0].perform(None) == r'asd\)' + assert subst[1].perform(None) == ' 10 10)' + + +def test_nested_substitutions(): + subst = default_parse_substitution('$(env what/$(test asd) 10) 10 10)') + assert len(subst) == 2 + assert len(subst[0].name) == 2 + assert subst[0].name[0].perform(None) == 'what/' + assert subst[0].name[1].perform(None) == 'asd' + assert subst[0].default_value[0].perform(None) == '10' + assert subst[1].perform(None) == ' 10 10)' + + +def test_quoted_nested_substitution(): + subst = default_parse_substitution( + 'go_to_$(env WHERE asd)_of_$(env ' + "'something $(test 10)')" + ) + assert len(subst) == 4 + assert subst[0].perform(None) == 'go_to_' + assert subst[1].name[0].perform(None) == 'WHERE' + assert subst[1].default_value[0].perform(None) == 'asd' + assert subst[2].perform(None) == '_of_' + assert subst[3].name[0].perform(None) == 'something ' + assert subst[3].name[1].perform(None) == '10' + assert subst[3].default_value[0].perform(None) == '' + + +def test_double_quoted_nested_substitution(): + subst = default_parse_substitution( + r'$(env "asd_bsd_qsd_$(test "asd_bds")" "$(env DEFAULT)_qsd")' + ) + assert len(subst) == 1 + + +if __name__ == '__main__': + test_text_only() + test_text_with_embedded_substitutions() + test_substitution_with_multiple_arguments() + test_escaped_characters() + test_nested_substitutions() + test_quoted_nested_substitution() + test_double_quoted_nested_substitution() diff --git a/launch_xml/launch_xml/__init__.py b/launch_xml/launch_xml/__init__.py index b079be908..f3d901609 100644 --- a/launch_xml/launch_xml/__init__.py +++ b/launch_xml/launch_xml/__init__.py @@ -15,5 +15,9 @@ """Main entry point for the `launch_xml` package.""" from .entity import Entity +from .parser import Parser -__all__ = ['Entity'] +__all__ = [ + 'Entity', + 'Parser', +] diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index cf936610d..fc8a38ed0 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -14,10 +14,8 @@ """Module for Entity class.""" -import io from typing import Optional from typing import Text -from typing import Union import xml.etree.ElementTree as ET import launch_frontend @@ -26,14 +24,6 @@ class Entity(launch_frontend.Entity): """Single item in the intermediate XML front_end representation.""" - @staticmethod - def load( - stream: Union[str, io.TextIOBase], - parent: 'Entity' = None - ) -> 'Entity': - """Return entity loaded with markup file.""" - return Entity(ET.parse(stream).getroot()) - def __init__(self, xml_element: ET.Element = None, *, @@ -42,11 +32,6 @@ def __init__(self, self.__xml_element = xml_element self.__parent = parent - @property - def frontend(self) -> Text: - """Get which frontend is wrapping.""" - return 'xml' - @property def type_name(self) -> Text: """Get Entity type.""" diff --git a/launch_xml/launch_xml/parser.py b/launch_xml/launch_xml/parser.py new file mode 100644 index 000000000..a1eb34ee0 --- /dev/null +++ b/launch_xml/launch_xml/parser.py @@ -0,0 +1,35 @@ +# Copyright 2019 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. + +"""Module for XML Parser class.""" + +import io +from typing import Union +import xml.etree.ElementTree as ET + +import launch_frontend + +from .entity import Entity + + +class Parser(launch_frontend.Parser): + """XML parser implementation.""" + + @classmethod + def load( + cls, + file: Union[str, io.TextIOBase], + ) -> (Entity, 'Parser'): + """Return entity loaded with markup file.""" + return (Entity(ET.parse(file).getroot()), cls) diff --git a/launch_xml/setup.py b/launch_xml/setup.py index 5625bce7a..ab6d82043 100644 --- a/launch_xml/setup.py +++ b/launch_xml/setup.py @@ -27,8 +27,8 @@ license='Apache License, Version 2.0', tests_require=['pytest'], entry_points={ - 'launch_frontend.entity': [ - 'xml = launch_xml:Entity', + 'launch_frontend.parser': [ + 'xml = launch_xml:Parser', ], } ) diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml index 55a52684b..437c7a1da 100644 --- a/launch_xml/test/launch_xml/executable.xml +++ b/launch_xml/test/launch_xml/executable.xml @@ -1,5 +1,5 @@ - + diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 1d71cca60..19c8feeda 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -18,13 +18,13 @@ from launch import LaunchService -from launch_frontend import Entity, parse_description +from launch_frontend import Parser def test_executable(): """Parse node xml example.""" - root_entity = Entity.load(str(Path(__file__).parent / 'executable.xml')) - ld = parse_description(root_entity) + root_entity, parser = Parser.load(str(Path(__file__).parent / 'executable.xml')) + ld = parser.parse_description(root_entity) executable = ld.entities[0] cmd = [i[0].perform(None) for i in executable.cmd] assert(cmd == From 2e281f408e2d93d06a785678bed3a793c3cb416b Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 10 May 2019 11:40:17 -0300 Subject: [PATCH 19/75] Corrected type annotations in substitution_parse_methods Signed-off-by: ivanpauno --- .../launch_frontend/substitution_parse_methods.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/launch_frontend/launch_frontend/substitution_parse_methods.py b/launch_frontend/launch_frontend/substitution_parse_methods.py index 4c9ba8a92..162ca0302 100644 --- a/launch_frontend/launch_frontend/substitution_parse_methods.py +++ b/launch_frontend/launch_frontend/substitution_parse_methods.py @@ -14,6 +14,8 @@ """Module for launch substitution parsing methods.""" +from typing import Iterable + import launch from .expose import expose_substitution @@ -28,7 +30,7 @@ @expose_substitution('env') -def parse_env(data: launch.SomeSubstitutionsType): +def parse_env(data: Iterable[launch.SomeSubstitutionsType]): """Parse EnviromentVariable substitution.""" if not data or len(data) > 2: raise AttributeError('env substitution expects 1 or 2 arguments') @@ -40,7 +42,7 @@ def parse_env(data: launch.SomeSubstitutionsType): @expose_substitution('var') -def parse_var(data: launch.SomeSubstitutionsType): +def parse_var(data: Iterable[launch.SomeSubstitutionsType]): """Parse FindExecutable substitution.""" if not data or len(data) > 2: raise AttributeError('var substitution expects 1 or 2 arguments') @@ -52,7 +54,7 @@ def parse_var(data: launch.SomeSubstitutionsType): @expose_substitution('find-exec') -def parse_find_exec(data: launch.SomeSubstitutionsType): +def parse_find_exec(data: Iterable[launch.SomeSubstitutionsType]): """Parse FindExecutable substitution.""" if not data or len(data) > 1: raise AttributeError('find-exec substitution expects 1 argument') From 9d4bca7f32a2f9908b9aa5d8361cd97173b6d6cc Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 10 May 2019 16:40:49 -0300 Subject: [PATCH 20/75] * Updated entity abstraction to allow get typed values. * Updated xml entity with above change. * Updated action parsing methods. Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/__init__.py | 12 +- .../launch_frontend/action_parse_methods.py | 49 +++--- .../launch_frontend/convert_text_to.py | 68 --------- launch_frontend/launch_frontend/entity.py | 64 +++++++- launch_frontend/launch_frontend/type_utils.py | 144 ++++++++++++++++++ launch_xml/launch_xml/entity.py | 77 +++++++--- 6 files changed, 292 insertions(+), 122 deletions(-) delete mode 100644 launch_frontend/launch_frontend/convert_text_to.py create mode 100644 launch_frontend/launch_frontend/type_utils.py diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index 6e4c11ad1..b51137374 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -15,19 +15,25 @@ """Main entry point for the `launch_frontend` package.""" # All files containing parsing methods should be imported here. -# If not, the action or substitution isn't going to be exposed. +# If not, the action or substitution are not going to be exposed. from . import action_parse_methods # noqa: F401 from . import substitution_parse_methods # noqa: F401 +from . import type_utils from .entity import Entity from .expose import __expose_impl, expose_action, expose_substitution from .parser import Parser __all__ = [ + # Classes 'Entity', - # Implementation detail, should only be imported in test_expose_decorators. - '__expose_impl', + # Decorators 'expose_action', 'expose_substitution', 'Parser', + # Modules + 'type_utils', + + # Implementation detail, should only be imported in test_expose_decorators. + '__expose_impl', ] diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index 987a6de3c..da7e42e96 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -21,7 +21,6 @@ from launch.conditions import UnlessCondition from launch.substitutions import TextSubstitution -from .convert_text_to import str_to_bool from .entity import Entity from .expose import expose_action from .parser import Parser @@ -30,37 +29,44 @@ @expose_action('executable') def parse_executable(entity: Entity, parser: Parser): """Parse executable tag.""" - cmd = parser.parse_substitution(entity.cmd) + cmd = parser.parse_substitution(entity.get_attr('cmd')) kwargs = {} - cwd = getattr(entity, 'cwd', None) + cwd = entity.get_attr('cwd', optional=True) if cwd is not None: kwargs['cwd'] = parser.parse_substitution(cwd) - name = getattr(entity, 'name', None) + name = entity.get_attr('name', optional=True) if name is not None: kwargs['name'] = parser.parse_substitution(name) - prefix = getattr(entity, 'launch-prefix', None) + prefix = entity.get_attr('launch-prefix', optional=True) if prefix is not None: kwargs['prefix'] = parser.parse_substitution(prefix) - output = getattr(entity, 'output', None) + output = entity.get_attr('output', optional=True) if output is not None: kwargs['output'] = output - shell = getattr(entity, 'shell', None) + shell = entity.get_attr('shell', types='bool', optional=True) if shell is not None: - kwargs['shell'] = str_to_bool(shell) + kwargs['shell'] = shell # Conditions won't be allowed in the `env` tag. # If that feature is needed, `set_enviroment_variable` and # `unset_enviroment_variable` actions should be used. - env = getattr(entity, 'env', None) + env = entity.get_attr('env', types='list[Entity]', optional=True) if env is not None: - env = {e.name: parser.parse_substitution(e.value) for e in env} + # TODO(ivanpauno): Change `ExecuteProcess` api. `additional_env` + # argument is supposed to be a dictionary with `SomeSubstitutionType` + # keys, but `SomeSubstitutionType` is not always hashable. + # Proposed `additional_env` type: + # Iterable[Tuple[SomeSubstitutionType, SomeSubstitutionsType]] + env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} kwargs['additional_env'] = env - args = getattr(entity, 'args', None) + args = entity.get_attr('args', optional=True) # `args` is supposed to be a list separated with ' '. # All the found `TextSubstitution` items are split and # added to the list again as a `TextSubstitution`. - # Another option: Enforce to explicetly write a list in - # the launch file (if that's wanted) - # In xml 'args' and 'args-sep' tags should be used. + # TODO(ivanpauno): Change `ExecuteProcess` api from accepting + # `Iterable[SomeSubstitutionType]` `cmd` to `SomeSubstitutionType`. + # After performing the substitution in `cmd`, shlex.split should be done. + # This will also allow having a substitution which ends in more than one + # argument. if args is not None: args = parser.parse_substitution(args) new_args = [] @@ -75,17 +81,10 @@ def parse_executable(entity: Entity, parser: Parser): args = new_args else: args = [] - # Option 2: - # if args is not None: - # if isinstance(args, Text): - # args = [args] - # args = [parser.parse_substitution(arg) for arg in args] - # else: - # args = [] cmd_list = [cmd] cmd_list.extend(args) - if_cond = getattr(entity, 'if', None) - unless_cond = getattr(entity, 'unless', None) + if_cond = entity.get_attr('if', optional=True) + unless_cond = entity.get_attr('unless', optional=True) if if_cond is not None and unless_cond is not None: raise RuntimeError("if and unless conditions can't be usede simultaneously") if if_cond is not None: @@ -102,8 +101,8 @@ def parse_executable(entity: Entity, parser: Parser): @expose_action('let') def parse_let(entity: Entity, parser: Parser): """Parse let tag.""" - name = parser.parse_substitution(entity.var) - value = parser.parse_substitution(entity.value) + name = parser.parse_substitution(entity.get_attr('name')) + value = parser.parse_substitution(entity.get_attr('value')) return launch.actions.SetLaunchConfiguration( name, value diff --git a/launch_frontend/launch_frontend/convert_text_to.py b/launch_frontend/launch_frontend/convert_text_to.py deleted file mode 100644 index 614be2c42..000000000 --- a/launch_frontend/launch_frontend/convert_text_to.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2019 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. - -"""Module that provides methods for converting string to other data types.""" - -from typing import Iterable -from typing import Text -from typing import Union - - -def str_to_bool(string: Text): - """Convert text to python bool.""" - if string.lower() in ('0', 'false'): - return False - if string.lower() in ('1', 'true'): - return True - raise RuntimeError('Expected "true" or "false", got {}'.format(string)) - - -def guess_type_from_string(value: Union[Text, Iterable[Text]]): - """Guess the desired type of the parameter based on the string value.""" - if not isinstance(value, Text): - return [__guess_type_from_string(item) for item in value] - return __guess_type_from_string(value) - - -def __guess_type_from_string(string_value: Text): - if __is_bool(string_value): - return string_value.lower() == 'true' - if __is_integer(string_value): - return int(string_value) - if __is_float(string_value): - return float(string_value) - else: - return string_value - - -def __is_bool(string_value: Text): - if string_value.lower() in ('false', 'true'): - return True - return False - - -def __is_integer(string_value: Text): - try: - int(string_value) - except ValueError: - return False - return True - - -def __is_float(string_value: Text): - try: - float(string_value) - except ValueError: - return False - return True diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index b7453fd9b..38fbbfa3f 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -14,10 +14,11 @@ """Module for Entity class.""" -from typing import Any from typing import List from typing import Optional from typing import Text +from typing import Tuple +from typing import Union class Entity: @@ -34,10 +35,63 @@ def parent(self) -> Optional['Entity']: raise NotImplementedError() @property - def children(self) -> Optional[List['Entity']]: - """Get Entity children.""" + def children(self) -> List['Entity']: + """Get the Entity's children.""" raise NotImplementedError() - def __getattr__(self, name: Text) -> Optional[Any]: - """Get attribute.""" + def get_attr( + self, + name: Text, + *, + types: Union[Text, Tuple[Text]] = 'str', + optional: bool = False + ) -> Optional[Union[ + Text, + int, + float, + List[Text], + List[int], + List[float], + List['Entity'] + ]]: + """ + Access an element in the tree. + + By default, it will try to return it as an string. + `types` is used in the following way: + - For frontends that natively recoginize data types (like yaml), + it will check if the attribute read match with one in `types`. + If it is one of them, the value is returned. + If not, an `TypeError` is raised. + - For frontends that don't natively recognize data types (like xml), + it will try to convert the value read to one of the specified `types`. + The first convertion that success is returned. + If no conversion success, a `TypeError` is raised. + + The allowed types are: + - 'str' + - 'int' + - 'float' + - 'bool' + - 'list[str]' + - 'list[int]' + - 'list[float]' + - 'list[bool]' + + Types that can not be combined with the others: + - 'guess' + - 'list[Entity]' + + 'guess' work in the same way as: + ('float', 'int', 'list[float]', 'list[int]', 'list[str]', 'str'). + 'list[Entity]' will return a list of more enties. + + See the frontend documentation to see how 'list' and 'list[Entity]' look like. + + If `optional` argument is `True`, will return `None` instead of raising `AttributeError`. + + Possible errors: + - `AttributeError`: Attribute not found. Only possible if `optional` is `False`. + - `TypeError`: Attribute found but it is not of the correct type. + """ raise NotImplementedError() diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py new file mode 100644 index 000000000..6ae91292b --- /dev/null +++ b/launch_frontend/launch_frontend/type_utils.py @@ -0,0 +1,144 @@ +# Copyright 2019 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. + +"""Module which implements get_typed_value function.""" + +from typing import Any +from typing import Text +from typing import Tuple +from typing import Union + + +def extract_type(name: Text): + """ + Extract type information from string. + + `name` can be one of: + - 'str' + - 'int' + - 'float' + - 'bool' + - 'list[str]' + - 'list[int]' + - 'list[float]' + - 'list[bool]' + + Returns a tuple (type_obj, is_list). + is_list is `True` for the supported list types, if not is `False`. + type_obj is the object representing that type in python. In the case of list + is the type of the items. + e.g.: + name = 'list[int]' -> (int, True) + name = 'bool' -> (bool, False) + """ + error = ValueError('Unrecognized type name: {}'.format(name)) + is_list = False + type_name = name + if 'list[' in name: + is_list = True + type_name = name[5:-1] + if name[-1] != ']': + raise error + if type_name not in ('int', 'float', 'str', 'bool'): + raise error + return (eval(type_name), is_list) + + +def check_type(value: Any, types: Union[Text, Tuple[Text]]) -> bool: + """ + Check if `value` is one of the types in `types`. + + The allowed types are: + - 'str' + - 'int' + - 'float' + - 'bool' + - 'list[str]' + - 'list[int]' + - 'list[float]' + - 'list[bool]' + """ + if isinstance(types, Text): + types = [types] + for x in types: + type_obj, is_list = extract_type(x) + if is_list: + if not isinstance(value, list) or not value: + continue + if isinstance(value[0], type_obj): + return True + else: + if isinstance(value, type_obj): + return True + return False + + +def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: + """ + Try to convert `value` to one of the types specified in `types`. + + It returns the first successful conversion. + If not raise `AttributeError`. + + The allowed types are: + - 'str' + - 'int' + - 'float' + - 'bool' + - 'list[str]' + - 'list[int]' + - 'list[float]' + - 'list[bool]' + + types = 'guess' works in the same way as: + ('float', 'int', 'list[float]', 'list[int]', 'list[str]', 'str') + """ + if types == 'guess': + types = ( + 'float', 'int', 'bool', 'list[float]', 'list[int]', + 'list[bool]' 'list[str]', 'str' + ) + if isinstance(types, Text): + types = [types] + typed_value = None + for x in types: + type_obj, is_list = extract_type(x) + if type_obj is bool: + def type_obj(x): + if x.lower() in ('true', 'false'): + return x.lower() == 'true' + raise ValueError() + if is_list: + if not isinstance(value, list): + continue + try: + typed_value = map(type_obj, value) + except ValueError: + pass + else: + break + else: + try: + typed_value = type_obj(value) + except ValueError: + pass + else: + break + if typed_value is None: + raise ValueError( + 'Can not convert value {} to one of the types in {}'.format( + value, types + ) + ) + return typed_value diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index fc8a38ed0..b305463c6 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -14,20 +14,26 @@ """Module for Entity class.""" +from typing import List from typing import Optional from typing import Text +from typing import Tuple +from typing import Union import xml.etree.ElementTree as ET -import launch_frontend +from launch_frontend import Entity as BaseEntity +from launch_frontend.type_utils import get_typed_value -class Entity(launch_frontend.Entity): +class Entity(BaseEntity): """Single item in the intermediate XML front_end representation.""" - def __init__(self, - xml_element: ET.Element = None, - *, - parent: 'Entity' = None) -> Text: + def __init__( + self, + xml_element: ET.Element = None, + *, + parent: 'Entity' = None + ) -> Text: """Construnctor.""" self.__xml_element = xml_element self.__parent = parent @@ -43,24 +49,53 @@ def parent(self) -> Optional['Entity']: return self.__parent @property - def children(self): - """Get Entity children.""" - return [Entity(child) for child in self.__xml_element] + def children(self) -> List['Entity']: + """Get the Entity's children.""" + return map(Entity, self.__xml_element) - def __getattr__(self, name): - """Abstraction of how to access the xml tree.""" + def get_attr( + self, + name: Text, + *, + types: Union[Text, Tuple[Text]] = 'str', + optional: bool = False + ) -> Optional[Union[ + Text, + int, + float, + List[Text], + List[int], + List[float], + List['Entity'] + ]]: + """Access an attribute of the entity.""" + if types == 'list[Entity]': + return_list = filter(lambda x: x.tag == name, self.__xml_element) + return map(Entity, return_list) + value = None if name in self.__xml_element.attrib: name_sep = name + '-sep' if name_sep not in self.__xml_element.attrib: - return self.__xml_element.attrib[name] + value = self.__xml_element.attrib[name] else: sep = self.__xml_element.attrib[name_sep] - return self.__xml_element.attrib[name].split(sep) - return_list = filter(lambda x: x.tag == name, - self.__xml_element) - return_list = [Entity(item) for item in return_list] - if not return_list: - raise AttributeError( - 'Can not find attribute {} in Entity {}'.format( - name, self.type_name)) - return return_list + value = self.__xml_element.attrib[name].split(sep) + if value is None: + if not optional: + raise AttributeError( + 'Attribute {} of type {} not found in Entity {}'.format( + name, types, self.type_name + ) + ) + else: + return None + try: + value = get_typed_value(value, types) + except ValueError: + raise TypeError( + 'Attribute {} of Entity {} expected to be of type {}.' + '`{}` can not be converted to one of those types'.format( + name, self.type_name, types, value + ) + ) + return value From bf2ad686cc725b1143480cb193e74e08fbdd30d7 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 10 May 2019 16:42:00 -0300 Subject: [PATCH 21/75] Added yaml launch frontend implementation Signed-off-by: ivanpauno --- launch_yaml/launch_yaml/__init__.py | 23 ++++ launch_yaml/launch_yaml/entity.py | 103 ++++++++++++++++++ launch_yaml/launch_yaml/parser.py | 39 +++++++ launch_yaml/package.xml | 21 ++++ launch_yaml/setup.py | 34 ++++++ launch_yaml/test/launch_yaml/executable.yaml | 12 ++ .../test/launch_yaml/test_executable.py | 48 ++++++++ launch_yaml/test/test_copyright.py | 23 ++++ launch_yaml/test/test_flake8.py | 23 ++++ launch_yaml/test/test_pep257.py | 23 ++++ 10 files changed, 349 insertions(+) create mode 100644 launch_yaml/launch_yaml/__init__.py create mode 100644 launch_yaml/launch_yaml/entity.py create mode 100644 launch_yaml/launch_yaml/parser.py create mode 100644 launch_yaml/package.xml create mode 100644 launch_yaml/setup.py create mode 100644 launch_yaml/test/launch_yaml/executable.yaml create mode 100644 launch_yaml/test/launch_yaml/test_executable.py create mode 100644 launch_yaml/test/test_copyright.py create mode 100644 launch_yaml/test/test_flake8.py create mode 100644 launch_yaml/test/test_pep257.py diff --git a/launch_yaml/launch_yaml/__init__.py b/launch_yaml/launch_yaml/__init__.py new file mode 100644 index 000000000..f3d901609 --- /dev/null +++ b/launch_yaml/launch_yaml/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2019 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. + +"""Main entry point for the `launch_xml` package.""" + +from .entity import Entity +from .parser import Parser + +__all__ = [ + 'Entity', + 'Parser', +] diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py new file mode 100644 index 000000000..34458008c --- /dev/null +++ b/launch_yaml/launch_yaml/entity.py @@ -0,0 +1,103 @@ +# Copyright 2019 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. + +"""Module for YAML Entity class.""" + +from typing import List +from typing import Optional +from typing import Text +from typing import Tuple +from typing import Union + +from launch_frontend import Entity as BaseEntity +from launch_frontend.type_utils import check_type + + +class Entity(BaseEntity): + """Single item in the intermediate YAML front_end representation.""" + + def __init__( + self, + element: dict, + *, + type_name: Optional[Text] = None, + parent: 'Entity' = None + ) -> Text: + """Construnctor.""" + if type_name is None: + if len(element) != 1: + raise RuntimeError('Expected a len 1 dictionary') + self.__type_name = list(element.keys())[0] + self.__element = element[self.__type_name] + else: + self.__type_name = type_name + self.__element = element + self.__parent = parent + + @property + def type_name(self) -> Text: + """Get Entity type.""" + return self.__type_name + + @property + def parent(self) -> Optional['Entity']: + """Get Entity parent.""" + return self.__parent + + @property + def children(self) -> List['Entity']: + """Get the Entity's children.""" + if not isinstance(self.__element, list): + raise TypeError('Expected a list, got {}'.format(type(self.element))) + return [Entity(child) for child in self.__element] + + def get_attr( + self, + name: Text, + *, + types: Union[Text, Tuple[Text]] = 'str', + optional: bool = False + ) -> Optional[Union[ + Text, + int, + float, + List[Text], + List[int], + List[float], + List['Entity'] + ]]: + """Access an attribute of the entity.""" + if name not in self.__element: + if not optional: + raise AttributeError( + 'Can not find attribute {} in Entity {}'.format( + name, self.type_name)) + else: + return None + data = self.__element[name] + if types == 'list[Entity]': + if isinstance(data, list) and isinstance(data[0], dict): + return [Entity(child, type_name=name) for child in data] + raise TypeError( + 'Attribute {} of Entity {} expected to be a list of dictionaries.'.format( + name, self.type_name + ) + ) + if not check_type(data, types): + raise TypeError( + 'Attribute {} of Entity {} expected to be of type {}, got {}'.format( + name, self.type_name, types, type(data) + ) + ) + return data diff --git a/launch_yaml/launch_yaml/parser.py b/launch_yaml/launch_yaml/parser.py new file mode 100644 index 000000000..b98e2315a --- /dev/null +++ b/launch_yaml/launch_yaml/parser.py @@ -0,0 +1,39 @@ +# Copyright 2019 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. + +"""Module for YAML Parser class.""" + +import io +from typing import Union + +import launch_frontend + +import yaml + +from .entity import Entity + + +class Parser(launch_frontend.Parser): + """YAML parser implementation.""" + + @classmethod + def load( + cls, + stream: Union[str, io.TextIOBase], + ) -> (Entity, 'Parser'): + """Load a YAML launch file.""" + if isinstance(stream, str): + stream = open(stream, 'r') + """Return entity loaded with markup file.""" + return (Entity(yaml.safe_load(stream)), cls) diff --git a/launch_yaml/package.xml b/launch_yaml/package.xml new file mode 100644 index 000000000..e0d5246d0 --- /dev/null +++ b/launch_yaml/package.xml @@ -0,0 +1,21 @@ + + + + launch_yaml + 0.7.3 + The ROS launch YAML frontend. + Ivan Paunovic + Apache License 2.0 + + launch + launch_frontend + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/launch_yaml/setup.py b/launch_yaml/setup.py new file mode 100644 index 000000000..9cd706167 --- /dev/null +++ b/launch_yaml/setup.py @@ -0,0 +1,34 @@ +from setuptools import find_packages +from setuptools import setup + +setup( + name='launch_yaml', + version='0.7.3', + packages=find_packages(exclude=['test']), + install_requires=['setuptools'], + zip_safe=True, + author='Ivan Paunovic', + author_email='ivanpauno@ekumenlabs.com', + maintainer='Ivan Paunovic', + maintainer_email='ivanpauno@ekumenlabs.com', + url='https://github.com/ros2/launch', + download_url='https://github.com/ros2/launch/releases', + keywords=['ROS'], + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Topic :: Software Development', + ], + description='YAML `launch` front-end extension.', + long_description=( + 'This package provides YAML parsing ability to `launch-frontend` package.' + ), + license='Apache License, Version 2.0', + tests_require=['pytest'], + entry_points={ + 'launch_frontend.parser': [ + 'yaml = launch_yaml:Parser', + ], + } +) diff --git a/launch_yaml/test/launch_yaml/executable.yaml b/launch_yaml/test/launch_yaml/executable.yaml new file mode 100644 index 000000000..8e205d94b --- /dev/null +++ b/launch_yaml/test/launch_yaml/executable.yaml @@ -0,0 +1,12 @@ +launch: + - executable: + cmd: ls + cwd: '/' + name: my_ls + args: -l -a -s + shell: true + output: log + launch_prefix: $(env LAUNCH_PREFIX) + env: + - name: var + value: '1' diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py new file mode 100644 index 000000000..7898c01cb --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -0,0 +1,48 @@ +# Copyright 2019 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. + +"""Test parsing an executable action.""" + +from pathlib import Path + +from launch import LaunchService + +from launch_frontend import Parser + + +def test_executable(): + """Parse executable yaml example.""" + root_entity, parser = Parser.load(str(Path(__file__).parent / 'executable.yaml')) + ld = parser.parse_description(root_entity) + executable = ld.entities[0] + cmd = [i[0].perform(None) for i in executable.cmd] + assert( + cmd == ['ls', '-l', '-a', '-s']) + assert(executable.cwd[0].perform(None) == '/') + assert(executable.name[0].perform(None) == 'my_ls') + assert(executable.shell is True) + assert(executable.output == 'log') + key, value = executable.additional_env[0] + key = key[0].perform(None) + value = value[0].perform(None) + assert(key == 'var') + assert(value == '1') + # assert(executable.prefix[0].perform(None) == 'time') + ls = LaunchService() + ls.include_launch_description(ld) + assert(0 == ls.run()) + + +if __name__ == '__main__': + test_executable() diff --git a/launch_yaml/test/test_copyright.py b/launch_yaml/test/test_copyright.py new file mode 100644 index 000000000..cf0fae31f --- /dev/null +++ b/launch_yaml/test/test_copyright.py @@ -0,0 +1,23 @@ +# Copyright 2017 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 ament_copyright.main import main +import pytest + + +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/launch_yaml/test/test_flake8.py b/launch_yaml/test/test_flake8.py new file mode 100644 index 000000000..eff829969 --- /dev/null +++ b/launch_yaml/test/test_flake8.py @@ -0,0 +1,23 @@ +# Copyright 2017 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 ament_flake8.main import main +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc = main(argv=[]) + assert rc == 0, 'Found errors' diff --git a/launch_yaml/test/test_pep257.py b/launch_yaml/test/test_pep257.py new file mode 100644 index 000000000..3aeb4d348 --- /dev/null +++ b/launch_yaml/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 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 ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=[]) + assert rc == 0, 'Found code style errors / warnings' From e5a0645ca08402fd945c2c22c23f7c74889edc26 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 10 May 2019 17:45:49 -0300 Subject: [PATCH 22/75] * Replaced map usage with list comprehensions. * Added not found attribute checking for type 'list[Entity]'. Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/type_utils.py | 2 +- launch_xml/launch_xml/entity.py | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index 6ae91292b..18b1b4c5e 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -123,7 +123,7 @@ def type_obj(x): if not isinstance(value, list): continue try: - typed_value = map(type_obj, value) + typed_value = [type_obj(x) for x in value] except ValueError: pass else: diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index b305463c6..d807d06f7 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -51,7 +51,7 @@ def parent(self) -> Optional['Entity']: @property def children(self) -> List['Entity']: """Get the Entity's children.""" - return map(Entity, self.__xml_element) + return [Entity(item) for item in self.__xml_element] def get_attr( self, @@ -69,9 +69,19 @@ def get_attr( List['Entity'] ]]: """Access an attribute of the entity.""" + attr_error = AttributeError( + 'Attribute {} of type {} not found in Entity {}'.format( + name, types, self.type_name + ) + ) if types == 'list[Entity]': return_list = filter(lambda x: x.tag == name, self.__xml_element) - return map(Entity, return_list) + if not return_list: + if optional: + return None + else: + raise attr_error + return [Entity(item) for item in return_list] value = None if name in self.__xml_element.attrib: name_sep = name + '-sep' @@ -82,11 +92,7 @@ def get_attr( value = self.__xml_element.attrib[name].split(sep) if value is None: if not optional: - raise AttributeError( - 'Attribute {} of type {} not found in Entity {}'.format( - name, types, self.type_name - ) - ) + raise attr_error else: return None try: From 43c77f5b31b1a93eced81fa43b5fcf093344f047 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 10 May 2019 17:47:45 -0300 Subject: [PATCH 23/75] Corrected xml test_list.py Signed-off-by: ivanpauno --- launch_xml/test/launch_xml/list.xml | 6 +++--- launch_xml/test/launch_xml/test_list.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/launch_xml/test/launch_xml/list.xml b/launch_xml/test/launch_xml/list.xml index 6a950a0a6..fc38f7aea 100644 --- a/launch_xml/test/launch_xml/list.xml +++ b/launch_xml/test/launch_xml/list.xml @@ -1,5 +1,5 @@ - - - + + + diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py index 88dc27a31..c2f90dd33 100644 --- a/launch_xml/test/launch_xml/test_list.py +++ b/launch_xml/test/launch_xml/test_list.py @@ -25,9 +25,10 @@ def test_list(): tree = ET.parse(str(Path(__file__).parent / 'list.xml')) root = tree.getroot() root_entity = Entity(root) - assert root_entity.tag1[0].attr == ['1', '2', '3'] - assert root_entity.tag2[0].attr == ['1', '2', '3'] - assert root_entity.tag3[0].attr == ['1', '2', '3'] + tags = root_entity.children + assert tags[0].get_attr('attr', types='list[str]') == ['1', '2', '3'] + assert tags[0].get_attr('attr', types='list[int]') == [1, 2, 3] + assert tags[0].get_attr('attr', types='list[float]') == [1., 2., 3.] if __name__ == '__main__': From 65871e957779cf744b7b89c22a826ef372f6d0ad Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 10 May 2019 17:48:06 -0300 Subject: [PATCH 24/75] Updated launch xml readme Signed-off-by: ivanpauno --- launch_xml/README.md | 59 +++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/launch_xml/README.md b/launch_xml/README.md index fadb41389..8b6f8dbbf 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -12,14 +12,26 @@ When having an xml tag like: ``` -If the entity `e` is wrapping it, the following two statements will be true: +If the entity `e` is wrapping it, the following statements will be true: ```python -hasattr(e, 'attr') == True -e.attr == '2' +e.get_attr('attr') == '2' +e.get_attr('attr', types='int') == 2 +e.get_attr('attr', types='float') == 2.0 +``` + +By default, the value of the attribute is returned as a string. +Allowed types are: +```python +'str', 'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]' +``` +A combination of them can be specified with a tuple. e.g.: `('int', 'str')`. +In that case, conversions are tried in order and the first successful conversion is returned. +`types` can also be set to `guess`, which works in the same way as passing: + +```python +'float', 'int', 'bool', 'list[float]', 'list[int]', 'list[bool]', 'list[str]', 'str' ``` -As a general rule, the value of the attribute is returned as a string. -Conversion to `float` or `int` should be explicitly done in the parser method. For handling lists, the `*-sep` attribute is used. e.g.: ```xml @@ -29,11 +41,21 @@ For handling lists, the `*-sep` attribute is used. e.g.: ``` ```python -e.tag.attr == [2, 3, 4] -e.tag2.attr == [2, 3, 4] -e.tag3.attr == [2, 3, 4] +tag.get_attr('attr', types='list[int]') == [2, 3, 4] +tag2.get_attr('attr', types='list[float]') == [2.0, 3.0, 4.0] +tag3.get_attr('attr', types='list[str]') == ['2', '3', '4'] +``` + +For checking if an attribute exists, use optional argument: + +```python +attr = e.get_attr('attr', optional=True) +if attr is not None: + do_something(attr) ``` +With `optional=False` (default), `AttributeError` is raised if it is not found. + ### Accessing XML children as attributes: In this xml: @@ -48,14 +70,15 @@ In this xml: The `env` children could be accessed like: ```python -len(e.env) == 2 -e.env[0].name == 'a' -e.env[0].value == '100' -e.env[1].name == 'b' -e.env[1].value == 'stuff' +env = e.get_attr('env', types='list[Entity]') +len(env) == 2 +env[0].get_attr('name') == 'a' +env[0].get_attr('value') == '100' +env[1].get_attr('name') == 'b' +env[1].get_attr('value') == 'stuff' ``` -In these cases, `e.env` is a list of entities, which could be accessed in the same abstract way. +In these cases, `e.env` is a list of entities, that can be accessed in the same abstract way. ### Accessing all the XML children: @@ -67,14 +90,6 @@ e.children It returns a list of launch_xml.Entity wrapping each of the xml children. -### Attribute lookup order - -The attributes are check in the following order: - -- Is tried to be accessed like a XML attribute. -- Is tried to be accessed like XML children. -- `AttributeError` is raised. - ## Built-in substitutions See [this](https://github.com/ros2/design/blob/d3a35d7ea201721892993e85e28a5a223cdaa001/articles/151_roslaunch_xml.md) document. From bc2f03c2b7a7b8c622ddc3ec9340d903d7b7f320 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 13 May 2019 12:12:05 -0300 Subject: [PATCH 25/75] Added readme to launch_yaml Signed-off-by: ivanpauno --- launch_yaml/README.md | 103 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 launch_yaml/README.md diff --git a/launch_yaml/README.md b/launch_yaml/README.md new file mode 100644 index 000000000..5dcb9a26c --- /dev/null +++ b/launch_yaml/README.md @@ -0,0 +1,103 @@ +# launch_yaml + +This package provides an abstraction of the YAML tree. + +## YAML front-end mapping rules + +### Accessing yaml attributes + +When having an YAML file like: + +```yaml +tag: + attr1: '2' + attr2: 2 + attr3: 2.0 +``` + +If the entity `e` is wrapping `tag`, the following statement will be true: +```python +e.get_attr('attr1') == '2' +e.get_attr('attr2', types='int') == 2 +e.get_attr('attr3', types='float') == 2.0 +``` + +By default, `get_attr` returns an string and it does type checking. The following code will raise a `TypeError`: + +```python +e.get_attr('attr1', types='int') +e.get_attr('attr2', types='float') +e.get_attr('attr3') +``` + +Allowed types are: +```python +'str', 'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]' +``` +A combination of them can be specified with a tuple. e.g.: `('int', 'str')`. +In that case, conversions are tried in order and the first successful conversion is returned. +`types` can also be set to `guess`, which works in the same way as passing: + +```python +'float', 'int', 'bool', 'list[float]', 'list[int]', 'list[bool]', 'list[str]', 'str' +``` + +For checking if an attribute exists, use optional argument: + +```python +attr = e.get_attr('attr', optional=True) +if attr is not None: + do_something(attr) +``` + +With `optional=False` (default), `AttributeError` is raised if it is not found. + +### Accessing attributes that are also an Entity: + +In this yaml: + +```yaml +executable: + cmd: ls + env: + - name: a + - value: '100' + - name: b + - value: 'stuff' +``` + +The `env` children could be accessed doing: + +```python +env = e.get_attr('env', types='list[Entity]') +len(env) == 2 +env[0].get_attr('name') == 'a' +env[0].get_attr('value') == '100' +env[1].get_attr('name') == 'b' +env[1].get_attr('value') == 'stuff' +``` + +In these cases, `e.env` is a list of entities, that can be accessed in the same abstract way. + +### Accessing children: + +All the children can be directly accessed. e.g.: + +```yaml +group: + - executable: + cmd: ls + - executable: + cmd: ps +``` + +```python +e.children +``` + +It returns a list of launch_xml.Entity wrapping each of the xml children. +In the example, the list has two `Entity` objects wrapping each of the `executable` tags. + +## Built-in substitutions + +See [this](https://github.com/ros2/design/blob/d3a35d7ea201721892993e85e28a5a223cdaa001/articles/151_roslaunch_xml.md) document. From 70891217e7a5318e1a03ec411ec27138e73ce080 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 13 May 2019 18:00:55 -0300 Subject: [PATCH 26/75] Add test for let action and var substitution Signed-off-by: ivanpauno --- launch_xml/test/launch_xml/let_var.xml | 4 +++ launch_xml/test/launch_xml/test_let_var.py | 37 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 launch_xml/test/launch_xml/let_var.xml create mode 100644 launch_xml/test/launch_xml/test_let_var.py diff --git a/launch_xml/test/launch_xml/let_var.xml b/launch_xml/test/launch_xml/let_var.xml new file mode 100644 index 000000000..d587e98a8 --- /dev/null +++ b/launch_xml/test/launch_xml/let_var.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/launch_xml/test/launch_xml/test_let_var.py b/launch_xml/test/launch_xml/test_let_var.py new file mode 100644 index 000000000..d1e181d2f --- /dev/null +++ b/launch_xml/test/launch_xml/test_let_var.py @@ -0,0 +1,37 @@ +# Copyright 2019 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. + +"""Test parsing an executable action.""" + +from pathlib import Path + +from launch import LaunchContext + +from launch_frontend import Parser + + +def test_let_var(): + """Parse node xml example.""" + root_entity, parser = Parser.load(str(Path(__file__).parent / 'let_var.xml')) + ld = parser.parse_description(root_entity) + context = LaunchContext() + assert len(ld.entities) == 2 + ld.entities[0].execute(context) + ld.entities[1].execute(context) + assert context.launch_configurations['var1'] == 'asd' + assert context.launch_configurations['var2'] == '2 asd' + + +if __name__ == '__main__': + test_let_var() From 3e1f9e2e11e92cdd0f8ade4c658f68b6abec559b Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 14 May 2019 12:26:07 -0300 Subject: [PATCH 27/75] Correct bugs in type_utils. Updated related doc. Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 2 +- launch_frontend/launch_frontend/type_utils.py | 23 ++++++++++++++----- launch_xml/README.md | 2 +- launch_yaml/README.md | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index 38fbbfa3f..12e422d19 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -83,7 +83,7 @@ def get_attr( - 'list[Entity]' 'guess' work in the same way as: - ('float', 'int', 'list[float]', 'list[int]', 'list[str]', 'str'). + ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') 'list[Entity]' will return a list of more enties. See the frontend documentation to see how 'list' and 'list[Entity]' look like. diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index 18b1b4c5e..564dbfb07 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -20,6 +20,12 @@ from typing import Union +types_guess__ = ( + 'int', 'float', 'bool', 'list[int]', 'list[float]', + 'list[bool]', 'list[str]', 'str' +) + + def extract_type(name: Text): """ Extract type information from string. @@ -68,7 +74,12 @@ def check_type(value: Any, types: Union[Text, Tuple[Text]]) -> bool: - 'list[int]' - 'list[float]' - 'list[bool]' + + types = 'guess' works in the same way as: + ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') """ + if types == 'guess': + types = types_guess__ if isinstance(types, Text): types = [types] for x in types: @@ -102,15 +113,13 @@ def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: - 'list[bool]' types = 'guess' works in the same way as: - ('float', 'int', 'list[float]', 'list[int]', 'list[str]', 'str') + ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') """ if types == 'guess': - types = ( - 'float', 'int', 'bool', 'list[float]', 'list[int]', - 'list[bool]' 'list[str]', 'str' - ) - if isinstance(types, Text): + types = types_guess__ + elif isinstance(types, Text): types = [types] + typed_value = None for x in types: type_obj, is_list = extract_type(x) @@ -129,6 +138,8 @@ def type_obj(x): else: break else: + if isinstance(value, list): + continue try: typed_value = type_obj(value) except ValueError: diff --git a/launch_xml/README.md b/launch_xml/README.md index 8b6f8dbbf..6288e5819 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -29,7 +29,7 @@ In that case, conversions are tried in order and the first successful conversion `types` can also be set to `guess`, which works in the same way as passing: ```python -'float', 'int', 'bool', 'list[float]', 'list[int]', 'list[bool]', 'list[str]', 'str' +'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str' ``` For handling lists, the `*-sep` attribute is used. e.g.: diff --git a/launch_yaml/README.md b/launch_yaml/README.md index 5dcb9a26c..6ba50e3ff 100644 --- a/launch_yaml/README.md +++ b/launch_yaml/README.md @@ -39,7 +39,7 @@ In that case, conversions are tried in order and the first successful conversion `types` can also be set to `guess`, which works in the same way as passing: ```python -'float', 'int', 'bool', 'list[float]', 'list[int]', 'list[bool]', 'list[str]', 'str' +'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str' ``` For checking if an attribute exists, use optional argument: From 0ba7e9df292fb0ab289a7dae77d4224393025724 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 14:07:53 -0300 Subject: [PATCH 28/75] Address PR comments Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/__init__.py | 5 +--- .../launch_frontend/action_parse_methods.py | 8 ------ launch_frontend/launch_frontend/entity.py | 2 +- launch_frontend/launch_frontend/expose.py | 2 +- launch_frontend/launch_frontend/parser.py | 4 +-- .../substitution_parse_methods.py | 8 ------ launch_frontend/launch_frontend/type_utils.py | 4 +-- launch_frontend/package.xml | 2 +- .../launch_frontend/test_expose_decorators.py | 2 +- launch_xml/README.md | 26 +++++++++---------- launch_xml/package.xml | 2 +- launch_xml/test/launch_xml/let_var.xml | 2 +- launch_yaml/README.md | 24 ++++++++--------- launch_yaml/launch_yaml/entity.py | 2 +- launch_yaml/package.xml | 2 +- 15 files changed, 38 insertions(+), 57 deletions(-) diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index b51137374..814d60518 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -20,7 +20,7 @@ from . import substitution_parse_methods # noqa: F401 from . import type_utils from .entity import Entity -from .expose import __expose_impl, expose_action, expose_substitution +from .expose import expose_action, expose_substitution from .parser import Parser @@ -33,7 +33,4 @@ 'Parser', # Modules 'type_utils', - - # Implementation detail, should only be imported in test_expose_decorators. - '__expose_impl', ] diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index da7e42e96..c2e5a3b40 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -107,11 +107,3 @@ def parse_let(entity: Entity, parser: Parser): name, value ) - -# def parse_include(entity: Entity): -# """Parse a launch file to be included.""" -# # TODO(ivanpauno): Should be allow to include a programmatic launch file? How? -# # TODO(ivanpauno): Create launch_ros.actions.IncludeAction, supporting namespacing. -# # TODO(ivanpauno): Handle if and unless conditions. -# loaded_entity = Entity.load(entity.file, entity.parent) -# parse_description(loaded_entity) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index 12e422d19..d1c88d6ea 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -84,7 +84,7 @@ def get_attr( 'guess' work in the same way as: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') - 'list[Entity]' will return a list of more enties. + 'list[Entity]' will return a list of more entities. See the frontend documentation to see how 'list' and 'list[Entity]' look like. diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index eb387a479..55e9d8191 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -28,7 +28,7 @@ def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): The returned decorator will check the following things in order: - If it is decorating a class, it will look for a method called `parse` and store it as the parsing method. The `parse` method is supposed to be static. If the class - don't have a `parse` method, it will raise a `RuntimeError`. + doesn't have a `parse` method, it will raise a `RuntimeError`. - If it is decorating a callable, it will store it as the parsing method. - If not, it will raise a `RuntimeError`. diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 27db1324e..b574cbcd1 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -93,8 +93,8 @@ def load( """Return an entity loaded with a markup file.""" cls.load_parser_implementations() if is_a(file, str): - # This automatically recognizes 'file.xml' or 'file.launch.xml' - # as a launch file using the xml frontend. + # This automatically recognizes the launch frontend markup + # from the extension. frontend_name = file.rsplit('.', 1)[1] if frontend_name in cls.frontend_parsers: return cls.frontend_parsers[frontend_name].load(file) diff --git a/launch_frontend/launch_frontend/substitution_parse_methods.py b/launch_frontend/launch_frontend/substitution_parse_methods.py index 162ca0302..abd827e50 100644 --- a/launch_frontend/launch_frontend/substitution_parse_methods.py +++ b/launch_frontend/launch_frontend/substitution_parse_methods.py @@ -21,14 +21,6 @@ from .expose import expose_substitution -# @expose_substitution('test') -# def test(data): -# """Delete me please.""" -# if len(data) > 1: -# raise AttributeError('Expected a len 1 list') -# return data[0] - - @expose_substitution('env') def parse_env(data: Iterable[launch.SomeSubstitutionsType]): """Parse EnviromentVariable substitution.""" diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index 564dbfb07..4431a72b5 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -125,8 +125,8 @@ def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: type_obj, is_list = extract_type(x) if type_obj is bool: def type_obj(x): - if x.lower() in ('true', 'false'): - return x.lower() == 'true' + if x.lower() in ('true', 'yes', 'on', '1', 'false', 'no', 'off', '0'): + return x.lower() in ('true', 'yes', 'on', '1') raise ValueError() if is_list: if not isinstance(value, list): diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml index 75bd88cea..033b41582 100644 --- a/launch_frontend/package.xml +++ b/launch_frontend/package.xml @@ -2,7 +2,7 @@ launch_frontend - 0.7.3 + 0.8.1 The ROS launch frontend. Ivan Paunovic Apache License 2.0 diff --git a/launch_frontend/test/launch_frontend/test_expose_decorators.py b/launch_frontend/test/launch_frontend/test_expose_decorators.py index 40364d59c..139c24a78 100644 --- a/launch_frontend/test/launch_frontend/test_expose_decorators.py +++ b/launch_frontend/test/launch_frontend/test_expose_decorators.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from launch_frontend import __expose_impl +from launch_frontend.expose import __expose_impl import pytest diff --git a/launch_xml/README.md b/launch_xml/README.md index 6288e5819..bf931f9b5 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -9,14 +9,14 @@ This package provides an abstraction of the XML tree. When having an xml tag like: ```xml - + ``` If the entity `e` is wrapping it, the following statements will be true: ```python -e.get_attr('attr') == '2' -e.get_attr('attr', types='int') == 2 -e.get_attr('attr', types='float') == 2.0 +e.get_attr('value') == '2' +e.get_attr('value', types='int') == 2 +e.get_attr('value', types='float') == 2.0 ``` By default, the value of the attribute is returned as a string. @@ -35,23 +35,23 @@ In that case, conversions are tried in order and the first successful conversion For handling lists, the `*-sep` attribute is used. e.g.: ```xml - - - + + + ``` ```python -tag.get_attr('attr', types='list[int]') == [2, 3, 4] -tag2.get_attr('attr', types='list[float]') == [2.0, 3.0, 4.0] -tag3.get_attr('attr', types='list[str]') == ['2', '3', '4'] +tag.get_attr('value', types='list[int]') == [2, 3, 4] +tag2.get_attr('value', types='list[float]') == [2.0, 3.0, 4.0] +tag3.get_attr('value', types='list[str]') == ['2', '3', '4'] ``` For checking if an attribute exists, use optional argument: ```python -attr = e.get_attr('attr', optional=True) -if attr is not None: - do_something(attr) +value = e.get_attr('value', optional=True) +if value is not None: + do_something(value) ``` With `optional=False` (default), `AttributeError` is raised if it is not found. diff --git a/launch_xml/package.xml b/launch_xml/package.xml index 7e8d6b695..ec054971f 100644 --- a/launch_xml/package.xml +++ b/launch_xml/package.xml @@ -2,7 +2,7 @@ launch_xml - 0.7.3 + 0.8.1 The ROS launch XML frontend. Ivan Paunovic Apache License 2.0 diff --git a/launch_xml/test/launch_xml/let_var.xml b/launch_xml/test/launch_xml/let_var.xml index d587e98a8..2a2390a18 100644 --- a/launch_xml/test/launch_xml/let_var.xml +++ b/launch_xml/test/launch_xml/let_var.xml @@ -1,4 +1,4 @@ - \ No newline at end of file + diff --git a/launch_yaml/README.md b/launch_yaml/README.md index 6ba50e3ff..2d5c984cc 100644 --- a/launch_yaml/README.md +++ b/launch_yaml/README.md @@ -10,24 +10,24 @@ When having an YAML file like: ```yaml tag: - attr1: '2' - attr2: 2 - attr3: 2.0 + value1: '2' + value2: 2 + value3: 2.0 ``` If the entity `e` is wrapping `tag`, the following statement will be true: ```python -e.get_attr('attr1') == '2' -e.get_attr('attr2', types='int') == 2 -e.get_attr('attr3', types='float') == 2.0 +e.get_attr('value1') == '2' +e.get_attr('value2', types='int') == 2 +e.get_attr('value3', types='float') == 2.0 ``` By default, `get_attr` returns an string and it does type checking. The following code will raise a `TypeError`: ```python -e.get_attr('attr1', types='int') -e.get_attr('attr2', types='float') -e.get_attr('attr3') +e.get_attr('value1', types='int') +e.get_attr('value2', types='float') +e.get_attr('value3') ``` Allowed types are: @@ -45,9 +45,9 @@ In that case, conversions are tried in order and the first successful conversion For checking if an attribute exists, use optional argument: ```python -attr = e.get_attr('attr', optional=True) -if attr is not None: - do_something(attr) +value = e.get_attr('value', optional=True) +if value is not None: + do_something(value) ``` With `optional=False` (default), `AttributeError` is raised if it is not found. diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index 34458008c..d0c3d9fc5 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -34,7 +34,7 @@ def __init__( type_name: Optional[Text] = None, parent: 'Entity' = None ) -> Text: - """Construnctor.""" + """Constructor.""" if type_name is None: if len(element) != 1: raise RuntimeError('Expected a len 1 dictionary') diff --git a/launch_yaml/package.xml b/launch_yaml/package.xml index e0d5246d0..17bd7c6ec 100644 --- a/launch_yaml/package.xml +++ b/launch_yaml/package.xml @@ -2,7 +2,7 @@ launch_yaml - 0.7.3 + 0.8.1 The ROS launch YAML frontend. Ivan Paunovic Apache License 2.0 From f3e08a4d7115f1c923580904fc8b0df5af8b6328 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 14:08:43 -0300 Subject: [PATCH 29/75] Corrected substitution grammar. Completed substitution test Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/grammar.lark | 12 ++++++------ .../test/launch_frontend/test_substitutions.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/launch_frontend/launch_frontend/grammar.lark b/launch_frontend/launch_frontend/grammar.lark index ffe854d59..b387355d6 100644 --- a/launch_frontend/launch_frontend/grammar.lark +++ b/launch_frontend/launch_frontend/grammar.lark @@ -6,14 +6,14 @@ IDENTIFIER: LETTER (LETTER | DIGIT | "_" | "-")* -UNQUOTED_STRING: (/[^'"$]|\$(?=!\()|(?<=\\)\$/ | "\"" | "\'")+ -UNQUOTED_RSTRING: (/[^ '"$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\"" | "\'")+ +UNQUOTED_STRING: (/[^'\\"$]|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\'" | "\\")+ +UNQUOTED_RSTRING: (/[^ '\\"$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\'" | "\\")+ -SINGLE_QUOTED_STRING: (/[^'$]|\$(?=!\()|(?<=\\)\$/ | "\'")+ -SINGLE_QUOTED_RSTRING: (/[^ '$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\'")+ +SINGLE_QUOTED_STRING: (/[^'\\$]|\$(?=!\()|(?<=\\)\$/ | "\\'" | "\\")+ +SINGLE_QUOTED_RSTRING: (/[^ '\\$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\\'" | "\\")+ -DOUBLE_QUOTED_STRING: (/[^"$]|\$(?=!\()|(?<=\\)\$/ | "\"")+ -DOUBLE_QUOTED_RSTRING: (/[^ "$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\"")+ +DOUBLE_QUOTED_STRING: (/[^"\\$]|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\")+ +DOUBLE_QUOTED_RSTRING: (/[^ "\\$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\")+ single_quoted_part: single_quoted_substitution | SINGLE_QUOTED_RSTRING diff --git a/launch_frontend/test/launch_frontend/test_substitutions.py b/launch_frontend/test/launch_frontend/test_substitutions.py index 276d706a2..4460998be 100644 --- a/launch_frontend/test/launch_frontend/test_substitutions.py +++ b/launch_frontend/test/launch_frontend/test_substitutions.py @@ -14,6 +14,7 @@ """Test the default substitution interpolator.""" +from launch import LaunchContext from launch.substitutions import TextSubstitution from launch_frontend.expose import expose_substitution @@ -97,9 +98,17 @@ def test_quoted_nested_substitution(): def test_double_quoted_nested_substitution(): subst = default_parse_substitution( - r'$(env "asd_bsd_qsd_$(test "asd_bds")" "$(env DEFAULT)_qsd")' + r'$(env "asd_bsd_qsd_$(test \"asd_bds\")" "$(env DEFAULT)_qsd")' ) + context = LaunchContext() assert len(subst) == 1 + assert len(subst[0].name) == 2 + assert subst[0].name[0].perform(context) == 'asd_bsd_qsd_' + assert subst[0].name[1].perform(context) == '"asd_bds"' + assert len(subst[0].default_value) == 2 + assert subst[0].default_value[0].name[0].perform(context) == 'DEFAULT' + assert subst[0].default_value[0].default_value[0].perform(context) == '' + assert subst[0].default_value[1].perform(context) == '_qsd' if __name__ == '__main__': From c0efe06a58df877f7d29f1e0256818290f49fda7 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 14:14:59 -0300 Subject: [PATCH 30/75] Add new test substitution case Signed-off-by: ivanpauno --- .../test/launch_frontend/test_substitutions.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/launch_frontend/test/launch_frontend/test_substitutions.py b/launch_frontend/test/launch_frontend/test_substitutions.py index 4460998be..57abc71f2 100644 --- a/launch_frontend/test/launch_frontend/test_substitutions.py +++ b/launch_frontend/test/launch_frontend/test_substitutions.py @@ -111,6 +111,21 @@ def test_double_quoted_nested_substitution(): assert subst[0].default_value[1].perform(context) == '_qsd' +def test_combining_quotes_nested_substitution(): + subst = default_parse_substitution( + '$(env "asd_bsd_qsd_$(test \'asd_bds\')" \'$(env DEFAULT)_qsd\')' + ) + context = LaunchContext() + assert len(subst) == 1 + assert len(subst[0].name) == 2 + assert subst[0].name[0].perform(context) == 'asd_bsd_qsd_' + assert subst[0].name[1].perform(context) == "'asd_bds'" + assert len(subst[0].default_value) == 2 + assert subst[0].default_value[0].name[0].perform(context) == 'DEFAULT' + assert subst[0].default_value[0].default_value[0].perform(context) == '' + assert subst[0].default_value[1].perform(context) == '_qsd' + + if __name__ == '__main__': test_text_only() test_text_with_embedded_substitutions() From 6a302707e5a31bfa1f267150433e662db9844eb4 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 14:22:35 -0300 Subject: [PATCH 31/75] Update setup.py versions Signed-off-by: ivanpauno --- launch_frontend/setup.py | 2 +- launch_xml/setup.py | 2 +- launch_yaml/setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launch_frontend/setup.py b/launch_frontend/setup.py index 181d7cd2b..7489b7a52 100644 --- a/launch_frontend/setup.py +++ b/launch_frontend/setup.py @@ -3,7 +3,7 @@ setup( name='launch_frontend', - version='0.7.3', + version='0.8.1', packages=find_packages(exclude=['test']), install_requires=['setuptools'], zip_safe=True, diff --git a/launch_xml/setup.py b/launch_xml/setup.py index ab6d82043..e36fb0e70 100644 --- a/launch_xml/setup.py +++ b/launch_xml/setup.py @@ -3,7 +3,7 @@ setup( name='launch_xml', - version='0.7.3', + version='0.8.1', packages=find_packages(exclude=['test']), install_requires=['setuptools'], zip_safe=True, diff --git a/launch_yaml/setup.py b/launch_yaml/setup.py index 9cd706167..ae3cf5a1d 100644 --- a/launch_yaml/setup.py +++ b/launch_yaml/setup.py @@ -3,7 +3,7 @@ setup( name='launch_yaml', - version='0.7.3', + version='0.8.1', packages=find_packages(exclude=['test']), install_requires=['setuptools'], zip_safe=True, From ea58a135d7476dfd33fbd8b11e6a9e6c825ca094 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 17:11:05 -0300 Subject: [PATCH 32/75] Allow code reusage of base class parsing method Signed-off-by: ivanpauno --- .../launch_frontend/action_parse_methods.py | 62 +++++++++++++------ launch_frontend/launch_frontend/parser.py | 3 +- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index c2e5a3b40..cc997eec6 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -26,10 +26,41 @@ from .parser import Parser +def parse_action(entity: Entity, parser: Parser): + """ + Parse action. + + This is only intented for code reusage, and it's not exposed. + """ + if_cond = entity.get_attr('if', optional=True) + unless_cond = entity.get_attr('unless', optional=True) + kwargs = {} + if if_cond is not None and unless_cond is not None: + raise RuntimeError("if and unless conditions can't be used simultaneously") + if if_cond is not None: + kwargs['condition'] = IfCondition(predicate_expression=if_cond) + if unless_cond is not None: + kwargs['condition'] = UnlessCondition(predicate_expression=unless_cond) + return launch.Action, kwargs + + @expose_action('executable') -def parse_executable(entity: Entity, parser: Parser): - """Parse executable tag.""" - cmd = parser.parse_substitution(entity.get_attr('cmd')) +def parse_executable( + entity: Entity, + parser: Parser, + optional_cmd: bool = False +): + """ + Parse executable tag. + + :param: optional_cmd Allow not specifying `cmd` argument. + Intended for code reuse in derived classes (e.g.: launch_ros.actions.Node). + """ + cmd = entity.get_attr('cmd', optional=optional_cmd) + if cmd is not None: + cmd_list = [parser.parse_substitution(cmd)] + else: + cmd_list = [] kwargs = {} cwd = entity.get_attr('cwd', optional=True) if cwd is not None: @@ -81,21 +112,12 @@ def parse_executable(entity: Entity, parser: Parser): args = new_args else: args = [] - cmd_list = [cmd] cmd_list.extend(args) - if_cond = entity.get_attr('if', optional=True) - unless_cond = entity.get_attr('unless', optional=True) - if if_cond is not None and unless_cond is not None: - raise RuntimeError("if and unless conditions can't be usede simultaneously") - if if_cond is not None: - kwargs['condition'] = IfCondition(predicate_expression=if_cond) - if unless_cond is not None: - kwargs['condition'] = UnlessCondition(predicate_expression=unless_cond) + kwargs['cmd'] = cmd_list + _, action_kwargs = parse_action(entity, parser) + kwargs.update(action_kwargs) - return launch.actions.ExecuteProcess( - cmd=cmd_list, - **kwargs - ) + return launch.actions.ExecuteProcess, kwargs @expose_action('let') @@ -103,7 +125,7 @@ def parse_let(entity: Entity, parser: Parser): """Parse let tag.""" name = parser.parse_substitution(entity.get_attr('name')) value = parser.parse_substitution(entity.get_attr('value')) - return launch.actions.SetLaunchConfiguration( - name, - value - ) + _, kwargs = parse_action(entity, parser) + kwargs['name'] = name + kwargs['value'] = value + return launch.actions.SetLaunchConfiguration, kwargs diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index b574cbcd1..5e20b12a0 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -70,7 +70,8 @@ def parse_action(cls, entity: Entity) -> launch.Action: cls.load_parser_extensions() if entity.type_name not in action_parse_methods: raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) - return action_parse_methods[entity.type_name](entity, cls) + action, kwargs = action_parse_methods[entity.type_name](entity, cls) + return action(**kwargs) @classmethod def parse_substitution(cls, value: Text) -> launch.SomeSubstitutionsType: From 51d080e3e91463733a990548e1b09a318b2c94f1 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 17:26:41 -0300 Subject: [PATCH 33/75] Clearer launch_yaml.Entity constructor Signed-off-by: ivanpauno --- launch_yaml/launch_yaml/entity.py | 22 +++++++++++----------- launch_yaml/launch_yaml/parser.py | 6 +++++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index d0c3d9fc5..71f1c6848 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -30,19 +30,13 @@ class Entity(BaseEntity): def __init__( self, element: dict, + type_name: Text = None, *, - type_name: Optional[Text] = None, parent: 'Entity' = None ) -> Text: """Constructor.""" - if type_name is None: - if len(element) != 1: - raise RuntimeError('Expected a len 1 dictionary') - self.__type_name = list(element.keys())[0] - self.__element = element[self.__type_name] - else: - self.__type_name = type_name - self.__element = element + self.__type_name = type_name + self.__element = element self.__parent = parent @property @@ -60,7 +54,13 @@ def children(self) -> List['Entity']: """Get the Entity's children.""" if not isinstance(self.__element, list): raise TypeError('Expected a list, got {}'.format(type(self.element))) - return [Entity(child) for child in self.__element] + entities = [] + for child in self.__element: + if len(child) != 1: + raise RuntimeError('Expected one root per child') + type_name = list(child.keys())[0] + entities.append(Entity(child[type_name], type_name)) + return entities def get_attr( self, @@ -88,7 +88,7 @@ def get_attr( data = self.__element[name] if types == 'list[Entity]': if isinstance(data, list) and isinstance(data[0], dict): - return [Entity(child, type_name=name) for child in data] + return [Entity(child, name) for child in data] raise TypeError( 'Attribute {} of Entity {} expected to be a list of dictionaries.'.format( name, self.type_name diff --git a/launch_yaml/launch_yaml/parser.py b/launch_yaml/launch_yaml/parser.py index b98e2315a..b8b4a2901 100644 --- a/launch_yaml/launch_yaml/parser.py +++ b/launch_yaml/launch_yaml/parser.py @@ -36,4 +36,8 @@ def load( if isinstance(stream, str): stream = open(stream, 'r') """Return entity loaded with markup file.""" - return (Entity(yaml.safe_load(stream)), cls) + tree = yaml.safe_load(stream) + if len(tree) != 1: + raise RuntimeError('Expected only one root') + type_name = list(tree.keys())[0] + return (Entity(tree[type_name], type_name), cls) From 3f13144e90702b265ef135f93f78299f3488c2dd Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 17 May 2019 17:54:41 -0300 Subject: [PATCH 34/75] typo Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/type_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index 4431a72b5..806ce6f2d 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -20,7 +20,7 @@ from typing import Union -types_guess__ = ( +types_for_guess__ = ( 'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str' ) From 595fa9c5e8192d106443891fd515d5cea4cf26ca Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 20 May 2019 10:58:16 -0300 Subject: [PATCH 35/75] Moved example tests in place Signed-off-by: ivanpauno --- launch_xml/test/launch_xml/executable.xml | 5 ----- launch_xml/test/launch_xml/let_var.xml | 4 ---- launch_xml/test/launch_xml/list.xml | 5 ----- launch_xml/test/launch_xml/test_executable.py | 14 +++++++++++-- launch_xml/test/launch_xml/test_let_var.py | 13 ++++++++++-- launch_xml/test/launch_xml/test_list.py | 19 +++++++++++------ launch_yaml/test/launch_yaml/executable.yaml | 12 ----------- .../test/launch_yaml/test_executable.py | 21 +++++++++++++++++-- 8 files changed, 55 insertions(+), 38 deletions(-) delete mode 100644 launch_xml/test/launch_xml/executable.xml delete mode 100644 launch_xml/test/launch_xml/let_var.xml delete mode 100644 launch_xml/test/launch_xml/list.xml delete mode 100644 launch_yaml/test/launch_yaml/executable.yaml diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml deleted file mode 100644 index 437c7a1da..000000000 --- a/launch_xml/test/launch_xml/executable.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/launch_xml/test/launch_xml/let_var.xml b/launch_xml/test/launch_xml/let_var.xml deleted file mode 100644 index 2a2390a18..000000000 --- a/launch_xml/test/launch_xml/let_var.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/launch_xml/test/launch_xml/list.xml b/launch_xml/test/launch_xml/list.xml deleted file mode 100644 index fc38f7aea..000000000 --- a/launch_xml/test/launch_xml/list.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 19c8feeda..1ffef523e 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -14,7 +14,8 @@ """Test parsing an executable action.""" -from pathlib import Path +import io +import textwrap from launch import LaunchService @@ -23,7 +24,16 @@ def test_executable(): """Parse node xml example.""" - root_entity, parser = Parser.load(str(Path(__file__).parent / 'executable.xml')) + xml_file = \ + """\ + + + + + + """ # noqa: E501 + xml_file = textwrap.dedent(xml_file) + root_entity, parser = Parser.load(io.StringIO(xml_file)) ld = parser.parse_description(root_entity) executable = ld.entities[0] cmd = [i[0].perform(None) for i in executable.cmd] diff --git a/launch_xml/test/launch_xml/test_let_var.py b/launch_xml/test/launch_xml/test_let_var.py index d1e181d2f..1b939b775 100644 --- a/launch_xml/test/launch_xml/test_let_var.py +++ b/launch_xml/test/launch_xml/test_let_var.py @@ -14,7 +14,8 @@ """Test parsing an executable action.""" -from pathlib import Path +import io +import textwrap from launch import LaunchContext @@ -23,7 +24,15 @@ def test_let_var(): """Parse node xml example.""" - root_entity, parser = Parser.load(str(Path(__file__).parent / 'let_var.xml')) + xml_file = \ + """\ + + + + + """ + xml_file = textwrap.dedent(xml_file) + root_entity, parser = Parser.load(io.StringIO(xml_file)) ld = parser.parse_description(root_entity) context = LaunchContext() assert len(ld.entities) == 2 diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py index c2f90dd33..5e3ba50b3 100644 --- a/launch_xml/test/launch_xml/test_list.py +++ b/launch_xml/test/launch_xml/test_list.py @@ -14,17 +14,24 @@ """Test parsing list attributes.""" -from pathlib import Path -import xml.etree.ElementTree as ET +import io +import textwrap -from launch_xml import Entity +from launch_frontend import Parser def test_list(): """Parse tags with list attributes.""" - tree = ET.parse(str(Path(__file__).parent / 'list.xml')) - root = tree.getroot() - root_entity = Entity(root) + xml_file = \ + """\ + + + + + + """ + xml_file = textwrap.dedent(xml_file) + root_entity, parser = Parser.load(io.StringIO(xml_file)) tags = root_entity.children assert tags[0].get_attr('attr', types='list[str]') == ['1', '2', '3'] assert tags[0].get_attr('attr', types='list[int]') == [1, 2, 3] diff --git a/launch_yaml/test/launch_yaml/executable.yaml b/launch_yaml/test/launch_yaml/executable.yaml deleted file mode 100644 index 8e205d94b..000000000 --- a/launch_yaml/test/launch_yaml/executable.yaml +++ /dev/null @@ -1,12 +0,0 @@ -launch: - - executable: - cmd: ls - cwd: '/' - name: my_ls - args: -l -a -s - shell: true - output: log - launch_prefix: $(env LAUNCH_PREFIX) - env: - - name: var - value: '1' diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py index 7898c01cb..aa20ea37d 100644 --- a/launch_yaml/test/launch_yaml/test_executable.py +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -14,7 +14,8 @@ """Test parsing an executable action.""" -from pathlib import Path +import io +import textwrap from launch import LaunchService @@ -23,7 +24,23 @@ def test_executable(): """Parse executable yaml example.""" - root_entity, parser = Parser.load(str(Path(__file__).parent / 'executable.yaml')) + yaml_file = \ + """\ + launch: + - executable: + cmd: ls + cwd: '/' + name: my_ls + args: -l -a -s + shell: true + output: log + launch_prefix: $(env LAUNCH_PREFIX) + env: + - name: var + value: '1' + """ + yaml_file = textwrap.dedent(yaml_file) + root_entity, parser = Parser.load(io.StringIO(yaml_file)) ld = parser.parse_description(root_entity) executable = ld.entities[0] cmd = [i[0].perform(None) for i in executable.cmd] From 244dbbe0e44eb062fc460c1c20fa4bbe01c26737 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 20 May 2019 12:36:19 -0300 Subject: [PATCH 36/75] Corrected error in type_utils Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/type_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index 806ce6f2d..bebb14212 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -79,7 +79,7 @@ def check_type(value: Any, types: Union[Text, Tuple[Text]]) -> bool: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') """ if types == 'guess': - types = types_guess__ + types = types_for_guess__ if isinstance(types, Text): types = [types] for x in types: @@ -116,7 +116,7 @@ def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') """ if types == 'guess': - types = types_guess__ + types = types_for_guess__ elif isinstance(types, Text): types = [types] From a711306be0a186d01dea3aa0cc63c220d8bdfc2b Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 20 May 2019 12:36:55 -0300 Subject: [PATCH 37/75] Correct error with brute force load Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 5e20b12a0..e8399656e 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -108,5 +108,6 @@ def load( try: return implementation.load(file) except Exception: - pass + if is_a(file, io.TextIOBase): + file.seek(0) raise RuntimeError('Not recognized front-end implementation.') From f4d7ecc6114d0e02f6f8281d9dca3c3ae3752951 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 20 May 2019 12:37:42 -0300 Subject: [PATCH 38/75] Correct indentation Signed-off-by: ivanpauno --- .../test/launch_yaml/test_executable.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py index aa20ea37d..afe61869d 100644 --- a/launch_yaml/test/launch_yaml/test_executable.py +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -27,17 +27,17 @@ def test_executable(): yaml_file = \ """\ launch: - - executable: - cmd: ls - cwd: '/' - name: my_ls - args: -l -a -s - shell: true - output: log - launch_prefix: $(env LAUNCH_PREFIX) - env: - - name: var - value: '1' + - executable: + cmd: ls + cwd: '/' + name: my_ls + args: -l -a -s + shell: true + output: log + launch_prefix: $(env LAUNCH_PREFIX) + env: + - name: var + value: '1' """ yaml_file = textwrap.dedent(yaml_file) root_entity, parser = Parser.load(io.StringIO(yaml_file)) From 889dbdbabc7803c8ad1e0b359cdc86f6d63dde64 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 3 Jun 2019 11:23:27 -0300 Subject: [PATCH 39/75] Add option for reading value as an string in yaml format Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 2 ++ launch_frontend/launch_frontend/type_utils.py | 14 ++++++++++++++ launch_frontend/package.xml | 1 + launch_yaml/launch_yaml/entity.py | 4 ++++ 4 files changed, 21 insertions(+) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index d1c88d6ea..2b502e284 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -81,10 +81,12 @@ def get_attr( Types that can not be combined with the others: - 'guess' - 'list[Entity]' + - 'yaml_format' 'guess' work in the same way as: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') 'list[Entity]' will return a list of more entities. + 'yaml_format' will return an string in yaml_format. See the frontend documentation to see how 'list' and 'list[Entity]' look like. diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index bebb14212..bba1a768b 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -19,6 +19,7 @@ from typing import Tuple from typing import Union +import yaml types_for_guess__ = ( 'int', 'float', 'bool', 'list[int]', 'list[float]', @@ -114,9 +115,15 @@ def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: types = 'guess' works in the same way as: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') + types = 'yaml_format' works as guess, but the value is converted back to a string in yaml + format. """ + convert_to_yaml = False if types == 'guess': types = types_for_guess__ + elif types == 'yaml_format': + types = types_for_guess__ + convert_to_yaml = True elif isinstance(types, Text): types = [types] @@ -152,4 +159,11 @@ def type_obj(x): value, types ) ) + if convert_to_yaml: + yaml_value = yaml.safe_dump(typed_value) + if not yaml.safe_load(yaml_value) == typed_value and isinstance(yaml_value, str): + yaml_value = "'" + yaml_value + "'" + if not yaml.safe_load(yaml_value) == typed_value: + raise RuntimeError('Unexpected failure. Inconsistency while dumping file to yaml format.') + return yaml_value return typed_value diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml index 033b41582..a2fae4637 100644 --- a/launch_frontend/package.xml +++ b/launch_frontend/package.xml @@ -8,6 +8,7 @@ Apache License 2.0 launch + python3-yaml ament_copyright ament_flake8 diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index 71f1c6848..5c368527c 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -23,6 +23,8 @@ from launch_frontend import Entity as BaseEntity from launch_frontend.type_utils import check_type +import yaml + class Entity(BaseEntity): """Single item in the intermediate YAML front_end representation.""" @@ -94,6 +96,8 @@ def get_attr( name, self.type_name ) ) + if types == 'yaml_format': + return yaml.safe_dump(data) # Return it again as an string in yaml format if not check_type(data, types): raise TypeError( 'Attribute {} of Entity {} expected to be of type {}, got {}'.format( From 7d6076bf7904b212edcd8de2c84c79d7a05591fb Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 3 Jun 2019 12:50:11 -0300 Subject: [PATCH 40/75] Revert "Add option for reading value as an string in yaml format" This reverts commit cd4ff85e2886938c834400ab241f657030b01a45. Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/entity.py | 2 -- launch_frontend/launch_frontend/type_utils.py | 14 -------------- launch_frontend/package.xml | 1 - launch_yaml/launch_yaml/entity.py | 4 ---- 4 files changed, 21 deletions(-) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index 2b502e284..d1c88d6ea 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -81,12 +81,10 @@ def get_attr( Types that can not be combined with the others: - 'guess' - 'list[Entity]' - - 'yaml_format' 'guess' work in the same way as: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') 'list[Entity]' will return a list of more entities. - 'yaml_format' will return an string in yaml_format. See the frontend documentation to see how 'list' and 'list[Entity]' look like. diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index bba1a768b..bebb14212 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -19,7 +19,6 @@ from typing import Tuple from typing import Union -import yaml types_for_guess__ = ( 'int', 'float', 'bool', 'list[int]', 'list[float]', @@ -115,15 +114,9 @@ def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: types = 'guess' works in the same way as: ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') - types = 'yaml_format' works as guess, but the value is converted back to a string in yaml - format. """ - convert_to_yaml = False if types == 'guess': types = types_for_guess__ - elif types == 'yaml_format': - types = types_for_guess__ - convert_to_yaml = True elif isinstance(types, Text): types = [types] @@ -159,11 +152,4 @@ def type_obj(x): value, types ) ) - if convert_to_yaml: - yaml_value = yaml.safe_dump(typed_value) - if not yaml.safe_load(yaml_value) == typed_value and isinstance(yaml_value, str): - yaml_value = "'" + yaml_value + "'" - if not yaml.safe_load(yaml_value) == typed_value: - raise RuntimeError('Unexpected failure. Inconsistency while dumping file to yaml format.') - return yaml_value return typed_value diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml index a2fae4637..033b41582 100644 --- a/launch_frontend/package.xml +++ b/launch_frontend/package.xml @@ -8,7 +8,6 @@ Apache License 2.0 launch - python3-yaml ament_copyright ament_flake8 diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index 5c368527c..71f1c6848 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -23,8 +23,6 @@ from launch_frontend import Entity as BaseEntity from launch_frontend.type_utils import check_type -import yaml - class Entity(BaseEntity): """Single item in the intermediate YAML front_end representation.""" @@ -96,8 +94,6 @@ def get_attr( name, self.type_name ) ) - if types == 'yaml_format': - return yaml.safe_dump(data) # Return it again as an string in yaml format if not check_type(data, types): raise TypeError( 'Attribute {} of Entity {} expected to be of type {}, got {}'.format( From 1f6f91123aa1f468630516adb83fdb76455842a3 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 3 Jun 2019 17:43:21 -0300 Subject: [PATCH 41/75] Minimal correction in grammar Signed-off-by: ivanpauno --- launch_frontend/launch_frontend/grammar.lark | 2 +- launch_frontend/test/launch_frontend/test_substitutions.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/launch_frontend/launch_frontend/grammar.lark b/launch_frontend/launch_frontend/grammar.lark index b387355d6..32ebbd793 100644 --- a/launch_frontend/launch_frontend/grammar.lark +++ b/launch_frontend/launch_frontend/grammar.lark @@ -6,7 +6,7 @@ IDENTIFIER: LETTER (LETTER | DIGIT | "_" | "-")* -UNQUOTED_STRING: (/[^'\\"$]|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\'" | "\\")+ +UNQUOTED_STRING: (/[^'\\"$]|\$(?=!\()|(?<=\\)\$|(?<=\\)\"|(?<=\\)'|(?<=\\)\\/)+ UNQUOTED_RSTRING: (/[^ '\\"$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\'" | "\\")+ SINGLE_QUOTED_STRING: (/[^'\\$]|\$(?=!\()|(?<=\\)\$/ | "\\'" | "\\")+ diff --git a/launch_frontend/test/launch_frontend/test_substitutions.py b/launch_frontend/test/launch_frontend/test_substitutions.py index 57abc71f2..1a063b762 100644 --- a/launch_frontend/test/launch_frontend/test_substitutions.py +++ b/launch_frontend/test/launch_frontend/test_substitutions.py @@ -22,9 +22,12 @@ def test_text_only(): - subst = default_parse_substitution('yes') + subst = default_parse_substitution("\\'yes\\'") assert len(subst) == 1 - assert subst[0].perform(None) == 'yes' + assert subst[0].perform(None) == "'yes'" + subst = default_parse_substitution('\\"yes\\"') + assert len(subst) == 1 + assert subst[0].perform(None) == '"yes"' subst = default_parse_substitution('10') assert len(subst) == 1 assert subst[0].perform(None) == '10' From b1e30cd1d41630060a981bb61fe0a72d4904cc88 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 11 Jun 2019 14:01:04 -0300 Subject: [PATCH 42/75] Addressed reviewer comments Signed-off-by: ivanpauno --- .../launch_frontend/action_parse_methods.py | 5 ---- launch_frontend/launch_frontend/entity.py | 25 ++++++++-------- launch_frontend/launch_frontend/expose.py | 12 ++++---- .../launch_frontend/parse_substitution.py | 14 ++++----- launch_frontend/launch_frontend/parser.py | 16 +++++----- .../substitution_parse_methods.py | 18 ++++++----- .../launch_frontend/test_expose_decorators.py | 8 ++--- .../launch_frontend/test_substitutions.py | 30 ++++++++++--------- launch_xml/README.md | 2 +- 9 files changed, 63 insertions(+), 67 deletions(-) diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index cc997eec6..2a0fab143 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -82,11 +82,6 @@ def parse_executable( # `unset_enviroment_variable` actions should be used. env = entity.get_attr('env', types='list[Entity]', optional=True) if env is not None: - # TODO(ivanpauno): Change `ExecuteProcess` api. `additional_env` - # argument is supposed to be a dictionary with `SomeSubstitutionType` - # keys, but `SomeSubstitutionType` is not always hashable. - # Proposed `additional_env` type: - # Iterable[Tuple[SomeSubstitutionType, SomeSubstitutionsType]] env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} kwargs['additional_env'] = env args = entity.get_attr('args', optional=True) diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index d1c88d6ea..a34340ab2 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -55,18 +55,11 @@ def get_attr( List['Entity'] ]]: """ - Access an element in the tree. + Access an element of the entity. By default, it will try to return it as an string. - `types` is used in the following way: - - For frontends that natively recoginize data types (like yaml), - it will check if the attribute read match with one in `types`. - If it is one of them, the value is returned. - If not, an `TypeError` is raised. - - For frontends that don't natively recognize data types (like xml), - it will try to convert the value read to one of the specified `types`. - The first convertion that success is returned. - If no conversion success, a `TypeError` is raised. + `types` states the expected types of the attribute. Type coercion or type checking is + applied depending on the particular frontend. The allowed types are: - 'str' @@ -90,8 +83,14 @@ def get_attr( If `optional` argument is `True`, will return `None` instead of raising `AttributeError`. - Possible errors: - - `AttributeError`: Attribute not found. Only possible if `optional` is `False`. - - `TypeError`: Attribute found but it is not of the correct type. + :param name: name of the attribute + :param types: type of the attribute to be read. Default to 'str' + :param optional: when `True`, it doesn't raise an error when the attribute is not found. + It returns `None` instead. Defaults to `False` + :raises `AttributeError`: Attribute not found. Only possible if `optional` is `False` + :raises `TypeError`: Attribute found but it is not of the correct type. + Only happens in froentends that do type checking + :raises `ValueError`: Attribute found but can't be coerced to one of the types. + Only happens in froentends that do type coercion """ raise NotImplementedError() diff --git a/launch_frontend/launch_frontend/expose.py b/launch_frontend/launch_frontend/expose.py index 55e9d8191..767fef544 100644 --- a/launch_frontend/launch_frontend/expose.py +++ b/launch_frontend/launch_frontend/expose.py @@ -35,10 +35,10 @@ def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): If two different parsing methods are exposed using the same `name`, a `RuntimeError` will be raised. - :param: name a string which specifies the key used for storing the parsing + :param name: a string which specifies the key used for storing the parsing method in the dictionary. - :param: parse_methods_map a dict where the parsing method will be stored. - :exposed_type: A string specifing the parsing function type. + :param parse_methods_map: a dict where the parsing method will be stored. + :param exposed_type: A string specifing the parsing function type. Only used for having clearer error log messages. """ # TODO(ivanpauno): Check signature of the registered method/parsing function. @@ -47,13 +47,13 @@ def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): def expose_impl_decorator(exposed): found_parse_method = None if inspect.isclass(exposed): - if 'parse' in dir(exposed): + if 'parse' in dir(exposed) and inspect.isfunction(exposed.parse): found_parse_method = exposed.parse else: raise RuntimeError( - "Did not found a method called 'parse' in the class being decorated." + "Did not find an static method called 'parse' in the class being decorated." ) - elif callable(exposed): + elif inspect.isfunction(exposed): found_parse_method = exposed if not found_parse_method: raise RuntimeError( diff --git a/launch_frontend/launch_frontend/parse_substitution.py b/launch_frontend/launch_frontend/parse_substitution.py index 8c517a242..84ed3c1ee 100644 --- a/launch_frontend/launch_frontend/parse_substitution.py +++ b/launch_frontend/launch_frontend/parse_substitution.py @@ -15,6 +15,7 @@ """Module for parsing substitutions.""" import os +import re from typing import Text from lark import Lark @@ -26,12 +27,9 @@ from launch_frontend.expose import substitution_parse_methods -def replace_escaped_characters(data: Text, beg: int = 0) -> Text: +def replace_escaped_characters(data: Text) -> Text: """Search escaped characters and replace them.""" - pos = data.find('\\', beg) - if pos == -1: - return data[beg:] - return data[beg:pos] + data[pos+1] + replace_escaped_characters(data, pos+2) + return re.sub(r'\\(.)', r'\1', data) class ExtractSubstitution(Transformer): @@ -71,11 +69,11 @@ def substitution(self, args): name = args[0] assert isinstance(name, Token) assert name.type == 'IDENTIFIER' - # TODO(hidmic): Lookup and instantiate Substitution if name.value not in substitution_parse_methods: raise RuntimeError( 'Unknown substitution: {}'.format(name.value)) - return substitution_parse_methods[name.value](*args[1:]) + subst, kwargs = substitution_parse_methods[name.value](*args[1:]) + return subst(**kwargs) single_quoted_substitution = substitution double_quoted_substitution = substitution @@ -103,6 +101,6 @@ def template(self, fragments): transformer = ExtractSubstitution() -def default_parse_substitution(string_value): +def parse_substitution(string_value): tree = parser.parse(string_value) return transformer.transform(tree) diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index e8399656e..649f4f5d7 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -15,7 +15,9 @@ """Module for Parser class and parsing methods.""" import io +from typing import Any from typing import Text +from typing import Tuple from typing import Union import launch @@ -25,19 +27,17 @@ from .entity import Entity from .expose import action_parse_methods -from .parse_substitution import default_parse_substitution +from .parse_substitution import parse_substitution interpolation_fuctions = { entry_point.name: entry_point.load() for entry_point in iter_entry_points('launch_frontend.interpolate_substitution') } -extensions_loaded = False - class Parser: """ - Abstract class for parsing actions, substitutions and descriptions. + Abstract class for parsing launch actions, substitutions and descriptions. Implementations of the parser class, should override the load method. They could also override the parse_substitution method, or not. @@ -49,9 +49,9 @@ class Parser: @classmethod def load_parser_extensions(cls): - """Load parser extension, in order to get all the exposed substitutions and actions.""" + """Load launch extensions, in order to get all the exposed substitutions and actions.""" if cls.extensions_loaded is False: - for entry_point in iter_entry_points('launch_frontend.parser_extension'): + for entry_point in iter_entry_points('launch_frontend.launch_extension'): entry_point.load() cls.extensions_loaded = True @@ -65,7 +65,7 @@ def load_parser_implementations(cls): } @classmethod - def parse_action(cls, entity: Entity) -> launch.Action: + def parse_action(cls, entity: Entity) -> (launch.Action, Tuple[Any]): """Parse an action, using its registered parsing method.""" cls.load_parser_extensions() if entity.type_name not in action_parse_methods: @@ -76,7 +76,7 @@ def parse_action(cls, entity: Entity) -> launch.Action: @classmethod def parse_substitution(cls, value: Text) -> launch.SomeSubstitutionsType: """Parse a substitution.""" - return default_parse_substitution(value) + return parse_substitution(value) @classmethod def parse_description(cls, entity: Entity) -> launch.LaunchDescription: diff --git a/launch_frontend/launch_frontend/substitution_parse_methods.py b/launch_frontend/launch_frontend/substitution_parse_methods.py index abd827e50..d775d3be8 100644 --- a/launch_frontend/launch_frontend/substitution_parse_methods.py +++ b/launch_frontend/launch_frontend/substitution_parse_methods.py @@ -25,24 +25,25 @@ def parse_env(data: Iterable[launch.SomeSubstitutionsType]): """Parse EnviromentVariable substitution.""" if not data or len(data) > 2: - raise AttributeError('env substitution expects 1 or 2 arguments') - name = data[0] + raise TypeError('env substitution expects 1 or 2 arguments') kwargs = {} + kwargs['name'] = data[0] if len(data) == 2: kwargs['default_value'] = data[1] - return launch.substitutions.EnvironmentVariable(name, **kwargs) + return launch.substitutions.EnvironmentVariable, kwargs @expose_substitution('var') def parse_var(data: Iterable[launch.SomeSubstitutionsType]): """Parse FindExecutable substitution.""" + # Reuse parse_env, as it is similar if not data or len(data) > 2: - raise AttributeError('var substitution expects 1 or 2 arguments') - name = data[0] + raise TypeError('var substitution expects 1 or 2 arguments') kwargs = {} + kwargs['variable_name'] = data[0] if len(data) == 2: kwargs['default'] = data[1] - return launch.substitutions.LaunchConfiguration(name, **kwargs) + return launch.substitutions.LaunchConfiguration, kwargs @expose_substitution('find-exec') @@ -50,5 +51,6 @@ def parse_find_exec(data: Iterable[launch.SomeSubstitutionsType]): """Parse FindExecutable substitution.""" if not data or len(data) > 1: raise AttributeError('find-exec substitution expects 1 argument') - name = data[0] - return launch.substitutions.FindExecutable(name=name) + kwargs = {} + kwargs['name'] = data[0] + return launch.substitutions.FindExecutable, kwargs diff --git a/launch_frontend/test/launch_frontend/test_expose_decorators.py b/launch_frontend/test/launch_frontend/test_expose_decorators.py index 139c24a78..841ac9357 100644 --- a/launch_frontend/test/launch_frontend/test_expose_decorators.py +++ b/launch_frontend/test/launch_frontend/test_expose_decorators.py @@ -20,12 +20,12 @@ class ToBeExposed: @staticmethod - def parse(entity): - return ToBeExposed() + def parse(entity, parser): + return ToBeExposed(), () -def to_be_exposed(entity): - return ToBeExposed() +def to_be_exposed(entity, parser): + return ToBeExposed(), () register = dict({}) diff --git a/launch_frontend/test/launch_frontend/test_substitutions.py b/launch_frontend/test/launch_frontend/test_substitutions.py index 1a063b762..72900056c 100644 --- a/launch_frontend/test/launch_frontend/test_substitutions.py +++ b/launch_frontend/test/launch_frontend/test_substitutions.py @@ -18,23 +18,23 @@ from launch.substitutions import TextSubstitution from launch_frontend.expose import expose_substitution -from launch_frontend.parse_substitution import default_parse_substitution +from launch_frontend.parse_substitution import parse_substitution def test_text_only(): - subst = default_parse_substitution("\\'yes\\'") + subst = parse_substitution("\\'yes\\'") assert len(subst) == 1 assert subst[0].perform(None) == "'yes'" - subst = default_parse_substitution('\\"yes\\"') + subst = parse_substitution('\\"yes\\"') assert len(subst) == 1 assert subst[0].perform(None) == '"yes"' - subst = default_parse_substitution('10') + subst = parse_substitution('10') assert len(subst) == 1 assert subst[0].perform(None) == '10' - subst = default_parse_substitution('10e4') + subst = parse_substitution('10e4') assert len(subst) == 1 assert subst[0].perform(None) == '10e4' - subst = default_parse_substitution('10e4') + subst = parse_substitution('10e4') assert len(subst) == 1 assert subst[0].perform(None) == '10e4' @@ -43,11 +43,13 @@ def test_text_only(): def parse_test_substitution(data): if not data or len(data) > 1: raise RuntimeError() - return TextSubstitution(text=''.join([i.perform(None) for i in data[0]])) + kwargs = {} + kwargs['text'] = ''.join([i.perform(None) for i in data[0]]) + return TextSubstitution, kwargs def test_text_with_embedded_substitutions(): - subst = default_parse_substitution('why_$(test asd)_asdasd_$(test bsd)') + subst = parse_substitution('why_$(test asd)_asdasd_$(test bsd)') assert len(subst) == 4 assert subst[0].perform(None) == 'why_' assert subst[1].perform(None) == 'asd' @@ -59,7 +61,7 @@ def test_text_with_embedded_substitutions(): def test_substitution_with_multiple_arguments(): - subst = default_parse_substitution('$(env what heck)') + subst = parse_substitution('$(env what heck)') assert len(subst) == 1 subst = subst[0] assert subst.name[0].perform(None) == 'what' @@ -67,7 +69,7 @@ def test_substitution_with_multiple_arguments(): def test_escaped_characters(): - subst = default_parse_substitution(r'$(env what/\$\(test asd\\\)) 10 10)') + subst = parse_substitution(r'$(env what/\$\(test asd\\\)) 10 10)') assert len(subst) == 2 assert subst[0].name[0].perform(None) == 'what/$(test' assert subst[0].default_value[0].perform(None) == r'asd\)' @@ -75,7 +77,7 @@ def test_escaped_characters(): def test_nested_substitutions(): - subst = default_parse_substitution('$(env what/$(test asd) 10) 10 10)') + subst = parse_substitution('$(env what/$(test asd) 10) 10 10)') assert len(subst) == 2 assert len(subst[0].name) == 2 assert subst[0].name[0].perform(None) == 'what/' @@ -85,7 +87,7 @@ def test_nested_substitutions(): def test_quoted_nested_substitution(): - subst = default_parse_substitution( + subst = parse_substitution( 'go_to_$(env WHERE asd)_of_$(env ' "'something $(test 10)')" ) @@ -100,7 +102,7 @@ def test_quoted_nested_substitution(): def test_double_quoted_nested_substitution(): - subst = default_parse_substitution( + subst = parse_substitution( r'$(env "asd_bsd_qsd_$(test \"asd_bds\")" "$(env DEFAULT)_qsd")' ) context = LaunchContext() @@ -115,7 +117,7 @@ def test_double_quoted_nested_substitution(): def test_combining_quotes_nested_substitution(): - subst = default_parse_substitution( + subst = parse_substitution( '$(env "asd_bsd_qsd_$(test \'asd_bds\')" \'$(env DEFAULT)_qsd\')' ) context = LaunchContext() diff --git a/launch_xml/README.md b/launch_xml/README.md index bf931f9b5..3029ac176 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -88,7 +88,7 @@ All the children can be directly accessed: e.children ``` -It returns a list of launch_xml.Entity wrapping each of the xml children. +It returns a list of `launch_xml.Entity` wrapping each of the XML children. ## Built-in substitutions From 5cb565cf29ada8d946e3385e66cda75ef554bcd1 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 13 Jun 2019 13:20:44 -0300 Subject: [PATCH 43/75] Address review comments Signed-off-by: ivanpauno --- .../launch_frontend/action_parse_methods.py | 5 ----- launch_frontend/launch_frontend/entity.py | 6 +++--- launch_frontend/launch_frontend/parser.py | 15 ++++++--------- launch_frontend/package.xml | 2 +- launch_frontend/setup.py | 4 ++-- launch_xml/launch_xml/parser.py | 2 +- launch_xml/package.xml | 2 +- launch_xml/setup.py | 2 +- launch_yaml/launch_yaml/parser.py | 2 +- launch_yaml/package.xml | 2 +- launch_yaml/setup.py | 2 +- 11 files changed, 18 insertions(+), 26 deletions(-) diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index 2a0fab143..e96f2af4f 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -88,11 +88,6 @@ def parse_executable( # `args` is supposed to be a list separated with ' '. # All the found `TextSubstitution` items are split and # added to the list again as a `TextSubstitution`. - # TODO(ivanpauno): Change `ExecuteProcess` api from accepting - # `Iterable[SomeSubstitutionType]` `cmd` to `SomeSubstitutionType`. - # After performing the substitution in `cmd`, shlex.split should be done. - # This will also allow having a substitution which ends in more than one - # argument. if args is not None: args = parser.parse_substitution(args) new_args = [] diff --git a/launch_frontend/launch_frontend/entity.py b/launch_frontend/launch_frontend/entity.py index a34340ab2..56684584c 100644 --- a/launch_frontend/launch_frontend/entity.py +++ b/launch_frontend/launch_frontend/entity.py @@ -55,7 +55,7 @@ def get_attr( List['Entity'] ]]: """ - Access an element of the entity. + Access an attribute of the entity. By default, it will try to return it as an string. `types` states the expected types of the attribute. Type coercion or type checking is @@ -89,8 +89,8 @@ def get_attr( It returns `None` instead. Defaults to `False` :raises `AttributeError`: Attribute not found. Only possible if `optional` is `False` :raises `TypeError`: Attribute found but it is not of the correct type. - Only happens in froentends that do type checking + Only happens in frontend implementations that do type checking :raises `ValueError`: Attribute found but can't be coerced to one of the types. - Only happens in froentends that do type coercion + Only happens in frontend implementations that do type coercion """ raise NotImplementedError() diff --git a/launch_frontend/launch_frontend/parser.py b/launch_frontend/launch_frontend/parser.py index 649f4f5d7..545d1e7a1 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch_frontend/launch_frontend/parser.py @@ -64,26 +64,23 @@ def load_parser_implementations(cls): for entry_point in iter_entry_points('launch_frontend.parser') } - @classmethod - def parse_action(cls, entity: Entity) -> (launch.Action, Tuple[Any]): + def parse_action(self, entity: Entity) -> (launch.Action, Tuple[Any]): """Parse an action, using its registered parsing method.""" - cls.load_parser_extensions() + self.load_parser_extensions() if entity.type_name not in action_parse_methods: raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) - action, kwargs = action_parse_methods[entity.type_name](entity, cls) + action, kwargs = action_parse_methods[entity.type_name](entity, self) return action(**kwargs) - @classmethod - def parse_substitution(cls, value: Text) -> launch.SomeSubstitutionsType: + def parse_substitution(self, value: Text) -> launch.SomeSubstitutionsType: """Parse a substitution.""" return parse_substitution(value) - @classmethod - def parse_description(cls, entity: Entity) -> launch.LaunchDescription: + def parse_description(self, entity: Entity) -> launch.LaunchDescription: """Parse a launch description.""" if entity.type_name != 'launch': raise RuntimeError('Expected \'launch\' as root tag') - actions = [cls.parse_action(child) for child in entity.children] + actions = [self.parse_action(child) for child in entity.children] return launch.LaunchDescription(actions) @classmethod diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml index 033b41582..50b44d271 100644 --- a/launch_frontend/package.xml +++ b/launch_frontend/package.xml @@ -3,7 +3,7 @@ launch_frontend 0.8.1 - The ROS launch frontend. + Frontend extensions for the launch package. Ivan Paunovic Apache License 2.0 diff --git a/launch_frontend/setup.py b/launch_frontend/setup.py index 7489b7a52..e7f464b57 100644 --- a/launch_frontend/setup.py +++ b/launch_frontend/setup.py @@ -20,9 +20,9 @@ 'Programming Language :: Python', 'Topic :: Software Development', ], - description='Front-end extensions to `launch`.', + description='Frontend extensions for the `launch` package.', long_description=( - 'This package provides front-end extensions to the `launch` package.' + 'This package provides front-end extensions for the `launch` package.' ), license='Apache License, Version 2.0', tests_require=['pytest'], diff --git a/launch_xml/launch_xml/parser.py b/launch_xml/launch_xml/parser.py index a1eb34ee0..6745d2d97 100644 --- a/launch_xml/launch_xml/parser.py +++ b/launch_xml/launch_xml/parser.py @@ -32,4 +32,4 @@ def load( file: Union[str, io.TextIOBase], ) -> (Entity, 'Parser'): """Return entity loaded with markup file.""" - return (Entity(ET.parse(file).getroot()), cls) + return (Entity(ET.parse(file).getroot()), cls()) diff --git a/launch_xml/package.xml b/launch_xml/package.xml index ec054971f..ba75c6a15 100644 --- a/launch_xml/package.xml +++ b/launch_xml/package.xml @@ -3,7 +3,7 @@ launch_xml 0.8.1 - The ROS launch XML frontend. + XML frontend for the launch package. Ivan Paunovic Apache License 2.0 diff --git a/launch_xml/setup.py b/launch_xml/setup.py index e36fb0e70..f3ce5973c 100644 --- a/launch_xml/setup.py +++ b/launch_xml/setup.py @@ -20,7 +20,7 @@ 'Programming Language :: Python', 'Topic :: Software Development', ], - description='XML `launch` front-end extension.', + description='XML frontend for the `launch` package.', long_description=( 'This package provides XML parsing ability to `launch-frontend` package.' ), diff --git a/launch_yaml/launch_yaml/parser.py b/launch_yaml/launch_yaml/parser.py index b8b4a2901..f56d8d9ec 100644 --- a/launch_yaml/launch_yaml/parser.py +++ b/launch_yaml/launch_yaml/parser.py @@ -40,4 +40,4 @@ def load( if len(tree) != 1: raise RuntimeError('Expected only one root') type_name = list(tree.keys())[0] - return (Entity(tree[type_name], type_name), cls) + return (Entity(tree[type_name], type_name), cls()) diff --git a/launch_yaml/package.xml b/launch_yaml/package.xml index 17bd7c6ec..f38d978ad 100644 --- a/launch_yaml/package.xml +++ b/launch_yaml/package.xml @@ -3,7 +3,7 @@ launch_yaml 0.8.1 - The ROS launch YAML frontend. + YAML frontend for the launch package. Ivan Paunovic Apache License 2.0 diff --git a/launch_yaml/setup.py b/launch_yaml/setup.py index ae3cf5a1d..737ae3fea 100644 --- a/launch_yaml/setup.py +++ b/launch_yaml/setup.py @@ -20,7 +20,7 @@ 'Programming Language :: Python', 'Topic :: Software Development', ], - description='YAML `launch` front-end extension.', + description='YAML frontend for the `launch` package.', long_description=( 'This package provides YAML parsing ability to `launch-frontend` package.' ), From e7d190f7a555fce91b92e72bc218546a5ba28187 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 18 Jun 2019 13:57:37 -0300 Subject: [PATCH 44/75] Add parsing function for include and group actions. Added related tests Signed-off-by: ivanpauno --- .../launch_description_sources/__init__.py | 8 +++ .../any_launch_description_source.py | 69 +++++++++++++++++++ .../frontend_launch_description_source.py | 57 +++++++++++++++ .../xml_launch_description_source.py | 46 +++++++++++++ .../yaml_launch_description_source.py | 46 +++++++++++++ .../launch_frontend/action_parse_methods.py | 34 +++++++++ launch_xml/test/launch_xml/test_group.py | 46 +++++++++++++ launch_xml/test/launch_xml/test_include.py | 44 ++++++++++++ launch_yaml/README.md | 16 +++++ launch_yaml/launch_yaml/entity.py | 14 +++- launch_yaml/test/launch_yaml/test_group.py | 50 ++++++++++++++ 11 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 launch/launch/launch_description_sources/any_launch_description_source.py create mode 100644 launch/launch/launch_description_sources/frontend_launch_description_source.py create mode 100644 launch/launch/launch_description_sources/xml_launch_description_source.py create mode 100644 launch/launch/launch_description_sources/yaml_launch_description_source.py create mode 100644 launch_xml/test/launch_xml/test_group.py create mode 100644 launch_xml/test/launch_xml/test_include.py create mode 100644 launch_yaml/test/launch_yaml/test_group.py diff --git a/launch/launch/launch_description_sources/__init__.py b/launch/launch/launch_description_sources/__init__.py index 88f88e181..a4521d7c3 100644 --- a/launch/launch/launch_description_sources/__init__.py +++ b/launch/launch/launch_description_sources/__init__.py @@ -14,7 +14,11 @@ """Package for launch_description_sources.""" +from .any_launch_description_source import AnyLaunchDescriptionSource +from .frontend_launch_description_source import FrontendLaunchDescriptionSource from .python_launch_description_source import PythonLaunchDescriptionSource +from .xml_launch_description_source import XMLLaunchDescriptionSource +from .yaml_launch_description_source import YAMLLaunchDescriptionSource from .python_launch_file_utilities import get_launch_description_from_python_launch_file from .python_launch_file_utilities import InvalidPythonLaunchFileError from .python_launch_file_utilities import load_python_launch_file_as_module @@ -23,5 +27,9 @@ 'get_launch_description_from_python_launch_file', 'InvalidPythonLaunchFileError', 'load_python_launch_file_as_module', + 'AnyLaunchDescriptionSource', + 'FrontendLaunchDescriptionSource', 'PythonLaunchDescriptionSource', + 'XMLLaunchDescriptionSource', + 'YAMLLaunchDescriptionSource', ] diff --git a/launch/launch/launch_description_sources/any_launch_description_source.py b/launch/launch/launch_description_sources/any_launch_description_source.py new file mode 100644 index 000000000..ad422b3ec --- /dev/null +++ b/launch/launch/launch_description_sources/any_launch_description_source.py @@ -0,0 +1,69 @@ +# Copyright 2019 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. + +"""Module for the AnyLaunchDescriptionSource class.""" + +from launch_frontend import Parser + +from .python_launch_file_utilities import get_launch_description_from_python_launch_file +from .python_launch_file_utilities import InvalidPythonLaunchFileError +from ..launch_description_source import LaunchDescriptionSource +from ..some_substitutions_type import SomeSubstitutionsType + + +class AnyLaunchDescriptionSource(LaunchDescriptionSource): + """ + Encapsulation of a launch file, which can be loaded during launch. + + This try to load the file as a python launch file, or as a frontend launch file. + It is recommended to use a specific `LaunchDescriptionSource` subclasses when possible. + """ + + def __init__( + self, + launch_file_path: SomeSubstitutionsType, + ) -> None: + """ + Constructor. + + The given file path should be to a launch file of any style. + The path should probably be absolute, since the current working + directory will be wherever the launch file was run from, which might + change depending on the situation. + The path can be made up of Substitution instances which are expanded + when :py:meth:`get_launch_description()` is called. + + :param launch_file_path: the path to the launch file + """ + super().__init__( + None, + launch_file_path, + 'interpreted launch file', + ) + + def _get_launch_description(self, location): + """Get the LaunchDescription from location.""" + launch_description = None + try: + launch_description = get_launch_description_from_python_launch_file(location) + except InvalidPythonLaunchFileError: + pass + try: + root_entity, parser = Parser.load(location) + launch_description = parser.parse_description(root_entity) + except RuntimeError: + pass + if launch_description is None: + raise RuntimeError('Can not load launch file') + return launch_description diff --git a/launch/launch/launch_description_sources/frontend_launch_description_source.py b/launch/launch/launch_description_sources/frontend_launch_description_source.py new file mode 100644 index 000000000..44bdf38c8 --- /dev/null +++ b/launch/launch/launch_description_sources/frontend_launch_description_source.py @@ -0,0 +1,57 @@ +# Copyright 2019 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. + +"""Module for the FrontendLaunchDescriptionSource class.""" + +from typing import Type + +from launch_frontend import Parser + +from ..launch_description_source import LaunchDescriptionSource +from ..some_substitutions_type import SomeSubstitutionsType + + +class FrontendLaunchDescriptionSource(LaunchDescriptionSource): + """Encapsulation of a FrontEnd launch file, which can be loaded during launch.""" + + def __init__( + self, + launch_file_path: SomeSubstitutionsType, + *, + method: str = 'interpreted frontend launch file', + parser: Type[Parser] = Parser + ) -> None: + """ + Constructor. + + The given file path should be to a launch frontend style file (like xml or yaml). + The path should probably be absolute, since the current working + directory will be wherever the launch file was run from, which might + change depending on the situation. + The path can be made up of Substitution instances which are expanded + when :py:meth:`get_launch_description()` is called. + + :param launch_file_path: the path to the launch file + :param parser: an specific parser implementation + """ + super().__init__( + None, + launch_file_path, + method + ) + + def _get_launch_description(self, location): + """Get the LaunchDescription from location.""" + root_entity, parser = Parser.load(location) + return parser.parse_description(root_entity) diff --git a/launch/launch/launch_description_sources/xml_launch_description_source.py b/launch/launch/launch_description_sources/xml_launch_description_source.py new file mode 100644 index 000000000..79f5fa5e5 --- /dev/null +++ b/launch/launch/launch_description_sources/xml_launch_description_source.py @@ -0,0 +1,46 @@ +# Copyright 2019 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. + +"""Module for the XMLLaunchDescriptionSource class.""" + +from launch_xml import Parser + +from . import FrontendLaunchDescriptionSource +from ..some_substitutions_type import SomeSubstitutionsType + + +class XMLLaunchDescriptionSource(FrontendLaunchDescriptionSource): + """Encapsulation of a XML launch file, which can be loaded during launch.""" + + def __init__( + self, + launch_file_path: SomeSubstitutionsType, + ) -> None: + """ + Constructor. + + The given file path should be to a launch XML style file (`.launch.xml`). + The path should probably be absolute, since the current working + directory will be wherever the launch file was run from, which might + change depending on the situation. + The path can be made up of Substitution instances which are expanded + when :py:meth:`get_launch_description()` is called. + + :param launch_file_path: the path to the launch file + """ + super().__init__( + launch_file_path, + method='interpreted XML launch file', + parser=Parser + ) diff --git a/launch/launch/launch_description_sources/yaml_launch_description_source.py b/launch/launch/launch_description_sources/yaml_launch_description_source.py new file mode 100644 index 000000000..85af3f936 --- /dev/null +++ b/launch/launch/launch_description_sources/yaml_launch_description_source.py @@ -0,0 +1,46 @@ +# Copyright 2019 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. + +"""Module for the YAMLLaunchDescriptionSource class.""" + +from launch_yaml import Parser + +from . import FrontendLaunchDescriptionSource +from ..some_substitutions_type import SomeSubstitutionsType + + +class YAMLLaunchDescriptionSource(FrontendLaunchDescriptionSource): + """Encapsulation of a YAML launch file, which can be loaded during launch.""" + + def __init__( + self, + launch_file_path: SomeSubstitutionsType, + ) -> None: + """ + Constructor. + + The given file path should be to a launch YAML style file (`.launch.yaml`). + The path should probably be absolute, since the current working + directory will be wherever the launch file was run from, which might + change depending on the situation. + The path can be made up of Substitution instances which are expanded + when :py:meth:`get_launch_description()` is called. + + :param launch_file_path: the path to the launch file + """ + super().__init__( + launch_file_path, + method='interpreted YAML launch file', + parser=Parser + ) diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index e96f2af4f..7dbefad1a 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -119,3 +119,37 @@ def parse_let(entity: Entity, parser: Parser): kwargs['name'] = name kwargs['value'] = value return launch.actions.SetLaunchConfiguration, kwargs + + +@expose_action('include') +def parse_include(entity: Entity, parser: Parser): + """Parse include tag.""" + # import here for avoiding recursive import + # TODO(ivanpauno): Put it at the top of the file after merging launch and + # launch_frontend packages. + from launch.launch_description_sources import AnyLaunchDescriptionSource + _, kwargs = parse_action(entity, parser) + file = parser.parse_substitution(entity.get_attr('file')) + kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) + args = entity.get_attr('arg', types='list[Entity]', optional=True) + if args is not None: + args = [ + ( + parser.parse_substitution(e.get_attr('name')), + parser.parse_substitution(e.get_attr('value')) + ) + for e in args + ] + kwargs['launch_arguments'] = args + return launch.actions.IncludeLaunchDescription, kwargs + + +@expose_action('group') +def parse_group(entity: Entity, parser: Parser): + """Parse include tag.""" + _, kwargs = parse_action(entity, parser) + scoped = entity.get_attr('scoped', types='bool', optional=True) + if scoped is not None: + kwargs['scoped'] = scoped + kwargs['actions'] = [parser.parse_action(e) for e in entity.children] + return launch.actions.GroupAction, kwargs diff --git a/launch_xml/test/launch_xml/test_group.py b/launch_xml/test/launch_xml/test_group.py new file mode 100644 index 000000000..492afaafa --- /dev/null +++ b/launch_xml/test/launch_xml/test_group.py @@ -0,0 +1,46 @@ +# Copyright 2019 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. + +"""Test parsing a group action.""" + +import io +import textwrap + +from launch.actions import SetLaunchConfiguration + +from launch_frontend import Parser + + +def test_group(): + xml_file = \ + """\ + + + + + + + """ # noqa: E501 + xml_file = textwrap.dedent(xml_file) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + ld = parser.parse_description(root_entity) + group = ld.entities[0] + actions = group.execute(None) + assert 2 == len(actions) + assert isinstance(actions[0], SetLaunchConfiguration) + assert isinstance(actions[1], SetLaunchConfiguration) + + +if __name__ == '__main__': + test_group() diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py new file mode 100644 index 000000000..02eeb14cb --- /dev/null +++ b/launch_xml/test/launch_xml/test_include.py @@ -0,0 +1,44 @@ +# Copyright 2019 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. + +"""Test parsing an include action.""" + +import io +import textwrap + +from launch.actions import IncludeLaunchDescription +from launch.launch_description_sources import AnyLaunchDescriptionSource + +from launch_frontend import Parser + + +def test_include(): + """Parse node xml example.""" + xml_file = \ + """\ + + + + """ # noqa: E501 + xml_file = textwrap.dedent(xml_file) + root_entity, parser = Parser.load(io.StringIO(xml_file)) + ld = parser.parse_description(root_entity) + include = ld.entities[0] + assert isinstance(include, IncludeLaunchDescription) + assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource) + # TODO(ivanpauno): Load something really + + +if __name__ == '__main__': + test_include() diff --git a/launch_yaml/README.md b/launch_yaml/README.md index 2d5c984cc..d324e7977 100644 --- a/launch_yaml/README.md +++ b/launch_yaml/README.md @@ -95,6 +95,22 @@ group: e.children ``` +or: + +```yaml +group: + scoped: False + children: + - executable: + cmd: ls + - executable: + cmd: ps +``` + +```python +e.children +``` + It returns a list of launch_xml.Entity wrapping each of the xml children. In the example, the list has two `Entity` objects wrapping each of the `executable` tags. diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index 71f1c6848..cbd7c26b5 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -52,10 +52,18 @@ def parent(self) -> Optional['Entity']: @property def children(self) -> List['Entity']: """Get the Entity's children.""" - if not isinstance(self.__element, list): - raise TypeError('Expected a list, got {}'.format(type(self.element))) + if not type(self.__element) in (dict, list): + raise TypeError('Expected a dict or list, got {}'.format(type(self.element))) + if isinstance(self.__element, dict): + if 'children' not in self.__element: + raise KeyError('Missing `children` key in Entity {}'.format(self.__type_name)) + children = self.__element['children'] + else: + children = self.__element + if not isinstance(children, list): + raise TypeError('children should be a list') entities = [] - for child in self.__element: + for child in children: if len(child) != 1: raise RuntimeError('Expected one root per child') type_name = list(child.keys())[0] diff --git a/launch_yaml/test/launch_yaml/test_group.py b/launch_yaml/test/launch_yaml/test_group.py new file mode 100644 index 000000000..06cff3ce6 --- /dev/null +++ b/launch_yaml/test/launch_yaml/test_group.py @@ -0,0 +1,50 @@ +# Copyright 2019 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. + +"""Test parsing a group action.""" + +import io +import textwrap + +from launch.actions import SetLaunchConfiguration + +from launch_frontend import Parser + + +def test_group(): + yaml_file = \ + """\ + launch: + - group: + scoped: False + children: + - let: + name: 'var1' + value: 'asd' + - let: + name: 'var2' + value: 'asd' + """ # noqa: E501 + yaml_file = textwrap.dedent(yaml_file) + root_entity, parser = Parser.load(io.StringIO(yaml_file)) + ld = parser.parse_description(root_entity) + group = ld.entities[0] + actions = group.execute(None) + assert 2 == len(actions) + assert isinstance(actions[0], SetLaunchConfiguration) + assert isinstance(actions[1], SetLaunchConfiguration) + + +if __name__ == '__main__': + test_group() From 8764df0139e9d61bcebc27f0344e00ccfff54f65 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 25 Jun 2019 15:51:36 -0300 Subject: [PATCH 45/75] Address per review comments Signed-off-by: ivanpauno --- .../launch_description_sources/__init__.py | 8 ------ launch_frontend/launch_frontend/__init__.py | 2 ++ .../launch_frontend/action_parse_methods.py | 7 +++-- .../launch_description_sources/__init__.py | 24 +++++++++++++++++ .../any_launch_description_source.py | 26 +++++++++---------- .../frontend_launch_description_source.py | 22 +++++++++------- launch_xml/launch_xml/__init__.py | 2 ++ .../launch_description_sources/__init__.py | 21 +++++++++++++++ .../xml_launch_description_source.py | 17 ++++++------ launch_xml/launch_xml/parser.py | 2 +- launch_xml/test/launch_xml/test_executable.py | 1 - launch_xml/test/launch_xml/test_include.py | 2 +- launch_yaml/launch_yaml/__init__.py | 4 ++- .../launch_description_sources/__init__.py | 21 +++++++++++++++ .../yaml_launch_description_source.py | 18 ++++++------- launch_yaml/launch_yaml/parser.py | 3 +-- .../test/launch_yaml/test_executable.py | 1 - 17 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 launch_frontend/launch_frontend/launch_description_sources/__init__.py rename {launch/launch => launch_frontend/launch_frontend}/launch_description_sources/any_launch_description_source.py (66%) rename {launch/launch => launch_frontend/launch_frontend}/launch_description_sources/frontend_launch_description_source.py (69%) create mode 100644 launch_xml/launch_xml/launch_description_sources/__init__.py rename {launch/launch => launch_xml/launch_xml}/launch_description_sources/xml_launch_description_source.py (69%) create mode 100644 launch_yaml/launch_yaml/launch_description_sources/__init__.py rename {launch/launch => launch_yaml/launch_yaml}/launch_description_sources/yaml_launch_description_source.py (69%) diff --git a/launch/launch/launch_description_sources/__init__.py b/launch/launch/launch_description_sources/__init__.py index a4521d7c3..88f88e181 100644 --- a/launch/launch/launch_description_sources/__init__.py +++ b/launch/launch/launch_description_sources/__init__.py @@ -14,11 +14,7 @@ """Package for launch_description_sources.""" -from .any_launch_description_source import AnyLaunchDescriptionSource -from .frontend_launch_description_source import FrontendLaunchDescriptionSource from .python_launch_description_source import PythonLaunchDescriptionSource -from .xml_launch_description_source import XMLLaunchDescriptionSource -from .yaml_launch_description_source import YAMLLaunchDescriptionSource from .python_launch_file_utilities import get_launch_description_from_python_launch_file from .python_launch_file_utilities import InvalidPythonLaunchFileError from .python_launch_file_utilities import load_python_launch_file_as_module @@ -27,9 +23,5 @@ 'get_launch_description_from_python_launch_file', 'InvalidPythonLaunchFileError', 'load_python_launch_file_as_module', - 'AnyLaunchDescriptionSource', - 'FrontendLaunchDescriptionSource', 'PythonLaunchDescriptionSource', - 'XMLLaunchDescriptionSource', - 'YAMLLaunchDescriptionSource', ] diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index 814d60518..ad9e8c676 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -17,6 +17,7 @@ # All files containing parsing methods should be imported here. # If not, the action or substitution are not going to be exposed. from . import action_parse_methods # noqa: F401 +from . import launch_description_sources from . import substitution_parse_methods # noqa: F401 from . import type_utils from .entity import Entity @@ -32,5 +33,6 @@ 'expose_substitution', 'Parser', # Modules + 'launch_description_sources', 'type_utils', ] diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py index 7dbefad1a..af8d07580 100644 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ b/launch_frontend/launch_frontend/action_parse_methods.py @@ -30,7 +30,7 @@ def parse_action(entity: Entity, parser: Parser): """ Parse action. - This is only intented for code reusage, and it's not exposed. + This is only intended for code reuse, and it's not exposed. """ if_cond = entity.get_attr('if', optional=True) unless_cond = entity.get_attr('unless', optional=True) @@ -127,20 +127,19 @@ def parse_include(entity: Entity, parser: Parser): # import here for avoiding recursive import # TODO(ivanpauno): Put it at the top of the file after merging launch and # launch_frontend packages. - from launch.launch_description_sources import AnyLaunchDescriptionSource + from .launch_description_sources import AnyLaunchDescriptionSource _, kwargs = parse_action(entity, parser) file = parser.parse_substitution(entity.get_attr('file')) kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) args = entity.get_attr('arg', types='list[Entity]', optional=True) if args is not None: - args = [ + kwargs['launch_arguments'] = [ ( parser.parse_substitution(e.get_attr('name')), parser.parse_substitution(e.get_attr('value')) ) for e in args ] - kwargs['launch_arguments'] = args return launch.actions.IncludeLaunchDescription, kwargs diff --git a/launch_frontend/launch_frontend/launch_description_sources/__init__.py b/launch_frontend/launch_frontend/launch_description_sources/__init__.py new file mode 100644 index 000000000..ca6d93ff1 --- /dev/null +++ b/launch_frontend/launch_frontend/launch_description_sources/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2018 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. + +"""Package for launch_description_sources.""" + +from .any_launch_description_source import AnyLaunchDescriptionSource +from .frontend_launch_description_source import FrontendLaunchDescriptionSource + + +__all__ = [ + 'AnyLaunchDescriptionSource', + 'FrontendLaunchDescriptionSource', +] diff --git a/launch/launch/launch_description_sources/any_launch_description_source.py b/launch_frontend/launch_frontend/launch_description_sources/any_launch_description_source.py similarity index 66% rename from launch/launch/launch_description_sources/any_launch_description_source.py rename to launch_frontend/launch_frontend/launch_description_sources/any_launch_description_source.py index ad422b3ec..23077ed84 100644 --- a/launch/launch/launch_description_sources/any_launch_description_source.py +++ b/launch_frontend/launch_frontend/launch_description_sources/any_launch_description_source.py @@ -14,19 +14,22 @@ """Module for the AnyLaunchDescriptionSource class.""" -from launch_frontend import Parser +from launch import SomeSubstitutionsType +from launch.launch_description_source import LaunchDescriptionSource +from launch.launch_description_sources.python_launch_file_utilities import \ + get_launch_description_from_python_launch_file +from launch.launch_description_sources.python_launch_file_utilities import \ + InvalidPythonLaunchFileError -from .python_launch_file_utilities import get_launch_description_from_python_launch_file -from .python_launch_file_utilities import InvalidPythonLaunchFileError -from ..launch_description_source import LaunchDescriptionSource -from ..some_substitutions_type import SomeSubstitutionsType +from ..parser import Parser class AnyLaunchDescriptionSource(LaunchDescriptionSource): """ Encapsulation of a launch file, which can be loaded during launch. - This try to load the file as a python launch file, or as a frontend launch file. + This launch description source will attempt to load the file at the given location as a python + launch file first, and as a declarative (markup based) launch file if the former fails. It is recommended to use a specific `LaunchDescriptionSource` subclasses when possible. """ @@ -37,14 +40,11 @@ def __init__( """ Constructor. - The given file path should be to a launch file of any style. - The path should probably be absolute, since the current working - directory will be wherever the launch file was run from, which might - change depending on the situation. - The path can be made up of Substitution instances which are expanded - when :py:meth:`get_launch_description()` is called. + If a relative path is passed, it will be relative to the current working + directory wherever the launch file was run from. - :param launch_file_path: the path to the launch file + :param launch_file_path: the path to the launch file. It can be made up of Substitution + instances which are expanded when :py:meth:`get_launch_description()` is called. """ super().__init__( None, diff --git a/launch/launch/launch_description_sources/frontend_launch_description_source.py b/launch_frontend/launch_frontend/launch_description_sources/frontend_launch_description_source.py similarity index 69% rename from launch/launch/launch_description_sources/frontend_launch_description_source.py rename to launch_frontend/launch_frontend/launch_description_sources/frontend_launch_description_source.py index 44bdf38c8..a48c129bf 100644 --- a/launch/launch/launch_description_sources/frontend_launch_description_source.py +++ b/launch_frontend/launch_frontend/launch_description_sources/frontend_launch_description_source.py @@ -16,14 +16,18 @@ from typing import Type -from launch_frontend import Parser +from launch.launch_description_source import LaunchDescriptionSource +from launch.some_substitutions_type import SomeSubstitutionsType -from ..launch_description_source import LaunchDescriptionSource -from ..some_substitutions_type import SomeSubstitutionsType +from ..parser import Parser class FrontendLaunchDescriptionSource(LaunchDescriptionSource): - """Encapsulation of a FrontEnd launch file, which can be loaded during launch.""" + """ + Encapsulation of a declarative (markup based) launch file. + + It can be loaded during launch using an `IncludeLaunchDescription` action. + """ def __init__( self, @@ -36,13 +40,11 @@ def __init__( Constructor. The given file path should be to a launch frontend style file (like xml or yaml). - The path should probably be absolute, since the current working - directory will be wherever the launch file was run from, which might - change depending on the situation. - The path can be made up of Substitution instances which are expanded - when :py:meth:`get_launch_description()` is called. + If a relative path is passed, it will be relative to the current working + directory wherever the launch file was run from. - :param launch_file_path: the path to the launch file + :param launch_file_path: the path to the launch file. It can be made up of Substitution + instances which are expanded when :py:meth:`get_launch_description()` is called. :param parser: an specific parser implementation """ super().__init__( diff --git a/launch_xml/launch_xml/__init__.py b/launch_xml/launch_xml/__init__.py index f3d901609..e97481c89 100644 --- a/launch_xml/launch_xml/__init__.py +++ b/launch_xml/launch_xml/__init__.py @@ -14,10 +14,12 @@ """Main entry point for the `launch_xml` package.""" +from . import launch_description_sources from .entity import Entity from .parser import Parser __all__ = [ + 'launch_description_sources', 'Entity', 'Parser', ] diff --git a/launch_xml/launch_xml/launch_description_sources/__init__.py b/launch_xml/launch_xml/launch_description_sources/__init__.py new file mode 100644 index 000000000..fabbfc7f1 --- /dev/null +++ b/launch_xml/launch_xml/launch_description_sources/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2019 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. + +"""Package for launch_description_sources.""" + +from .xml_launch_description_source import XMLLaunchDescriptionSource + +__all__ = [ + 'XMLLaunchDescriptionSource', +] diff --git a/launch/launch/launch_description_sources/xml_launch_description_source.py b/launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py similarity index 69% rename from launch/launch/launch_description_sources/xml_launch_description_source.py rename to launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py index 79f5fa5e5..db5c65fd5 100644 --- a/launch/launch/launch_description_sources/xml_launch_description_source.py +++ b/launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py @@ -14,10 +14,11 @@ """Module for the XMLLaunchDescriptionSource class.""" -from launch_xml import Parser +from launch import SomeSubstitutionsType -from . import FrontendLaunchDescriptionSource -from ..some_substitutions_type import SomeSubstitutionsType +from launch_frontend.launch_description_sources import FrontendLaunchDescriptionSource + +from ..parser import Parser class XMLLaunchDescriptionSource(FrontendLaunchDescriptionSource): @@ -31,13 +32,11 @@ def __init__( Constructor. The given file path should be to a launch XML style file (`.launch.xml`). - The path should probably be absolute, since the current working - directory will be wherever the launch file was run from, which might - change depending on the situation. - The path can be made up of Substitution instances which are expanded - when :py:meth:`get_launch_description()` is called. + If a relative path is passed, it will be relative to the current working + directory wherever the launch file was run from. - :param launch_file_path: the path to the launch file + :param launch_file_path: the path to the launch file. It can be made up of Substitution + instances which are expanded when :py:meth:`get_launch_description()` is called. """ super().__init__( launch_file_path, diff --git a/launch_xml/launch_xml/parser.py b/launch_xml/launch_xml/parser.py index 6745d2d97..6ffb6f63e 100644 --- a/launch_xml/launch_xml/parser.py +++ b/launch_xml/launch_xml/parser.py @@ -31,5 +31,5 @@ def load( cls, file: Union[str, io.TextIOBase], ) -> (Entity, 'Parser'): - """Return entity loaded with markup file.""" + """Return entity loaded from XML file.""" return (Entity(ET.parse(file).getroot()), cls()) diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 1ffef523e..25b0079d1 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -48,7 +48,6 @@ def test_executable(): value = value[0].perform(None) assert(key == 'var') assert(value == '1') - # assert(executable.prefix[0].perform(None) == 'time') ls = LaunchService() ls.include_launch_description(ld) assert(0 == ls.run()) diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py index 02eeb14cb..8db3508fc 100644 --- a/launch_xml/test/launch_xml/test_include.py +++ b/launch_xml/test/launch_xml/test_include.py @@ -18,9 +18,9 @@ import textwrap from launch.actions import IncludeLaunchDescription -from launch.launch_description_sources import AnyLaunchDescriptionSource from launch_frontend import Parser +from launch_frontend.launch_description_sources import AnyLaunchDescriptionSource def test_include(): diff --git a/launch_yaml/launch_yaml/__init__.py b/launch_yaml/launch_yaml/__init__.py index f3d901609..67f899355 100644 --- a/launch_yaml/launch_yaml/__init__.py +++ b/launch_yaml/launch_yaml/__init__.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Main entry point for the `launch_xml` package.""" +"""Package for launch_description_sources.""" +from . import launch_description_sources from .entity import Entity from .parser import Parser __all__ = [ + 'launch_description_sources', 'Entity', 'Parser', ] diff --git a/launch_yaml/launch_yaml/launch_description_sources/__init__.py b/launch_yaml/launch_yaml/launch_description_sources/__init__.py new file mode 100644 index 000000000..1863e67ac --- /dev/null +++ b/launch_yaml/launch_yaml/launch_description_sources/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2019 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. + +"""Package for launch_description_sources.""" + +from .yaml_launch_description_source import YAMLLaunchDescriptionSource + +__all__ = [ + 'YAMLLaunchDescriptionSource', +] diff --git a/launch/launch/launch_description_sources/yaml_launch_description_source.py b/launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py similarity index 69% rename from launch/launch/launch_description_sources/yaml_launch_description_source.py rename to launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py index 85af3f936..74afc9c67 100644 --- a/launch/launch/launch_description_sources/yaml_launch_description_source.py +++ b/launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py @@ -14,10 +14,11 @@ """Module for the YAMLLaunchDescriptionSource class.""" -from launch_yaml import Parser +from launch import SomeSubstitutionsType -from . import FrontendLaunchDescriptionSource -from ..some_substitutions_type import SomeSubstitutionsType +from launch_frontend.launch_description_sources import FrontendLaunchDescriptionSource + +from ..parser import Parser class YAMLLaunchDescriptionSource(FrontendLaunchDescriptionSource): @@ -31,13 +32,12 @@ def __init__( Constructor. The given file path should be to a launch YAML style file (`.launch.yaml`). - The path should probably be absolute, since the current working - directory will be wherever the launch file was run from, which might - change depending on the situation. - The path can be made up of Substitution instances which are expanded - when :py:meth:`get_launch_description()` is called. + If a relative path is passed, it will be relative to the current working + directory wherever the launch file was run from. - :param launch_file_path: the path to the launch file + :param launch_file_path: the path to the launch file. It path can be made up of + Substitution instances which are expanded when :py:meth:`get_launch_description()` + is called. """ super().__init__( launch_file_path, diff --git a/launch_yaml/launch_yaml/parser.py b/launch_yaml/launch_yaml/parser.py index f56d8d9ec..b9acf5904 100644 --- a/launch_yaml/launch_yaml/parser.py +++ b/launch_yaml/launch_yaml/parser.py @@ -32,10 +32,9 @@ def load( cls, stream: Union[str, io.TextIOBase], ) -> (Entity, 'Parser'): - """Load a YAML launch file.""" + """Return entity loaded from YAML file.""" if isinstance(stream, str): stream = open(stream, 'r') - """Return entity loaded with markup file.""" tree = yaml.safe_load(stream) if len(tree) != 1: raise RuntimeError('Expected only one root') diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py index afe61869d..c5b984501 100644 --- a/launch_yaml/test/launch_yaml/test_executable.py +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -55,7 +55,6 @@ def test_executable(): value = value[0].perform(None) assert(key == 'var') assert(value == '1') - # assert(executable.prefix[0].perform(None) == 'time') ls = LaunchService() ls.include_launch_description(ld) assert(0 == ls.run()) From be1e683cd5707ce95abffba71dd8542e14e23279 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 28 Jun 2019 16:54:37 -0300 Subject: [PATCH 46/75] Address review comments Signed-off-by: ivanpauno --- launch/launch/actions/execute_process.py | 2 +- launch/launch/launch_service.py | 5 +++++ launch_frontend/launch_frontend/__init__.py | 5 +---- launch_frontend/launch_frontend/type_utils.py | 16 ++++++++-------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 52f0400c1..9a3276589 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -640,5 +640,5 @@ def shell(self): @property def prefix(self): - """Getter for shell.""" + """Getter for prefix.""" return self.__prefix diff --git a/launch/launch/launch_service.py b/launch/launch/launch_service.py index 205132a94..b6f7c99a7 100644 --- a/launch/launch/launch_service.py +++ b/launch/launch/launch_service.py @@ -421,3 +421,8 @@ def shutdown(self) -> None: if self.__loop_from_run_thread is not None: ret = self._shutdown(reason='LaunchService.shutdown() called', due_to_sigint=False) assert ret is None, ret + + @property + def context(self): + """Getter for context.""" + return self.__context diff --git a/launch_frontend/launch_frontend/__init__.py b/launch_frontend/launch_frontend/__init__.py index ad9e8c676..295c7e2b7 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch_frontend/launch_frontend/__init__.py @@ -26,13 +26,10 @@ __all__ = [ - # Classes 'Entity', - # Decorators + 'Parser', 'expose_action', 'expose_substitution', - 'Parser', - # Modules 'launch_description_sources', 'type_utils', ] diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch_frontend/launch_frontend/type_utils.py index bebb14212..400ee9dc0 100644 --- a/launch_frontend/launch_frontend/type_utils.py +++ b/launch_frontend/launch_frontend/type_utils.py @@ -30,7 +30,7 @@ def extract_type(name: Text): """ Extract type information from string. - `name` can be one of: + :param name: a string specifying a type. can be one of: - 'str' - 'int' - 'float' @@ -40,13 +40,13 @@ def extract_type(name: Text): - 'list[float]' - 'list[bool]' - Returns a tuple (type_obj, is_list). - is_list is `True` for the supported list types, if not is `False`. - type_obj is the object representing that type in python. In the case of list - is the type of the items. - e.g.: - name = 'list[int]' -> (int, True) - name = 'bool' -> (bool, False) + :returns: a tuple (type_obj, is_list). + is_list is `True` for the supported list types, if not is `False`. + type_obj is the object representing that type in python. In the case of list + is the type of the items. + e.g.: + name = 'list[int]' -> (int, True) + name = 'bool' -> (bool, False) """ error = ValueError('Unrecognized type name: {}'.format(name)) is_list = False From 68dc4879e031aeb73bd0e31e3e7b56d6bae3c123 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 28 Jun 2019 17:50:45 -0300 Subject: [PATCH 47/75] Merged launch_frontend into launch package Signed-off-by: ivanpauno --- launch/launch/__init__.py | 2 + launch/launch/action.py | 26 +++ launch/launch/actions/execute_process.py | 70 ++++++++ launch/launch/actions/group_action.py | 14 ++ .../actions/include_launch_description.py | 25 +++ .../actions/set_launch_configuration.py | 14 ++ .../launch_description_sources/__init__.py | 4 + .../any_launch_description_source.py | 13 +- .../frontend_launch_description_source.py | 7 +- .../launch}/launch_frontend/__init__.py | 6 - .../launch}/launch_frontend/entity.py | 0 .../launch}/launch_frontend/expose.py | 0 .../launch}/launch_frontend/grammar.lark | 0 .../launch_frontend/parse_substitution.py | 5 +- .../launch}/launch_frontend/parser.py | 15 +- .../launch}/launch_frontend/type_utils.py | 0 .../substitutions/environment_variable.py | 14 ++ .../launch/substitutions/find_executable.py | 12 ++ .../substitutions/launch_configuration.py | 13 ++ ...normalize_to_list_of_substitutions_impl.py | 4 +- .../launch_frontend/test_expose_decorators.py | 2 +- .../launch_frontend/test_substitutions.py | 5 +- .../launch_frontend/action_parse_methods.py | 154 ------------------ .../launch_description_sources/__init__.py | 24 --- .../substitution_parse_methods.py | 56 ------- launch_frontend/package.xml | 20 --- launch_frontend/setup.py | 29 ---- launch_frontend/test/test_copyright.py | 23 --- launch_frontend/test/test_flake8.py | 23 --- launch_frontend/test/test_pep257.py | 23 --- launch_xml/launch_xml/entity.py | 4 +- .../xml_launch_description_source.py | 3 +- launch_xml/launch_xml/parser.py | 2 +- launch_xml/test/launch_xml/test_executable.py | 3 +- launch_xml/test/launch_xml/test_group.py | 3 +- launch_xml/test/launch_xml/test_include.py | 5 +- launch_xml/test/launch_xml/test_let_var.py | 3 +- launch_xml/test/launch_xml/test_list.py | 2 +- launch_yaml/launch_yaml/entity.py | 4 +- .../yaml_launch_description_source.py | 3 +- launch_yaml/launch_yaml/parser.py | 2 +- .../test/launch_yaml/test_executable.py | 3 +- launch_yaml/test/launch_yaml/test_group.py | 3 +- 43 files changed, 234 insertions(+), 409 deletions(-) rename {launch_frontend/launch_frontend => launch/launch}/launch_description_sources/any_launch_description_source.py (85%) rename {launch_frontend/launch_frontend => launch/launch}/launch_description_sources/frontend_launch_description_source.py (92%) rename {launch_frontend => launch/launch}/launch_frontend/__init__.py (74%) rename {launch_frontend => launch/launch}/launch_frontend/entity.py (100%) rename {launch_frontend => launch/launch}/launch_frontend/expose.py (100%) rename {launch_frontend => launch/launch}/launch_frontend/grammar.lark (100%) rename {launch_frontend => launch/launch}/launch_frontend/parse_substitution.py (96%) rename {launch_frontend => launch/launch}/launch_frontend/parser.py (90%) rename {launch_frontend => launch/launch}/launch_frontend/type_utils.py (100%) rename {launch_frontend/test => launch/test/launch}/launch_frontend/test_expose_decorators.py (96%) rename {launch_frontend/test => launch/test/launch}/launch_frontend/test_substitutions.py (97%) delete mode 100644 launch_frontend/launch_frontend/action_parse_methods.py delete mode 100644 launch_frontend/launch_frontend/launch_description_sources/__init__.py delete mode 100644 launch_frontend/launch_frontend/substitution_parse_methods.py delete mode 100644 launch_frontend/package.xml delete mode 100644 launch_frontend/setup.py delete mode 100644 launch_frontend/test/test_copyright.py delete mode 100644 launch_frontend/test/test_flake8.py delete mode 100644 launch_frontend/test/test_pep257.py diff --git a/launch/launch/__init__.py b/launch/launch/__init__.py index 72aeeecd6..aa76dc93f 100644 --- a/launch/launch/__init__.py +++ b/launch/launch/__init__.py @@ -17,6 +17,7 @@ from . import actions from . import conditions from . import events +from . import launch_frontend from . import logging from . import substitutions from .action import Action @@ -39,6 +40,7 @@ 'actions', 'conditions', 'events', + 'launch_frontend', 'logging', 'substitutions', 'Action', diff --git a/launch/launch/action.py b/launch/launch/action.py index 112c953e2..2d2a65ee5 100644 --- a/launch/launch/action.py +++ b/launch/launch/action.py @@ -23,6 +23,10 @@ from .launch_context import LaunchContext from .launch_description_entity import LaunchDescriptionEntity +if False: + from .launch_frontend import Entity + from .launch_frontend import Parser + class Action(LaunchDescriptionEntity): """ @@ -44,6 +48,28 @@ def __init__(self, *, condition: Optional[Condition] = None) -> None: """ self.__condition = condition + @staticmethod + def parse(entity: 'Entity', parser: 'Parser'): + """ + Return the `Action` action and kwargs for constructing it. + + This is only intended for code reuse. + This class is not exposed with `expose_action`. + """ + # Import here for avoiding cyclic imports. + from .conditions import IfCondition + from .conditions import UnlessCondition + if_cond = entity.get_attr('if', optional=True) + unless_cond = entity.get_attr('unless', optional=True) + kwargs = {} + if if_cond is not None and unless_cond is not None: + raise RuntimeError("if and unless conditions can't be used simultaneously") + if if_cond is not None: + kwargs['condition'] = IfCondition(predicate_expression=if_cond) + if unless_cond is not None: + kwargs['condition'] = UnlessCondition(predicate_expression=unless_cond) + return Action, kwargs + @property def condition(self) -> Optional[Condition]: """Getter for condition.""" diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 9a3276589..bb9e40ee7 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -60,11 +60,15 @@ from ..events.process import SignalProcess from ..launch_context import LaunchContext from ..launch_description import LaunchDescription +from ..launch_frontend import Entity +from ..launch_frontend import expose_action +from ..launch_frontend import Parser from ..some_actions_type import SomeActionsType from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution # noqa: F401 from ..substitutions import LaunchConfiguration from ..substitutions import PythonExpression +from ..substitutions import TextSubstitution from ..utilities import create_future from ..utilities import is_a_subclass from ..utilities import normalize_to_list_of_substitutions @@ -74,6 +78,7 @@ _global_process_counter = 0 # in Python3, this number is unbounded (no rollover) +@expose_action('executable') class ExecuteProcess(Action): """Action that begins executing a process and sets up event handlers for the process.""" @@ -225,6 +230,71 @@ def __init__( self.__stdout_buffer = io.StringIO() self.__stderr_buffer = io.StringIO() + @staticmethod + def parse( + entity: Entity, + parser: Parser, + optional_cmd: bool = False + ): + """ + Return the `ExecuteProcess` action and kwargs for constructing it. + + :param: optional_cmd Allow not specifying `cmd` argument. + Intended for code reuse in derived classes (e.g.: launch_ros.actions.Node). + """ + cmd = entity.get_attr('cmd', optional=optional_cmd) + if cmd is not None: + cmd_list = [parser.parse_substitution(cmd)] + else: + cmd_list = [] + kwargs = {} + cwd = entity.get_attr('cwd', optional=True) + if cwd is not None: + kwargs['cwd'] = parser.parse_substitution(cwd) + name = entity.get_attr('name', optional=True) + if name is not None: + kwargs['name'] = parser.parse_substitution(name) + prefix = entity.get_attr('launch-prefix', optional=True) + if prefix is not None: + kwargs['prefix'] = parser.parse_substitution(prefix) + output = entity.get_attr('output', optional=True) + if output is not None: + kwargs['output'] = output + shell = entity.get_attr('shell', types='bool', optional=True) + if shell is not None: + kwargs['shell'] = shell + # Conditions won't be allowed in the `env` tag. + # If that feature is needed, `set_enviroment_variable` and + # `unset_enviroment_variable` actions should be used. + env = entity.get_attr('env', types='list[Entity]', optional=True) + if env is not None: + env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} + kwargs['additional_env'] = env + args = entity.get_attr('args', optional=True) + # `args` is supposed to be a list separated with ' '. + # All the found `TextSubstitution` items are split and + # added to the list again as a `TextSubstitution`. + if args is not None: + args = parser.parse_substitution(args) + new_args = [] + for arg in args: + if isinstance(arg, TextSubstitution): + text = arg.text + text = shlex.split(text) + text = [TextSubstitution(text=item) for item in text] + new_args.extend(text) + else: + new_args.append(arg) + args = new_args + else: + args = [] + cmd_list.extend(args) + kwargs['cmd'] = cmd_list + _, action_kwargs = super(ExecuteProcess, ExecuteProcess).parse(entity, parser) + kwargs.update(action_kwargs) + + return ExecuteProcess, kwargs + @property def output(self): """Getter for output.""" diff --git a/launch/launch/actions/group_action.py b/launch/launch/actions/group_action.py index 1bee4db6e..b90e362df 100644 --- a/launch/launch/actions/group_action.py +++ b/launch/launch/actions/group_action.py @@ -24,9 +24,13 @@ from .set_launch_configuration import SetLaunchConfiguration from ..action import Action from ..launch_context import LaunchContext +from ..launch_frontend import Entity +from ..launch_frontend import expose_action +from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType +@expose_action('group') class GroupAction(Action): """ Action that yields other actions, optionally scoping launch configurations. @@ -54,6 +58,16 @@ def __init__( else: self.__launch_configurations = {} + @staticmethod + def parse(entity: Entity, parser: Parser): + """Return `GroupAction` action and kwargs for constructing it.""" + _, kwargs = super(GroupAction, GroupAction).parse(entity, parser) + scoped = entity.get_attr('scoped', types='bool', optional=True) + if scoped is not None: + kwargs['scoped'] = scoped + kwargs['actions'] = [parser.parse_action(e) for e in entity.children] + return GroupAction, kwargs + def execute(self, context: LaunchContext) -> Optional[List[Action]]: """Execute the action.""" actions = [] # type: List[Action] diff --git a/launch/launch/actions/include_launch_description.py b/launch/launch/actions/include_launch_description.py index c6ca76a20..b70e6e4de 100644 --- a/launch/launch/actions/include_launch_description.py +++ b/launch/launch/actions/include_launch_description.py @@ -25,11 +25,16 @@ from ..launch_context import LaunchContext from ..launch_description_entity import LaunchDescriptionEntity from ..launch_description_source import LaunchDescriptionSource +from ..launch_description_sources import AnyLaunchDescriptionSource +from ..launch_frontend import Entity +from ..launch_frontend import expose_action +from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType from ..utilities import normalize_to_list_of_substitutions from ..utilities import perform_substitutions +@expose_action('include') class IncludeLaunchDescription(Action): """ Action that includes a launch description source and yields its entities when visited. @@ -69,6 +74,26 @@ def __init__( self.__launch_description_source = launch_description_source self.__launch_arguments = launch_arguments + @staticmethod + def parse(entity: Entity, parser: Parser): + """Return `IncludeLaunchDescription` action and kwargs for constructing it.""" + _, kwargs = super( + IncludeLaunchDescription, + IncludeLaunchDescription + ).parse(entity, parser) + file = parser.parse_substitution(entity.get_attr('file')) + kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) + args = entity.get_attr('arg', types='list[Entity]', optional=True) + if args is not None: + kwargs['launch_arguments'] = [ + ( + parser.parse_substitution(e.get_attr('name')), + parser.parse_substitution(e.get_attr('value')) + ) + for e in args + ] + return IncludeLaunchDescription, kwargs + @property def launch_description_source(self) -> LaunchDescriptionSource: """Getter for self.__launch_description_source.""" diff --git a/launch/launch/actions/set_launch_configuration.py b/launch/launch/actions/set_launch_configuration.py index 9108c023f..88d635e2e 100644 --- a/launch/launch/actions/set_launch_configuration.py +++ b/launch/launch/actions/set_launch_configuration.py @@ -18,12 +18,16 @@ from ..action import Action from ..launch_context import LaunchContext +from ..launch_frontend import Entity +from ..launch_frontend import expose_action +from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution from ..utilities import normalize_to_list_of_substitutions from ..utilities import perform_substitutions +@expose_action('let') class SetLaunchConfiguration(Action): """ Action that sets a launch configuration by name. @@ -44,6 +48,16 @@ def __init__( self.__name = normalize_to_list_of_substitutions(name) self.__value = normalize_to_list_of_substitutions(value) + @staticmethod + def parse(entity: Entity, parser: Parser): + """Return `SetLaunchConfiguration` action and kwargs for constructing it.""" + name = parser.parse_substitution(entity.get_attr('name')) + value = parser.parse_substitution(entity.get_attr('value')) + _, kwargs = super(SetLaunchConfiguration, SetLaunchConfiguration).parse(entity, parser) + kwargs['name'] = name + kwargs['value'] = value + return SetLaunchConfiguration, kwargs + @property def name(self) -> List[Substitution]: """Getter for self.__name.""" diff --git a/launch/launch/launch_description_sources/__init__.py b/launch/launch/launch_description_sources/__init__.py index 88f88e181..a34838d0e 100644 --- a/launch/launch/launch_description_sources/__init__.py +++ b/launch/launch/launch_description_sources/__init__.py @@ -14,6 +14,8 @@ """Package for launch_description_sources.""" +from .any_launch_description_source import AnyLaunchDescriptionSource +from .frontend_launch_description_source import FrontendLaunchDescriptionSource from .python_launch_description_source import PythonLaunchDescriptionSource from .python_launch_file_utilities import get_launch_description_from_python_launch_file from .python_launch_file_utilities import InvalidPythonLaunchFileError @@ -23,5 +25,7 @@ 'get_launch_description_from_python_launch_file', 'InvalidPythonLaunchFileError', 'load_python_launch_file_as_module', + 'AnyLaunchDescriptionSource', + 'FrontendLaunchDescriptionSource', 'PythonLaunchDescriptionSource', ] diff --git a/launch_frontend/launch_frontend/launch_description_sources/any_launch_description_source.py b/launch/launch/launch_description_sources/any_launch_description_source.py similarity index 85% rename from launch_frontend/launch_frontend/launch_description_sources/any_launch_description_source.py rename to launch/launch/launch_description_sources/any_launch_description_source.py index 23077ed84..25b3ce390 100644 --- a/launch_frontend/launch_frontend/launch_description_sources/any_launch_description_source.py +++ b/launch/launch/launch_description_sources/any_launch_description_source.py @@ -14,14 +14,11 @@ """Module for the AnyLaunchDescriptionSource class.""" -from launch import SomeSubstitutionsType -from launch.launch_description_source import LaunchDescriptionSource -from launch.launch_description_sources.python_launch_file_utilities import \ - get_launch_description_from_python_launch_file -from launch.launch_description_sources.python_launch_file_utilities import \ - InvalidPythonLaunchFileError - -from ..parser import Parser +from .python_launch_file_utilities import get_launch_description_from_python_launch_file +from .python_launch_file_utilities import InvalidPythonLaunchFileError +from ..launch_description_source import LaunchDescriptionSource +from ..launch_frontend import Parser +from ..some_substitutions_type import SomeSubstitutionsType class AnyLaunchDescriptionSource(LaunchDescriptionSource): diff --git a/launch_frontend/launch_frontend/launch_description_sources/frontend_launch_description_source.py b/launch/launch/launch_description_sources/frontend_launch_description_source.py similarity index 92% rename from launch_frontend/launch_frontend/launch_description_sources/frontend_launch_description_source.py rename to launch/launch/launch_description_sources/frontend_launch_description_source.py index a48c129bf..cc8275b9b 100644 --- a/launch_frontend/launch_frontend/launch_description_sources/frontend_launch_description_source.py +++ b/launch/launch/launch_description_sources/frontend_launch_description_source.py @@ -16,10 +16,9 @@ from typing import Type -from launch.launch_description_source import LaunchDescriptionSource -from launch.some_substitutions_type import SomeSubstitutionsType - -from ..parser import Parser +from ..launch_description_source import LaunchDescriptionSource +from ..launch_frontend import Parser +from ..some_substitutions_type import SomeSubstitutionsType class FrontendLaunchDescriptionSource(LaunchDescriptionSource): diff --git a/launch_frontend/launch_frontend/__init__.py b/launch/launch/launch_frontend/__init__.py similarity index 74% rename from launch_frontend/launch_frontend/__init__.py rename to launch/launch/launch_frontend/__init__.py index 295c7e2b7..3527ffe61 100644 --- a/launch_frontend/launch_frontend/__init__.py +++ b/launch/launch/launch_frontend/__init__.py @@ -14,11 +14,6 @@ """Main entry point for the `launch_frontend` package.""" -# All files containing parsing methods should be imported here. -# If not, the action or substitution are not going to be exposed. -from . import action_parse_methods # noqa: F401 -from . import launch_description_sources -from . import substitution_parse_methods # noqa: F401 from . import type_utils from .entity import Entity from .expose import expose_action, expose_substitution @@ -30,6 +25,5 @@ 'Parser', 'expose_action', 'expose_substitution', - 'launch_description_sources', 'type_utils', ] diff --git a/launch_frontend/launch_frontend/entity.py b/launch/launch/launch_frontend/entity.py similarity index 100% rename from launch_frontend/launch_frontend/entity.py rename to launch/launch/launch_frontend/entity.py diff --git a/launch_frontend/launch_frontend/expose.py b/launch/launch/launch_frontend/expose.py similarity index 100% rename from launch_frontend/launch_frontend/expose.py rename to launch/launch/launch_frontend/expose.py diff --git a/launch_frontend/launch_frontend/grammar.lark b/launch/launch/launch_frontend/grammar.lark similarity index 100% rename from launch_frontend/launch_frontend/grammar.lark rename to launch/launch/launch_frontend/grammar.lark diff --git a/launch_frontend/launch_frontend/parse_substitution.py b/launch/launch/launch_frontend/parse_substitution.py similarity index 96% rename from launch_frontend/launch_frontend/parse_substitution.py rename to launch/launch/launch_frontend/parse_substitution.py index 84ed3c1ee..d5818a76d 100644 --- a/launch_frontend/launch_frontend/parse_substitution.py +++ b/launch/launch/launch_frontend/parse_substitution.py @@ -22,9 +22,8 @@ from lark import Token from lark import Transformer -from launch.substitutions import TextSubstitution - -from launch_frontend.expose import substitution_parse_methods +from .expose import substitution_parse_methods +from ..substitutions import TextSubstitution def replace_escaped_characters(data: Text) -> Text: diff --git a/launch_frontend/launch_frontend/parser.py b/launch/launch/launch_frontend/parser.py similarity index 90% rename from launch_frontend/launch_frontend/parser.py rename to launch/launch/launch_frontend/parser.py index 545d1e7a1..c5e6cfc55 100644 --- a/launch_frontend/launch_frontend/parser.py +++ b/launch/launch/launch_frontend/parser.py @@ -20,14 +20,15 @@ from typing import Tuple from typing import Union -import launch -from launch.utilities import is_a - from pkg_resources import iter_entry_points from .entity import Entity from .expose import action_parse_methods from .parse_substitution import parse_substitution +from ..action import Action +from ..launch_description import LaunchDescription +from ..some_substitutions_type import SomeSubstitutionsType +from ..utilities import is_a interpolation_fuctions = { entry_point.name: entry_point.load() @@ -64,7 +65,7 @@ def load_parser_implementations(cls): for entry_point in iter_entry_points('launch_frontend.parser') } - def parse_action(self, entity: Entity) -> (launch.Action, Tuple[Any]): + def parse_action(self, entity: Entity) -> (Action, Tuple[Any]): """Parse an action, using its registered parsing method.""" self.load_parser_extensions() if entity.type_name not in action_parse_methods: @@ -72,16 +73,16 @@ def parse_action(self, entity: Entity) -> (launch.Action, Tuple[Any]): action, kwargs = action_parse_methods[entity.type_name](entity, self) return action(**kwargs) - def parse_substitution(self, value: Text) -> launch.SomeSubstitutionsType: + def parse_substitution(self, value: Text) -> SomeSubstitutionsType: """Parse a substitution.""" return parse_substitution(value) - def parse_description(self, entity: Entity) -> launch.LaunchDescription: + def parse_description(self, entity: Entity) -> LaunchDescription: """Parse a launch description.""" if entity.type_name != 'launch': raise RuntimeError('Expected \'launch\' as root tag') actions = [self.parse_action(child) for child in entity.children] - return launch.LaunchDescription(actions) + return LaunchDescription(actions) @classmethod def load( diff --git a/launch_frontend/launch_frontend/type_utils.py b/launch/launch/launch_frontend/type_utils.py similarity index 100% rename from launch_frontend/launch_frontend/type_utils.py rename to launch/launch/launch_frontend/type_utils.py diff --git a/launch/launch/substitutions/environment_variable.py b/launch/launch/substitutions/environment_variable.py index 9cda7c743..ae0dd1a06 100644 --- a/launch/launch/substitutions/environment_variable.py +++ b/launch/launch/substitutions/environment_variable.py @@ -15,14 +15,17 @@ """Module for the EnvironmentVariable substitution.""" import os +from typing import Iterable from typing import List from typing import Text from ..launch_context import LaunchContext +from ..launch_frontend.expose import expose_substitution from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution +@expose_substitution('env') class EnvironmentVariable(Substitution): """ Substitution that gets an environment variable value as a string. @@ -43,6 +46,17 @@ def __init__( self.__name = normalize_to_list_of_substitutions(name) self.__default_value = normalize_to_list_of_substitutions(default_value) + @staticmethod + def parse(data: Iterable[SomeSubstitutionsType]): + """Parse `EnviromentVariable` substitution.""" + if not data or len(data) > 2: + raise TypeError('env substitution expects 1 or 2 arguments') + kwargs = {} + kwargs['name'] = data[0] + if len(data) == 2: + kwargs['default_value'] = data[1] + return EnvironmentVariable, kwargs + @property def name(self) -> List[Substitution]: """Getter for name.""" diff --git a/launch/launch/substitutions/find_executable.py b/launch/launch/substitutions/find_executable.py index 962544293..4bd18288d 100644 --- a/launch/launch/substitutions/find_executable.py +++ b/launch/launch/substitutions/find_executable.py @@ -14,6 +14,7 @@ """Module for the FindExecutable substitution.""" +from typing import Iterable from typing import List from typing import Text @@ -21,10 +22,12 @@ from .substitution_failure import SubstitutionFailure from ..launch_context import LaunchContext +from ..launch_frontend import expose_substitution from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution +@expose_substitution('find-exec') class FindExecutable(Substitution): """ Substitution that tries to locate an executable on the PATH. @@ -39,6 +42,15 @@ def __init__(self, *, name: SomeSubstitutionsType) -> None: from ..utilities import normalize_to_list_of_substitutions # import here to avoid loop self.__name = normalize_to_list_of_substitutions(name) + @staticmethod + def parse(data: Iterable[SomeSubstitutionsType]): + """Parse `FindExecutable` substitution.""" + if not data or len(data) > 1: + raise AttributeError('find-exec substitution expects 1 argument') + kwargs = {} + kwargs['name'] = data[0] + return FindExecutable, kwargs + @property def name(self) -> List[Substitution]: """Getter for name.""" diff --git a/launch/launch/substitutions/launch_configuration.py b/launch/launch/substitutions/launch_configuration.py index 236da3fef..d4316131e 100644 --- a/launch/launch/substitutions/launch_configuration.py +++ b/launch/launch/substitutions/launch_configuration.py @@ -24,10 +24,12 @@ from .substitution_failure import SubstitutionFailure from ..launch_context import LaunchContext +from ..launch_frontend import expose_substitution from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution +@expose_substitution('var') class LaunchConfiguration(Substitution): """Substitution that can access launch configuration variables.""" @@ -62,6 +64,17 @@ def __init__( normalize_to_list_of_substitutions( str_normalized_default) # type: List[Substitution] + @staticmethod + def parse(data: Iterable[SomeSubstitutionsType]): + """Parse `FindExecutable` substitution.""" + if not data or len(data) > 2: + raise TypeError('var substitution expects 1 or 2 arguments') + kwargs = {} + kwargs['variable_name'] = data[0] + if len(data) == 2: + kwargs['default'] = data[1] + return LaunchConfiguration, kwargs + @property def variable_name(self) -> List[Substitution]: """Getter for variable_name.""" diff --git a/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py b/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py index bbc475aba..160a71c94 100644 --- a/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py +++ b/launch/launch/utilities/normalize_to_list_of_substitutions_impl.py @@ -21,11 +21,13 @@ from .class_tools_impl import is_a_subclass from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution -from ..substitutions import TextSubstitution def normalize_to_list_of_substitutions(subs: SomeSubstitutionsType) -> List[Substitution]: """Return a list of Substitutions given a variety of starting inputs.""" + # Avoid recursive import + from ..substitutions import TextSubstitution + def normalize(x): if isinstance(x, Substitution): return x diff --git a/launch_frontend/test/launch_frontend/test_expose_decorators.py b/launch/test/launch/launch_frontend/test_expose_decorators.py similarity index 96% rename from launch_frontend/test/launch_frontend/test_expose_decorators.py rename to launch/test/launch/launch_frontend/test_expose_decorators.py index 841ac9357..d84b2caae 100644 --- a/launch_frontend/test/launch_frontend/test_expose_decorators.py +++ b/launch/test/launch/launch_frontend/test_expose_decorators.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from launch_frontend.expose import __expose_impl +from launch.launch_frontend.expose import __expose_impl import pytest diff --git a/launch_frontend/test/launch_frontend/test_substitutions.py b/launch/test/launch/launch_frontend/test_substitutions.py similarity index 97% rename from launch_frontend/test/launch_frontend/test_substitutions.py rename to launch/test/launch/launch_frontend/test_substitutions.py index 72900056c..9bd51f657 100644 --- a/launch_frontend/test/launch_frontend/test_substitutions.py +++ b/launch/test/launch/launch_frontend/test_substitutions.py @@ -15,11 +15,10 @@ """Test the default substitution interpolator.""" from launch import LaunchContext +from launch.launch_frontend.expose import expose_substitution +from launch.launch_frontend.parse_substitution import parse_substitution from launch.substitutions import TextSubstitution -from launch_frontend.expose import expose_substitution -from launch_frontend.parse_substitution import parse_substitution - def test_text_only(): subst = parse_substitution("\\'yes\\'") diff --git a/launch_frontend/launch_frontend/action_parse_methods.py b/launch_frontend/launch_frontend/action_parse_methods.py deleted file mode 100644 index af8d07580..000000000 --- a/launch_frontend/launch_frontend/action_parse_methods.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2019 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. - -"""Module for launch action parsing methods.""" - -import shlex - -import launch -from launch.conditions import IfCondition -from launch.conditions import UnlessCondition -from launch.substitutions import TextSubstitution - -from .entity import Entity -from .expose import expose_action -from .parser import Parser - - -def parse_action(entity: Entity, parser: Parser): - """ - Parse action. - - This is only intended for code reuse, and it's not exposed. - """ - if_cond = entity.get_attr('if', optional=True) - unless_cond = entity.get_attr('unless', optional=True) - kwargs = {} - if if_cond is not None and unless_cond is not None: - raise RuntimeError("if and unless conditions can't be used simultaneously") - if if_cond is not None: - kwargs['condition'] = IfCondition(predicate_expression=if_cond) - if unless_cond is not None: - kwargs['condition'] = UnlessCondition(predicate_expression=unless_cond) - return launch.Action, kwargs - - -@expose_action('executable') -def parse_executable( - entity: Entity, - parser: Parser, - optional_cmd: bool = False -): - """ - Parse executable tag. - - :param: optional_cmd Allow not specifying `cmd` argument. - Intended for code reuse in derived classes (e.g.: launch_ros.actions.Node). - """ - cmd = entity.get_attr('cmd', optional=optional_cmd) - if cmd is not None: - cmd_list = [parser.parse_substitution(cmd)] - else: - cmd_list = [] - kwargs = {} - cwd = entity.get_attr('cwd', optional=True) - if cwd is not None: - kwargs['cwd'] = parser.parse_substitution(cwd) - name = entity.get_attr('name', optional=True) - if name is not None: - kwargs['name'] = parser.parse_substitution(name) - prefix = entity.get_attr('launch-prefix', optional=True) - if prefix is not None: - kwargs['prefix'] = parser.parse_substitution(prefix) - output = entity.get_attr('output', optional=True) - if output is not None: - kwargs['output'] = output - shell = entity.get_attr('shell', types='bool', optional=True) - if shell is not None: - kwargs['shell'] = shell - # Conditions won't be allowed in the `env` tag. - # If that feature is needed, `set_enviroment_variable` and - # `unset_enviroment_variable` actions should be used. - env = entity.get_attr('env', types='list[Entity]', optional=True) - if env is not None: - env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} - kwargs['additional_env'] = env - args = entity.get_attr('args', optional=True) - # `args` is supposed to be a list separated with ' '. - # All the found `TextSubstitution` items are split and - # added to the list again as a `TextSubstitution`. - if args is not None: - args = parser.parse_substitution(args) - new_args = [] - for arg in args: - if isinstance(arg, TextSubstitution): - text = arg.text - text = shlex.split(text) - text = [TextSubstitution(text=item) for item in text] - new_args.extend(text) - else: - new_args.append(arg) - args = new_args - else: - args = [] - cmd_list.extend(args) - kwargs['cmd'] = cmd_list - _, action_kwargs = parse_action(entity, parser) - kwargs.update(action_kwargs) - - return launch.actions.ExecuteProcess, kwargs - - -@expose_action('let') -def parse_let(entity: Entity, parser: Parser): - """Parse let tag.""" - name = parser.parse_substitution(entity.get_attr('name')) - value = parser.parse_substitution(entity.get_attr('value')) - _, kwargs = parse_action(entity, parser) - kwargs['name'] = name - kwargs['value'] = value - return launch.actions.SetLaunchConfiguration, kwargs - - -@expose_action('include') -def parse_include(entity: Entity, parser: Parser): - """Parse include tag.""" - # import here for avoiding recursive import - # TODO(ivanpauno): Put it at the top of the file after merging launch and - # launch_frontend packages. - from .launch_description_sources import AnyLaunchDescriptionSource - _, kwargs = parse_action(entity, parser) - file = parser.parse_substitution(entity.get_attr('file')) - kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) - args = entity.get_attr('arg', types='list[Entity]', optional=True) - if args is not None: - kwargs['launch_arguments'] = [ - ( - parser.parse_substitution(e.get_attr('name')), - parser.parse_substitution(e.get_attr('value')) - ) - for e in args - ] - return launch.actions.IncludeLaunchDescription, kwargs - - -@expose_action('group') -def parse_group(entity: Entity, parser: Parser): - """Parse include tag.""" - _, kwargs = parse_action(entity, parser) - scoped = entity.get_attr('scoped', types='bool', optional=True) - if scoped is not None: - kwargs['scoped'] = scoped - kwargs['actions'] = [parser.parse_action(e) for e in entity.children] - return launch.actions.GroupAction, kwargs diff --git a/launch_frontend/launch_frontend/launch_description_sources/__init__.py b/launch_frontend/launch_frontend/launch_description_sources/__init__.py deleted file mode 100644 index ca6d93ff1..000000000 --- a/launch_frontend/launch_frontend/launch_description_sources/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2018 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. - -"""Package for launch_description_sources.""" - -from .any_launch_description_source import AnyLaunchDescriptionSource -from .frontend_launch_description_source import FrontendLaunchDescriptionSource - - -__all__ = [ - 'AnyLaunchDescriptionSource', - 'FrontendLaunchDescriptionSource', -] diff --git a/launch_frontend/launch_frontend/substitution_parse_methods.py b/launch_frontend/launch_frontend/substitution_parse_methods.py deleted file mode 100644 index d775d3be8..000000000 --- a/launch_frontend/launch_frontend/substitution_parse_methods.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2019 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. - -"""Module for launch substitution parsing methods.""" - -from typing import Iterable - -import launch - -from .expose import expose_substitution - - -@expose_substitution('env') -def parse_env(data: Iterable[launch.SomeSubstitutionsType]): - """Parse EnviromentVariable substitution.""" - if not data or len(data) > 2: - raise TypeError('env substitution expects 1 or 2 arguments') - kwargs = {} - kwargs['name'] = data[0] - if len(data) == 2: - kwargs['default_value'] = data[1] - return launch.substitutions.EnvironmentVariable, kwargs - - -@expose_substitution('var') -def parse_var(data: Iterable[launch.SomeSubstitutionsType]): - """Parse FindExecutable substitution.""" - # Reuse parse_env, as it is similar - if not data or len(data) > 2: - raise TypeError('var substitution expects 1 or 2 arguments') - kwargs = {} - kwargs['variable_name'] = data[0] - if len(data) == 2: - kwargs['default'] = data[1] - return launch.substitutions.LaunchConfiguration, kwargs - - -@expose_substitution('find-exec') -def parse_find_exec(data: Iterable[launch.SomeSubstitutionsType]): - """Parse FindExecutable substitution.""" - if not data or len(data) > 1: - raise AttributeError('find-exec substitution expects 1 argument') - kwargs = {} - kwargs['name'] = data[0] - return launch.substitutions.FindExecutable, kwargs diff --git a/launch_frontend/package.xml b/launch_frontend/package.xml deleted file mode 100644 index 50b44d271..000000000 --- a/launch_frontend/package.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - launch_frontend - 0.8.1 - Frontend extensions for the launch package. - Ivan Paunovic - Apache License 2.0 - - launch - - ament_copyright - ament_flake8 - ament_pep257 - python3-pytest - - - ament_python - - diff --git a/launch_frontend/setup.py b/launch_frontend/setup.py deleted file mode 100644 index e7f464b57..000000000 --- a/launch_frontend/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -from setuptools import find_packages -from setuptools import setup - -setup( - name='launch_frontend', - version='0.8.1', - packages=find_packages(exclude=['test']), - install_requires=['setuptools'], - zip_safe=True, - author='Ivan Paunovic', - author_email='ivanpauno@ekumenlabs.com', - maintainer='Ivan Paunovic', - maintainer_email='ivanpauno@ekumenlabs.com', - url='https://github.com/ros2/launch', - download_url='https://github.com/ros2/launch/releases', - keywords=['ROS'], - classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python', - 'Topic :: Software Development', - ], - description='Frontend extensions for the `launch` package.', - long_description=( - 'This package provides front-end extensions for the `launch` package.' - ), - license='Apache License, Version 2.0', - tests_require=['pytest'], -) diff --git a/launch_frontend/test/test_copyright.py b/launch_frontend/test/test_copyright.py deleted file mode 100644 index cf0fae31f..000000000 --- a/launch_frontend/test/test_copyright.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2017 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 ament_copyright.main import main -import pytest - - -@pytest.mark.copyright -@pytest.mark.linter -def test_copyright(): - rc = main(argv=['.', 'test']) - assert rc == 0, 'Found errors' diff --git a/launch_frontend/test/test_flake8.py b/launch_frontend/test/test_flake8.py deleted file mode 100644 index eff829969..000000000 --- a/launch_frontend/test/test_flake8.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2017 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 ament_flake8.main import main -import pytest - - -@pytest.mark.flake8 -@pytest.mark.linter -def test_flake8(): - rc = main(argv=[]) - assert rc == 0, 'Found errors' diff --git a/launch_frontend/test/test_pep257.py b/launch_frontend/test/test_pep257.py deleted file mode 100644 index 3aeb4d348..000000000 --- a/launch_frontend/test/test_pep257.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015 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 ament_pep257.main import main -import pytest - - -@pytest.mark.linter -@pytest.mark.pep257 -def test_pep257(): - rc = main(argv=[]) - assert rc == 0, 'Found code style errors / warnings' diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index d807d06f7..71c58bc56 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -21,8 +21,8 @@ from typing import Union import xml.etree.ElementTree as ET -from launch_frontend import Entity as BaseEntity -from launch_frontend.type_utils import get_typed_value +from launch.launch_frontend import Entity as BaseEntity +from launch.launch_frontend.type_utils import get_typed_value class Entity(BaseEntity): diff --git a/launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py b/launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py index db5c65fd5..a017ff2d0 100644 --- a/launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py +++ b/launch_xml/launch_xml/launch_description_sources/xml_launch_description_source.py @@ -15,8 +15,7 @@ """Module for the XMLLaunchDescriptionSource class.""" from launch import SomeSubstitutionsType - -from launch_frontend.launch_description_sources import FrontendLaunchDescriptionSource +from launch.launch_description_sources import FrontendLaunchDescriptionSource from ..parser import Parser diff --git a/launch_xml/launch_xml/parser.py b/launch_xml/launch_xml/parser.py index 6ffb6f63e..502d17494 100644 --- a/launch_xml/launch_xml/parser.py +++ b/launch_xml/launch_xml/parser.py @@ -18,7 +18,7 @@ from typing import Union import xml.etree.ElementTree as ET -import launch_frontend +from launch import launch_frontend from .entity import Entity diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 25b0079d1..4a39f0002 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -18,8 +18,7 @@ import textwrap from launch import LaunchService - -from launch_frontend import Parser +from launch.launch_frontend import Parser def test_executable(): diff --git a/launch_xml/test/launch_xml/test_group.py b/launch_xml/test/launch_xml/test_group.py index 492afaafa..ee21ed2a3 100644 --- a/launch_xml/test/launch_xml/test_group.py +++ b/launch_xml/test/launch_xml/test_group.py @@ -18,8 +18,7 @@ import textwrap from launch.actions import SetLaunchConfiguration - -from launch_frontend import Parser +from launch.launch_frontend import Parser def test_group(): diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py index 8db3508fc..430cdaf1a 100644 --- a/launch_xml/test/launch_xml/test_include.py +++ b/launch_xml/test/launch_xml/test_include.py @@ -18,9 +18,8 @@ import textwrap from launch.actions import IncludeLaunchDescription - -from launch_frontend import Parser -from launch_frontend.launch_description_sources import AnyLaunchDescriptionSource +from launch.launch_description_sources import AnyLaunchDescriptionSource +from launch.launch_frontend import Parser def test_include(): diff --git a/launch_xml/test/launch_xml/test_let_var.py b/launch_xml/test/launch_xml/test_let_var.py index 1b939b775..2b4d9b851 100644 --- a/launch_xml/test/launch_xml/test_let_var.py +++ b/launch_xml/test/launch_xml/test_let_var.py @@ -18,8 +18,7 @@ import textwrap from launch import LaunchContext - -from launch_frontend import Parser +from launch.launch_frontend import Parser def test_let_var(): diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py index 5e3ba50b3..46cd433f3 100644 --- a/launch_xml/test/launch_xml/test_list.py +++ b/launch_xml/test/launch_xml/test_list.py @@ -17,7 +17,7 @@ import io import textwrap -from launch_frontend import Parser +from launch.launch_frontend import Parser def test_list(): diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index cbd7c26b5..be14d986f 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -20,8 +20,8 @@ from typing import Tuple from typing import Union -from launch_frontend import Entity as BaseEntity -from launch_frontend.type_utils import check_type +from launch.launch_frontend import Entity as BaseEntity +from launch.launch_frontend.type_utils import check_type class Entity(BaseEntity): diff --git a/launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py b/launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py index 74afc9c67..cbeb3cee9 100644 --- a/launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py +++ b/launch_yaml/launch_yaml/launch_description_sources/yaml_launch_description_source.py @@ -15,8 +15,7 @@ """Module for the YAMLLaunchDescriptionSource class.""" from launch import SomeSubstitutionsType - -from launch_frontend.launch_description_sources import FrontendLaunchDescriptionSource +from launch.launch_description_sources import FrontendLaunchDescriptionSource from ..parser import Parser diff --git a/launch_yaml/launch_yaml/parser.py b/launch_yaml/launch_yaml/parser.py index b9acf5904..236d0e7de 100644 --- a/launch_yaml/launch_yaml/parser.py +++ b/launch_yaml/launch_yaml/parser.py @@ -17,7 +17,7 @@ import io from typing import Union -import launch_frontend +from launch import launch_frontend import yaml diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py index c5b984501..b4444b8ce 100644 --- a/launch_yaml/test/launch_yaml/test_executable.py +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -18,8 +18,7 @@ import textwrap from launch import LaunchService - -from launch_frontend import Parser +from launch.launch_frontend import Parser def test_executable(): diff --git a/launch_yaml/test/launch_yaml/test_group.py b/launch_yaml/test/launch_yaml/test_group.py index 06cff3ce6..522c844b4 100644 --- a/launch_yaml/test/launch_yaml/test_group.py +++ b/launch_yaml/test/launch_yaml/test_group.py @@ -18,8 +18,7 @@ import textwrap from launch.actions import SetLaunchConfiguration - -from launch_frontend import Parser +from launch.launch_frontend import Parser def test_group(): From 740c204e93de90fd14b0f76caf9e372f5dfe1053 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 2 Jul 2019 12:33:33 -0300 Subject: [PATCH 48/75] Update type_utils Signed-off-by: ivanpauno --- launch/launch/actions/execute_process.py | 4 +- launch/launch/actions/group_action.py | 2 +- .../actions/include_launch_description.py | 2 +- launch/launch/launch_frontend/entity.py | 40 ++-- launch/launch/launch_frontend/expose.py | 26 +++ .../launch_frontend/parse_substitution.py | 8 +- launch/launch/launch_frontend/parser.py | 7 +- launch/launch/launch_frontend/type_utils.py | 182 +++++++++--------- launch_xml/launch_xml/entity.py | 14 +- launch_xml/test/launch_xml/test_list.py | 7 +- launch_yaml/launch_yaml/entity.py | 14 +- 11 files changed, 180 insertions(+), 126 deletions(-) diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index bb9e40ee7..255f96213 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -260,13 +260,13 @@ def parse( output = entity.get_attr('output', optional=True) if output is not None: kwargs['output'] = output - shell = entity.get_attr('shell', types='bool', optional=True) + shell = entity.get_attr('shell', types=bool, optional=True) if shell is not None: kwargs['shell'] = shell # Conditions won't be allowed in the `env` tag. # If that feature is needed, `set_enviroment_variable` and # `unset_enviroment_variable` actions should be used. - env = entity.get_attr('env', types='list[Entity]', optional=True) + env = entity.get_attr('env', types=List[Entity], optional=True) if env is not None: env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} kwargs['additional_env'] = env diff --git a/launch/launch/actions/group_action.py b/launch/launch/actions/group_action.py index b90e362df..81cbde90a 100644 --- a/launch/launch/actions/group_action.py +++ b/launch/launch/actions/group_action.py @@ -62,7 +62,7 @@ def __init__( def parse(entity: Entity, parser: Parser): """Return `GroupAction` action and kwargs for constructing it.""" _, kwargs = super(GroupAction, GroupAction).parse(entity, parser) - scoped = entity.get_attr('scoped', types='bool', optional=True) + scoped = entity.get_attr('scoped', types=bool, optional=True) if scoped is not None: kwargs['scoped'] = scoped kwargs['actions'] = [parser.parse_action(e) for e in entity.children] diff --git a/launch/launch/actions/include_launch_description.py b/launch/launch/actions/include_launch_description.py index b70e6e4de..ef8f245a2 100644 --- a/launch/launch/actions/include_launch_description.py +++ b/launch/launch/actions/include_launch_description.py @@ -83,7 +83,7 @@ def parse(entity: Entity, parser: Parser): ).parse(entity, parser) file = parser.parse_substitution(entity.get_attr('file')) kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) - args = entity.get_attr('arg', types='list[Entity]', optional=True) + args = entity.get_attr('arg', types=List[Entity], optional=True) if args is not None: kwargs['launch_arguments'] = [ ( diff --git a/launch/launch/launch_frontend/entity.py b/launch/launch/launch_frontend/entity.py index 56684584c..b3aba3a0f 100644 --- a/launch/launch/launch_frontend/entity.py +++ b/launch/launch/launch_frontend/entity.py @@ -17,9 +17,11 @@ from typing import List from typing import Optional from typing import Text -from typing import Tuple +from typing import Type from typing import Union +from .type_utils import SomeAllowedTypes + class Entity: """Single item in the intermediate front_end representation.""" @@ -43,7 +45,13 @@ def get_attr( self, name: Text, *, - types: Union[Text, Tuple[Text]] = 'str', + types: + Optional[ + Union[ + SomeAllowedTypes, + Type[List['Entity']], + ] + ] = str, optional: bool = False ) -> Optional[Union[ Text, @@ -62,24 +70,24 @@ def get_attr( applied depending on the particular frontend. The allowed types are: - - 'str' - - 'int' - - 'float' - - 'bool' - - 'list[str]' - - 'list[int]' - - 'list[float]' - - 'list[bool]' + - `str` + - `int` + - `float` + - `bool` + - `List[str]` + - `List[int]` + - `List[float]` + - `List[bool]` Types that can not be combined with the others: - - 'guess' - - 'list[Entity]' + - `List[Entity]` - 'guess' work in the same way as: - ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') - 'list[Entity]' will return a list of more entities. + `types = None` work in the same way as: + `(int, float, bool, List[int], List[float], List[bool], List[str], str)` + `List[Entity]` will return a list of more entities. - See the frontend documentation to see how 'list' and 'list[Entity]' look like. + See the frontend documentation to see how `list` and `List[Entity]` look like for each + frontend implementation. If `optional` argument is `True`, will return `None` instead of raising `AttributeError`. diff --git a/launch/launch/launch_frontend/expose.py b/launch/launch/launch_frontend/expose.py index 767fef544..e6a21759b 100644 --- a/launch/launch/launch_frontend/expose.py +++ b/launch/launch/launch_frontend/expose.py @@ -15,12 +15,38 @@ """Module which adds methods for exposing parsing methods.""" import inspect +from typing import Iterable from typing import Text +from ..action import Action +from ..some_substitutions_type import SomeSubstitutionsType +from ..substitution import Substitution + +if False: + from .entity import Entity + from .parser import Parser + action_parse_methods = {} substitution_parse_methods = {} +def instantiate_action(entity: 'Entity', parser: 'Parser') -> Action: + """Call the registered parsing method for the `Entity`.""" + if entity.type_name not in action_parse_methods: + raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) + action_type, kwargs = action_parse_methods[entity.type_name](entity, parser) + return action_type(**kwargs) + + +def instantiate_substitution(type_name: Text, args: Iterable[SomeSubstitutionsType]) -> Substitution: + """Call the registered substitution parsing method, according to `args`.""" + if type_name not in substitution_parse_methods: + raise RuntimeError( + 'Unknown substitution: {}'.format(type_name)) + subst_type, kwargs = substitution_parse_methods[type_name](*args) + return subst_type(**kwargs) + + def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): """ Return a decorator for exposing a parsing method in a dictionary. diff --git a/launch/launch/launch_frontend/parse_substitution.py b/launch/launch/launch_frontend/parse_substitution.py index d5818a76d..f6024490c 100644 --- a/launch/launch/launch_frontend/parse_substitution.py +++ b/launch/launch/launch_frontend/parse_substitution.py @@ -22,7 +22,7 @@ from lark import Token from lark import Transformer -from .expose import substitution_parse_methods +from .expose import instantiate_substitution from ..substitutions import TextSubstitution @@ -68,11 +68,7 @@ def substitution(self, args): name = args[0] assert isinstance(name, Token) assert name.type == 'IDENTIFIER' - if name.value not in substitution_parse_methods: - raise RuntimeError( - 'Unknown substitution: {}'.format(name.value)) - subst, kwargs = substitution_parse_methods[name.value](*args[1:]) - return subst(**kwargs) + return instantiate_substitution(name.value, args[1:]) single_quoted_substitution = substitution double_quoted_substitution = substitution diff --git a/launch/launch/launch_frontend/parser.py b/launch/launch/launch_frontend/parser.py index c5e6cfc55..f446bad15 100644 --- a/launch/launch/launch_frontend/parser.py +++ b/launch/launch/launch_frontend/parser.py @@ -23,7 +23,7 @@ from pkg_resources import iter_entry_points from .entity import Entity -from .expose import action_parse_methods +from .expose import instantiate_action from .parse_substitution import parse_substitution from ..action import Action from ..launch_description import LaunchDescription @@ -68,10 +68,7 @@ def load_parser_implementations(cls): def parse_action(self, entity: Entity) -> (Action, Tuple[Any]): """Parse an action, using its registered parsing method.""" self.load_parser_extensions() - if entity.type_name not in action_parse_methods: - raise RuntimeError('Unrecognized entity of the type: {}'.format(entity.type_name)) - action, kwargs = action_parse_methods[entity.type_name](entity, self) - return action(**kwargs) + return instantiate_action(entity, self) def parse_substitution(self, value: Text) -> SomeSubstitutionsType: """Parse a substitution.""" diff --git a/launch/launch/launch_frontend/type_utils.py b/launch/launch/launch_frontend/type_utils.py index 400ee9dc0..21d1a8ca7 100644 --- a/launch/launch/launch_frontend/type_utils.py +++ b/launch/launch/launch_frontend/type_utils.py @@ -15,72 +15,76 @@ """Module which implements get_typed_value function.""" from typing import Any +from typing import Iterable +from typing import List +from typing import Optional from typing import Text -from typing import Tuple +from typing import Type from typing import Union +import yaml -types_for_guess__ = ( - 'int', 'float', 'bool', 'list[int]', 'list[float]', - 'list[bool]', 'list[str]', 'str' +__types_for_guess = ( + int, float, bool, List[int], List[float], + List[bool], List[str], str ) -def extract_type(name: Text): +AllowedTypes = Type[Union[__types_for_guess]] +SomeAllowedTypes = Union[AllowedTypes, Iterable[AllowedTypes]] + + +def extract_type(data_type: AllowedTypes): """ - Extract type information from string. - - :param name: a string specifying a type. can be one of: - - 'str' - - 'int' - - 'float' - - 'bool' - - 'list[str]' - - 'list[int]' - - 'list[float]' - - 'list[bool]' + Extract type information from type object. + + :param data_type: Can be one of: + - `str` + - `int` + - `float` + - `bool` + - `List[str]` + - `List[int]` + - `List[float]` + - `List[bool]` :returns: a tuple (type_obj, is_list). is_list is `True` for the supported list types, if not is `False`. type_obj is the object representing that type in python. In the case of list is the type of the items. e.g.: - name = 'list[int]' -> (int, True) - name = 'bool' -> (bool, False) + `name = List[int]` -> `(int, True)` + `name = bool` -> `(bool, False)` """ - error = ValueError('Unrecognized type name: {}'.format(name)) is_list = False - type_name = name - if 'list[' in name: + if data_type not in __types_for_guess: + raise ValueError('Unrecognized data type: {}'.format(data_type)) + if issubclass(data_type, List): is_list = True - type_name = name[5:-1] - if name[-1] != ']': - raise error - if type_name not in ('int', 'float', 'str', 'bool'): - raise error - return (eval(type_name), is_list) + data_type = data_type.__args__[0] + return (data_type, is_list) -def check_type(value: Any, types: Union[Text, Tuple[Text]]) -> bool: +def check_type(value: Any, types: Optional[SomeAllowedTypes]) -> bool: """ Check if `value` is one of the types in `types`. The allowed types are: - - 'str' - - 'int' - - 'float' - - 'bool' - - 'list[str]' - - 'list[int]' - - 'list[float]' - - 'list[bool]' - - types = 'guess' works in the same way as: - ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') + - `str` + - `int` + - `float` + - `bool` + - `List[str]` + - `List[int]` + - `List[float]` + - `List[bool]` + + `types = None` works in the same way as: + `(int, float, bool, List[int], List[float], List[bool], List[str], str)` """ - if types == 'guess': - types = types_for_guess__ - if isinstance(types, Text): + if types is None: + types = __types_for_guess + elif not isinstance(types, Iterable): types = [types] for x in types: type_obj, is_list = extract_type(x) @@ -95,7 +99,10 @@ def check_type(value: Any, types: Union[Text, Tuple[Text]]) -> bool: return False -def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: +def get_typed_value( + value: Union[Text, List[Text]], + types: Optional[SomeAllowedTypes] +) -> Any: """ Try to convert `value` to one of the types specified in `types`. @@ -103,53 +110,56 @@ def get_typed_value(value: Text, types: Union[Text, Tuple[Text]]) -> Any: If not raise `AttributeError`. The allowed types are: - - 'str' - - 'int' - - 'float' - - 'bool' - - 'list[str]' - - 'list[int]' - - 'list[float]' - - 'list[bool]' - - types = 'guess' works in the same way as: - ('int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str') + - `str` + - `int` + - `float` + - `bool` + - `List[str]` + - `List[int]` + - `List[float]` + - `List[bool]` + + `types = None` works in the same way as: + `(int, float, bool, List[int], List[float], List[bool], List[str], str)` """ - if types == 'guess': - types = types_for_guess__ - elif isinstance(types, Text): + if types is None: + types = __types_for_guess + elif types == str: + # Avoid unnecessary calculations for the usual case + return value + elif not isinstance(types, Iterable): types = [types] - typed_value = None + if isinstance(value, list): + yaml_value = [yaml.safe_load(x) for x in value] + else: + yaml_value = yaml.safe_load(value) + for x in types: + if x is str: + # Return strings as-is + return value type_obj, is_list = extract_type(x) - if type_obj is bool: - def type_obj(x): - if x.lower() in ('true', 'yes', 'on', '1', 'false', 'no', 'off', '0'): - return x.lower() in ('true', 'yes', 'on', '1') - raise ValueError() - if is_list: - if not isinstance(value, list): - continue - try: - typed_value = [type_obj(x) for x in value] - except ValueError: - pass - else: - break - else: - if isinstance(value, list): - continue - try: - typed_value = type_obj(value) - except ValueError: - pass + if type_obj is str and is_list: + # Return list of strings as-is + return value + if type_obj is float: + # Allow coercing int to float + if not is_list: + try: + return float(yaml_value) + except ValueError: + continue else: - break - if typed_value is None: - raise ValueError( - 'Can not convert value {} to one of the types in {}'.format( - value, types - ) + try: + return [float(x) for x in yaml_value] + except ValueError: + continue + if check_type(yaml_value, x): + # Any other case, check if type is ok and do yaml conversion + return yaml_value + raise ValueError( + 'Can not convert value {} to one of the types in {}'.format( + value, types ) - return typed_value + ) diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index 71c58bc56..03e70a5fb 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -17,12 +17,13 @@ from typing import List from typing import Optional from typing import Text -from typing import Tuple +from typing import Type from typing import Union import xml.etree.ElementTree as ET from launch.launch_frontend import Entity as BaseEntity from launch.launch_frontend.type_utils import get_typed_value +from launch.launch_frontend.type_utils import SomeAllowedTypes class Entity(BaseEntity): @@ -57,7 +58,14 @@ def get_attr( self, name: Text, *, - types: Union[Text, Tuple[Text]] = 'str', + types: + Optional[ + Union[ + SomeAllowedTypes, + Type[List[BaseEntity]], + Type[List['Entity']], + ] + ] = str, optional: bool = False ) -> Optional[Union[ Text, @@ -74,7 +82,7 @@ def get_attr( name, types, self.type_name ) ) - if types == 'list[Entity]': + if issubclass(types, List) and issubclass(types.__args__[0], BaseEntity): return_list = filter(lambda x: x.tag == name, self.__xml_element) if not return_list: if optional: diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py index 46cd433f3..50c1d19fc 100644 --- a/launch_xml/test/launch_xml/test_list.py +++ b/launch_xml/test/launch_xml/test_list.py @@ -16,6 +16,7 @@ import io import textwrap +from typing import List from launch.launch_frontend import Parser @@ -33,9 +34,9 @@ def test_list(): xml_file = textwrap.dedent(xml_file) root_entity, parser = Parser.load(io.StringIO(xml_file)) tags = root_entity.children - assert tags[0].get_attr('attr', types='list[str]') == ['1', '2', '3'] - assert tags[0].get_attr('attr', types='list[int]') == [1, 2, 3] - assert tags[0].get_attr('attr', types='list[float]') == [1., 2., 3.] + assert tags[0].get_attr('attr', types=List[str]) == ['1', '2', '3'] + assert tags[0].get_attr('attr', types=List[int]) == [1, 2, 3] + assert tags[0].get_attr('attr', types=List[float]) == [1., 2., 3.] if __name__ == '__main__': diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index be14d986f..c8a1f8914 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -17,11 +17,12 @@ from typing import List from typing import Optional from typing import Text -from typing import Tuple +from typing import Type from typing import Union from launch.launch_frontend import Entity as BaseEntity from launch.launch_frontend.type_utils import check_type +from launch.launch_frontend.type_utils import SomeAllowedTypes class Entity(BaseEntity): @@ -74,7 +75,14 @@ def get_attr( self, name: Text, *, - types: Union[Text, Tuple[Text]] = 'str', + types: + Optional[ + Union[ + SomeAllowedTypes, + Type[List[BaseEntity]], + Type[List['Entity']], + ] + ] = str, optional: bool = False ) -> Optional[Union[ Text, @@ -94,7 +102,7 @@ def get_attr( else: return None data = self.__element[name] - if types == 'list[Entity]': + if issubclass(types, List) and issubclass(types.__args__[0], BaseEntity): if isinstance(data, list) and isinstance(data[0], dict): return [Entity(child, name) for child in data] raise TypeError( From f0d074902c67a49d5e487f7bcb1e8731ec296783 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 2 Jul 2019 17:05:49 -0300 Subject: [PATCH 49/75] Corrected type utils again after further testing Signed-off-by: ivanpauno --- launch/launch/launch_frontend/type_utils.py | 16 ++++++---------- launch_xml/launch_xml/entity.py | 4 +++- launch_yaml/launch_yaml/entity.py | 4 +++- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launch/launch/launch_frontend/type_utils.py b/launch/launch/launch_frontend/type_utils.py index 21d1a8ca7..7c77db29c 100644 --- a/launch/launch/launch_frontend/type_utils.py +++ b/launch/launch/launch_frontend/type_utils.py @@ -125,7 +125,7 @@ def get_typed_value( if types is None: types = __types_for_guess elif types == str: - # Avoid unnecessary calculations for the usual case + # Avoid evaluating as yaml if types was just `str` return value elif not isinstance(types, Iterable): types = [types] @@ -136,28 +136,24 @@ def get_typed_value( yaml_value = yaml.safe_load(value) for x in types: - if x is str: - # Return strings as-is - return value type_obj, is_list = extract_type(x) - if type_obj is str and is_list: - # Return list of strings as-is - return value if type_obj is float: # Allow coercing int to float if not is_list: try: return float(yaml_value) - except ValueError: + except (ValueError, TypeError): continue else: try: return [float(x) for x in yaml_value] - except ValueError: + except (ValueError, TypeError): continue if check_type(yaml_value, x): - # Any other case, check if type is ok and do yaml conversion + # Check if type is ok and do yaml conversion return yaml_value + if type_obj is str: + return value raise ValueError( 'Can not convert value {} to one of the types in {}'.format( value, types diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index 03e70a5fb..fec979cfa 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -82,7 +82,9 @@ def get_attr( name, types, self.type_name ) ) - if issubclass(types, List) and issubclass(types.__args__[0], BaseEntity): + is_list_entity = types is not None and not isinstance(types, tuple) \ + and issubclass(types, List) and issubclass(types.__args__[0], BaseEntity) + if is_list_entity: return_list = filter(lambda x: x.tag == name, self.__xml_element) if not return_list: if optional: diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index c8a1f8914..bde85451d 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -102,7 +102,9 @@ def get_attr( else: return None data = self.__element[name] - if issubclass(types, List) and issubclass(types.__args__[0], BaseEntity): + is_list_entity = types is not None and not isinstance(types, tuple) \ + and issubclass(types, List) and issubclass(types.__args__[0], BaseEntity) + if is_list_entity: if isinstance(data, list) and isinstance(data[0], dict): return [Entity(child, name) for child in data] raise TypeError( From d9a28456161339ae6c39ccb9df970eb5b77e92e0 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 3 Jul 2019 09:42:35 -0300 Subject: [PATCH 50/75] Delete launch_frontend dependency Signed-off-by: ivan --- launch_xml/package.xml | 1 - launch_yaml/package.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/launch_xml/package.xml b/launch_xml/package.xml index ba75c6a15..5f29bd1e9 100644 --- a/launch_xml/package.xml +++ b/launch_xml/package.xml @@ -8,7 +8,6 @@ Apache License 2.0 launch - launch_frontend ament_copyright ament_flake8 diff --git a/launch_yaml/package.xml b/launch_yaml/package.xml index f38d978ad..3da3c6047 100644 --- a/launch_yaml/package.xml +++ b/launch_yaml/package.xml @@ -8,7 +8,6 @@ Apache License 2.0 launch - launch_frontend ament_copyright ament_flake8 From 75268fc666901de3b32c01744322a794a2e7336e Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 3 Jul 2019 10:01:31 -0300 Subject: [PATCH 51/75] Update readmes Signed-off-by: ivan --- launch_xml/README.md | 23 ++++++++++++----------- launch_yaml/README.md | 18 +++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/launch_xml/README.md b/launch_xml/README.md index 3029ac176..66d6ba5cb 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -15,21 +15,22 @@ When having an xml tag like: If the entity `e` is wrapping it, the following statements will be true: ```python e.get_attr('value') == '2' -e.get_attr('value', types='int') == 2 -e.get_attr('value', types='float') == 2.0 +e.get_attr('value', types=int) == 2 +e.get_attr('value', types=float) == 2.0 ``` By default, the value of the attribute is returned as a string. Allowed types are: ```python -'str', 'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]' +str, int, float, bool, List[int], List[float], List[bool], List[str] ``` -A combination of them can be specified with a tuple. e.g.: `('int', 'str')`. +`List` is the usual object from the `typing` package. +A combination of them can be specified with a tuple. e.g.: `(int, str)`. In that case, conversions are tried in order and the first successful conversion is returned. -`types` can also be set to `guess`, which works in the same way as passing: +`types` can also be set to `None`, which works in the same way as passing: ```python -'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str' +int, float, bool, List[int], List[float], List[bool], List[str], str ``` For handling lists, the `*-sep` attribute is used. e.g.: @@ -41,12 +42,12 @@ For handling lists, the `*-sep` attribute is used. e.g.: ``` ```python -tag.get_attr('value', types='list[int]') == [2, 3, 4] -tag2.get_attr('value', types='list[float]') == [2.0, 3.0, 4.0] -tag3.get_attr('value', types='list[str]') == ['2', '3', '4'] +tag.get_attr('value', types=List[int]) == [2, 3, 4] +tag2.get_attr('value', types=List[float]) == [2.0, 3.0, 4.0] +tag3.get_attr('value', types=List[str]) == ['2', '3', '4'] ``` -For checking if an attribute exists, use optional argument: +For checking if an attribute exists, use an optional argument: ```python value = e.get_attr('value', optional=True) @@ -70,7 +71,7 @@ In this xml: The `env` children could be accessed like: ```python -env = e.get_attr('env', types='list[Entity]') +env = e.get_attr('env', types=List[Entity]) len(env) == 2 env[0].get_attr('name') == 'a' env[0].get_attr('value') == '100' diff --git a/launch_yaml/README.md b/launch_yaml/README.md index d324e7977..6b8240c71 100644 --- a/launch_yaml/README.md +++ b/launch_yaml/README.md @@ -18,28 +18,28 @@ tag: If the entity `e` is wrapping `tag`, the following statement will be true: ```python e.get_attr('value1') == '2' -e.get_attr('value2', types='int') == 2 -e.get_attr('value3', types='float') == 2.0 +e.get_attr('value2', types=int) == 2 +e.get_attr('value3', types=float) == 2.0 ``` By default, `get_attr` returns an string and it does type checking. The following code will raise a `TypeError`: ```python -e.get_attr('value1', types='int') -e.get_attr('value2', types='float') +e.get_attr('value1', types=int) +e.get_attr('value2', types=float) e.get_attr('value3') ``` Allowed types are: ```python -'str', 'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]' +str, int, float, bool, List[int], List[float], List[bool], List[str] ``` -A combination of them can be specified with a tuple. e.g.: `('int', 'str')`. +A combination of them can be specified with a tuple. e.g.: `(int, str)`. In that case, conversions are tried in order and the first successful conversion is returned. -`types` can also be set to `guess`, which works in the same way as passing: +`types` can also be set to `None`, which works in the same way as passing: ```python -'int', 'float', 'bool', 'list[int]', 'list[float]', 'list[bool]', 'list[str]', 'str' +int, float, bool, List[int], List[float], List[bool], List[str], str ``` For checking if an attribute exists, use optional argument: @@ -69,7 +69,7 @@ executable: The `env` children could be accessed doing: ```python -env = e.get_attr('env', types='list[Entity]') +env = e.get_attr('env', types=List[Entity]) len(env) == 2 env[0].get_attr('name') == 'a' env[0].get_attr('value') == '100' From f8a4b064e439fe0e74ebfc8e0c4b6afb631e455b Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 3 Jul 2019 11:23:35 -0300 Subject: [PATCH 52/75] Test include tag properly Signed-off-by: ivan --- .../any_launch_description_source.py | 2 +- launch_xml/test/launch_xml/executable.xml | 5 +++ launch_xml/test/launch_xml/test_executable.py | 32 +++++++------------ launch_xml/test/launch_xml/test_include.py | 11 +++++-- 4 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 launch_xml/test/launch_xml/executable.xml diff --git a/launch/launch/launch_description_sources/any_launch_description_source.py b/launch/launch/launch_description_sources/any_launch_description_source.py index 25b3ce390..5c9e608cd 100644 --- a/launch/launch/launch_description_sources/any_launch_description_source.py +++ b/launch/launch/launch_description_sources/any_launch_description_source.py @@ -54,7 +54,7 @@ def _get_launch_description(self, location): launch_description = None try: launch_description = get_launch_description_from_python_launch_file(location) - except InvalidPythonLaunchFileError: + except (InvalidPythonLaunchFileError, SyntaxError): pass try: root_entity, parser = Parser.load(location) diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml new file mode 100644 index 000000000..437c7a1da --- /dev/null +++ b/launch_xml/test/launch_xml/executable.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 4a39f0002..364fc6f93 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -14,8 +14,7 @@ """Test parsing an executable action.""" -import io -import textwrap +from pathlib import Path from launch import LaunchService from launch.launch_frontend import Parser @@ -23,33 +22,24 @@ def test_executable(): """Parse node xml example.""" - xml_file = \ - """\ - - - - - - """ # noqa: E501 - xml_file = textwrap.dedent(xml_file) - root_entity, parser = Parser.load(io.StringIO(xml_file)) + xml_file = str(Path(__file__).parent / 'executable.xml') + root_entity, parser = Parser.load(xml_file) ld = parser.parse_description(root_entity) executable = ld.entities[0] cmd = [i[0].perform(None) for i in executable.cmd] - assert(cmd == - ['ls', '-l', '-a', '-s']) - assert(executable.cwd[0].perform(None) == '/') - assert(executable.name[0].perform(None) == 'my_ls') - assert(executable.shell is True) - assert(executable.output == 'log') + assert cmd == ['ls', '-l', '-a', '-s'] + assert executable.cwd[0].perform(None) == '/' + assert executable.name[0].perform(None) == 'my_ls' + assert executable.shell is True + assert executable.output == 'log' key, value = executable.additional_env[0] key = key[0].perform(None) value = value[0].perform(None) - assert(key == 'var') - assert(value == '1') + assert key == 'var' + assert value == '1' ls = LaunchService() ls.include_launch_description(ld) - assert(0 == ls.run()) + assert 0 == ls.run() if __name__ == '__main__': diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py index 430cdaf1a..ba10f0621 100644 --- a/launch_xml/test/launch_xml/test_include.py +++ b/launch_xml/test/launch_xml/test_include.py @@ -15,8 +15,10 @@ """Test parsing an include action.""" import io +from pathlib import Path import textwrap +from launch import LaunchService from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import AnyLaunchDescriptionSource from launch.launch_frontend import Parser @@ -24,19 +26,22 @@ def test_include(): """Parse node xml example.""" + path = str(Path(__file__).parent / 'executable.xml') xml_file = \ """\ - + - """ # noqa: E501 + """.format(path) # noqa: E501 xml_file = textwrap.dedent(xml_file) root_entity, parser = Parser.load(io.StringIO(xml_file)) ld = parser.parse_description(root_entity) include = ld.entities[0] assert isinstance(include, IncludeLaunchDescription) assert isinstance(include.launch_description_source, AnyLaunchDescriptionSource) - # TODO(ivanpauno): Load something really + ls = LaunchService(debug=True) + ls.include_launch_description(ld) + assert 0 == ls.run() if __name__ == '__main__': From a64faaa11c3bd39cec747f0412d311c63634dedb Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 3 Jul 2019 15:33:31 -0300 Subject: [PATCH 53/75] Address PR comments Signed-off-by: ivan --- launch/launch/__init__.py | 4 ++-- launch/launch/action.py | 4 ++-- launch/launch/actions/execute_process.py | 15 +++++++-------- launch/launch/actions/group_action.py | 14 +++++++------- .../launch/actions/include_launch_description.py | 15 ++++++--------- launch/launch/actions/set_launch_configuration.py | 14 +++++++------- .../{launch_frontend => frontend}/__init__.py | 2 +- .../{launch_frontend => frontend}/entity.py | 0 .../{launch_frontend => frontend}/expose.py | 9 ++++++--- .../{launch_frontend => frontend}/grammar.lark | 0 .../parse_substitution.py | 0 .../{launch_frontend => frontend}/parser.py | 14 +++++++------- .../{launch_frontend => frontend}/type_utils.py | 0 .../any_launch_description_source.py | 2 +- .../frontend_launch_description_source.py | 2 +- .../launch/substitutions/environment_variable.py | 10 +++++----- launch/launch/substitutions/find_executable.py | 12 +++++------- .../launch/substitutions/launch_configuration.py | 10 +++++----- .../launch_frontend/test_expose_decorators.py | 8 ++++---- .../launch/launch_frontend/test_substitutions.py | 4 ++-- launch_xml/launch_xml/entity.py | 6 +++--- launch_xml/launch_xml/parser.py | 4 ++-- launch_xml/setup.py | 2 +- launch_xml/test/launch_xml/test_executable.py | 2 +- launch_xml/test/launch_xml/test_group.py | 2 +- launch_xml/test/launch_xml/test_include.py | 2 +- launch_xml/test/launch_xml/test_let_var.py | 2 +- launch_xml/test/launch_xml/test_list.py | 2 +- launch_yaml/launch_yaml/entity.py | 6 +++--- launch_yaml/launch_yaml/parser.py | 4 ++-- launch_yaml/setup.py | 2 +- launch_yaml/test/launch_yaml/test_executable.py | 2 +- launch_yaml/test/launch_yaml/test_group.py | 2 +- 33 files changed, 87 insertions(+), 90 deletions(-) rename launch/launch/{launch_frontend => frontend}/__init__.py (93%) rename launch/launch/{launch_frontend => frontend}/entity.py (100%) rename launch/launch/{launch_frontend => frontend}/expose.py (93%) rename launch/launch/{launch_frontend => frontend}/grammar.lark (100%) rename launch/launch/{launch_frontend => frontend}/parse_substitution.py (100%) rename launch/launch/{launch_frontend => frontend}/parser.py (89%) rename launch/launch/{launch_frontend => frontend}/type_utils.py (100%) diff --git a/launch/launch/__init__.py b/launch/launch/__init__.py index aa76dc93f..c2272fb19 100644 --- a/launch/launch/__init__.py +++ b/launch/launch/__init__.py @@ -17,7 +17,7 @@ from . import actions from . import conditions from . import events -from . import launch_frontend +from . import frontend from . import logging from . import substitutions from .action import Action @@ -40,7 +40,7 @@ 'actions', 'conditions', 'events', - 'launch_frontend', + 'frontend', 'logging', 'substitutions', 'Action', diff --git a/launch/launch/action.py b/launch/launch/action.py index 2d2a65ee5..21637d5aa 100644 --- a/launch/launch/action.py +++ b/launch/launch/action.py @@ -24,8 +24,8 @@ from .launch_description_entity import LaunchDescriptionEntity if False: - from .launch_frontend import Entity - from .launch_frontend import Parser + from .frontend import Entity + from .frontend import Parser class Action(LaunchDescriptionEntity): diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 255f96213..8c397e5f2 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -58,11 +58,11 @@ from ..events.process import ProcessStdout from ..events.process import ShutdownProcess from ..events.process import SignalProcess +from ..frontend import Entity +from ..frontend import expose_action +from ..frontend import Parser from ..launch_context import LaunchContext from ..launch_description import LaunchDescription -from ..launch_frontend import Entity -from ..launch_frontend import expose_action -from ..launch_frontend import Parser from ..some_actions_type import SomeActionsType from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution # noqa: F401 @@ -230,8 +230,9 @@ def __init__( self.__stdout_buffer = io.StringIO() self.__stderr_buffer = io.StringIO() - @staticmethod + @classmethod def parse( + cls, entity: Entity, parser: Parser, optional_cmd: bool = False @@ -247,7 +248,7 @@ def parse( cmd_list = [parser.parse_substitution(cmd)] else: cmd_list = [] - kwargs = {} + _, kwargs = super().parse(entity, parser) cwd = entity.get_attr('cwd', optional=True) if cwd is not None: kwargs['cwd'] = parser.parse_substitution(cwd) @@ -290,10 +291,8 @@ def parse( args = [] cmd_list.extend(args) kwargs['cmd'] = cmd_list - _, action_kwargs = super(ExecuteProcess, ExecuteProcess).parse(entity, parser) - kwargs.update(action_kwargs) - return ExecuteProcess, kwargs + return cls, kwargs @property def output(self): diff --git a/launch/launch/actions/group_action.py b/launch/launch/actions/group_action.py index 81cbde90a..058d0a4da 100644 --- a/launch/launch/actions/group_action.py +++ b/launch/launch/actions/group_action.py @@ -23,10 +23,10 @@ from .push_launch_configurations import PushLaunchConfigurations from .set_launch_configuration import SetLaunchConfiguration from ..action import Action +from ..frontend import Entity +from ..frontend import expose_action +from ..frontend import Parser from ..launch_context import LaunchContext -from ..launch_frontend import Entity -from ..launch_frontend import expose_action -from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType @@ -58,15 +58,15 @@ def __init__( else: self.__launch_configurations = {} - @staticmethod - def parse(entity: Entity, parser: Parser): + @classmethod + def parse(cls, entity: Entity, parser: Parser): """Return `GroupAction` action and kwargs for constructing it.""" - _, kwargs = super(GroupAction, GroupAction).parse(entity, parser) + _, kwargs = super().parse(entity, parser) scoped = entity.get_attr('scoped', types=bool, optional=True) if scoped is not None: kwargs['scoped'] = scoped kwargs['actions'] = [parser.parse_action(e) for e in entity.children] - return GroupAction, kwargs + return cls, kwargs def execute(self, context: LaunchContext) -> Optional[List[Action]]: """Execute the action.""" diff --git a/launch/launch/actions/include_launch_description.py b/launch/launch/actions/include_launch_description.py index ef8f245a2..45b9cd57c 100644 --- a/launch/launch/actions/include_launch_description.py +++ b/launch/launch/actions/include_launch_description.py @@ -22,13 +22,13 @@ from .set_launch_configuration import SetLaunchConfiguration from ..action import Action +from ..frontend import Entity +from ..frontend import expose_action +from ..frontend import Parser from ..launch_context import LaunchContext from ..launch_description_entity import LaunchDescriptionEntity from ..launch_description_source import LaunchDescriptionSource from ..launch_description_sources import AnyLaunchDescriptionSource -from ..launch_frontend import Entity -from ..launch_frontend import expose_action -from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType from ..utilities import normalize_to_list_of_substitutions from ..utilities import perform_substitutions @@ -74,13 +74,10 @@ def __init__( self.__launch_description_source = launch_description_source self.__launch_arguments = launch_arguments - @staticmethod - def parse(entity: Entity, parser: Parser): + @classmethod + def parse(cls, entity: Entity, parser: Parser): """Return `IncludeLaunchDescription` action and kwargs for constructing it.""" - _, kwargs = super( - IncludeLaunchDescription, - IncludeLaunchDescription - ).parse(entity, parser) + _, kwargs = super().parse(entity, parser) file = parser.parse_substitution(entity.get_attr('file')) kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) args = entity.get_attr('arg', types=List[Entity], optional=True) diff --git a/launch/launch/actions/set_launch_configuration.py b/launch/launch/actions/set_launch_configuration.py index 88d635e2e..47e52bbe5 100644 --- a/launch/launch/actions/set_launch_configuration.py +++ b/launch/launch/actions/set_launch_configuration.py @@ -17,10 +17,10 @@ from typing import List from ..action import Action +from ..frontend import Entity +from ..frontend import expose_action +from ..frontend import Parser from ..launch_context import LaunchContext -from ..launch_frontend import Entity -from ..launch_frontend import expose_action -from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution from ..utilities import normalize_to_list_of_substitutions @@ -48,15 +48,15 @@ def __init__( self.__name = normalize_to_list_of_substitutions(name) self.__value = normalize_to_list_of_substitutions(value) - @staticmethod - def parse(entity: Entity, parser: Parser): + @classmethod + def parse(cls, entity: Entity, parser: Parser): """Return `SetLaunchConfiguration` action and kwargs for constructing it.""" name = parser.parse_substitution(entity.get_attr('name')) value = parser.parse_substitution(entity.get_attr('value')) - _, kwargs = super(SetLaunchConfiguration, SetLaunchConfiguration).parse(entity, parser) + _, kwargs = super().parse(entity, parser) kwargs['name'] = name kwargs['value'] = value - return SetLaunchConfiguration, kwargs + return cls, kwargs @property def name(self) -> List[Substitution]: diff --git a/launch/launch/launch_frontend/__init__.py b/launch/launch/frontend/__init__.py similarity index 93% rename from launch/launch/launch_frontend/__init__.py rename to launch/launch/frontend/__init__.py index 3527ffe61..b50274b6d 100644 --- a/launch/launch/launch_frontend/__init__.py +++ b/launch/launch/frontend/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Main entry point for the `launch_frontend` package.""" +"""Package for launch markup.""" from . import type_utils from .entity import Entity diff --git a/launch/launch/launch_frontend/entity.py b/launch/launch/frontend/entity.py similarity index 100% rename from launch/launch/launch_frontend/entity.py rename to launch/launch/frontend/entity.py diff --git a/launch/launch/launch_frontend/expose.py b/launch/launch/frontend/expose.py similarity index 93% rename from launch/launch/launch_frontend/expose.py rename to launch/launch/frontend/expose.py index e6a21759b..3139f9815 100644 --- a/launch/launch/launch_frontend/expose.py +++ b/launch/launch/frontend/expose.py @@ -38,7 +38,10 @@ def instantiate_action(entity: 'Entity', parser: 'Parser') -> Action: return action_type(**kwargs) -def instantiate_substitution(type_name: Text, args: Iterable[SomeSubstitutionsType]) -> Substitution: +def instantiate_substitution( + type_name: Text, + args: Iterable[SomeSubstitutionsType] +) -> Substitution: """Call the registered substitution parsing method, according to `args`.""" if type_name not in substitution_parse_methods: raise RuntimeError( @@ -73,11 +76,11 @@ def __expose_impl(name: Text, parse_methods_map: dict, exposed_type: Text): def expose_impl_decorator(exposed): found_parse_method = None if inspect.isclass(exposed): - if 'parse' in dir(exposed) and inspect.isfunction(exposed.parse): + if 'parse' in dir(exposed) and inspect.ismethod(exposed.parse): found_parse_method = exposed.parse else: raise RuntimeError( - "Did not find an static method called 'parse' in the class being decorated." + "Did not find a class method called 'parse' in the class being decorated." ) elif inspect.isfunction(exposed): found_parse_method = exposed diff --git a/launch/launch/launch_frontend/grammar.lark b/launch/launch/frontend/grammar.lark similarity index 100% rename from launch/launch/launch_frontend/grammar.lark rename to launch/launch/frontend/grammar.lark diff --git a/launch/launch/launch_frontend/parse_substitution.py b/launch/launch/frontend/parse_substitution.py similarity index 100% rename from launch/launch/launch_frontend/parse_substitution.py rename to launch/launch/frontend/parse_substitution.py diff --git a/launch/launch/launch_frontend/parser.py b/launch/launch/frontend/parser.py similarity index 89% rename from launch/launch/launch_frontend/parser.py rename to launch/launch/frontend/parser.py index f446bad15..5280ba427 100644 --- a/launch/launch/launch_frontend/parser.py +++ b/launch/launch/frontend/parser.py @@ -32,7 +32,7 @@ interpolation_fuctions = { entry_point.name: entry_point.load() - for entry_point in iter_entry_points('launch_frontend.interpolate_substitution') + for entry_point in iter_entry_points('launch.frontend.interpolate_substitution_method') } @@ -42,17 +42,17 @@ class Parser: Implementations of the parser class, should override the load method. They could also override the parse_substitution method, or not. - load_parser_extensions, parse_action and parse_description are not suposed to be overrided. + load_launch_extensions, parse_action and parse_description are not suposed to be overrided. """ extensions_loaded = False frontend_parsers = None @classmethod - def load_parser_extensions(cls): + def load_launch_extensions(cls): """Load launch extensions, in order to get all the exposed substitutions and actions.""" if cls.extensions_loaded is False: - for entry_point in iter_entry_points('launch_frontend.launch_extension'): + for entry_point in iter_entry_points('launch.frontend.launch_extension'): entry_point.load() cls.extensions_loaded = True @@ -62,12 +62,12 @@ def load_parser_implementations(cls): if cls.frontend_parsers is None: cls.frontend_parsers = { entry_point.name: entry_point.load() - for entry_point in iter_entry_points('launch_frontend.parser') + for entry_point in iter_entry_points('launch.frontend.parser') } def parse_action(self, entity: Entity) -> (Action, Tuple[Any]): """Parse an action, using its registered parsing method.""" - self.load_parser_extensions() + self.load_launch_extensions() return instantiate_action(entity, self) def parse_substitution(self, value: Text) -> SomeSubstitutionsType: @@ -77,7 +77,7 @@ def parse_substitution(self, value: Text) -> SomeSubstitutionsType: def parse_description(self, entity: Entity) -> LaunchDescription: """Parse a launch description.""" if entity.type_name != 'launch': - raise RuntimeError('Expected \'launch\' as root tag') + raise RuntimeError("Expected 'launch' as root tag") actions = [self.parse_action(child) for child in entity.children] return LaunchDescription(actions) diff --git a/launch/launch/launch_frontend/type_utils.py b/launch/launch/frontend/type_utils.py similarity index 100% rename from launch/launch/launch_frontend/type_utils.py rename to launch/launch/frontend/type_utils.py diff --git a/launch/launch/launch_description_sources/any_launch_description_source.py b/launch/launch/launch_description_sources/any_launch_description_source.py index 5c9e608cd..f32e8a832 100644 --- a/launch/launch/launch_description_sources/any_launch_description_source.py +++ b/launch/launch/launch_description_sources/any_launch_description_source.py @@ -16,8 +16,8 @@ from .python_launch_file_utilities import get_launch_description_from_python_launch_file from .python_launch_file_utilities import InvalidPythonLaunchFileError +from ..frontend import Parser from ..launch_description_source import LaunchDescriptionSource -from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType diff --git a/launch/launch/launch_description_sources/frontend_launch_description_source.py b/launch/launch/launch_description_sources/frontend_launch_description_source.py index cc8275b9b..512513100 100644 --- a/launch/launch/launch_description_sources/frontend_launch_description_source.py +++ b/launch/launch/launch_description_sources/frontend_launch_description_source.py @@ -16,8 +16,8 @@ from typing import Type +from ..frontend import Parser from ..launch_description_source import LaunchDescriptionSource -from ..launch_frontend import Parser from ..some_substitutions_type import SomeSubstitutionsType diff --git a/launch/launch/substitutions/environment_variable.py b/launch/launch/substitutions/environment_variable.py index ae0dd1a06..24d4743c0 100644 --- a/launch/launch/substitutions/environment_variable.py +++ b/launch/launch/substitutions/environment_variable.py @@ -19,8 +19,8 @@ from typing import List from typing import Text +from ..frontend.expose import expose_substitution from ..launch_context import LaunchContext -from ..launch_frontend.expose import expose_substitution from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution @@ -46,16 +46,16 @@ def __init__( self.__name = normalize_to_list_of_substitutions(name) self.__default_value = normalize_to_list_of_substitutions(default_value) - @staticmethod - def parse(data: Iterable[SomeSubstitutionsType]): + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): """Parse `EnviromentVariable` substitution.""" - if not data or len(data) > 2: + if len(data) < 1 or len(data) > 2: raise TypeError('env substitution expects 1 or 2 arguments') kwargs = {} kwargs['name'] = data[0] if len(data) == 2: kwargs['default_value'] = data[1] - return EnvironmentVariable, kwargs + return cls, kwargs @property def name(self) -> List[Substitution]: diff --git a/launch/launch/substitutions/find_executable.py b/launch/launch/substitutions/find_executable.py index 4bd18288d..1b77df33c 100644 --- a/launch/launch/substitutions/find_executable.py +++ b/launch/launch/substitutions/find_executable.py @@ -21,8 +21,8 @@ from osrf_pycommon.process_utils import which from .substitution_failure import SubstitutionFailure +from ..frontend import expose_substitution from ..launch_context import LaunchContext -from ..launch_frontend import expose_substitution from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution @@ -42,14 +42,12 @@ def __init__(self, *, name: SomeSubstitutionsType) -> None: from ..utilities import normalize_to_list_of_substitutions # import here to avoid loop self.__name = normalize_to_list_of_substitutions(name) - @staticmethod - def parse(data: Iterable[SomeSubstitutionsType]): + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): """Parse `FindExecutable` substitution.""" - if not data or len(data) > 1: + if len(data) != 1: raise AttributeError('find-exec substitution expects 1 argument') - kwargs = {} - kwargs['name'] = data[0] - return FindExecutable, kwargs + return cls, {'name': data[0]} @property def name(self) -> List[Substitution]: diff --git a/launch/launch/substitutions/launch_configuration.py b/launch/launch/substitutions/launch_configuration.py index d4316131e..deb74391b 100644 --- a/launch/launch/substitutions/launch_configuration.py +++ b/launch/launch/substitutions/launch_configuration.py @@ -23,8 +23,8 @@ from typing import Union from .substitution_failure import SubstitutionFailure +from ..frontend import expose_substitution from ..launch_context import LaunchContext -from ..launch_frontend import expose_substitution from ..some_substitutions_type import SomeSubstitutionsType from ..substitution import Substitution @@ -64,16 +64,16 @@ def __init__( normalize_to_list_of_substitutions( str_normalized_default) # type: List[Substitution] - @staticmethod - def parse(data: Iterable[SomeSubstitutionsType]): + @classmethod + def parse(cls, data: Iterable[SomeSubstitutionsType]): """Parse `FindExecutable` substitution.""" - if not data or len(data) > 2: + if len(data) < 1 or len(data) > 2: raise TypeError('var substitution expects 1 or 2 arguments') kwargs = {} kwargs['variable_name'] = data[0] if len(data) == 2: kwargs['default'] = data[1] - return LaunchConfiguration, kwargs + return cls, kwargs @property def variable_name(self) -> List[Substitution]: diff --git a/launch/test/launch/launch_frontend/test_expose_decorators.py b/launch/test/launch/launch_frontend/test_expose_decorators.py index d84b2caae..80fc480b9 100644 --- a/launch/test/launch/launch_frontend/test_expose_decorators.py +++ b/launch/test/launch/launch_frontend/test_expose_decorators.py @@ -12,15 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from launch.launch_frontend.expose import __expose_impl +from launch.frontend.expose import __expose_impl import pytest class ToBeExposed: - @staticmethod - def parse(entity, parser): + @classmethod + def parse(cls, entity, parser): return ToBeExposed(), () @@ -39,7 +39,7 @@ def test_expose_decorators(): expose_test('ToBeExposed')(ToBeExposed) assert 'ToBeExposed' in register if 'ToBeExposed' in register: - assert register['ToBeExposed'] is ToBeExposed.parse + assert register['ToBeExposed'] == ToBeExposed.parse expose_test('to_be_exposed')(to_be_exposed) assert 'to_be_exposed' in register if 'to_be_exposed' in register: diff --git a/launch/test/launch/launch_frontend/test_substitutions.py b/launch/test/launch/launch_frontend/test_substitutions.py index 9bd51f657..bf380b458 100644 --- a/launch/test/launch/launch_frontend/test_substitutions.py +++ b/launch/test/launch/launch_frontend/test_substitutions.py @@ -15,8 +15,8 @@ """Test the default substitution interpolator.""" from launch import LaunchContext -from launch.launch_frontend.expose import expose_substitution -from launch.launch_frontend.parse_substitution import parse_substitution +from launch.frontend.expose import expose_substitution +from launch.frontend.parse_substitution import parse_substitution from launch.substitutions import TextSubstitution diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index fec979cfa..a3ef9b33e 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -21,9 +21,9 @@ from typing import Union import xml.etree.ElementTree as ET -from launch.launch_frontend import Entity as BaseEntity -from launch.launch_frontend.type_utils import get_typed_value -from launch.launch_frontend.type_utils import SomeAllowedTypes +from launch.frontend import Entity as BaseEntity +from launch.frontend.type_utils import get_typed_value +from launch.frontend.type_utils import SomeAllowedTypes class Entity(BaseEntity): diff --git a/launch_xml/launch_xml/parser.py b/launch_xml/launch_xml/parser.py index 502d17494..dff3a0df7 100644 --- a/launch_xml/launch_xml/parser.py +++ b/launch_xml/launch_xml/parser.py @@ -18,12 +18,12 @@ from typing import Union import xml.etree.ElementTree as ET -from launch import launch_frontend +from launch import frontend from .entity import Entity -class Parser(launch_frontend.Parser): +class Parser(frontend.Parser): """XML parser implementation.""" @classmethod diff --git a/launch_xml/setup.py b/launch_xml/setup.py index f3ce5973c..fc6ee5487 100644 --- a/launch_xml/setup.py +++ b/launch_xml/setup.py @@ -27,7 +27,7 @@ license='Apache License, Version 2.0', tests_require=['pytest'], entry_points={ - 'launch_frontend.parser': [ + 'launch.frontend.parser': [ 'xml = launch_xml:Parser', ], } diff --git a/launch_xml/test/launch_xml/test_executable.py b/launch_xml/test/launch_xml/test_executable.py index 364fc6f93..727573248 100644 --- a/launch_xml/test/launch_xml/test_executable.py +++ b/launch_xml/test/launch_xml/test_executable.py @@ -17,7 +17,7 @@ from pathlib import Path from launch import LaunchService -from launch.launch_frontend import Parser +from launch.frontend import Parser def test_executable(): diff --git a/launch_xml/test/launch_xml/test_group.py b/launch_xml/test/launch_xml/test_group.py index ee21ed2a3..303449dd8 100644 --- a/launch_xml/test/launch_xml/test_group.py +++ b/launch_xml/test/launch_xml/test_group.py @@ -18,7 +18,7 @@ import textwrap from launch.actions import SetLaunchConfiguration -from launch.launch_frontend import Parser +from launch.frontend import Parser def test_group(): diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py index ba10f0621..32393b59e 100644 --- a/launch_xml/test/launch_xml/test_include.py +++ b/launch_xml/test/launch_xml/test_include.py @@ -20,8 +20,8 @@ from launch import LaunchService from launch.actions import IncludeLaunchDescription +from launch.frontend import Parser from launch.launch_description_sources import AnyLaunchDescriptionSource -from launch.launch_frontend import Parser def test_include(): diff --git a/launch_xml/test/launch_xml/test_let_var.py b/launch_xml/test/launch_xml/test_let_var.py index 2b4d9b851..43c50f321 100644 --- a/launch_xml/test/launch_xml/test_let_var.py +++ b/launch_xml/test/launch_xml/test_let_var.py @@ -18,7 +18,7 @@ import textwrap from launch import LaunchContext -from launch.launch_frontend import Parser +from launch.frontend import Parser def test_let_var(): diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py index 50c1d19fc..4c323415f 100644 --- a/launch_xml/test/launch_xml/test_list.py +++ b/launch_xml/test/launch_xml/test_list.py @@ -18,7 +18,7 @@ import textwrap from typing import List -from launch.launch_frontend import Parser +from launch.frontend import Parser def test_list(): diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index bde85451d..fe8e9cee8 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -20,9 +20,9 @@ from typing import Type from typing import Union -from launch.launch_frontend import Entity as BaseEntity -from launch.launch_frontend.type_utils import check_type -from launch.launch_frontend.type_utils import SomeAllowedTypes +from launch.frontend import Entity as BaseEntity +from launch.frontend.type_utils import check_type +from launch.frontend.type_utils import SomeAllowedTypes class Entity(BaseEntity): diff --git a/launch_yaml/launch_yaml/parser.py b/launch_yaml/launch_yaml/parser.py index 236d0e7de..722961bb4 100644 --- a/launch_yaml/launch_yaml/parser.py +++ b/launch_yaml/launch_yaml/parser.py @@ -17,14 +17,14 @@ import io from typing import Union -from launch import launch_frontend +from launch import frontend import yaml from .entity import Entity -class Parser(launch_frontend.Parser): +class Parser(frontend.Parser): """YAML parser implementation.""" @classmethod diff --git a/launch_yaml/setup.py b/launch_yaml/setup.py index 737ae3fea..bbd07e7b2 100644 --- a/launch_yaml/setup.py +++ b/launch_yaml/setup.py @@ -27,7 +27,7 @@ license='Apache License, Version 2.0', tests_require=['pytest'], entry_points={ - 'launch_frontend.parser': [ + 'launch.frontend.parser': [ 'yaml = launch_yaml:Parser', ], } diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py index b4444b8ce..dd5cac55e 100644 --- a/launch_yaml/test/launch_yaml/test_executable.py +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -18,7 +18,7 @@ import textwrap from launch import LaunchService -from launch.launch_frontend import Parser +from launch.frontend import Parser def test_executable(): diff --git a/launch_yaml/test/launch_yaml/test_group.py b/launch_yaml/test/launch_yaml/test_group.py index 522c844b4..0c16e0118 100644 --- a/launch_yaml/test/launch_yaml/test_group.py +++ b/launch_yaml/test/launch_yaml/test_group.py @@ -18,7 +18,7 @@ import textwrap from launch.actions import SetLaunchConfiguration -from launch.launch_frontend import Parser +from launch.frontend import Parser def test_group(): From fc8b2c8a9a3da25f9c38392086dd08e8c687a9e7 Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 3 Jul 2019 17:20:22 -0300 Subject: [PATCH 54/75] Address remaining comments Signed-off-by: ivan --- launch/launch/actions/include_launch_description.py | 6 +++--- launch/launch/frontend/entity.py | 8 +++++--- .../any_launch_description_source.py | 4 ++-- launch/launch/substitutions/environment_variable.py | 3 +-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/launch/launch/actions/include_launch_description.py b/launch/launch/actions/include_launch_description.py index 45b9cd57c..64a1ef36f 100644 --- a/launch/launch/actions/include_launch_description.py +++ b/launch/launch/actions/include_launch_description.py @@ -78,8 +78,8 @@ def __init__( def parse(cls, entity: Entity, parser: Parser): """Return `IncludeLaunchDescription` action and kwargs for constructing it.""" _, kwargs = super().parse(entity, parser) - file = parser.parse_substitution(entity.get_attr('file')) - kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file) + file_path = parser.parse_substitution(entity.get_attr('file')) + kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file_path) args = entity.get_attr('arg', types=List[Entity], optional=True) if args is not None: kwargs['launch_arguments'] = [ @@ -89,7 +89,7 @@ def parse(cls, entity: Entity, parser: Parser): ) for e in args ] - return IncludeLaunchDescription, kwargs + return cls, kwargs @property def launch_description_source(self) -> LaunchDescriptionSource: diff --git a/launch/launch/frontend/entity.py b/launch/launch/frontend/entity.py index b3aba3a0f..8a5fb1e38 100644 --- a/launch/launch/frontend/entity.py +++ b/launch/launch/frontend/entity.py @@ -82,14 +82,16 @@ def get_attr( Types that can not be combined with the others: - `List[Entity]` - `types = None` work in the same way as: + `types=None` implies: `(int, float, bool, List[int], List[float], List[bool], List[str], str)` - `List[Entity]` will return a list of more entities. + `List[Entity]` will return the list of entities that are identified by the given + attribute. See the frontend documentation to see how `list` and `List[Entity]` look like for each frontend implementation. - If `optional` argument is `True`, will return `None` instead of raising `AttributeError`. + If `optional` is `True` and the attribute cannot be found, `None` will be returned + instead of raising `AttributeError`. :param name: name of the attribute :param types: type of the attribute to be read. Default to 'str' diff --git a/launch/launch/launch_description_sources/any_launch_description_source.py b/launch/launch/launch_description_sources/any_launch_description_source.py index f32e8a832..329cace24 100644 --- a/launch/launch/launch_description_sources/any_launch_description_source.py +++ b/launch/launch/launch_description_sources/any_launch_description_source.py @@ -27,7 +27,7 @@ class AnyLaunchDescriptionSource(LaunchDescriptionSource): This launch description source will attempt to load the file at the given location as a python launch file first, and as a declarative (markup based) launch file if the former fails. - It is recommended to use a specific `LaunchDescriptionSource` subclasses when possible. + It is recommended to use specific `LaunchDescriptionSource` subclasses when possible. """ def __init__( @@ -62,5 +62,5 @@ def _get_launch_description(self, location): except RuntimeError: pass if launch_description is None: - raise RuntimeError('Can not load launch file') + raise RuntimeError('Cannot load launch file') return launch_description diff --git a/launch/launch/substitutions/environment_variable.py b/launch/launch/substitutions/environment_variable.py index 24d4743c0..f7c4a4691 100644 --- a/launch/launch/substitutions/environment_variable.py +++ b/launch/launch/substitutions/environment_variable.py @@ -51,8 +51,7 @@ def parse(cls, data: Iterable[SomeSubstitutionsType]): """Parse `EnviromentVariable` substitution.""" if len(data) < 1 or len(data) > 2: raise TypeError('env substitution expects 1 or 2 arguments') - kwargs = {} - kwargs['name'] = data[0] + kwargs = {'name': data[0]} if len(data) == 2: kwargs['default_value'] = data[1] return cls, kwargs From 18cf4a06acaa0b5002e7b5c5f4fe90e03b08651d Mon Sep 17 00:00:00 2001 From: ivan Date: Wed, 3 Jul 2019 17:32:22 -0300 Subject: [PATCH 55/75] Restrict conversions in type_utils Signed-off-by: ivan --- launch/launch/frontend/type_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 7c77db29c..8b3055cb9 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -130,16 +130,19 @@ def get_typed_value( elif not isinstance(types, Iterable): types = [types] - if isinstance(value, list): + value_is_list = isinstance(value, list) + if value_is_list: yaml_value = [yaml.safe_load(x) for x in value] else: yaml_value = yaml.safe_load(value) for x in types: - type_obj, is_list = extract_type(x) + type_obj, type_is_list = extract_type(x) + if value_is_list != type_is_list: + continue if type_obj is float: # Allow coercing int to float - if not is_list: + if not type_is_list: try: return float(yaml_value) except (ValueError, TypeError): From ecf9a04c2dcd1b42da97456c95d28572a920791e Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 4 Jul 2019 09:53:40 -0300 Subject: [PATCH 56/75] Correct ExecuteProcess parsing method Signed-off-by: ivanpauno --- launch/launch/actions/execute_process.py | 46 ++++++++----------- launch_xml/test/launch_xml/executable.xml | 2 +- .../test/launch_yaml/test_executable.py | 3 +- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 8c397e5f2..34cd67d17 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -235,20 +235,32 @@ def parse( cls, entity: Entity, parser: Parser, - optional_cmd: bool = False + cmd_arg_name: str = 'cmd' ): """ Return the `ExecuteProcess` action and kwargs for constructing it. - :param: optional_cmd Allow not specifying `cmd` argument. + :param: cmd_arg_name Allow changing the name of `cmd` tag. Intended for code reuse in derived classes (e.g.: launch_ros.actions.Node). """ - cmd = entity.get_attr('cmd', optional=optional_cmd) - if cmd is not None: - cmd_list = [parser.parse_substitution(cmd)] - else: - cmd_list = [] _, kwargs = super().parse(entity, parser) + + cmd = entity.get_attr(cmd_arg_name) + # `cmd` is supposed to be a list separated with ' '. + # All the found `TextSubstitution` items are split and + # added to the list again as a `TextSubstitution`. + cmd = parser.parse_substitution(cmd) + cmd_list = [] + for arg in cmd: + if isinstance(arg, TextSubstitution): + text = arg.text + text = shlex.split(text) + text = [TextSubstitution(text=item) for item in text] + cmd_list.extend(text) + else: + cmd_list.append(arg) + kwargs[cmd_arg_name] = cmd_list + cwd = entity.get_attr('cwd', optional=True) if cwd is not None: kwargs['cwd'] = parser.parse_substitution(cwd) @@ -271,26 +283,6 @@ def parse( if env is not None: env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} kwargs['additional_env'] = env - args = entity.get_attr('args', optional=True) - # `args` is supposed to be a list separated with ' '. - # All the found `TextSubstitution` items are split and - # added to the list again as a `TextSubstitution`. - if args is not None: - args = parser.parse_substitution(args) - new_args = [] - for arg in args: - if isinstance(arg, TextSubstitution): - text = arg.text - text = shlex.split(text) - text = [TextSubstitution(text=item) for item in text] - new_args.extend(text) - else: - new_args.append(arg) - args = new_args - else: - args = [] - cmd_list.extend(args) - kwargs['cmd'] = cmd_list return cls, kwargs diff --git a/launch_xml/test/launch_xml/executable.xml b/launch_xml/test/launch_xml/executable.xml index 437c7a1da..f0e43b604 100644 --- a/launch_xml/test/launch_xml/executable.xml +++ b/launch_xml/test/launch_xml/executable.xml @@ -1,5 +1,5 @@ - + diff --git a/launch_yaml/test/launch_yaml/test_executable.py b/launch_yaml/test/launch_yaml/test_executable.py index dd5cac55e..d2d0cec9a 100644 --- a/launch_yaml/test/launch_yaml/test_executable.py +++ b/launch_yaml/test/launch_yaml/test_executable.py @@ -27,10 +27,9 @@ def test_executable(): """\ launch: - executable: - cmd: ls + cmd: ls -l -a -s cwd: '/' name: my_ls - args: -l -a -s shell: true output: log launch_prefix: $(env LAUNCH_PREFIX) From 44786033f38205747577b2185bfaf11e8c35ee64 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 4 Jul 2019 15:16:59 -0300 Subject: [PATCH 57/75] Allow quoted strings in substitution grammar Signed-off-by: ivanpauno --- launch/launch/frontend/grammar.lark | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launch/launch/frontend/grammar.lark b/launch/launch/frontend/grammar.lark index 32ebbd793..7f0eed36d 100644 --- a/launch/launch/frontend/grammar.lark +++ b/launch/launch/frontend/grammar.lark @@ -57,5 +57,7 @@ substitution: "$" "(" IDENTIFIER (" " arguments)? ")" fragment: substitution | UNQUOTED_STRING + | SINGLE_QUOTED_STRING + | DOUBLE_QUOTED_STRING template: fragment+ From 6e52e946095cf4e3dcaa1ceff60501cb583107af Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 4 Jul 2019 15:17:37 -0300 Subject: [PATCH 58/75] Avoid using yaml convertions in type_utils Signed-off-by: ivanpauno --- launch/launch/frontend/type_utils.py | 52 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 8b3055cb9..7e7001c51 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -22,8 +22,6 @@ from typing import Type from typing import Union -import yaml - __types_for_guess = ( int, float, bool, List[int], List[float], List[bool], List[str], str @@ -131,32 +129,40 @@ def get_typed_value( types = [types] value_is_list = isinstance(value, list) - if value_is_list: - yaml_value = [yaml.safe_load(x) for x in value] - else: - yaml_value = yaml.safe_load(value) for x in types: type_obj, type_is_list = extract_type(x) - if value_is_list != type_is_list: + if type_obj is bool: + def type_obj(x): + """Convert string to bool value.""" + if x.lower() in ('true', 'yes', 'on', '1', 'false', 'no', 'off', '0'): + return x.lower() in ('true', 'yes', 'on', '1') + raise ValueError() + elif type_obj is str: + def type_obj(x): + """Strip outer quotes if we have them.""" + if x.startswith("'") and x.endswith('"'): + return x[1:-1] + elif x.startswith('"') and x.endswith('"'): + return x[1:-1] + else: + return x + if type_is_list != value_is_list: continue - if type_obj is float: - # Allow coercing int to float - if not type_is_list: - try: - return float(yaml_value) - except (ValueError, TypeError): - continue + if type_is_list: + try: + return [type_obj(x) for x in value] + except ValueError: + pass else: - try: - return [float(x) for x in yaml_value] - except (ValueError, TypeError): - continue - if check_type(yaml_value, x): - # Check if type is ok and do yaml conversion - return yaml_value - if type_obj is str: - return value + break + else: + if isinstance(value, list): + continue + try: + return type_obj(value) + except ValueError: + pass raise ValueError( 'Can not convert value {} to one of the types in {}'.format( value, types From 5dd0ac02d595658831d4c1ea031d6833fa328590 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Thu, 4 Jul 2019 15:18:29 -0300 Subject: [PATCH 59/75] Renamed launch_frontend folder in test to frontend. Updated test_substitution.py Signed-off-by: ivanpauno --- .../{launch_frontend => frontend}/test_expose_decorators.py | 0 .../{launch_frontend => frontend}/test_substitutions.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename launch/test/launch/{launch_frontend => frontend}/test_expose_decorators.py (100%) rename launch/test/launch/{launch_frontend => frontend}/test_substitutions.py (98%) diff --git a/launch/test/launch/launch_frontend/test_expose_decorators.py b/launch/test/launch/frontend/test_expose_decorators.py similarity index 100% rename from launch/test/launch/launch_frontend/test_expose_decorators.py rename to launch/test/launch/frontend/test_expose_decorators.py diff --git a/launch/test/launch/launch_frontend/test_substitutions.py b/launch/test/launch/frontend/test_substitutions.py similarity index 98% rename from launch/test/launch/launch_frontend/test_substitutions.py rename to launch/test/launch/frontend/test_substitutions.py index bf380b458..604497635 100644 --- a/launch/test/launch/launch_frontend/test_substitutions.py +++ b/launch/test/launch/frontend/test_substitutions.py @@ -21,10 +21,10 @@ def test_text_only(): - subst = parse_substitution("\\'yes\\'") + subst = parse_substitution("'yes'") assert len(subst) == 1 assert subst[0].perform(None) == "'yes'" - subst = parse_substitution('\\"yes\\"') + subst = parse_substitution('"yes"') assert len(subst) == 1 assert subst[0].perform(None) == '"yes"' subst = parse_substitution('10') From 85b39c9cf0c2fb66bff6e00886c844784caaa1ee Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 5 Jul 2019 17:47:43 -0300 Subject: [PATCH 60/75] Clearer grammar Signed-off-by: ivanpauno --- launch/launch/frontend/grammar.lark | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/launch/launch/frontend/grammar.lark b/launch/launch/frontend/grammar.lark index 7f0eed36d..342740245 100644 --- a/launch/launch/frontend/grammar.lark +++ b/launch/launch/frontend/grammar.lark @@ -6,7 +6,7 @@ IDENTIFIER: LETTER (LETTER | DIGIT | "_" | "-")* -UNQUOTED_STRING: (/[^'\\"$]|\$(?=!\()|(?<=\\)\$|(?<=\\)\"|(?<=\\)'|(?<=\\)\\/)+ +UNQUOTED_STRING: (/[^$]|\$(?=!\()|(?<=\\)\$/)+ UNQUOTED_RSTRING: (/[^ '\\"$\(\)]|(?<=\\)\(|(?<=\\)\)|\$(?=!\()|(?<=\\)\$/ | "\\\"" | "\\'" | "\\")+ SINGLE_QUOTED_STRING: (/[^'\\$]|\$(?=!\()|(?<=\\)\$/ | "\\'" | "\\")+ @@ -57,7 +57,5 @@ substitution: "$" "(" IDENTIFIER (" " arguments)? ")" fragment: substitution | UNQUOTED_STRING - | SINGLE_QUOTED_STRING - | DOUBLE_QUOTED_STRING template: fragment+ From 2e2f5aeefffdd23afed7cc21f8f19f56e00be06b Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 5 Jul 2019 17:48:31 -0300 Subject: [PATCH 61/75] Update type utils for allowing non uniform lists Signed-off-by: ivanpauno --- launch/launch/frontend/type_utils.py | 112 +++++++++++++++++++-------- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 7e7001c51..4fc823e95 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -22,13 +22,20 @@ from typing import Type from typing import Union +__supported_types = ( + int, float, bool, str, List[str], List[int], List[float], List[bool], list, List +) + +__scalar_types = ( + int, float, bool, str +) + __types_for_guess = ( - int, float, bool, List[int], List[float], - List[bool], List[str], str + int, float, bool, list, str ) -AllowedTypes = Type[Union[__types_for_guess]] +AllowedTypes = Type[Union[__supported_types]] SomeAllowedTypes = Union[AllowedTypes, Iterable[AllowedTypes]] @@ -45,6 +52,7 @@ def extract_type(data_type: AllowedTypes): - `List[int]` - `List[float]` - `List[bool]` + - `list` or `List` :returns: a tuple (type_obj, is_list). is_list is `True` for the supported list types, if not is `False`. @@ -53,13 +61,17 @@ def extract_type(data_type: AllowedTypes): e.g.: `name = List[int]` -> `(int, True)` `name = bool` -> `(bool, False)` + For `data_type=list`, the returned value is (None, True). """ - is_list = False - if data_type not in __types_for_guess: + if data_type not in __supported_types: raise ValueError('Unrecognized data type: {}'.format(data_type)) + is_list = False if issubclass(data_type, List): is_list = True data_type = data_type.__args__[0] + elif data_type is list: + is_list = True + data_type = None return (data_type, is_list) @@ -76,9 +88,10 @@ def check_type(value: Any, types: Optional[SomeAllowedTypes]) -> bool: - `List[int]` - `List[float]` - `List[bool]` + - `list` or `List` `types = None` works in the same way as: - `(int, float, bool, List[int], List[float], List[bool], List[str], str)` + `(int, float, bool, list, str)` """ if types is None: types = __types_for_guess @@ -89,7 +102,9 @@ def check_type(value: Any, types: Optional[SomeAllowedTypes]) -> bool: if is_list: if not isinstance(value, list) or not value: continue - if isinstance(value[0], type_obj): + if type_obj is None: + return True + if all(isinstance(x, type_obj) for x in value): return True else: if isinstance(value, type_obj): @@ -97,6 +112,60 @@ def check_type(value: Any, types: Optional[SomeAllowedTypes]) -> bool: return False +def coerce_to_bool(x: str): + """Convert string to bool value.""" + if x.lower() in ('true', 'yes', 'on', '1', 'false', 'no', 'off', '0'): + return x.lower() in ('true', 'yes', 'on', '1') + raise ValueError() + + +def coerce_to_str(x: str): + """Strip outer quotes if we have them.""" + if x.startswith("'") and x.endswith('"'): + return x[1:-1] + elif x.startswith('"') and x.endswith('"'): + return x[1:-1] + else: + return x + + +__coercion_rules = { + str: coerce_to_str, + bool: coerce_to_bool, + int: int, + float: float, +} + + +def coerce_scalar(x: str, types=None): + """ + Convert string to int, flot, bool, str with the above conversion rules. + + If types is not `None`, only those conversions are tried. + If not, all the possible convertions are tried in order. + + :param x: string to be converted. + :param type_obj: should be `int`, `float`, `bool`, `str`. + It can also be an iterable combining the above types, or `None`. + """ + conversions_to_try = types + if conversions_to_try is None: + conversions_to_try = __scalar_types + elif not isinstance(conversions_to_try, Iterable): + conversions_to_try = [conversions_to_try] + for t in conversions_to_try: + try: + return __coercion_rules[t](x) + except ValueError: + pass + raise ValueError('Not conversion is possible') + + +def coerce_list(x: List[str], types=None): + """Coerce each member of the list using `coerce_scalar` function.""" + return [coerce_scalar(i, types) for i in x] + + def get_typed_value( value: Union[Text, List[Text]], types: Optional[SomeAllowedTypes] @@ -116,15 +185,13 @@ def get_typed_value( - `List[int]` - `List[float]` - `List[bool]` + - `list` or `List` `types = None` works in the same way as: - `(int, float, bool, List[int], List[float], List[bool], List[str], str)` + `(int, float, bool, list)` """ if types is None: types = __types_for_guess - elif types == str: - # Avoid evaluating as yaml if types was just `str` - return value elif not isinstance(types, Iterable): types = [types] @@ -132,35 +199,16 @@ def get_typed_value( for x in types: type_obj, type_is_list = extract_type(x) - if type_obj is bool: - def type_obj(x): - """Convert string to bool value.""" - if x.lower() in ('true', 'yes', 'on', '1', 'false', 'no', 'off', '0'): - return x.lower() in ('true', 'yes', 'on', '1') - raise ValueError() - elif type_obj is str: - def type_obj(x): - """Strip outer quotes if we have them.""" - if x.startswith("'") and x.endswith('"'): - return x[1:-1] - elif x.startswith('"') and x.endswith('"'): - return x[1:-1] - else: - return x if type_is_list != value_is_list: continue if type_is_list: try: - return [type_obj(x) for x in value] + return coerce_list(value, type_obj) except ValueError: pass - else: - break else: - if isinstance(value, list): - continue try: - return type_obj(value) + return coerce_scalar(value, type_obj) except ValueError: pass raise ValueError( From afee3e63464b2f0b96022ec51635353a813b212f Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 5 Jul 2019 17:49:13 -0300 Subject: [PATCH 62/75] Add a way of escaping the characters as the string passed by the substitution parser Signed-off-by: ivanpauno --- launch/launch/frontend/parser.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launch/launch/frontend/parser.py b/launch/launch/frontend/parser.py index 5280ba427..256756965 100644 --- a/launch/launch/frontend/parser.py +++ b/launch/launch/frontend/parser.py @@ -25,6 +25,7 @@ from .entity import Entity from .expose import instantiate_action from .parse_substitution import parse_substitution +from .parse_substitution import replace_escaped_characters from ..action import Action from ..launch_description import LaunchDescription from ..some_substitutions_type import SomeSubstitutionsType @@ -74,6 +75,10 @@ def parse_substitution(self, value: Text) -> SomeSubstitutionsType: """Parse a substitution.""" return parse_substitution(value) + def escape_characters(self, value: Text) -> SomeSubstitutionsType: + """Escape characters in strings.""" + return replace_escaped_characters(value) + def parse_description(self, entity: Entity) -> LaunchDescription: """Parse a launch description.""" if entity.type_name != 'launch': From ec3ce5ca954f02f0c1be4bd1ffe4a0311c802c39 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Fri, 5 Jul 2019 17:49:46 -0300 Subject: [PATCH 63/75] Correct execute_process method for correctly escape characters Signed-off-by: ivanpauno --- launch/launch/actions/execute_process.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 34cd67d17..7c7d95f8e 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -272,7 +272,7 @@ def parse( kwargs['prefix'] = parser.parse_substitution(prefix) output = entity.get_attr('output', optional=True) if output is not None: - kwargs['output'] = output + kwargs['output'] = parser.escape_characters(output) shell = entity.get_attr('shell', types=bool, optional=True) if shell is not None: kwargs['shell'] = shell @@ -281,7 +281,10 @@ def parse( # `unset_enviroment_variable` actions should be used. env = entity.get_attr('env', types=List[Entity], optional=True) if env is not None: - env = {e.get_attr('name'): parser.parse_substitution(e.get_attr('value')) for e in env} + env = { + tuple(parser.parse_substitution(e.get_attr('name'))): + parser.parse_substitution(e.get_attr('value')) for e in env + } kwargs['additional_env'] = env return cls, kwargs From 4b3ecf2f46393e2685dda80c114effe435c47f0a Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 8 Jul 2019 12:39:11 -0300 Subject: [PATCH 64/75] Update type_utils in favour of heavy usage of typing objects Signed-off-by: ivanpauno --- launch/launch/action.py | 8 +- launch/launch/actions/execute_process.py | 4 +- launch/launch/actions/group_action.py | 2 +- .../actions/include_launch_description.py | 2 +- launch/launch/frontend/entity.py | 44 ++--- launch/launch/frontend/type_utils.py | 180 ++++++++++-------- launch_xml/README.md | 30 +-- launch_xml/launch_xml/entity.py | 30 +-- launch_xml/test/launch_xml/test_list.py | 6 +- launch_yaml/README.md | 28 +-- launch_yaml/launch_yaml/entity.py | 28 +-- 11 files changed, 170 insertions(+), 192 deletions(-) diff --git a/launch/launch/action.py b/launch/launch/action.py index 21637d5aa..5fd818be2 100644 --- a/launch/launch/action.py +++ b/launch/launch/action.py @@ -65,9 +65,13 @@ def parse(entity: 'Entity', parser: 'Parser'): if if_cond is not None and unless_cond is not None: raise RuntimeError("if and unless conditions can't be used simultaneously") if if_cond is not None: - kwargs['condition'] = IfCondition(predicate_expression=if_cond) + kwargs['condition'] = IfCondition( + predicate_expression=parser.parse_substitution(if_cond) + ) if unless_cond is not None: - kwargs['condition'] = UnlessCondition(predicate_expression=unless_cond) + kwargs['condition'] = UnlessCondition( + predicate_expression=parser.parse_substitution(unless_cond) + ) return Action, kwargs @property diff --git a/launch/launch/actions/execute_process.py b/launch/launch/actions/execute_process.py index 7c7d95f8e..46af6cb0e 100644 --- a/launch/launch/actions/execute_process.py +++ b/launch/launch/actions/execute_process.py @@ -273,13 +273,13 @@ def parse( output = entity.get_attr('output', optional=True) if output is not None: kwargs['output'] = parser.escape_characters(output) - shell = entity.get_attr('shell', types=bool, optional=True) + shell = entity.get_attr('shell', data_type=bool, optional=True) if shell is not None: kwargs['shell'] = shell # Conditions won't be allowed in the `env` tag. # If that feature is needed, `set_enviroment_variable` and # `unset_enviroment_variable` actions should be used. - env = entity.get_attr('env', types=List[Entity], optional=True) + env = entity.get_attr('env', data_type=List[Entity], optional=True) if env is not None: env = { tuple(parser.parse_substitution(e.get_attr('name'))): diff --git a/launch/launch/actions/group_action.py b/launch/launch/actions/group_action.py index 058d0a4da..8c22c7136 100644 --- a/launch/launch/actions/group_action.py +++ b/launch/launch/actions/group_action.py @@ -62,7 +62,7 @@ def __init__( def parse(cls, entity: Entity, parser: Parser): """Return `GroupAction` action and kwargs for constructing it.""" _, kwargs = super().parse(entity, parser) - scoped = entity.get_attr('scoped', types=bool, optional=True) + scoped = entity.get_attr('scoped', data_type=bool, optional=True) if scoped is not None: kwargs['scoped'] = scoped kwargs['actions'] = [parser.parse_action(e) for e in entity.children] diff --git a/launch/launch/actions/include_launch_description.py b/launch/launch/actions/include_launch_description.py index 64a1ef36f..bdff3066a 100644 --- a/launch/launch/actions/include_launch_description.py +++ b/launch/launch/actions/include_launch_description.py @@ -80,7 +80,7 @@ def parse(cls, entity: Entity, parser: Parser): _, kwargs = super().parse(entity, parser) file_path = parser.parse_substitution(entity.get_attr('file')) kwargs['launch_description_source'] = AnyLaunchDescriptionSource(file_path) - args = entity.get_attr('arg', types=List[Entity], optional=True) + args = entity.get_attr('arg', data_type=List[Entity], optional=True) if args is not None: kwargs['launch_arguments'] = [ ( diff --git a/launch/launch/frontend/entity.py b/launch/launch/frontend/entity.py index 8a5fb1e38..50cc94bc1 100644 --- a/launch/launch/frontend/entity.py +++ b/launch/launch/frontend/entity.py @@ -14,14 +14,13 @@ """Module for Entity class.""" +from typing import Any from typing import List from typing import Optional from typing import Text from typing import Type from typing import Union -from .type_utils import SomeAllowedTypes - class Entity: """Single item in the intermediate front_end representation.""" @@ -45,21 +44,11 @@ def get_attr( self, name: Text, *, - types: - Optional[ - Union[ - SomeAllowedTypes, - Type[List['Entity']], - ] - ] = str, + data_type: Any = str, optional: bool = False ) -> Optional[Union[ - Text, - int, - float, - List[Text], - List[int], - List[float], + List[Union[int, str, float, bool]], + Union[int, str, float, bool], List['Entity'] ]]: """ @@ -70,22 +59,15 @@ def get_attr( applied depending on the particular frontend. The allowed types are: - - `str` - - `int` - - `float` - - `bool` - - `List[str]` - - `List[int]` - - `List[float]` - - `List[bool]` - - Types that can not be combined with the others: - - `List[Entity]` - - `types=None` implies: - `(int, float, bool, List[int], List[float], List[bool], List[str], str)` - `List[Entity]` will return the list of entities that are identified by the given - attribute. + - a scalar type: `str`, `int`, `float`, `bool` + - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` + - a non-uniform list. + Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. + `list` or `List`, means that any of the scalar types are allowed. + - an union of the above. + + `types = None` works in the same way as: + `Union[int, float, bool, list, str]` See the frontend documentation to see how `list` and `List[Entity]` look like for each frontend implementation. diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 4fc823e95..60b2b9c09 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -15,44 +15,43 @@ """Module which implements get_typed_value function.""" from typing import Any -from typing import Iterable from typing import List -from typing import Optional from typing import Text -from typing import Type +from typing import Tuple from typing import Union -__supported_types = ( - int, float, bool, str, List[str], List[int], List[float], List[bool], list, List -) - -__scalar_types = ( +__ScalarTypesTuple = ( int, float, bool, str ) -__types_for_guess = ( +__TypesForGuessTuple = ( int, float, bool, list, str ) -AllowedTypes = Type[Union[__supported_types]] -SomeAllowedTypes = Union[AllowedTypes, Iterable[AllowedTypes]] +def get_tuple_of_types(data_type: Any) -> Tuple: + """Convert typing.Union to tuple of types. If not, return `(data_type,)`.""" + if hasattr(data_type, '__origin__') and data_type.__origin__ is Union: + return data_type.__args__ + else: + return (data_type,) + +def check_valid_scalar_type(data_type: Any) -> bool: + """Check if it is a valid scalar type.""" + return all(data_type in __ScalarTypesTuple for x in get_tuple_of_types(data_type)) -def extract_type(data_type: AllowedTypes): + +def extract_type(data_type: Any) -> Tuple[Any, bool]: """ Extract type information from type object. - :param data_type: Can be one of: - - `str` - - `int` - - `float` - - `bool` - - `List[str]` - - `List[int]` - - `List[float]` - - `List[bool]` - - `list` or `List` + :param data_type: It can be: + - a scalar type: `str`, `int`, `float`, `bool` + - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` + - a non-uniform list. + Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. + `list` or `List`, means that any of the scalar types are allowed. :returns: a tuple (type_obj, is_list). is_list is `True` for the supported list types, if not is `False`. @@ -61,42 +60,43 @@ def extract_type(data_type: AllowedTypes): e.g.: `name = List[int]` -> `(int, True)` `name = bool` -> `(bool, False)` - For `data_type=list`, the returned value is (None, True). + `name = Union[bool, str]` -> `(Union[bool, str], False)` + `name = List[Union[bool, str]]` -> `(Union[bool, str], True)` + `name = List -> `(None, True)` """ - if data_type not in __supported_types: - raise ValueError('Unrecognized data type: {}'.format(data_type)) is_list = False - if issubclass(data_type, List): - is_list = True - data_type = data_type.__args__[0] - elif data_type is list: + if data_type is list: is_list = True data_type = None + elif issubclass(data_type, List): + is_list = True + data_type = data_type.__args__[0] + if data_type is not None and check_valid_scalar_type(data_type) is False: + raise ValueError('Unrecognized data type: {}'.format(data_type)) return (data_type, is_list) -def check_type(value: Any, types: Optional[SomeAllowedTypes]) -> bool: +def check_type(value: Any, data_type: Any) -> bool: """ - Check if `value` is one of the types in `types`. + Check if `value` is of `type`. The allowed types are: - - `str` - - `int` - - `float` - - `bool` - - `List[str]` - - `List[int]` - - `List[float]` - - `List[bool]` - - `list` or `List` + - a scalar type: `str`, `int`, `float`, `bool` + - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` + - a non-uniform list. + Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. + `list` or `List`, means that any of the scalar types are allowed. + - an union of the above. `types = None` works in the same way as: - `(int, float, bool, list, str)` + `Union[int, float, bool, list, str]` """ - if types is None: - types = __types_for_guess - elif not isinstance(types, Iterable): - types = [types] + def check_scalar_type(value, data_type): + data_type = get_tuple_of_types(data_type) + return isinstance(value, data_type) + types = __TypesForGuessTuple + if data_type is not None: + data_type = get_tuple_of_types(data_type) for x in types: type_obj, is_list = extract_type(x) if is_list: @@ -104,22 +104,22 @@ def check_type(value: Any, types: Optional[SomeAllowedTypes]) -> bool: continue if type_obj is None: return True - if all(isinstance(x, type_obj) for x in value): + if all(check_scalar_type(x, type_obj) for x in value): return True else: - if isinstance(value, type_obj): + if check_scalar_type(value, type_obj): return True return False -def coerce_to_bool(x: str): +def coerce_to_bool(x: str) -> bool: """Convert string to bool value.""" if x.lower() in ('true', 'yes', 'on', '1', 'false', 'no', 'off', '0'): return x.lower() in ('true', 'yes', 'on', '1') raise ValueError() -def coerce_to_str(x: str): +def coerce_to_str(x: str) -> str: """Strip outer quotes if we have them.""" if x.startswith("'") and x.endswith('"'): return x[1:-1] @@ -129,71 +129,83 @@ def coerce_to_str(x: str): return x -__coercion_rules = { - str: coerce_to_str, - bool: coerce_to_bool, - int: int, - float: float, -} +def scalar_type_key(data_type: Any) -> int: + """Get key. Used for sorting the scalar data_types.""" + keys = { + int: 0, + float: 1, + bool: 2, + str: 3, + } + return keys[data_type] -def coerce_scalar(x: str, types=None): +def coerce_scalar(x: str, data_type: Any = None) -> Union[int, str, float, bool]: """ Convert string to int, flot, bool, str with the above conversion rules. - If types is not `None`, only those conversions are tried. - If not, all the possible convertions are tried in order. + If data_type is not `None`, only those conversions are tried. + If not, all the possible convertions are tried. + The order is always: `int`, `float`, `bool`, `str`. :param x: string to be converted. :param type_obj: should be `int`, `float`, `bool`, `str`. It can also be an iterable combining the above types, or `None`. """ - conversions_to_try = types - if conversions_to_try is None: - conversions_to_try = __scalar_types - elif not isinstance(conversions_to_try, Iterable): - conversions_to_try = [conversions_to_try] + coercion_rules = { + str: coerce_to_str, + bool: coerce_to_bool, + int: int, + float: float, + } + if data_type is None: + conversions_to_try = __ScalarTypesTuple + else: + conversions_to_try = sorted(get_tuple_of_types(data_type), key=scalar_type_key) + if not set(conversions_to_try).issubset(set(__ScalarTypesTuple)): + raise ValueError('Unrecognized data type: {}'.format(data_type)) for t in conversions_to_try: try: - return __coercion_rules[t](x) + return coercion_rules[t](x) except ValueError: pass raise ValueError('Not conversion is possible') -def coerce_list(x: List[str], types=None): +def coerce_list(x: List[str], data_type: Any = None) -> List[Union[int, str, float, bool]]: """Coerce each member of the list using `coerce_scalar` function.""" - return [coerce_scalar(i, types) for i in x] + return [coerce_scalar(i, data_type) for i in x] def get_typed_value( value: Union[Text, List[Text]], - types: Optional[SomeAllowedTypes] -) -> Any: + data_type: Any +) -> Union[ + List[Union[int, str, float, bool]], + Union[int, str, float, bool], +]: """ - Try to convert `value` to one of the types specified in `types`. + Try to convert `value` to the type specified in `data_type`. - It returns the first successful conversion. If not raise `AttributeError`. The allowed types are: - - `str` - - `int` - - `float` - - `bool` - - `List[str]` - - `List[int]` - - `List[float]` - - `List[bool]` - - `list` or `List` + - a scalar type: `str`, `int`, `float`, `bool` + - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` + - a non-uniform list. + Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. + `list` or `List`, means that any of the scalar types are allowed. + - an union of the above. `types = None` works in the same way as: - `(int, float, bool, list)` + `Union[int, float, bool, list, str]` + + The coercion order for scalars is always: `int`, `float`, `bool`, `str`. """ - if types is None: - types = __types_for_guess - elif not isinstance(types, Iterable): - types = [types] + if data_type is None: + types = __TypesForGuessTuple + else: + types = get_tuple_of_types(data_type) value_is_list = isinstance(value, list) diff --git a/launch_xml/README.md b/launch_xml/README.md index 66d6ba5cb..cb318f158 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -15,22 +15,24 @@ When having an xml tag like: If the entity `e` is wrapping it, the following statements will be true: ```python e.get_attr('value') == '2' -e.get_attr('value', types=int) == 2 -e.get_attr('value', types=float) == 2.0 +e.get_attr('value', data_type=int) == 2 +e.get_attr('value', data_type=float) == 2.0 ``` By default, the value of the attribute is returned as a string. -Allowed types are: -```python -str, int, float, bool, List[int], List[float], List[bool], List[str] -``` + +Allowed types are: + - scalar types: `str, int, float, bool` + - lists: Can be uniform like `List[int]`, or non-uniform like `List[Union[str, int]]`, `List` (same as `list`). + In any case, the members should be of one of the scalar types. + - An union of both any of the above. e.g.: `Union[List[int], int]`. + - The list of entities type: `List[Entity]` (see below). + `List` is the usual object from the `typing` package. -A combination of them can be specified with a tuple. e.g.: `(int, str)`. -In that case, conversions are tried in order and the first successful conversion is returned. -`types` can also be set to `None`, which works in the same way as passing: +`data_type` can also be set to `None`, which works in the same way as passing: ```python -int, float, bool, List[int], List[float], List[bool], List[str], str +Union[int, float, bool, list, str] ``` For handling lists, the `*-sep` attribute is used. e.g.: @@ -42,9 +44,9 @@ For handling lists, the `*-sep` attribute is used. e.g.: ``` ```python -tag.get_attr('value', types=List[int]) == [2, 3, 4] -tag2.get_attr('value', types=List[float]) == [2.0, 3.0, 4.0] -tag3.get_attr('value', types=List[str]) == ['2', '3', '4'] +tag.get_attr('value', data_type=List[int]) == [2, 3, 4] +tag2.get_attr('value', data_type=List[float]) == [2.0, 3.0, 4.0] +tag3.get_attr('value', data_type=List[str]) == ['2', '3', '4'] ``` For checking if an attribute exists, use an optional argument: @@ -71,7 +73,7 @@ In this xml: The `env` children could be accessed like: ```python -env = e.get_attr('env', types=List[Entity]) +env = e.get_attr('env', data_type=List[Entity]) len(env) == 2 env[0].get_attr('name') == 'a' env[0].get_attr('value') == '100' diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index a3ef9b33e..c6576ffcb 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -14,16 +14,15 @@ """Module for Entity class.""" +from typing import Any from typing import List from typing import Optional from typing import Text -from typing import Type from typing import Union import xml.etree.ElementTree as ET from launch.frontend import Entity as BaseEntity from launch.frontend.type_utils import get_typed_value -from launch.frontend.type_utils import SomeAllowedTypes class Entity(BaseEntity): @@ -58,32 +57,21 @@ def get_attr( self, name: Text, *, - types: - Optional[ - Union[ - SomeAllowedTypes, - Type[List[BaseEntity]], - Type[List['Entity']], - ] - ] = str, + data_type: Any = str, optional: bool = False ) -> Optional[Union[ - Text, - int, - float, - List[Text], - List[int], - List[float], + List[Union[int, str, float, bool]], + Union[int, str, float, bool], List['Entity'] ]]: """Access an attribute of the entity.""" attr_error = AttributeError( 'Attribute {} of type {} not found in Entity {}'.format( - name, types, self.type_name + name, data_type, self.type_name ) ) - is_list_entity = types is not None and not isinstance(types, tuple) \ - and issubclass(types, List) and issubclass(types.__args__[0], BaseEntity) + is_list_entity = data_type is not None and not isinstance(data_type, tuple) \ + and issubclass(data_type, List) and issubclass(data_type.__args__[0], BaseEntity) if is_list_entity: return_list = filter(lambda x: x.tag == name, self.__xml_element) if not return_list: @@ -106,12 +94,12 @@ def get_attr( else: return None try: - value = get_typed_value(value, types) + value = get_typed_value(value, data_type) except ValueError: raise TypeError( 'Attribute {} of Entity {} expected to be of type {}.' '`{}` can not be converted to one of those types'.format( - name, self.type_name, types, value + name, self.type_name, data_type, value ) ) return value diff --git a/launch_xml/test/launch_xml/test_list.py b/launch_xml/test/launch_xml/test_list.py index 4c323415f..86244048a 100644 --- a/launch_xml/test/launch_xml/test_list.py +++ b/launch_xml/test/launch_xml/test_list.py @@ -34,9 +34,9 @@ def test_list(): xml_file = textwrap.dedent(xml_file) root_entity, parser = Parser.load(io.StringIO(xml_file)) tags = root_entity.children - assert tags[0].get_attr('attr', types=List[str]) == ['1', '2', '3'] - assert tags[0].get_attr('attr', types=List[int]) == [1, 2, 3] - assert tags[0].get_attr('attr', types=List[float]) == [1., 2., 3.] + assert tags[0].get_attr('attr', data_type=List[str]) == ['1', '2', '3'] + assert tags[0].get_attr('attr', data_type=List[int]) == [1, 2, 3] + assert tags[0].get_attr('attr', data_type=List[float]) == [1., 2., 3.] if __name__ == '__main__': diff --git a/launch_yaml/README.md b/launch_yaml/README.md index 6b8240c71..61cc1aa95 100644 --- a/launch_yaml/README.md +++ b/launch_yaml/README.md @@ -18,28 +18,30 @@ tag: If the entity `e` is wrapping `tag`, the following statement will be true: ```python e.get_attr('value1') == '2' -e.get_attr('value2', types=int) == 2 -e.get_attr('value3', types=float) == 2.0 +e.get_attr('value2', data_type=int) == 2 +e.get_attr('value3', data_type=float) == 2.0 ``` By default, `get_attr` returns an string and it does type checking. The following code will raise a `TypeError`: ```python -e.get_attr('value1', types=int) -e.get_attr('value2', types=float) +e.get_attr('value1', data_type=int) +e.get_attr('value2', data_type=float) e.get_attr('value3') ``` -Allowed types are: -```python -str, int, float, bool, List[int], List[float], List[bool], List[str] -``` -A combination of them can be specified with a tuple. e.g.: `(int, str)`. -In that case, conversions are tried in order and the first successful conversion is returned. -`types` can also be set to `None`, which works in the same way as passing: +Allowed types are: + - scalar types: `str, int, float, bool` + - lists: Can be uniform like `List[int]`, or non-uniform like `List[Union[str, int]]`, `List` (same as `list`). + In any case, the members should be of one of the scalar types. + - An union of both any of the above. e.g.: `Union[List[int], int]`. + - The list of entities type: `List[Entity]` (see below). + +`List` is the usual object from the `typing` package. +`data_type` can also be set to `None`, which works in the same way as passing: ```python -int, float, bool, List[int], List[float], List[bool], List[str], str +Union[int, float, bool, list, str] ``` For checking if an attribute exists, use optional argument: @@ -69,7 +71,7 @@ executable: The `env` children could be accessed doing: ```python -env = e.get_attr('env', types=List[Entity]) +env = e.get_attr('env', data_type=List[Entity]) len(env) == 2 env[0].get_attr('name') == 'a' env[0].get_attr('value') == '100' diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index fe8e9cee8..8fa14c5a2 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -14,15 +14,14 @@ """Module for YAML Entity class.""" +from typing import Any from typing import List from typing import Optional from typing import Text -from typing import Type from typing import Union from launch.frontend import Entity as BaseEntity from launch.frontend.type_utils import check_type -from launch.frontend.type_utils import SomeAllowedTypes class Entity(BaseEntity): @@ -75,22 +74,11 @@ def get_attr( self, name: Text, *, - types: - Optional[ - Union[ - SomeAllowedTypes, - Type[List[BaseEntity]], - Type[List['Entity']], - ] - ] = str, + data_type: Any = str, optional: bool = False ) -> Optional[Union[ - Text, - int, - float, - List[Text], - List[int], - List[float], + List[Union[int, str, float, bool]], + Union[int, str, float, bool], List['Entity'] ]]: """Access an attribute of the entity.""" @@ -102,8 +90,8 @@ def get_attr( else: return None data = self.__element[name] - is_list_entity = types is not None and not isinstance(types, tuple) \ - and issubclass(types, List) and issubclass(types.__args__[0], BaseEntity) + is_list_entity = data_type is not None and not isinstance(data_type, tuple) \ + and issubclass(data_type, List) and issubclass(data_type.__args__[0], BaseEntity) if is_list_entity: if isinstance(data, list) and isinstance(data[0], dict): return [Entity(child, name) for child in data] @@ -112,10 +100,10 @@ def get_attr( name, self.type_name ) ) - if not check_type(data, types): + if not check_type(data, data_type): raise TypeError( 'Attribute {} of Entity {} expected to be of type {}, got {}'.format( - name, self.type_name, types, type(data) + name, self.type_name, data_type, type(data) ) ) return data From 38b8c0c0752ec2207c1f396e8501ccc6ce4c18f0 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 8 Jul 2019 13:15:08 -0300 Subject: [PATCH 65/75] Correct error in coerce_to_str method Signed-off-by: ivanpauno --- launch/launch/frontend/type_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 60b2b9c09..73a127e3a 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -121,7 +121,7 @@ def coerce_to_bool(x: str) -> bool: def coerce_to_str(x: str) -> str: """Strip outer quotes if we have them.""" - if x.startswith("'") and x.endswith('"'): + if x.startswith("'") and x.endswith("'"): return x[1:-1] elif x.startswith('"') and x.endswith('"'): return x[1:-1] From efaf9c308d8f6d491c9ef5b59f2cc5fb3e35e6fd Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 8 Jul 2019 16:38:34 -0300 Subject: [PATCH 66/75] Install grammar.lark correctly Signed-off-by: ivanpauno --- launch/launch/frontend/parse_substitution.py | 4 +++- launch/package.xml | 2 ++ launch/setup.py | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/launch/launch/frontend/parse_substitution.py b/launch/launch/frontend/parse_substitution.py index f6024490c..000c5704c 100644 --- a/launch/launch/frontend/parse_substitution.py +++ b/launch/launch/frontend/parse_substitution.py @@ -18,6 +18,8 @@ import re from typing import Text +from ament_index_python.packages import get_package_share_directory + from lark import Lark from lark import Token from lark import Transformer @@ -91,7 +93,7 @@ def template(self, fragments): double_quoted_template = template -grammar_file = os.path.join(os.path.dirname(__file__), 'grammar.lark') +grammar_file = os.path.join(get_package_share_directory('launch'), 'frontend', 'grammar.lark') parser = Lark.open(grammar_file, start='template') transformer = ExtractSubstitution() diff --git a/launch/package.xml b/launch/package.xml index 2f6acca6b..5fd355f35 100644 --- a/launch/package.xml +++ b/launch/package.xml @@ -9,6 +9,8 @@ osrf_pycommon + ament_index_python + ament_copyright ament_flake8 ament_pep257 diff --git a/launch/setup.py b/launch/setup.py index 70cfe1688..dce98bebe 100644 --- a/launch/setup.py +++ b/launch/setup.py @@ -1,10 +1,14 @@ from setuptools import find_packages from setuptools import setup + setup( name='launch', version='0.8.3', packages=find_packages(exclude=['test']), + data_files=[ + ('share/launch/frontend', ['launch/frontend/grammar.lark']), + ], install_requires=['setuptools'], zip_safe=True, author='Dirk Thomas', From 3822fcdcf0a0574a6d29bd7a138d588ff5f57acf Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Mon, 8 Jul 2019 17:01:25 -0300 Subject: [PATCH 67/75] Solved strange problem when installing grammar.lark when using --symlink-install Signed-off-by: ivanpauno --- launch/setup.py | 2 +- launch/{ => share}/launch/frontend/grammar.lark | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename launch/{ => share}/launch/frontend/grammar.lark (100%) diff --git a/launch/setup.py b/launch/setup.py index dce98bebe..bcaec57af 100644 --- a/launch/setup.py +++ b/launch/setup.py @@ -7,7 +7,7 @@ version='0.8.3', packages=find_packages(exclude=['test']), data_files=[ - ('share/launch/frontend', ['launch/frontend/grammar.lark']), + ('share/launch/frontend', ['share/launch/frontend/grammar.lark']), ], install_requires=['setuptools'], zip_safe=True, diff --git a/launch/launch/frontend/grammar.lark b/launch/share/launch/frontend/grammar.lark similarity index 100% rename from launch/launch/frontend/grammar.lark rename to launch/share/launch/frontend/grammar.lark From 1b73e5dfb257fee24b73f196a73e5fb6fc110cb1 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 09:27:57 -0300 Subject: [PATCH 68/75] Please flake8 Signed-off-by: ivanpauno --- launch/launch/frontend/entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/launch/launch/frontend/entity.py b/launch/launch/frontend/entity.py index 50cc94bc1..bf1e03f9e 100644 --- a/launch/launch/frontend/entity.py +++ b/launch/launch/frontend/entity.py @@ -18,7 +18,6 @@ from typing import List from typing import Optional from typing import Text -from typing import Type from typing import Union From f15b0f21739d9c2b4514a658e73287238d7642c9 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 10:21:35 -0300 Subject: [PATCH 69/75] Avoid using issubclass with typing objects Signed-off-by: ivanpauno --- launch/launch/frontend/type_utils.py | 28 ++++++++++++++++++++++++++-- launch_xml/launch_xml/entity.py | 5 ++--- launch_yaml/launch_yaml/entity.py | 5 ++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 73a127e3a..4d3aff2d7 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -20,6 +20,8 @@ from typing import Tuple from typing import Union +from .entity import Entity + __ScalarTypesTuple = ( int, float, bool, str ) @@ -29,9 +31,31 @@ ) +def check_typing_object_origin(data_type: Any, origin: Any) -> bool: + """ + Check if `data_type` was origined from `origin`. + + e.g.: + `check_typing_object_origin(List[int], List)` returns `True`. + `check_typing_object_origin(Union[int, str], Union)` returns `True`. + `check_typing_object_origin(int, Union)` returns `False`. + """ + return hasattr(data_type, '__origin__') and data_type.__origin__ is origin + + +def check_is_list_entity(data_type: Any) -> bool: + """ + Return `True` if `data_type` is `List[launch.frontend.Entity]`. + + It returns also `True` the member type is a subclass of `launch.frontend.Entity`. + """ + return check_typing_object_origin(data_type, List) and \ + issubclass(data_type.__args__[0], Entity) + + def get_tuple_of_types(data_type: Any) -> Tuple: """Convert typing.Union to tuple of types. If not, return `(data_type,)`.""" - if hasattr(data_type, '__origin__') and data_type.__origin__ is Union: + if check_typing_object_origin(data_type, Union): return data_type.__args__ else: return (data_type,) @@ -68,7 +92,7 @@ def extract_type(data_type: Any) -> Tuple[Any, bool]: if data_type is list: is_list = True data_type = None - elif issubclass(data_type, List): + elif check_typing_object_origin(data_type, List): is_list = True data_type = data_type.__args__[0] if data_type is not None and check_valid_scalar_type(data_type) is False: diff --git a/launch_xml/launch_xml/entity.py b/launch_xml/launch_xml/entity.py index c6576ffcb..8b4a33837 100644 --- a/launch_xml/launch_xml/entity.py +++ b/launch_xml/launch_xml/entity.py @@ -22,6 +22,7 @@ import xml.etree.ElementTree as ET from launch.frontend import Entity as BaseEntity +from launch.frontend.type_utils import check_is_list_entity from launch.frontend.type_utils import get_typed_value @@ -70,9 +71,7 @@ def get_attr( name, data_type, self.type_name ) ) - is_list_entity = data_type is not None and not isinstance(data_type, tuple) \ - and issubclass(data_type, List) and issubclass(data_type.__args__[0], BaseEntity) - if is_list_entity: + if check_is_list_entity(data_type): return_list = filter(lambda x: x.tag == name, self.__xml_element) if not return_list: if optional: diff --git a/launch_yaml/launch_yaml/entity.py b/launch_yaml/launch_yaml/entity.py index 8fa14c5a2..1cd2d5cbe 100644 --- a/launch_yaml/launch_yaml/entity.py +++ b/launch_yaml/launch_yaml/entity.py @@ -21,6 +21,7 @@ from typing import Union from launch.frontend import Entity as BaseEntity +from launch.frontend.type_utils import check_is_list_entity from launch.frontend.type_utils import check_type @@ -90,9 +91,7 @@ def get_attr( else: return None data = self.__element[name] - is_list_entity = data_type is not None and not isinstance(data_type, tuple) \ - and issubclass(data_type, List) and issubclass(data_type.__args__[0], BaseEntity) - if is_list_entity: + if check_is_list_entity(data_type): if isinstance(data, list) and isinstance(data[0], dict): return [Entity(child, name) for child in data] raise TypeError( From b5f6a95f9c3bac97ff38eb787fcf29a25b1c2dc9 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 11:03:06 -0300 Subject: [PATCH 70/75] Add missing dependency with lark parcer Signed-off-by: ivanpauno --- launch/package.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/launch/package.xml b/launch/package.xml index 5fd355f35..851ec7032 100644 --- a/launch/package.xml +++ b/launch/package.xml @@ -10,6 +10,7 @@ osrf_pycommon ament_index_python + python3-lark-parser ament_copyright ament_flake8 From 3c81e11a5d862b40c760aada03df6d26b30e2ba2 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 11:37:04 -0300 Subject: [PATCH 71/75] Use posix style paths in launch files Signed-off-by: ivanpauno --- launch_xml/test/launch_xml/test_include.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py index 32393b59e..95c7333e1 100644 --- a/launch_xml/test/launch_xml/test_include.py +++ b/launch_xml/test/launch_xml/test_include.py @@ -15,7 +15,6 @@ """Test parsing an include action.""" import io -from pathlib import Path import textwrap from launch import LaunchService @@ -26,7 +25,7 @@ def test_include(): """Parse node xml example.""" - path = str(Path(__file__).parent / 'executable.xml') + path = __file__ + '/executable.xml' xml_file = \ """\ From ba9ab096be27a90c849f5544d23f939fcd61ed18 Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 11:47:47 -0300 Subject: [PATCH 72/75] Correct type_utils on windows Signed-off-by: ivanpauno --- launch/launch/frontend/type_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 4d3aff2d7..f44ace193 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -40,7 +40,9 @@ def check_typing_object_origin(data_type: Any, origin: Any) -> bool: `check_typing_object_origin(Union[int, str], Union)` returns `True`. `check_typing_object_origin(int, Union)` returns `False`. """ - return hasattr(data_type, '__origin__') and data_type.__origin__ is origin + # Checking __origin__ in two ways, because it works differently on Linux/Windows. + return hasattr(data_type, '__origin__') and \ + (data_type.__origin__ is origin or data_type.__origin__ is origin.__origin__) def check_is_list_entity(data_type: Any) -> bool: From 7b073333e009f7f5137fb80eb5a78bde7a5cb6ea Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 13:51:50 -0300 Subject: [PATCH 73/75] Further corrections of type_utils on Windows Signed-off-by: ivanpauno --- launch/launch/frontend/type_utils.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index f44ace193..5b5fc29a5 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -31,18 +31,16 @@ ) -def check_typing_object_origin(data_type: Any, origin: Any) -> bool: - """ - Check if `data_type` was origined from `origin`. +def check_is_list(data_type: Any) -> bool: + """Check if `data_type` was origined from `typing.List`.""" + return hasattr(data_type, '__origin__') and \ + data_type.__origin__ in (list, List) # On Linux/Mac is List, on Windows is list. - e.g.: - `check_typing_object_origin(List[int], List)` returns `True`. - `check_typing_object_origin(Union[int, str], Union)` returns `True`. - `check_typing_object_origin(int, Union)` returns `False`. - """ - # Checking __origin__ in two ways, because it works differently on Linux/Windows. + +def check_is_union(data_type: Any) -> bool: + """Check if `data_type` was origined from `typing.Union`.""" return hasattr(data_type, '__origin__') and \ - (data_type.__origin__ is origin or data_type.__origin__ is origin.__origin__) + data_type.__origin__ is Union def check_is_list_entity(data_type: Any) -> bool: @@ -51,13 +49,13 @@ def check_is_list_entity(data_type: Any) -> bool: It returns also `True` the member type is a subclass of `launch.frontend.Entity`. """ - return check_typing_object_origin(data_type, List) and \ + return check_is_list(data_type) and \ issubclass(data_type.__args__[0], Entity) def get_tuple_of_types(data_type: Any) -> Tuple: """Convert typing.Union to tuple of types. If not, return `(data_type,)`.""" - if check_typing_object_origin(data_type, Union): + if check_is_union(data_type): return data_type.__args__ else: return (data_type,) @@ -94,7 +92,7 @@ def extract_type(data_type: Any) -> Tuple[Any, bool]: if data_type is list: is_list = True data_type = None - elif check_typing_object_origin(data_type, List): + elif check_is_list(data_type): is_list = True data_type = data_type.__args__[0] if data_type is not None and check_valid_scalar_type(data_type) is False: From 091ae9c8882e0332498dc848eb30c7593094b4ab Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Tue, 9 Jul 2019 14:02:12 -0300 Subject: [PATCH 74/75] Solve path problem in test_include Signed-off-by: ivanpauno --- launch_xml/test/launch_xml/test_include.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launch_xml/test/launch_xml/test_include.py b/launch_xml/test/launch_xml/test_include.py index 95c7333e1..9fc23fc2e 100644 --- a/launch_xml/test/launch_xml/test_include.py +++ b/launch_xml/test/launch_xml/test_include.py @@ -15,6 +15,7 @@ """Test parsing an include action.""" import io +from pathlib import Path import textwrap from launch import LaunchService @@ -25,7 +26,8 @@ def test_include(): """Parse node xml example.""" - path = __file__ + '/executable.xml' + # Always use posix style paths in launch XML files. + path = (Path(__file__).parent / 'executable.xml').as_posix() xml_file = \ """\ From deada40c053b45f682a45f33b8fc53c5c817391f Mon Sep 17 00:00:00 2001 From: ivanpauno Date: Wed, 10 Jul 2019 18:22:59 -0300 Subject: [PATCH 75/75] Address review comments Signed-off-by: ivanpauno --- launch/launch/frontend/entity.py | 17 +++++----- launch/launch/frontend/type_utils.py | 50 +++++++++++++--------------- launch_xml/README.md | 6 ++-- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/launch/launch/frontend/entity.py b/launch/launch/frontend/entity.py index bf1e03f9e..ccc66de12 100644 --- a/launch/launch/frontend/entity.py +++ b/launch/launch/frontend/entity.py @@ -58,18 +58,19 @@ def get_attr( applied depending on the particular frontend. The allowed types are: - - a scalar type: `str`, `int`, `float`, `bool` - - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` - - a non-uniform list. - Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. - `list` or `List`, means that any of the scalar types are allowed. - - an union of the above. + - a scalar type i.e. `str`, `int`, `float`, `bool`; + - a uniform list i.e `List[str]`, `List[int]`, `List[float]`, `List[bool]`; + - a non-uniform list of known scalar types e.g. `List[Union[int, str]]`; + - a non-uniform list of any scalar type i.e. `list` or `List`; + - a `Union` of any of the above; + - `List[Entity]`, see below. `types = None` works in the same way as: `Union[int, float, bool, list, str]` - See the frontend documentation to see how `list` and `List[Entity]` look like for each - frontend implementation. + `List[Entity]` allows accessing a list of subentities with an specific name. + Check the documentation of each specific frontend implementation to see how `list` + and `List[Entity]` look like. If `optional` is `True` and the attribute cannot be found, `None` will be returned instead of raising `AttributeError`. diff --git a/launch/launch/frontend/type_utils.py b/launch/launch/frontend/type_utils.py index 5b5fc29a5..7e676fcd1 100644 --- a/launch/launch/frontend/type_utils.py +++ b/launch/launch/frontend/type_utils.py @@ -32,29 +32,30 @@ def check_is_list(data_type: Any) -> bool: - """Check if `data_type` was origined from `typing.List`.""" + """Check if `data_type` is based on a `typing.List`.""" return hasattr(data_type, '__origin__') and \ data_type.__origin__ in (list, List) # On Linux/Mac is List, on Windows is list. def check_is_union(data_type: Any) -> bool: - """Check if `data_type` was origined from `typing.Union`.""" + """Check if `data_type` is based on a `typing.Union`.""" return hasattr(data_type, '__origin__') and \ data_type.__origin__ is Union def check_is_list_entity(data_type: Any) -> bool: - """ - Return `True` if `data_type` is `List[launch.frontend.Entity]`. - - It returns also `True` the member type is a subclass of `launch.frontend.Entity`. - """ + """Check if `data_type` is a `typing.List` with elements of `Entity` type or derived.""" return check_is_list(data_type) and \ issubclass(data_type.__args__[0], Entity) def get_tuple_of_types(data_type: Any) -> Tuple: - """Convert typing.Union to tuple of types. If not, return `(data_type,)`.""" + """ + Normalize `data_type` to a tuple of types. + + If `data_type` is based on a `typing.Union`, return union types. + Otherwise, return a `(data_type,)` tuple. + """ if check_is_union(data_type): return data_type.__args__ else: @@ -62,7 +63,7 @@ def get_tuple_of_types(data_type: Any) -> Tuple: def check_valid_scalar_type(data_type: Any) -> bool: - """Check if it is a valid scalar type.""" + """Check if `data_type` is a valid scalar type.""" return all(data_type in __ScalarTypesTuple for x in get_tuple_of_types(data_type)) @@ -71,11 +72,10 @@ def extract_type(data_type: Any) -> Tuple[Any, bool]: Extract type information from type object. :param data_type: It can be: - - a scalar type: `str`, `int`, `float`, `bool` - - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` - - a non-uniform list. - Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. - `list` or `List`, means that any of the scalar types are allowed. + - a scalar type i.e. `str`, `int`, `float`, `bool`; + - a uniform list i.e `List[str]`, `List[int]`, `List[float]`, `List[bool]`; + - a non-uniform list of known scalar types e.g. `List[Union[int, str]]`; + - a non-uniform list of any scalar type i.e. `list` or `List`; :returns: a tuple (type_obj, is_list). is_list is `True` for the supported list types, if not is `False`. @@ -105,12 +105,11 @@ def check_type(value: Any, data_type: Any) -> bool: Check if `value` is of `type`. The allowed types are: - - a scalar type: `str`, `int`, `float`, `bool` - - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` - - a non-uniform list. - Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. - `list` or `List`, means that any of the scalar types are allowed. - - an union of the above. + - a scalar type i.e. `str`, `int`, `float`, `bool`; + - a uniform list i.e `List[str]`, `List[int]`, `List[float]`, `List[bool]`; + - a non-uniform list of known scalar types e.g. `List[Union[int, str]]`; + - a non-uniform list of any scalar type i.e. `list` or `List`; + - a `Union` of any of the above. `types = None` works in the same way as: `Union[int, float, bool, list, str]` @@ -214,12 +213,11 @@ def get_typed_value( If not raise `AttributeError`. The allowed types are: - - a scalar type: `str`, `int`, `float`, `bool` - - a uniform list: `List[str]`, `List[int]`, `List[float]`, `List[bool]` - - a non-uniform list. - Can specify some of the scalar types for its members, e.g.: List[Union[int, str]]. - `list` or `List`, means that any of the scalar types are allowed. - - an union of the above. + - a scalar type i.e. `str`, `int`, `float`, `bool`; + - a uniform list i.e `List[str]`, `List[int]`, `List[float]`, `List[bool]`; + - a non-uniform list of known scalar types e.g. `List[Union[int, str]]`; + - a non-uniform list of any scalar type i.e. `list` or `List`; + - a `Union` of any of the above. `types = None` works in the same way as: `Union[int, float, bool, list, str]` diff --git a/launch_xml/README.md b/launch_xml/README.md index cb318f158..9cac9191e 100644 --- a/launch_xml/README.md +++ b/launch_xml/README.md @@ -38,9 +38,9 @@ Union[int, float, bool, list, str] For handling lists, the `*-sep` attribute is used. e.g.: ```xml - - - + + + ``` ```python