diff --git a/lite_bootstrap/instruments/base.py b/lite_bootstrap/instruments/base.py index 04d2410..32256ea 100644 --- a/lite_bootstrap/instruments/base.py +++ b/lite_bootstrap/instruments/base.py @@ -15,17 +15,15 @@ class BaseConfig: @classmethod def from_dict(cls, data: dict[str, typing.Any]) -> typing_extensions.Self: + """Build a config from a dict; unknown keys are silently dropped, explicit None overrides defaults.""" field_names = {f.name for f in dataclasses.fields(cls)} return cls(**{k: v for k, v in data.items() if k in field_names}) @classmethod def from_object(cls, obj: object) -> typing_extensions.Self: - prepared_data = {} + """Build a config by merging non-None attributes from obj; None or missing attributes fall back to defaults.""" field_names = {f.name for f in dataclasses.fields(cls)} - - for field in field_names: - if (value := getattr(obj, field, None)) is not None: - prepared_data[field] = value + prepared_data = {field: value for field in field_names if (value := getattr(obj, field, None)) is not None} return cls(**prepared_data) diff --git a/tests/test_config.py b/tests/test_config.py index bffa7f8..27f5d56 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -49,3 +49,46 @@ def test_config_from_object() -> None: short_config = BaseConfig.from_object(big_config) for field in dataclasses.fields(BaseConfig): assert getattr(short_config, field.name) == getattr(big_config, field.name) + + +def test_from_object_skips_none_attribute() -> None: + @dataclasses.dataclass + class Source: + service_name: str | None = None + service_version: str = "2.0.0" + + config = BaseConfig.from_object(Source()) + assert config.service_name == "micro-service" + assert config.service_version == "2.0.0" + + +def test_from_object_skips_missing_attribute() -> None: + class Source: + pass + + config = BaseConfig.from_object(Source()) + assert config.service_name == "micro-service" + assert config.service_version == "1.0.0" + assert config.service_debug is True + + +def test_from_object_preserves_falsy_values() -> None: + @dataclasses.dataclass + class Source: + service_name: str = "" + service_debug: bool = False + + config = BaseConfig.from_object(Source()) + assert config.service_name == "" + assert config.service_debug is False + + +def test_from_dict_drops_unknown_keys_silently() -> None: + config = BaseConfig.from_dict({"service_name": "test", "unknown_key": "value"}) + assert config.service_name == "test" + assert config.service_version == "1.0.0" + + +def test_from_dict_explicit_none_overrides_default() -> None: + config = BaseConfig.from_dict({"service_name": None}) + assert config.service_name is None