-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from MartinGotelli/pydantic_matcher
feat: Add pylint package and matchers
- Loading branch information
Showing
26 changed files
with
613 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,11 @@ | ||
annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0" | ||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" | ||
exceptiongroup==1.2.2 ; python_version >= "3.10" and python_version < "3.11" | ||
iniconfig==2.0.0 ; python_version >= "3.10" and python_version < "4.0" | ||
packaging==24.1 ; python_version >= "3.10" and python_version < "4.0" | ||
pluggy==1.5.0 ; python_version >= "3.10" and python_version < "4.0" | ||
pydantic-core==2.20.1 ; python_version >= "3.10" and python_version < "4.0" | ||
pydantic==2.8.2 ; python_version >= "3.10" and python_version < "4.0" | ||
pytest==8.3.2 ; python_version >= "3.10" and python_version < "4.0" | ||
tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.11" | ||
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
attrs==24.2.0 ; python_version >= "3.10" and python_version < "4.0" | ||
py==1.11.0 ; python_version >= "3.10" and python_version < "4.0" | ||
pydantic==1.10.16 ; python_version >= "3.10" and python_version < "4.0" | ||
pytest==7.0.0 ; python_version >= "3.10" and python_version < "4.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
from typing import Type | ||
|
||
from pydantic import BaseModel | ||
from pydantic.v1 import BaseModel as BaseModelV1 | ||
|
||
from pytest_matchers.pydantic.matchers import PydanticModel | ||
|
||
|
||
def is_pydantic( | ||
model_class: Type[BaseModel | BaseModelV1] | None = None, | ||
*, | ||
strict: bool = None, | ||
exempt_defaults: bool = True, | ||
attributes: dict = None, | ||
**kwargs, | ||
): | ||
model_class = model_class or BaseModel | ||
if strict is None: | ||
strict = bool(attributes) or bool(kwargs) | ||
return PydanticModel( | ||
model_class, | ||
strict=strict, | ||
exempt_defaults=exempt_defaults, | ||
attributes=attributes, | ||
**kwargs, | ||
) | ||
|
||
|
||
def is_pydantic_v1( | ||
model_class: Type[BaseModelV1] | None = None, | ||
*, | ||
strict: bool = None, | ||
exempt_defaults: bool = True, | ||
attributes: dict = None, | ||
**kwargs, | ||
): | ||
return is_pydantic( | ||
model_class or BaseModelV1, | ||
strict=strict, | ||
exempt_defaults=exempt_defaults, | ||
attributes=attributes, | ||
**kwargs, | ||
) |
1 change: 1 addition & 0 deletions
1
src/pytest_matchers/pytest_matchers/pydantic/matchers/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .pydantic_model import PydanticModel |
110 changes: 110 additions & 0 deletions
110
src/pytest_matchers/pytest_matchers/pydantic/matchers/pydantic_model.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from typing import Any, Type | ||
|
||
from pydantic import BaseModel | ||
from pydantic.fields import FieldInfo | ||
from pydantic.v1 import BaseModel as BaseModelV1 | ||
from pydantic.v1.fields import ModelField | ||
|
||
from pytest_matchers.matchers import HasAttribute, Matcher | ||
from pytest_matchers.utils.matcher_utils import is_instance_matcher | ||
from pytest_matchers.utils.repr_utils import concat_reprs | ||
|
||
|
||
def _model_fields(model_class: Type[BaseModel | BaseModelV1]): | ||
if issubclass(model_class, BaseModelV1): | ||
return model_class.__fields__ | ||
return model_class.model_fields | ||
|
||
|
||
def _is_required(field: FieldInfo | ModelField) -> bool: | ||
if isinstance(field, ModelField): | ||
return field.required | ||
return field.is_required() | ||
|
||
|
||
def _default_value(field: FieldInfo | ModelField) -> Any: | ||
if field.default_factory is None: | ||
return field.default | ||
return field.default_factory() | ||
|
||
|
||
def _version(model_class: Type) -> str | None: | ||
is_pydantic = hasattr(model_class, "__pydantic_complete__") or hasattr( | ||
model_class, "__fields__" | ||
) | ||
if is_pydantic: | ||
if hasattr(model_class, "__pydantic_complete__"): | ||
return "v2" | ||
return "v1" | ||
return None | ||
|
||
|
||
class PydanticModel(Matcher): | ||
def __init__( | ||
self, | ||
model_class: Type[BaseModel | BaseModelV1], | ||
*, | ||
strict: bool = True, | ||
exempt_defaults: bool = True, | ||
attributes: dict = None, | ||
**kwargs, | ||
): | ||
super().__init__() | ||
self._model_class = model_class | ||
self._is_instance_matcher = is_instance_matcher(model_class) | ||
self._strict = strict | ||
self._exempt_defaults = exempt_defaults | ||
self._attributes = kwargs | ||
self._attributes.update(attributes or {}) | ||
self._assert_matching_all_attributes() | ||
|
||
def _assert_matching_all_attributes(self): | ||
if not self._strict: | ||
return | ||
fields = _model_fields(self._model_class) | ||
if self._exempt_defaults: | ||
fields = {name: field for name, field in fields.items() if _is_required(field)} | ||
attribute_names = set(self._attributes.keys()) | ||
model_attribute_names = set(fields.keys()) | ||
missing_attributes = model_attribute_names - attribute_names | ||
if missing_attributes: | ||
raise ValueError( | ||
f"Attributes {missing_attributes} are not present in " | ||
f"{self._model_class.__name__}.\n" | ||
"Consider using strict=False" | ||
) | ||
|
||
def _optional_matchers(self, value: Any = None) -> list[Matcher]: | ||
if value is None: | ||
model_class = self._model_class | ||
else: | ||
model_class = type(value) | ||
if self._strict and self._exempt_defaults: | ||
fields = _model_fields(model_class) | ||
return [ | ||
HasAttribute(name, _default_value(field)) | ||
for name, field in fields.items() | ||
if not _is_required(field) and name not in self._attributes | ||
] | ||
return [] | ||
|
||
def _attribute_matchers(self, value: Any = None) -> list[Matcher]: | ||
return [ | ||
HasAttribute(name, value) for name, value in self._attributes.items() | ||
] + self._optional_matchers(value) | ||
|
||
def matches(self, value: Any) -> bool: | ||
return self._matches_instance(value) and all( | ||
matcher == value for matcher in self._attribute_matchers(value) | ||
) | ||
|
||
def _matches_instance(self, value: Any) -> bool: | ||
expected_model_version = _version(self._model_class) | ||
value_version = _version(type(value)) | ||
return expected_model_version == value_version and self._is_instance_matcher == value | ||
|
||
def __repr__(self) -> str: | ||
return concat_reprs( | ||
f"To be a pydantic model instance of {repr(self._model_class.__name__)}", | ||
*self._attribute_matchers(), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.