Source code for polyfactory.field_meta

from __future__ import annotations

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

from typing_extensions import get_args, get_origin

from polyfactory.constants import DEFAULT_RANDOM, TYPE_MAPPING
from polyfactory.utils.deprecation import check_for_deprecated_parameters
from polyfactory.utils.helpers import get_annotation_metadata, unwrap_annotated, unwrap_new_type
from polyfactory.utils.predicates import is_annotated
from polyfactory.utils.types import NoneType

if TYPE_CHECKING:
    import datetime
    from decimal import Decimal
    from random import Random
    from typing import Sequence

    from typing_extensions import NotRequired, Self


[docs]class Null: """Sentinel class for empty values"""
[docs]class UrlConstraints(TypedDict): max_length: NotRequired[int] allowed_schemes: NotRequired[list[str]] host_required: NotRequired[bool] default_host: NotRequired[str] default_port: NotRequired[int] default_path: NotRequired[str]
[docs]class Constraints(TypedDict): """Metadata regarding a type constraints, if any""" allow_inf_nan: NotRequired[bool] decimal_places: NotRequired[int] ge: NotRequired[int | float | Decimal] gt: NotRequired[int | float | Decimal] item_type: NotRequired[Any] le: NotRequired[int | float | Decimal] lower_case: NotRequired[bool] lt: NotRequired[int | float | Decimal] max_digits: NotRequired[int] max_length: NotRequired[int] min_length: NotRequired[int] multiple_of: NotRequired[int | float | Decimal] path_type: NotRequired[Literal["file", "dir", "new"]] pattern: NotRequired[str | Pattern] tz: NotRequired[datetime.tzinfo] unique_items: NotRequired[bool] upper_case: NotRequired[bool] url: NotRequired[UrlConstraints] uuid_version: NotRequired[Literal[1, 3, 4, 5]]
[docs]class FieldMeta: """Factory field metadata container. This class is used to store the data about a field of a factory's model.""" __slots__ = ("annotation", "children", "constraints", "default", "name", "random") annotation: Any random: Random children: list[FieldMeta] | None default: Any name: str constraints: Constraints | None
[docs] def __init__( self, *, name: str, annotation: type, random: Random | None = None, 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 or DEFAULT_RANDOM self.children = children self.default = default self.name = name self.constraints = constraints
@property def type_args(self) -> tuple[Any, ...]: """Return the normalized type args of the annotation, if any. :returns: a tuple of types. """ return tuple(TYPE_MAPPING.get(arg, arg) for arg in get_args(self.annotation))
[docs] @classmethod def from_type( cls, annotation: Any, random: Random = DEFAULT_RANDOM, name: str = "", default: Any = Null, constraints: Constraints | None = None, randomize_collection_length: bool | None = None, min_collection_length: int | None = None, max_collection_length: int | None = None, children: list[FieldMeta] | None = None, ) -> Self: """Builder method to create a FieldMeta from a type annotation. :param annotation: A type annotation. :param random: An instance of random.Random. :param name: Field name :param default: Default value, if any. :param constraints: A dictionary of constraints, if any. :param randomize_collection_length: A boolean flag whether to randomize collections lengths :param min_collection_length: Minimum number of elements in randomized collection :param max_collection_length: Maximum number of elements in randomized collection :returns: A field meta instance. """ check_for_deprecated_parameters( "2.11.0", parameters=( ("randomize_collection_length", randomize_collection_length), ("min_collection_length", min_collection_length), ("max_collection_length", max_collection_length), ), ) annotated = is_annotated(annotation) if not constraints and annotated: metadata = cls.get_constraints_metadata(annotation) constraints = cls.parse_constraints(metadata) if annotated: annotation = get_args(annotation)[0] elif (origin := get_origin(annotation)) and origin in TYPE_MAPPING: # pragma: no cover container = TYPE_MAPPING[origin] annotation = container[get_args(annotation)] # type: ignore[index] field = cls( annotation=annotation, random=random, name=name, default=default, children=children, constraints=constraints, ) if field.type_args and not field.children: field.children = [ cls.from_type( annotation=unwrap_new_type(arg), random=random, ) for arg in field.type_args if arg is not NoneType ] return field
@classmethod def parse_constraints(cls, metadata: Sequence[Any]) -> "Constraints": constraints = {} for value in metadata: if is_annotated(value): _, inner_metadata = unwrap_annotated(value, random=DEFAULT_RANDOM) constraints.update(cast("dict[str, Any]", cls.parse_constraints(metadata=inner_metadata))) elif func := getattr(value, "func", None): if func is str.islower: constraints["lower_case"] = True elif func is str.isupper: constraints["upper_case"] = True elif func is str.isascii: constraints["pattern"] = "[[:ascii:]]" elif func is str.isdigit: constraints["pattern"] = "[[:digit:]]" elif is_dataclass(value) and (value_dict := asdict(value)) and ("allowed_schemes" in value_dict): # type: ignore[arg-type] constraints["url"] = {k: v for k, v in value_dict.items() if v is not None} # This is to support `Constraints`, but we can't do a isinstance with `Constraints` since isinstance # checks with `TypedDict` is not supported. elif isinstance(value, Mapping): constraints.update(value) else: constraints.update( { k: v for k, v in { "allow_inf_nan": getattr(value, "allow_inf_nan", None), "decimal_places": getattr(value, "decimal_places", None), "ge": getattr(value, "ge", None), "gt": getattr(value, "gt", None), "item_type": getattr(value, "item_type", None), "le": getattr(value, "le", None), "lower_case": getattr(value, "to_lower", None), "lt": getattr(value, "lt", None), "max_digits": getattr(value, "max_digits", None), "max_length": getattr(value, "max_length", getattr(value, "max_length", None)), "min_length": getattr(value, "min_length", getattr(value, "min_items", None)), "multiple_of": getattr(value, "multiple_of", None), "path_type": getattr(value, "path_type", None), "pattern": getattr(value, "regex", getattr(value, "pattern", None)), "tz": getattr(value, "tz", None), "unique_items": getattr(value, "unique_items", None), "upper_case": getattr(value, "to_upper", None), "uuid_version": getattr(value, "uuid_version", None), }.items() if v is not None }, ) return cast("Constraints", constraints)
[docs] @classmethod def get_constraints_metadata(cls, annotation: Any) -> Sequence[Any]: """Get the metadatas of the constraints from the given annotation. :param annotation: A type annotation. :param random: An instance of random.Random. :returns: A list of the metadata in the annotation. """ return get_annotation_metadata(annotation)