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,992 @@
|
|
|
1
|
+
"""CLI command for diffing regenerated artifacts against existing output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import difflib
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from pydantic_fixturegen.core.config import load_config
|
|
17
|
+
from pydantic_fixturegen.core.errors import (
|
|
18
|
+
DiffError,
|
|
19
|
+
DiscoveryError,
|
|
20
|
+
EmitError,
|
|
21
|
+
MappingError,
|
|
22
|
+
PFGError,
|
|
23
|
+
)
|
|
24
|
+
from pydantic_fixturegen.core.field_policies import FieldPolicy
|
|
25
|
+
from pydantic_fixturegen.core.generate import GenerationConfig, InstanceGenerator
|
|
26
|
+
from pydantic_fixturegen.core.seed import SeedManager
|
|
27
|
+
from pydantic_fixturegen.core.seed_freeze import (
|
|
28
|
+
FreezeStatus,
|
|
29
|
+
SeedFreezeFile,
|
|
30
|
+
compute_model_digest,
|
|
31
|
+
derive_default_model_seed,
|
|
32
|
+
model_identifier,
|
|
33
|
+
resolve_freeze_path,
|
|
34
|
+
)
|
|
35
|
+
from pydantic_fixturegen.emitters.json_out import emit_json_samples
|
|
36
|
+
from pydantic_fixturegen.emitters.pytest_codegen import (
|
|
37
|
+
PytestEmitConfig,
|
|
38
|
+
emit_pytest_fixtures,
|
|
39
|
+
)
|
|
40
|
+
from pydantic_fixturegen.emitters.schema_out import emit_model_schema, emit_models_schema
|
|
41
|
+
from pydantic_fixturegen.logging import get_logger
|
|
42
|
+
from pydantic_fixturegen.plugins.hookspecs import EmitterContext
|
|
43
|
+
from pydantic_fixturegen.plugins.loader import emit_artifact, load_entrypoint_plugins
|
|
44
|
+
|
|
45
|
+
from .gen._common import (
|
|
46
|
+
JSON_ERRORS_OPTION,
|
|
47
|
+
NOW_OPTION,
|
|
48
|
+
DiscoveryMethod,
|
|
49
|
+
clear_module_cache,
|
|
50
|
+
discover_models,
|
|
51
|
+
emit_constraint_summary,
|
|
52
|
+
load_model_class,
|
|
53
|
+
render_cli_error,
|
|
54
|
+
split_patterns,
|
|
55
|
+
)
|
|
56
|
+
from .gen.fixtures import (
|
|
57
|
+
DEFAULT_RETURN,
|
|
58
|
+
ReturnLiteral,
|
|
59
|
+
StyleLiteral,
|
|
60
|
+
_coerce_return_type,
|
|
61
|
+
_coerce_scope,
|
|
62
|
+
_coerce_style,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
PATH_ARGUMENT = typer.Argument(
|
|
66
|
+
...,
|
|
67
|
+
help="Python module file containing Pydantic models to diff against artifacts.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
INCLUDE_OPTION = typer.Option(
|
|
71
|
+
None,
|
|
72
|
+
"--include",
|
|
73
|
+
"-i",
|
|
74
|
+
help="Comma-separated glob pattern(s) of fully-qualified model names to include.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
EXCLUDE_OPTION = typer.Option(
|
|
78
|
+
None,
|
|
79
|
+
"--exclude",
|
|
80
|
+
"-e",
|
|
81
|
+
help="Comma-separated glob pattern(s) of fully-qualified model names to exclude.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
AST_OPTION = typer.Option(False, "--ast", help="Use AST discovery only (no imports executed).")
|
|
85
|
+
|
|
86
|
+
HYBRID_OPTION = typer.Option(False, "--hybrid", help="Combine AST and safe import discovery.")
|
|
87
|
+
|
|
88
|
+
TIMEOUT_OPTION = typer.Option(
|
|
89
|
+
5.0,
|
|
90
|
+
"--timeout",
|
|
91
|
+
min=0.1,
|
|
92
|
+
help="Timeout in seconds for safe import execution.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
MEMORY_LIMIT_OPTION = typer.Option(
|
|
96
|
+
256,
|
|
97
|
+
"--memory-limit-mb",
|
|
98
|
+
min=1,
|
|
99
|
+
help="Memory limit in megabytes for safe import subprocess.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
SEED_OPTION = typer.Option(
|
|
103
|
+
None,
|
|
104
|
+
"--seed",
|
|
105
|
+
help="Seed override for regenerated artifacts.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
PNONE_OPTION = typer.Option(
|
|
109
|
+
None,
|
|
110
|
+
"--p-none",
|
|
111
|
+
min=0.0,
|
|
112
|
+
max=1.0,
|
|
113
|
+
help="Override probability of None for optional fields.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
PRESET_OPTION = typer.Option(
|
|
117
|
+
None,
|
|
118
|
+
"--preset",
|
|
119
|
+
help="Apply a curated generation preset during diff regeneration.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
FREEZE_SEEDS_OPTION = typer.Option(
|
|
123
|
+
False,
|
|
124
|
+
"--freeze-seeds/--no-freeze-seeds",
|
|
125
|
+
help="Read/write per-model seeds using a freeze file for deterministic diffs.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
FREEZE_FILE_OPTION = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--freeze-seeds-file",
|
|
131
|
+
help="Seed freeze file path (defaults to `.pfg-seeds.json`).",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
JSON_OUT_OPTION = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--json-out",
|
|
137
|
+
help="Existing JSON/JSONL artifact path to compare.",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
JSON_COUNT_OPTION = typer.Option(
|
|
141
|
+
1,
|
|
142
|
+
"--json-count",
|
|
143
|
+
min=1,
|
|
144
|
+
help="Number of JSON samples to regenerate for comparison.",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
JSON_JSONL_OPTION = typer.Option(
|
|
148
|
+
False,
|
|
149
|
+
"--json-jsonl/--no-json-jsonl",
|
|
150
|
+
help="Treat JSON artifact as newline-delimited JSON.",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
JSON_INDENT_OPTION = typer.Option(
|
|
154
|
+
None,
|
|
155
|
+
"--json-indent",
|
|
156
|
+
min=0,
|
|
157
|
+
help="Indentation override for JSON output.",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
JSON_ORJSON_OPTION = typer.Option(
|
|
161
|
+
None,
|
|
162
|
+
"--json-orjson/--json-std",
|
|
163
|
+
help="Toggle orjson serialization for JSON diff generation.",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
JSON_SHARD_OPTION = typer.Option(
|
|
167
|
+
None,
|
|
168
|
+
"--json-shard-size",
|
|
169
|
+
min=1,
|
|
170
|
+
help="Shard size used when the JSON artifact was generated.",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
FIXTURES_OUT_OPTION = typer.Option(
|
|
174
|
+
None,
|
|
175
|
+
"--fixtures-out",
|
|
176
|
+
help="Existing pytest fixtures module path to compare.",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
FIXTURES_STYLE_OPTION = typer.Option(
|
|
180
|
+
None,
|
|
181
|
+
"--fixtures-style",
|
|
182
|
+
help="Fixture style override (functions, factory, class).",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
FIXTURES_SCOPE_OPTION = typer.Option(
|
|
186
|
+
None,
|
|
187
|
+
"--fixtures-scope",
|
|
188
|
+
help="Fixture scope override (function, module, session).",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
FIXTURES_CASES_OPTION = typer.Option(
|
|
192
|
+
1,
|
|
193
|
+
"--fixtures-cases",
|
|
194
|
+
min=1,
|
|
195
|
+
help="Number of parametrised cases per fixture.",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
FIXTURES_RETURN_OPTION = typer.Option(
|
|
199
|
+
None,
|
|
200
|
+
"--fixtures-return-type",
|
|
201
|
+
help="Return type override for fixtures (model or dict).",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
SCHEMA_OUT_OPTION = typer.Option(
|
|
205
|
+
None,
|
|
206
|
+
"--schema-out",
|
|
207
|
+
help="Existing JSON schema file path to compare.",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
SCHEMA_INDENT_OPTION = typer.Option(
|
|
211
|
+
None,
|
|
212
|
+
"--schema-indent",
|
|
213
|
+
min=0,
|
|
214
|
+
help="Indentation override for schema JSON output.",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
SHOW_DIFF_OPTION = typer.Option(
|
|
218
|
+
False,
|
|
219
|
+
"--show-diff/--no-show-diff",
|
|
220
|
+
help="Show unified diffs when differences are detected.",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
app = typer.Typer(invoke_without_command=True, subcommand_metavar="")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(slots=True)
|
|
228
|
+
class DiffReport:
|
|
229
|
+
kind: str
|
|
230
|
+
target: Path
|
|
231
|
+
checked_paths: list[Path]
|
|
232
|
+
messages: list[str]
|
|
233
|
+
diff_outputs: list[tuple[str, str]]
|
|
234
|
+
summary: str | None
|
|
235
|
+
constraint_report: dict[str, Any] | None = None
|
|
236
|
+
time_anchor: str | None = None
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def changed(self) -> bool:
|
|
240
|
+
return bool(self.messages)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def diff( # noqa: PLR0913 - CLI mirrors documented parameters
|
|
244
|
+
ctx: typer.Context,
|
|
245
|
+
path: str = PATH_ARGUMENT,
|
|
246
|
+
include: str | None = INCLUDE_OPTION,
|
|
247
|
+
exclude: str | None = EXCLUDE_OPTION,
|
|
248
|
+
ast_mode: bool = AST_OPTION,
|
|
249
|
+
hybrid_mode: bool = HYBRID_OPTION,
|
|
250
|
+
timeout: float = TIMEOUT_OPTION,
|
|
251
|
+
memory_limit_mb: int = MEMORY_LIMIT_OPTION,
|
|
252
|
+
seed: int | None = SEED_OPTION,
|
|
253
|
+
p_none: float | None = PNONE_OPTION,
|
|
254
|
+
now: str | None = NOW_OPTION,
|
|
255
|
+
json_out: Path | None = JSON_OUT_OPTION,
|
|
256
|
+
json_count: int = JSON_COUNT_OPTION,
|
|
257
|
+
json_jsonl: bool = JSON_JSONL_OPTION,
|
|
258
|
+
json_indent: int | None = JSON_INDENT_OPTION,
|
|
259
|
+
json_orjson: bool | None = JSON_ORJSON_OPTION,
|
|
260
|
+
json_shard_size: int | None = JSON_SHARD_OPTION,
|
|
261
|
+
fixtures_out: Path | None = FIXTURES_OUT_OPTION,
|
|
262
|
+
fixtures_style: str | None = FIXTURES_STYLE_OPTION,
|
|
263
|
+
fixtures_scope: str | None = FIXTURES_SCOPE_OPTION,
|
|
264
|
+
fixtures_cases: int = FIXTURES_CASES_OPTION,
|
|
265
|
+
fixtures_return_type: str | None = FIXTURES_RETURN_OPTION,
|
|
266
|
+
schema_out: Path | None = SCHEMA_OUT_OPTION,
|
|
267
|
+
schema_indent: int | None = SCHEMA_INDENT_OPTION,
|
|
268
|
+
show_diff: bool = SHOW_DIFF_OPTION,
|
|
269
|
+
json_errors: bool = JSON_ERRORS_OPTION,
|
|
270
|
+
preset: str | None = PRESET_OPTION,
|
|
271
|
+
freeze_seeds: bool = FREEZE_SEEDS_OPTION,
|
|
272
|
+
freeze_seeds_file: Path | None = FREEZE_FILE_OPTION,
|
|
273
|
+
) -> None:
|
|
274
|
+
_ = ctx
|
|
275
|
+
logger = get_logger()
|
|
276
|
+
try:
|
|
277
|
+
reports = _execute_diff(
|
|
278
|
+
target=path,
|
|
279
|
+
include=include,
|
|
280
|
+
exclude=exclude,
|
|
281
|
+
ast_mode=ast_mode,
|
|
282
|
+
hybrid_mode=hybrid_mode,
|
|
283
|
+
timeout=timeout,
|
|
284
|
+
memory_limit_mb=memory_limit_mb,
|
|
285
|
+
seed_override=seed,
|
|
286
|
+
p_none_override=p_none,
|
|
287
|
+
json_options=JsonDiffOptions(
|
|
288
|
+
out=json_out,
|
|
289
|
+
count=json_count,
|
|
290
|
+
jsonl=json_jsonl,
|
|
291
|
+
indent=json_indent,
|
|
292
|
+
use_orjson=json_orjson,
|
|
293
|
+
shard_size=json_shard_size,
|
|
294
|
+
),
|
|
295
|
+
fixtures_options=FixturesDiffOptions(
|
|
296
|
+
out=fixtures_out,
|
|
297
|
+
style=fixtures_style,
|
|
298
|
+
scope=fixtures_scope,
|
|
299
|
+
cases=fixtures_cases,
|
|
300
|
+
return_type=fixtures_return_type,
|
|
301
|
+
),
|
|
302
|
+
schema_options=SchemaDiffOptions(
|
|
303
|
+
out=schema_out,
|
|
304
|
+
indent=schema_indent,
|
|
305
|
+
),
|
|
306
|
+
preset=preset,
|
|
307
|
+
freeze_seeds=freeze_seeds,
|
|
308
|
+
freeze_seeds_file=freeze_seeds_file,
|
|
309
|
+
now_override=now,
|
|
310
|
+
)
|
|
311
|
+
except PFGError as exc:
|
|
312
|
+
render_cli_error(exc, json_errors=json_errors)
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
changed = any(report.changed for report in reports)
|
|
316
|
+
|
|
317
|
+
if json_errors and changed:
|
|
318
|
+
payload = {
|
|
319
|
+
"artifacts": [
|
|
320
|
+
{
|
|
321
|
+
"kind": report.kind,
|
|
322
|
+
"target": str(report.target),
|
|
323
|
+
"checked": [str(path) for path in report.checked_paths],
|
|
324
|
+
"messages": report.messages,
|
|
325
|
+
"diffs": [
|
|
326
|
+
{"path": path, "diff": diff_text} for path, diff_text in report.diff_outputs
|
|
327
|
+
],
|
|
328
|
+
"constraints": report.constraint_report,
|
|
329
|
+
"time_anchor": report.time_anchor,
|
|
330
|
+
}
|
|
331
|
+
for report in reports
|
|
332
|
+
if report.kind and (report.changed or report.messages or report.checked_paths)
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
render_cli_error(DiffError("Artifacts differ.", details=payload), json_errors=True)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
_render_reports(reports, show_diff, logger, logger.config.json)
|
|
339
|
+
|
|
340
|
+
if changed:
|
|
341
|
+
raise typer.Exit(code=1)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
app.callback(invoke_without_command=True)(diff)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@dataclass(slots=True)
|
|
348
|
+
class JsonDiffOptions:
|
|
349
|
+
out: Path | None
|
|
350
|
+
count: int
|
|
351
|
+
jsonl: bool
|
|
352
|
+
indent: int | None
|
|
353
|
+
use_orjson: bool | None
|
|
354
|
+
shard_size: int | None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@dataclass(slots=True)
|
|
358
|
+
class FixturesDiffOptions:
|
|
359
|
+
out: Path | None
|
|
360
|
+
style: str | None
|
|
361
|
+
scope: str | None
|
|
362
|
+
cases: int
|
|
363
|
+
return_type: str | None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass(slots=True)
|
|
367
|
+
class SchemaDiffOptions:
|
|
368
|
+
out: Path | None
|
|
369
|
+
indent: int | None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _execute_diff(
|
|
373
|
+
*,
|
|
374
|
+
target: str,
|
|
375
|
+
include: str | None,
|
|
376
|
+
exclude: str | None,
|
|
377
|
+
ast_mode: bool,
|
|
378
|
+
hybrid_mode: bool,
|
|
379
|
+
timeout: float,
|
|
380
|
+
memory_limit_mb: int,
|
|
381
|
+
seed_override: int | None,
|
|
382
|
+
p_none_override: float | None,
|
|
383
|
+
json_options: JsonDiffOptions,
|
|
384
|
+
fixtures_options: FixturesDiffOptions,
|
|
385
|
+
schema_options: SchemaDiffOptions,
|
|
386
|
+
freeze_seeds: bool,
|
|
387
|
+
freeze_seeds_file: Path | None,
|
|
388
|
+
preset: str | None,
|
|
389
|
+
now_override: str | None,
|
|
390
|
+
) -> list[DiffReport]:
|
|
391
|
+
if not any((json_options.out, fixtures_options.out, schema_options.out)):
|
|
392
|
+
raise DiscoveryError("Provide at least one artifact path to diff.")
|
|
393
|
+
|
|
394
|
+
target_path = Path(target)
|
|
395
|
+
if not target_path.exists():
|
|
396
|
+
raise DiscoveryError(f"Target path '{target}' does not exist.", details={"path": target})
|
|
397
|
+
if not target_path.is_file():
|
|
398
|
+
raise DiscoveryError("Target must be a Python module file.", details={"path": target})
|
|
399
|
+
|
|
400
|
+
clear_module_cache()
|
|
401
|
+
load_entrypoint_plugins()
|
|
402
|
+
|
|
403
|
+
logger = get_logger()
|
|
404
|
+
|
|
405
|
+
freeze_manager: SeedFreezeFile | None = None
|
|
406
|
+
if freeze_seeds:
|
|
407
|
+
freeze_path = resolve_freeze_path(freeze_seeds_file, root=Path.cwd())
|
|
408
|
+
freeze_manager = SeedFreezeFile.load(freeze_path)
|
|
409
|
+
for message in freeze_manager.messages:
|
|
410
|
+
logger.warn(
|
|
411
|
+
"Seed freeze file ignored",
|
|
412
|
+
event="seed_freeze_invalid",
|
|
413
|
+
path=str(freeze_path),
|
|
414
|
+
reason=message,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
config_cli_overrides: dict[str, Any] = {}
|
|
418
|
+
if preset is not None:
|
|
419
|
+
config_cli_overrides["preset"] = preset
|
|
420
|
+
if now_override is not None:
|
|
421
|
+
config_cli_overrides["now"] = now_override
|
|
422
|
+
|
|
423
|
+
app_config = load_config(
|
|
424
|
+
root=Path.cwd(), cli=config_cli_overrides if config_cli_overrides else None
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
anchor_iso = app_config.now.isoformat() if app_config.now else None
|
|
428
|
+
|
|
429
|
+
if anchor_iso:
|
|
430
|
+
logger.info(
|
|
431
|
+
"Using temporal anchor",
|
|
432
|
+
event="temporal_anchor_set",
|
|
433
|
+
time_anchor=anchor_iso,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
include_patterns = split_patterns(include) if include is not None else list(app_config.include)
|
|
437
|
+
exclude_patterns = split_patterns(exclude) if exclude is not None else list(app_config.exclude)
|
|
438
|
+
|
|
439
|
+
method = _resolve_method(ast_mode, hybrid_mode)
|
|
440
|
+
discovery = discover_models(
|
|
441
|
+
target_path,
|
|
442
|
+
include=include_patterns,
|
|
443
|
+
exclude=exclude_patterns,
|
|
444
|
+
method=method,
|
|
445
|
+
timeout=timeout,
|
|
446
|
+
memory_limit_mb=memory_limit_mb,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if discovery.errors:
|
|
450
|
+
raise DiscoveryError("; ".join(discovery.errors))
|
|
451
|
+
|
|
452
|
+
for warning in discovery.warnings:
|
|
453
|
+
if warning.strip():
|
|
454
|
+
typer.secho(f"warning: {warning.strip()}", err=True, fg=typer.colors.YELLOW)
|
|
455
|
+
|
|
456
|
+
if not discovery.models:
|
|
457
|
+
raise DiscoveryError("No models discovered.")
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
model_classes = [load_model_class(model) for model in discovery.models]
|
|
461
|
+
except RuntimeError as exc:
|
|
462
|
+
raise DiscoveryError(str(exc)) from exc
|
|
463
|
+
|
|
464
|
+
seed_value = seed_override if seed_override is not None else app_config.seed
|
|
465
|
+
p_none_value = p_none_override if p_none_override is not None else app_config.p_none
|
|
466
|
+
|
|
467
|
+
per_model_seeds: dict[str, int] = {}
|
|
468
|
+
model_digests: dict[str, str | None] = {}
|
|
469
|
+
|
|
470
|
+
for model_cls in model_classes:
|
|
471
|
+
model_id = model_identifier(model_cls)
|
|
472
|
+
digest = compute_model_digest(model_cls)
|
|
473
|
+
model_digests[model_id] = digest
|
|
474
|
+
|
|
475
|
+
default_seed = derive_default_model_seed(seed_value, model_id)
|
|
476
|
+
selected_seed = default_seed
|
|
477
|
+
|
|
478
|
+
if freeze_manager is not None:
|
|
479
|
+
stored_seed, status = freeze_manager.resolve_seed(model_id, model_digest=digest)
|
|
480
|
+
if status is FreezeStatus.VALID and stored_seed is not None:
|
|
481
|
+
selected_seed = stored_seed
|
|
482
|
+
else:
|
|
483
|
+
event = (
|
|
484
|
+
"seed_freeze_missing" if status is FreezeStatus.MISSING else "seed_freeze_stale"
|
|
485
|
+
)
|
|
486
|
+
logger.warn(
|
|
487
|
+
"Seed freeze entry unavailable; deriving new seed",
|
|
488
|
+
event=event,
|
|
489
|
+
model=model_id,
|
|
490
|
+
path=str(freeze_manager.path),
|
|
491
|
+
)
|
|
492
|
+
selected_seed = default_seed
|
|
493
|
+
|
|
494
|
+
per_model_seeds[model_id] = selected_seed
|
|
495
|
+
|
|
496
|
+
reports: list[DiffReport] = []
|
|
497
|
+
|
|
498
|
+
if json_options.out is not None:
|
|
499
|
+
json_model_id = model_identifier(model_classes[0])
|
|
500
|
+
json_seed_value = (
|
|
501
|
+
per_model_seeds[json_model_id] if freeze_manager is not None else seed_value
|
|
502
|
+
)
|
|
503
|
+
reports.append(
|
|
504
|
+
_diff_json_artifact(
|
|
505
|
+
model_classes=model_classes,
|
|
506
|
+
seed_value=json_seed_value,
|
|
507
|
+
app_config_indent=app_config.json.indent,
|
|
508
|
+
app_config_orjson=app_config.json.orjson,
|
|
509
|
+
app_config_enum=app_config.enum_policy,
|
|
510
|
+
app_config_union=app_config.union_policy,
|
|
511
|
+
app_config_p_none=p_none_value,
|
|
512
|
+
app_config_now=app_config.now,
|
|
513
|
+
options=json_options,
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if fixtures_options.out is not None:
|
|
518
|
+
reports.append(
|
|
519
|
+
_diff_fixtures_artifact(
|
|
520
|
+
model_classes=model_classes,
|
|
521
|
+
app_config_seed=seed_value,
|
|
522
|
+
app_config_p_none=p_none_value,
|
|
523
|
+
app_config_style=app_config.emitters.pytest.style,
|
|
524
|
+
app_config_scope=app_config.emitters.pytest.scope,
|
|
525
|
+
options=fixtures_options,
|
|
526
|
+
per_model_seeds=per_model_seeds if freeze_manager is not None else None,
|
|
527
|
+
app_config_now=app_config.now,
|
|
528
|
+
app_config_field_policies=app_config.field_policies,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if schema_options.out is not None:
|
|
533
|
+
reports.append(
|
|
534
|
+
_diff_schema_artifact(
|
|
535
|
+
model_classes=model_classes,
|
|
536
|
+
app_config_indent=app_config.json.indent,
|
|
537
|
+
options=schema_options,
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if freeze_manager is not None:
|
|
542
|
+
for model_cls in model_classes:
|
|
543
|
+
model_id = model_identifier(model_cls)
|
|
544
|
+
freeze_manager.record_seed(
|
|
545
|
+
model_id,
|
|
546
|
+
per_model_seeds[model_id],
|
|
547
|
+
model_digest=model_digests[model_id],
|
|
548
|
+
)
|
|
549
|
+
freeze_manager.save()
|
|
550
|
+
|
|
551
|
+
return reports
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _diff_json_artifact(
|
|
555
|
+
*,
|
|
556
|
+
model_classes: list[type[BaseModel]],
|
|
557
|
+
seed_value: int | str | None,
|
|
558
|
+
app_config_indent: int | None,
|
|
559
|
+
app_config_orjson: bool,
|
|
560
|
+
app_config_enum: str,
|
|
561
|
+
app_config_union: str,
|
|
562
|
+
app_config_p_none: float | None,
|
|
563
|
+
app_config_now: datetime.datetime | None,
|
|
564
|
+
options: JsonDiffOptions,
|
|
565
|
+
) -> DiffReport:
|
|
566
|
+
if not model_classes:
|
|
567
|
+
raise DiscoveryError("No models available for JSON diff.")
|
|
568
|
+
if len(model_classes) > 1:
|
|
569
|
+
names = ", ".join(model.__name__ for model in model_classes)
|
|
570
|
+
raise DiscoveryError(
|
|
571
|
+
"Multiple models discovered. Use --include/--exclude to narrow selection for JSON"
|
|
572
|
+
" diffs.",
|
|
573
|
+
details={"models": names},
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
if options.out is None:
|
|
577
|
+
raise DiscoveryError("JSON diff requires --json-out.")
|
|
578
|
+
|
|
579
|
+
target_model = model_classes[0]
|
|
580
|
+
output_path = Path(options.out)
|
|
581
|
+
|
|
582
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
583
|
+
temp_base = Path(tmp_dir) / "json" / output_path.name
|
|
584
|
+
temp_base.parent.mkdir(parents=True, exist_ok=True)
|
|
585
|
+
|
|
586
|
+
generator = _build_instance_generator(
|
|
587
|
+
seed_value=seed_value,
|
|
588
|
+
union_policy=app_config_union,
|
|
589
|
+
enum_policy=app_config_enum,
|
|
590
|
+
p_none=app_config_p_none,
|
|
591
|
+
time_anchor=app_config_now,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def sample_factory() -> BaseModel:
|
|
595
|
+
instance = generator.generate_one(target_model)
|
|
596
|
+
if instance is None:
|
|
597
|
+
raise MappingError(
|
|
598
|
+
f"Failed to generate instance for {target_model.__name__}.",
|
|
599
|
+
details={"model": target_model.__name__},
|
|
600
|
+
)
|
|
601
|
+
return instance
|
|
602
|
+
|
|
603
|
+
indent_value = options.indent if options.indent is not None else app_config_indent
|
|
604
|
+
use_orjson_value = (
|
|
605
|
+
options.use_orjson if options.use_orjson is not None else app_config_orjson
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
generated_paths = emit_json_samples(
|
|
610
|
+
sample_factory,
|
|
611
|
+
output_path=temp_base,
|
|
612
|
+
count=options.count,
|
|
613
|
+
jsonl=options.jsonl,
|
|
614
|
+
indent=indent_value,
|
|
615
|
+
shard_size=options.shard_size,
|
|
616
|
+
use_orjson=use_orjson_value,
|
|
617
|
+
ensure_ascii=False,
|
|
618
|
+
)
|
|
619
|
+
except RuntimeError as exc:
|
|
620
|
+
raise EmitError(str(exc)) from exc
|
|
621
|
+
|
|
622
|
+
constraint_summary = generator.constraint_report.summary()
|
|
623
|
+
|
|
624
|
+
generated_paths = sorted(generated_paths, key=lambda p: p.name)
|
|
625
|
+
actual_parent = output_path.parent if output_path.parent != Path("") else Path(".")
|
|
626
|
+
|
|
627
|
+
checked_paths: list[Path] = []
|
|
628
|
+
messages: list[str] = []
|
|
629
|
+
diff_outputs: list[tuple[str, str]] = []
|
|
630
|
+
|
|
631
|
+
for generated_path in generated_paths:
|
|
632
|
+
actual_path = actual_parent / generated_path.name
|
|
633
|
+
checked_paths.append(actual_path)
|
|
634
|
+
if not actual_path.exists():
|
|
635
|
+
messages.append(f"Missing JSON artifact: {actual_path}")
|
|
636
|
+
continue
|
|
637
|
+
if actual_path.is_dir():
|
|
638
|
+
messages.append(f"JSON artifact path is a directory: {actual_path}")
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
actual_text = actual_path.read_text(encoding="utf-8")
|
|
642
|
+
generated_text = generated_path.read_text(encoding="utf-8")
|
|
643
|
+
if actual_text != generated_text:
|
|
644
|
+
messages.append(f"JSON artifact differs: {actual_path}")
|
|
645
|
+
diff_outputs.append(
|
|
646
|
+
(
|
|
647
|
+
str(actual_path),
|
|
648
|
+
_build_unified_diff(
|
|
649
|
+
actual_text,
|
|
650
|
+
generated_text,
|
|
651
|
+
str(actual_path),
|
|
652
|
+
f"{actual_path} (generated)",
|
|
653
|
+
),
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
expected_names = {path.name for path in generated_paths}
|
|
658
|
+
suffix = ".jsonl" if options.jsonl else ".json"
|
|
659
|
+
stem = output_path.stem if output_path.suffix else output_path.name
|
|
660
|
+
pattern = f"{stem}*{suffix}"
|
|
661
|
+
extra_candidates = [p for p in actual_parent.glob(pattern) if p.name not in expected_names]
|
|
662
|
+
|
|
663
|
+
for extra in sorted(extra_candidates, key=lambda p: p.name):
|
|
664
|
+
if extra.is_file():
|
|
665
|
+
messages.append(f"Unexpected extra JSON artifact: {extra}")
|
|
666
|
+
|
|
667
|
+
summary = None
|
|
668
|
+
if not messages:
|
|
669
|
+
summary = f"JSON artifacts match ({len(checked_paths)} file(s))."
|
|
670
|
+
|
|
671
|
+
anchor_iso = app_config_now.isoformat() if app_config_now else None
|
|
672
|
+
|
|
673
|
+
return DiffReport(
|
|
674
|
+
kind="json",
|
|
675
|
+
target=output_path,
|
|
676
|
+
checked_paths=checked_paths,
|
|
677
|
+
messages=messages,
|
|
678
|
+
diff_outputs=diff_outputs,
|
|
679
|
+
summary=summary,
|
|
680
|
+
constraint_report=(constraint_summary if constraint_summary.get("models") else None),
|
|
681
|
+
time_anchor=anchor_iso,
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _diff_fixtures_artifact(
|
|
686
|
+
*,
|
|
687
|
+
model_classes: list[type[BaseModel]],
|
|
688
|
+
app_config_seed: int | str | None,
|
|
689
|
+
app_config_p_none: float | None,
|
|
690
|
+
app_config_style: str,
|
|
691
|
+
app_config_scope: str,
|
|
692
|
+
options: FixturesDiffOptions,
|
|
693
|
+
per_model_seeds: dict[str, int] | None,
|
|
694
|
+
app_config_now: datetime.datetime | None,
|
|
695
|
+
app_config_field_policies: tuple[FieldPolicy, ...],
|
|
696
|
+
) -> DiffReport:
|
|
697
|
+
if options.out is None:
|
|
698
|
+
raise DiscoveryError("Fixtures diff requires --fixtures-out.")
|
|
699
|
+
|
|
700
|
+
output_path = Path(options.out)
|
|
701
|
+
style_value = _coerce_style(options.style)
|
|
702
|
+
scope_value = _coerce_scope(options.scope)
|
|
703
|
+
return_type_value = _coerce_return_type(options.return_type)
|
|
704
|
+
|
|
705
|
+
seed_normalized: int | None = None
|
|
706
|
+
if app_config_seed is not None:
|
|
707
|
+
seed_normalized = SeedManager(seed=app_config_seed).normalized_seed
|
|
708
|
+
|
|
709
|
+
style_default = cast(StyleLiteral, app_config_style)
|
|
710
|
+
style_final: StyleLiteral = style_value or style_default
|
|
711
|
+
scope_final = scope_value or app_config_scope
|
|
712
|
+
return_type_default: ReturnLiteral = DEFAULT_RETURN
|
|
713
|
+
return_type_final: ReturnLiteral = return_type_value or return_type_default
|
|
714
|
+
|
|
715
|
+
constraint_summary: dict[str, Any] | None = None
|
|
716
|
+
|
|
717
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
718
|
+
temp_out = Path(tmp_dir) / "fixtures" / output_path.name
|
|
719
|
+
temp_out.parent.mkdir(parents=True, exist_ok=True)
|
|
720
|
+
|
|
721
|
+
header_seed = seed_normalized if per_model_seeds is None else None
|
|
722
|
+
|
|
723
|
+
pytest_config = PytestEmitConfig(
|
|
724
|
+
scope=scope_final,
|
|
725
|
+
style=style_final,
|
|
726
|
+
return_type=return_type_final,
|
|
727
|
+
cases=options.cases,
|
|
728
|
+
seed=header_seed,
|
|
729
|
+
optional_p_none=app_config_p_none,
|
|
730
|
+
per_model_seeds=per_model_seeds,
|
|
731
|
+
time_anchor=app_config_now,
|
|
732
|
+
field_policies=app_config_field_policies,
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
context = EmitterContext(
|
|
736
|
+
models=tuple(model_classes),
|
|
737
|
+
output=temp_out,
|
|
738
|
+
parameters={
|
|
739
|
+
"style": style_final,
|
|
740
|
+
"scope": scope_final,
|
|
741
|
+
"cases": options.cases,
|
|
742
|
+
"return_type": return_type_final,
|
|
743
|
+
},
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
generated_path: Path
|
|
747
|
+
if emit_artifact("fixtures", context):
|
|
748
|
+
generated_path = temp_out
|
|
749
|
+
else:
|
|
750
|
+
try:
|
|
751
|
+
result = emit_pytest_fixtures(
|
|
752
|
+
model_classes,
|
|
753
|
+
output_path=temp_out,
|
|
754
|
+
config=pytest_config,
|
|
755
|
+
)
|
|
756
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
757
|
+
raise EmitError(str(exc)) from exc
|
|
758
|
+
generated_path = result.path
|
|
759
|
+
if result.metadata and "constraints" in result.metadata:
|
|
760
|
+
constraint_summary = result.metadata["constraints"]
|
|
761
|
+
|
|
762
|
+
if not generated_path.exists() or generated_path.is_dir():
|
|
763
|
+
raise EmitError("Fixture emitter did not produce a file to diff.")
|
|
764
|
+
|
|
765
|
+
generated_text = generated_path.read_text(encoding="utf-8")
|
|
766
|
+
|
|
767
|
+
actual_path = output_path
|
|
768
|
+
checked_paths = [actual_path]
|
|
769
|
+
messages: list[str] = []
|
|
770
|
+
diff_outputs: list[tuple[str, str]] = []
|
|
771
|
+
|
|
772
|
+
if not actual_path.exists():
|
|
773
|
+
messages.append(f"Missing fixtures module: {actual_path}")
|
|
774
|
+
elif actual_path.is_dir():
|
|
775
|
+
messages.append(f"Fixtures path is a directory: {actual_path}")
|
|
776
|
+
else:
|
|
777
|
+
actual_text = actual_path.read_text(encoding="utf-8")
|
|
778
|
+
if actual_text != generated_text:
|
|
779
|
+
messages.append(f"Fixtures module differs: {actual_path}")
|
|
780
|
+
diff_outputs.append(
|
|
781
|
+
(
|
|
782
|
+
str(actual_path),
|
|
783
|
+
_build_unified_diff(
|
|
784
|
+
actual_text,
|
|
785
|
+
generated_text,
|
|
786
|
+
str(actual_path),
|
|
787
|
+
f"{actual_path} (generated)",
|
|
788
|
+
),
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
summary = None
|
|
793
|
+
if not messages:
|
|
794
|
+
summary = "Fixtures artifact matches."
|
|
795
|
+
|
|
796
|
+
anchor_iso = app_config_now.isoformat() if app_config_now else None
|
|
797
|
+
|
|
798
|
+
return DiffReport(
|
|
799
|
+
kind="fixtures",
|
|
800
|
+
target=output_path,
|
|
801
|
+
checked_paths=checked_paths,
|
|
802
|
+
messages=messages,
|
|
803
|
+
diff_outputs=diff_outputs,
|
|
804
|
+
summary=summary,
|
|
805
|
+
constraint_report=(
|
|
806
|
+
constraint_summary if constraint_summary and constraint_summary.get("models") else None
|
|
807
|
+
),
|
|
808
|
+
time_anchor=anchor_iso,
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _diff_schema_artifact(
|
|
813
|
+
*,
|
|
814
|
+
model_classes: list[type[BaseModel]],
|
|
815
|
+
app_config_indent: int | None,
|
|
816
|
+
options: SchemaDiffOptions,
|
|
817
|
+
) -> DiffReport:
|
|
818
|
+
if options.out is None:
|
|
819
|
+
raise DiscoveryError("Schema diff requires --schema-out.")
|
|
820
|
+
|
|
821
|
+
output_path = Path(options.out)
|
|
822
|
+
|
|
823
|
+
indent_value = options.indent if options.indent is not None else app_config_indent
|
|
824
|
+
|
|
825
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
826
|
+
temp_out = Path(tmp_dir) / "schema" / output_path.name
|
|
827
|
+
temp_out.parent.mkdir(parents=True, exist_ok=True)
|
|
828
|
+
|
|
829
|
+
context = EmitterContext(
|
|
830
|
+
models=tuple(model_classes),
|
|
831
|
+
output=temp_out,
|
|
832
|
+
parameters={"indent": indent_value},
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
if emit_artifact("schema", context):
|
|
836
|
+
generated_path = temp_out
|
|
837
|
+
else:
|
|
838
|
+
try:
|
|
839
|
+
if len(model_classes) == 1:
|
|
840
|
+
generated_path = emit_model_schema(
|
|
841
|
+
model_classes[0],
|
|
842
|
+
output_path=temp_out,
|
|
843
|
+
indent=indent_value,
|
|
844
|
+
ensure_ascii=False,
|
|
845
|
+
)
|
|
846
|
+
else:
|
|
847
|
+
generated_path = emit_models_schema(
|
|
848
|
+
model_classes,
|
|
849
|
+
output_path=temp_out,
|
|
850
|
+
indent=indent_value,
|
|
851
|
+
ensure_ascii=False,
|
|
852
|
+
)
|
|
853
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
854
|
+
raise EmitError(str(exc)) from exc
|
|
855
|
+
|
|
856
|
+
if not generated_path.exists() or generated_path.is_dir():
|
|
857
|
+
raise EmitError("Schema emitter did not produce a file to diff.")
|
|
858
|
+
|
|
859
|
+
generated_text = generated_path.read_text(encoding="utf-8")
|
|
860
|
+
|
|
861
|
+
actual_path = output_path
|
|
862
|
+
checked_paths = [actual_path]
|
|
863
|
+
messages: list[str] = []
|
|
864
|
+
diff_outputs: list[tuple[str, str]] = []
|
|
865
|
+
|
|
866
|
+
if not actual_path.exists():
|
|
867
|
+
messages.append(f"Missing schema artifact: {actual_path}")
|
|
868
|
+
elif actual_path.is_dir():
|
|
869
|
+
messages.append(f"Schema path is a directory: {actual_path}")
|
|
870
|
+
else:
|
|
871
|
+
actual_text = actual_path.read_text(encoding="utf-8")
|
|
872
|
+
if actual_text != generated_text:
|
|
873
|
+
messages.append(f"Schema artifact differs: {actual_path}")
|
|
874
|
+
diff_outputs.append(
|
|
875
|
+
(
|
|
876
|
+
str(actual_path),
|
|
877
|
+
_build_unified_diff(
|
|
878
|
+
actual_text,
|
|
879
|
+
generated_text,
|
|
880
|
+
str(actual_path),
|
|
881
|
+
f"{actual_path} (generated)",
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
summary = None
|
|
887
|
+
if not messages:
|
|
888
|
+
summary = "Schema artifact matches."
|
|
889
|
+
|
|
890
|
+
return DiffReport(
|
|
891
|
+
kind="schema",
|
|
892
|
+
target=output_path,
|
|
893
|
+
checked_paths=checked_paths,
|
|
894
|
+
messages=messages,
|
|
895
|
+
diff_outputs=diff_outputs,
|
|
896
|
+
summary=summary,
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _build_instance_generator(
|
|
901
|
+
*,
|
|
902
|
+
seed_value: int | str | None,
|
|
903
|
+
union_policy: str,
|
|
904
|
+
enum_policy: str,
|
|
905
|
+
p_none: float | None,
|
|
906
|
+
time_anchor: datetime.datetime | None,
|
|
907
|
+
) -> InstanceGenerator:
|
|
908
|
+
normalized_seed: int | None = None
|
|
909
|
+
if seed_value is not None:
|
|
910
|
+
normalized_seed = SeedManager(seed=seed_value).normalized_seed
|
|
911
|
+
|
|
912
|
+
p_none_value = p_none if p_none is not None else 0.0
|
|
913
|
+
|
|
914
|
+
gen_config = GenerationConfig(
|
|
915
|
+
seed=normalized_seed,
|
|
916
|
+
enum_policy=enum_policy,
|
|
917
|
+
union_policy=union_policy,
|
|
918
|
+
default_p_none=p_none_value,
|
|
919
|
+
optional_p_none=p_none_value,
|
|
920
|
+
time_anchor=time_anchor,
|
|
921
|
+
)
|
|
922
|
+
return InstanceGenerator(config=gen_config)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def _resolve_method(ast_mode: bool, hybrid_mode: bool) -> DiscoveryMethod:
|
|
926
|
+
if ast_mode and hybrid_mode:
|
|
927
|
+
raise DiscoveryError("Choose only one of --ast or --hybrid.")
|
|
928
|
+
if hybrid_mode:
|
|
929
|
+
return "hybrid"
|
|
930
|
+
if ast_mode:
|
|
931
|
+
return "ast"
|
|
932
|
+
return "import"
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _render_reports(
|
|
936
|
+
reports: Iterable[DiffReport],
|
|
937
|
+
show_diff: bool,
|
|
938
|
+
logger: Any,
|
|
939
|
+
json_mode: bool,
|
|
940
|
+
) -> None:
|
|
941
|
+
reports = list(reports)
|
|
942
|
+
if not reports:
|
|
943
|
+
typer.secho("No artifacts were compared.", fg=typer.colors.YELLOW)
|
|
944
|
+
return
|
|
945
|
+
|
|
946
|
+
any_changes = False
|
|
947
|
+
for report in reports:
|
|
948
|
+
if report.changed:
|
|
949
|
+
any_changes = True
|
|
950
|
+
typer.secho(f"{report.kind.upper()} differences detected:", fg=typer.colors.YELLOW)
|
|
951
|
+
for message in report.messages:
|
|
952
|
+
typer.echo(f" - {message}")
|
|
953
|
+
if show_diff:
|
|
954
|
+
for _path, diff_text in report.diff_outputs:
|
|
955
|
+
if diff_text:
|
|
956
|
+
typer.echo(diff_text.rstrip())
|
|
957
|
+
typer.echo()
|
|
958
|
+
else:
|
|
959
|
+
if report.summary:
|
|
960
|
+
typer.echo(report.summary)
|
|
961
|
+
|
|
962
|
+
if report.time_anchor:
|
|
963
|
+
typer.echo(f" Temporal anchor: {report.time_anchor}")
|
|
964
|
+
|
|
965
|
+
if report.constraint_report:
|
|
966
|
+
emit_constraint_summary(
|
|
967
|
+
report.constraint_report,
|
|
968
|
+
logger=logger,
|
|
969
|
+
json_mode=json_mode,
|
|
970
|
+
heading=f"{report.kind.upper()} constraint report",
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
if not any_changes:
|
|
974
|
+
typer.secho("All compared artifacts match.", fg=typer.colors.GREEN)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _build_unified_diff(
|
|
978
|
+
original: str,
|
|
979
|
+
regenerated: str,
|
|
980
|
+
original_label: str,
|
|
981
|
+
regenerated_label: str,
|
|
982
|
+
) -> str:
|
|
983
|
+
diff = difflib.unified_diff(
|
|
984
|
+
original.splitlines(keepends=True),
|
|
985
|
+
regenerated.splitlines(keepends=True),
|
|
986
|
+
fromfile=original_label,
|
|
987
|
+
tofile=regenerated_label,
|
|
988
|
+
)
|
|
989
|
+
return "".join(diff)
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
__all__ = ["app"]
|