From 3225ddefb4bb363d20f8b0d0a1e1881a12d3d7c9 Mon Sep 17 00:00:00 2001 From: JafarAbdi Date: Fri, 21 Apr 2023 21:46:43 +0000 Subject: [PATCH 1/3] Add jinja support for loading template yaml files --- example/example_node.py | 28 ++++++++++++++++ .../launch_param_builder_example.launch.py | 11 +++++++ launch_param_builder/launch_param_builder.py | 24 ++++++++++---- launch_param_builder/utils.py | 33 ++++++++++++++----- package.xml | 3 +- test/data/parameter_file_template | 1 + test/data/parameters_template.yaml | 8 +++++ test/test_launch_param_builder.py | 33 ++++++++++++++++++- 8 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 test/data/parameter_file_template create mode 100644 test/data/parameters_template.yaml diff --git a/example/example_node.py b/example/example_node.py index 586d14b..caa510a 100755 --- a/example/example_node.py +++ b/example/example_node.py @@ -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}" ) @@ -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): diff --git a/example/launch_param_builder_example.launch.py b/example/launch_param_builder_example.launch.py index 5113457..823df20 100644 --- a/example/launch_param_builder_example.launch.py +++ b/example/launch_param_builder_example.launch.py @@ -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 diff --git a/launch_param_builder/launch_param_builder.py b/launch_param_builder/launch_param_builder.py index d63a993..e40ce19 100644 --- a/launch_param_builder/launch_param_builder.py +++ b/launch_param_builder/launch_param_builder.py @@ -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): @@ -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( diff --git a/launch_param_builder/utils.py b/launch_param_builder/utils.py index b2a517e..4e8d1bb 100644 --- a/launch_param_builder/utils.py +++ b/launch_param_builder/utils.py @@ -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): @@ -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 + 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) diff --git a/package.xml b/package.xml index 9b28ce1..40509d3 100644 --- a/package.xml +++ b/package.xml @@ -11,9 +11,10 @@ python3-pytest ament_index_python + python3-jinja2 python3-yaml - xacro rclpy + xacro ament_python diff --git a/test/data/parameter_file_template b/test/data/parameter_file_template new file mode 100644 index 0000000..49dba44 --- /dev/null +++ b/test/data/parameter_file_template @@ -0,0 +1 @@ +This's a template parameter file {{ test_name }} diff --git a/test/data/parameters_template.yaml b/test/data/parameters_template.yaml new file mode 100644 index 0000000..c8217c5 --- /dev/null +++ b/test/data/parameters_template.yaml @@ -0,0 +1,8 @@ +{{ namespace }}: + names: + {% for robot in robots %} + {{ robot.name }}: "{{ robot.ip }}" + {% endfor %} +radians: {{ radians(120) }} +degrees: {{ degrees(1.0) }} +names: {{ names }} diff --git a/test/test_launch_param_builder.py b/test/test_launch_param_builder.py index c629f5f..4a62a93 100644 --- a/test/test_launch_param_builder.py +++ b/test/test_launch_param_builder.py @@ -28,6 +28,7 @@ from launch_param_builder import ParameterBuilder +import math def test_builder(): @@ -35,9 +36,26 @@ def test_builder(): 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 @@ -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 From 92ca068036143841dabaa02f908b25908d5cd089 Mon Sep 17 00:00:00 2001 From: JafarAbdi Date: Fri, 21 Apr 2023 21:50:05 +0000 Subject: [PATCH 2/3] Add pre-commit to the readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index cc2cfb2..2e7fef1 100644 --- a/README.md +++ b/README.md @@ -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 From 6f905d9f7fbda417eff1e90e2c01517091d28c9a Mon Sep 17 00:00:00 2001 From: JafarAbdi Date: Fri, 21 Apr 2023 21:56:04 +0000 Subject: [PATCH 3/3] exclude template yaml file --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ecb36e..3c7b67a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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