From 9b1c232a6d72d6f64371de10123b97cd6b2748cb Mon Sep 17 00:00:00 2001 From: Naveen Chatlapalli Date: Wed, 30 Sep 2026 22:31:39 -0500 Subject: [PATCH] Coerce non-optional union settings from environment values --- .../core/agent_framework/_settings.py | 2 +- .../packages/core/tests/core/test_settings.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_settings.py b/python/packages/core/agent_framework/_settings.py index d674c47f99e..ffcd225c005 100644 --- a/python/packages/core/agent_framework/_settings.py +++ b/python/packages/core/agent_framework/_settings.py @@ -153,7 +153,7 @@ def _coerce_value(value: str, target_type: type) -> Any: if origin is type(None): return None - if args and type(None) in args: + if origin is Union or origin is type(int | str): for arg in args: if arg is not type(None): if arg is Any: diff --git a/python/packages/core/tests/core/test_settings.py b/python/packages/core/tests/core/test_settings.py index af480ffe833..6853079f00b 100644 --- a/python/packages/core/tests/core/test_settings.py +++ b/python/packages/core/tests/core/test_settings.py @@ -42,6 +42,45 @@ class ExclusiveSettings(TypedDict, total=False): class TestLoadSettingsBasic: """Test basic load_settings functionality.""" + @pytest.mark.parametrize("raw, expected", [("5", 5), ("2.5", 2.5)]) + @pytest.mark.parametrize("source", ["environment", "dotenv"]) + def test_non_optional_union_coercion( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, raw: str, expected: int | float, source: str + ) -> None: + class NumericSettings(TypedDict): + value: int | float + + env_file = tmp_path / ".env" + monkeypatch.delenv("UNION_VALUE", raising=False) + env_file.write_text(f"UNION_VALUE={raw}\n" if source == "dotenv" else "", encoding="utf-8") + if source == "environment": + monkeypatch.setenv("UNION_VALUE", raw) + + value = load_settings(NumericSettings, env_prefix="UNION_", env_file_path=str(env_file))["value"] + + assert value == expected + assert type(value) is type(expected) + + @pytest.mark.parametrize("source", ["environment", "dotenv"]) + def test_invalid_non_optional_union_reports_source( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, source: str + ) -> None: + class NumericSettings(TypedDict): + value: int | float + + invalid = "private-invalid-value" + env_file = tmp_path / ".env" + monkeypatch.delenv("UNION_VALUE", raising=False) + env_file.write_text(f"UNION_VALUE={invalid}\n" if source == "dotenv" else "", encoding="utf-8") + if source == "environment": + monkeypatch.setenv("UNION_VALUE", invalid) + + with pytest.raises(ValueError, match="setting 'value' from") as exc_info: + load_settings(NumericSettings, env_prefix="UNION_", env_file_path=str(env_file)) + + assert ("environment variable" if source == "environment" else str(env_file)) in str(exc_info.value) + assert invalid not in str(exc_info.value) + def test_fields_are_none_when_unset(self) -> None: settings = load_settings(SimpleSettings, env_prefix="TEST_APP_")