Skip to content

Commit

Permalink
Re-allow nested inherited inits without breaking self attribute
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Lnaden committed Jul 8, 2019
1 parent b52d877 commit ae6f982
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 19 deletions.
5 changes: 3 additions & 2 deletions pydantic/env_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
24 changes: 10 additions & 14 deletions pydantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 27 additions & 3 deletions tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pydantic import (
BaseConfig,
BaseModel,
BaseSettings,
Extra,
NoneStrBytes,
StrBytes,
Expand Down Expand Up @@ -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

0 comments on commit ae6f982

Please sign in to comment.