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,164 @@
1
+ """CLI command for listing Pydantic models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from pydantic_fixturegen.core.errors import DiscoveryError, PFGError, UnsafeImportError
10
+ from pydantic_fixturegen.core.introspect import DiscoveryMethod, IntrospectionResult, discover
11
+
12
+ from .gen._common import JSON_ERRORS_OPTION, render_cli_error, split_patterns
13
+
14
+ PATH_ARGUMENT = typer.Argument(
15
+ ...,
16
+ help="Python module file to inspect.",
17
+ )
18
+
19
+ INCLUDE_OPTION = typer.Option(
20
+ None,
21
+ "--include",
22
+ "-i",
23
+ help="Comma-separated glob pattern(s) of fully-qualified model names to include.",
24
+ )
25
+
26
+ EXCLUDE_OPTION = typer.Option(
27
+ None,
28
+ "--exclude",
29
+ "-e",
30
+ help="Comma-separated glob pattern(s) of fully-qualified model names to exclude.",
31
+ )
32
+
33
+ PUBLIC_OPTION = typer.Option(
34
+ False,
35
+ "--public-only",
36
+ help="Only list public models (respects __all__).",
37
+ )
38
+
39
+ AST_OPTION = typer.Option(
40
+ False,
41
+ "--ast",
42
+ help="Use AST discovery only (no imports executed).",
43
+ )
44
+
45
+ HYBRID_OPTION = typer.Option(
46
+ False,
47
+ "--hybrid",
48
+ help="Combine AST and safe import discovery.",
49
+ )
50
+
51
+ TIMEOUT_OPTION = typer.Option(
52
+ 5.0,
53
+ "--timeout",
54
+ min=0.1,
55
+ help="Timeout in seconds for safe import execution.",
56
+ )
57
+
58
+ MEMORY_LIMIT_OPTION = typer.Option(
59
+ 256,
60
+ "--memory-limit-mb",
61
+ min=1,
62
+ help="Memory limit in megabytes for safe import subprocess.",
63
+ )
64
+
65
+
66
+ app = typer.Typer(invoke_without_command=True, subcommand_metavar="")
67
+
68
+
69
+ def _resolve_method(ast_mode: bool, hybrid_mode: bool) -> DiscoveryMethod:
70
+ if ast_mode and hybrid_mode:
71
+ raise DiscoveryError("Choose only one of --ast or --hybrid.")
72
+ if hybrid_mode:
73
+ return "hybrid"
74
+ if ast_mode:
75
+ return "ast"
76
+ return "import"
77
+
78
+
79
+ def list_models( # noqa: D401 - Typer callback
80
+ ctx: typer.Context,
81
+ path: str = PATH_ARGUMENT,
82
+ include: str | None = INCLUDE_OPTION,
83
+ exclude: str | None = EXCLUDE_OPTION,
84
+ public_only: bool = PUBLIC_OPTION,
85
+ ast_mode: bool = AST_OPTION,
86
+ hybrid_mode: bool = HYBRID_OPTION,
87
+ timeout: float = TIMEOUT_OPTION,
88
+ memory_limit_mb: int = MEMORY_LIMIT_OPTION,
89
+ json_errors: bool = JSON_ERRORS_OPTION,
90
+ ) -> None:
91
+ _ = ctx # unused
92
+ try:
93
+ _execute_list_command(
94
+ target=path,
95
+ include=include,
96
+ exclude=exclude,
97
+ public_only=public_only,
98
+ ast_mode=ast_mode,
99
+ hybrid_mode=hybrid_mode,
100
+ timeout=timeout,
101
+ memory_limit_mb=memory_limit_mb,
102
+ )
103
+ except PFGError as exc:
104
+ render_cli_error(exc, json_errors=json_errors)
105
+
106
+
107
+ app.callback(invoke_without_command=True)(list_models)
108
+
109
+
110
+ def _execute_list_command(
111
+ *,
112
+ target: str,
113
+ include: str | None,
114
+ exclude: str | None,
115
+ public_only: bool,
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
+ method = _resolve_method(ast_mode, hybrid_mode)
128
+
129
+ result = discover(
130
+ [path],
131
+ method=method,
132
+ include=_split_patterns(include),
133
+ exclude=_split_patterns(exclude),
134
+ public_only=public_only,
135
+ safe_import_timeout=timeout,
136
+ safe_import_memory_limit_mb=memory_limit_mb,
137
+ )
138
+ _render_result(result)
139
+
140
+
141
+ def _render_result(result: IntrospectionResult) -> None:
142
+ for warning in result.warnings:
143
+ if warning.strip():
144
+ typer.secho(f"warning: {warning.strip()}", err=True, fg=typer.colors.YELLOW)
145
+
146
+ if result.errors:
147
+ message = "; ".join(result.errors)
148
+ if any("network" in error.lower() for error in result.errors):
149
+ raise UnsafeImportError(message)
150
+ raise DiscoveryError(message)
151
+
152
+ if not result.models:
153
+ typer.echo("No models discovered.")
154
+ return
155
+
156
+ for model in result.models:
157
+ typer.echo(f"{model.qualname} [{model.discovery}]")
158
+
159
+
160
+ def _split_patterns(option_value: str | None) -> list[str]:
161
+ return split_patterns(option_value)
162
+
163
+
164
+ __all__ = ["app"]
@@ -0,0 +1,103 @@
1
+ """Core utilities for pydantic-fixturegen."""
2
+
3
+ from .ast_discover import AstDiscoveryResult, AstModel, discover_models
4
+ from .config import (
5
+ AppConfig,
6
+ ConfigError,
7
+ EmittersConfig,
8
+ JsonConfig,
9
+ PytestEmitterConfig,
10
+ load_config,
11
+ )
12
+ from .errors import (
13
+ DiscoveryError,
14
+ EmitError,
15
+ ErrorCode,
16
+ MappingError,
17
+ PFGError,
18
+ UnsafeImportError,
19
+ )
20
+ from .generate import GenerationConfig, InstanceGenerator
21
+ from .introspect import IntrospectedModel, IntrospectionResult
22
+ from .introspect import discover as introspect
23
+ from .io_utils import WriteResult, write_atomic_bytes, write_atomic_text
24
+ from .providers import (
25
+ ProviderRef,
26
+ ProviderRegistry,
27
+ create_default_registry,
28
+ register_collection_providers,
29
+ register_identifier_providers,
30
+ register_numeric_providers,
31
+ register_string_providers,
32
+ register_temporal_providers,
33
+ )
34
+ from .providers.collections import generate_collection
35
+ from .providers.identifiers import generate_identifier
36
+ from .providers.numbers import generate_numeric
37
+ from .providers.strings import generate_string
38
+ from .providers.temporal import generate_temporal
39
+ from .safe_import import SafeImportResult, safe_import_models
40
+ from .schema import (
41
+ FieldConstraints,
42
+ FieldSummary,
43
+ extract_constraints,
44
+ extract_model_constraints,
45
+ summarize_field,
46
+ summarize_model_fields,
47
+ )
48
+ from .seed import SeedManager
49
+ from .strategies import Strategy, StrategyBuilder, UnionStrategy
50
+ from .version import build_artifact_header, get_tool_version
51
+
52
+ __all__ = [
53
+ "AppConfig",
54
+ "AstDiscoveryResult",
55
+ "AstModel",
56
+ "ConfigError",
57
+ "EmittersConfig",
58
+ "JsonConfig",
59
+ "PytestEmitterConfig",
60
+ "SafeImportResult",
61
+ "SeedManager",
62
+ "WriteResult",
63
+ "FieldConstraints",
64
+ "FieldSummary",
65
+ "ProviderRef",
66
+ "ProviderRegistry",
67
+ "Strategy",
68
+ "StrategyBuilder",
69
+ "UnionStrategy",
70
+ "create_default_registry",
71
+ "generate_collection",
72
+ "generate_identifier",
73
+ "generate_numeric",
74
+ "generate_string",
75
+ "generate_temporal",
76
+ "GenerationConfig",
77
+ "InstanceGenerator",
78
+ "register_string_providers",
79
+ "register_numeric_providers",
80
+ "register_collection_providers",
81
+ "register_identifier_providers",
82
+ "register_temporal_providers",
83
+ "build_artifact_header",
84
+ "discover_models",
85
+ "introspect",
86
+ "IntrospectedModel",
87
+ "IntrospectionResult",
88
+ "get_tool_version",
89
+ "load_config",
90
+ "extract_constraints",
91
+ "extract_model_constraints",
92
+ "summarize_field",
93
+ "summarize_model_fields",
94
+ "safe_import_models",
95
+ "write_atomic_text",
96
+ "write_atomic_bytes",
97
+ "PFGError",
98
+ "DiscoveryError",
99
+ "MappingError",
100
+ "EmitError",
101
+ "UnsafeImportError",
102
+ "ErrorCode",
103
+ ]
@@ -0,0 +1,169 @@
1
+ """AST-based discovery of Pydantic model classes without executing modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from collections.abc import Iterable
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class AstModel:
13
+ module: str
14
+ name: str
15
+ qualname: str
16
+ path: Path
17
+ lineno: int
18
+ is_public: bool
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class AstDiscoveryResult:
23
+ models: list[AstModel]
24
+ warnings: list[str]
25
+
26
+
27
+ PYDANTIC_BASES = {
28
+ ("pydantic", "BaseModel"),
29
+ ("pydantic", "RootModel"),
30
+ ("pydantic.v1", "BaseModel"),
31
+ ("pydantic.v1", "RootModel"),
32
+ }
33
+
34
+
35
+ def discover_models(
36
+ paths: Iterable[Path | str],
37
+ *,
38
+ infer_module: bool = False,
39
+ public_only: bool = False,
40
+ ) -> AstDiscoveryResult:
41
+ """Discover Pydantic models from the given Python source files using AST parsing."""
42
+ models: list[AstModel] = []
43
+ warnings: list[str] = []
44
+
45
+ for raw_path in paths:
46
+ path = Path(raw_path)
47
+ try:
48
+ source = path.read_text(encoding="utf-8")
49
+ except OSError as exc:
50
+ warnings.append(f"Failed to read {path}: {exc}")
51
+ continue
52
+
53
+ try:
54
+ tree = ast.parse(source, filename=str(path))
55
+ except SyntaxError as exc:
56
+ warnings.append(f"Failed to parse {path}: {exc}")
57
+ continue
58
+
59
+ resolver = _ImportResolver()
60
+ resolver.visit(tree)
61
+
62
+ module_name = path.stem if infer_module else "unknown"
63
+ public_names = _extract_public_names(tree)
64
+
65
+ for class_node in [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]:
66
+ if not _is_pydantic_model(class_node, resolver):
67
+ continue
68
+
69
+ if public_names is not None:
70
+ is_public = class_node.name in public_names
71
+ else:
72
+ is_public = not class_node.name.startswith("_")
73
+ if public_only and not is_public:
74
+ continue
75
+
76
+ model = AstModel(
77
+ module=module_name,
78
+ name=class_node.name,
79
+ qualname=f"{module_name}.{class_node.name}",
80
+ path=path,
81
+ lineno=class_node.lineno,
82
+ is_public=is_public,
83
+ )
84
+ models.append(model)
85
+
86
+ models.sort(key=lambda m: (m.module, m.name))
87
+ return AstDiscoveryResult(models=models, warnings=warnings)
88
+
89
+
90
+ class _ImportResolver(ast.NodeVisitor):
91
+ def __init__(self) -> None:
92
+ self.aliases: dict[str, tuple[str, str]] = {}
93
+
94
+ def visit_Import(self, node: ast.Import) -> None:
95
+ for alias in node.names:
96
+ name = alias.asname or alias.name
97
+ base_name = alias.name.split(".")[0]
98
+ for module, _ in PYDANTIC_BASES:
99
+ if base_name == module:
100
+ self.aliases[name] = (module, "*")
101
+
102
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
103
+ module = node.module
104
+ if module is None:
105
+ return
106
+ base_module = module.split(".")[0]
107
+ for alias in node.names:
108
+ target_name = alias.asname or alias.name
109
+ qual = alias.name
110
+ if module in {"pydantic", "pydantic.v1"} and qual in {"BaseModel", "RootModel"}:
111
+ self.aliases[target_name] = (module, qual)
112
+ elif base_module in {"pydantic", "pydantic.v1"}:
113
+ self.aliases[target_name] = (base_module, "*")
114
+
115
+
116
+ def _extract_public_names(tree: ast.AST) -> set[str] | None:
117
+ for node in tree.body if isinstance(tree, ast.Module) else []:
118
+ if isinstance(node, ast.Assign):
119
+ for target in node.targets:
120
+ if (
121
+ isinstance(target, ast.Name)
122
+ and target.id == "__all__"
123
+ and isinstance(node.value, (ast.List, ast.Tuple))
124
+ ):
125
+ names: set[str] = set()
126
+ for element in node.value.elts:
127
+ if isinstance(element, ast.Constant) and isinstance(element.value, str):
128
+ names.add(element.value)
129
+ return names
130
+ return None
131
+
132
+
133
+ def _is_pydantic_model(node: ast.ClassDef, resolver: _ImportResolver) -> bool:
134
+ for base in node.bases:
135
+ module_class = _resolve_base(base, resolver)
136
+ if module_class is None:
137
+ continue
138
+ module, class_name = module_class
139
+ if (module, class_name) in PYDANTIC_BASES:
140
+ return True
141
+ return False
142
+
143
+
144
+ def _resolve_base(base: ast.expr, resolver: _ImportResolver) -> tuple[str, str] | None:
145
+ if isinstance(base, ast.Name):
146
+ return resolver.aliases.get(base.id)
147
+ if isinstance(base, ast.Attribute):
148
+ parts = _flatten_attribute(base)
149
+ if len(parts) >= 2:
150
+ module_candidate = parts[0]
151
+ class_candidate = parts[-1]
152
+ alias = resolver.aliases.get(module_candidate)
153
+ if alias:
154
+ module, _ = alias
155
+ return (module, class_candidate)
156
+ if module_candidate in {"pydantic", "pydantic.v1"}:
157
+ return (module_candidate, class_candidate)
158
+ return None
159
+
160
+
161
+ def _flatten_attribute(attr: ast.Attribute) -> list[str]:
162
+ parts: list[str] = []
163
+ current: ast.AST | None = attr
164
+ while isinstance(current, ast.Attribute):
165
+ parts.insert(0, current.attr)
166
+ current = current.value
167
+ if isinstance(current, ast.Name):
168
+ parts.insert(0, current.id)
169
+ return parts