diff --git a/exca/base.py b/exca/base.py index 121c4bdd..bcbf0c12 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..37b1f3c8 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("Here 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 07367c7c..c74e8ec4 100644 --- a/exca/utils.py +++ b/exca/utils.py @@ -363,6 +363,29 @@ def find_models( return out +_CHECKED: list[tp.Type[pydantic.BaseModel]] = [] + + +def check_extra_forbid(cls: tp.Type[pydantic.BaseModel]) -> None: + if cls in _CHECKED: + 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) + cfg["extra"] = "forbid" + for val in cls.model_fields.values(): + for annot in _pydantic_hints(val.annotation): + 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""" try: