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

markdown/rst: Support __map_ and nested parameters #164

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@

import argparse
import os
import re
import sys
from jinja2 import Template
from typeguard import typechecked
import yaml

from generate_parameter_library_py.parse_yaml import (
GenerateCode,
DeclareParameter,
DeclareRuntimeParameter,
ValidationFunction,
)

Expand Down Expand Up @@ -95,7 +98,44 @@ def __str__(self):
constraints = '\n'.join(str(val) for val in self.param_validations)

data = {
'name': self.declare_parameters.code_gen_variable.name,
'name': self.declare_parameters.parameter_name,
'read_only': self.declare_parameters.parameter_read_only,
'type': self.declare_parameters.code_gen_variable.defined_type,
'default_value': self.declare_parameters.code_gen_variable.lang_str_value,
'constraints': constraints,
# remove leading whitespace from description, this is necessary for correct indentation of multi-line descriptions
'description': re.sub(
r'(?m)^(?!$)\s*',
'',
str(self.declare_parameters.parameter_description),
flags=re.MULTILINE,
),
}

j2_template = Template(GenerateCode.templates['parameter_detail'])
code = j2_template.render(data, trim_blocks=True)
return code


class RuntimeParameterDetailMarkdown:
@typechecked
def __init__(self, declare_parameters: DeclareRuntimeParameter):
self.declare_parameters = declare_parameters
self.param_validations = [
ParameterValidationMarkdown(val)
for val in declare_parameters.parameter_validations
]

def __str__(self):
constraints = '\n'.join(str(val) for val in self.param_validations)
data = {
# replace __map_key with <key>
'name': re.sub(
r'__map_(\w+)',
lambda match: '<' + match.group(1) + '>',
self.declare_parameters.parameter_name,
),
'read_only': self.declare_parameters.parameter_read_only,
'type': self.declare_parameters.code_gen_variable.defined_type,
'default_value': self.declare_parameters.code_gen_variable.lang_str_value,
'constraints': constraints,
Expand All @@ -119,14 +159,65 @@ def __init__(self, gen_param_struct: GenerateCode):
def __str__(self):
j2_template = Template(GenerateCode.templates['default_config'])

tmp = '\n'.join(
param.parameter_name + ': ' + str(param.code_gen_variable.lang_str_value)
for param in self.gen_param_struct.declare_parameters
tmp = (
'\n'.join(
param.parameter_name
+ ': '
+ str(param.code_gen_variable.lang_str_value)
for param in self.gen_param_struct.declare_parameters
)
+ '\n'
+ '\n'.join(
# replace __map_key with <key>
re.sub(
r'__map_(\w+)',
lambda match: '<' + match.group(1) + '>',
param.parameter_name,
)
+ ': '
+ str(param.code_gen_variable.lang_str_value)
for param in self.gen_param_struct.declare_dynamic_parameters
)
)

# Split the string into lines and group them by the first part
def nest_dict(d, keys, value):
# Check if the value is a string
if isinstance(value, str):
# Remove double quotes from the string
value = value.replace('"', '')
# Try to convert the value to a boolean, number, or leave it as a string
if value.lower() == 'true':
value = True
elif value.lower() == 'false':
value = False
else:
try:
value = float(value)
except ValueError:
pass

if len(keys) == 1:
d[keys[0]] = value
else:
key = keys.pop(0)
if key not in d:
d[key] = {}
nest_dict(d[key], keys, value)

# Split the string into lines and create a dictionary
d = {}
for line in tmp.strip().split('\n'):
name, value = line.split(':', 1)
keys = name.split('.')
nest_dict(d, keys, value.strip())

# Convert the dictionary to a string
result = yaml.dump(d, default_flow_style=False)

data = {
'namespace': self.gen_param_struct.namespace,
'default_param_values': tmp,
'default_param_values': result,
}
code = j2_template.render(data, trim_blocks=True)

Expand All @@ -142,6 +233,10 @@ def __init__(self, gen_param_struct: GenerateCode):
ParameterDetailMarkdown(param)
for param in self.gen_param_struct.declare_parameters
]
self.runtime_param_details = [
RuntimeParameterDetailMarkdown(param)
for param in self.gen_param_struct.declare_dynamic_parameters
]

def __str__(self):
words = self.gen_param_struct.namespace.split('_')
Expand All @@ -150,7 +245,9 @@ def __str__(self):
data = {
'title': title,
'default_config': str(self.default_config),
'parameter_details': '\n'.join(str(val) for val in self.param_details),
'parameter_details': '\n'.join(str(val) for val in self.param_details)
+ '\n'
+ '\n'.join(str(val) for val in self.runtime_param_details),
}

j2_template = Template(GenerateCode.templates['documentation'])
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
if (!parameters_interface_->has_parameter(prefix_ + "{{parameter_name}}")) {
{%- filter indent(width=4) %}
rcl_interfaces::msg::ParameterDescriptor descriptor;
descriptor.description = "{{parameter_description}}";
descriptor.description = {{parameter_description | valid_string_cpp}};
descriptor.read_only = {{parameter_read_only}};
{%- for validation in parameter_validations if ("bounds" in validation.function_name or "lt" in validation.function_name or "gt" in validation.function_name) %}
{%- if "DOUBLE" in parameter_type %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ auto param_name = fmt::format("{}{}.{}.{}", prefix_, "{{struct_name}}", value, "
if (!parameters_interface_->has_parameter(param_name)) {
{%- filter indent(width=4) %}
rcl_interfaces::msg::ParameterDescriptor descriptor;
descriptor.description = "{{parameter_description}}";
descriptor.description = {{parameter_description | valid_string_cpp}};
descriptor.read_only = {{parameter_read_only}};
{%- for validation in parameter_validations if ("bounds" in validation.function_name or "lt" in validation.function_name or "gt" in validation.function_name) %}
{%- if "DOUBLE" in parameter_type %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
{% endif %}
* Type: `{{type}}`
{%- if default_value|length %}
* Default Value: {{default_value}}
{% endif %}
{%- if constraints|length %}
* Default Value: {{default_value}}{% endif %}{% if read_only %}
* Read only: {{read_only}}{% endif %}{%- if constraints|length %}

*Constraints:*
{{constraints}}
{% else %}
{% endif %}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
{{name}} ({{type}}){% if description|length %}
{{description}}
{{name}} ({{type}}){%- filter indent(width=2) %}{% if description|length %}
{{description}}
{% endif %}
{%- if read_only %}
Read only: {{read_only}}
{% endif %}
{%- if default_value|length %}
Default: {{default_value}}
Default: {{default_value}}
{% endif %}
{%- if constraints|length %}

Constraints:
Constraints:

{%- filter indent(width=2) %}

{{constraints}}
{% endfilter -%}

{% endif %}
{% endfilter -%}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from jinja2 import Template
from jinja2 import Template, Environment
from typeguard import typechecked
from typing import Any, List, Optional
from yaml.parser import ParserError
Expand All @@ -39,6 +39,7 @@

from generate_parameter_library_py.cpp_convertions import CPPConverstions
from generate_parameter_library_py.python_convertions import PythonConvertions
from generate_parameter_library_py.string_filters_cpp import valid_string_cpp


# YAMLSyntaxError standardizes compiler error messages
Expand Down Expand Up @@ -499,6 +500,7 @@ def __str__(self):
bool_to_str = self.code_gen_variable.conversation.bool_to_str

parameter_validations = self.parameter_validations

data = {
'parameter_name': self.parameter_name,
'parameter_value': self.parameter_value,
Expand All @@ -507,7 +509,11 @@ def __str__(self):
'parameter_read_only': bool_to_str(self.parameter_read_only),
'parameter_validations': parameter_validations,
}
j2_template = Template(GenerateCode.templates['declare_parameter'])

# Create a Jinja2 environment to register the custom filter
env = Environment()
env.filters['valid_string_cpp'] = valid_string_cpp
j2_template = env.from_string(GenerateCode.templates['declare_parameter'])
code = j2_template.render(data, trim_blocks=True)
return code

Expand Down Expand Up @@ -566,7 +572,12 @@ def __str__(self):
'parameter_validations': self.parameter_validations,
}

j2_template = Template(GenerateCode.templates['declare_runtime_parameter'])
# Create a Jinja2 environment to register the custom filter
env = Environment()
env.filters['valid_string_cpp'] = valid_string_cpp
j2_template = env.from_string(
GenerateCode.templates['declare_runtime_parameter']
)
code = j2_template.render(data, trim_blocks=True)
return code

Expand Down Expand Up @@ -689,6 +700,18 @@ class GenerateCode:
templates = None

def __init__(self, language: str):
if language == 'cpp':
self.comments = '// auto-generated DO NOT EDIT'
elif language == 'rst':
self.comments = '.. auto-generated DO NOT EDIT'
elif language == 'markdown':
self.comments = '<!--- auto-generated DO NOT EDIT -->'
elif language == 'python' or language == 'markdown':
self.comments = '# auto-generated DO NOT EDIT'
else:
raise compile_error(
'Invalid language, only cpp, markdown, rst, and python are currently supported.'
)
GenerateCode.templates = get_all_templates(language)
self.language = language
self.namespace = ''
Expand All @@ -702,16 +725,6 @@ def __init__(self, language: str):
self.remove_dynamic_parameter = []
self.declare_parameter_sets = []
self.set_stack_params = []
if language == 'cpp':
self.comments = '// auto-generated DO NOT EDIT'
elif language == 'rst':
self.comments = '.. auto-generated DO NOT EDIT'
elif language == 'python' or language == 'markdown':
self.comments = '# auto-generated DO NOT EDIT'
else:
raise compile_error(
'Invalid language, only c++ and python are currently supported.'
)
self.user_validation_file = ''

def parse(self, yaml_file, validate_header):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
def valid_string_cpp(description):
"""
Filter a string to make it a valid C++ string literal.

Args:
description (str): The input string to be filtered.

Returns:
str: The filtered string that is a valid C++ string.
"""
if description:
filtered_description = (
description.replace('\\', '\\\\').replace('"', '\\"').replace('`', '')
)
# create a quote delimited string for every line
filtered_description = '\n'.join(
f'"{line}"' for line in filtered_description.splitlines()
)
return filtered_description
else:
return '""'
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
import os

from ament_index_python.packages import get_package_share_path
from generate_parameter_library_py.generate_cpp_header import run as run_python
from generate_parameter_library_py.generate_python_module import run as run_cpp
from generate_parameter_library_py.generate_cpp_header import run as run_cpp
from generate_parameter_library_py.generate_python_module import run as run_python
from generate_parameter_library_py.generate_markdown import run as run_md
from generate_parameter_library_py.parse_yaml import YAMLSyntaxError
from generate_parameter_library_py.generate_cpp_header import parse_args

Expand All @@ -29,16 +30,42 @@ def set_up(yaml_test_file):
full_file_path = os.path.join(
get_package_share_path('generate_parameter_library_py'), 'test', yaml_test_file
)
testargs = [sys.argv[0], '/tmp/admittance_controller.h', full_file_path]
testargs = [sys.argv[0], '/tmp/' + yaml_test_file + '.h', full_file_path]

with patch.object(sys, 'argv', testargs):
args = parse_args()
output_file = args.output_cpp_header_file
yaml_file = args.input_yaml_file
validate_header = args.validate_header
run_cpp(output_file, yaml_file, validate_header)

testargs = [sys.argv[0], '/tmp/' + yaml_test_file + '.py', full_file_path]

with patch.object(sys, 'argv', testargs):
args = parse_args()
output_file = args.output_cpp_header_file
yaml_file = args.input_yaml_file
validate_header = args.validate_header
run_python(output_file, yaml_file, validate_header)

testargs = [sys.argv[0], '/tmp/' + yaml_test_file + '.md', full_file_path]

with patch.object(sys, 'argv', testargs):
args = parse_args()
output_file = args.output_cpp_header_file
yaml_file = args.input_yaml_file
validate_header = args.validate_header
run_md(yaml_file, output_file, 'markdown')

testargs = [sys.argv[0], '/tmp/' + yaml_test_file + '.rst', full_file_path]

with patch.object(sys, 'argv', testargs):
args = parse_args()
output_file = args.output_cpp_header_file
yaml_file = args.input_yaml_file
validate_header = args.validate_header
run_md(yaml_file, output_file, 'rst')


# class TestViewValidCodeGen(unittest.TestCase):
@pytest.mark.parametrize(
Expand Down
Loading