From 8cc62a66211d93f4ca834967b82c2c7589980b91 Mon Sep 17 00:00:00 2001 From: "Tristan F." Date: Sun, 14 Jun 2026 13:29:48 -0700 Subject: [PATCH] fix: allow for coercions after python_evalish_coerce My assumption that BeforeValidators ran after numeric coercion was wrong. Since the only return types for `python_evalish_coerce` are from `is_numpy_friendly`, we can bake in a check for `python_evalish_coerce`'s guard statement that checks if the input is coercable to a float: this does not cover all of the behavior for Pydantic's coersion (https://pydantic.dev/docs/validation/latest/concepts/conversion_table), but it will at least allow pydantic to coerce strings. [Optimally, we would trigger pydantic's coercion logic ourselves, but it doesn't seem to expose a nice API for this.] --- spras/config/algorithms.py | 10 +++++++++- test/test_config.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spras/config/algorithms.py b/spras/config/algorithms.py index 552fbc4e..4951b45f 100644 --- a/spras/config/algorithms.py +++ b/spras/config/algorithms.py @@ -22,6 +22,14 @@ # This contains the dynamically generated algorithm schema for use in `schema.py` __all__ = ['AlgorithmUnion'] +def is_numeric(value: Any) -> bool: + """Checks if the input value can be parsed by `float`.""" + try: + float(value) + return True + except ValueError: + return False + def is_numpy_friendly(type: type[Any] | None) -> bool: """ Whether the passed in type can have any numpy helpers. @@ -44,7 +52,7 @@ def python_evalish_coerce(value: Any) -> Any: resources if wanted. This only prevents secret leakage. """ - if not isinstance(value, str): + if not isinstance(value, str) or is_numeric(value): return value # These strings are in the form of function calls `function.name(param1, param2, ...)`. diff --git a/test/test_config.py b/test/test_config.py index cf4cdf0f..8edc56c1 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -67,6 +67,9 @@ def get_test_config(): "include": True, "runs": { "strings": {"dummy_mode": ["terminals", "others"], "b": 3}, + # We include slightly absurd numbers here for b + # to test https://github.com/Reed-CompBio/spras/issues/484. + "scientific": {"dummy_mode": "terminals", "b": ["1e-2", "1e2"]}, # spacing in np.linspace is on purpose "singleton_string_np_linspace": {"dummy_mode": "terminals", "b": "np.linspace(0, 5,2,)"}, "str_array_np_logspace": {"dummy_mode": ["others", "all"], "g": "np.logspace(1,1)"} @@ -296,6 +299,11 @@ def test_config_values(self): OmicsIntegrator2Params(dummy_mode=DummyMode.others, b=3) ]) + value_test_util('omicsintegrator2', 'scientific', OmicsIntegrator2Params, [ + OmicsIntegrator2Params(dummy_mode=DummyMode.terminals, b=100), + OmicsIntegrator2Params(dummy_mode=DummyMode.terminals, b=0.01) + ]) + value_test_util('omicsintegrator2', 'singleton_string_np_linspace', OmicsIntegrator2Params, [ OmicsIntegrator2Params(dummy_mode=DummyMode.terminals, b=5.0), OmicsIntegrator2Params(dummy_mode=DummyMode.terminals, b=0.0)