pydantic-fixturegen 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pydantic-fixturegen might be problematic. Click here for more details.

Files changed (41) hide show
  1. pydantic_fixturegen/__init__.py +7 -0
  2. pydantic_fixturegen/cli/__init__.py +85 -0
  3. pydantic_fixturegen/cli/doctor.py +235 -0
  4. pydantic_fixturegen/cli/gen/__init__.py +23 -0
  5. pydantic_fixturegen/cli/gen/_common.py +139 -0
  6. pydantic_fixturegen/cli/gen/explain.py +145 -0
  7. pydantic_fixturegen/cli/gen/fixtures.py +283 -0
  8. pydantic_fixturegen/cli/gen/json.py +262 -0
  9. pydantic_fixturegen/cli/gen/schema.py +164 -0
  10. pydantic_fixturegen/cli/list.py +164 -0
  11. pydantic_fixturegen/core/__init__.py +103 -0
  12. pydantic_fixturegen/core/ast_discover.py +169 -0
  13. pydantic_fixturegen/core/config.py +440 -0
  14. pydantic_fixturegen/core/errors.py +136 -0
  15. pydantic_fixturegen/core/generate.py +311 -0
  16. pydantic_fixturegen/core/introspect.py +141 -0
  17. pydantic_fixturegen/core/io_utils.py +77 -0
  18. pydantic_fixturegen/core/providers/__init__.py +32 -0
  19. pydantic_fixturegen/core/providers/collections.py +74 -0
  20. pydantic_fixturegen/core/providers/identifiers.py +68 -0
  21. pydantic_fixturegen/core/providers/numbers.py +133 -0
  22. pydantic_fixturegen/core/providers/registry.py +98 -0
  23. pydantic_fixturegen/core/providers/strings.py +109 -0
  24. pydantic_fixturegen/core/providers/temporal.py +42 -0
  25. pydantic_fixturegen/core/safe_import.py +403 -0
  26. pydantic_fixturegen/core/schema.py +320 -0
  27. pydantic_fixturegen/core/seed.py +154 -0
  28. pydantic_fixturegen/core/strategies.py +193 -0
  29. pydantic_fixturegen/core/version.py +52 -0
  30. pydantic_fixturegen/emitters/__init__.py +15 -0
  31. pydantic_fixturegen/emitters/json_out.py +373 -0
  32. pydantic_fixturegen/emitters/pytest_codegen.py +365 -0
  33. pydantic_fixturegen/emitters/schema_out.py +84 -0
  34. pydantic_fixturegen/plugins/builtin.py +45 -0
  35. pydantic_fixturegen/plugins/hookspecs.py +59 -0
  36. pydantic_fixturegen/plugins/loader.py +72 -0
  37. pydantic_fixturegen-1.0.0.dist-info/METADATA +280 -0
  38. pydantic_fixturegen-1.0.0.dist-info/RECORD +41 -0
  39. pydantic_fixturegen-1.0.0.dist-info/WHEEL +4 -0
  40. pydantic_fixturegen-1.0.0.dist-info/entry_points.txt +5 -0
  41. pydantic_fixturegen-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,7 @@
1
+ """Top-level package for pydantic-fixturegen."""
2
+
3
+ from .core.version import get_tool_version
4
+
5
+ __all__ = ["__version__", "get_tool_version"]
6
+
7
+ __version__ = get_tool_version()
@@ -0,0 +1,85 @@
1
+ """Command line interface for pydantic-fixturegen."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ from importlib import import_module
7
+
8
+ import typer
9
+ from typer.main import get_command
10
+
11
+
12
+ def _load_typer(import_path: str) -> typer.Typer:
13
+ module_name, attr = import_path.split(":", 1)
14
+ module = import_module(module_name)
15
+ loaded = getattr(module, attr)
16
+ if not isinstance(loaded, typer.Typer):
17
+ raise TypeError(f"Attribute {attr!r} in module {module_name!r} is not a Typer app.")
18
+ return loaded
19
+
20
+
21
+ def _invoke(import_path: str, ctx: typer.Context) -> None:
22
+ sub_app = _load_typer(import_path)
23
+ command = get_command(sub_app)
24
+ args = builtins.list(ctx.args)
25
+ result = command.main(
26
+ args=args,
27
+ prog_name=ctx.command_path,
28
+ standalone_mode=False,
29
+ )
30
+ if isinstance(result, int):
31
+ raise typer.Exit(code=result)
32
+
33
+
34
+ app = typer.Typer(
35
+ help="pydantic-fixturegen command line interface",
36
+ invoke_without_command=True,
37
+ context_settings={
38
+ "allow_extra_args": True,
39
+ "ignore_unknown_options": True,
40
+ },
41
+ )
42
+
43
+
44
+ @app.callback(invoke_without_command=True)
45
+ def _root(ctx: typer.Context) -> None: # noqa: D401
46
+ if ctx.invoked_subcommand is None:
47
+ _invoke("pydantic_fixturegen.cli.list:app", ctx)
48
+ raise typer.Exit()
49
+
50
+
51
+ def _proxy(name: str, import_path: str, help_text: str) -> None:
52
+ context_settings = {
53
+ "allow_extra_args": True,
54
+ "ignore_unknown_options": True,
55
+ }
56
+
57
+ @app.command(name, context_settings=context_settings)
58
+ def command(ctx: typer.Context) -> None:
59
+ _invoke(import_path, ctx)
60
+
61
+ command.__doc__ = help_text
62
+
63
+
64
+ _proxy(
65
+ "list",
66
+ "pydantic_fixturegen.cli.list:app",
67
+ "List Pydantic models from modules or files.",
68
+ )
69
+ _proxy(
70
+ "gen",
71
+ "pydantic_fixturegen.cli.gen:app",
72
+ "Generate artifacts for discovered models.",
73
+ )
74
+ _proxy(
75
+ "doctor",
76
+ "pydantic_fixturegen.cli.doctor:app",
77
+ "Inspect models for coverage and risks.",
78
+ )
79
+ _proxy(
80
+ "explain",
81
+ "pydantic_fixturegen.cli.gen.explain:app",
82
+ "Explain generation strategies per model field.",
83
+ )
84
+
85
+ __all__ = ["app"]
@@ -0,0 +1,235 @@
1
+ """CLI command for inspecting project health."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from pydantic import BaseModel
10
+
11
+ from pydantic_fixturegen.core.errors import DiscoveryError, PFGError
12
+ from pydantic_fixturegen.core.providers import create_default_registry
13
+ from pydantic_fixturegen.core.schema import FieldSummary, summarize_model_fields
14
+ from pydantic_fixturegen.core.strategies import StrategyBuilder, StrategyResult, UnionStrategy
15
+ from pydantic_fixturegen.plugins.loader import get_plugin_manager
16
+
17
+ from .gen._common import (
18
+ JSON_ERRORS_OPTION,
19
+ DiscoveryMethod,
20
+ clear_module_cache,
21
+ discover_models,
22
+ load_model_class,
23
+ render_cli_error,
24
+ split_patterns,
25
+ )
26
+
27
+ PATH_ARGUMENT = typer.Argument(..., help="Path to a Python module containing Pydantic models.")
28
+
29
+ INCLUDE_OPTION = typer.Option(
30
+ None,
31
+ "--include",
32
+ "-i",
33
+ help="Comma-separated pattern(s) of fully-qualified model names to include.",
34
+ )
35
+
36
+ EXCLUDE_OPTION = typer.Option(
37
+ None,
38
+ "--exclude",
39
+ "-e",
40
+ help="Comma-separated pattern(s) of fully-qualified model names to exclude.",
41
+ )
42
+
43
+ AST_OPTION = typer.Option(False, "--ast", help="Use AST discovery only (no imports executed).")
44
+
45
+ HYBRID_OPTION = typer.Option(False, "--hybrid", help="Combine AST and safe import discovery.")
46
+
47
+ TIMEOUT_OPTION = typer.Option(
48
+ 5.0,
49
+ "--timeout",
50
+ min=0.1,
51
+ help="Timeout in seconds for safe import execution.",
52
+ )
53
+
54
+ MEMORY_LIMIT_OPTION = typer.Option(
55
+ 256,
56
+ "--memory-limit-mb",
57
+ min=1,
58
+ help="Memory limit in megabytes for safe import subprocess.",
59
+ )
60
+
61
+
62
+ app = typer.Typer(invoke_without_command=True, subcommand_metavar="")
63
+
64
+
65
+ @dataclass
66
+ class ModelReport:
67
+ model: type[BaseModel]
68
+ coverage: tuple[int, int]
69
+ issues: list[str]
70
+
71
+
72
+ def doctor( # noqa: D401 - Typer callback
73
+ ctx: typer.Context,
74
+ path: str = PATH_ARGUMENT,
75
+ include: str | None = INCLUDE_OPTION,
76
+ exclude: str | None = EXCLUDE_OPTION,
77
+ ast_mode: bool = AST_OPTION,
78
+ hybrid_mode: bool = HYBRID_OPTION,
79
+ timeout: float = TIMEOUT_OPTION,
80
+ memory_limit_mb: int = MEMORY_LIMIT_OPTION,
81
+ json_errors: bool = JSON_ERRORS_OPTION,
82
+ ) -> None:
83
+ _ = ctx # unused
84
+ try:
85
+ _execute_doctor(
86
+ target=path,
87
+ include=include,
88
+ exclude=exclude,
89
+ ast_mode=ast_mode,
90
+ hybrid_mode=hybrid_mode,
91
+ timeout=timeout,
92
+ memory_limit_mb=memory_limit_mb,
93
+ )
94
+ except PFGError as exc:
95
+ render_cli_error(exc, json_errors=json_errors)
96
+
97
+
98
+ app.callback(invoke_without_command=True)(doctor)
99
+
100
+
101
+ def _resolve_method(ast_mode: bool, hybrid_mode: bool) -> DiscoveryMethod:
102
+ if ast_mode and hybrid_mode:
103
+ raise DiscoveryError("Choose only one of --ast or --hybrid.")
104
+ if hybrid_mode:
105
+ return "hybrid"
106
+ if ast_mode:
107
+ return "ast"
108
+ return "import"
109
+
110
+
111
+ def _execute_doctor(
112
+ *,
113
+ target: str,
114
+ include: str | None,
115
+ exclude: str | None,
116
+ ast_mode: bool,
117
+ hybrid_mode: bool,
118
+ timeout: float,
119
+ memory_limit_mb: int,
120
+ ) -> None:
121
+ path = Path(target)
122
+ if not path.exists():
123
+ raise DiscoveryError(f"Target path '{target}' does not exist.", details={"path": target})
124
+ if not path.is_file():
125
+ raise DiscoveryError("Target must be a Python module file.", details={"path": target})
126
+
127
+ clear_module_cache()
128
+
129
+ method = _resolve_method(ast_mode, hybrid_mode)
130
+ discovery = discover_models(
131
+ path,
132
+ include=split_patterns(include),
133
+ exclude=split_patterns(exclude),
134
+ method=method,
135
+ timeout=timeout,
136
+ memory_limit_mb=memory_limit_mb,
137
+ )
138
+
139
+ if discovery.errors:
140
+ raise DiscoveryError("; ".join(discovery.errors))
141
+
142
+ for warning in discovery.warnings:
143
+ if warning.strip():
144
+ typer.secho(f"warning: {warning.strip()}", err=True, fg=typer.colors.YELLOW)
145
+
146
+ if not discovery.models:
147
+ typer.echo("No models discovered.")
148
+ return
149
+
150
+ registry = create_default_registry(load_plugins=True)
151
+ builder = StrategyBuilder(registry, plugin_manager=get_plugin_manager())
152
+
153
+ reports: list[ModelReport] = []
154
+ for model_info in discovery.models:
155
+ try:
156
+ model_cls = load_model_class(model_info)
157
+ except RuntimeError as exc:
158
+ raise DiscoveryError(str(exc)) from exc
159
+ reports.append(_analyse_model(model_cls, builder))
160
+
161
+ _render_report(reports)
162
+
163
+
164
+ def _analyse_model(model: type[BaseModel], builder: StrategyBuilder) -> ModelReport:
165
+ total_fields = 0
166
+ covered_fields = 0
167
+ issues: list[str] = []
168
+
169
+ try:
170
+ strategies = builder.build_model_strategies(model)
171
+ except ValueError as exc: # missing provider
172
+ summaries = summarize_model_fields(model)
173
+ message = str(exc)
174
+ return ModelReport(
175
+ model=model,
176
+ coverage=(0, len(summaries)),
177
+ issues=[message],
178
+ )
179
+
180
+ summaries = summarize_model_fields(model)
181
+
182
+ for field_name, strategy in strategies.items():
183
+ total_fields += 1
184
+ summary = summaries[field_name]
185
+ covered, field_issues = _strategy_status(summary, strategy)
186
+ if covered:
187
+ covered_fields += 1
188
+ issues.extend(f"{model.__name__}.{field_name}: {msg}" for msg in field_issues)
189
+
190
+ return ModelReport(model=model, coverage=(covered_fields, total_fields), issues=issues)
191
+
192
+
193
+ def _strategy_status(summary: FieldSummary, strategy: StrategyResult) -> tuple[bool, list[str]]:
194
+ if isinstance(strategy, UnionStrategy):
195
+ issue_messages: list[str] = []
196
+ covered = True
197
+ for choice in strategy.choices:
198
+ choice_ok, choice_issues = _strategy_status(choice.summary, choice)
199
+ if not choice_ok:
200
+ covered = False
201
+ issue_messages.extend(choice_issues)
202
+ return covered, issue_messages
203
+
204
+ messages: list[str] = []
205
+ if strategy.enum_values:
206
+ return True, messages
207
+
208
+ if summary.type in {"model", "dataclass"}:
209
+ return True, messages
210
+
211
+ if strategy.provider_ref is None:
212
+ messages.append(f"no provider for type '{summary.type}'")
213
+ return False, messages
214
+
215
+ if summary.type == "any":
216
+ messages.append("falls back to generic type")
217
+ return True, messages
218
+
219
+
220
+ def _render_report(reports: list[ModelReport]) -> None:
221
+ for report in reports:
222
+ covered, total = report.coverage
223
+ coverage_pct = (covered / total * 100) if total else 100.0
224
+ typer.echo(f"Model: {report.model.__module__}.{report.model.__name__}")
225
+ typer.echo(f" Coverage: {covered}/{total} fields ({coverage_pct:.0f}%)")
226
+ if report.issues:
227
+ typer.echo(" Issues:")
228
+ for issue in report.issues:
229
+ typer.echo(f" - {issue}")
230
+ else:
231
+ typer.echo(" Issues: none")
232
+ typer.echo("")
233
+
234
+
235
+ __all__ = ["app"]
@@ -0,0 +1,23 @@
1
+ """Grouping for generation-related CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .explain import app as explain_app
8
+ from .fixtures import register as register_fixtures
9
+ from .json import register as register_json
10
+ from .schema import register as register_schema
11
+
12
+ app = typer.Typer(help="Generate data artifacts from Pydantic models.")
13
+
14
+ register_json(app)
15
+ register_schema(app)
16
+ register_fixtures(app)
17
+ app.add_typer(
18
+ explain_app,
19
+ name="explain",
20
+ help="Explain generation strategy per model field.",
21
+ )
22
+
23
+ __all__ = ["app"]
@@ -0,0 +1,139 @@
1
+ """Shared helpers for generation CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import importlib.util
7
+ import json
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+ from typing import Literal
13
+
14
+ import typer
15
+ from pydantic import BaseModel
16
+
17
+ from pydantic_fixturegen.core.errors import PFGError
18
+ from pydantic_fixturegen.core.introspect import (
19
+ IntrospectedModel,
20
+ IntrospectionResult,
21
+ discover,
22
+ )
23
+
24
+ __all__ = [
25
+ "JSON_ERRORS_OPTION",
26
+ "clear_module_cache",
27
+ "discover_models",
28
+ "load_model_class",
29
+ "render_cli_error",
30
+ "split_patterns",
31
+ ]
32
+
33
+
34
+ _module_cache: dict[str, ModuleType] = {}
35
+
36
+
37
+ JSON_ERRORS_OPTION = typer.Option(
38
+ False,
39
+ "--json-errors",
40
+ help="Emit structured JSON errors to stdout.",
41
+ )
42
+
43
+
44
+ def clear_module_cache() -> None:
45
+ """Clear cached module imports used during CLI execution."""
46
+
47
+ _module_cache.clear()
48
+
49
+
50
+ def split_patterns(raw: str | None) -> list[str]:
51
+ if not raw:
52
+ return []
53
+ return [part.strip() for part in raw.split(",") if part.strip()]
54
+
55
+
56
+ DiscoveryMethod = Literal["ast", "import", "hybrid"]
57
+
58
+
59
+ def discover_models(
60
+ path: Path,
61
+ *,
62
+ include: Sequence[str] | None = None,
63
+ exclude: Sequence[str] | None = None,
64
+ method: DiscoveryMethod = "import",
65
+ timeout: float = 5.0,
66
+ memory_limit_mb: int = 256,
67
+ ) -> IntrospectionResult:
68
+ return discover(
69
+ [path],
70
+ method=method,
71
+ include=list(include or ()),
72
+ exclude=list(exclude or ()),
73
+ public_only=False,
74
+ safe_import_timeout=timeout,
75
+ safe_import_memory_limit_mb=memory_limit_mb,
76
+ )
77
+
78
+
79
+ def load_model_class(model_info: IntrospectedModel) -> type[BaseModel]:
80
+ module = _load_module(model_info.module, Path(model_info.locator))
81
+ attr = getattr(module, model_info.name, None)
82
+ if not isinstance(attr, type) or not issubclass(attr, BaseModel):
83
+ raise RuntimeError(
84
+ f"Attribute {model_info.name!r} in module "
85
+ f"{module.__name__} is not a Pydantic BaseModel."
86
+ )
87
+ return attr
88
+
89
+
90
+ def render_cli_error(error: PFGError, *, json_errors: bool) -> None:
91
+ if json_errors:
92
+ payload = {"error": error.to_payload()}
93
+ typer.echo(json.dumps(payload, indent=2))
94
+ else:
95
+ typer.secho(f"{error.kind}: {error}", err=True, fg=typer.colors.RED)
96
+ if error.hint:
97
+ typer.secho(f"hint: {error.hint}", err=True, fg=typer.colors.YELLOW)
98
+ raise typer.Exit(code=int(error.code))
99
+
100
+
101
+ def _load_module(module_name: str, locator: Path) -> ModuleType:
102
+ module = _module_cache.get(module_name)
103
+ if module is not None:
104
+ return module
105
+
106
+ existing = sys.modules.get(module_name)
107
+ if existing is not None:
108
+ existing_path = getattr(existing, "__file__", None)
109
+ if existing_path and Path(existing_path).resolve() == locator.resolve():
110
+ _module_cache[module_name] = existing
111
+ return existing
112
+
113
+ return _import_module_by_path(module_name, locator)
114
+
115
+
116
+ def _import_module_by_path(module_name: str, path: Path) -> ModuleType:
117
+ if not path.exists():
118
+ raise RuntimeError(f"Could not locate module source at {path}.")
119
+
120
+ sys_path_entry = str(path.parent.resolve())
121
+ if sys_path_entry not in sys.path:
122
+ sys.path.insert(0, sys_path_entry)
123
+
124
+ unique_name = module_name
125
+ if module_name in sys.modules:
126
+ unique_name = f"{module_name}_pfg_{len(_module_cache)}"
127
+
128
+ spec = importlib.util.spec_from_file_location(unique_name, path)
129
+ if spec is None or spec.loader is None:
130
+ raise RuntimeError(f"Failed to load module from {path}.")
131
+
132
+ module = importlib.util.module_from_spec(spec)
133
+ try:
134
+ spec.loader.exec_module(module)
135
+ except Exception as exc: # pragma: no cover - surface to caller
136
+ raise RuntimeError(f"Error importing module {path}: {exc}") from exc
137
+
138
+ _module_cache[module_name] = module
139
+ return module
@@ -0,0 +1,145 @@
1
+ """CLI command to explain strategies for models and fields."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from pydantic import BaseModel
9
+
10
+ from pydantic_fixturegen.core.errors import DiscoveryError, PFGError
11
+ from pydantic_fixturegen.core.providers import create_default_registry
12
+ from pydantic_fixturegen.core.schema import summarize_model_fields
13
+ from pydantic_fixturegen.core.strategies import StrategyBuilder, StrategyResult, UnionStrategy
14
+ from pydantic_fixturegen.plugins.loader import get_plugin_manager
15
+
16
+ from ._common import (
17
+ JSON_ERRORS_OPTION,
18
+ clear_module_cache,
19
+ discover_models,
20
+ load_model_class,
21
+ render_cli_error,
22
+ split_patterns,
23
+ )
24
+
25
+ PATH_ARGUMENT = typer.Argument(..., help="Path to a Python module containing Pydantic models.")
26
+
27
+ INCLUDE_OPTION = typer.Option(
28
+ None,
29
+ "--include",
30
+ "-i",
31
+ help="Comma-separated pattern(s) of fully-qualified model names to include.",
32
+ )
33
+
34
+ EXCLUDE_OPTION = typer.Option(
35
+ None,
36
+ "--exclude",
37
+ "-e",
38
+ help="Comma-separated pattern(s) of fully-qualified model names to exclude.",
39
+ )
40
+
41
+
42
+ app = typer.Typer(invoke_without_command=True, subcommand_metavar="")
43
+
44
+
45
+ def explain( # noqa: D401 - Typer callback
46
+ ctx: typer.Context,
47
+ path: str = PATH_ARGUMENT,
48
+ include: str | None = INCLUDE_OPTION,
49
+ exclude: str | None = EXCLUDE_OPTION,
50
+ json_errors: bool = JSON_ERRORS_OPTION,
51
+ ) -> None:
52
+ _ = ctx # unused
53
+ try:
54
+ _execute_explain(
55
+ target=path,
56
+ include=include,
57
+ exclude=exclude,
58
+ )
59
+ except PFGError as exc:
60
+ render_cli_error(exc, json_errors=json_errors)
61
+ except ValueError as exc:
62
+ render_cli_error(DiscoveryError(str(exc)), json_errors=json_errors)
63
+
64
+
65
+ app.callback(invoke_without_command=True)(explain)
66
+
67
+
68
+ def _execute_explain(*, target: str, include: str | None, exclude: str | None) -> None:
69
+ path = Path(target)
70
+ if not path.exists():
71
+ raise ValueError(f"Target path '{target}' does not exist.")
72
+ if not path.is_file():
73
+ raise ValueError("Target must be a Python module file.")
74
+
75
+ clear_module_cache()
76
+
77
+ discovery = discover_models(
78
+ path,
79
+ include=split_patterns(include),
80
+ exclude=split_patterns(exclude),
81
+ )
82
+
83
+ if discovery.errors:
84
+ raise ValueError("; ".join(discovery.errors))
85
+
86
+ for warning in discovery.warnings:
87
+ if warning.strip():
88
+ typer.secho(f"warning: {warning.strip()}", err=True, fg=typer.colors.YELLOW)
89
+
90
+ if not discovery.models:
91
+ typer.echo("No models discovered.")
92
+ return
93
+
94
+ registry = create_default_registry(load_plugins=True)
95
+ builder = StrategyBuilder(registry, plugin_manager=get_plugin_manager())
96
+
97
+ for model_info in discovery.models:
98
+ model_cls = load_model_class(model_info)
99
+ _render_model_report(model_cls, builder)
100
+
101
+
102
+ def _render_model_report(model: type[BaseModel], builder: StrategyBuilder) -> None:
103
+ typer.echo(f"Model: {model.__module__}.{model.__name__}")
104
+ summaries = summarize_model_fields(model)
105
+ strategies = {}
106
+ field_failures: dict[str, str] = {}
107
+ for name, model_field in model.model_fields.items():
108
+ summary = summaries[name]
109
+ try:
110
+ strategies[name] = builder.build_field_strategy(
111
+ model,
112
+ name,
113
+ model_field.annotation,
114
+ summary,
115
+ )
116
+ except ValueError as exc:
117
+ field_failures[name] = str(exc)
118
+
119
+ for field_name, strategy in strategies.items():
120
+ summary = summaries[field_name]
121
+ typer.echo(f" Field: {field_name}")
122
+ typer.echo(f" Type: {summary.type}")
123
+ _render_strategy(strategy, indent=" ")
124
+ for field_name, message in field_failures.items():
125
+ typer.echo(f" Field: {field_name}")
126
+ typer.echo(" Type: unknown")
127
+ typer.echo(f" Issue: {message}")
128
+ typer.echo("")
129
+
130
+
131
+ def _render_strategy(strategy: StrategyResult, indent: str) -> None:
132
+ if isinstance(strategy, UnionStrategy):
133
+ typer.echo(f"{indent}Union policy: {strategy.policy}")
134
+ for idx, choice in enumerate(strategy.choices, start=1):
135
+ typer.echo(f"{indent} Option {idx} -> type: {choice.summary.type}")
136
+ _render_strategy(choice, indent=f"{indent} ")
137
+ return
138
+
139
+ typer.echo(f"{indent}Provider: {strategy.provider_name}")
140
+ if strategy.enum_values:
141
+ typer.echo(f"{indent}Enum values: {strategy.enum_values}")
142
+ typer.echo(f"{indent}p_none: {strategy.p_none}")
143
+
144
+
145
+ __all__ = ["app"]