pydantic-fixturegen 1.0.0__py3-none-any.whl → 1.1.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/api/__init__.py +137 -0
- pydantic_fixturegen/api/_runtime.py +726 -0
- pydantic_fixturegen/api/models.py +73 -0
- pydantic_fixturegen/cli/__init__.py +32 -1
- pydantic_fixturegen/cli/check.py +230 -0
- pydantic_fixturegen/cli/diff.py +992 -0
- pydantic_fixturegen/cli/doctor.py +188 -35
- pydantic_fixturegen/cli/gen/_common.py +134 -7
- pydantic_fixturegen/cli/gen/explain.py +597 -40
- pydantic_fixturegen/cli/gen/fixtures.py +244 -112
- pydantic_fixturegen/cli/gen/json.py +229 -138
- pydantic_fixturegen/cli/gen/schema.py +170 -85
- pydantic_fixturegen/cli/init.py +333 -0
- pydantic_fixturegen/cli/schema.py +45 -0
- pydantic_fixturegen/cli/watch.py +126 -0
- pydantic_fixturegen/core/config.py +137 -3
- pydantic_fixturegen/core/config_schema.py +178 -0
- pydantic_fixturegen/core/constraint_report.py +305 -0
- pydantic_fixturegen/core/errors.py +42 -0
- pydantic_fixturegen/core/field_policies.py +100 -0
- pydantic_fixturegen/core/generate.py +241 -37
- pydantic_fixturegen/core/io_utils.py +10 -2
- pydantic_fixturegen/core/path_template.py +197 -0
- pydantic_fixturegen/core/presets.py +73 -0
- pydantic_fixturegen/core/providers/temporal.py +10 -0
- pydantic_fixturegen/core/safe_import.py +146 -12
- pydantic_fixturegen/core/seed_freeze.py +176 -0
- pydantic_fixturegen/emitters/json_out.py +65 -16
- pydantic_fixturegen/emitters/pytest_codegen.py +68 -13
- pydantic_fixturegen/emitters/schema_out.py +27 -3
- pydantic_fixturegen/logging.py +114 -0
- pydantic_fixturegen/schemas/config.schema.json +244 -0
- pydantic_fixturegen-1.1.0.dist-info/METADATA +173 -0
- pydantic_fixturegen-1.1.0.dist-info/RECORD +57 -0
- pydantic_fixturegen-1.0.0.dist-info/METADATA +0 -280
- pydantic_fixturegen-1.0.0.dist-info/RECORD +0 -41
- {pydantic_fixturegen-1.0.0.dist-info → pydantic_fixturegen-1.1.0.dist-info}/WHEEL +0 -0
- {pydantic_fixturegen-1.0.0.dist-info → pydantic_fixturegen-1.1.0.dist-info}/entry_points.txt +0 -0
- {pydantic_fixturegen-1.0.0.dist-info → pydantic_fixturegen-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Helpers for implementing watch mode across CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from pydantic_fixturegen.core.errors import WatchError
|
|
10
|
+
from pydantic_fixturegen.logging import get_logger
|
|
11
|
+
|
|
12
|
+
_CONFIG_FILENAMES = (
|
|
13
|
+
Path("pyproject.toml"),
|
|
14
|
+
Path("pydantic-fixturegen.yaml"),
|
|
15
|
+
Path("pydantic-fixturegen.yml"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def gather_default_watch_paths(
|
|
20
|
+
target: Path,
|
|
21
|
+
*,
|
|
22
|
+
output: Path | None = None,
|
|
23
|
+
extra: Iterable[Path] | None = None,
|
|
24
|
+
) -> list[Path]:
|
|
25
|
+
"""Collect filesystem locations to monitor for change events."""
|
|
26
|
+
|
|
27
|
+
paths: set[Path] = set()
|
|
28
|
+
target = target.resolve()
|
|
29
|
+
paths.add(target)
|
|
30
|
+
paths.add(target.parent)
|
|
31
|
+
|
|
32
|
+
cwd = Path.cwd()
|
|
33
|
+
for candidate in _CONFIG_FILENAMES:
|
|
34
|
+
path = (cwd / candidate).resolve()
|
|
35
|
+
if path.exists():
|
|
36
|
+
paths.add(path)
|
|
37
|
+
|
|
38
|
+
if output is not None:
|
|
39
|
+
out_path = output.resolve()
|
|
40
|
+
if out_path.exists():
|
|
41
|
+
paths.add(out_path)
|
|
42
|
+
parent = out_path.parent
|
|
43
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
paths.add(parent)
|
|
45
|
+
|
|
46
|
+
if extra:
|
|
47
|
+
for candidate in extra:
|
|
48
|
+
resolved = candidate.resolve()
|
|
49
|
+
if resolved.exists():
|
|
50
|
+
paths.add(resolved)
|
|
51
|
+
parent = resolved.parent
|
|
52
|
+
if parent.exists():
|
|
53
|
+
paths.add(parent)
|
|
54
|
+
|
|
55
|
+
normalized = _normalize_watch_paths(paths)
|
|
56
|
+
if not normalized:
|
|
57
|
+
raise WatchError("No valid paths available for watch mode.")
|
|
58
|
+
return normalized
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
WatchIterator = Iterator[set[tuple[int, Path]]]
|
|
62
|
+
WatchBackend = Callable[..., WatchIterator]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_with_watch(
|
|
66
|
+
run_once: Callable[[], None],
|
|
67
|
+
watch_paths: Iterable[Path],
|
|
68
|
+
*,
|
|
69
|
+
debounce: float,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Execute ``run_once`` immediately and re-run when filesystem changes occur."""
|
|
72
|
+
|
|
73
|
+
watch_fn: WatchBackend = _import_watch_backend()
|
|
74
|
+
normalized = _normalize_watch_paths(watch_paths)
|
|
75
|
+
if not normalized:
|
|
76
|
+
raise WatchError("No valid paths available for watch mode.")
|
|
77
|
+
|
|
78
|
+
run_once()
|
|
79
|
+
logger = get_logger()
|
|
80
|
+
logger.info(
|
|
81
|
+
"Watch mode active. Press Ctrl+C to stop.",
|
|
82
|
+
event="watch_started",
|
|
83
|
+
paths=[str(path) for path in normalized],
|
|
84
|
+
debounce=debounce,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
for changes in watch_fn(*normalized, debounce=debounce):
|
|
89
|
+
changed_paths = sorted({str(path) for _, path in changes})
|
|
90
|
+
logger.info(
|
|
91
|
+
"Detected changes",
|
|
92
|
+
event="watch_change_detected",
|
|
93
|
+
paths=changed_paths,
|
|
94
|
+
)
|
|
95
|
+
run_once()
|
|
96
|
+
except KeyboardInterrupt:
|
|
97
|
+
logger.warn("Watch mode stopped.", event="watch_stopped")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _normalize_watch_paths(paths: Iterable[Path]) -> list[Path]:
|
|
101
|
+
normalized: list[Path] = []
|
|
102
|
+
seen: set[Path] = set()
|
|
103
|
+
|
|
104
|
+
for path in paths:
|
|
105
|
+
candidate = path.resolve()
|
|
106
|
+
monitor = candidate if candidate.exists() else candidate.parent
|
|
107
|
+
if not monitor.exists():
|
|
108
|
+
continue
|
|
109
|
+
if monitor not in seen:
|
|
110
|
+
normalized.append(monitor)
|
|
111
|
+
seen.add(monitor)
|
|
112
|
+
return normalized
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _import_watch_backend() -> WatchBackend:
|
|
116
|
+
try:
|
|
117
|
+
from watchfiles import watch as watch_fn
|
|
118
|
+
except ModuleNotFoundError as exc: # pragma: no cover - depends on optional extra
|
|
119
|
+
raise WatchError(
|
|
120
|
+
"Watch mode requires the optional 'watchfiles' dependency. Install it via"
|
|
121
|
+
" `pip install pydantic-fixturegen[watch]`."
|
|
122
|
+
) from exc
|
|
123
|
+
return cast(WatchBackend, watch_fn)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ["gather_default_watch_paths", "run_with_watch"]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import datetime
|
|
5
6
|
import os
|
|
6
7
|
from collections.abc import Mapping, MutableMapping, Sequence
|
|
7
8
|
from dataclasses import dataclass, field, replace
|
|
@@ -9,6 +10,8 @@ from importlib import import_module
|
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, TypeVar, cast
|
|
11
12
|
|
|
13
|
+
from .field_policies import FieldPolicy
|
|
14
|
+
from .presets import get_preset_spec, normalize_preset_name
|
|
12
15
|
from .seed import DEFAULT_LOCALE
|
|
13
16
|
|
|
14
17
|
|
|
@@ -62,6 +65,7 @@ class EmittersConfig:
|
|
|
62
65
|
|
|
63
66
|
@dataclass(frozen=True)
|
|
64
67
|
class AppConfig:
|
|
68
|
+
preset: str | None = None
|
|
65
69
|
seed: int | str | None = None
|
|
66
70
|
locale: str = DEFAULT_LOCALE
|
|
67
71
|
include: tuple[str, ...] = ()
|
|
@@ -69,7 +73,9 @@ class AppConfig:
|
|
|
69
73
|
p_none: float | None = None
|
|
70
74
|
union_policy: str = "first"
|
|
71
75
|
enum_policy: str = "first"
|
|
76
|
+
now: datetime.datetime | None = None
|
|
72
77
|
overrides: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
|
|
78
|
+
field_policies: tuple[FieldPolicy, ...] = ()
|
|
73
79
|
emitters: EmittersConfig = field(default_factory=EmittersConfig)
|
|
74
80
|
json: JsonConfig = field(default_factory=JsonConfig)
|
|
75
81
|
|
|
@@ -96,19 +102,20 @@ def load_config(
|
|
|
96
102
|
_deep_merge(data, _config_defaults_dict())
|
|
97
103
|
|
|
98
104
|
file_config = _load_file_config(pyproject, yaml_file)
|
|
99
|
-
|
|
105
|
+
_merge_source_with_preset(data, file_config)
|
|
100
106
|
|
|
101
107
|
env_config = _load_env_config(env or os.environ)
|
|
102
|
-
|
|
108
|
+
_merge_source_with_preset(data, env_config)
|
|
103
109
|
|
|
104
110
|
if cli:
|
|
105
|
-
|
|
111
|
+
_merge_source_with_preset(data, cli)
|
|
106
112
|
|
|
107
113
|
return _build_app_config(data)
|
|
108
114
|
|
|
109
115
|
|
|
110
116
|
def _config_defaults_dict() -> dict[str, Any]:
|
|
111
117
|
return {
|
|
118
|
+
"preset": DEFAULT_CONFIG.preset,
|
|
112
119
|
"seed": DEFAULT_CONFIG.seed,
|
|
113
120
|
"locale": DEFAULT_CONFIG.locale,
|
|
114
121
|
"include": list(DEFAULT_CONFIG.include),
|
|
@@ -116,6 +123,8 @@ def _config_defaults_dict() -> dict[str, Any]:
|
|
|
116
123
|
"p_none": DEFAULT_CONFIG.p_none,
|
|
117
124
|
"union_policy": DEFAULT_CONFIG.union_policy,
|
|
118
125
|
"enum_policy": DEFAULT_CONFIG.enum_policy,
|
|
126
|
+
"now": DEFAULT_CONFIG.now,
|
|
127
|
+
"field_policies": {},
|
|
119
128
|
"overrides": {},
|
|
120
129
|
"emitters": {
|
|
121
130
|
"pytest": {
|
|
@@ -233,6 +242,8 @@ def _coerce_env_value(value: str) -> Any:
|
|
|
233
242
|
|
|
234
243
|
|
|
235
244
|
def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
|
|
245
|
+
preset_value = _coerce_preset_value(data.get("preset"))
|
|
246
|
+
|
|
236
247
|
seed = data.get("seed")
|
|
237
248
|
locale = _coerce_str(data.get("locale"), "locale")
|
|
238
249
|
include = _normalize_sequence(data.get("include"))
|
|
@@ -257,6 +268,8 @@ def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
|
|
|
257
268
|
|
|
258
269
|
emitters_value = _normalize_emitters(data.get("emitters"))
|
|
259
270
|
json_value = _normalize_json(data.get("json"))
|
|
271
|
+
field_policies_value = _normalize_field_policies(data.get("field_policies"))
|
|
272
|
+
now_value = _coerce_datetime(data.get("now"), "now")
|
|
260
273
|
|
|
261
274
|
seed_value: int | str | None
|
|
262
275
|
if isinstance(seed, (int, str)) or seed is None:
|
|
@@ -265,6 +278,7 @@ def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
|
|
|
265
278
|
raise ConfigError("seed must be an int, str, or null.")
|
|
266
279
|
|
|
267
280
|
config = AppConfig(
|
|
281
|
+
preset=preset_value,
|
|
268
282
|
seed=seed_value,
|
|
269
283
|
locale=locale,
|
|
270
284
|
include=include,
|
|
@@ -272,7 +286,9 @@ def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
|
|
|
272
286
|
p_none=p_none_value,
|
|
273
287
|
union_policy=union_policy,
|
|
274
288
|
enum_policy=enum_policy,
|
|
289
|
+
now=now_value,
|
|
275
290
|
overrides=overrides_value,
|
|
291
|
+
field_policies=field_policies_value,
|
|
276
292
|
emitters=emitters_value,
|
|
277
293
|
json=json_value,
|
|
278
294
|
)
|
|
@@ -315,6 +331,30 @@ def _coerce_policy(value: Any, allowed: set[str], field_name: str) -> str:
|
|
|
315
331
|
return value
|
|
316
332
|
|
|
317
333
|
|
|
334
|
+
def _coerce_datetime(value: Any, field_name: str) -> datetime.datetime | None:
|
|
335
|
+
if value is None:
|
|
336
|
+
return None
|
|
337
|
+
if isinstance(value, datetime.datetime):
|
|
338
|
+
if value.tzinfo is None:
|
|
339
|
+
return value.replace(tzinfo=datetime.timezone.utc)
|
|
340
|
+
return value
|
|
341
|
+
if isinstance(value, datetime.date):
|
|
342
|
+
return datetime.datetime.combine(value, datetime.time(), tzinfo=datetime.timezone.utc)
|
|
343
|
+
if isinstance(value, str):
|
|
344
|
+
text = value.strip()
|
|
345
|
+
if not text or text.lower() == "none":
|
|
346
|
+
return None
|
|
347
|
+
normalized = text.replace("Z", "+00:00")
|
|
348
|
+
try:
|
|
349
|
+
parsed = datetime.datetime.fromisoformat(normalized)
|
|
350
|
+
except ValueError as exc:
|
|
351
|
+
raise ConfigError(f"{field_name} must be an ISO 8601 datetime string.") from exc
|
|
352
|
+
if parsed.tzinfo is None:
|
|
353
|
+
parsed = parsed.replace(tzinfo=datetime.timezone.utc)
|
|
354
|
+
return parsed
|
|
355
|
+
raise ConfigError(f"{field_name} must be an ISO 8601 datetime string or datetime object.")
|
|
356
|
+
|
|
357
|
+
|
|
318
358
|
def _normalize_overrides(value: Any) -> Mapping[str, Mapping[str, Any]]:
|
|
319
359
|
if value is None:
|
|
320
360
|
return {}
|
|
@@ -335,6 +375,59 @@ def _normalize_overrides(value: Any) -> Mapping[str, Mapping[str, Any]]:
|
|
|
335
375
|
return overrides
|
|
336
376
|
|
|
337
377
|
|
|
378
|
+
def _normalize_field_policies(value: Any) -> tuple[FieldPolicy, ...]:
|
|
379
|
+
if value is None:
|
|
380
|
+
return ()
|
|
381
|
+
if not isinstance(value, Mapping):
|
|
382
|
+
raise ConfigError("field_policies must be a mapping of pattern to policy settings.")
|
|
383
|
+
|
|
384
|
+
policies: list[FieldPolicy] = []
|
|
385
|
+
for index, (pattern, raw_options) in enumerate(value.items()):
|
|
386
|
+
if not isinstance(pattern, str) or not pattern.strip():
|
|
387
|
+
raise ConfigError("field policy keys must be non-empty strings.")
|
|
388
|
+
if not isinstance(raw_options, Mapping):
|
|
389
|
+
raise ConfigError(f"Field policy '{pattern}' must be a mapping of options.")
|
|
390
|
+
|
|
391
|
+
p_none = raw_options.get("p_none")
|
|
392
|
+
if p_none is not None:
|
|
393
|
+
try:
|
|
394
|
+
p_none = float(p_none)
|
|
395
|
+
except (TypeError, ValueError) as exc:
|
|
396
|
+
raise ConfigError(
|
|
397
|
+
f"Field policy '{pattern}' p_none must be a float value."
|
|
398
|
+
) from exc
|
|
399
|
+
if not (0.0 <= p_none <= 1.0):
|
|
400
|
+
raise ConfigError(f"Field policy '{pattern}' p_none must be between 0.0 and 1.0.")
|
|
401
|
+
|
|
402
|
+
enum_policy = raw_options.get("enum_policy")
|
|
403
|
+
if enum_policy is not None and (
|
|
404
|
+
not isinstance(enum_policy, str) or enum_policy not in ENUM_POLICIES
|
|
405
|
+
):
|
|
406
|
+
raise ConfigError(
|
|
407
|
+
f"Field policy '{pattern}' enum_policy must be one of {sorted(ENUM_POLICIES)}."
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
union_policy = raw_options.get("union_policy")
|
|
411
|
+
if union_policy is not None and (
|
|
412
|
+
not isinstance(union_policy, str) or union_policy not in UNION_POLICIES
|
|
413
|
+
):
|
|
414
|
+
raise ConfigError(
|
|
415
|
+
f"Field policy '{pattern}' union_policy must be one of {sorted(UNION_POLICIES)}."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
allowed_keys = {"p_none", "enum_policy", "union_policy"}
|
|
419
|
+
for option_key in raw_options:
|
|
420
|
+
if option_key not in allowed_keys:
|
|
421
|
+
raise ConfigError(
|
|
422
|
+
f"Field policy '{pattern}' contains unsupported option '{option_key}'."
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
options = {"p_none": p_none, "enum_policy": enum_policy, "union_policy": union_policy}
|
|
426
|
+
policies.append(FieldPolicy(pattern=pattern, options=options, index=index))
|
|
427
|
+
|
|
428
|
+
return tuple(policies)
|
|
429
|
+
|
|
430
|
+
|
|
338
431
|
def _normalize_emitters(value: Any) -> EmittersConfig:
|
|
339
432
|
pytest_config = PytestEmitterConfig()
|
|
340
433
|
|
|
@@ -409,6 +502,22 @@ def _coerce_optional_str(value: Any, field_name: str) -> str:
|
|
|
409
502
|
return value
|
|
410
503
|
|
|
411
504
|
|
|
505
|
+
def _coerce_preset_value(value: Any) -> str | None:
|
|
506
|
+
if value is None:
|
|
507
|
+
return None
|
|
508
|
+
if not isinstance(value, str):
|
|
509
|
+
raise ConfigError("preset must be a string when specified.")
|
|
510
|
+
stripped = value.strip()
|
|
511
|
+
if not stripped:
|
|
512
|
+
return None
|
|
513
|
+
normalized = normalize_preset_name(stripped)
|
|
514
|
+
try:
|
|
515
|
+
spec = get_preset_spec(normalized)
|
|
516
|
+
except KeyError as exc:
|
|
517
|
+
raise ConfigError(f"Unknown preset '{value}'.") from exc
|
|
518
|
+
return spec.name
|
|
519
|
+
|
|
520
|
+
|
|
412
521
|
def _ensure_mutable(mapping: Mapping[str, Any]) -> dict[str, Any]:
|
|
413
522
|
mutable: dict[str, Any] = {}
|
|
414
523
|
for key, value in mapping.items():
|
|
@@ -438,3 +547,28 @@ def _deep_merge(target: MutableMapping[str, Any], source: Mapping[str, Any]) ->
|
|
|
438
547
|
target[key] = list(value)
|
|
439
548
|
else:
|
|
440
549
|
target[key] = value
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _merge_source_with_preset(data: MutableMapping[str, Any], source: Mapping[str, Any]) -> None:
|
|
553
|
+
if not source:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
mutable = _ensure_mutable(source)
|
|
557
|
+
|
|
558
|
+
if "preset" in mutable:
|
|
559
|
+
preset_raw = mutable.pop("preset")
|
|
560
|
+
if preset_raw is None:
|
|
561
|
+
data["preset"] = None
|
|
562
|
+
else:
|
|
563
|
+
if not isinstance(preset_raw, str):
|
|
564
|
+
raise ConfigError("preset must be a string when specified.")
|
|
565
|
+
preset_name = normalize_preset_name(preset_raw)
|
|
566
|
+
try:
|
|
567
|
+
preset_spec = get_preset_spec(preset_name)
|
|
568
|
+
except KeyError as exc:
|
|
569
|
+
raise ConfigError(f"Unknown preset '{preset_raw}'.") from exc
|
|
570
|
+
|
|
571
|
+
_deep_merge(data, _ensure_mutable(preset_spec.settings))
|
|
572
|
+
data["preset"] = preset_spec.name
|
|
573
|
+
|
|
574
|
+
_deep_merge(data, mutable)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""JSON Schema generation for pydantic-fixturegen configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from .config import DEFAULT_CONFIG
|
|
10
|
+
from .seed import DEFAULT_LOCALE
|
|
11
|
+
|
|
12
|
+
SCHEMA_ID = "https://raw.githubusercontent.com/CasperKristiansson/pydantic-fixturegen/main/pydantic_fixturegen/schemas/config.schema.json"
|
|
13
|
+
SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
UnionPolicyLiteral = Literal["first", "random", "weighted"]
|
|
17
|
+
EnumPolicyLiteral = Literal["first", "random"]
|
|
18
|
+
|
|
19
|
+
DEFAULT_PYTEST_STYLE = cast(
|
|
20
|
+
Literal["functions", "factory", "class"],
|
|
21
|
+
DEFAULT_CONFIG.emitters.pytest.style,
|
|
22
|
+
)
|
|
23
|
+
DEFAULT_PYTEST_SCOPE = cast(
|
|
24
|
+
Literal["function", "module", "session"],
|
|
25
|
+
DEFAULT_CONFIG.emitters.pytest.scope,
|
|
26
|
+
)
|
|
27
|
+
DEFAULT_UNION_POLICY = cast(UnionPolicyLiteral, DEFAULT_CONFIG.union_policy)
|
|
28
|
+
DEFAULT_ENUM_POLICY = cast(EnumPolicyLiteral, DEFAULT_CONFIG.enum_policy)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PytestEmitterSchema(BaseModel):
|
|
32
|
+
"""Schema for pytest emitter settings."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(extra="forbid")
|
|
35
|
+
|
|
36
|
+
style: Literal["functions", "factory", "class"] = Field(
|
|
37
|
+
default=DEFAULT_PYTEST_STYLE,
|
|
38
|
+
description="How emitted pytest fixtures are structured.",
|
|
39
|
+
)
|
|
40
|
+
scope: Literal["function", "module", "session"] = Field(
|
|
41
|
+
default=DEFAULT_PYTEST_SCOPE,
|
|
42
|
+
description="Default pytest fixture scope.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EmittersSchema(BaseModel):
|
|
47
|
+
"""Schema for emitter configuration sections."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="forbid")
|
|
50
|
+
|
|
51
|
+
pytest: PytestEmitterSchema = Field(
|
|
52
|
+
default_factory=PytestEmitterSchema,
|
|
53
|
+
description="Configuration for the built-in pytest fixture emitter.",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class JsonSchema(BaseModel):
|
|
58
|
+
"""Schema for JSON emitter settings."""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="forbid")
|
|
61
|
+
|
|
62
|
+
indent: int = Field(
|
|
63
|
+
default=DEFAULT_CONFIG.json.indent,
|
|
64
|
+
ge=0,
|
|
65
|
+
description="Indentation level for JSON output (0 for compact).",
|
|
66
|
+
)
|
|
67
|
+
orjson: bool = Field(
|
|
68
|
+
default=DEFAULT_CONFIG.json.orjson,
|
|
69
|
+
description="Use orjson for serialization when available.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FieldPolicyOptionsSchema(BaseModel):
|
|
74
|
+
"""Schema describing supported field policy overrides."""
|
|
75
|
+
|
|
76
|
+
model_config = ConfigDict(extra="forbid")
|
|
77
|
+
|
|
78
|
+
p_none: float | None = Field(
|
|
79
|
+
default=None,
|
|
80
|
+
ge=0.0,
|
|
81
|
+
le=1.0,
|
|
82
|
+
description="Probability override for returning None on optional fields.",
|
|
83
|
+
)
|
|
84
|
+
enum_policy: EnumPolicyLiteral | None = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
description="Enum selection policy for matching fields.",
|
|
87
|
+
)
|
|
88
|
+
union_policy: Literal["first", "random"] | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="Union selection policy for matching fields.",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ConfigSchemaModel(BaseModel):
|
|
95
|
+
"""Authoritative schema for `[tool.pydantic_fixturegen]` configuration."""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(
|
|
98
|
+
extra="allow",
|
|
99
|
+
populate_by_name=True,
|
|
100
|
+
title="pydantic-fixturegen configuration",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
preset: str | None = Field(
|
|
104
|
+
default=DEFAULT_CONFIG.preset,
|
|
105
|
+
description="Curated preset name applied before other configuration (e.g., 'boundary').",
|
|
106
|
+
)
|
|
107
|
+
seed: int | str | None = Field(
|
|
108
|
+
default=DEFAULT_CONFIG.seed,
|
|
109
|
+
description="Global seed controlling deterministic generation. Accepts int or string.",
|
|
110
|
+
)
|
|
111
|
+
locale: str = Field(
|
|
112
|
+
default=DEFAULT_LOCALE,
|
|
113
|
+
description="Default Faker locale used when generating data.",
|
|
114
|
+
)
|
|
115
|
+
include: list[str] = Field(
|
|
116
|
+
default_factory=list,
|
|
117
|
+
description="Glob patterns of fully-qualified model names to include by default.",
|
|
118
|
+
)
|
|
119
|
+
exclude: list[str] = Field(
|
|
120
|
+
default_factory=list,
|
|
121
|
+
description="Glob patterns of fully-qualified model names to exclude by default.",
|
|
122
|
+
)
|
|
123
|
+
p_none: float | None = Field(
|
|
124
|
+
default=DEFAULT_CONFIG.p_none,
|
|
125
|
+
ge=0.0,
|
|
126
|
+
le=1.0,
|
|
127
|
+
description="Probability of sampling `None` for optional fields when unspecified.",
|
|
128
|
+
)
|
|
129
|
+
union_policy: UnionPolicyLiteral = Field(
|
|
130
|
+
default=DEFAULT_UNION_POLICY,
|
|
131
|
+
description="Strategy for selecting branches of `typing.Union`.",
|
|
132
|
+
)
|
|
133
|
+
enum_policy: EnumPolicyLiteral = Field(
|
|
134
|
+
default=DEFAULT_ENUM_POLICY,
|
|
135
|
+
description="Strategy for selecting enum members.",
|
|
136
|
+
)
|
|
137
|
+
overrides: dict[str, dict[str, Any]] = Field(
|
|
138
|
+
default_factory=dict,
|
|
139
|
+
description="Per-model overrides keyed by fully-qualified model name.",
|
|
140
|
+
)
|
|
141
|
+
emitters: EmittersSchema = Field(
|
|
142
|
+
default_factory=EmittersSchema,
|
|
143
|
+
description="Emitter-specific configuration sections.",
|
|
144
|
+
)
|
|
145
|
+
json_settings: JsonSchema = Field(
|
|
146
|
+
default_factory=JsonSchema,
|
|
147
|
+
alias="json",
|
|
148
|
+
description="Settings shared by JSON-based emitters.",
|
|
149
|
+
)
|
|
150
|
+
field_policies: dict[str, FieldPolicyOptionsSchema] = Field(
|
|
151
|
+
default_factory=dict,
|
|
152
|
+
description=(
|
|
153
|
+
"Field policy definitions keyed by glob or regex patterns that may target model "
|
|
154
|
+
"names or dotted field paths."
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def build_config_schema() -> dict[str, Any]:
|
|
160
|
+
"""Return the JSON schema describing the project configuration."""
|
|
161
|
+
|
|
162
|
+
schema = ConfigSchemaModel.model_json_schema(by_alias=True)
|
|
163
|
+
schema["$schema"] = SCHEMA_DRAFT
|
|
164
|
+
schema["$id"] = SCHEMA_ID
|
|
165
|
+
schema.setdefault("description", "Configuration options for pydantic-fixturegen")
|
|
166
|
+
return schema
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_config_schema_json(*, indent: int | None = 2) -> str:
|
|
170
|
+
"""Return the configuration schema serialized to JSON."""
|
|
171
|
+
|
|
172
|
+
import json
|
|
173
|
+
|
|
174
|
+
schema = build_config_schema()
|
|
175
|
+
return json.dumps(schema, indent=indent, sort_keys=True) + ("\n" if indent else "")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = ["build_config_schema", "get_config_schema_json", "SCHEMA_ID", "SCHEMA_DRAFT"]
|