diff options
author | cyfraeviolae <cyfraeviolae> | 2024-04-03 03:10:44 -0400 |
---|---|---|
committer | cyfraeviolae <cyfraeviolae> | 2024-04-03 03:10:44 -0400 |
commit | 6d7ba58f880be618ade07f8ea080fe8c4bf8a896 (patch) | |
tree | b1c931051ffcebd2bd9d61d98d6233ffa289bbce /venv/lib/python3.11/site-packages/polyfactory/value_generators | |
parent | 4f884c9abc32990b4061a1bb6997b4b37e58ea0b (diff) |
venv
Diffstat (limited to 'venv/lib/python3.11/site-packages/polyfactory/value_generators')
22 files changed, 1155 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__init__.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__init__.py diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/__init__.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..c95bde0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/__init__.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/complex_types.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/complex_types.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..b99a526 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/complex_types.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_collections.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_collections.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..bb33c8a --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_collections.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_dates.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_dates.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..f649c36 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_dates.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_numbers.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_numbers.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..926ca52 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_numbers.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_path.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_path.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..5808e81 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_path.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_strings.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_strings.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..53efa2f --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_strings.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_url.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_url.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..4dd4ad5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_url.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_uuid.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_uuid.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..e653a72 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/constrained_uuid.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/primitives.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/primitives.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..97787ad --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/primitives.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/regex.cpython-311.pyc b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/regex.cpython-311.pyc Binary files differnew file mode 100644 index 0000000..319510e --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/__pycache__/regex.cpython-311.pyc diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/complex_types.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/complex_types.py new file mode 100644 index 0000000..2706891 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/complex_types.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, AbstractSet, Any, Iterable, MutableMapping, MutableSequence, Set, Tuple, cast + +from typing_extensions import is_typeddict + +from polyfactory.constants import INSTANTIABLE_TYPE_MAPPING, PY_38 +from polyfactory.field_meta import FieldMeta +from polyfactory.utils.model_coverage import CoverageContainer + +if TYPE_CHECKING: + from polyfactory.factories.base import BaseFactory + + +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. + :param factory: A factory. + :param field_meta: A field meta instance. + + :returns: A built result. + """ + + if PY_38 and container_type in INSTANTIABLE_TYPE_MAPPING: + container_type = INSTANTIABLE_TYPE_MAPPING[container_type] # type: ignore[assignment] + + container = container_type() + if not field_meta.children: + return container + + if issubclass(container_type, MutableMapping) or is_typeddict(container_type): + for key_field_meta, value_field_meta in cast( + Iterable[Tuple[FieldMeta, FieldMeta]], + zip(field_meta.children[::2], field_meta.children[1::2]), + ): + key = factory.get_field_value(key_field_meta) + value = factory.get_field_value(value_field_meta) + container[key] = value + return container + + if issubclass(container_type, MutableSequence): + container.extend([factory.get_field_value(subfield_meta) for subfield_meta in field_meta.children]) + return container + + if issubclass(container_type, Set): + for subfield_meta in field_meta.children: + container.add(factory.get_field_value(subfield_meta)) + return container + + if issubclass(container_type, AbstractSet): + return container.union(handle_collection_type(field_meta, set, factory)) + + if issubclass(container_type, tuple): + return container_type(map(factory.get_field_value, field_meta.children)) + + msg = f"Unsupported container type: {container_type}" + raise NotImplementedError(msg) + + +def handle_collection_type_coverage( + field_meta: FieldMeta, + container_type: type, + factory: type[BaseFactory[Any]], +) -> Any: + """Handle coverage generation of container types recursively. + + :param container_type: A type that can accept type arguments. + :param factory: A factory. + :param field_meta: A field meta instance. + + :returns: An unresolved built result. + """ + container = container_type() + if not field_meta.children: + return container + + if issubclass(container_type, MutableMapping) or is_typeddict(container_type): + for key_field_meta, value_field_meta in cast( + Iterable[Tuple[FieldMeta, FieldMeta]], + zip(field_meta.children[::2], field_meta.children[1::2]), + ): + key = CoverageContainer(factory.get_field_value_coverage(key_field_meta)) + value = CoverageContainer(factory.get_field_value_coverage(value_field_meta)) + container[key] = value + return container + + if issubclass(container_type, MutableSequence): + container_instance = container_type() + for subfield_meta in field_meta.children: + container_instance.extend(factory.get_field_value_coverage(subfield_meta)) + + return container_instance + + if issubclass(container_type, Set): + set_instance = container_type() + for subfield_meta in field_meta.children: + set_instance = set_instance.union(factory.get_field_value_coverage(subfield_meta)) + + return set_instance + + if issubclass(container_type, AbstractSet): + return container.union(handle_collection_type_coverage(field_meta, set, factory)) + + if issubclass(container_type, tuple): + return container_type( + CoverageContainer(factory.get_field_value_coverage(subfield_meta)) for subfield_meta in field_meta.children + ) + + msg = f"Unsupported container type: {container_type}" + raise NotImplementedError(msg) diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_collections.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_collections.py new file mode 100644 index 0000000..405d02d --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_collections.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, List, Mapping, TypeVar, cast + +from polyfactory.exceptions import ParameterException +from polyfactory.field_meta import FieldMeta + +if TYPE_CHECKING: + from polyfactory.factories.base import BaseFactory + +T = TypeVar("T", list, set, frozenset) + + +def handle_constrained_collection( + collection_type: Callable[..., T], + factory: type[BaseFactory[Any]], + field_meta: FieldMeta, + item_type: Any, + max_items: int | None = None, + min_items: int | None = None, + unique_items: bool = False, +) -> T: + """Generate a constrained list or set. + + :param collection_type: A type that can accept type arguments. + :param factory: A factory. + :param field_meta: A field meta instance. + :param item_type: Type of the collection items. + :param max_items: Maximal number of items. + :param min_items: Minimal number of items. + :param unique_items: Whether the items should be unique. + + :returns: A collection value. + """ + min_items = abs(min_items if min_items is not None else (max_items or 0)) + max_items = abs(max_items if max_items is not None else min_items + 1) + + if max_items < min_items: + msg = "max_items must be larger or equal to min_items" + raise ParameterException(msg) + + collection: set[T] | list[T] = set() if (collection_type in (frozenset, set) or unique_items) else [] + + try: + length = factory.__random__.randint(min_items, max_items) or 1 + while len(collection) < length: + value = factory.get_field_value(field_meta) + if isinstance(collection, set): + collection.add(value) + else: + collection.append(value) + return collection_type(collection) + except TypeError as e: + msg = f"cannot generate a constrained collection of type: {item_type}" + raise ParameterException(msg) from e + + +def handle_constrained_mapping( + factory: type[BaseFactory[Any]], + field_meta: FieldMeta, + max_items: int | None = None, + min_items: int | None = None, +) -> Mapping[Any, Any]: + """Generate a constrained mapping. + + :param factory: A factory. + :param field_meta: A field meta instance. + :param max_items: Maximal number of items. + :param min_items: Minimal number of items. + + :returns: A mapping instance. + """ + min_items = abs(min_items if min_items is not None else (max_items or 0)) + max_items = abs(max_items if max_items is not None else min_items + 1) + + if max_items < min_items: + msg = "max_items must be larger or equal to min_items" + raise ParameterException(msg) + + length = factory.__random__.randint(min_items, max_items) or 1 + + collection: dict[Any, Any] = {} + + children = cast(List[FieldMeta], field_meta.children) + key_field_meta = children[0] + value_field_meta = children[1] + while len(collection) < length: + key = factory.get_field_value(key_field_meta) + value = factory.get_field_value(value_field_meta) + collection[key] = value + + return collection diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_dates.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_dates.py new file mode 100644 index 0000000..4e92601 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_dates.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from datetime import date, datetime, timedelta, timezone, tzinfo +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from faker import Faker + + +def handle_constrained_date( + faker: Faker, + ge: date | None = None, + gt: date | None = None, + le: date | None = None, + lt: date | None = None, + tz: tzinfo = timezone.utc, +) -> date: + """Generates a date value fulfilling the expected constraints. + + :param faker: An instance of faker. + :param lt: Less than value. + :param le: Less than or equal value. + :param gt: Greater than value. + :param ge: Greater than or equal value. + :param tz: A timezone. + + :returns: A date instance. + """ + start_date = datetime.now(tz=tz).date() - timedelta(days=100) + if ge: + start_date = ge + elif gt: + start_date = gt + timedelta(days=1) + + end_date = datetime.now(tz=timezone.utc).date() + timedelta(days=100) + if le: + end_date = le + elif lt: + end_date = lt - timedelta(days=1) + + return cast("date", faker.date_between(start_date=start_date, end_date=end_date)) diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_numbers.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_numbers.py new file mode 100644 index 0000000..23516ce --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_numbers.py @@ -0,0 +1,440 @@ +from __future__ import annotations + +from decimal import Decimal +from sys import float_info +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast + +from polyfactory.exceptions import ParameterException +from polyfactory.value_generators.primitives import create_random_decimal, create_random_float, create_random_integer + +if TYPE_CHECKING: + from random import Random + +T = TypeVar("T", Decimal, int, float) + + +class NumberGeneratorProtocol(Protocol[T]): + """Protocol for custom callables used to generate numerical values""" + + def __call__(self, random: "Random", minimum: T | None = None, maximum: T | None = None) -> T: + """Signature of the callable. + + :param random: An instance of random. + :param minimum: A minimum value. + :param maximum: A maximum value. + :return: The generated numeric value. + """ + ... + + +def almost_equal_floats(value_1: float, value_2: float, *, delta: float = 1e-8) -> bool: + """Return True if two floats are almost equal + + :param value_1: A float value. + :param value_2: A float value. + :param delta: A minimal delta. + + :returns: Boolean dictating whether the floats can be considered equal - given python's problematic comparison of floats. + """ + return abs(value_1 - value_2) <= delta + + +def is_multiply_of_multiple_of_in_range( + minimum: T, + maximum: T, + multiple_of: T, +) -> bool: + """Determine if at least one multiply of `multiple_of` lies in the given range. + + :param minimum: T: A minimum value. + :param maximum: T: A maximum value. + :param multiple_of: T: A value to use as a base for multiplication. + + :returns: Boolean dictating whether at least one multiply of `multiple_of` lies in the given range between minimum and maximum. + """ + + # if the range has infinity on one of its ends then infinite number of multipliers + # can be found within the range + + # if we were given floats and multiple_of is really close to zero then it doesn't make sense + # to continue trying to check the range + if ( + isinstance(minimum, float) + and isinstance(multiple_of, float) + and minimum / multiple_of in [float("+inf"), float("-inf")] + ): + return False + + multiplier = round(minimum / multiple_of) + step = 1 if multiple_of > 0 else -1 + + # since rounding can go either up or down we may end up in a situation when + # minimum is less or equal to `multiplier * multiple_of` + # or when it is greater than `multiplier * multiple_of` + # (in this case minimum is less than `(multiplier + 1)* multiple_of`). So we need to check + # that any of two values is inside the given range. ASCII graphic below explain this + # + # minimum + # -----------------+-------+-----------------------------------+---------------------------- + # + # + # minimum + # -------------------------+--------+--------------------------+---------------------------- + # + # since `multiple_of` can be a negative number adding +1 to `multiplier` drives `(multiplier + 1) * multiple_of`` + # away from `minimum` to the -infinity. It looks like this: + # minimum + # -----------------------+--------------------------------+------------------------+-------- + # + # so for negative `multiple_of` we want to subtract 1 from multiplier + for multiply in [multiplier * multiple_of, (multiplier + step) * multiple_of]: + multiply_float = float(multiply) + if ( + almost_equal_floats(multiply_float, float(minimum)) + or almost_equal_floats(multiply_float, float(maximum)) + or minimum < multiply < maximum + ): + return True + + return False + + +def passes_pydantic_multiple_validator(value: T, multiple_of: T) -> bool: + """Determine whether a given value passes the pydantic multiple_of validation. + + :param value: A numeric value. + :param multiple_of: Another numeric value. + + :returns: Boolean dictating whether value is a multiple of value. + + """ + if multiple_of == 0: + return True + mod = float(value) / float(multiple_of) % 1 + return almost_equal_floats(mod, 0.0) or almost_equal_floats(mod, 1.0) + + +def get_increment(t_type: type[T]) -> T: + """Get a small increment base to add to constrained values, i.e. lt/gt entries. + + :param t_type: A value of type T. + + :returns: An increment T. + """ + values: dict[Any, Any] = { + int: 1, + float: float_info.epsilon, + Decimal: Decimal("0.001"), + } + return cast("T", values[t_type]) + + +def get_value_or_none( + t_type: type[T], + lt: T | None = None, + le: T | None = None, + gt: T | None = None, + ge: T | None = None, +) -> tuple[T | None, T | None]: + """Return an optional value. + + :param equal_value: An GE/LE value. + :param constrained: An GT/LT value. + :param increment: increment + + :returns: Optional T. + """ + if ge is not None: + minimum_value = ge + elif gt is not None: + minimum_value = gt + get_increment(t_type) + else: + minimum_value = None + + if le is not None: + maximum_value = le + elif lt is not None: + maximum_value = lt - get_increment(t_type) + else: + maximum_value = None + return minimum_value, maximum_value + + +def get_constrained_number_range( + t_type: type[T], + random: Random, + lt: T | None = None, + le: T | None = None, + gt: T | None = None, + ge: T | None = None, + multiple_of: T | None = None, +) -> tuple[T | None, T | None]: + """Return the minimum and maximum values given a field_meta's constraints. + + :param t_type: A primitive constructor - int, float or Decimal. + :param random: An instance of Random. + :param lt: Less than value. + :param le: Less than or equal value. + :param gt: Greater than value. + :param ge: Greater than or equal value. + :param multiple_of: Multiple of value. + + :returns: a tuple of optional minimum and maximum values. + """ + seed = t_type(random.random() * 10) + minimum, maximum = get_value_or_none(lt=lt, le=le, gt=gt, ge=ge, t_type=t_type) + + if minimum is not None and maximum is not None and maximum < minimum: + msg = "maximum value must be greater than minimum value" + raise ParameterException(msg) + + if multiple_of is None: + if minimum is not None and maximum is None: + return ( + (minimum, seed) if minimum == 0 else (minimum, minimum + seed) + ) # pyright: ignore[reportGeneralTypeIssues] + if maximum is not None and minimum is None: + return maximum - seed, maximum + else: + if multiple_of == 0.0: # TODO: investigate @guacs # noqa: PLR2004, FIX002 + msg = "multiple_of can not be zero" + raise ParameterException(msg) + if ( + minimum is not None + and maximum is not None + and not is_multiply_of_multiple_of_in_range(minimum=minimum, maximum=maximum, multiple_of=multiple_of) + ): + msg = "given range should include at least one multiply of multiple_of" + raise ParameterException(msg) + + return minimum, maximum + + +def generate_constrained_number( + random: Random, + minimum: T | None, + maximum: T | None, + multiple_of: T | None, + method: "NumberGeneratorProtocol[T]", +) -> T: + """Generate a constrained number, output depends on the passed in callbacks. + + :param random: An instance of random. + :param minimum: A minimum value. + :param maximum: A maximum value. + :param multiple_of: A multiple of value. + :param method: A function that generates numbers of type T. + + :returns: A value of type T. + """ + if minimum is None or maximum is None: + return multiple_of if multiple_of is not None else method(random=random) + if multiple_of is None: + return method(random=random, minimum=minimum, maximum=maximum) + if multiple_of >= minimum: + return multiple_of + result = minimum + while not passes_pydantic_multiple_validator(result, multiple_of): + result = round(method(random=random, minimum=minimum, maximum=maximum) / multiple_of) * multiple_of + return result + + +def handle_constrained_int( + random: Random, + multiple_of: int | None = None, + gt: int | None = None, + ge: int | None = None, + lt: int | None = None, + le: int | None = None, +) -> int: + """Handle constrained integers. + + :param random: An instance of Random. + :param lt: Less than value. + :param le: Less than or equal value. + :param gt: Greater than value. + :param ge: Greater than or equal value. + :param multiple_of: Multiple of value. + + :returns: An integer. + + """ + + minimum, maximum = get_constrained_number_range( + gt=gt, + ge=ge, + lt=lt, + le=le, + t_type=int, + multiple_of=multiple_of, + random=random, + ) + return generate_constrained_number( + random=random, + minimum=minimum, + maximum=maximum, + multiple_of=multiple_of, + method=create_random_integer, + ) + + +def handle_constrained_float( + random: Random, + multiple_of: float | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, +) -> float: + """Handle constrained floats. + + :param random: An instance of Random. + :param lt: Less than value. + :param le: Less than or equal value. + :param gt: Greater than value. + :param ge: Greater than or equal value. + :param multiple_of: Multiple of value. + + :returns: A float. + """ + + minimum, maximum = get_constrained_number_range( + gt=gt, + ge=ge, + lt=lt, + le=le, + t_type=float, + multiple_of=multiple_of, + random=random, + ) + + return generate_constrained_number( + random=random, + minimum=minimum, + maximum=maximum, + multiple_of=multiple_of, + method=create_random_float, + ) + + +def validate_max_digits( + max_digits: int, + minimum: Decimal | None, + decimal_places: int | None, +) -> None: + """Validate that max digits is greater than minimum and decimal places. + + :param max_digits: The maximal number of digits for the decimal. + :param minimum: Minimal value. + :param decimal_places: Number of decimal places + + :returns: 'None' + + """ + if max_digits <= 0: + msg = "max_digits must be greater than 0" + raise ParameterException(msg) + + if minimum is not None: + min_str = str(minimum).split(".")[1] if "." in str(minimum) else str(minimum) + + if max_digits <= len(min_str): + msg = "minimum is greater than max_digits" + raise ParameterException(msg) + + if decimal_places is not None and max_digits <= decimal_places: + msg = "max_digits must be greater than decimal places" + raise ParameterException(msg) + + +def handle_decimal_length( + generated_decimal: Decimal, + decimal_places: int | None, + max_digits: int | None, +) -> Decimal: + """Handle the length of the decimal. + + :param generated_decimal: A decimal value. + :param decimal_places: Number of decimal places. + :param max_digits: Maximal number of digits. + + """ + string_number = str(generated_decimal) + sign = "-" if "-" in string_number else "+" + string_number = string_number.replace("-", "") + whole_numbers, decimals = string_number.split(".") + + if ( + max_digits is not None + and decimal_places is not None + and len(whole_numbers) + decimal_places > max_digits + or (max_digits is None or decimal_places is None) + and max_digits is not None + ): + max_decimals = max_digits - len(whole_numbers) + elif max_digits is not None: + max_decimals = decimal_places # type: ignore[assignment] + else: + max_decimals = cast("int", decimal_places) + + if max_decimals < 0: # pyright: ignore[reportOptionalOperand] + return Decimal(sign + whole_numbers[:max_decimals]) + + decimals = decimals[:max_decimals] + return Decimal(sign + whole_numbers + "." + decimals[:decimal_places]) + + +def handle_constrained_decimal( + random: Random, + multiple_of: Decimal | None = None, + decimal_places: int | None = None, + max_digits: int | None = None, + gt: Decimal | None = None, + ge: Decimal | None = None, + lt: Decimal | None = None, + le: Decimal | None = None, +) -> Decimal: + """Handle a constrained decimal. + + :param random: An instance of Random. + :param multiple_of: Multiple of value. + :param decimal_places: Number of decimal places. + :param max_digits: Maximal number of digits. + :param lt: Less than value. + :param le: Less than or equal value. + :param gt: Greater than value. + :param ge: Greater than or equal value. + + :returns: A decimal. + + """ + + minimum, maximum = get_constrained_number_range( + gt=gt, + ge=ge, + lt=lt, + le=le, + multiple_of=multiple_of, + t_type=Decimal, + random=random, + ) + + if max_digits is not None: + validate_max_digits(max_digits=max_digits, minimum=minimum, decimal_places=decimal_places) + + generated_decimal = generate_constrained_number( + random=random, + minimum=minimum, + maximum=maximum, + multiple_of=multiple_of, + method=create_random_decimal, + ) + + if max_digits is not None or decimal_places is not None: + return handle_decimal_length( + generated_decimal=generated_decimal, + max_digits=max_digits, + decimal_places=decimal_places, + ) + + return generated_decimal diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_path.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_path.py new file mode 100644 index 0000000..debaf86 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_path.py @@ -0,0 +1,13 @@ +from os.path import realpath +from pathlib import Path +from typing import Literal, cast + +from faker import Faker + + +def handle_constrained_path(constraint: Literal["file", "dir", "new"], faker: Faker) -> Path: + if constraint == "new": + return cast("Path", faker.file_path(depth=1, category=None, extension=None)) + if constraint == "file": + return Path(realpath(__file__)) + return Path(realpath(__file__)).parent diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_strings.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_strings.py new file mode 100644 index 0000000..c7da72b --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_strings.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, Pattern, TypeVar, Union, cast + +from polyfactory.exceptions import ParameterException +from polyfactory.value_generators.primitives import create_random_bytes, create_random_string +from polyfactory.value_generators.regex import RegexFactory + +T = TypeVar("T", bound=Union[bytes, str]) + +if TYPE_CHECKING: + from random import Random + + +def _validate_length( + min_length: int | None = None, + max_length: int | None = None, +) -> None: + """Validate the length parameters make sense. + + :param min_length: Minimum length. + :param max_length: Maximum length. + + :raises: ParameterException. + + :returns: None. + """ + if min_length is not None and min_length < 0: + msg = "min_length must be greater or equal to 0" + raise ParameterException(msg) + + if max_length is not None and max_length < 0: + msg = "max_length must be greater or equal to 0" + raise ParameterException(msg) + + if max_length is not None and min_length is not None and max_length < min_length: + msg = "max_length must be greater than min_length" + raise ParameterException(msg) + + +def _generate_pattern( + random: Random, + pattern: str | Pattern, + lower_case: bool = False, + upper_case: bool = False, + min_length: int | None = None, + max_length: int | None = None, +) -> str: + """Generate a regex. + + :param random: An instance of random. + :param pattern: A regex or string pattern. + :param lower_case: Whether to lowercase the result. + :param upper_case: Whether to uppercase the result. + :param min_length: A minimum length. + :param max_length: A maximum length. + + :returns: A string matching the given pattern. + """ + regex_factory = RegexFactory(random=random) + result = regex_factory(pattern) + if min_length: + while len(result) < min_length: + result += regex_factory(pattern) + + if max_length is not None and len(result) > max_length: + result = result[:max_length] + + if lower_case: + result = result.lower() + + if upper_case: + result = result.upper() + + return result + + +def handle_constrained_string_or_bytes( + random: Random, + t_type: Callable[[], T], + lower_case: bool = False, + upper_case: bool = False, + min_length: int | None = None, + max_length: int | None = None, + pattern: str | Pattern | None = None, +) -> T: + """Handle constrained string or bytes, for example - pydantic `constr` or `conbytes`. + + :param random: An instance of random. + :param t_type: A type (str or bytes) + :param lower_case: Whether to lowercase the result. + :param upper_case: Whether to uppercase the result. + :param min_length: A minimum length. + :param max_length: A maximum length. + :param pattern: A regex or string pattern. + + :returns: A value of type T. + """ + _validate_length(min_length=min_length, max_length=max_length) + + if max_length == 0: + return t_type() + + if pattern: + return cast( + "T", + _generate_pattern( + random=random, + pattern=pattern, + lower_case=lower_case, + upper_case=upper_case, + min_length=min_length, + max_length=max_length, + ), + ) + + if t_type is str: + return cast( + "T", + create_random_string( + min_length=min_length, + max_length=max_length, + lower_case=lower_case, + upper_case=upper_case, + random=random, + ), + ) + + return cast( + "T", + create_random_bytes( + min_length=min_length, + max_length=max_length, + lower_case=lower_case, + upper_case=upper_case, + random=random, + ), + ) diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_url.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_url.py new file mode 100644 index 0000000..d29555e --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_url.py @@ -0,0 +1,10 @@ +from polyfactory.field_meta import UrlConstraints + + +def handle_constrained_url(constraints: UrlConstraints) -> str: + schema = (constraints.get("allowed_schemes") or ["http", "https"])[0] + default_host = constraints.get("default_host") or "localhost" + default_port = constraints.get("default_port") or 80 + default_path = constraints.get("default_path") or "" + + return f"{schema}://{default_host}:{default_port}{default_path}" diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_uuid.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_uuid.py new file mode 100644 index 0000000..053f047 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/constrained_uuid.py @@ -0,0 +1,31 @@ +from typing import Literal, cast +from uuid import NAMESPACE_DNS, UUID, uuid1, uuid3, uuid5 + +from faker import Faker + +UUID_VERSION_1 = 1 +UUID_VERSION_3 = 3 +UUID_VERSION_4 = 4 +UUID_VERSION_5 = 5 + + +def handle_constrained_uuid(uuid_version: Literal[1, 3, 4, 5], faker: Faker) -> UUID: + """Generate a UUID based on the version specified. + + Args: + uuid_version: The version of the UUID to generate. + faker: The Faker instance to use. + + Returns: + The generated UUID. + """ + if uuid_version == UUID_VERSION_1: + return uuid1() + if uuid_version == UUID_VERSION_3: + return uuid3(NAMESPACE_DNS, faker.pystr()) + if uuid_version == UUID_VERSION_4: + return cast("UUID", faker.uuid4()) + if uuid_version == UUID_VERSION_5: + return uuid5(NAMESPACE_DNS, faker.pystr()) + msg = f"Unknown UUID version: {uuid_version}" + raise ValueError(msg) diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/primitives.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/primitives.py new file mode 100644 index 0000000..2cf6b41 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/primitives.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from binascii import hexlify +from decimal import Decimal +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from random import Random + + +def create_random_float( + random: Random, + minimum: Decimal | float | None = None, + maximum: Decimal | float | None = None, +) -> float: + """Generate a random float given the constraints. + + :param random: An instance of random. + :param minimum: A minimum value + :param maximum: A maximum value. + + :returns: A random float. + """ + if minimum is None: + minimum = float(random.randint(0, 100)) if maximum is None else float(maximum) - 100.0 + if maximum is None: + maximum = float(minimum) + 1.0 * 2.0 if minimum >= 0 else float(minimum) + 1.0 / 2.0 + return random.uniform(float(minimum), float(maximum)) + + +def create_random_integer(random: Random, minimum: int | None = None, maximum: int | None = None) -> int: + """Generate a random int given the constraints. + + :param random: An instance of random. + :param minimum: A minimum value + :param maximum: A maximum value. + + :returns: A random integer. + """ + return round(create_random_float(random=random, minimum=minimum, maximum=maximum)) + + +def create_random_decimal( + random: Random, + minimum: Decimal | None = None, + maximum: Decimal | None = None, +) -> Decimal: + """Generate a random Decimal given the constraints. + + :param random: An instance of random. + :param minimum: A minimum value + :param maximum: A maximum value. + + :returns: A random decimal. + """ + return Decimal(str(create_random_float(random=random, minimum=minimum, maximum=maximum))) + + +def create_random_bytes( + random: Random, + min_length: int | None = None, + max_length: int | None = None, + lower_case: bool = False, + upper_case: bool = False, +) -> bytes: + """Generate a random bytes given the constraints. + + :param random: An instance of random. + :param min_length: A minimum length. + :param max_length: A maximum length. + :param lower_case: Whether to lowercase the result. + :param upper_case: Whether to uppercase the result. + + :returns: A random byte-string. + """ + if min_length is None: + min_length = 0 + if max_length is None: + max_length = min_length + 1 * 2 + + length = random.randint(min_length, max_length) + result = b"" if length == 0 else hexlify(random.getrandbits(length * 8).to_bytes(length, "little")) + + if lower_case: + result = result.lower() + elif upper_case: + result = result.upper() + + if max_length and len(result) > max_length: + end = random.randint(min_length or 0, max_length) + return result[:end] + + return result + + +def create_random_string( + random: Random, + min_length: int | None = None, + max_length: int | None = None, + lower_case: bool = False, + upper_case: bool = False, +) -> str: + """Generate a random string given the constraints. + + :param random: An instance of random. + :param min_length: A minimum length. + :param max_length: A maximum length. + :param lower_case: Whether to lowercase the result. + :param upper_case: Whether to uppercase the result. + + :returns: A random string. + """ + return create_random_bytes( + random=random, + min_length=min_length, + max_length=max_length, + lower_case=lower_case, + upper_case=upper_case, + ).decode("utf-8") + + +def create_random_boolean(random: Random) -> bool: + """Generate a random boolean value. + + :param random: An instance of random. + + :returns: A random boolean. + """ + return bool(random.getrandbits(1)) diff --git a/venv/lib/python3.11/site-packages/polyfactory/value_generators/regex.py b/venv/lib/python3.11/site-packages/polyfactory/value_generators/regex.py new file mode 100644 index 0000000..eab9bd0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/polyfactory/value_generators/regex.py @@ -0,0 +1,150 @@ +"""The code in this files is adapted from https://github.com/crdoconnor/xeger/blob/master/xeger/xeger.py.Which in turn +adapted it from https://bitbucket.org/leapfrogdevelopment/rstr/. + +Copyright (C) 2015, Colm O'Connor +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Leapfrog Direct Response, LLC, including + its subsidiaries and affiliates nor the names of its + contributors, may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LEAPFROG DIRECT +RESPONSE, LLC, INCLUDING ITS SUBSIDIARIES AND AFFILIATES, BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" +from __future__ import annotations + +from itertools import chain +from string import ( + ascii_letters, + ascii_lowercase, + ascii_uppercase, + digits, + printable, + punctuation, + whitespace, +) +from typing import TYPE_CHECKING, Any, Pattern + +try: # >=3.11 + from re._parser import SubPattern, parse +except ImportError: # < 3.11 + from sre_parse import SubPattern, parse # pylint: disable=deprecated-module + +if TYPE_CHECKING: + from random import Random + +_alphabets = { + "printable": printable, + "letters": ascii_letters, + "uppercase": ascii_uppercase, + "lowercase": ascii_lowercase, + "digits": digits, + "punctuation": punctuation, + "nondigits": ascii_letters + punctuation, + "nonletters": digits + punctuation, + "whitespace": whitespace, + "nonwhitespace": printable.strip(), + "normal": ascii_letters + digits + " ", + "word": ascii_letters + digits + "_", + "nonword": "".join(set(printable).difference(ascii_letters + digits + "_")), + "postalsafe": ascii_letters + digits + " .-#/", + "urlsafe": ascii_letters + digits + "-._~", + "domainsafe": ascii_letters + digits + "-", +} + +_categories = { + "category_digit": _alphabets["digits"], + "category_not_digit": _alphabets["nondigits"], + "category_space": _alphabets["whitespace"], + "category_not_space": _alphabets["nonwhitespace"], + "category_word": _alphabets["word"], + "category_not_word": _alphabets["nonword"], +} + + +class RegexFactory: + """Factory for regexes.""" + + def __init__(self, random: Random, limit: int = 10) -> None: + """Create a RegexFactory""" + self._limit = limit + self._cache: dict[str, Any] = {} + self._random = random + + self._cases = { + "literal": chr, + "not_literal": lambda x: self._random.choice(printable.replace(chr(x), "")), + "at": lambda x: "", + "in": self._handle_in, + "any": lambda x: self._random.choice(printable.replace("\n", "")), + "range": lambda x: [chr(i) for i in range(x[0], x[1] + 1)], + "category": lambda x: _categories[str(x).lower()], + "branch": lambda x: "".join(self._handle_state(i) for i in self._random.choice(x[1])), + "subpattern": self._handle_group, + "assert": lambda x: "".join(self._handle_state(i) for i in x[1]), + "assert_not": lambda x: "", + "groupref": lambda x: self._cache[x], + "min_repeat": lambda x: self._handle_repeat(*x), + "max_repeat": lambda x: self._handle_repeat(*x), + "negate": lambda x: [False], + } + + def __call__(self, string_or_regex: str | Pattern) -> str: + """Generate a string matching a regex. + + :param string_or_regex: A string or pattern. + + :return: The generated string. + """ + pattern = string_or_regex.pattern if isinstance(string_or_regex, Pattern) else string_or_regex + parsed = parse(pattern) + result = self._build_string(parsed) # pyright: ignore[reportGeneralTypeIssues] + self._cache.clear() + return result + + def _build_string(self, parsed: SubPattern) -> str: + return "".join([self._handle_state(state) for state in parsed]) # pyright:ignore[reportGeneralTypeIssues] + + def _handle_state(self, state: tuple[SubPattern, tuple[Any, ...]]) -> Any: + opcode, value = state + return self._cases[str(opcode).lower()](value) # type: ignore[no-untyped-call] + + def _handle_group(self, value: tuple[Any, ...]) -> str: + result = "".join(self._handle_state(i) for i in value[3]) + if value[0]: + self._cache[value[0]] = result + return result + + def _handle_in(self, value: tuple[Any, ...]) -> Any: + candidates = list(chain(*(self._handle_state(i) for i in value))) + if candidates and candidates[0] is False: + candidates = list(set(printable).difference(candidates[1:])) + return self._random.choice(candidates) + return self._random.choice(candidates) + + def _handle_repeat(self, start_range: int, end_range: Any, value: SubPattern) -> str: + end_range = min(end_range, self._limit) + + result = [ + "".join(self._handle_state(v) for v in list(value)) # pyright: ignore[reportGeneralTypeIssues] + for _ in range(self._random.randint(start_range, max(start_range, end_range))) + ] + return "".join(result) |