Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ repos:
- id: check-toml
- id: check-xml
- id: check-yaml
exclude: test/data/parameters_template.yaml
- id: debug-statements
- id: destroyed-symlinks
- id: detect-private-key
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ colcon test --packages-select launch_param_builder --event-handlers console_dire
colcon test-result
```

To run pre-commit, use the following command.

```bash
pre-commit run --all-files
```

To add a copyright for a new file

```bash
Expand Down
28 changes: 28 additions & 0 deletions example/example_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,28 @@ def __init__(self):
"my_robot",
descriptor=ParameterDescriptor(type=ParameterType.PARAMETER_STRING),
)
self.declare_parameter(
"env_0.names.ur",
descriptor=ParameterDescriptor(type=ParameterType.PARAMETER_STRING),
)
self.declare_parameter(
"env_0.names.panda",
descriptor=ParameterDescriptor(type=ParameterType.PARAMETER_STRING),
)
self.declare_parameter(
"radians",
descriptor=ParameterDescriptor(type=ParameterType.PARAMETER_DOUBLE),
)
self.declare_parameter(
"degrees",
descriptor=ParameterDescriptor(type=ParameterType.PARAMETER_DOUBLE),
)
self.declare_parameter(
"names",
descriptor=ParameterDescriptor(type=ParameterType.PARAMETER_STRING_ARRAY),
)

self.get_logger().info(f"Parameters: {self.get_parameters_by_prefix('')}")
self.get_logger().info(
f"my_parameter: {self.get_parameter('my_parameter').value}"
)
Expand All @@ -81,6 +102,13 @@ def __init__(self):
self.get_logger().info(
f"package_name: {self.get_parameter('package_name').value}"
)
self.get_logger().info(f"ur ip: {self.get_parameter('env_0.names.ur').value}")
self.get_logger().info(
f"panda ip: {self.get_parameter('env_0.names.panda').value}"
)
self.get_logger().info(f"degrees: {self.get_parameter('degrees').value}")
self.get_logger().info(f"radians: {self.get_parameter('radians').value}")
self.get_logger().info(f"names: {self.get_parameter('names').value}")


def main(args=None):
Expand Down
11 changes: 11 additions & 0 deletions example/launch_param_builder_example.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def generate_launch_description():
.file_parameter(
"parameter_file", "config/parameter_file"
) # Or /absolute/path/to/file
.yaml(
file_path="config/parameters_template.yaml",
mappings={
"namespace": "env_0",
"robots": [
{"name": "ur", "ip": "127.0.0.1"},
{"name": "panda", "ip": "127.0.0.2"},
],
"names": ["name1", "name2", "name3"],
},
)
.yaml(
file_path="config/parameters.yaml"
) # Or /absolute/path/to/file
Expand Down
24 changes: 18 additions & 6 deletions launch_param_builder/launch_param_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .utils import load_file, load_yaml, load_xacro, ParameterValueType

from ament_index_python.packages import get_package_share_directory
from typing import Optional


class ParameterBuilder(object):
Expand All @@ -42,22 +43,33 @@ def __init__(self, package_name: str):
self._package_path = Path(get_package_share_directory(package_name))
self._parameters = {}

def yaml(self, file_path: str, parameter_namespace: str = None):
def yaml(
self,
file_path: str,
parameter_namespace: str = None,
mappings: Optional[dict] = None,
):
if parameter_namespace:
if parameter_namespace in self._parameters:
self._parameters[parameter_namespace].update(
load_yaml(self._package_path / file_path)
load_yaml(self._package_path / file_path, mappings=mappings)
)
else:
self._parameters[parameter_namespace] = load_yaml(
self._package_path / file_path
self._package_path / file_path, mappings=mappings
)
else:
self._parameters.update(load_yaml(self._package_path / file_path))
self._parameters.update(
load_yaml(self._package_path / file_path, mappings=mappings)
)
return self

def file_parameter(self, parameter_name: str, file_path: str):
self._parameters[parameter_name] = load_file(self._package_path / file_path)
def file_parameter(
self, parameter_name: str, file_path: str, mappings: Optional[dict] = None
):
self._parameters[parameter_name] = load_file(
self._package_path / file_path, mappings=mappings
)
return self

def xacro_parameter(
Expand Down
33 changes: 25 additions & 8 deletions launch_param_builder/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@

import yaml
from pathlib import Path
from typing import List, Union
from typing import List, Union, Optional
import xacro
from ament_index_python.packages import get_package_share_directory
import jinja2
import math


class ParameterBuilderFileNotFoundError(KeyError):
Expand All @@ -52,31 +54,46 @@ class ParameterBuilderFileNotFoundError(KeyError):
]


def render_template(template: Path, mappings: dict):
with template.open("r") as file:
jinja2_template = jinja2.Template(file.read())
jinja2_template.globals["radians"] = math.radians

Choose a reason for hiding this comment

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

Is this what parses the !degrees and !radians in YAML?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, it adds a function to the jinja template

jinja2_template.globals["degrees"] = math.degrees
return jinja2_template.render(mappings)


def raise_if_file_not_found(file_path: Path):
if not file_path.exists():
raise ParameterBuilderFileNotFoundError(f"File {file_path} doesn't exist")


def load_file(file_path: Path):
def load_file(file_path: Path, mappings: Optional[dict] = None):
raise_if_file_not_found(file_path)
if mappings is not None:
return render_template(file_path, mappings)
try:
with open(file_path, "r") as file:
return file.read()
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
except (
EnvironmentError
): # parent of IOError, OSError *and* WindowsError where available
return None


def load_yaml(file_path: Path):
def load_yaml(file_path: Path, mappings: Optional[dict] = None):
raise_if_file_not_found(file_path)

try:
with open(file_path, "r") as file:
return yaml.load(file, Loader=yaml.FullLoader)
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
return yaml.load(
render_template(file_path, mappings or {}), Loader=yaml.FullLoader
)
except (
EnvironmentError
): # parent of IOError, OSError *and* WindowsError where available
return None


def load_xacro(file_path: Path, mappings: dict = None):
def load_xacro(file_path: Path, mappings: Optional[dict] = None):
raise_if_file_not_found(file_path)

file = xacro.process_file(file_path, mappings=mappings)
Expand Down
3 changes: 2 additions & 1 deletion package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
<test_depend>python3-pytest</test_depend>

<exec_depend>ament_index_python</exec_depend>
<exec_depend>python3-jinja2</exec_depend>
<exec_depend>python3-yaml</exec_depend>
<exec_depend>xacro</exec_depend>
<exec_depend>rclpy</exec_depend>
<exec_depend>xacro</exec_depend>

<export>
<build_type>ament_python</build_type>
Expand Down
1 change: 1 addition & 0 deletions test/data/parameter_file_template
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This's a template parameter file {{ test_name }}
8 changes: 8 additions & 0 deletions test/data/parameters_template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{{ namespace }}:
names:
{% for robot in robots %}
{{ robot.name }}: "{{ robot.ip }}"
{% endfor %}
radians: {{ radians(120) }}

Choose a reason for hiding this comment

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

And are these parsed equivalently to !degrees/!radians?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, plus with jinja we could do more mathematical operations

degrees: {{ degrees(1.0) }}
names: {{ names }}
33 changes: 32 additions & 1 deletion test/test_launch_param_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,34 @@


from launch_param_builder import ParameterBuilder
import math


def test_builder():
parameters = (
ParameterBuilder("launch_param_builder")
.parameter("my_parameter", 20.0)
.file_parameter(
"parameter_file", "config/parameter_file"
"parameter_file",
"config/parameter_file",
) # Or /absolute/path/to/file
.file_parameter(
"parameter_file_template",
"config/parameter_file_template",
mappings={"test_name": "testing"},
)
.yaml(file_path="config/parameters.yaml") # Or /absolute/path/to/file
.yaml(
file_path="config/parameters_template.yaml",
mappings={
"namespace": "env_0",
"robots": [
{"name": "ur", "ip": "127.0.0.1"},
{"name": "panda", "ip": "127.0.0.2"},
],
"names": ["name1", "name2", "name3"],
},
)
.xacro_parameter(
parameter_name="my_robot",
file_path="config/parameter.xacro", # Or /absolute/path/to/file
Expand All @@ -52,3 +70,16 @@ def test_builder():
assert parameters["the_answer_to_life"] == 42
assert parameters["package_name"] == "launch_param_builder"
assert parameters.get("my_robot") is not None, "Parameter xacro not loaded"
assert (
parameters["parameter_file_template"]
== "This's a template parameter file testing"
)
assert math.isclose(parameters["radians"], 2.0943951, rel_tol=1e-6)
assert math.isclose(parameters["degrees"], 57.2958, rel_tol=1e-6)
assert (
parameters.get("env_0").get("names") is not None
), "Parameter yaml file not loaded"
names = parameters["env_0"]["names"]
assert names["ur"] == "127.0.0.1"
assert names["panda"] == "127.0.0.2"
assert len(parameters["names"]) == 3