Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: URL constraints and strict pydantic v2 values #241

Merged
merged 4 commits into from
Jun 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from sphinx.addnodes import document
from sphinx.application import Sphinx

PY_CLASS = "py:class"
PY_RE = r"py:.*"

project = "Polyfactory"
copyright = "2023, Litestar Org"
Expand Down Expand Up @@ -63,27 +65,28 @@

nitpicky = True
nitpick_ignore = [
("py:class", "BaseModel"),
("py:class", "Decimal"),
("py:class", "Faker"),
("py:class", "FieldInfo"),
("py:class", "Random"),
("py:class", "Scope"),
("py:class", "T"),
("py:class", "P"),
("py:class", "P.args"),
("py:class", "P.kwargs"),
("py:class", "Self"),
("py:class", "TypeGuard"),
("py:class", "date"),
(PY_CLASS, "BaseModel"),
(PY_CLASS, "Decimal"),
(PY_CLASS, "Faker"),
(PY_CLASS, "FieldInfo"),
(PY_CLASS, "Random"),
(PY_CLASS, "Scope"),
(PY_CLASS, "T"),
(PY_CLASS, "P"),
(PY_CLASS, "P.args"),
(PY_CLASS, "P.kwargs"),
(PY_CLASS, "Self"),
(PY_CLASS, "TypeGuard"),
(PY_CLASS, "date"),
(PY_CLASS, "tzinfo"),
]
nitpick_ignore_regex = [
(r"py:.*", r"typing_extensions.*"),
(r"py:.*", r"polyfactory.*\.T"),
(r"py:.*", r"polyfactory.*\.P"),
(r"py:.*", r".*TypedDictT"),
(r"py:.*", r"pydantic.*"),
(r"py:.*", r"msgspec.*"),
(PY_RE, r"typing_extensions.*"),
(PY_RE, r"polyfactory.*\.T"),
(PY_RE, r"polyfactory.*\.P"),
(PY_RE, r".*TypedDictT"),
(PY_RE, r"pydantic.*"),
(PY_RE, r"msgspec.*"),
]

html_theme_options = {
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion polyfactory/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

from collections import abc, defaultdict, deque
from random import Random
from typing import (
Any,
DefaultDict,
Deque,
Dict,
Expand Down Expand Up @@ -39,6 +42,6 @@
UnionType: Union,
}

IGNORED_TYPE_ARGS: Set = {Ellipsis}
IGNORED_TYPE_ARGS: set[Any] = {Ellipsis}

DEFAULT_RANDOM = Random()
18 changes: 9 additions & 9 deletions polyfactory/factories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
from polyfactory.persistence import AsyncPersistenceProtocol, SyncPersistenceProtocol


def _create_pydantic_type_map(cls: "type[BaseFactory]") -> dict[type, Callable[[], Any]]:
def _create_pydantic_type_map(cls: type[BaseFactory[Any]]) -> dict[type, Callable[[], Any]]:
"""Creates a mapping of pydantic types to mock data functions.

:param cls: The base factory class.
Expand Down Expand Up @@ -169,7 +169,7 @@ def _create_pydantic_type_map(cls: "type[BaseFactory]") -> dict[type, Callable[[
T = TypeVar("T")


def is_factory(value: Any) -> "TypeGuard[type[BaseFactory]]":
def is_factory(value: Any) -> "TypeGuard[type[BaseFactory[Any]]]":
"""Determine if a given value is a subclass of ModelFactory.

:param value: An arbitrary value.
Expand Down Expand Up @@ -207,7 +207,7 @@ class BaseFactory(ABC, Generic[T]):
Flag dictating whether the factory is a 'base' factory. Base factories are registered globally as handlers for types.
For example, the 'DataclassFactory', 'TypedDictFactory' and 'ModelFactory' are all base factories.
"""
__base_factory_overrides__: dict[Any, type[BaseFactory]] | None = None
__base_factory_overrides__: dict[Any, type[BaseFactory[Any]]] | None = None
"""
A base factory to override with this factory. If this value is set, the given factory will replace the given base factory.

Expand All @@ -230,8 +230,8 @@ class BaseFactory(ABC, Generic[T]):
# cached attributes
_fields_metadata: list[FieldMeta]
# BaseFactory only attributes
_factory_type_mapping: ClassVar[dict[Any, type[BaseFactory]]]
_base_factories: ClassVar[list[type[BaseFactory]]]
_factory_type_mapping: ClassVar[dict[Any, type[BaseFactory[Any]]]]
_base_factories: ClassVar[list[type[BaseFactory[Any]]]]

def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
super().__init_subclass__(*args, **kwargs)
Expand Down Expand Up @@ -327,7 +327,7 @@ def _handle_factory_field(cls, field_value: Any, field_build_parameters: Any | N
def _get_or_create_factory(
cls,
model: type,
) -> type[BaseFactory]:
) -> type[BaseFactory[Any]]:
"""Get a factory from registered factories or generate a factory dynamically.

:param model: A model type.
Expand Down Expand Up @@ -513,9 +513,9 @@ def get_mock_value(cls, annotation: type) -> Any:
def create_factory(
cls,
model: type,
bases: tuple[type[BaseFactory], ...] | None = None,
bases: tuple[type[BaseFactory[Any]], ...] | None = None,
**kwargs: Any,
) -> type[BaseFactory]:
) -> type[BaseFactory[Any]]:
"""Generate a factory for the given type dynamically.

:param model: A type to model.
Expand All @@ -526,7 +526,7 @@ def create_factory(

"""
return cast(
"Type[BaseFactory]",
"Type[BaseFactory[Any]]",
type(
f"{model.__name__}Factory",
(*(bases or ()), cls),
Expand Down
19 changes: 9 additions & 10 deletions polyfactory/factories/pydantic_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,6 @@ def from_field_info(
# pydantic uses a sentinel value for url constraints
annotation = str

constraints = {
**constraints, # type: ignore[misc]
"constant": None,
"unique_items": None,
"upper_case": None,
"lower_case": None,
"item_type": None,
}

return PydanticFieldMeta.from_type(
name=name,
random=random,
Expand Down Expand Up @@ -174,7 +165,15 @@ def from_model_field(cls, model_field: ModelField, use_alias: bool) -> PydanticF
)

# pydantic v1 has constraints set for these values, but we generate them using faker
if unwrap_optional(annotation) in (AnyUrl, HttpUrl, KafkaDsn, PostgresDsn, RedisDsn, AmqpDsn, AnyHttpUrl):
if pydantic_version == 1 and unwrap_optional(annotation) in (
AnyUrl,
HttpUrl,
KafkaDsn,
PostgresDsn,
RedisDsn,
AmqpDsn,
AnyHttpUrl,
):
constraints = {}

children: list[FieldMeta] = []
Expand Down
30 changes: 7 additions & 23 deletions polyfactory/field_meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from dataclasses import asdict, is_dataclass
from typing import TYPE_CHECKING, Any, Literal, Pattern, TypedDict, cast

from polyfactory.constants import DEFAULT_RANDOM, IGNORED_TYPE_ARGS, TYPE_MAPPING
Expand Down Expand Up @@ -45,7 +46,6 @@ class Constraints(TypedDict):
multiple_of: NotRequired[int | float | Decimal]
path_type: NotRequired[Literal["file", "dir", "new"]]
pattern: NotRequired[str | Pattern]
strict: NotRequired[bool]
tz: NotRequired[datetime.tzinfo]
unique_items: NotRequired[bool]
upper_case: NotRequired[bool]
Expand Down Expand Up @@ -74,7 +74,7 @@ def __init__(
default: Any = Null,
children: list[FieldMeta] | None = None,
constraints: Constraints | None = None,
):
) -> None:
"""Create a factory field metadata instance."""
self.annotation = annotation
self.random = random
Expand Down Expand Up @@ -143,29 +143,14 @@ def parse_constraints(cls, metadata: list[Any]) -> "Constraints": # pragma: no
elif func := getattr(value, "func", None):
if func is str.islower:
constraints.update({"lower_case": True})
if func is str.isupper:
elif func is str.isupper:
constraints.update({"upper_case": True})
if func is str.isascii:
elif func is str.isascii:
constraints.update({"pattern": "[[:ascii:]]"})
if func is str.isdigit:
elif func is str.isdigit:
constraints.update({"pattern": "[[:digit:]]"})
elif allowed_schemas := getattr(value, "allowed_schemas", None):
constraints.update(
{
"url": {
k: v
for k, v in {
"max_length": getattr(value, "max_length", None),
"allowed_schemes": allowed_schemas,
"host_required": getattr(value, "host_required", None),
"default_host": getattr(value, "default_host", None),
"default_port": getattr(value, "default_port", None),
"default_path": getattr(value, "default_path", None),
}.items()
if v is not None
}
}
)
elif is_dataclass(value) and (value_dict := asdict(value)) and ("allowed_schemes" in value_dict):
constraints.update({"url": {k: v for k, v in value_dict.items() if v is not None}})
else:
constraints.update(
{
Expand All @@ -186,7 +171,6 @@ def parse_constraints(cls, metadata: list[Any]) -> "Constraints": # pragma: no
"multiple_of": getattr(value, "multiple_of", None),
"path_type": getattr(value, "path_type", None),
"pattern": getattr(value, "regex", getattr(value, "pattern", None)),
"strict": getattr(value, "strict", None),
"tz": getattr(value, "tz", None),
"unique_items": getattr(value, "unique_items", None),
"upper_case": getattr(value, "to_upper", None),
Expand Down
6 changes: 3 additions & 3 deletions polyfactory/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def to_value(self, name: str, values: dict[str, Any]) -> Any:
class Fixture:
"""Factory field to create a pytest fixture from a factory."""

__slots__ = ("fixture", "size", "kwargs")
__slots__ = ("ref", "size", "kwargs")

def __init__(self, fixture: Callable, size: int | None = None, **kwargs: Any) -> None:
"""Create a fixture from a factory.
Expand All @@ -94,7 +94,7 @@ def __init__(self, fixture: Callable, size: int | None = None, **kwargs: Any) ->
:param size: Optional batch size.
:param kwargs: Any build kwargs.
"""
self.fixture: WrappedCallable = {"value": fixture}
self.ref: WrappedCallable = {"value": fixture}
self.size = size
self.kwargs = kwargs

Expand All @@ -107,7 +107,7 @@ def to_value(self) -> Any:
"""
from polyfactory.pytest_plugin import FactoryFixture

if factory := FactoryFixture.factory_class_map.get(self.fixture["value"]):
if factory := FactoryFixture.factory_class_map.get(self.ref["value"]):
if self.size:
return factory.batch(self.size, **self.kwargs)
return factory.build(**self.kwargs)
Expand Down
10 changes: 5 additions & 5 deletions polyfactory/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ class FactoryFixture:

__slots__ = ("scope", "autouse", "name")

factory_class_map: ClassVar[dict[Callable, type[BaseFactory]]] = {}
factory_class_map: ClassVar[dict[Callable, type[BaseFactory[Any]]]] = {}

def __init__(
self,
scope: Scope = "function",
autouse: bool = False,
name: str | None = None,
):
) -> None:
"""Create a factory fixture decorator

:param scope: Fixture scope
Expand All @@ -67,7 +67,7 @@ def __init__(
self.autouse = autouse
self.name = name

def __call__(self, factory: type[BaseFactory]) -> Any:
def __call__(self, factory: type[BaseFactory[Any]]) -> Any:
from polyfactory.factories.base import is_factory

if not is_factory(factory):
Expand All @@ -76,7 +76,7 @@ def __call__(self, factory: type[BaseFactory]) -> Any:
fixture_name = self.name or _get_fixture_name(factory.__name__)
fixture_register = fixture(scope=self.scope, name=fixture_name, autouse=self.autouse) # pyright: ignore

def _factory_fixture() -> type[BaseFactory]:
def _factory_fixture() -> type[BaseFactory[Any]]:
"""The wrapped factory"""
return factory

Expand All @@ -87,7 +87,7 @@ def _factory_fixture() -> type[BaseFactory]:


def register_fixture(
factory: type[BaseFactory] | None = None,
factory: type[BaseFactory[Any]] | None = None,
*,
scope: Scope = "function",
autouse: bool = False,
Expand Down
4 changes: 2 additions & 2 deletions polyfactory/value_generators/complex_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from polyfactory.field_meta import FieldMeta


def handle_collection_type(field_meta: FieldMeta, container_type: type, factory: type[BaseFactory]) -> Any:
def handle_collection_type(field_meta: FieldMeta, container_type: type, factory: type[BaseFactory[Any]]) -> Any:
"""Handle generation of container types recursively.

:param container_type: A type that can accept type arguments.
Expand Down Expand Up @@ -58,7 +58,7 @@ def handle_collection_type(field_meta: FieldMeta, container_type: type, factory:
return container


def handle_complex_type(field_meta: FieldMeta, factory: type[BaseFactory]) -> Any:
def handle_complex_type(field_meta: FieldMeta, factory: type[BaseFactory[Any]]) -> Any:
"""Recursive type generation based on typing info stored in the graph like structure
of pydantic field_metas.

Expand Down
2 changes: 1 addition & 1 deletion polyfactory/value_generators/constrained_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

def handle_constrained_collection(
collection_type: Callable[..., T],
factory: type[BaseFactory],
factory: type[BaseFactory[Any]],
field_meta: FieldMeta,
item_type: Any,
max_items: int | None = None,
Expand Down
4 changes: 2 additions & 2 deletions polyfactory/value_generators/regex.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _handle_repeat(self, start_range: int, end_range: Any, value: SubPattern) ->
result: list[str] = []
end_range = min(end_range, self._limit)

for i in range(self._random.randint(start_range, max(start_range, end_range))):
result.append("".join(self._handle_state(i) for i in list(value))) # pyright:ignore
for _ in range(self._random.randint(start_range, max(start_range, end_range))):
result.append("".join(self._handle_state(v) for v in list(value))) # pyright:ignore

return "".join(result)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ pydantic = "*"
pytest = "*"
pytest-asyncio = "*"
pytest-cov = "*"
annotated-types = ">=0.5.0"
annotated-types = "*"

[tool.poetry.group.docs.dependencies]
sphinx-autobuild = "*"
Expand Down
1 change: 1 addition & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ sonar.projectKey=starlite-api_polyfactory
sonar.organization=litestar-api
sonar.python.coverage.reportPaths=coverage.xml
sonar.tests=tests
sonar.exclusions=docs/**/*, tests/**/*
sonar.sources=polyfactory
sonar.sourceEncoding=UTF-8
sonar.python.version=3.8, 3.9, 3.10, 3.11
1 change: 1 addition & 0 deletions tests/test_base_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class FooDataclassFactory(DataclassFactory):
def get_provider_map(cls) -> Dict[Any, Any]:
return {Foo: lambda: Foo("foo"), **super().get_provider_map()}

# noinspection PyUnusedLocal
class DummyDataclassFactory(DataclassFactory):
__is_base_factory__ = True

Expand Down
Loading