Skip to content

Commit ae6f982

Browse files
committed
Re-allow nested inherited inits without breaking self attribute
This commit enables nested model `__init__` statements to be executed while still allowing `self` as an argument. Effectively reverses the changes from pydantic#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.
1 parent b52d877 commit ae6f982

File tree

3 files changed

+40
-19
lines changed

3 files changed

+40
-19
lines changed

pydantic/env_settings.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ class BaseSettings(BaseModel):
2020
Heroku and any 12 factor app design.
2121
"""
2222

23-
def __init__(self, **values: Any) -> None:
24-
super().__init__(**self._build_values(values))
23+
def __init__(pydantic_base_settings_init, **values: Any) -> None:
24+
# Uses something other than `self` the first arg to allow "self" as a settable attribute
25+
super().__init__(**pydantic_base_settings_init._build_values(values))
2526

2627
def _build_values(self, init_kwargs: Dict[str, Any]) -> Dict[str, Any]:
2728
return {**self._build_environ(), **init_kwargs}

pydantic/main.py

+10-14
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,14 @@ class BaseModel(metaclass=MetaModel):
263263
Config = BaseConfig
264264
__slots__ = ('__values__', '__fields_set__')
265265

266-
def __init__(self, **data: Any) -> None:
266+
def __init__(pydantic_base_model_init, **data: Any) -> None:
267+
# Uses something other than `self` the first arg to allow "self" as a settable attribute
267268
if TYPE_CHECKING: # pragma: no cover
268-
self.__values__: Dict[str, Any] = {}
269-
self.__fields_set__: 'SetStr' = set()
270-
values, fields_set, _ = validate_model(self, data)
271-
object.__setattr__(self, '__values__', values)
272-
object.__setattr__(self, '__fields_set__', fields_set)
269+
pydantic_base_model_init.__values__: Dict[str, Any] = {}
270+
pydantic_base_model_init.__fields_set__: 'SetStr' = set()
271+
values, fields_set, _ = validate_model(pydantic_base_model_init, data)
272+
object.__setattr__(pydantic_base_model_init, '__values__', values)
273+
object.__setattr__(pydantic_base_model_init, '__fields_set__', fields_set)
273274

274275
@no_type_check
275276
def __getattr__(self, name):
@@ -358,12 +359,7 @@ def parse_obj(cls: Type['Model'], obj: Any) -> 'Model':
358359
except (TypeError, ValueError) as e:
359360
exc = TypeError(f'{cls.__name__} expected dict not {type(obj).__name__}')
360361
raise ValidationError([ErrorWrapper(exc, loc='__obj__')]) from e
361-
362-
m = cls.__new__(cls)
363-
values, fields_set, _ = validate_model(m, obj)
364-
object.__setattr__(m, '__values__', values)
365-
object.__setattr__(m, '__fields_set__', fields_set)
366-
return m
362+
return cls(**obj)
367363

368364
@classmethod
369365
def parse_raw(
@@ -477,14 +473,14 @@ def __get_validators__(cls) -> 'CallableGenerator':
477473
@classmethod
478474
def validate(cls: Type['Model'], value: Any) -> 'Model':
479475
if isinstance(value, dict):
480-
return cls.parse_obj(value)
476+
return cls(**value)
481477
elif isinstance(value, cls):
482478
return value.copy()
483479
elif cls.__config__.orm_mode:
484480
return cls.from_orm(value)
485481
else:
486482
with change_exception(DictError, TypeError, ValueError):
487-
return cls.parse_obj(value)
483+
return cls(**dict(value))
488484

489485
@classmethod
490486
def _decompose_class(cls: Type['Model'], obj: Any) -> GetterDict:

tests/test_edge_cases.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pydantic import (
99
BaseConfig,
1010
BaseModel,
11+
BaseSettings,
1112
Extra,
1213
NoneStrBytes,
1314
StrBytes,
@@ -897,12 +898,35 @@ class Model(BaseModel):
897898
}
898899

899900

900-
def test_self_recursive():
901-
class SubModel(BaseModel):
901+
@pytest.mark.parametrize('model', [BaseModel, BaseSettings])
902+
def test_self_recursive(model):
903+
class SubModel(model):
902904
self: int
903905

904-
class Model(BaseModel):
906+
class Model(model):
905907
sm: SubModel
906908

907909
m = Model.parse_obj({'sm': {'self': '123'}})
908910
assert m.dict() == {'sm': {'self': 123}}
911+
912+
913+
@pytest.mark.parametrize('model', [BaseModel, BaseSettings])
914+
def test_nested_init(model):
915+
class NestedModel(model):
916+
self: str
917+
modified_number: int = 1
918+
919+
def __init__(someinit, **kwargs):
920+
super().__init__(**kwargs)
921+
someinit.modified_number += 1
922+
923+
class TopModel(model):
924+
self: str
925+
nest: NestedModel
926+
927+
m = TopModel.parse_obj(dict(self="Top Model",
928+
nest=dict(self="Nested Model",
929+
modified_number=0)))
930+
assert m.self == "Top Model"
931+
assert m.nest.self == "Nested Model"
932+
assert m.nest.modified_number == 1

0 commit comments

Comments
 (0)