From 2b97076a36d3efede6dd0da591155b2698c61068 Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Mon, 4 Dec 2017 23:50:28 +1100 Subject: [PATCH 1/7] Fix typo --- doc/HowToWriteYourFirstParamsFile.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/HowToWriteYourFirstParamsFile.md b/doc/HowToWriteYourFirstParamsFile.md index c0a5ae3..cf7a1ac 100644 --- a/doc/HowToWriteYourFirstParamsFile.md +++ b/doc/HowToWriteYourFirstParamsFile.md @@ -150,7 +150,7 @@ NOTE: The third parameter should be equal to the params file name, without exten ## Add params file to CMakeLists -In order to make this params file usable it must be executable, so lets use the following command to make it excecutable +In order to make this params file usable it must be executable, so lets use the following command to make it executable ```shell chmod a+x cfg/Tutorials.params From f6afe40ce130984bcb68c0c5ac8a1a20c56b1394 Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 5 Dec 2017 03:10:21 +1100 Subject: [PATCH 2/7] New feature: load from another package .params file it's params and then add your own --- .../parameter_generator_catkin.py | 57 +++++++-- src/rosparam_handler/parameter_importer.py | 117 ++++++++++++++++++ 2 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 src/rosparam_handler/parameter_importer.py diff --git a/src/rosparam_handler/parameter_generator_catkin.py b/src/rosparam_handler/parameter_generator_catkin.py index 015b112..5b0fd35 100644 --- a/src/rosparam_handler/parameter_generator_catkin.py +++ b/src/rosparam_handler/parameter_generator_catkin.py @@ -31,6 +31,7 @@ import sys import os import re +from parameter_importer import load_generator def eprint(*args, **kwargs): @@ -56,19 +57,48 @@ def __init__(self, parent=None, group=""): self.group = "gen" self.group_variable = filter(str.isalnum, self.group) - if len(sys.argv) != 5: - eprint( - "ParameterGenerator: Unexpected amount of args, did you try to call this directly? You shouldn't do this!") - - self.dynconfpath = sys.argv[1] - self.share_dir = sys.argv[2] - self.cpp_gen_dir = sys.argv[3] - self.py_gen_dir = sys.argv[4] - self.pkgname = None self.nodename = None self.classname = None + def _load_generator(self, package_name, params_file_name): + """ + Load from another package .params file it's generator. + :param package_name: name of the package where the .params file is + :param params_file_name: name of the .params file, in the cfg folder + :return: ParameterGenerator instance from the provided file + """ + gen = load_generator(package_name, params_file_name) + if gen is None: + eprint("Could not load generator from package " + package_name + + " and file " + params_file_name) + return gen + + def _initialize_from_generator(self, generator): + """ + Initialize this ParameterGenerator from another instance. + :param generator: a ParameterGenerator instance + :return: + """ + print("Initializing from generator...") + self.enums = generator.enums + self.parameters = generator.parameters + print("self.parameters is now: " + str(self.parameters)) + self.childs = generator.childs + self.parent = generator.parent + self.group = generator.group + self.group_variable = generator.group_variable + + def initialize_from_file(self, package_name, params_file_name): + """ + Initialize this ParameterGenerator from another package .params file. + :param package_name: name of the package where the .params file is + :param params_file_name: name of the .params file, in the cfg folder + :return: + """ + self._initialize_from_generator(self._load_generator(package_name, + params_file_name)) + def add_group(self, name): """ Add a new group in the dynamic reconfigure selection @@ -327,6 +357,15 @@ def generate(self, pkgname, nodename, classname): self.nodename = nodename self.classname = classname + if len(sys.argv) != 5: + eprint( + "ParameterGenerator: Unexpected amount of args, did you try to call this directly? You shouldn't do this!") + + self.dynconfpath = sys.argv[1] + self.share_dir = sys.argv[2] + self.cpp_gen_dir = sys.argv[3] + self.py_gen_dir = sys.argv[4] + if self.parent: eprint("You should not call generate on a group! Call it on the main parameter generator instead!") diff --git a/src/rosparam_handler/parameter_importer.py b/src/rosparam_handler/parameter_importer.py new file mode 100644 index 0000000..c02187b --- /dev/null +++ b/src/rosparam_handler/parameter_importer.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +# Copyright (c) 2017, Sammy Pfeiffer +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the organization nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Author: Sammy Pfeiffer +# +# Utilities to import from another package .params file +# + +from imp import load_source +from rospkg import RosPack, ResourceNotFound +from tempfile import NamedTemporaryFile +import cStringIO +import tokenize +import re + + +def remove_comments(source): + """ + Returns 'source' minus comments, based on + https://stackoverflow.com/a/2962727 + """ + io_obj = cStringIO.StringIO(source) + out = "" + last_lineno = -1 + last_col = 0 + for tok in tokenize.generate_tokens(io_obj.readline): + token_type = tok[0] + token_string = tok[1] + start_line, start_col = tok[2] + end_line, end_col = tok[3] + if start_line > last_lineno: + last_col = 0 + if start_col > last_col: + out += (" " * (start_col - last_col)) + # Remove comments: + if token_type == tokenize.COMMENT: + pass + else: + out += token_string + last_col = end_col + last_lineno = end_line + return out + + +def load_generator(package_name, params_file_name): + """ + Returns the generator created in another .params file from another package. + Python does not allow to import from files without the extension .py + so we need to hack a bit to be able to import from .params file. + Also the .params file was never thought to be imported, so we need + to do some extra tricks. + """ + # Get the file path + rp = RosPack() + try: + pkg_path = rp.get_path(package_name) + except ResourceNotFound: + return None + full_file_path = pkg_path + '/cfg/' + params_file_name + # print("Loading rosparam_handler params from file: " + full_file_path) + + # Read the file and check for exit() calls + # Look for line with exit function to not use it or we will get an error + with open(full_file_path, 'r') as f: + file_str = f.read() + # Remove all comment lines first + clean_file = remove_comments(file_str) + # Find exit( calls + exit_finds = [m.start() for m in re.finditer('exit\(', clean_file)] + # If there are, get the last one + if exit_finds: + last_exit_idx = exit_finds[-1] + clean_file = clean_file[:last_exit_idx] + with NamedTemporaryFile() as f: + f.file.write(clean_file) + f.file.close() + tmp_module = load_source('tmp_module', f.name) + else: + # Looks like the exit call is not there + # or it's surrounded by if __name__ == '__main__' + # so we can just load the source + tmp_module = load_source('tmp_module', full_file_path) + + for var in dir(tmp_module): + if not var.startswith('_'): + module_element = getattr(tmp_module, var) + type_str = str(type(module_element)) + # Looks like: + # + if 'parameter_generator_catkin.ParameterGenerator' in type_str: + return module_element + + return None From b12863b413b73fb21056f2cbfad7e234a0916ba2 Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 5 Dec 2017 03:24:35 +1100 Subject: [PATCH 3/7] Add docs --- README.md | 1 + doc/HowToImportFromAnotherParamsFile.md | 29 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 doc/HowToImportFromAnotherParamsFile.md diff --git a/README.md b/README.md index ebb2419..d33bbdf 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ See the Tutorials on - [How to write your first .params file](doc/HowToWriteYourFirstParamsFile.md) - [How to use your parameter struct](doc/HowToUseYourParameterStruct.md) - [rosparam_handler_tutorial](https://github.com/cbandera/rosparam_handler_tutorial) +- [How to import from another .params file][doc/HowToImportFromAnotherParamsFile.md] ## Installation `rosparam_handler` has been released in version `0.1.1` for diff --git a/doc/HowToImportFromAnotherParamsFile.md b/doc/HowToImportFromAnotherParamsFile.md new file mode 100644 index 0000000..7691c2d --- /dev/null +++ b/doc/HowToImportFromAnotherParamsFile.md @@ -0,0 +1,29 @@ +# How to Import from another .params file +**Description**: This tutorial will show you how to import a .params file from another ROS package instead of copy-pasting it all over. +**Tutorial Level**: ADVANCED + +## Setup + +Note that your package will still need to depend on rosparam_handler and dynamic_reconfigure. + +You can find an example of a minimal package called [imported_rosparam_handler_test](https://github.com/awesomebytes/imported_rosparam_handler_test) which imports from [rosparam_handler_tutorial](https://github.com/cbandera/rosparam_handler_tutorial). + + +## The params File + +```python +#!/usr/bin/env python +from rosparam_handler.parameter_generator_catkin import * +gen = ParameterGenerator() +# Do it at the start, as it overwrites all current params +gen.initialize_from_file('rosparam_handler_tutorial', 'Demo.params') + +# Do your usual business +gen.add("some_other_param", paramtype="int",description="Awesome int", default=2, min=1, max=10, configurable=True) +gen.add("non_configurable_thing", paramtype="int",description="Im not configurable", default=2, min=1, max=10, configurable=False) + +# Syntax : Package, Node, Config Name(The final name will be MyDummyConfig) +exit(gen.generate("imported_rosparam_handler_test", "example_node", "Example")) +``` + +You just need to call `initialize_from_file(ros_package_name, File.params)`. Note that it will overwrite all params. Should be called at the start (that's why it's called initialize). \ No newline at end of file From 41f7b9e4e86b283a6d9dce51fe6e7385562ed3db Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 5 Dec 2017 03:34:31 +1100 Subject: [PATCH 4/7] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d33bbdf..4d36f08 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ See the Tutorials on - [How to write your first .params file](doc/HowToWriteYourFirstParamsFile.md) - [How to use your parameter struct](doc/HowToUseYourParameterStruct.md) - [rosparam_handler_tutorial](https://github.com/cbandera/rosparam_handler_tutorial) -- [How to import from another .params file][doc/HowToImportFromAnotherParamsFile.md] +- [How to import from another .params file](doc/HowToImportFromAnotherParamsFile.md) ## Installation `rosparam_handler` has been released in version `0.1.1` for From 95432b24a6a996a3854c47813999d63da917eb8a Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 5 Dec 2017 03:37:57 +1100 Subject: [PATCH 5/7] Remove prints... Sorry --- src/rosparam_handler/parameter_generator_catkin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rosparam_handler/parameter_generator_catkin.py b/src/rosparam_handler/parameter_generator_catkin.py index 5b0fd35..8d1084d 100644 --- a/src/rosparam_handler/parameter_generator_catkin.py +++ b/src/rosparam_handler/parameter_generator_catkin.py @@ -80,10 +80,8 @@ def _initialize_from_generator(self, generator): :param generator: a ParameterGenerator instance :return: """ - print("Initializing from generator...") self.enums = generator.enums self.parameters = generator.parameters - print("self.parameters is now: " + str(self.parameters)) self.childs = generator.childs self.parent = generator.parent self.group = generator.group From 50f3c04d2a7b93b2deec34a9d5d325ed1796d0a6 Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 5 Dec 2017 11:32:15 +1100 Subject: [PATCH 6/7] Add option to add relative path in case .params file is somewhere else than the cfg folder --- doc/HowToImportFromAnotherParamsFile.md | 6 ++++-- src/rosparam_handler/parameter_generator_catkin.py | 9 +++++++-- src/rosparam_handler/parameter_importer.py | 8 +++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/doc/HowToImportFromAnotherParamsFile.md b/doc/HowToImportFromAnotherParamsFile.md index 7691c2d..538d39b 100644 --- a/doc/HowToImportFromAnotherParamsFile.md +++ b/doc/HowToImportFromAnotherParamsFile.md @@ -16,7 +16,7 @@ You can find an example of a minimal package called [imported_rosparam_handler_t from rosparam_handler.parameter_generator_catkin import * gen = ParameterGenerator() # Do it at the start, as it overwrites all current params -gen.initialize_from_file('rosparam_handler_tutorial', 'Demo.params') +gen.initialize_from_file('rosparam_handler_tutorial', 'Demo.params', relative_path='/cfg/') # Do your usual business gen.add("some_other_param", paramtype="int",description="Awesome int", default=2, min=1, max=10, configurable=True) @@ -26,4 +26,6 @@ gen.add("non_configurable_thing", paramtype="int",description="Im not configurab exit(gen.generate("imported_rosparam_handler_test", "example_node", "Example")) ``` -You just need to call `initialize_from_file(ros_package_name, File.params)`. Note that it will overwrite all params. Should be called at the start (that's why it's called initialize). \ No newline at end of file +You just need to call `initialize_from_file(ros_package_name, File.params)`. Note that it will overwrite all params. Should be called at the start (that's why it's called initialize). + +You have the optional parameter `relative_path` in case you store your .params file somewhere else than in the `/cfg/` folder. \ No newline at end of file diff --git a/src/rosparam_handler/parameter_generator_catkin.py b/src/rosparam_handler/parameter_generator_catkin.py index 8d1084d..642822c 100644 --- a/src/rosparam_handler/parameter_generator_catkin.py +++ b/src/rosparam_handler/parameter_generator_catkin.py @@ -87,15 +87,20 @@ def _initialize_from_generator(self, generator): self.group = generator.group self.group_variable = generator.group_variable - def initialize_from_file(self, package_name, params_file_name): + def initialize_from_file(self, package_name, + params_file_name, + relative_path='/cfg/'): """ Initialize this ParameterGenerator from another package .params file. :param package_name: name of the package where the .params file is :param params_file_name: name of the .params file, in the cfg folder + :param relative_path: path in between package_name and params_file_name + defaults to /cfg/ e.g.: package_name/cfg/File.params :return: """ self._initialize_from_generator(self._load_generator(package_name, - params_file_name)) + params_file_name, + relative_path)) def add_group(self, name): """ diff --git a/src/rosparam_handler/parameter_importer.py b/src/rosparam_handler/parameter_importer.py index c02187b..c438f16 100644 --- a/src/rosparam_handler/parameter_importer.py +++ b/src/rosparam_handler/parameter_importer.py @@ -42,6 +42,8 @@ def remove_comments(source): """ Returns 'source' minus comments, based on https://stackoverflow.com/a/2962727 + :param source: string with Python source code + :return: source code without comments """ io_obj = cStringIO.StringIO(source) out = "" @@ -66,13 +68,17 @@ def remove_comments(source): return out -def load_generator(package_name, params_file_name): +def load_generator(package_name, params_file_name, relative_path='/cfg/'): """ Returns the generator created in another .params file from another package. Python does not allow to import from files without the extension .py so we need to hack a bit to be able to import from .params file. Also the .params file was never thought to be imported, so we need to do some extra tricks. + :param package_name: ROS package name + :param params_file_name: .params file name + :param relative_path: path in between package_name and params_file_name, defaults to /cfg/ + :return: """ # Get the file path rp = RosPack() From 45b73145e0fe5aeaa103f17c4f7e2dfb9ca62ebf Mon Sep 17 00:00:00 2001 From: Sam Pfeiffer Date: Tue, 5 Dec 2017 22:45:54 +1100 Subject: [PATCH 7/7] Forgot to actually apply the new param --- src/rosparam_handler/parameter_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rosparam_handler/parameter_importer.py b/src/rosparam_handler/parameter_importer.py index c438f16..d48df37 100644 --- a/src/rosparam_handler/parameter_importer.py +++ b/src/rosparam_handler/parameter_importer.py @@ -86,7 +86,7 @@ def load_generator(package_name, params_file_name, relative_path='/cfg/'): pkg_path = rp.get_path(package_name) except ResourceNotFound: return None - full_file_path = pkg_path + '/cfg/' + params_file_name + full_file_path = pkg_path + relative_path + params_file_name # print("Loading rosparam_handler params from file: " + full_file_path) # Read the file and check for exit() calls