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
|
@@ -3,27 +3,18 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Any
|
|
7
6
|
|
|
8
7
|
import typer
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
from pydantic_fixturegen.
|
|
12
|
-
from pydantic_fixturegen.core.
|
|
13
|
-
from pydantic_fixturegen.core.
|
|
14
|
-
from pydantic_fixturegen.core.
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
|
|
19
|
-
from ._common import (
|
|
20
|
-
JSON_ERRORS_OPTION,
|
|
21
|
-
clear_module_cache,
|
|
22
|
-
discover_models,
|
|
23
|
-
load_model_class,
|
|
24
|
-
render_cli_error,
|
|
25
|
-
split_patterns,
|
|
26
|
-
)
|
|
8
|
+
|
|
9
|
+
from pydantic_fixturegen.api._runtime import generate_json_artifacts
|
|
10
|
+
from pydantic_fixturegen.api.models import JsonGenerationResult
|
|
11
|
+
from pydantic_fixturegen.core.config import ConfigError
|
|
12
|
+
from pydantic_fixturegen.core.errors import DiscoveryError, EmitError, PFGError
|
|
13
|
+
from pydantic_fixturegen.core.path_template import OutputTemplate
|
|
14
|
+
|
|
15
|
+
from ...logging import Logger, get_logger
|
|
16
|
+
from ..watch import gather_default_watch_paths, run_with_watch
|
|
17
|
+
from ._common import JSON_ERRORS_OPTION, NOW_OPTION, emit_constraint_summary, render_cli_error
|
|
27
18
|
|
|
28
19
|
TARGET_ARGUMENT = typer.Argument(
|
|
29
20
|
...,
|
|
@@ -91,6 +82,37 @@ SEED_OPTION = typer.Option(
|
|
|
91
82
|
help="Seed override for deterministic generation.",
|
|
92
83
|
)
|
|
93
84
|
|
|
85
|
+
WATCH_OPTION = typer.Option(
|
|
86
|
+
False,
|
|
87
|
+
"--watch",
|
|
88
|
+
help="Watch source files and regenerate when changes are detected.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
WATCH_DEBOUNCE_OPTION = typer.Option(
|
|
92
|
+
0.5,
|
|
93
|
+
"--watch-debounce",
|
|
94
|
+
min=0.1,
|
|
95
|
+
help="Debounce interval in seconds for filesystem events.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
FREEZE_SEEDS_OPTION = typer.Option(
|
|
99
|
+
False,
|
|
100
|
+
"--freeze-seeds/--no-freeze-seeds",
|
|
101
|
+
help="Read/write per-model seeds using a freeze file to ensure deterministic regeneration.",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
FREEZE_FILE_OPTION = typer.Option(
|
|
105
|
+
None,
|
|
106
|
+
"--freeze-seeds-file",
|
|
107
|
+
help="Seed freeze file path (defaults to `.pfg-seeds.json` in the project root).",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
PRESET_OPTION = typer.Option(
|
|
111
|
+
None,
|
|
112
|
+
"--preset",
|
|
113
|
+
help="Apply a curated generation preset (e.g. 'boundary', 'boundary-max').",
|
|
114
|
+
)
|
|
115
|
+
|
|
94
116
|
|
|
95
117
|
def register(app: typer.Typer) -> None:
|
|
96
118
|
@app.command("json")
|
|
@@ -105,33 +127,87 @@ def register(app: typer.Typer) -> None:
|
|
|
105
127
|
include: str | None = INCLUDE_OPTION,
|
|
106
128
|
exclude: str | None = EXCLUDE_OPTION,
|
|
107
129
|
seed: int | None = SEED_OPTION,
|
|
130
|
+
now: str | None = NOW_OPTION,
|
|
108
131
|
json_errors: bool = JSON_ERRORS_OPTION,
|
|
132
|
+
watch: bool = WATCH_OPTION,
|
|
133
|
+
watch_debounce: float = WATCH_DEBOUNCE_OPTION,
|
|
134
|
+
freeze_seeds: bool = FREEZE_SEEDS_OPTION,
|
|
135
|
+
freeze_seeds_file: Path | None = FREEZE_FILE_OPTION,
|
|
136
|
+
preset: str | None = PRESET_OPTION,
|
|
109
137
|
) -> None:
|
|
138
|
+
logger = get_logger()
|
|
139
|
+
|
|
110
140
|
try:
|
|
111
|
-
|
|
112
|
-
target=target,
|
|
113
|
-
out=out,
|
|
114
|
-
count=count,
|
|
115
|
-
jsonl=jsonl,
|
|
116
|
-
indent=indent,
|
|
117
|
-
use_orjson=use_orjson,
|
|
118
|
-
shard_size=shard_size,
|
|
119
|
-
include=include,
|
|
120
|
-
exclude=exclude,
|
|
121
|
-
seed=seed,
|
|
122
|
-
)
|
|
141
|
+
output_template = OutputTemplate(str(out))
|
|
123
142
|
except PFGError as exc:
|
|
124
143
|
render_cli_error(exc, json_errors=json_errors)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
watch_output: Path | None = None
|
|
147
|
+
watch_extra: list[Path] | None = None
|
|
148
|
+
if output_template.has_dynamic_directories():
|
|
149
|
+
watch_extra = [output_template.watch_parent()]
|
|
150
|
+
else:
|
|
151
|
+
watch_output = output_template.preview_path()
|
|
152
|
+
|
|
153
|
+
def invoke(exit_app: bool) -> None:
|
|
154
|
+
try:
|
|
155
|
+
_execute_json_command(
|
|
156
|
+
target=target,
|
|
157
|
+
output_template=output_template,
|
|
158
|
+
count=count,
|
|
159
|
+
jsonl=jsonl,
|
|
160
|
+
indent=indent,
|
|
161
|
+
use_orjson=use_orjson,
|
|
162
|
+
shard_size=shard_size,
|
|
163
|
+
include=include,
|
|
164
|
+
exclude=exclude,
|
|
165
|
+
seed=seed,
|
|
166
|
+
freeze_seeds=freeze_seeds,
|
|
167
|
+
freeze_seeds_file=freeze_seeds_file,
|
|
168
|
+
preset=preset,
|
|
169
|
+
now=now,
|
|
170
|
+
)
|
|
171
|
+
except PFGError as exc:
|
|
172
|
+
render_cli_error(exc, json_errors=json_errors, exit_app=exit_app)
|
|
173
|
+
except ConfigError as exc:
|
|
174
|
+
render_cli_error(
|
|
175
|
+
DiscoveryError(str(exc)),
|
|
176
|
+
json_errors=json_errors,
|
|
177
|
+
exit_app=exit_app,
|
|
178
|
+
)
|
|
179
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
180
|
+
render_cli_error(
|
|
181
|
+
EmitError(str(exc)),
|
|
182
|
+
json_errors=json_errors,
|
|
183
|
+
exit_app=exit_app,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if watch:
|
|
187
|
+
watch_paths = gather_default_watch_paths(
|
|
188
|
+
Path(target),
|
|
189
|
+
output=watch_output,
|
|
190
|
+
extra=watch_extra,
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
logger.debug(
|
|
194
|
+
"Entering watch loop",
|
|
195
|
+
event="watch_loop_enter",
|
|
196
|
+
target=str(target),
|
|
197
|
+
output=str(watch_output or output_template.preview_path()),
|
|
198
|
+
debounce=watch_debounce,
|
|
199
|
+
)
|
|
200
|
+
run_with_watch(lambda: invoke(exit_app=False), watch_paths, debounce=watch_debounce)
|
|
201
|
+
except PFGError as exc:
|
|
202
|
+
render_cli_error(exc, json_errors=json_errors)
|
|
203
|
+
else:
|
|
204
|
+
invoke(exit_app=True)
|
|
129
205
|
|
|
130
206
|
|
|
131
207
|
def _execute_json_command(
|
|
132
208
|
*,
|
|
133
209
|
target: str,
|
|
134
|
-
|
|
210
|
+
output_template: OutputTemplate,
|
|
135
211
|
count: int,
|
|
136
212
|
jsonl: bool,
|
|
137
213
|
indent: int | None,
|
|
@@ -140,123 +216,138 @@ def _execute_json_command(
|
|
|
140
216
|
include: str | None,
|
|
141
217
|
exclude: str | None,
|
|
142
218
|
seed: int | None,
|
|
219
|
+
now: str | None,
|
|
220
|
+
freeze_seeds: bool,
|
|
221
|
+
freeze_seeds_file: Path | None,
|
|
222
|
+
preset: str | None,
|
|
143
223
|
) -> None:
|
|
144
|
-
|
|
145
|
-
if not path.exists():
|
|
146
|
-
raise DiscoveryError(f"Target path '{target}' does not exist.", details={"path": target})
|
|
147
|
-
if not path.is_file():
|
|
148
|
-
raise DiscoveryError("Target must be a Python module file.", details={"path": target})
|
|
149
|
-
|
|
150
|
-
clear_module_cache()
|
|
151
|
-
load_entrypoint_plugins()
|
|
152
|
-
|
|
153
|
-
cli_overrides: dict[str, Any] = {}
|
|
154
|
-
if seed is not None:
|
|
155
|
-
cli_overrides["seed"] = seed
|
|
156
|
-
json_overrides: dict[str, Any] = {}
|
|
157
|
-
if indent is not None:
|
|
158
|
-
json_overrides["indent"] = indent
|
|
159
|
-
if use_orjson is not None:
|
|
160
|
-
json_overrides["orjson"] = use_orjson
|
|
161
|
-
if json_overrides:
|
|
162
|
-
cli_overrides["json"] = json_overrides
|
|
163
|
-
if include:
|
|
164
|
-
cli_overrides["include"] = split_patterns(include)
|
|
165
|
-
if exclude:
|
|
166
|
-
cli_overrides["exclude"] = split_patterns(exclude)
|
|
167
|
-
|
|
168
|
-
app_config = load_config(root=Path.cwd(), cli=cli_overrides if cli_overrides else None)
|
|
169
|
-
|
|
170
|
-
discovery = discover_models(
|
|
171
|
-
path,
|
|
172
|
-
include=app_config.include,
|
|
173
|
-
exclude=app_config.exclude,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
if discovery.errors:
|
|
177
|
-
raise DiscoveryError("; ".join(discovery.errors))
|
|
178
|
-
|
|
179
|
-
for warning in discovery.warnings:
|
|
180
|
-
if warning.strip():
|
|
181
|
-
typer.secho(warning.strip(), err=True, fg=typer.colors.YELLOW)
|
|
182
|
-
|
|
183
|
-
if not discovery.models:
|
|
184
|
-
raise DiscoveryError("No models discovered.")
|
|
224
|
+
logger = get_logger()
|
|
185
225
|
|
|
186
|
-
if
|
|
187
|
-
|
|
188
|
-
raise DiscoveryError(
|
|
189
|
-
f"Multiple models discovered ({names}). Use --include/--exclude to narrow selection.",
|
|
190
|
-
details={"models": names},
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
target_model = discovery.models[0]
|
|
194
|
-
|
|
195
|
-
try:
|
|
196
|
-
model_cls = load_model_class(target_model)
|
|
197
|
-
except RuntimeError as exc:
|
|
198
|
-
raise DiscoveryError(str(exc)) from exc
|
|
199
|
-
|
|
200
|
-
generator = _build_instance_generator(app_config)
|
|
201
|
-
|
|
202
|
-
def sample_factory() -> BaseModel:
|
|
203
|
-
instance = generator.generate_one(model_cls)
|
|
204
|
-
if instance is None:
|
|
205
|
-
raise MappingError(
|
|
206
|
-
f"Failed to generate instance for {target_model.qualname}.",
|
|
207
|
-
details={"model": target_model.qualname},
|
|
208
|
-
)
|
|
209
|
-
return instance
|
|
210
|
-
|
|
211
|
-
indent_value = indent if indent is not None else app_config.json.indent
|
|
212
|
-
use_orjson_value = use_orjson if use_orjson is not None else app_config.json.orjson
|
|
213
|
-
|
|
214
|
-
context = EmitterContext(
|
|
215
|
-
models=(model_cls,),
|
|
216
|
-
output=out,
|
|
217
|
-
parameters={
|
|
218
|
-
"count": count,
|
|
219
|
-
"jsonl": jsonl,
|
|
220
|
-
"indent": indent_value,
|
|
221
|
-
"shard_size": shard_size,
|
|
222
|
-
"use_orjson": use_orjson_value,
|
|
223
|
-
},
|
|
224
|
-
)
|
|
225
|
-
if emit_artifact("json", context):
|
|
226
|
-
return
|
|
226
|
+
include_patterns = [include] if include else None
|
|
227
|
+
exclude_patterns = [exclude] if exclude else None
|
|
227
228
|
|
|
228
229
|
try:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
result = generate_json_artifacts(
|
|
231
|
+
target=target,
|
|
232
|
+
output_template=output_template,
|
|
232
233
|
count=count,
|
|
233
234
|
jsonl=jsonl,
|
|
234
|
-
indent=
|
|
235
|
+
indent=indent,
|
|
236
|
+
use_orjson=use_orjson,
|
|
235
237
|
shard_size=shard_size,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
include=include_patterns,
|
|
239
|
+
exclude=exclude_patterns,
|
|
240
|
+
seed=seed,
|
|
241
|
+
now=now,
|
|
242
|
+
freeze_seeds=freeze_seeds,
|
|
243
|
+
freeze_seeds_file=freeze_seeds_file,
|
|
244
|
+
preset=preset,
|
|
245
|
+
logger=logger,
|
|
238
246
|
)
|
|
239
|
-
except
|
|
247
|
+
except PFGError as exc:
|
|
248
|
+
_handle_generation_error(logger, exc)
|
|
249
|
+
raise
|
|
250
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
251
|
+
if isinstance(exc, ConfigError):
|
|
252
|
+
raise
|
|
240
253
|
raise EmitError(str(exc)) from exc
|
|
241
254
|
|
|
242
|
-
|
|
255
|
+
if _log_generation_snapshot(logger, result, count):
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
emit_constraint_summary(
|
|
259
|
+
result.constraint_summary,
|
|
260
|
+
logger=logger,
|
|
261
|
+
json_mode=logger.config.json,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for emitted_path in result.paths:
|
|
243
265
|
typer.echo(str(emitted_path))
|
|
244
266
|
|
|
245
267
|
|
|
246
|
-
def
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
|
|
268
|
+
def _log_generation_snapshot(logger: Logger, result: JsonGenerationResult, count: int) -> bool:
|
|
269
|
+
config_snapshot = result.config
|
|
270
|
+
anchor_iso = config_snapshot.time_anchor.isoformat() if config_snapshot.time_anchor else None
|
|
271
|
+
|
|
272
|
+
logger.debug(
|
|
273
|
+
"Loaded configuration",
|
|
274
|
+
event="config_loaded",
|
|
275
|
+
seed=config_snapshot.seed,
|
|
276
|
+
include=list(config_snapshot.include),
|
|
277
|
+
exclude=list(config_snapshot.exclude),
|
|
278
|
+
time_anchor=anchor_iso,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if anchor_iso:
|
|
282
|
+
logger.info(
|
|
283
|
+
"Using temporal anchor",
|
|
284
|
+
event="temporal_anchor_set",
|
|
285
|
+
time_anchor=anchor_iso,
|
|
286
|
+
)
|
|
250
287
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
288
|
+
for warning in result.warnings:
|
|
289
|
+
logger.warn(
|
|
290
|
+
warning,
|
|
291
|
+
event="discovery_warning",
|
|
292
|
+
warning=warning,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if result.delegated:
|
|
296
|
+
logger.info(
|
|
297
|
+
"JSON generation handled by plugin",
|
|
298
|
+
event="json_generation_delegated",
|
|
299
|
+
output=str(result.base_output),
|
|
300
|
+
time_anchor=anchor_iso,
|
|
301
|
+
)
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
logger.info(
|
|
305
|
+
"JSON generation complete",
|
|
306
|
+
event="json_generation_complete",
|
|
307
|
+
files=[str(path) for path in result.paths],
|
|
308
|
+
count=count,
|
|
309
|
+
time_anchor=anchor_iso,
|
|
258
310
|
)
|
|
259
|
-
return
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _handle_generation_error(logger: Logger, exc: PFGError) -> None:
|
|
315
|
+
details = getattr(exc, "details", {}) or {}
|
|
316
|
+
config_info = details.get("config")
|
|
317
|
+
anchor_iso = None
|
|
318
|
+
if isinstance(config_info, dict):
|
|
319
|
+
anchor_iso = config_info.get("time_anchor")
|
|
320
|
+
logger.debug(
|
|
321
|
+
"Loaded configuration",
|
|
322
|
+
event="config_loaded",
|
|
323
|
+
seed=config_info.get("seed"),
|
|
324
|
+
include=config_info.get("include", []),
|
|
325
|
+
exclude=config_info.get("exclude", []),
|
|
326
|
+
time_anchor=anchor_iso,
|
|
327
|
+
)
|
|
328
|
+
if anchor_iso:
|
|
329
|
+
logger.info(
|
|
330
|
+
"Using temporal anchor",
|
|
331
|
+
event="temporal_anchor_set",
|
|
332
|
+
time_anchor=anchor_iso,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
warnings = details.get("warnings") or []
|
|
336
|
+
for warning in warnings:
|
|
337
|
+
if isinstance(warning, str):
|
|
338
|
+
logger.warn(
|
|
339
|
+
warning,
|
|
340
|
+
event="discovery_warning",
|
|
341
|
+
warning=warning,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
constraint_summary = details.get("constraint_summary")
|
|
345
|
+
if constraint_summary:
|
|
346
|
+
emit_constraint_summary(
|
|
347
|
+
constraint_summary,
|
|
348
|
+
logger=logger,
|
|
349
|
+
json_mode=logger.config.json,
|
|
350
|
+
)
|
|
260
351
|
|
|
261
352
|
|
|
262
353
|
__all__ = ["register"]
|