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,25 +3,19 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Literal, cast
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
|
|
10
|
-
from pydantic_fixturegen.
|
|
10
|
+
from pydantic_fixturegen.api._runtime import generate_fixtures_artifacts
|
|
11
|
+
from pydantic_fixturegen.api.models import FixturesGenerationResult
|
|
12
|
+
from pydantic_fixturegen.core.config import ConfigError
|
|
11
13
|
from pydantic_fixturegen.core.errors import DiscoveryError, EmitError, PFGError
|
|
12
|
-
from pydantic_fixturegen.core.
|
|
13
|
-
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
from ._common import (
|
|
18
|
-
JSON_ERRORS_OPTION,
|
|
19
|
-
clear_module_cache,
|
|
20
|
-
discover_models,
|
|
21
|
-
load_model_class,
|
|
22
|
-
render_cli_error,
|
|
23
|
-
split_patterns,
|
|
24
|
-
)
|
|
14
|
+
from pydantic_fixturegen.core.path_template import OutputTemplate
|
|
15
|
+
|
|
16
|
+
from ...logging import Logger, get_logger
|
|
17
|
+
from ..watch import gather_default_watch_paths, run_with_watch
|
|
18
|
+
from ._common import JSON_ERRORS_OPTION, NOW_OPTION, emit_constraint_summary, render_cli_error
|
|
25
19
|
|
|
26
20
|
STYLE_CHOICES = {"functions", "factory", "class"}
|
|
27
21
|
SCOPE_CHOICES = {"function", "module", "session"}
|
|
@@ -96,6 +90,37 @@ EXCLUDE_OPTION = typer.Option(
|
|
|
96
90
|
help="Comma-separated pattern(s) of fully-qualified model names to exclude.",
|
|
97
91
|
)
|
|
98
92
|
|
|
93
|
+
WATCH_OPTION = typer.Option(
|
|
94
|
+
False,
|
|
95
|
+
"--watch",
|
|
96
|
+
help="Watch source files and regenerate when changes are detected.",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
WATCH_DEBOUNCE_OPTION = typer.Option(
|
|
100
|
+
0.5,
|
|
101
|
+
"--watch-debounce",
|
|
102
|
+
min=0.1,
|
|
103
|
+
help="Debounce interval in seconds for filesystem events.",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
FREEZE_SEEDS_OPTION = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--freeze-seeds/--no-freeze-seeds",
|
|
109
|
+
help="Read/write per-model seeds using a freeze file to stabilize fixture output.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
FREEZE_FILE_OPTION = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--freeze-seeds-file",
|
|
115
|
+
help="Seed freeze file path (defaults to `.pfg-seeds.json` in the project root).",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
PRESET_OPTION = typer.Option(
|
|
119
|
+
None,
|
|
120
|
+
"--preset",
|
|
121
|
+
help="Apply a curated generation preset (e.g. 'boundary', 'boundary-max').",
|
|
122
|
+
)
|
|
123
|
+
|
|
99
124
|
|
|
100
125
|
def register(app: typer.Typer) -> None:
|
|
101
126
|
@app.command("fixtures")
|
|
@@ -107,146 +132,253 @@ def register(app: typer.Typer) -> None:
|
|
|
107
132
|
cases: int = CASES_OPTION,
|
|
108
133
|
return_type: str | None = RETURN_OPTION,
|
|
109
134
|
seed: int | None = SEED_OPTION,
|
|
135
|
+
now: str | None = NOW_OPTION,
|
|
110
136
|
p_none: float | None = P_NONE_OPTION,
|
|
111
137
|
include: str | None = INCLUDE_OPTION,
|
|
112
138
|
exclude: str | None = EXCLUDE_OPTION,
|
|
113
139
|
json_errors: bool = JSON_ERRORS_OPTION,
|
|
140
|
+
watch: bool = WATCH_OPTION,
|
|
141
|
+
watch_debounce: float = WATCH_DEBOUNCE_OPTION,
|
|
142
|
+
freeze_seeds: bool = FREEZE_SEEDS_OPTION,
|
|
143
|
+
freeze_seeds_file: Path | None = FREEZE_FILE_OPTION,
|
|
144
|
+
preset: str | None = PRESET_OPTION,
|
|
114
145
|
) -> None:
|
|
146
|
+
logger = get_logger()
|
|
147
|
+
|
|
115
148
|
try:
|
|
116
|
-
|
|
117
|
-
target=target,
|
|
118
|
-
out=out,
|
|
119
|
-
style=style,
|
|
120
|
-
scope=scope,
|
|
121
|
-
cases=cases,
|
|
122
|
-
return_type=return_type,
|
|
123
|
-
seed=seed,
|
|
124
|
-
p_none=p_none,
|
|
125
|
-
include=include,
|
|
126
|
-
exclude=exclude,
|
|
127
|
-
)
|
|
149
|
+
output_template = OutputTemplate(str(out))
|
|
128
150
|
except PFGError as exc:
|
|
129
151
|
render_cli_error(exc, json_errors=json_errors)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
watch_output: Path | None = None
|
|
155
|
+
watch_extra: list[Path] | None = None
|
|
156
|
+
if output_template.has_dynamic_directories():
|
|
157
|
+
watch_extra = [output_template.watch_parent()]
|
|
158
|
+
else:
|
|
159
|
+
watch_output = output_template.preview_path()
|
|
160
|
+
|
|
161
|
+
def invoke(exit_app: bool) -> None:
|
|
162
|
+
try:
|
|
163
|
+
_execute_fixtures_command(
|
|
164
|
+
target=target,
|
|
165
|
+
output_template=output_template,
|
|
166
|
+
style=style,
|
|
167
|
+
scope=scope,
|
|
168
|
+
cases=cases,
|
|
169
|
+
return_type=return_type,
|
|
170
|
+
seed=seed,
|
|
171
|
+
now=now,
|
|
172
|
+
p_none=p_none,
|
|
173
|
+
include=include,
|
|
174
|
+
exclude=exclude,
|
|
175
|
+
freeze_seeds=freeze_seeds,
|
|
176
|
+
freeze_seeds_file=freeze_seeds_file,
|
|
177
|
+
preset=preset,
|
|
178
|
+
)
|
|
179
|
+
except PFGError as exc:
|
|
180
|
+
render_cli_error(exc, json_errors=json_errors, exit_app=exit_app)
|
|
181
|
+
except ConfigError as exc:
|
|
182
|
+
render_cli_error(
|
|
183
|
+
DiscoveryError(str(exc)),
|
|
184
|
+
json_errors=json_errors,
|
|
185
|
+
exit_app=exit_app,
|
|
186
|
+
)
|
|
187
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
188
|
+
render_cli_error(
|
|
189
|
+
EmitError(str(exc)),
|
|
190
|
+
json_errors=json_errors,
|
|
191
|
+
exit_app=exit_app,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if watch:
|
|
195
|
+
watch_paths = gather_default_watch_paths(
|
|
196
|
+
Path(target),
|
|
197
|
+
output=watch_output,
|
|
198
|
+
extra=watch_extra,
|
|
199
|
+
)
|
|
200
|
+
try:
|
|
201
|
+
logger.debug(
|
|
202
|
+
"Entering watch loop",
|
|
203
|
+
event="watch_loop_enter",
|
|
204
|
+
target=str(target),
|
|
205
|
+
output=str(watch_output or output_template.preview_path()),
|
|
206
|
+
debounce=watch_debounce,
|
|
207
|
+
)
|
|
208
|
+
run_with_watch(lambda: invoke(exit_app=False), watch_paths, debounce=watch_debounce)
|
|
209
|
+
except PFGError as exc:
|
|
210
|
+
render_cli_error(exc, json_errors=json_errors)
|
|
211
|
+
else:
|
|
212
|
+
invoke(exit_app=True)
|
|
134
213
|
|
|
135
214
|
|
|
136
215
|
def _execute_fixtures_command(
|
|
137
216
|
*,
|
|
138
217
|
target: str,
|
|
139
|
-
|
|
218
|
+
output_template: OutputTemplate,
|
|
140
219
|
style: str | None,
|
|
141
220
|
scope: str | None,
|
|
142
221
|
cases: int,
|
|
143
222
|
return_type: str | None,
|
|
144
223
|
seed: int | None,
|
|
224
|
+
now: str | None,
|
|
145
225
|
p_none: float | None,
|
|
146
226
|
include: str | None,
|
|
147
227
|
exclude: str | None,
|
|
228
|
+
freeze_seeds: bool,
|
|
229
|
+
freeze_seeds_file: Path | None,
|
|
230
|
+
preset: str | None,
|
|
148
231
|
) -> None:
|
|
149
|
-
|
|
150
|
-
if not path.exists():
|
|
151
|
-
raise DiscoveryError(f"Target path '{target}' does not exist.", details={"path": target})
|
|
152
|
-
if not path.is_file():
|
|
153
|
-
raise DiscoveryError("Target must be a Python module file.", details={"path": target})
|
|
232
|
+
logger = get_logger()
|
|
154
233
|
|
|
155
234
|
style_value = _coerce_style(style)
|
|
156
235
|
scope_value = _coerce_scope(scope)
|
|
157
236
|
return_type_value = _coerce_return_type(return_type)
|
|
158
237
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
238
|
+
include_patterns = [include] if include else None
|
|
239
|
+
exclude_patterns = [exclude] if exclude else None
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
result = generate_fixtures_artifacts(
|
|
243
|
+
target=target,
|
|
244
|
+
output_template=output_template,
|
|
245
|
+
style=style_value,
|
|
246
|
+
scope=scope_value,
|
|
247
|
+
cases=cases,
|
|
248
|
+
return_type=return_type_value,
|
|
249
|
+
seed=seed,
|
|
250
|
+
now=now,
|
|
251
|
+
p_none=p_none,
|
|
252
|
+
include=include_patterns,
|
|
253
|
+
exclude=exclude_patterns,
|
|
254
|
+
freeze_seeds=freeze_seeds,
|
|
255
|
+
freeze_seeds_file=freeze_seeds_file,
|
|
256
|
+
preset=preset,
|
|
257
|
+
logger=logger,
|
|
258
|
+
)
|
|
259
|
+
except PFGError as exc:
|
|
260
|
+
_handle_fixtures_error(logger, exc)
|
|
261
|
+
raise
|
|
262
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
263
|
+
if isinstance(exc, ConfigError):
|
|
264
|
+
raise
|
|
265
|
+
raise EmitError(str(exc)) from exc
|
|
266
|
+
|
|
267
|
+
if _log_fixtures_snapshot(logger, result):
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
emit_constraint_summary(
|
|
271
|
+
result.constraint_summary,
|
|
272
|
+
logger=logger,
|
|
273
|
+
json_mode=logger.config.json,
|
|
185
274
|
)
|
|
186
275
|
|
|
187
|
-
|
|
188
|
-
|
|
276
|
+
output_path = result.path or result.base_output
|
|
277
|
+
message = str(output_path)
|
|
278
|
+
if result.skipped:
|
|
279
|
+
message += " (unchanged)"
|
|
280
|
+
typer.echo(message)
|
|
189
281
|
|
|
190
|
-
for warning in discovery.warnings:
|
|
191
|
-
if warning.strip():
|
|
192
|
-
typer.secho(warning.strip(), err=True, fg=typer.colors.YELLOW)
|
|
193
282
|
|
|
194
|
-
|
|
195
|
-
|
|
283
|
+
def _log_fixtures_snapshot(logger: Logger, result: FixturesGenerationResult) -> bool:
|
|
284
|
+
config_snapshot = result.config
|
|
285
|
+
anchor_iso = config_snapshot.time_anchor.isoformat() if config_snapshot.time_anchor else None
|
|
196
286
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
seed_value = SeedManager(seed=app_config.seed).normalized_seed
|
|
205
|
-
|
|
206
|
-
style_final = style_value or cast(StyleLiteral, app_config.emitters.pytest.style)
|
|
207
|
-
scope_final = scope_value or app_config.emitters.pytest.scope
|
|
208
|
-
return_type_final = return_type_value or DEFAULT_RETURN
|
|
209
|
-
|
|
210
|
-
pytest_config = PytestEmitConfig(
|
|
211
|
-
scope=scope_final,
|
|
212
|
-
style=style_final,
|
|
213
|
-
return_type=return_type_final,
|
|
214
|
-
cases=cases,
|
|
215
|
-
seed=seed_value,
|
|
216
|
-
optional_p_none=app_config.p_none,
|
|
287
|
+
logger.debug(
|
|
288
|
+
"Loaded configuration",
|
|
289
|
+
event="config_loaded",
|
|
290
|
+
seed=config_snapshot.seed,
|
|
291
|
+
include=list(config_snapshot.include),
|
|
292
|
+
exclude=list(config_snapshot.exclude),
|
|
293
|
+
time_anchor=anchor_iso,
|
|
217
294
|
)
|
|
218
295
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
"cases": cases,
|
|
226
|
-
"return_type": return_type_final,
|
|
227
|
-
},
|
|
228
|
-
)
|
|
229
|
-
if emit_artifact("fixtures", context):
|
|
230
|
-
return
|
|
296
|
+
if anchor_iso:
|
|
297
|
+
logger.info(
|
|
298
|
+
"Using temporal anchor",
|
|
299
|
+
event="temporal_anchor_set",
|
|
300
|
+
time_anchor=anchor_iso,
|
|
301
|
+
)
|
|
231
302
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
303
|
+
for warning in result.warnings:
|
|
304
|
+
logger.warn(
|
|
305
|
+
warning,
|
|
306
|
+
event="discovery_warning",
|
|
307
|
+
warning=warning,
|
|
237
308
|
)
|
|
238
|
-
except Exception as exc:
|
|
239
|
-
raise EmitError(str(exc)) from exc
|
|
240
309
|
|
|
241
|
-
|
|
310
|
+
if result.delegated:
|
|
311
|
+
logger.info(
|
|
312
|
+
"Fixtures generation handled by plugin",
|
|
313
|
+
event="fixtures_generation_delegated",
|
|
314
|
+
output=str(result.base_output),
|
|
315
|
+
style=result.style,
|
|
316
|
+
scope=result.scope,
|
|
317
|
+
time_anchor=anchor_iso,
|
|
318
|
+
)
|
|
319
|
+
return True
|
|
320
|
+
|
|
242
321
|
if result.skipped:
|
|
243
|
-
|
|
244
|
-
|
|
322
|
+
logger.info(
|
|
323
|
+
"Fixtures unchanged",
|
|
324
|
+
event="fixtures_generation_unchanged",
|
|
325
|
+
output=str(result.path),
|
|
326
|
+
time_anchor=anchor_iso,
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
logger.info(
|
|
330
|
+
"Fixtures generation complete",
|
|
331
|
+
event="fixtures_generation_complete",
|
|
332
|
+
output=str(result.path),
|
|
333
|
+
style=result.style,
|
|
334
|
+
scope=result.scope,
|
|
335
|
+
time_anchor=anchor_iso,
|
|
336
|
+
)
|
|
337
|
+
return False
|
|
245
338
|
|
|
246
339
|
|
|
247
340
|
__all__ = ["register"]
|
|
248
341
|
|
|
249
342
|
|
|
343
|
+
def _handle_fixtures_error(logger: Logger, exc: PFGError) -> None:
|
|
344
|
+
details = getattr(exc, "details", {}) or {}
|
|
345
|
+
config_info = details.get("config")
|
|
346
|
+
anchor_iso = None
|
|
347
|
+
if isinstance(config_info, dict):
|
|
348
|
+
anchor_iso = config_info.get("time_anchor")
|
|
349
|
+
logger.debug(
|
|
350
|
+
"Loaded configuration",
|
|
351
|
+
event="config_loaded",
|
|
352
|
+
seed=config_info.get("seed"),
|
|
353
|
+
include=config_info.get("include", []),
|
|
354
|
+
exclude=config_info.get("exclude", []),
|
|
355
|
+
time_anchor=anchor_iso,
|
|
356
|
+
)
|
|
357
|
+
if anchor_iso:
|
|
358
|
+
logger.info(
|
|
359
|
+
"Using temporal anchor",
|
|
360
|
+
event="temporal_anchor_set",
|
|
361
|
+
time_anchor=anchor_iso,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
warnings = details.get("warnings") or []
|
|
365
|
+
for warning in warnings:
|
|
366
|
+
if isinstance(warning, str):
|
|
367
|
+
logger.warn(
|
|
368
|
+
warning,
|
|
369
|
+
event="discovery_warning",
|
|
370
|
+
warning=warning,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
constraint_summary = details.get("constraint_summary")
|
|
374
|
+
if constraint_summary:
|
|
375
|
+
emit_constraint_summary(
|
|
376
|
+
constraint_summary,
|
|
377
|
+
logger=logger,
|
|
378
|
+
json_mode=logger.config.json,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
250
382
|
def _coerce_style(value: str | None) -> StyleLiteral | None:
|
|
251
383
|
if value is None:
|
|
252
384
|
return None
|