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,311 @@
1
+ """Instance generation engine using provider strategies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import enum
7
+ import inspect
8
+ import random
9
+ from collections.abc import Iterable, Sized
10
+ from dataclasses import dataclass, is_dataclass
11
+ from dataclasses import fields as dataclass_fields
12
+ from typing import Any, get_type_hints
13
+
14
+ from faker import Faker
15
+ from pydantic import BaseModel
16
+ from pydantic.fields import FieldInfo
17
+
18
+ from pydantic_fixturegen.core import schema as schema_module
19
+ from pydantic_fixturegen.core.providers import ProviderRegistry, create_default_registry
20
+ from pydantic_fixturegen.core.schema import FieldConstraints, FieldSummary, extract_constraints
21
+ from pydantic_fixturegen.core.strategies import (
22
+ Strategy,
23
+ StrategyBuilder,
24
+ StrategyResult,
25
+ UnionStrategy,
26
+ )
27
+ from pydantic_fixturegen.plugins.loader import get_plugin_manager, load_entrypoint_plugins
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class GenerationConfig:
32
+ max_depth: int = 5
33
+ max_objects: int = 100
34
+ enum_policy: str = "first"
35
+ union_policy: str = "first"
36
+ default_p_none: float = 0.0
37
+ optional_p_none: float = 0.0
38
+ seed: int | None = None
39
+
40
+
41
+ class InstanceGenerator:
42
+ """Generate instances of Pydantic models with recursion guards."""
43
+
44
+ def __init__(
45
+ self,
46
+ registry: ProviderRegistry | None = None,
47
+ *,
48
+ config: GenerationConfig | None = None,
49
+ ) -> None:
50
+ self.config = config or GenerationConfig()
51
+ self.registry = registry or create_default_registry(load_plugins=False)
52
+ self.random = random.Random(self.config.seed)
53
+ self.faker = Faker()
54
+ if self.config.seed is not None:
55
+ Faker.seed(self.config.seed)
56
+ self.faker.seed_instance(self.config.seed)
57
+
58
+ load_entrypoint_plugins()
59
+ self._plugin_manager = get_plugin_manager()
60
+
61
+ if registry is None:
62
+ self.registry.load_entrypoint_plugins()
63
+
64
+ self.builder = StrategyBuilder(
65
+ self.registry,
66
+ enum_policy=self.config.enum_policy,
67
+ union_policy=self.config.union_policy,
68
+ default_p_none=self.config.default_p_none,
69
+ optional_p_none=self.config.optional_p_none,
70
+ plugin_manager=self._plugin_manager,
71
+ )
72
+ self._strategy_cache: dict[type[Any], dict[str, StrategyResult]] = {}
73
+
74
+ # ------------------------------------------------------------------ public API
75
+ def generate_one(self, model: type[BaseModel]) -> BaseModel | None:
76
+ self._objects_remaining = self.config.max_objects
77
+ return self._build_model_instance(model, depth=0)
78
+
79
+ def generate(self, model: type[BaseModel], count: int = 1) -> list[BaseModel]:
80
+ results: list[BaseModel] = []
81
+ for _ in range(count):
82
+ instance = self.generate_one(model)
83
+ if instance is None:
84
+ break
85
+ results.append(instance)
86
+ return results
87
+
88
+ # ------------------------------------------------------------------ internals
89
+ def _build_model_instance(self, model_type: type[Any], *, depth: int) -> Any | None:
90
+ if depth >= self.config.max_depth:
91
+ return None
92
+ if not self._consume_object():
93
+ return None
94
+
95
+ try:
96
+ strategies = self._get_model_strategies(model_type)
97
+ except TypeError:
98
+ return None
99
+
100
+ values: dict[str, Any] = {}
101
+ for field_name, strategy in strategies.items():
102
+ values[field_name] = self._evaluate_strategy(strategy, depth)
103
+
104
+ try:
105
+ if isinstance(model_type, type) and issubclass(model_type, BaseModel):
106
+ return model_type(**values)
107
+ if is_dataclass(model_type):
108
+ return model_type(**values)
109
+ except Exception:
110
+ return None
111
+ return None
112
+
113
+ def _evaluate_strategy(self, strategy: StrategyResult, depth: int) -> Any:
114
+ if isinstance(strategy, UnionStrategy):
115
+ return self._evaluate_union(strategy, depth)
116
+ return self._evaluate_single(strategy, depth)
117
+
118
+ def _evaluate_union(self, strategy: UnionStrategy, depth: int) -> Any:
119
+ choices = strategy.choices
120
+ if not choices:
121
+ return None
122
+
123
+ selected = self.random.choice(choices) if strategy.policy == "random" else choices[0]
124
+ return self._evaluate_single(selected, depth)
125
+
126
+ def _evaluate_single(self, strategy: Strategy, depth: int) -> Any:
127
+ if self._should_return_none(strategy):
128
+ return None
129
+
130
+ summary = strategy.summary
131
+ enum_values = strategy.enum_values or summary.enum_values
132
+
133
+ if enum_values:
134
+ return self._select_enum_value(strategy, enum_values)
135
+
136
+ annotation = strategy.annotation
137
+
138
+ if self._is_model_like(annotation):
139
+ return self._build_model_instance(annotation, depth=depth + 1)
140
+
141
+ if summary.type in {"list", "set", "tuple", "mapping"}:
142
+ return self._evaluate_collection(strategy, depth)
143
+
144
+ if strategy.provider_ref is None:
145
+ return None
146
+ return self._call_strategy_provider(strategy)
147
+
148
+ def _evaluate_collection(self, strategy: Strategy, depth: int) -> Any:
149
+ summary = strategy.summary
150
+ base_value = self._call_strategy_provider(strategy)
151
+
152
+ item_annotation = summary.item_annotation
153
+ if item_annotation is None or not self._is_model_like(item_annotation):
154
+ return base_value
155
+
156
+ if summary.type == "mapping":
157
+ return self._build_mapping_collection(base_value, item_annotation, depth)
158
+
159
+ length = self._collection_length_from_value(base_value)
160
+ count = max(1, length)
161
+ items: list[Any] = []
162
+ for _ in range(count):
163
+ nested = self._build_model_instance(item_annotation, depth=depth + 1)
164
+ if nested is not None:
165
+ items.append(nested)
166
+
167
+ if summary.type == "list":
168
+ return items
169
+ if summary.type == "tuple":
170
+ return tuple(items)
171
+ if summary.type == "set":
172
+ try:
173
+ return set(items)
174
+ except TypeError:
175
+ return set()
176
+ return base_value
177
+
178
+ def _build_mapping_collection(
179
+ self,
180
+ base_value: Any,
181
+ annotation: Any,
182
+ depth: int,
183
+ ) -> dict[str, Any]:
184
+ if isinstance(base_value, dict) and base_value:
185
+ keys: Iterable[str] = base_value.keys()
186
+ else:
187
+ length = self._collection_length_from_value(base_value)
188
+ count = max(1, length)
189
+ keys = (self.faker.pystr(min_chars=3, max_chars=6) for _ in range(count))
190
+
191
+ result: dict[str, Any] = {}
192
+ for key in keys:
193
+ nested = self._build_model_instance(annotation, depth=depth + 1)
194
+ if nested is not None:
195
+ result[str(key)] = nested
196
+ return result
197
+
198
+ def _consume_object(self) -> bool:
199
+ if getattr(self, "_objects_remaining", 0) <= 0:
200
+ return False
201
+ self._objects_remaining -= 1
202
+ return True
203
+
204
+ def _get_model_strategies(self, model_type: type[Any]) -> dict[str, StrategyResult]:
205
+ cached = self._strategy_cache.get(model_type)
206
+ if cached is not None:
207
+ return cached
208
+
209
+ if isinstance(model_type, type) and issubclass(model_type, BaseModel):
210
+ strategies = dict(self.builder.build_model_strategies(model_type))
211
+ elif is_dataclass(model_type):
212
+ strategies = self._build_dataclass_strategies(model_type)
213
+ else:
214
+ raise TypeError(f"Unsupported model type: {model_type!r}")
215
+
216
+ self._strategy_cache[model_type] = strategies
217
+ return strategies
218
+
219
+ def _build_dataclass_strategies(self, cls: type[Any]) -> dict[str, StrategyResult]:
220
+ strategies: dict[str, StrategyResult] = {}
221
+ type_hints = get_type_hints(cls)
222
+ for field in dataclass_fields(cls):
223
+ if not field.init:
224
+ continue
225
+ annotation = type_hints.get(field.name, field.type)
226
+ summary = self._summarize_dataclass_field(field, annotation)
227
+ strategies[field.name] = self.builder.build_field_strategy(
228
+ cls,
229
+ field.name,
230
+ annotation,
231
+ summary,
232
+ )
233
+ return strategies
234
+
235
+ def _summarize_dataclass_field(
236
+ self,
237
+ field: dataclasses.Field[Any],
238
+ annotation: Any,
239
+ ) -> FieldSummary:
240
+ field_info = self._extract_field_info(field)
241
+ if field_info is not None:
242
+ constraints = extract_constraints(field_info)
243
+ else:
244
+ constraints = FieldConstraints()
245
+ return schema_module._summarize_annotation(annotation, constraints)
246
+
247
+ @staticmethod
248
+ def _extract_field_info(field: dataclasses.Field[Any]) -> FieldInfo | None:
249
+ for meta in getattr(field, "metadata", ()):
250
+ if isinstance(meta, FieldInfo):
251
+ return meta
252
+ return None
253
+
254
+ def _should_return_none(self, strategy: Strategy) -> bool:
255
+ if strategy.p_none <= 0:
256
+ return False
257
+ return self.random.random() < strategy.p_none
258
+
259
+ def _select_enum_value(self, strategy: Strategy, enum_values: list[Any]) -> Any:
260
+ if not enum_values:
261
+ return None
262
+
263
+ policy = strategy.enum_policy or self.config.enum_policy
264
+ selection = self.random.choice(enum_values) if policy == "random" else enum_values[0]
265
+
266
+ annotation = strategy.annotation
267
+ if isinstance(annotation, type) and issubclass(annotation, enum.Enum):
268
+ try:
269
+ return annotation(selection)
270
+ except Exception:
271
+ return selection
272
+ return selection
273
+
274
+ @staticmethod
275
+ def _collection_length_from_value(value: Any) -> int:
276
+ if value is None:
277
+ return 0
278
+ if isinstance(value, Sized):
279
+ return len(value)
280
+ return 0
281
+
282
+ @staticmethod
283
+ def _is_model_like(annotation: Any) -> bool:
284
+ if not isinstance(annotation, type):
285
+ return False
286
+ try:
287
+ return issubclass(annotation, BaseModel) or is_dataclass(annotation)
288
+ except TypeError:
289
+ return False
290
+
291
+ def _call_strategy_provider(self, strategy: Strategy) -> Any:
292
+ if strategy.provider_ref is None:
293
+ return None
294
+
295
+ func = strategy.provider_ref.func
296
+ kwargs = {
297
+ "summary": strategy.summary,
298
+ "faker": self.faker,
299
+ "random_generator": self.random,
300
+ }
301
+ kwargs.update(strategy.provider_kwargs)
302
+
303
+ sig = inspect.signature(func)
304
+ applicable = {name: value for name, value in kwargs.items() if name in sig.parameters}
305
+ try:
306
+ return func(**applicable)
307
+ except Exception:
308
+ return None
309
+
310
+
311
+ __all__ = ["InstanceGenerator", "GenerationConfig"]
@@ -0,0 +1,141 @@
1
+ """High-level discovery orchestration combining AST and safe import approaches."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ from collections.abc import Iterable, Sequence
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Literal
10
+
11
+ from .ast_discover import AstModel
12
+ from .ast_discover import discover_models as ast_discover
13
+ from .safe_import import safe_import_models
14
+
15
+ DiscoveryMethod = Literal["ast", "import", "hybrid"]
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class IntrospectedModel:
20
+ module: str
21
+ name: str
22
+ qualname: str
23
+ locator: str
24
+ lineno: int | None
25
+ discovery: DiscoveryMethod
26
+ is_public: bool
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class IntrospectionResult:
31
+ models: list[IntrospectedModel]
32
+ warnings: list[str]
33
+ errors: list[str]
34
+
35
+
36
+ def discover(
37
+ paths: Iterable[Path | str],
38
+ *,
39
+ method: DiscoveryMethod = "hybrid",
40
+ include: Sequence[str] | None = None,
41
+ exclude: Sequence[str] | None = None,
42
+ public_only: bool = False,
43
+ safe_import_timeout: float = 5.0,
44
+ safe_import_memory_limit_mb: int = 256,
45
+ ) -> IntrospectionResult:
46
+ """Discover Pydantic models using AST, safe import, or both."""
47
+ include_patterns = tuple(include or [])
48
+ exclude_patterns = tuple(exclude or [])
49
+ normalized_paths = [Path(path) for path in paths]
50
+
51
+ models: dict[str, IntrospectedModel] = {}
52
+ warnings: list[str] = []
53
+ errors: list[str] = []
54
+
55
+ if method in {"ast", "hybrid"}:
56
+ ast_result = ast_discover(
57
+ normalized_paths,
58
+ infer_module=True,
59
+ public_only=public_only,
60
+ )
61
+ warnings.extend(ast_result.warnings)
62
+
63
+ for ast_model in ast_result.models:
64
+ entry = _to_introspection_model_from_ast(ast_model)
65
+ if _should_include(entry, include_patterns, exclude_patterns):
66
+ models.setdefault(entry.qualname, entry)
67
+
68
+ if method == "ast":
69
+ return IntrospectionResult(
70
+ models=sorted(models.values(), key=lambda m: m.qualname),
71
+ warnings=warnings,
72
+ errors=errors,
73
+ )
74
+
75
+ if method in {"import", "hybrid"}:
76
+ safe_result = safe_import_models(
77
+ normalized_paths,
78
+ timeout=safe_import_timeout,
79
+ memory_limit_mb=safe_import_memory_limit_mb,
80
+ )
81
+ if not safe_result.success and safe_result.error:
82
+ errors.append(safe_result.error)
83
+ if safe_result.stderr:
84
+ warnings.append(safe_result.stderr.strip())
85
+
86
+ for import_model in safe_result.models:
87
+ entry = _to_introspection_model_from_import(import_model)
88
+ if public_only and entry.name.startswith("_"):
89
+ continue
90
+ if _should_include(entry, include_patterns, exclude_patterns):
91
+ models[entry.qualname] = entry
92
+
93
+ return IntrospectionResult(
94
+ models=sorted(models.values(), key=lambda m: m.qualname),
95
+ warnings=warnings,
96
+ errors=errors,
97
+ )
98
+
99
+
100
+ def _should_include(
101
+ model: IntrospectedModel,
102
+ include_patterns: Sequence[str],
103
+ exclude_patterns: Sequence[str],
104
+ ) -> bool:
105
+ identifier = model.qualname
106
+ if include_patterns and not any(
107
+ fnmatch.fnmatchcase(identifier, pattern) for pattern in include_patterns
108
+ ):
109
+ return False
110
+ return not (
111
+ exclude_patterns
112
+ and any(fnmatch.fnmatchcase(identifier, pattern) for pattern in exclude_patterns)
113
+ )
114
+
115
+
116
+ def _to_introspection_model_from_ast(model: AstModel) -> IntrospectedModel:
117
+ return IntrospectedModel(
118
+ module=model.module,
119
+ name=model.name,
120
+ qualname=model.qualname,
121
+ locator=str(model.path),
122
+ lineno=model.lineno,
123
+ discovery="ast",
124
+ is_public=model.is_public,
125
+ )
126
+
127
+
128
+ def _to_introspection_model_from_import(model: dict[str, object]) -> IntrospectedModel:
129
+ module = str(model.get("module"))
130
+ name = str(model.get("name"))
131
+ qualname = str(model.get("qualname") or f"{module}.{name}")
132
+ locator = str(model.get("path") or module)
133
+ return IntrospectedModel(
134
+ module=module,
135
+ name=name,
136
+ qualname=qualname,
137
+ locator=locator,
138
+ lineno=None,
139
+ discovery="import",
140
+ is_public=not name.startswith("_"),
141
+ )
@@ -0,0 +1,77 @@
1
+ """Filesystem utilities for atomic, hash-aware writes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import tempfile
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+ __all__ = ["WriteResult", "write_atomic_text", "write_atomic_bytes"]
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class WriteResult:
16
+ """Metadata describing the outcome of an atomic write."""
17
+
18
+ path: Path
19
+ wrote: bool
20
+ skipped: bool
21
+ reason: str | None = None
22
+
23
+
24
+ def write_atomic_text(
25
+ path: str | Path,
26
+ content: str,
27
+ *,
28
+ encoding: str = "utf-8",
29
+ hash_compare: bool = False,
30
+ ) -> WriteResult:
31
+ """Atomically write text content to ``path``."""
32
+
33
+ data = content.encode(encoding)
34
+ return _write_atomic(Path(path), data, hash_compare=hash_compare)
35
+
36
+
37
+ def write_atomic_bytes(
38
+ path: str | Path,
39
+ data: bytes,
40
+ *,
41
+ hash_compare: bool = False,
42
+ ) -> WriteResult:
43
+ """Atomically write binary content to ``path``."""
44
+
45
+ return _write_atomic(Path(path), data, hash_compare=hash_compare)
46
+
47
+
48
+ def _write_atomic(path: Path, data: bytes, *, hash_compare: bool) -> WriteResult:
49
+ path = path.expanduser()
50
+ path.parent.mkdir(parents=True, exist_ok=True)
51
+
52
+ digest = _hash_bytes(data) if hash_compare else None
53
+ if hash_compare and path.exists():
54
+ existing = path.read_bytes()
55
+ if _hash_bytes(existing) == digest:
56
+ return WriteResult(path=path, wrote=False, skipped=True, reason="unchanged")
57
+
58
+ with tempfile.NamedTemporaryFile(
59
+ delete=False,
60
+ dir=path.parent,
61
+ prefix=f".{path.name}.",
62
+ suffix=".tmp",
63
+ ) as temp_file:
64
+ temp_path = Path(temp_file.name)
65
+
66
+ try:
67
+ temp_path.write_bytes(data)
68
+ os.replace(temp_path, path)
69
+ except Exception as exc:
70
+ temp_path.unlink(missing_ok=True)
71
+ raise exc
72
+
73
+ return WriteResult(path=path, wrote=True, skipped=False, reason=None)
74
+
75
+
76
+ def _hash_bytes(data: bytes) -> str:
77
+ return hashlib.sha256(data).hexdigest()
@@ -0,0 +1,32 @@
1
+ """Provider registry and built-in providers."""
2
+
3
+ from .collections import register_collection_providers
4
+ from .identifiers import register_identifier_providers
5
+ from .numbers import register_numeric_providers
6
+ from .registry import ProviderRef, ProviderRegistry
7
+ from .strings import register_string_providers
8
+ from .temporal import register_temporal_providers
9
+
10
+
11
+ def create_default_registry(load_plugins: bool = True) -> ProviderRegistry:
12
+ registry = ProviderRegistry()
13
+ register_numeric_providers(registry)
14
+ register_string_providers(registry)
15
+ register_collection_providers(registry)
16
+ register_temporal_providers(registry)
17
+ register_identifier_providers(registry)
18
+ if load_plugins:
19
+ registry.load_entrypoint_plugins()
20
+ return registry
21
+
22
+
23
+ __all__ = [
24
+ "ProviderRef",
25
+ "ProviderRegistry",
26
+ "create_default_registry",
27
+ "register_string_providers",
28
+ "register_numeric_providers",
29
+ "register_collection_providers",
30
+ "register_temporal_providers",
31
+ "register_identifier_providers",
32
+ ]
@@ -0,0 +1,74 @@
1
+ """Collection providers for lists, sets, tuples, and mappings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
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 FieldConstraints, FieldSummary
12
+
13
+ DEFAULT_COLLECTION_SIZE = 3
14
+
15
+
16
+ def generate_collection(
17
+ summary: FieldSummary,
18
+ *,
19
+ faker: Faker | None = None,
20
+ random_generator: random.Random | None = None,
21
+ ) -> Any:
22
+ rng = random_generator or random.Random()
23
+ faker = faker or Faker()
24
+
25
+ length = _collection_length(summary.constraints, rng)
26
+ item_values = [_basic_value(summary.item_type, faker, rng) for _ in range(length)]
27
+
28
+ if summary.type == "list":
29
+ return item_values
30
+ if summary.type == "set":
31
+ return set(item_values)
32
+ if summary.type == "tuple":
33
+ return tuple(item_values)
34
+ if summary.type == "mapping":
35
+ return {faker.pystr(min_chars=3, max_chars=6): value for value in item_values}
36
+
37
+ raise ValueError(f"Unsupported collection type: {summary.type}")
38
+
39
+
40
+ def register_collection_providers(registry: ProviderRegistry) -> None:
41
+ for collection_type in ("list", "set", "tuple", "mapping"):
42
+ registry.register(
43
+ collection_type,
44
+ generate_collection,
45
+ name=f"collection.{collection_type}",
46
+ metadata={"type": collection_type},
47
+ )
48
+
49
+
50
+ def _collection_length(constraints: FieldConstraints, rng: random.Random) -> int:
51
+ minimum = constraints.min_length or 1
52
+ maximum = constraints.max_length or max(minimum, DEFAULT_COLLECTION_SIZE)
53
+ if minimum > maximum:
54
+ minimum = maximum
55
+ return rng.randint(minimum, maximum)
56
+
57
+
58
+ def _basic_value(item_type: str | None, faker: Faker, rng: random.Random) -> Any:
59
+ if item_type == "int":
60
+ return rng.randint(-10, 10)
61
+ if item_type == "float":
62
+ return rng.uniform(-10, 10)
63
+ if item_type == "bool":
64
+ return rng.choice([True, False])
65
+ if item_type == "string" or item_type is None:
66
+ return faker.pystr(min_chars=1, max_chars=8)
67
+ if item_type == "decimal":
68
+ return faker.pydecimal(left_digits=3, right_digits=2)
69
+ if item_type == "model":
70
+ return {}
71
+ return faker.pystr(min_chars=1, max_chars=8)
72
+
73
+
74
+ __all__ = ["generate_collection", "register_collection_providers"]
@@ -0,0 +1,68 @@
1
+ """Identifier providers for emails, URLs, UUIDs, IPs, secrets, and payment cards."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import uuid
7
+ from typing import Any
8
+
9
+ from faker import Faker
10
+ from pydantic import SecretBytes, SecretStr
11
+
12
+ from pydantic_fixturegen.core.providers.registry import ProviderRegistry
13
+ from pydantic_fixturegen.core.schema import FieldSummary
14
+
15
+
16
+ def generate_identifier(
17
+ summary: FieldSummary,
18
+ *,
19
+ faker: Faker | None = None,
20
+ ) -> Any:
21
+ faker = faker or Faker()
22
+ type_name = summary.type
23
+
24
+ if type_name == "email":
25
+ return faker.unique.email()
26
+ if type_name == "url":
27
+ return faker.unique.url()
28
+ if type_name == "uuid":
29
+ return uuid.uuid4()
30
+ if type_name == "payment-card":
31
+ return faker.credit_card_number()
32
+ if type_name == "secret-str":
33
+ return SecretStr(faker.password())
34
+ if type_name == "secret-bytes":
35
+ return SecretBytes(faker.binary(length=16))
36
+ if type_name == "ip-address":
37
+ return faker.ipv4()
38
+ if type_name == "ip-interface":
39
+ address = faker.ipv4()
40
+ return str(ipaddress.ip_interface(f"{address}/24"))
41
+ if type_name == "ip-network":
42
+ address = faker.ipv4()
43
+ return str(ipaddress.ip_network(f"{address}/24", strict=False))
44
+
45
+ raise ValueError(f"Unsupported identifier type: {type_name}")
46
+
47
+
48
+ def register_identifier_providers(registry: ProviderRegistry) -> None:
49
+ for identifier_type in (
50
+ "email",
51
+ "url",
52
+ "uuid",
53
+ "payment-card",
54
+ "secret-str",
55
+ "secret-bytes",
56
+ "ip-address",
57
+ "ip-interface",
58
+ "ip-network",
59
+ ):
60
+ registry.register(
61
+ identifier_type,
62
+ generate_identifier,
63
+ name=f"identifier.{identifier_type}",
64
+ metadata={"type": identifier_type},
65
+ )
66
+
67
+
68
+ __all__ = ["generate_identifier", "register_identifier_providers"]