Skip to content

Commit

Permalink
Merge branch 'master' into f/dataclasses-field
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Feb 24, 2021
2 parents 9032fd1 + c8883e3 commit a5bcea9
Show file tree
Hide file tree
Showing 29 changed files with 711 additions and 325 deletions.
1 change: 1 addition & 0 deletions changes/1416-AlexanderSov.md
@@ -0,0 +1 @@
Added support for 13/19 digits VISA credit cards in `PaymentCardNumber` type
2 changes: 2 additions & 0 deletions changes/1880-rhuille.md
@@ -0,0 +1,2 @@
Add a new `frozen` boolean parameter to `Config` (default: `False`).
Setting `frozen=True` does everything that `allow_mutation=False` does, and also generates a `__hash__()` method for the model. This makes instances of the model potentially hashable if all the attributes are hashable.
1 change: 1 addition & 0 deletions changes/2098-PrettyWood.md
@@ -0,0 +1 @@
Fix mypy complaints about most custom _pydantic_ types
2 changes: 2 additions & 0 deletions changes/2176-thomascobb.md
@@ -0,0 +1,2 @@
Allow `Field` with a `default_factory` to be used as an argument to a function
decorated with `validate_arguments`
1 change: 1 addition & 0 deletions changes/2356-MrMrRobat.md
@@ -0,0 +1 @@
Allow configuring models through class kwargs
11 changes: 11 additions & 0 deletions docs/examples/model_config_class_kwargs.py
@@ -0,0 +1,11 @@
from pydantic import BaseModel, ValidationError, Extra


class Model(BaseModel, extra=Extra.forbid):
a: str


try:
Model(a='spam', b='oh no')
except ValidationError as e:
print(e)
22 changes: 22 additions & 0 deletions docs/examples/validation_decorator_field.py
@@ -0,0 +1,22 @@
from datetime import datetime
from pydantic import validate_arguments, Field, ValidationError
from pydantic.typing import Annotated


@validate_arguments
def how_many(num: Annotated[int, Field(gt=10)]):
return num


try:
how_many(1)
except ValidationError as e:
print(e)


@validate_arguments
def when(dt: datetime = Field(default_factory=datetime.now)):
return dt


print(type(when()))
2 changes: 1 addition & 1 deletion docs/requirements.txt
@@ -1,7 +1,7 @@
ansi2html==1.6.0
flake8==3.8.4
flake8-quotes==3.2.0
hypothesis==5.44.0
hypothesis==6.3.0
mkdocs==1.1.2
mkdocs-exclude==1.0.2
mkdocs-material==6.2.8
Expand Down
15 changes: 14 additions & 1 deletion docs/usage/model_config.md
Expand Up @@ -29,6 +29,14 @@ Options:
**`allow_mutation`**
: whether or not models are faux-immutable, i.e. whether `__setattr__` is allowed (default: `True`)

**`frozen`**

!!! warning
This parameter is in beta

: setting `frozen=True` does everything that `allow_mutation=False` does, and also generates a `__hash__()` method for the model. This makes instances of the model potentially hashable if all the attributes are hashable. (default: `False`)


**`use_enum_values`**
: whether to populate models with the `value` property of enums, rather than the raw enum.
This may be useful if you want to serialise `model.dict()` later (default: `False`)
Expand Down Expand Up @@ -89,8 +97,13 @@ not be included in the model schemas. **Note**: this means that attributes on th
```
_(This script is complete, it should run "as is")_

Similarly, if using the `@dataclass` decorator:
Also, you can specify config options as model class kwargs:
```py
{!.tmp_examples/model_config_class_kwargs.py!}
```
_(This script is complete, it should run "as is")_

Similarly, if using the `@dataclass` decorator:
```py
{!.tmp_examples/model_config_dataclass.py!}
```
Expand Down
18 changes: 15 additions & 3 deletions docs/usage/validation_decorator.md
Expand Up @@ -55,6 +55,18 @@ To demonstrate all the above parameter types:
```
_(This script is complete, it should run "as is")_

## Using Field to describe function arguments

[Field](schema.md#field-customisation) can also be used with `validate_arguments` to provide extra information about
the field and validations. In general it should be used in a type hint with
[Annotated](schema.md#typingannotated-fields), unless `default_factory` is specified, in which case it should be used
as the default value of the field:

```py
{!.tmp_examples/validation_decorator_field.py!}
```
_(This script is complete, it should run "as is")_

## Usage with mypy

The `validate_arguments` decorator should work "out of the box" with [mypy](http://mypy-lang.org/) since it's
Expand Down Expand Up @@ -93,13 +105,13 @@ _(This script is complete, it should run "as is")_

## Custom Config

The model behind `validate_arguments` can be customised using a config setting which is equivalent to
The model behind `validate_arguments` can be customised using a config setting which is equivalent to
setting the `Config` sub-class in normal models.

!!! warning
The `fields` and `alias_generator` properties of `Config` which allow aliases to be configured are not supported
yet with `@validate_arguments`, using them will raise an error.

Configuration is set using the `config` keyword argument to the decorator, it may be either a config class
or a dict of properties which are converted to a class later.

Expand Down Expand Up @@ -154,7 +166,7 @@ in future.
### Config and Validators

`fields` and `alias_generator` on custom [`Config`](model_config.md) are not supported, see [above](#custom-config).

Neither are [validators](validators.md).

### Model fields and reserved arguments
Expand Down
12 changes: 6 additions & 6 deletions pydantic/_hypothesis_plugin.py
Expand Up @@ -76,7 +76,7 @@ def is_valid_email(s: str) -> bool:

# PyObject - dotted names, in this case taken from the math module.
st.register_type_strategy(
pydantic.PyObject,
pydantic.PyObject, # type: ignore[arg-type]
st.sampled_from(
[cast(pydantic.PyObject, f'math.{name}') for name in sorted(vars(math)) if not name.startswith('_')]
),
Expand Down Expand Up @@ -152,18 +152,18 @@ def add_luhn_digit(card_number: str) -> str:
st.register_type_strategy(pydantic.IPvAnyAddress, st.ip_addresses())
st.register_type_strategy(
pydantic.IPvAnyInterface,
st.from_type(ipaddress.IPv4Interface) | st.from_type(ipaddress.IPv6Interface),
st.from_type(ipaddress.IPv4Interface) | st.from_type(ipaddress.IPv6Interface), # type: ignore[arg-type]
)
st.register_type_strategy(
pydantic.IPvAnyNetwork,
st.from_type(ipaddress.IPv4Network) | st.from_type(ipaddress.IPv6Network),
st.from_type(ipaddress.IPv4Network) | st.from_type(ipaddress.IPv6Network), # type: ignore[arg-type]
)

# We hook into the con***() functions and the ConstrainedNumberMeta metaclass,
# so here we only have to register subclasses for other constrained types which
# don't go via those mechanisms. Then there are the registration hooks below.
st.register_type_strategy(pydantic.StrictBool, st.booleans())
st.register_type_strategy(pydantic.StrictStr, st.text()) # type: ignore[arg-type]
st.register_type_strategy(pydantic.StrictStr, st.text())


# Constrained-type resolver functions
Expand Down Expand Up @@ -212,7 +212,6 @@ def inner(f): # type: ignore
# Type-to-strategy resolver functions


@resolves(pydantic.Json)
@resolves(pydantic.JsonWrapper)
def resolve_json(cls): # type: ignore[no-untyped-def]
try:
Expand All @@ -221,7 +220,7 @@ def resolve_json(cls): # type: ignore[no-untyped-def]
finite = st.floats(allow_infinity=False, allow_nan=False)
inner = st.recursive(
base=st.one_of(st.none(), st.booleans(), st.integers(), finite, st.text()),
extend=lambda x: st.lists(x) | st.dictionaries(st.text(), x),
extend=lambda x: st.lists(x) | st.dictionaries(st.text(), x), # type: ignore
)
return st.builds(
json.dumps,
Expand Down Expand Up @@ -347,3 +346,4 @@ def resolve_constr(cls): # type: ignore[no-untyped-def] # pragma: no cover
for typ in pydantic.types._DEFINED_TYPES:
_registered(typ)
pydantic.types._registered = _registered
st.register_type_strategy(pydantic.Json, resolve_json)
2 changes: 1 addition & 1 deletion pydantic/decorator.py
Expand Up @@ -184,7 +184,7 @@ def build_values(self, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Dict[st
return values

def execute(self, m: BaseModel) -> Any:
d = {k: v for k, v in m._iter() if k in m.__fields_set__}
d = {k: v for k, v in m._iter() if k in m.__fields_set__ or m.__fields__[k].default_factory}
var_kwargs = d.pop(self.v_kwargs_name, {})

if self.v_args_name in d:
Expand Down
36 changes: 24 additions & 12 deletions pydantic/main.py
Expand Up @@ -120,6 +120,7 @@ class BaseConfig:
validate_all = False
extra = Extra.ignore
allow_mutation = True
frozen = False
allow_population_by_field_name = False
use_enum_values = False
fields: Dict[str, Union[str, Dict[str, str]]] = {}
Expand Down Expand Up @@ -168,18 +169,18 @@ def prepare_field(cls, field: 'ModelField') -> None:
pass


def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType') -> 'ConfigType':
namespace = {}
def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType', **namespace: Any) -> 'ConfigType':
if not self_config:
base_classes = (parent_config,)
base_classes: Tuple['ConfigType', ...] = (parent_config,)
elif self_config == parent_config:
base_classes = (self_config,)
else:
base_classes = self_config, parent_config # type: ignore
namespace['json_encoders'] = {
**getattr(parent_config, 'json_encoders', {}),
**getattr(self_config, 'json_encoders', {}),
}
base_classes = self_config, parent_config

namespace['json_encoders'] = {
**getattr(parent_config, 'json_encoders', {}),
**getattr(self_config, 'json_encoders', {}),
}

return type('Config', base_classes, namespace)

Expand Down Expand Up @@ -215,12 +216,17 @@ def validate_custom_root_type(fields: Dict[str, ModelField]) -> None:
raise ValueError(f'{ROOT_KEY} cannot be mixed with other fields')


# Annotated fields can have many types like `str`, `int`, `List[str]`, `Callable`...
def generate_hash_function(frozen: bool) -> Optional[Callable[[Any], int]]:
def hash_function(self_: Any) -> int:
return hash(self_.__class__) + hash(tuple(self_.__dict__.values()))

return hash_function if frozen else None


# If a field is of type `Callable`, its default value should be a function and cannot to ignored.
ANNOTATED_FIELD_UNTOUCHED_TYPES: Tuple[Any, ...] = (property, type, classmethod, staticmethod)
# When creating a `BaseModel` instance, we bypass all the methods, properties... added to the model
UNTOUCHED_TYPES: Tuple[Any, ...] = (FunctionType,) + ANNOTATED_FIELD_UNTOUCHED_TYPES

# Note `ModelMetaclass` refers to `BaseModel`, but is also used to *create* `BaseModel`, so we need to add this extra
# (somewhat hacky) boolean to keep track of whether we've created the `BaseModel` class yet, and therefore whether it's
# safe to refer to it. If it *hasn't* been created, we assume that the `__new__` call we're in the middle of is for
Expand Down Expand Up @@ -251,7 +257,12 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
private_attributes.update(base.__private_attributes__)
class_vars.update(base.__class_vars__)

config = inherit_config(namespace.get('Config'), config)
config_kwargs = {key: kwargs.pop(key) for key in kwargs.keys() & BaseConfig.__dict__.keys()}
config_from_namespace = namespace.get('Config')
if config_kwargs and config_from_namespace:
raise TypeError('Specifying config in two places is ambiguous, use either Config attribute or class kwargs')
config = inherit_config(config_from_namespace, config, **config_kwargs)

validators = inherit_validators(extract_validators(namespace), validators)
vg = ValidatorGroup(validators)

Expand Down Expand Up @@ -348,6 +359,7 @@ def is_untouched(v: Any) -> bool:
'__custom_root_type__': _custom_root_type,
'__private_attributes__': private_attributes,
'__slots__': slots | private_attributes.keys(),
'__hash__': generate_hash_function(config.frozen),
'__class_vars__': class_vars,
**{n: v for n, v in namespace.items() if n not in exclude_from_namespace},
}
Expand Down Expand Up @@ -408,7 +420,7 @@ def __setattr__(self, name, value): # noqa: C901 (ignore complexity)

if self.__config__.extra is not Extra.allow and name not in self.__fields__:
raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"')
elif not self.__config__.allow_mutation:
elif not self.__config__.allow_mutation or self.__config__.frozen:
raise TypeError(f'"{self.__class__.__name__}" is immutable and does not support item assignment')
elif self.__config__.validate_assignment:
new_values = {**self.__dict__, name: value}
Expand Down
7 changes: 5 additions & 2 deletions pydantic/mypy.py
Expand Up @@ -143,6 +143,7 @@ class PydanticModelTransformer:
tracked_config_fields: Set[str] = {
'extra',
'allow_mutation',
'frozen',
'orm_mode',
'allow_population_by_field_name',
'alias_generator',
Expand All @@ -159,7 +160,7 @@ def transform(self) -> None:
In particular:
* determines the model config and fields,
* adds a fields-aware signature for the initializer and construct methods
* freezes the class if allow_mutation = False
* freezes the class if allow_mutation = False or frozen = True
* stores the fields, config, and if the class is settings in the mypy metadata for access by subclasses
"""
ctx = self._ctx
Expand All @@ -174,7 +175,7 @@ def transform(self) -> None:
is_settings = any(get_fullname(base) == BASESETTINGS_FULLNAME for base in info.mro[:-1])
self.add_initializer(fields, config, is_settings)
self.add_construct_method(fields)
self.set_frozen(fields, frozen=config.allow_mutation is False)
self.set_frozen(fields, frozen=config.allow_mutation is False or config.frozen is True)
info.metadata[METADATA_KEY] = {
'fields': {field.name: field.serialize() for field in fields},
'config': config.set_values_dict(),
Expand Down Expand Up @@ -529,12 +530,14 @@ def __init__(
self,
forbid_extra: Optional[bool] = None,
allow_mutation: Optional[bool] = None,
frozen: Optional[bool] = None,
orm_mode: Optional[bool] = None,
allow_population_by_field_name: Optional[bool] = None,
has_alias_generator: Optional[bool] = None,
):
self.forbid_extra = forbid_extra
self.allow_mutation = allow_mutation
self.frozen = frozen
self.orm_mode = orm_mode
self.allow_population_by_field_name = allow_population_by_field_name
self.has_alias_generator = has_alias_generator
Expand Down

0 comments on commit a5bcea9

Please sign in to comment.