From 6074aaa8a6f5cf92c068358f3a24983ed641aad5 Mon Sep 17 00:00:00 2001 From: Levi Naden Date: Mon, 8 Jul 2019 14:57:06 -0400 Subject: [PATCH] Re-enable nested model init calls while still allowing self This commit enables nested model `__init__` statements to be executed while still allowing `self` as an argument. Effectively reverses the changes from #632 while still enabling the feature it implemented. In theory, there will still be a collision if someone ever tried to use `pydantic_base_model/settings_init` as an arg, but I don't know how to engineer a case where a collision would *never* happen, I'm not sure there is one. This commit also added a test for both BaseModel` and `BaseSettings` for both the `self`-as-a-parameter and the nested `__init__` features since `BaseSettings` now has the same issue as `BaseModel` since it invoked an `__init__` with self. I have added a comment under the `__init__` for both `BaseModel` and `BaseSetting` since not having `self` as the first arg is such a rarity within Python that it will likely confuse future developers who encounter it. The actual name of the variable referencing the class itself can be up for debate. --- HISTORY.rst | 5 +++++ pydantic/env_settings.py | 5 +++-- pydantic/main.py | 24 ++++++++++-------------- tests/test_edge_cases.py | 30 +++++++++++++++++++++++++++--- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 57ebaa83323..e719fa8f483 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ History ------- +v0.31 (2019-??-??) +.................. +* nested classes which inherit and change `__init__` are now correctly processed while still allowing `self` as a + parameter, #644 by @lnaden and @dgasmith + v0.30 (2019-07-07) .................. * enforce single quotes in code, #612 by @samuelcolvin diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index c94faefdebd..42964e0e294 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -20,8 +20,9 @@ class BaseSettings(BaseModel): Heroku and any 12 factor app design. """ - def __init__(self, **values: Any) -> None: - super().__init__(**self._build_values(values)) + def __init__(pydantic_base_settings_init, **values: Any) -> None: + # Uses something other than `self` the first arg to allow "self" as a settable attribute + super().__init__(**pydantic_base_settings_init._build_values(values)) def _build_values(self, init_kwargs: Dict[str, Any]) -> Dict[str, Any]: return {**self._build_environ(), **init_kwargs} diff --git a/pydantic/main.py b/pydantic/main.py index 1ef8a7bf7a2..56203336b0b 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -263,13 +263,14 @@ class BaseModel(metaclass=MetaModel): Config = BaseConfig __slots__ = ('__values__', '__fields_set__') - def __init__(self, **data: Any) -> None: + def __init__(pydantic_base_model_init, **data: Any) -> None: + # Uses something other than `self` the first arg to allow "self" as a settable attribute if TYPE_CHECKING: # pragma: no cover - self.__values__: Dict[str, Any] = {} - self.__fields_set__: 'SetStr' = set() - values, fields_set, _ = validate_model(self, data) - object.__setattr__(self, '__values__', values) - object.__setattr__(self, '__fields_set__', fields_set) + pydantic_base_model_init.__values__: Dict[str, Any] = {} + pydantic_base_model_init.__fields_set__: 'SetStr' = set() + values, fields_set, _ = validate_model(pydantic_base_model_init, data) + object.__setattr__(pydantic_base_model_init, '__values__', values) + object.__setattr__(pydantic_base_model_init, '__fields_set__', fields_set) @no_type_check def __getattr__(self, name): @@ -358,12 +359,7 @@ def parse_obj(cls: Type['Model'], obj: Any) -> 'Model': except (TypeError, ValueError) as e: exc = TypeError(f'{cls.__name__} expected dict not {type(obj).__name__}') raise ValidationError([ErrorWrapper(exc, loc='__obj__')]) from e - - m = cls.__new__(cls) - values, fields_set, _ = validate_model(m, obj) - object.__setattr__(m, '__values__', values) - object.__setattr__(m, '__fields_set__', fields_set) - return m + return cls(**obj) @classmethod def parse_raw( @@ -477,14 +473,14 @@ def __get_validators__(cls) -> 'CallableGenerator': @classmethod def validate(cls: Type['Model'], value: Any) -> 'Model': if isinstance(value, dict): - return cls.parse_obj(value) + return cls(**value) elif isinstance(value, cls): return value.copy() elif cls.__config__.orm_mode: return cls.from_orm(value) else: with change_exception(DictError, TypeError, ValueError): - return cls.parse_obj(value) + return cls(**dict(value)) @classmethod def _decompose_class(cls: Type['Model'], obj: Any) -> GetterDict: diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 6336997aa17..af1e3abad98 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -8,6 +8,7 @@ from pydantic import ( BaseConfig, BaseModel, + BaseSettings, Extra, NoneStrBytes, StrBytes, @@ -897,12 +898,35 @@ class Model(BaseModel): } -def test_self_recursive(): - class SubModel(BaseModel): +@pytest.mark.parametrize('model', [BaseModel, BaseSettings]) +def test_self_recursive(model): + class SubModel(model): self: int - class Model(BaseModel): + class Model(model): sm: SubModel m = Model.parse_obj({'sm': {'self': '123'}}) assert m.dict() == {'sm': {'self': 123}} + + +@pytest.mark.parametrize('model', [BaseModel, BaseSettings]) +def test_nested_init(model): + class NestedModel(model): + self: str + modified_number: int = 1 + + def __init__(someinit, **kwargs): + super().__init__(**kwargs) + someinit.modified_number += 1 + + class TopModel(model): + self: str + nest: NestedModel + + m = TopModel.parse_obj(dict(self="Top Model", + nest=dict(self="Nested Model", + modified_number=0))) + assert m.self == "Top Model" + assert m.nest.self == "Nested Model" + assert m.nest.modified_number == 1