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

Restrict yaml loading in evaluate_parameters #33

Merged
merged 9 commits into from
Jul 3, 2019

Conversation

ivanpauno
Copy link
Member

@ivanpauno ivanpauno commented Jun 3, 2019

Follow up of #31.

The idea of this PR is to restrict what conversions from yaml are allowed, and what conversions are precluded. Also, substitutions that are later loaded as a list (from yaml), are converted to tuples (as it was done in the other cases).

I didn't want to raise an error in the conversions that aren't allowed, but I think it would be a good idea.

@sloretz
Copy link
Contributor

sloretz commented Jun 3, 2019

Mind adding a test here to show what this is correcting?

https://github.com/ros2/launch_ros/blob/master/test_launch_ros/test/test_launch_ros/test_normalize_parameters.py

@ivanpauno
Copy link
Member Author

ivanpauno commented Jun 3, 2019

Mind adding a test here to show what this is correcting?

https://github.com/ros2/launch_ros/blob/master/test_launch_ros/test/test_launch_ros/test_normalize_parameters.py

I will add tests tomorrow.

@hidmic
Copy link
Contributor

hidmic commented Jun 3, 2019

I second @sloretz request. Just for the record though, code seems to be precluding nested parameter arrays and verifying that arrays evaluated from substitutions have a single element type.

@wjwwood
Copy link
Member

wjwwood commented Jun 3, 2019

Same, a test would be nice, it would also help convey what's being changed here.

@ivanpauno
Copy link
Member Author

Sorry for the unclear PR yesterday. I updated the PR with a clearer description.
Tests were added in 1456aca.

@ivanpauno ivanpauno changed the title Correct how evaluate_parameters handles a list loaded from yaml Restrict yaml loading in evaluate_parameters Jun 4, 2019
Copy link
Member

@wjwwood wjwwood left a comment

Choose a reason for hiding this comment

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

Thanks for adding the tests, it makes the expected change in behavior clearer. I find the logic difficult to follow, but that's not uncommon when parsing schema-less data like this, so in general I think this is looking good. I had only a few comments.

launch_ros/launch_ros/utilities/evaluate_parameters.py Outdated Show resolved Hide resolved
launch_ros/launch_ros/utilities/evaluate_parameters.py Outdated Show resolved Hide resolved
launch_ros/launch_ros/utilities/evaluate_parameters.py Outdated Show resolved Hide resolved
Copy link
Member Author

@ivanpauno ivanpauno left a comment

Choose a reason for hiding this comment

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

Addressed review comments in 3f328d7.
Started raising exceptions in 4efbfe5.

yaml_evaluated_value: Union[List[str], List[int], List[float], List[bool]] = [
yaml.safe_load(item) for item in evaluated_value
]
if check_sequence_type_is_allowed(yaml_evaluated_value):
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggested change
if check_sequence_type_is_allowed(yaml_evaluated_value):
if not check_sequence_type_is_allowed(yaml_evaluated_value):
raise TypeError(
'Expected a non-empty sequence, with items of uniform type. '
'Allowed sequence item types are bool, int, float, str.'
)
evaluated_value = tuple(yaml_evaluated_value)

That change will also raise an error in:

def test_dictionary_with_dissimilar_array():
orig = [{'foo': 1, 'fiz': [True, 2.0, 3]}]
norm = normalize_parameters(orig)
expected = ({'foo': 1, 'fiz': ('True', '2.0', '3')},)
# assert evaluate_parameters(LaunchContext(), norm) == expected
orig = [{'foo': 1, 'fiz': [True, 1, TextSubstitution(text='foo')]}]
norm = normalize_parameters(orig)
print(norm)
expected = ({'foo': 1, 'fiz': ('True', '1', 'foo')},)
assert evaluate_parameters(LaunchContext(), norm) == expected
orig = [{'foo': 1, 'fiz': [TextSubstitution(text='foo'), True, 1]}]
norm = normalize_parameters(orig)
expected = ({'foo': 1, 'fiz': ('foo', 'True', '1')},)
assert evaluate_parameters(LaunchContext(), norm) == expected
orig = [{'foo': 1, 'fiz': [True, 1, [TextSubstitution(text='foo')]]}]
norm = normalize_parameters(orig)
expected = ({'foo': 1, 'fiz': ('True', '1', 'foo')},)
assert evaluate_parameters(LaunchContext(), norm) == expected
orig = [{'foo': 1, 'fiz': [[TextSubstitution(text='foo')], True, 1]}]
norm = normalize_parameters(orig)
expected = ({'foo': 1, 'fiz': ('foo', 'True', '1')},)
assert evaluate_parameters(LaunchContext(), norm) == expected

def test_list_of_substitutions_with_yaml_lists():
orig = [{
'foo': 1,
'fiz': [
[TextSubstitution(text="['asd', 'bsd']")],
[TextSubstitution(text="['asd', 'csd']")]
]
}]
norm = normalize_parameters(orig)
expected = ({'foo': 1, 'fiz': ("['asd', 'bsd']", "['asd', 'csd']")},)
assert evaluate_parameters(LaunchContext(), norm) == expected

I can add complicated logic for allowing interpreting some dissimilar arrays as arrays of strings, and some others not.
But, what's that useful for?

I think that in all the cases in test_dictionary_with_dissimilar_array, the user could write easily the parameters in other wat for getting an array of strings (if they want that). I wouldn't be so flexible as we are being now.

If we want that change, I could later simplify normalize_parameters logic in a follow-up PR.

Copy link
Member

Choose a reason for hiding this comment

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

That sounds fine to me. It's better to be strict and relax it later if someone makes a case for it, than to make it stricter later.

Copy link
Member

Choose a reason for hiding this comment

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

To be clear, +1 to ratcheting up the exception more if all the cases don't throw yet, whether or not that's just this change or something additional.

@ivanpauno ivanpauno requested a review from wjwwood June 6, 2019 19:32
expected = ({'foo': 1, 'fiz': ('True', '2.0', '3')},)
# assert evaluate_parameters(LaunchContext(), norm) == expected
with pytest.raises(TypeError) as exc:
orig = [{'foo': 1, 'fiz': [True, 2.0, 3]}]
Copy link
Contributor

@sloretz sloretz Jun 7, 2019

Choose a reason for hiding this comment

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

What happens if orig is [{'foo': 1, 'fiz': ['True', '2.0', '3']}] ? Does that get correcly interpreted as a list of strings, or does it raise TypeError?

Copy link
Member Author

Choose a reason for hiding this comment

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

It raises TypeError. For avoiding it, the user should write ["'True'", "'2.0'", "'3'"].
I added test cases in: 23421c9.

I don't know if this is the best way to go or not. But I think that behavior is consistent with the other test cases I have added (in 23421c9).

Copy link
Contributor

Choose a reason for hiding this comment

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

How so? Are plain dictionary values evaluated as YAML too? It doesn't look like it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Strings are converted to TextSubstitution in normalize_parameters before.
I don't have a better solution in mind, for avoiding the extra quotes.

Copy link
Member Author

Choose a reason for hiding this comment

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

How so? Are plain dictionary values evaluated as YAML too? It doesn't look like it.

Thinking about this again, I stopped converting raw strings (str) and TextSubstitution like yaml.
The second one is needed, as str are converted to TextSubstitution in normalize_parameters.

I'm happy with how the test looks now, but I have to recognize that both normalize_parameters and evaluate_parameters need a refactor. But I think that's ok for a follow-up.

@ivanpauno
Copy link
Member Author

@sloretz do you think this is ready? (after having green CI)

@ivanpauno ivanpauno requested a review from sloretz July 1, 2019 19:38
@ivanpauno
Copy link
Member Author

Refactored in db6a00f after being lightened by @hidmic.
In 821395a, I have corrected an old error I found in normalize_parameters while reading the code. Added test case for it.

@ivanpauno ivanpauno requested a review from hidmic July 2, 2019 19:19
Copy link
Contributor

@hidmic hidmic left a comment

Choose a reason for hiding this comment

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

LGTM now! @sloretz ?

@@ -74,7 +76,7 @@ def _normalize_parameter_array_value(value: SomeParameterValue) -> ParameterValu
# all were floats or ints, so return floats
make_mypy_happy_float = cast(List[Union[int, float]], value)
return tuple(float(e) for e in make_mypy_happy_float)
elif Substitution in has_types and has_types.issubset({str, Substitution, tuple}):
elif Substitution in has_types and has_types.issubset({str, Substitution}):
Copy link
Contributor

Choose a reason for hiding this comment

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

@ivanpauno why was tuple removed?

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't have sense, normalize_to_list_of_substitutions can't handle that and fails:

elif Substitution in has_types and has_types.issubset({str, Substitution, tuple}):
# make a list of substitutions forming a single string
return tuple(normalize_to_list_of_substitutions(cast(SomeSubstitutionsType, value)))

I added a test case for that:
orig = [{'foo': [
[TextSubstitution(text='fiz'), TextSubstitution(text='buz')],
TextSubstitution(text='fiz')
]}]
norm = normalize_parameters(orig)
expected = ({'foo': ('fizbuz', 'fiz')},)
assert evaluate_parameters(LaunchContext(), norm) == expected
.
Without removing tuple, it was failing.
The old test cases are passing with the removal.

Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
Signed-off-by: ivanpauno <ivanpauno@ekumenlabs.com>
@ivanpauno ivanpauno force-pushed the ivanpauno/correct-evaluate-parameters branch from 821395a to 2672750 Compare July 3, 2019 14:45
@ivanpauno
Copy link
Member Author

Rebased with master before running CI.

CI (up to test_launch_ros, launch_ros):

  • Linux Build Status
  • Linux-aarch64 Build Status
  • macOS Build Status
  • Windows Build Status

@ivanpauno ivanpauno merged commit 098c084 into master Jul 3, 2019
@delete-merged-branch delete-merged-branch bot deleted the ivanpauno/correct-evaluate-parameters branch July 3, 2019 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants