pydantic-fixturegen 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pydantic-fixturegen might be problematic. Click here for more details.
- pydantic_fixturegen/__init__.py +7 -0
- pydantic_fixturegen/cli/__init__.py +85 -0
- pydantic_fixturegen/cli/doctor.py +235 -0
- pydantic_fixturegen/cli/gen/__init__.py +23 -0
- pydantic_fixturegen/cli/gen/_common.py +139 -0
- pydantic_fixturegen/cli/gen/explain.py +145 -0
- pydantic_fixturegen/cli/gen/fixtures.py +283 -0
- pydantic_fixturegen/cli/gen/json.py +262 -0
- pydantic_fixturegen/cli/gen/schema.py +164 -0
- pydantic_fixturegen/cli/list.py +164 -0
- pydantic_fixturegen/core/__init__.py +103 -0
- pydantic_fixturegen/core/ast_discover.py +169 -0
- pydantic_fixturegen/core/config.py +440 -0
- pydantic_fixturegen/core/errors.py +136 -0
- pydantic_fixturegen/core/generate.py +311 -0
- pydantic_fixturegen/core/introspect.py +141 -0
- pydantic_fixturegen/core/io_utils.py +77 -0
- pydantic_fixturegen/core/providers/__init__.py +32 -0
- pydantic_fixturegen/core/providers/collections.py +74 -0
- pydantic_fixturegen/core/providers/identifiers.py +68 -0
- pydantic_fixturegen/core/providers/numbers.py +133 -0
- pydantic_fixturegen/core/providers/registry.py +98 -0
- pydantic_fixturegen/core/providers/strings.py +109 -0
- pydantic_fixturegen/core/providers/temporal.py +42 -0
- pydantic_fixturegen/core/safe_import.py +403 -0
- pydantic_fixturegen/core/schema.py +320 -0
- pydantic_fixturegen/core/seed.py +154 -0
- pydantic_fixturegen/core/strategies.py +193 -0
- pydantic_fixturegen/core/version.py +52 -0
- pydantic_fixturegen/emitters/__init__.py +15 -0
- pydantic_fixturegen/emitters/json_out.py +373 -0
- pydantic_fixturegen/emitters/pytest_codegen.py +365 -0
- pydantic_fixturegen/emitters/schema_out.py +84 -0
- pydantic_fixturegen/plugins/builtin.py +45 -0
- pydantic_fixturegen/plugins/hookspecs.py +59 -0
- pydantic_fixturegen/plugins/loader.py +72 -0
- pydantic_fixturegen-1.0.0.dist-info/METADATA +280 -0
- pydantic_fixturegen-1.0.0.dist-info/RECORD +41 -0
- pydantic_fixturegen-1.0.0.dist-info/WHEEL +4 -0
- pydantic_fixturegen-1.0.0.dist-info/entry_points.txt +5 -0
- pydantic_fixturegen-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Numeric providers for ints, floats, decimals, and booleans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import decimal
|
|
6
|
+
import random
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic_fixturegen.core.providers.registry import ProviderRegistry
|
|
10
|
+
from pydantic_fixturegen.core.schema import FieldConstraints, FieldSummary
|
|
11
|
+
|
|
12
|
+
INT_DEFAULT_MIN = -10
|
|
13
|
+
INT_DEFAULT_MAX = 10
|
|
14
|
+
FLOAT_DEFAULT_MIN = -10.0
|
|
15
|
+
FLOAT_DEFAULT_MAX = 10.0
|
|
16
|
+
DECIMAL_DEFAULT_PLACES = decimal.Decimal("0.01")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_numeric(
|
|
20
|
+
summary: FieldSummary,
|
|
21
|
+
*,
|
|
22
|
+
random_generator: random.Random | None = None,
|
|
23
|
+
) -> Any:
|
|
24
|
+
rng = random_generator or random.Random()
|
|
25
|
+
|
|
26
|
+
if summary.type == "bool":
|
|
27
|
+
return rng.choice([True, False])
|
|
28
|
+
|
|
29
|
+
if summary.type == "int":
|
|
30
|
+
return _generate_int(summary.constraints, rng)
|
|
31
|
+
|
|
32
|
+
if summary.type == "float":
|
|
33
|
+
return _generate_float(summary.constraints, rng)
|
|
34
|
+
|
|
35
|
+
if summary.type == "decimal":
|
|
36
|
+
return _generate_decimal(summary.constraints, rng)
|
|
37
|
+
|
|
38
|
+
raise ValueError(f"Unsupported numeric type: {summary.type}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_numeric_providers(registry: ProviderRegistry) -> None:
|
|
42
|
+
registry.register(
|
|
43
|
+
"int",
|
|
44
|
+
generate_numeric,
|
|
45
|
+
name="number.int",
|
|
46
|
+
metadata={"type": "int"},
|
|
47
|
+
)
|
|
48
|
+
registry.register(
|
|
49
|
+
"float",
|
|
50
|
+
generate_numeric,
|
|
51
|
+
name="number.float",
|
|
52
|
+
metadata={"type": "float"},
|
|
53
|
+
)
|
|
54
|
+
registry.register(
|
|
55
|
+
"decimal",
|
|
56
|
+
generate_numeric,
|
|
57
|
+
name="number.decimal",
|
|
58
|
+
metadata={"type": "decimal"},
|
|
59
|
+
)
|
|
60
|
+
registry.register(
|
|
61
|
+
"bool",
|
|
62
|
+
generate_numeric,
|
|
63
|
+
name="number.bool",
|
|
64
|
+
metadata={"type": "bool"},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _generate_int(constraints: FieldConstraints, rng: random.Random) -> int:
|
|
69
|
+
minimum = INT_DEFAULT_MIN
|
|
70
|
+
maximum = INT_DEFAULT_MAX
|
|
71
|
+
|
|
72
|
+
if constraints.ge is not None:
|
|
73
|
+
minimum = int(constraints.ge)
|
|
74
|
+
if constraints.gt is not None:
|
|
75
|
+
minimum = int(constraints.gt) + 1
|
|
76
|
+
if constraints.le is not None:
|
|
77
|
+
maximum = int(constraints.le)
|
|
78
|
+
if constraints.lt is not None:
|
|
79
|
+
maximum = int(constraints.lt) - 1
|
|
80
|
+
|
|
81
|
+
if minimum > maximum:
|
|
82
|
+
minimum = maximum
|
|
83
|
+
|
|
84
|
+
return rng.randint(minimum, maximum)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _generate_float(constraints: FieldConstraints, rng: random.Random) -> float:
|
|
88
|
+
minimum = FLOAT_DEFAULT_MIN
|
|
89
|
+
maximum = FLOAT_DEFAULT_MAX
|
|
90
|
+
|
|
91
|
+
if constraints.ge is not None:
|
|
92
|
+
minimum = float(constraints.ge)
|
|
93
|
+
if constraints.gt is not None:
|
|
94
|
+
minimum = float(constraints.gt) + 1e-6
|
|
95
|
+
if constraints.le is not None:
|
|
96
|
+
maximum = float(constraints.le)
|
|
97
|
+
if constraints.lt is not None:
|
|
98
|
+
maximum = float(constraints.lt) - 1e-6
|
|
99
|
+
|
|
100
|
+
if minimum > maximum:
|
|
101
|
+
minimum = maximum
|
|
102
|
+
|
|
103
|
+
return rng.uniform(minimum, maximum)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _generate_decimal(constraints: FieldConstraints, rng: random.Random) -> decimal.Decimal:
|
|
107
|
+
minimum = decimal.Decimal(FLOAT_DEFAULT_MIN)
|
|
108
|
+
maximum = decimal.Decimal(FLOAT_DEFAULT_MAX)
|
|
109
|
+
|
|
110
|
+
if constraints.ge is not None:
|
|
111
|
+
minimum = decimal.Decimal(str(constraints.ge))
|
|
112
|
+
if constraints.gt is not None:
|
|
113
|
+
minimum = decimal.Decimal(str(constraints.gt)) + decimal.Decimal("0.0001")
|
|
114
|
+
if constraints.le is not None:
|
|
115
|
+
maximum = decimal.Decimal(str(constraints.le))
|
|
116
|
+
if constraints.lt is not None:
|
|
117
|
+
maximum = decimal.Decimal(str(constraints.lt)) - decimal.Decimal("0.0001")
|
|
118
|
+
|
|
119
|
+
if minimum > maximum:
|
|
120
|
+
minimum = maximum
|
|
121
|
+
|
|
122
|
+
raw = decimal.Decimal(str(rng.uniform(float(minimum), float(maximum))))
|
|
123
|
+
quantizer = _quantizer(constraints)
|
|
124
|
+
return raw.quantize(quantizer, rounding=decimal.ROUND_HALF_UP)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _quantizer(constraints: FieldConstraints) -> decimal.Decimal:
|
|
128
|
+
if constraints.decimal_places is not None:
|
|
129
|
+
return decimal.Decimal("1").scaleb(-constraints.decimal_places)
|
|
130
|
+
return DECIMAL_DEFAULT_PLACES
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = ["generate_numeric", "register_numeric_providers"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Provider registry for mapping types to value generators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic_fixturegen.plugins.loader import (
|
|
10
|
+
get_plugin_manager,
|
|
11
|
+
load_entrypoint_plugins,
|
|
12
|
+
register_plugin,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
ProviderFunc = Callable[..., Any]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class ProviderRef:
|
|
20
|
+
"""Descriptor for a registered provider."""
|
|
21
|
+
|
|
22
|
+
type_id: str
|
|
23
|
+
format: str | None
|
|
24
|
+
name: str
|
|
25
|
+
func: ProviderFunc
|
|
26
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ProviderRegistry:
|
|
30
|
+
"""Registry of provider functions addressable by type identifier and format."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._providers: dict[tuple[str, str | None], ProviderRef] = {}
|
|
34
|
+
self._plugin_manager = get_plugin_manager()
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------------ registration
|
|
37
|
+
def register(
|
|
38
|
+
self,
|
|
39
|
+
type_id: str,
|
|
40
|
+
provider: ProviderFunc,
|
|
41
|
+
*,
|
|
42
|
+
format: str | None = None,
|
|
43
|
+
name: str | None = None,
|
|
44
|
+
metadata: Mapping[str, Any] | None = None,
|
|
45
|
+
override: bool = False,
|
|
46
|
+
) -> ProviderRef:
|
|
47
|
+
key = (type_id, format)
|
|
48
|
+
if not override and key in self._providers:
|
|
49
|
+
raise ValueError(f"Provider already registered for {type_id!r} with format {format!r}.")
|
|
50
|
+
ref = ProviderRef(
|
|
51
|
+
type_id=type_id,
|
|
52
|
+
format=format,
|
|
53
|
+
name=name or provider.__name__,
|
|
54
|
+
func=provider,
|
|
55
|
+
metadata=metadata or {},
|
|
56
|
+
)
|
|
57
|
+
self._providers[key] = ref
|
|
58
|
+
return ref
|
|
59
|
+
|
|
60
|
+
def unregister(self, type_id: str, format: str | None = None) -> None:
|
|
61
|
+
self._providers.pop((type_id, format), None)
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------ lookup
|
|
64
|
+
def get(self, type_id: str, format: str | None = None) -> ProviderRef | None:
|
|
65
|
+
key = (type_id, format)
|
|
66
|
+
if key in self._providers:
|
|
67
|
+
return self._providers[key]
|
|
68
|
+
fallback_key = (type_id, None)
|
|
69
|
+
return self._providers.get(fallback_key)
|
|
70
|
+
|
|
71
|
+
def available(self) -> Iterable[ProviderRef]:
|
|
72
|
+
return self._providers.values()
|
|
73
|
+
|
|
74
|
+
def clear(self) -> None:
|
|
75
|
+
self._providers.clear()
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------ plugins
|
|
78
|
+
def register_plugin(self, plugin: Any) -> None:
|
|
79
|
+
"""Register a plugin object and invoke its provider hook."""
|
|
80
|
+
|
|
81
|
+
register_plugin(plugin)
|
|
82
|
+
self._plugin_manager.hook.pfg_register_providers(registry=self)
|
|
83
|
+
|
|
84
|
+
def load_entrypoint_plugins(
|
|
85
|
+
self,
|
|
86
|
+
group: str = "pydantic_fixturegen",
|
|
87
|
+
*,
|
|
88
|
+
force: bool = False,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Load plugins defined via Python entry points and invoke hooks."""
|
|
91
|
+
|
|
92
|
+
plugins = load_entrypoint_plugins(group, force=force)
|
|
93
|
+
if not plugins:
|
|
94
|
+
return
|
|
95
|
+
self._plugin_manager.hook.pfg_register_providers(registry=self)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["ProviderRegistry", "ProviderRef", "ProviderFunc"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""String-related providers for pydantic-fixturegen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
try: # Optional dependency for regex generation
|
|
10
|
+
import rstr as _rstr
|
|
11
|
+
except ImportError: # pragma: no cover - optional extra not installed
|
|
12
|
+
rstr: ModuleType | None = None
|
|
13
|
+
else:
|
|
14
|
+
rstr = _rstr
|
|
15
|
+
|
|
16
|
+
from faker import Faker
|
|
17
|
+
|
|
18
|
+
from pydantic_fixturegen.core.providers import ProviderRegistry
|
|
19
|
+
from pydantic_fixturegen.core.schema import FieldSummary
|
|
20
|
+
|
|
21
|
+
DEFAULT_MIN_CHARS = 1
|
|
22
|
+
DEFAULT_MAX_CHARS = 16
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate_string(
|
|
26
|
+
summary: FieldSummary,
|
|
27
|
+
*,
|
|
28
|
+
faker: Faker | None = None,
|
|
29
|
+
random_generator: random.Random | None = None,
|
|
30
|
+
) -> str | bytes:
|
|
31
|
+
"""Generate a string that satisfies the provided constraints."""
|
|
32
|
+
|
|
33
|
+
if summary.type not in {"string", "secret-str", "secret-bytes"}:
|
|
34
|
+
raise ValueError(f"Unsupported string type: {summary.type}")
|
|
35
|
+
|
|
36
|
+
faker = faker or Faker()
|
|
37
|
+
rng = random_generator or random.Random()
|
|
38
|
+
|
|
39
|
+
if summary.type == "secret-str":
|
|
40
|
+
return _random_string(rng, summary, faker=faker)
|
|
41
|
+
if summary.type == "secret-bytes":
|
|
42
|
+
length = _determine_length(summary)
|
|
43
|
+
return os.urandom(length)
|
|
44
|
+
|
|
45
|
+
if summary.constraints.pattern:
|
|
46
|
+
return _regex_string(summary, faker=faker)
|
|
47
|
+
return _random_string(rng, summary, faker=faker)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register_string_providers(registry: ProviderRegistry) -> None:
|
|
51
|
+
registry.register(
|
|
52
|
+
"string",
|
|
53
|
+
generate_string,
|
|
54
|
+
name="string.default",
|
|
55
|
+
metadata={"description": "Faker-backed string provider"},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _regex_string(summary: FieldSummary, *, faker: Faker) -> str:
|
|
60
|
+
pattern = summary.constraints.pattern or ".*"
|
|
61
|
+
candidate: str
|
|
62
|
+
candidate = (
|
|
63
|
+
rstr.xeger(pattern)
|
|
64
|
+
if rstr is not None
|
|
65
|
+
else _fallback_regex(pattern, faker) # pragma: no cover - fallback path without regex extra
|
|
66
|
+
)
|
|
67
|
+
return _apply_length(candidate, summary, faker=faker)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _fallback_regex(pattern: str, faker: Faker) -> str:
|
|
71
|
+
stripped = pattern.strip("^$")
|
|
72
|
+
if not stripped:
|
|
73
|
+
return faker.pystr(min_chars=DEFAULT_MIN_CHARS, max_chars=DEFAULT_MAX_CHARS)
|
|
74
|
+
# crude fallback: ensure prefix matches stripped text ignoring regex tokens
|
|
75
|
+
prefix = "".join(ch for ch in stripped if ch.isalnum())
|
|
76
|
+
remainder = faker.pystr(min_chars=0, max_chars=max(DEFAULT_MIN_CHARS, len(prefix)))
|
|
77
|
+
return prefix + remainder
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _random_string(rng: random.Random, summary: FieldSummary, *, faker: Faker) -> str:
|
|
81
|
+
min_chars, max_chars = _length_bounds(summary)
|
|
82
|
+
# Faker's pystr respects min/max characters
|
|
83
|
+
return faker.pystr(min_chars=min_chars, max_chars=max_chars)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _apply_length(value: str, summary: FieldSummary, *, faker: Faker) -> str:
|
|
87
|
+
min_chars, max_chars = _length_bounds(summary)
|
|
88
|
+
if len(value) < min_chars:
|
|
89
|
+
padding = faker.pystr(min_chars=min_chars - len(value), max_chars=min_chars - len(value))
|
|
90
|
+
value = value + padding
|
|
91
|
+
if len(value) > max_chars:
|
|
92
|
+
value = value[:max_chars]
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _length_bounds(summary: FieldSummary) -> tuple[int, int]:
|
|
97
|
+
min_chars = summary.constraints.min_length or DEFAULT_MIN_CHARS
|
|
98
|
+
max_chars = summary.constraints.max_length or max(min_chars, DEFAULT_MAX_CHARS)
|
|
99
|
+
if min_chars > max_chars:
|
|
100
|
+
min_chars = max_chars
|
|
101
|
+
return min_chars, max_chars
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _determine_length(summary: FieldSummary) -> int:
|
|
105
|
+
min_chars, max_chars = _length_bounds(summary)
|
|
106
|
+
return max(min_chars, min(max_chars, DEFAULT_MAX_CHARS))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = ["generate_string", "register_string_providers"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Temporal providers for datetime, date, and time types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from faker import Faker
|
|
9
|
+
|
|
10
|
+
from pydantic_fixturegen.core.providers.registry import ProviderRegistry
|
|
11
|
+
from pydantic_fixturegen.core.schema import FieldSummary
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_temporal(
|
|
15
|
+
summary: FieldSummary,
|
|
16
|
+
*,
|
|
17
|
+
faker: Faker | None = None,
|
|
18
|
+
) -> Any:
|
|
19
|
+
faker = faker or Faker()
|
|
20
|
+
type_name = summary.type
|
|
21
|
+
|
|
22
|
+
if type_name == "datetime":
|
|
23
|
+
return faker.date_time(tzinfo=datetime.timezone.utc)
|
|
24
|
+
if type_name == "date":
|
|
25
|
+
return faker.date_object()
|
|
26
|
+
if type_name == "time":
|
|
27
|
+
return faker.time_object()
|
|
28
|
+
|
|
29
|
+
raise ValueError(f"Unsupported temporal type: {type_name}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def register_temporal_providers(registry: ProviderRegistry) -> None:
|
|
33
|
+
for temporal_type in ("datetime", "date", "time"):
|
|
34
|
+
registry.register(
|
|
35
|
+
temporal_type,
|
|
36
|
+
generate_temporal,
|
|
37
|
+
name=f"temporal.{temporal_type}",
|
|
38
|
+
metadata={"type": temporal_type},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["generate_temporal", "register_temporal_providers"]
|