Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature load from another package #36

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions doc/HowToImportFromAnotherParamsFile.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 1 addition & 1 deletion doc/HowToWriteYourFirstParamsFile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 46 additions & 9 deletions src/rosparam_handler/parameter_generator_catkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import sys
import os
import re
from parameter_importer import load_generator


def eprint(*args, **kwargs):
Expand All @@ -56,19 +57,46 @@ 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):
"""
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
Expand Down Expand Up @@ -327,6 +355,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!")

Expand Down
117 changes: 117 additions & 0 deletions src/rosparam_handler/parameter_importer.py
Original file line number Diff line number Diff line change
@@ -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 <COPYRIGHT HOLDER> 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be handy to have the possibility to specify the relative path.
An extra param to the load_generator (and affiliated functions) with default value : path='/cfg/'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the last commit. Updated docs accordingly

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👍

# 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:
# <class 'PACKAGE_NAME.parameter_generator_catkin.ParameterGenerator'>
if 'parameter_generator_catkin.ParameterGenerator' in type_str:
return module_element

return None