diff --git a/README.md b/README.md index 77d546f..5433b5e 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..538d39b --- /dev/null +++ b/doc/HowToImportFromAnotherParamsFile.md @@ -0,0 +1,31 @@ +# 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', 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) +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). + +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/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 diff --git a/src/rosparam_handler/parameter_generator_catkin.py b/src/rosparam_handler/parameter_generator_catkin.py index 015b112..642822c 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,51 @@ 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: + """ + self.enums = generator.enums + self.parameters = generator.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, + 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, + relative_path)) + def add_group(self, name): """ Add a new group in the dynamic reconfigure selection @@ -327,6 +360,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..d48df37 --- /dev/null +++ b/src/rosparam_handler/parameter_importer.py @@ -0,0 +1,123 @@ +#!/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 + :param source: string with Python source code + :return: source code without comments + """ + 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, 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() + try: + pkg_path = rp.get_path(package_name) + except ResourceNotFound: + return None + 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 + # 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