From a73e69b4bb0412b4bbd1e74f2aaa47edb67f866e Mon Sep 17 00:00:00 2001 From: Dennis Lanov Date: Thu, 30 Jul 2026 17:01:01 -0500 Subject: [PATCH] Validate iterable elements in ensure_argument_type ensure_argument_type accepted iterables without validating that every element was a type. Invalid entries could expose Python's generic isinstance error or be skipped when any() short-circuited after an earlier match. Materialize the iterable once and validate all elements before matching, while preserving existing behavior for valid inputs and empty iterables. Signed-off-by: Dennis Lanov --- .../utilities/ensure_argument_type_impl.py | 11 ++++++- .../utilities/test_ensure_argument_type.py | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/launch/launch/utilities/ensure_argument_type_impl.py b/launch/launch/utilities/ensure_argument_type_impl.py index 965756e3a..2cb32a1b3 100644 --- a/launch/launch/utilities/ensure_argument_type_impl.py +++ b/launch/launch/utilities/ensure_argument_type_impl.py @@ -69,7 +69,16 @@ def check_argument(argument: Any, type_var: Type[Any]) -> bool: result |= issubclass(argument.__class__, type_var) return result - list_of_types = types if isinstance(types, collections.abc.Iterable) else [types] + list_of_types = list(types) if isinstance(types, collections.abc.Iterable) else [types] + for type_var in list_of_types: + if not isinstance(type_var, type): + raise TypeError(error_msg_template.format( + "'ensure_argument_type()' e", + 'types', + 'type, collections.abc.Iterable of type', + type_var, + type(type_var), + )) if not any(check_argument(argument, type_var) for type_var in list_of_types): raise TypeError(error_msg_template.format( 'E' if caller is None else "'{}' e".format(caller), diff --git a/launch/test/launch/utilities/test_ensure_argument_type.py b/launch/test/launch/utilities/test_ensure_argument_type.py index b49bc75ac..641b167d9 100644 --- a/launch/test/launch/utilities/test_ensure_argument_type.py +++ b/launch/test/launch/utilities/test_ensure_argument_type.py @@ -40,6 +40,37 @@ class MockChildClass(MockClass): ensure_argument_type(mock_child_obj, MockClass, 'MockChildClass') +def test_valid_iterables_of_types(): + """Test the ensure_argument_type function with valid iterables of types.""" + ensure_argument_type('foo', (str, int), 'arg_foo') + ensure_argument_type(1, [str, int], 'arg_bar') + ensure_argument_type('foo', (t for t in (str, int)), 'arg_baz') + + +def test_invalid_element_in_types(): + """Test the ensure_argument_type function with non-type elements in the types iterable.""" + # Invalid element before a valid type. + with pytest.raises(TypeError) as ex: + ensure_argument_type('foo', [123, str], 'arg_foo') + assert "'types'" in str(ex.value) + assert 'type, collections.abc.Iterable of type' in str(ex.value) + # Invalid element after a type that matches the argument. + with pytest.raises(TypeError) as ex: + ensure_argument_type('foo', [str, 123], 'arg_foo') + assert "'types'" in str(ex.value) + assert 'type, collections.abc.Iterable of type' in str(ex.value) + # A string is an iterable, but not of types. + with pytest.raises(TypeError) as ex: + ensure_argument_type('foo', 'str', 'arg_foo') + assert "'types'" in str(ex.value) + assert 'type, collections.abc.Iterable of type' in str(ex.value) + # The error message should identify the 'types' parameter and the expectation. + with pytest.raises(TypeError) as ex: + ensure_argument_type('foo', (t for t in (str, None)), 'arg_foo') + assert "'ensure_argument_type()' expected 'types'" in str(ex.value) + assert 'type, collections.abc.Iterable of type' in str(ex.value) + + def test_invalid_argument_types(): """Test the ensure_argument_type function with invalid input.""" with pytest.raises(TypeError):