From cbc66f2fd6e82a376d33ff06b6d4d85b40026b30 Mon Sep 17 00:00:00 2001 From: Brian Li Date: Sat, 7 Feb 2026 17:43:42 +0800 Subject: [PATCH] feat: auto-instantiate type-annotated Config subclasses Extends Config initialization to automatically instantiate subclasses declared via type annotations (e.g., sub: SubConfig), not just direct assignments (e.g., sub = SubConfig). Type Safety Benefits: - 'sub = SubConfig' assigns the class itself, making type checkers infer type[SubConfig] (the class), breaking autocomplete and type checking - 'sub: SubConfig' declares an instance annotation, allowing type checkers to correctly infer SubConfig (an instance) for proper IDE support - This commit makes the type-safe pattern work at runtime --- configurize/config.py | 21 +++++++++++++++++++++ tests/test_config.py | 28 ++++++++++++++++++---------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/configurize/config.py b/configurize/config.py index f5ad9a0..2492786 100644 --- a/configurize/config.py +++ b/configurize/config.py @@ -311,6 +311,7 @@ def __init__(self, **kwargs): ) self._merge_args(kwargs) + # Handle assigned Config subclasses (e.g., sub = SubConfig) for k, v in self.items(deref=False): if type(v) is type and issubclass(v, Config): v = v() @@ -319,6 +320,26 @@ def __init__(self, **kwargs): v._father = weakref.ref(self) v._sub_cfg_name = k + # Handle type-annotated Config subclasses (e.g., sub: SubConfig) + annotations = self._get_class_annotations() + for k, annotation_type in annotations.items(): + # Skip if already set as an attribute + if hasattr(self, k): + continue + # Check if the annotation is a Config subclass type + try: + if type(annotation_type) is type and issubclass( + annotation_type, Config + ): + # Instantiate the Config subclass + v = annotation_type() + setattr(self, k, v) + v._father = weakref.ref(self) + v._sub_cfg_name = k + except TypeError: + # issubclass raises TypeError if annotation_type is not a class + pass + if self._allow_search: self._flatten_args = self._flatten_config() diff --git a/tests/test_config.py b/tests/test_config.py index 9ebd6a8..1c68259 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,7 @@ """Basic tests for configurize.Config""" +from __future__ import annotations + from configurize import Config from configurize.reference import Ref @@ -29,25 +31,31 @@ class BaseConfig(Config): assert cfg.b == 2 -def test_config_references(): - """Test using Ref to reference other config values""" +class SubConfig(Config): + value = 100 + self_ref = Ref(".value") + parent_ref = Ref("..base_value") + - class SubConfig(Config): - value = 100 - self_ref = Ref(".value") - parent_ref = Ref("..base_value") +class ParentConfig(Config): + base_value = 42 + optional: int | None + sub = SubConfig + sub2: SubConfig - class ParentConfig(Config): - base_value = 42 - optional: int | None - sub = SubConfig + +def test_config_references(): + """Test using Ref to reference other config values""" cfg = ParentConfig() cfg.sanity_check() # Test self-reference assert cfg.sub.self_ref == 100 + assert cfg.sub2.self_ref == 100 # Test parent reference assert cfg.sub.parent_ref == 42 + assert cfg.sub2.parent_ref == 42 # Verify references update when source changes cfg.base_value = 99 assert cfg.sub.parent_ref == 99 + assert cfg.sub2.parent_ref == 99