From 7114beb0983ec79a84a94cc953448a2039cc8f01 Mon Sep 17 00:00:00 2001 From: Jeremy Rapin Date: Thu, 2 Jan 2025 13:44:56 +0100 Subject: [PATCH 1/2] [WIP] Experiment with validation of subfields --- exca/base.py | 5 ++++- exca/test_base.py | 17 +++++++++++++++++ exca/utils.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/exca/base.py b/exca/base.py index c4d5031e..123ca475 100644 --- a/exca/base.py +++ b/exca/base.py @@ -44,6 +44,7 @@ def _add_name( obj: pydantic.BaseModel, propagate_defaults: bool = False ) -> pydantic.BaseModel: """Provide owner object to the infra""" + private = obj.__pydantic_private__ or {} params = collections.ChainMap(dict(obj), private) for name, val in params.items(): @@ -84,10 +85,11 @@ def _add_name( @pydantic.model_validator(mode="before") -def model_with_infra_validator_before(obj: tp.Any) -> tp.Any: +def model_with_infra_validator_before(cls, obj: tp.Any) -> tp.Any: """Provide owner object to the infra (this is set to the owner class during __set_name__) """ + utils.check_extra_forbid(cls) if not isinstance(obj, dict): return obj # should not happen for name, val in obj.items(): @@ -149,6 +151,7 @@ def __set_name__(self, owner: tp.Type[pydantic.BaseModel], name: str) -> None: msg += f"{cls} must inherit from pydantic.BaseModel" raise RuntimeError(msg) owner.model_config.setdefault("extra", "forbid") + owner.model_config.setdefault("revalidate_instances", "always") self._infra_name = name # set mechanism to provide owner obj to the infra: owner._model_with_infra_validator_after = model_with_infra_validator_after # type: ignore diff --git a/exca/test_base.py b/exca/test_base.py index 2477f946..596d130c 100644 --- a/exca/test_base.py +++ b/exca/test_base.py @@ -406,6 +406,23 @@ def test_weird_types(tmp_path: Path) -> None: whatever.build() +def test_recursive_model() -> None: + class SubData(pydantic.BaseModel): + x: int = 12 + + class Recursive(pydantic.BaseModel): + submodels: list["Recursive"] = [] + subd: SubData = SubData() + infra: TaskInfra = TaskInfra() + + r = Recursive(submodels=[{}, {}], subd={"y": 3}) # type: ignore + print("Cfg", SubData.model_config) + r = Recursive(submodels=[{}, {}], subd={"y": 3}) # type: ignore + r.subd.y = 13 + print(r.subd.y) + raise + + def test_defined_in_main() -> None: try: import neuralset as ns diff --git a/exca/utils.py b/exca/utils.py index a35c0f5b..c7c53385 100644 --- a/exca/utils.py +++ b/exca/utils.py @@ -342,8 +342,38 @@ def find_models( return out +_CHECKED: list[tp.Type[pydantic.BaseModel]] = [] + + +def check_extra_forbid(cls: tp.Type[pydantic.BaseModel]) -> None: + print("Checking", cls) + if cls in _CHECKED: + print("Bypassing", cls) + return + _CHECKED.append(cls) + cfg = cls.model_config + if "extra" not in cfg: + msg = f"Automatically setting extra='forbid' for {cls.__name__} " + msg += "(bypass by explicitely setting: model_config = pydantic.ConfigDict(extra='forbid'))" + logging.debug(msg) + print(f"setting extra to {cls}") + cfg["extra"] = "forbid" + cls.model_config = cfg + print(cls.model_config) + print(cls.model_fields) + for val in cls.model_fields.values(): + # print(f"Here is {val}") + for annot in _pydantic_hints(val.annotation): + print("SubChecking", annot) + try: + check_extra_forbid(annot) + except Exception as e: + raise ValueError(f"Failing for {val.annotation} ({annot=})") from e + + def _pydantic_hints(hint: tp.Any) -> tp.List[tp.Type[pydantic.BaseModel]]: """Checks if a type hint contains pydantic models""" + print("checking sub") try: if issubclass(hint, pydantic.BaseModel): return [hint] From b47b4e4dac2530ea2832d4066cd39af8043cfd01 Mon Sep 17 00:00:00 2001 From: Jeremy Rapin Date: Wed, 2 Apr 2025 14:03:10 +0200 Subject: [PATCH 2/2] better --- exca/test_base.py | 6 +++--- exca/utils.py | 11 ++--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/exca/test_base.py b/exca/test_base.py index 596d130c..37b1f3c8 100644 --- a/exca/test_base.py +++ b/exca/test_base.py @@ -416,10 +416,10 @@ class Recursive(pydantic.BaseModel): infra: TaskInfra = TaskInfra() r = Recursive(submodels=[{}, {}], subd={"y": 3}) # type: ignore - print("Cfg", SubData.model_config) + print("Here Cfg", SubData.model_config) r = Recursive(submodels=[{}, {}], subd={"y": 3}) # type: ignore - r.subd.y = 13 - print(r.subd.y) + # r.subd.y = 13 + # print(r.subd.y) raise diff --git a/exca/utils.py b/exca/utils.py index 8f128d84..c74e8ec4 100644 --- a/exca/utils.py +++ b/exca/utils.py @@ -367,9 +367,7 @@ def find_models( def check_extra_forbid(cls: tp.Type[pydantic.BaseModel]) -> None: - print("Checking", cls) if cls in _CHECKED: - print("Bypassing", cls) return _CHECKED.append(cls) cfg = cls.model_config @@ -377,24 +375,19 @@ def check_extra_forbid(cls: tp.Type[pydantic.BaseModel]) -> None: msg = f"Automatically setting extra='forbid' for {cls.__name__} " msg += "(bypass by explicitely setting: model_config = pydantic.ConfigDict(extra='forbid'))" logging.debug(msg) - print(f"setting extra to {cls}") cfg["extra"] = "forbid" - cls.model_config = cfg - print(cls.model_config) - print(cls.model_fields) for val in cls.model_fields.values(): - # print(f"Here is {val}") for annot in _pydantic_hints(val.annotation): - print("SubChecking", annot) try: check_extra_forbid(annot) except Exception as e: raise ValueError(f"Failing for {val.annotation} ({annot=})") from e + # rebuilding only after subclasses were rebuilt + cls.model_rebuild(force=True) def _pydantic_hints(hint: tp.Any) -> tp.List[tp.Type[pydantic.BaseModel]]: """Checks if a type hint contains pydantic models""" - print("checking sub") try: if issubclass(hint, pydantic.BaseModel): return [hint]