Made fields in python structs assigned per-instance instead of at the class scope#351
Made fields in python structs assigned per-instance instead of at the class scope#351alexnavtt wants to merge 8 commits intoPickNikRobotics:mainfrom
Conversation
|
@Mat198 Can you have a look please? |
christophfroehlich
left a comment
There was a problem hiding this comment.
Thank you for opening this PR. Can you please fix pre-commit issues? and better install it for the future.
There was a problem hiding this comment.
Pull request overview
This PR updates the Python code generation path so generated parameter “struct” fields are initialized per-instance (in __init__) rather than at class scope, preventing unintended shared state across Params instances and nested sub-structs/maps (as reported in #313).
Changes:
- Add
__init__methods to generated Python parameter structs and move field initialization into__init__. - Update generated Python variable declarations to use
self.<field> = ...instead of class attributes. - Extend YAML parsing/codegen to support emitting sub-struct instance initialization via a new
is_structpath.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
generate_parameter_library_py/generate_parameter_library_py/parse_yaml.py |
Adds an is_struct path in variable codegen and injects generated “struct instance” fields intended for Python __init__. |
generate_parameter_library_py/generate_parameter_library_py/jinja_templates/python/parameter_library_header |
Wraps Params fields in an __init__ method and updates stamp_ to self.stamp_. |
generate_parameter_library_py/generate_parameter_library_py/jinja_templates/python/declare_variable |
Switches generated assignments to self.<name> = .... |
generate_parameter_library_py/generate_parameter_library_py/jinja_templates/python/declare_struct |
Adds __init__ for all generated struct classes and removes class-scope instantiation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def __init__(self): | ||
| {%- filter indent(width=4) %} | ||
| # for detecting if the parameter struct has been updated | ||
| stamp_ = Time() | ||
| self.stamp_ = Time() | ||
|
|
||
| {{field_content-}} | ||
| {{sub_struct_content-}} | ||
| {% endfilter -%} | ||
| {{sub_struct_content-}} | ||
| {%- endfilter %} |
There was a problem hiding this comment.
This change fixes a subtle runtime behavior (instance state isolation), but there isn’t an automated test asserting that two Params() instances (or two map entries) don’t share mutable defaults (lists / nested structs). Adding a regression test that instantiates multiple Params objects and verifies nested fields are independent would help prevent this from reappearing.
|
Hmm, I was accidentally working under the assumption that parse_yaml.py was only used for Python code, but I see that that isn't quite correct. I'll have to take another look find a way to cleanly scope it to just python code |
Yeah. Sure! |
|
Alright, I reworked my solution to add python-specific fields to the Jinja data for both |
|
Thanks for your contribution @alexnavtt! Could you please remove the module reload from the consistency test (test_params_consistency.py)? This shoudn't be necessary with this fix. |
|
Done |
christophfroehlich
left a comment
There was a problem hiding this comment.
Can you please add a test for this (as suggested by copilot, too)?
@Mat198 If the changes are working for you, please leave a review via github UI. Thanks!
|
Hey @alexnavtt, the consistency tests are failling. It's very easy to fix but I don't have ter permissions to edit it. Just fix the import of the admitance_controller parameters: To run the tests locally, you need to build with tests enabled: Them run the tests: To run the precommit tests you can use: Or install them: |
Mat198
left a comment
There was a problem hiding this comment.
The consistency test is broken as stated in the previous comment.
|
Alright, I've added tests for both the problem that I was seeing the shared state in maps, and the problem that @Mat198 was seeing with shared state across instances: def test_params_do_not_share_state(self):
params1 = self.listener.get_params()
params1.pid.rate = 1.0
params2 = self.listener.get_params()
params2.pid.rate = 2.0
self.assertNotEqual(params1.pid.rate, params2.pid.rate)
self.node.set_parameters([Parameter('pid.rate', value='3.0')])
params2 = self.listener.get_params()
self.assertNotEqual(params1.pid.rate, params2.pid.rate)
def test_maps_do_not_share_state(self):
self.node.set_parameters(
[
Parameter(
'nested_map_struct.A.nested_struct.nested_struct_field',
value='valueA',
),
Parameter(
'nested_map_struct.B.nested_struct.nested_struct_field',
value='valueB',
),
]
)
params = self.listener.get_params()
self.assertNotEqual(
params.nested_map_struct.get_entry('A').nested_struct.nested_struct_field,
params.nested_map_struct.get_entry('B').nested_struct.nested_struct_field,
) |
Mat198
left a comment
There was a problem hiding this comment.
Great work, @alexnavtt! LGTM


As stated in #313, struct variables declared in python modules were declared at the class scope, causing unwanted state sharing in certain circumstances. This PR fixes that by declaring all fields within an
__init__function, including sub-struct instances.As an examples of changes made, I'll highlight my use case and what changed. The parameter definition file is located here. When setting multiple depth camera sensors via this parameter set, I noticed that the
camera_infoparameter would be shared and take the last set value across all Map entries. This issue could also appear when creating multipleParamsinstances.This is the generated code prior to the changes:
And this is the generated code after the changes:
I have tested this code in my project and verified that it fixes the issue for me.
Also, this is my first time working with this codebase, so I would appreciate a thourough review making sure I didn't accidentally affect some other part of the system.