From 2c54607a8ce9df72c5606f4380f934246fa5db22 Mon Sep 17 00:00:00 2001 From: Filip Grcar Date: Wed, 22 Jul 2026 15:14:58 +0200 Subject: [PATCH 1/3] Support ports that are types and treat string ports conversion properly. --- py_trees/parsers/behaviour_tree_xml.py | 4 +- py_trees/ports.py | 2 +- py_trees/ports_utils.py | 104 +++++++++---- tests/test_ports_utils_conversions.py | 202 +++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 tests/test_ports_utils_conversions.py diff --git a/py_trees/parsers/behaviour_tree_xml.py b/py_trees/parsers/behaviour_tree_xml.py index 13639002..f9a59154 100644 --- a/py_trees/parsers/behaviour_tree_xml.py +++ b/py_trees/parsers/behaviour_tree_xml.py @@ -647,7 +647,7 @@ def instantiate_ports_node( ctor_callable = node_registry[elem.tag] # Try to convert the constructor arguments to the correct type. ignore_keys = {"child", "children", "behaviour_class_name"} - constructor_kwargs, success = apply_type_hints(ctor_callable, constructor_kwargs, logger=logger, ignore=ignore_keys) + success, constructor_kwargs = apply_type_hints(ctor_callable, constructor_kwargs, logger=logger, ignore=ignore_keys) if not success: logger.warning( "Failed to apply type hints to constructor arguments. See error log. Proceeding, but leaving " @@ -841,7 +841,7 @@ def build_tree_from_xml( constructor_kwargs[key] = elem.attrib.get(key) ignore_keys = {"child", "children", "behaviour_class_name"} - constructor_kwargs, success = apply_type_hints(cls, constructor_kwargs, logger=logger, ignore=ignore_keys) + success, constructor_kwargs = apply_type_hints(cls, constructor_kwargs, logger=logger, ignore=ignore_keys) if not success: logger.warning( "Failed to apply type hints to constructor arguments. See error log. " diff --git a/py_trees/ports.py b/py_trees/ports.py index 9a970f6b..87a7ee0d 100644 --- a/py_trees/ports.py +++ b/py_trees/ports.py @@ -428,7 +428,7 @@ def setup_ports( port_type = self.input_ports()[port].data_type try: - updated_value = convert_str_to_type(value, port_type, logger=self._ports_logger) + updated_value = convert_str_to_type(value, port_type, logger=self._ports_logger)[1] self.log_debug(f"Port {port}: Converted const value '{value}' to type {port_type}.") except ValueError as e: raise ValueError(f"Cannot convert Value '{value}' to type {port_type}") from e diff --git a/py_trees/ports_utils.py b/py_trees/ports_utils.py index eaf724dc..b9b3a9b0 100644 --- a/py_trees/ports_utils.py +++ b/py_trees/ports_utils.py @@ -13,6 +13,7 @@ # Imports ############################################################################## +import importlib import inspect import re import uuid @@ -116,25 +117,61 @@ def _convert_to_enum(value: str, enum_type: type[Enum]) -> Enum: raise ValueError(f"Cannot convert '{value}' to enum {enum_type.__name__}") -def _convert_simple(value: str, target_type: type) -> Any: +def str_to_type(type_spec: str) -> type: + """ + Resolve a fully-qualified or built-in type name into the actual type. + + A fully-qualified name includes the module path, e.g. ``'package.module.Type'``. + Built-in types can be specified by their simple name, e.g. ``'int'``, ``'str'``, ``'list'``, etc. + Raises :class:`ValueError` if the type cannot be resolved. + """ + if "." not in type_spec: + builtins_dict = vars(__import__("builtins")) + candidate = builtins_dict.get(type_spec) + if isinstance(candidate, type): + return candidate + raise ValueError("Type specification must include module path, e.g. 'package.module.Type'.") + + module_name, _, type_name = type_spec.rpartition(".") + if not module_name: + raise ValueError("Type specification must include module path, e.g. 'package.module.Type'.") + module = importlib.import_module(module_name) + resolved = getattr(module, type_name) + if not isinstance(resolved, type): + raise TypeError(f"Resolved '{type_spec}' to non-type object {resolved!r}.") + return resolved + + +def _convert_simple(value: str, target_type: type) -> tuple[bool, Any]: """Convert *value* to a supported scalar type, or return it unchanged.""" if target_type is str: - return value + return True, value if target_type is bool: - return _try_bool(value) + return True, _try_bool(value) if target_type is int: - return int(value) + return True, int(value) if target_type is float: - return float(value) + return True, float(value) if _is_enum_type(target_type): - return _convert_to_enum(value, target_type) # type: ignore - return value + return True, _convert_to_enum(value, target_type) # type: ignore + # Fallback: leave as string + return False, value -def convert_str_to_type(value: str, target_type: type | UnionType, logger: PortsLogger | None = None) -> Any: +def convert_str_to_type( + value: str, target_type: type | UnionType, logger: PortsLogger | None = None +) -> tuple[bool, Any]: """Convert a string *value* to *target_type* (handles unions, list, tuple, enum).""" if logger is None: logger = NOOP_LOGGER + + # If target type is `type`, the result needs to be parsed to a type. Example: 'float' -> + if target_type is type: + try: + return True, str_to_type(value) + except (ValueError, ImportError, AttributeError, TypeError): + pass # Fall back to try to parse it further down + origin = get_origin(target_type) if isinstance(target_type, type) and issubclass(target_type, list): @@ -149,7 +186,7 @@ def convert_str_to_type(value: str, target_type: type | UnionType, logger: Ports has_none = any(a is type(None) for a in args) # noqa: E721 lowered = value.strip().lower() if has_none and (lowered == "" or lowered == "none" or lowered == "null"): - return None + return True, None for arg in args: if arg is type(None): # noqa: E721 continue @@ -157,15 +194,17 @@ def convert_str_to_type(value: str, target_type: type | UnionType, logger: Ports return convert_str_to_type(value, arg, logger) except Exception: continue - return value + return False, value if origin is list: (inner_type,) = get_args(target_type) or (str,) parts = [p.strip() for p in re.split(r"[,;]", value)] if value.strip() else [] try: - return [convert_str_to_type(p, inner_type, logger) for p in parts] + results = [convert_str_to_type(p, inner_type, logger) for p in parts] + successes = [s for s, _ in results] + return all(successes), [r for _, r in results] except Exception: - return [p.strip() for p in parts] + return False, [p.strip() for p in parts] if origin is tuple: inner_types = get_args(target_type) @@ -176,10 +215,11 @@ def convert_str_to_type(value: str, target_type: type | UnionType, logger: Ports try: converted.append(convert_str_to_type(p, t, logger)) except Exception: - converted.append(p) - return tuple(converted) + converted.append((False, p)) + successes = [s for s, _ in converted] + return all(successes), tuple(r for _, r in converted) - return value + return False, value def collect_type_hints(constructor: Callable) -> dict[str, Any]: @@ -198,8 +238,10 @@ def collect_type_hints(constructor: Callable) -> dict[str, Any]: Returns: dict[str, Any]: Mapping of parameter name to its type annotation. Parameters - without an annotation anywhere in the hierarchy are omitted, as are - ``self`` and any ``*args``/``**kwargs`` catch-alls. + without an annotation anywhere in the hierarchy are omitted, as is + ``self`` and any ``*args`` catch-all. A ``**kwargs`` catch-all is kept + (typically under the key ``"kwargs"``) so callers can fall back to it + for keys that don't have their own individual type hint. """ classes = constructor.__mro__ if inspect.isclass(constructor) else (constructor,) @@ -216,7 +258,7 @@ def collect_type_hints(constructor: Callable) -> dict[str, Any]: for pname, param in sig.parameters.items(): if pname == "self": continue - if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): + if param.kind is inspect.Parameter.VAR_POSITIONAL: continue if param.annotation is inspect._empty: continue @@ -233,7 +275,7 @@ def apply_type_hints( kwargs: dict[str, Any], logger: PortsLogger | None = None, ignore: set[str] | None = None, -) -> tuple[dict[str, Any], bool]: +) -> tuple[bool, dict[str, Any]]: """ Convert XML string kwargs into hinted types from the constructor signature. @@ -241,12 +283,15 @@ def apply_type_hints( - If `constructor` is a class, its `__init__` is inspected (excluding `self`). - Only parameters that have type annotations are converted. + - A key with no individual type hint falls back to the constructor's `**kwargs` + annotation, if any (e.g. dynamic keys consumed by a `**kwargs: SomeType` catch-all). - On conversion failure, the original string is preserved and a warning is printed. The function return indicates that there was a failure in one of the values. Returns: - tuple[dict[str, Any], bool]: The converted dictionary and a flag set to False if one of - the values was attempted bo convert but it failed (a warning was also printed then). + tuple[bool, dict[str, Any]]: Success of conversion (True if all values were successfully + converted, False if any conversion failed, a warning was also printed then), and the + converted dictionary. """ if ignore is None: ignore = set() @@ -262,9 +307,10 @@ def apply_type_hints( converted[k] = v continue - tp = hints.get(k) + # Obtain the type hint with fallback to the constructor's **kwargs annotation, if any. + tp = hints.get(k) or hints.get("kwargs") # Default behavior: keep the original value. - # Warning will be printed at the end of this loop if it is not overwritten. + # Warning will be printed at the end of this loop if it failed to be converted. converted[k] = v # No type hint given: keep the original value @@ -290,17 +336,15 @@ def apply_type_hints( continue try: - converted[k] = convert_str_to_type(v, tp, logger) + conversion_success, converted[k] = convert_str_to_type(v, tp, logger) + if not conversion_success: + logger.warning(f"Failed to convert '{k}: {v}' to type '{tp}'. Result: '{converted[k]}'.") + success = False except ValueError as e: logger.warning(f"Failed to convert '{k}: {v}' to type '{tp}': {e}") success = False - continue - - if converted[k] == v: - logger.warning(f"Failed to convert '{k}: {v}' to type '{tp}'. Preserved original value.") - success = False - return converted, success + return success, converted def reset_blackboard_key( diff --git a/tests/test_ports_utils_conversions.py b/tests/test_ports_utils_conversions.py new file mode 100644 index 00000000..4ef44e7d --- /dev/null +++ b/tests/test_ports_utils_conversions.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# License: BSD +# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE +# + +############################################################################## +# Imports +############################################################################## + +import unittest +from enum import Enum +from typing import Any + +from py_trees.ports_utils import apply_type_hints, convert_str_to_type + + +class StdoutLogger: + """Simple stdout logger matching the PortsLogger protocol.""" + + def debug(self, msg: str) -> None: + print(f"[DEBUG] {msg}") + + def info(self, msg: str) -> None: + print(f"[INFO] {msg}") + + def warning(self, msg: str) -> None: + print(f"[WARNING] {msg}") + + def error(self, msg: str) -> None: + print(f"[ERROR] {msg}") + + +class ColorInt(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + +class ColorStr(Enum): + RED = "red" + GREEN = "green" + BLUE = "blue" + + +class TestConvertStrToType(unittest.TestCase): + def test_primitives_and_enums(self) -> None: + self.assertEqual(convert_str_to_type("true", bool)[1], True) + self.assertEqual(convert_str_to_type("False", bool)[1], False) + self.assertEqual(convert_str_to_type(" 7 ", int)[1], 7) + self.assertAlmostEqual(convert_str_to_type("2.5", float)[1], 2.5) + self.assertEqual(convert_str_to_type("hello", str)[1], "hello") + + # enum by NAME (case-insensitive) + self.assertEqual(convert_str_to_type("green", ColorInt)[1], ColorInt.GREEN) + self.assertEqual(convert_str_to_type("BLUE", ColorStr)[1], ColorStr.BLUE) + + # enum by VALUE (int-backed / str-backed) + self.assertEqual(convert_str_to_type("2", ColorInt)[1], ColorInt.GREEN) + self.assertEqual(convert_str_to_type("blue", ColorStr)[1], ColorStr.BLUE) + + def test_type_target(self) -> None: + # target_type `type` resolves the string to an actual type object. + self.assertEqual(convert_str_to_type("float", type), (True, float)) + self.assertEqual(convert_str_to_type("int", type), (True, int)) + self.assertEqual(convert_str_to_type("str", type), (True, str)) + self.assertEqual(convert_str_to_type("bool", type), (True, bool)) + + def test_optional_union_lists_tuples(self) -> None: + self.assertIsNone(convert_str_to_type("", int | None)[1]) + self.assertIsNone(convert_str_to_type("None", int | None)[1]) + self.assertEqual(convert_str_to_type("42", int | str)[1], 42) + self.assertEqual(convert_str_to_type("forty-two", int | str)[1], "forty-two") + + self.assertEqual(convert_str_to_type("1, 2,3", list[int])[1], [1, 2, 3]) + self.assertEqual( + convert_str_to_type("RED, green", list[ColorInt])[1], + [ColorInt.RED, ColorInt.GREEN], + ) + + self.assertEqual( + convert_str_to_type("9, hello, true", tuple[int, str, bool])[1], + (9, "hello", True), + ) + + def test_union_with_str_fallback_reports_success(self) -> None: + # A value that fails the enum branch but matches the trailing `str` branch of a + # Union is a legitimate successful conversion, not a failure. + success, value = convert_str_to_type("not-an-enum", ColorInt | str) + self.assertTrue(success) + self.assertEqual(value, "not-an-enum") + self.assertIsInstance(value, str) + + +class DummyCtorTarget: + def __init__( + self, + a_int: int, + b_float: float, + c_str: str, + d_bool: bool, + e_color: ColorInt, + f_opt_int: int | None, + g_union_num: int | float, + h_union_enum: ColorInt | str, + i_default_str: str = "default", + **kwargs: int | str, + ): + # not used; we only need type hints from the signature + pass + + +class TestApplyTypeHints(unittest.TestCase): + def setUp(self) -> None: + self.logger = StdoutLogger() + return super().setUp() + + def test_fallback_on_failure(self) -> None: + def ctor_with_hints(a: int, b: float, c: bool): # pragma: no cover - only signature used + ... + + raw = {"a": "oops", "b": "2.0", "c": "true"} + success, converted = apply_type_hints(ctor_with_hints, raw, self.logger) + self.assertFalse(success) + self.assertEqual(converted["a"], "oops") + self.assertEqual(converted["b"], 2.0) + self.assertEqual(converted["c"], True) + + def test_primitives_and_bool(self) -> None: + raw: dict[str, Any] = { + "a_int": "42", + "b_float": "3.125", + "c_str": "hello", + "d_bool": "true", + "e_color": "RED", + "f_opt_int": "none", + "g_union_num": "10", + "h_union_enum": "GREEN", + } + success, converted = apply_type_hints(DummyCtorTarget, raw, self.logger) + self.assertTrue(success) + self.assertEqual(converted["a_int"], 42) + self.assertEqual(converted["d_bool"], True) + self.assertIsNone(converted["f_opt_int"]) + self.assertEqual(converted["h_union_enum"], ColorInt.GREEN) + + def test_union_falling_through_to_str_is_success(self) -> None: + # Previously flagged as failure since the converted value looked "unchanged". + raw = { + "a_int": "1", + "b_float": "0", + "c_str": "x", + "d_bool": "true", + "e_color": "RED", + "f_opt_int": "None", + "g_union_num": "1", + "h_union_enum": "not-an-enum", + } + success, converted = apply_type_hints(DummyCtorTarget, raw, self.logger) + self.assertTrue(success) + self.assertEqual(converted["h_union_enum"], "not-an-enum") + self.assertIsInstance(converted["h_union_enum"], str) + + def test_kwargs_type_fallback(self) -> None: + # Keys not in the signature (e.g. dynamic kwargs) should fall back to the + # constructor's **kwargs annotation instead of being reported as unconvertible. + raw = { + "a_int": "1", + "b_float": "0", + "c_str": "x", + "d_bool": "true", + "e_color": "RED", + "f_opt_int": "None", + "g_union_num": "1", + "h_union_enum": "RED", + "unexpected_kwarg": "should remain string", + } + success, converted = apply_type_hints(DummyCtorTarget, raw, self.logger) + self.assertTrue(success) + self.assertEqual(converted["unexpected_kwarg"], "should remain string") + self.assertIsInstance(converted["unexpected_kwarg"], str) + + raw["unexpected_kwarg"] = "123" + success, converted = apply_type_hints(DummyCtorTarget, raw, self.logger) + self.assertTrue(success) + self.assertEqual(converted["unexpected_kwarg"], 123) + self.assertIsInstance(converted["unexpected_kwarg"], int) + + def test_no_type_hint_available(self) -> None: + raw = {"totally_unknown": "value"} + + class NoKwargsTarget: + def __init__(self, a_int: int): # pragma: no cover - only signature used + pass + + success, converted = apply_type_hints(NoKwargsTarget, raw, self.logger) + self.assertFalse(success) + self.assertEqual(converted["totally_unknown"], "value") + + +if __name__ == "__main__": + unittest.main() From 2e5edda0b0109f0c37d7bf54cf220c5e9f210fc4 Mon Sep 17 00:00:00 2001 From: Jennifer Buehler Date: Thu, 20 Aug 2026 18:07:19 +0200 Subject: [PATCH 2/3] Some suggested improvements (#1) * suggested improvements * address feedback --- py_trees/ports.py | 6 ++++-- py_trees/ports_utils.py | 23 +++++++++++++++++++---- tests/test_ports_utils_conversions.py | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/py_trees/ports.py b/py_trees/ports.py index 87a7ee0d..512c4e2d 100644 --- a/py_trees/ports.py +++ b/py_trees/ports.py @@ -428,10 +428,12 @@ def setup_ports( port_type = self.input_ports()[port].data_type try: - updated_value = convert_str_to_type(value, port_type, logger=self._ports_logger)[1] - self.log_debug(f"Port {port}: Converted const value '{value}' to type {port_type}.") + converted, updated_value = convert_str_to_type(value, port_type, logger=self._ports_logger) except ValueError as e: raise ValueError(f"Cannot convert Value '{value}' to type {port_type}") from e + if not converted: + raise ValueError(f"Cannot convert Value '{value}' to type {port_type}") + self.log_debug(f"Port {port}: Converted const value '{value}' to type {port_type}.") key = local_key # Remap to the local key holding the constant value self._blackboard_client.set(key, updated_value) # Resolve relative remap targets under the subtree namespace. diff --git a/py_trees/ports_utils.py b/py_trees/ports_utils.py index b9b3a9b0..f701a673 100644 --- a/py_trees/ports_utils.py +++ b/py_trees/ports_utils.py @@ -178,7 +178,10 @@ def convert_str_to_type( origin = list if origin is None: - assert isinstance(target_type, type) + if not isinstance(target_type, type): + # Not a runtime type (e.g. typing.Any on Python 3.10, or a stringised annotation): + # there is nothing we can convert to, so report failure and keep the string. + return False, value return _convert_simple(value, target_type) if origin is Union or origin is UnionType: @@ -191,9 +194,12 @@ def convert_str_to_type( if arg is type(None): # noqa: E721 continue try: - return convert_str_to_type(value, arg, logger) + arg_success, arg_value = convert_str_to_type(value, arg, logger) except Exception: continue + # A member reporting failure is no better than one that raised: try the next one. + if arg_success: + return True, arg_value return False, value if origin is list: @@ -285,6 +291,10 @@ def apply_type_hints( - Only parameters that have type annotations are converted. - A key with no individual type hint falls back to the constructor's `**kwargs` annotation, if any (e.g. dynamic keys consumed by a `**kwargs: SomeType` catch-all). + The catch-all is looked up by the conventional name `kwargs`, so one declared under + another name (e.g. `**options`) provides no fallback. + - An unconstrained target type (`Any`, `object`) counts as a success with the string kept, + since any value satisfies it. - On conversion failure, the original string is preserved and a warning is printed. The function return indicates that there was a failure in one of the values. @@ -308,7 +318,7 @@ def apply_type_hints( continue # Obtain the type hint with fallback to the constructor's **kwargs annotation, if any. - tp = hints.get(k) or hints.get("kwargs") + tp = hints.get(k, hints.get("kwargs")) # Default behavior: keep the original value. # Warning will be printed at the end of this loop if it failed to be converted. converted[k] = v @@ -324,6 +334,10 @@ def apply_type_hints( # Target type is already a string: no need to do anything. continue + if tp is Any or tp is object: + # The target puts no constraint on the value, so string is already fine. + continue + # Not a string: if the target is already of the correct type, we can just keep it as-is. if not isinstance(v, str): if tp is not type(v): @@ -340,7 +354,8 @@ def apply_type_hints( if not conversion_success: logger.warning(f"Failed to convert '{k}: {v}' to type '{tp}'. Result: '{converted[k]}'.") success = False - except ValueError as e: + except Exception as e: + # Resolving a type by name can raise anything the target module raises on import. logger.warning(f"Failed to convert '{k}: {v}' to type '{tp}': {e}") success = False diff --git a/tests/test_ports_utils_conversions.py b/tests/test_ports_utils_conversions.py index 4ef44e7d..f8a86aa3 100644 --- a/tests/test_ports_utils_conversions.py +++ b/tests/test_ports_utils_conversions.py @@ -83,6 +83,12 @@ def test_optional_union_lists_tuples(self) -> None: (9, "hello", True), ) + def test_union_member_order_does_not_matter(self) -> None: + # An unsupported member reports failure without raising, so it must not stop the + # remaining members from being tried. + self.assertEqual(convert_str_to_type("5", dict | int), (True, 5)) + self.assertEqual(convert_str_to_type("5", int | dict), (True, 5)) + def test_union_with_str_fallback_reports_success(self) -> None: # A value that fails the enum branch but matches the trailing `str` branch of a # Union is a legitimate successful conversion, not a failure. @@ -186,6 +192,20 @@ def test_kwargs_type_fallback(self) -> None: self.assertEqual(converted["unexpected_kwarg"], 123) self.assertIsInstance(converted["unexpected_kwarg"], int) + def test_any_kwargs_catch_all_is_a_pass_through(self) -> None: + # `**kwargs: Any` is the convention throughout py_trees, so an unknown attribute must + # stay a string and still count as a success. Note that on Python 3.10 `typing.Any` is + # not a class, so this also covers conversion of a non-class annotation. + class AnyKwargsTarget: + def __init__(self, name: str, **kwargs: Any): # pragma: no cover - only signature used + pass + + raw = {"name": "node", "unhinted": "42"} + success, converted = apply_type_hints(AnyKwargsTarget, raw, self.logger) + self.assertTrue(success) + self.assertEqual(converted["unhinted"], "42") + self.assertIsInstance(converted["unhinted"], str) + def test_no_type_hint_available(self) -> None: raw = {"totally_unknown": "value"} From 6bdfb35cdf2515fba3e4be6dd2a1eaa36b010480 Mon Sep 17 00:00:00 2001 From: Filip Grcar Date: Fri, 21 Aug 2026 11:03:16 +0200 Subject: [PATCH 3/3] Address PR comments. --- py_trees/ports_utils.py | 5 ++-- tests/test_ports_utils_conversions.py | 38 ++++++++++++++------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/py_trees/ports_utils.py b/py_trees/ports_utils.py index f701a673..f10e90bc 100644 --- a/py_trees/ports_utils.py +++ b/py_trees/ports_utils.py @@ -125,14 +125,13 @@ def str_to_type(type_spec: str) -> type: Built-in types can be specified by their simple name, e.g. ``'int'``, ``'str'``, ``'list'``, etc. Raises :class:`ValueError` if the type cannot be resolved. """ - if "." not in type_spec: + module_name, partition, type_name = type_spec.rpartition(".") + if not partition: builtins_dict = vars(__import__("builtins")) candidate = builtins_dict.get(type_spec) if isinstance(candidate, type): return candidate - raise ValueError("Type specification must include module path, e.g. 'package.module.Type'.") - module_name, _, type_name = type_spec.rpartition(".") if not module_name: raise ValueError("Type specification must include module path, e.g. 'package.module.Type'.") module = importlib.import_module(module_name) diff --git a/tests/test_ports_utils_conversions.py b/tests/test_ports_utils_conversions.py index f8a86aa3..c0020eea 100644 --- a/tests/test_ports_utils_conversions.py +++ b/tests/test_ports_utils_conversions.py @@ -45,19 +45,21 @@ class ColorStr(Enum): class TestConvertStrToType(unittest.TestCase): def test_primitives_and_enums(self) -> None: - self.assertEqual(convert_str_to_type("true", bool)[1], True) - self.assertEqual(convert_str_to_type("False", bool)[1], False) - self.assertEqual(convert_str_to_type(" 7 ", int)[1], 7) - self.assertAlmostEqual(convert_str_to_type("2.5", float)[1], 2.5) - self.assertEqual(convert_str_to_type("hello", str)[1], "hello") + self.assertEqual(convert_str_to_type("true", bool), (True, True)) + self.assertEqual(convert_str_to_type("False", bool), (True, False)) + self.assertEqual(convert_str_to_type(" 7 ", int), (True, 7)) + success, value = convert_str_to_type("2.5", float) + self.assertTrue(success) + self.assertAlmostEqual(value, 2.5) + self.assertEqual(convert_str_to_type("hello", str), (True, "hello")) # enum by NAME (case-insensitive) - self.assertEqual(convert_str_to_type("green", ColorInt)[1], ColorInt.GREEN) - self.assertEqual(convert_str_to_type("BLUE", ColorStr)[1], ColorStr.BLUE) + self.assertEqual(convert_str_to_type("green", ColorInt), (True, ColorInt.GREEN)) + self.assertEqual(convert_str_to_type("BLUE", ColorStr), (True, ColorStr.BLUE)) # enum by VALUE (int-backed / str-backed) - self.assertEqual(convert_str_to_type("2", ColorInt)[1], ColorInt.GREEN) - self.assertEqual(convert_str_to_type("blue", ColorStr)[1], ColorStr.BLUE) + self.assertEqual(convert_str_to_type("2", ColorInt), (True, ColorInt.GREEN)) + self.assertEqual(convert_str_to_type("blue", ColorStr), (True, ColorStr.BLUE)) def test_type_target(self) -> None: # target_type `type` resolves the string to an actual type object. @@ -67,20 +69,20 @@ def test_type_target(self) -> None: self.assertEqual(convert_str_to_type("bool", type), (True, bool)) def test_optional_union_lists_tuples(self) -> None: - self.assertIsNone(convert_str_to_type("", int | None)[1]) - self.assertIsNone(convert_str_to_type("None", int | None)[1]) - self.assertEqual(convert_str_to_type("42", int | str)[1], 42) - self.assertEqual(convert_str_to_type("forty-two", int | str)[1], "forty-two") + self.assertEqual(convert_str_to_type("", int | None), (True, None)) + self.assertEqual(convert_str_to_type("None", int | None), (True, None)) + self.assertEqual(convert_str_to_type("42", int | str), (True, 42)) + self.assertEqual(convert_str_to_type("forty-two", int | str), (True, "forty-two")) - self.assertEqual(convert_str_to_type("1, 2,3", list[int])[1], [1, 2, 3]) + self.assertEqual(convert_str_to_type("1, 2,3", list[int]), (True, [1, 2, 3])) self.assertEqual( - convert_str_to_type("RED, green", list[ColorInt])[1], - [ColorInt.RED, ColorInt.GREEN], + convert_str_to_type("RED, green", list[ColorInt]), + (True, [ColorInt.RED, ColorInt.GREEN]), ) self.assertEqual( - convert_str_to_type("9, hello, true", tuple[int, str, bool])[1], - (9, "hello", True), + convert_str_to_type("9, hello, true", tuple[int, str, bool]), + (True, (9, "hello", True)), ) def test_union_member_order_does_not_matter(self) -> None: