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.

Files changed (41) hide show
  1. pydantic_fixturegen/__init__.py +7 -0
  2. pydantic_fixturegen/cli/__init__.py +85 -0
  3. pydantic_fixturegen/cli/doctor.py +235 -0
  4. pydantic_fixturegen/cli/gen/__init__.py +23 -0
  5. pydantic_fixturegen/cli/gen/_common.py +139 -0
  6. pydantic_fixturegen/cli/gen/explain.py +145 -0
  7. pydantic_fixturegen/cli/gen/fixtures.py +283 -0
  8. pydantic_fixturegen/cli/gen/json.py +262 -0
  9. pydantic_fixturegen/cli/gen/schema.py +164 -0
  10. pydantic_fixturegen/cli/list.py +164 -0
  11. pydantic_fixturegen/core/__init__.py +103 -0
  12. pydantic_fixturegen/core/ast_discover.py +169 -0
  13. pydantic_fixturegen/core/config.py +440 -0
  14. pydantic_fixturegen/core/errors.py +136 -0
  15. pydantic_fixturegen/core/generate.py +311 -0
  16. pydantic_fixturegen/core/introspect.py +141 -0
  17. pydantic_fixturegen/core/io_utils.py +77 -0
  18. pydantic_fixturegen/core/providers/__init__.py +32 -0
  19. pydantic_fixturegen/core/providers/collections.py +74 -0
  20. pydantic_fixturegen/core/providers/identifiers.py +68 -0
  21. pydantic_fixturegen/core/providers/numbers.py +133 -0
  22. pydantic_fixturegen/core/providers/registry.py +98 -0
  23. pydantic_fixturegen/core/providers/strings.py +109 -0
  24. pydantic_fixturegen/core/providers/temporal.py +42 -0
  25. pydantic_fixturegen/core/safe_import.py +403 -0
  26. pydantic_fixturegen/core/schema.py +320 -0
  27. pydantic_fixturegen/core/seed.py +154 -0
  28. pydantic_fixturegen/core/strategies.py +193 -0
  29. pydantic_fixturegen/core/version.py +52 -0
  30. pydantic_fixturegen/emitters/__init__.py +15 -0
  31. pydantic_fixturegen/emitters/json_out.py +373 -0
  32. pydantic_fixturegen/emitters/pytest_codegen.py +365 -0
  33. pydantic_fixturegen/emitters/schema_out.py +84 -0
  34. pydantic_fixturegen/plugins/builtin.py +45 -0
  35. pydantic_fixturegen/plugins/hookspecs.py +59 -0
  36. pydantic_fixturegen/plugins/loader.py +72 -0
  37. pydantic_fixturegen-1.0.0.dist-info/METADATA +280 -0
  38. pydantic_fixturegen-1.0.0.dist-info/RECORD +41 -0
  39. pydantic_fixturegen-1.0.0.dist-info/WHEEL +4 -0
  40. pydantic_fixturegen-1.0.0.dist-info/entry_points.txt +5 -0
  41. 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"]