conformly 0.7.2__tar.gz → 0.7.3__tar.gz
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.
- {conformly-0.7.2/src/conformly.egg-info → conformly-0.7.3}/PKG-INFO +19 -2
- {conformly-0.7.2 → conformly-0.7.3}/README.md +18 -1
- {conformly-0.7.2 → conformly-0.7.3}/pyproject.toml +2 -1
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/case.py +9 -7
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/cases.py +5 -4
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/context.py +1 -1
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/orchestration.py +18 -13
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/url.py +3 -1
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/uuid.py +6 -6
- {conformly-0.7.2 → conformly-0.7.3/src/conformly.egg-info}/PKG-INFO +19 -2
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/SOURCES.txt +1 -0
- conformly-0.7.3/tests/test_collection.py +29 -0
- {conformly-0.7.2 → conformly-0.7.3}/LICENSE +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/setup.cfg +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/_utils.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/path.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/numeric.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/fields/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/registry.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/_utils.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/float.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/integer.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/list.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/string.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/tuple.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/core/element.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/core/field.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/types.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/models.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/field.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/model.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/session.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/models.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/resolve.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/factory.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/tracer.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/primitives.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/strategies.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/py.typed +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly/tracer/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/top_level.txt +0 -0
- {conformly-0.7.2 → conformly-0.7.3}/tests/test_path.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conformly
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Generate valid & invalid test data from your typed schemas
|
|
5
5
|
Author-email: Nikita Shabanov <nik.shabanov2024@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -252,6 +252,12 @@ overrides=[
|
|
|
252
252
|
]
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
+
Integer seeds, including `0` and negative values, make `case()` and `cases()`
|
|
256
|
+
reproducible for internal generation, including nested models and UUID values.
|
|
257
|
+
The same model, options, and seed produce the same payloads. User-provided default
|
|
258
|
+
factories run once per generated field and must themselves be deterministic;
|
|
259
|
+
the seed does not control their randomness or external state.
|
|
260
|
+
|
|
255
261
|
## Error Handling
|
|
256
262
|
|
|
257
263
|
All `conformly` errors inherit from `ConformlyError`, providing consistent interface for debugging and programmatic handling.
|
|
@@ -648,7 +654,15 @@ print(trace.generated_value)
|
|
|
648
654
|
print(trace.seed)
|
|
649
655
|
```
|
|
650
656
|
|
|
651
|
-
|
|
657
|
+
Tracing is available for `case(..., valid=False)`. The trace describes the selected
|
|
658
|
+
target: its dotted path, violation, generated value, seed, and value source. This
|
|
659
|
+
applies to DSL and string paths as well as `"first"` and `"random"` selection.
|
|
660
|
+
Unrelated fields, defaults, and overrides do not overwrite target metadata. For
|
|
661
|
+
`MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
|
|
662
|
+
absent from the payload.
|
|
663
|
+
|
|
664
|
+
Enabling tracing leaves payloads, internal RNG consumption, and default factory
|
|
665
|
+
call counts unchanged. A reused tracer describes the latest completed generation.
|
|
652
666
|
|
|
653
667
|
|
|
654
668
|
## Development
|
|
@@ -737,3 +751,6 @@ Contributions welcome!
|
|
|
737
751
|
- Add tests for new functionality
|
|
738
752
|
- Run `uv run -m pytest` and `uv run -m ruff check .`
|
|
739
753
|
- Submit a pull request
|
|
754
|
+
|
|
755
|
+
Test collection and assertion audit findings, including deferred collection checks,
|
|
756
|
+
are documented in [the test audit](docs/test-audit.md).
|
|
@@ -219,6 +219,12 @@ overrides=[
|
|
|
219
219
|
]
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
Integer seeds, including `0` and negative values, make `case()` and `cases()`
|
|
223
|
+
reproducible for internal generation, including nested models and UUID values.
|
|
224
|
+
The same model, options, and seed produce the same payloads. User-provided default
|
|
225
|
+
factories run once per generated field and must themselves be deterministic;
|
|
226
|
+
the seed does not control their randomness or external state.
|
|
227
|
+
|
|
222
228
|
## Error Handling
|
|
223
229
|
|
|
224
230
|
All `conformly` errors inherit from `ConformlyError`, providing consistent interface for debugging and programmatic handling.
|
|
@@ -615,7 +621,15 @@ print(trace.generated_value)
|
|
|
615
621
|
print(trace.seed)
|
|
616
622
|
```
|
|
617
623
|
|
|
618
|
-
|
|
624
|
+
Tracing is available for `case(..., valid=False)`. The trace describes the selected
|
|
625
|
+
target: its dotted path, violation, generated value, seed, and value source. This
|
|
626
|
+
applies to DSL and string paths as well as `"first"` and `"random"` selection.
|
|
627
|
+
Unrelated fields, defaults, and overrides do not overwrite target metadata. For
|
|
628
|
+
`MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
|
|
629
|
+
absent from the payload.
|
|
630
|
+
|
|
631
|
+
Enabling tracing leaves payloads, internal RNG consumption, and default factory
|
|
632
|
+
call counts unchanged. A reused tracer describes the latest completed generation.
|
|
619
633
|
|
|
620
634
|
|
|
621
635
|
## Development
|
|
@@ -704,3 +718,6 @@ Contributions welcome!
|
|
|
704
718
|
- Add tests for new functionality
|
|
705
719
|
- Run `uv run -m pytest` and `uv run -m ruff check .`
|
|
706
720
|
- Submit a pull request
|
|
721
|
+
|
|
722
|
+
Test collection and assertion audit findings, including deferred collection checks,
|
|
723
|
+
are documented in [the test audit](docs/test-audit.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "conformly"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.3"
|
|
4
4
|
description = "Generate valid & invalid test data from your typed schemas"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -90,6 +90,7 @@ line-ending = "auto"
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
[tool.pytest.ini_options]
|
|
93
|
+
xfail_strict = true
|
|
93
94
|
markers = ["benchmark: performance benchmarks"]
|
|
94
95
|
|
|
95
96
|
addopts = ["-v", "--strict-markers"]
|
|
@@ -38,9 +38,11 @@ def case(
|
|
|
38
38
|
If False, generate an invalid one.
|
|
39
39
|
|
|
40
40
|
seed:
|
|
41
|
-
Random seed for reproducible generation.
|
|
41
|
+
Random seed for reproducible internal generation.
|
|
42
42
|
- `None` (default): Use system randomness (different output each run).
|
|
43
|
-
- `int`: Initialize RNG with fixed seed
|
|
43
|
+
- `int`: Initialize RNG with a fixed seed, including 0 and negatives.
|
|
44
|
+
User-provided default factories must be deterministic for identical
|
|
45
|
+
payloads; their randomness is not controlled by this seed.
|
|
44
46
|
|
|
45
47
|
strategy:
|
|
46
48
|
Define which field to violate when valid=False.
|
|
@@ -63,14 +65,17 @@ def case(
|
|
|
63
65
|
|
|
64
66
|
overrides:
|
|
65
67
|
Optional list of `PathSelector` to set field values.
|
|
66
|
-
-
|
|
67
|
-
- Override generated values
|
|
68
|
+
- Used instead of generated values and model defaults
|
|
68
69
|
- With valid=False: act as defaults (ignored if field is violated)
|
|
69
70
|
|
|
70
71
|
allow_type_mismatch:
|
|
71
72
|
If True fields could be type mismatched.
|
|
72
73
|
Availiable only when valid=False.
|
|
73
74
|
|
|
75
|
+
tracer:
|
|
76
|
+
Optional tracer for invalid generation. Records the selected target
|
|
77
|
+
path, violation, and value without changing generation or factory calls.
|
|
78
|
+
|
|
74
79
|
Returns:
|
|
75
80
|
A dictionary representing the instance.
|
|
76
81
|
"""
|
|
@@ -111,9 +116,6 @@ def case(
|
|
|
111
116
|
strategy=strategy,
|
|
112
117
|
)
|
|
113
118
|
|
|
114
|
-
if tracer and isinstance(strategy, PathSelector):
|
|
115
|
-
tracer.set_target_path(strategy.raw_path)
|
|
116
|
-
|
|
117
119
|
task = plan_tasks(
|
|
118
120
|
model,
|
|
119
121
|
ctx=ctx,
|
|
@@ -38,9 +38,11 @@ def cases(
|
|
|
38
38
|
If False, generate invalid ones.
|
|
39
39
|
|
|
40
40
|
seed:
|
|
41
|
-
Random seed for reproducible generation.
|
|
41
|
+
Random seed for reproducible internal generation.
|
|
42
42
|
- `None` (default): Use system randomness (different output each run).
|
|
43
|
-
- `int`: Initialize RNG with fixed seed
|
|
43
|
+
- `int`: Initialize RNG with a fixed seed, including 0 and negatives.
|
|
44
|
+
User-provided default factories must be deterministic for identical
|
|
45
|
+
payloads; their randomness is not controlled by this seed.
|
|
44
46
|
|
|
45
47
|
strategy:
|
|
46
48
|
Define how fields are selected for violation when valid=False.
|
|
@@ -74,8 +76,7 @@ def cases(
|
|
|
74
76
|
|
|
75
77
|
overrides:
|
|
76
78
|
Optional list of `PathSelector` to set field values.
|
|
77
|
-
-
|
|
78
|
-
- Override generated values
|
|
79
|
+
- Used instead of generated values and model defaults
|
|
79
80
|
- With valid=False: act as defaults (ignored if field is violated)
|
|
80
81
|
|
|
81
82
|
|
|
@@ -54,6 +54,7 @@ def _built_dict_with_violations(
|
|
|
54
54
|
violations: tuple[ViolationType, ...],
|
|
55
55
|
overrides: dict[FieldPath, Any] | None = None,
|
|
56
56
|
tracer: Tracer | None = None,
|
|
57
|
+
path_names: tuple[str, ...] = (),
|
|
57
58
|
) -> dict[str, Any]:
|
|
58
59
|
result: dict[str, Any] = {}
|
|
59
60
|
target_index = target_path[depth]
|
|
@@ -72,7 +73,7 @@ def _built_dict_with_violations(
|
|
|
72
73
|
|
|
73
74
|
for i in range(0, target_index):
|
|
74
75
|
field = fields[i]
|
|
75
|
-
result[field.name] = generate_field(ctx, field, None, overrides
|
|
76
|
+
result[field.name] = generate_field(ctx, field, None, overrides)
|
|
76
77
|
|
|
77
78
|
if target_index < total_fields:
|
|
78
79
|
field = fields[target_index]
|
|
@@ -85,6 +86,8 @@ def _built_dict_with_violations(
|
|
|
85
86
|
field=field.name,
|
|
86
87
|
)
|
|
87
88
|
|
|
89
|
+
if tracer:
|
|
90
|
+
tracer.set_target_path(".".join((*path_names, field.name)))
|
|
88
91
|
value = generate_field(ctx, field, violations, overrides, tracer)
|
|
89
92
|
if value is not UNSET:
|
|
90
93
|
result[field.name] = value
|
|
@@ -105,18 +108,25 @@ def _built_dict_with_violations(
|
|
|
105
108
|
violations=violations,
|
|
106
109
|
overrides=overrides,
|
|
107
110
|
tracer=tracer,
|
|
111
|
+
path_names=(*path_names, field.name),
|
|
108
112
|
)
|
|
109
113
|
|
|
110
114
|
else:
|
|
111
|
-
for i, field in enumerate(fields):
|
|
112
|
-
result[field.name] = generate_field(ctx, field, tracer=tracer)
|
|
113
|
-
|
|
114
115
|
key, value = _generate_extra_field_value(ctx)
|
|
116
|
+
suffix = 1
|
|
117
|
+
while key in result:
|
|
118
|
+
key = f"extra_{suffix}"
|
|
119
|
+
suffix += 1
|
|
115
120
|
result[key] = value
|
|
121
|
+
if tracer:
|
|
122
|
+
tracer.set_target_path(".".join((*path_names, key)))
|
|
123
|
+
tracer.set_violation(ViolationType.EXTRA_FIELD)
|
|
124
|
+
tracer.set_value_source(ValueSource.GENERATED)
|
|
125
|
+
tracer.set_generated_value(value)
|
|
116
126
|
|
|
117
127
|
for i in range(target_index + 1, total_fields):
|
|
118
128
|
field = fields[i]
|
|
119
|
-
result[field.name] = generate_field(ctx, field,
|
|
129
|
+
result[field.name] = generate_field(ctx, field, overrides=overrides)
|
|
120
130
|
|
|
121
131
|
return result
|
|
122
132
|
|
|
@@ -144,15 +154,10 @@ def generate_field(
|
|
|
144
154
|
tracer.set_value_source(ValueSource.MODEL_DEFAULT)
|
|
145
155
|
|
|
146
156
|
default = field.default
|
|
147
|
-
if callable(default)
|
|
148
|
-
if tracer:
|
|
149
|
-
tracer.set_generated_value(default())
|
|
150
|
-
return default()
|
|
151
|
-
|
|
157
|
+
value = default() if callable(default) else default
|
|
152
158
|
if tracer:
|
|
153
|
-
tracer.set_generated_value(
|
|
154
|
-
|
|
155
|
-
return default
|
|
159
|
+
tracer.set_generated_value(value)
|
|
160
|
+
return value
|
|
156
161
|
|
|
157
162
|
if field.nested_model and not isinstance(
|
|
158
163
|
field.semantic, (ListSemantic, TupleSemantic)
|
|
@@ -97,7 +97,6 @@ def _generate_invalid_url(
|
|
|
97
97
|
"",
|
|
98
98
|
" ",
|
|
99
99
|
"not-a-url",
|
|
100
|
-
"http:/broken",
|
|
101
100
|
"http:// bad.com",
|
|
102
101
|
"://missing.scheme.com",
|
|
103
102
|
]
|
|
@@ -105,6 +104,9 @@ def _generate_invalid_url(
|
|
|
105
104
|
|
|
106
105
|
if violation == ViolationType.WRONG_URL_SCHEME:
|
|
107
106
|
scheme = ctx.rng.choice(INVALID_SCHEMES)
|
|
107
|
+
if semantic.kind == FieldKind.URL:
|
|
108
|
+
# Arbitrary alphabetic schemes are valid for unrestricted URLs.
|
|
109
|
+
scheme = "1" + scheme
|
|
108
110
|
return f"{scheme}://example.com"
|
|
109
111
|
|
|
110
112
|
return "not-a-url"
|
|
@@ -24,26 +24,26 @@ def generate_value(
|
|
|
24
24
|
ctx: GenerationContext, semantic: UUIDSemantic, violation: ViolationType | None
|
|
25
25
|
) -> str:
|
|
26
26
|
return (
|
|
27
|
-
_generate_valid_uuid()
|
|
27
|
+
_generate_valid_uuid(ctx)
|
|
28
28
|
if not violation
|
|
29
29
|
else _generate_invalid_uuid(ctx, violation)
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _generate_valid_uuid() -> str:
|
|
34
|
-
return str(uuid.
|
|
33
|
+
def _generate_valid_uuid(ctx: GenerationContext) -> str:
|
|
34
|
+
return str(uuid.UUID(int=ctx.rng.getrandbits(128), version=4))
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _generate_invalid_uuid(ctx: GenerationContext, violation: ViolationType) -> str:
|
|
38
38
|
match violation:
|
|
39
39
|
case ViolationType.TOO_SHORT:
|
|
40
|
-
uid =
|
|
40
|
+
uid = _generate_valid_uuid(ctx)
|
|
41
41
|
chars_to_remove = ctx.rng.randint(1, min(10, len(uid) - 1))
|
|
42
42
|
result = uid[:-chars_to_remove]
|
|
43
43
|
return result if result else "x"
|
|
44
44
|
|
|
45
45
|
case ViolationType.TOO_LONG:
|
|
46
|
-
uid =
|
|
46
|
+
uid = _generate_valid_uuid(ctx)
|
|
47
47
|
extra_len = ctx.rng.randint(1, 10)
|
|
48
48
|
extra = "".join(ctx.rng.choice(HEX_CHARS) for _ in range(extra_len))
|
|
49
49
|
return uid + extra
|
|
@@ -57,7 +57,7 @@ def _generate_invalid_uuid(ctx: GenerationContext, violation: ViolationType) ->
|
|
|
57
57
|
return template
|
|
58
58
|
|
|
59
59
|
case ViolationType.WRONG_UUID_CHARACTER:
|
|
60
|
-
uuid_chars: list[str] = list(
|
|
60
|
+
uuid_chars: list[str] = list(_generate_valid_uuid(ctx))
|
|
61
61
|
hex_positions = [i for i, ch in enumerate(uuid_chars) if ch != "-"]
|
|
62
62
|
if hex_positions:
|
|
63
63
|
pos = ctx.rng.choice(hex_positions)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conformly
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Generate valid & invalid test data from your typed schemas
|
|
5
5
|
Author-email: Nikita Shabanov <nik.shabanov2024@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -252,6 +252,12 @@ overrides=[
|
|
|
252
252
|
]
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
+
Integer seeds, including `0` and negative values, make `case()` and `cases()`
|
|
256
|
+
reproducible for internal generation, including nested models and UUID values.
|
|
257
|
+
The same model, options, and seed produce the same payloads. User-provided default
|
|
258
|
+
factories run once per generated field and must themselves be deterministic;
|
|
259
|
+
the seed does not control their randomness or external state.
|
|
260
|
+
|
|
255
261
|
## Error Handling
|
|
256
262
|
|
|
257
263
|
All `conformly` errors inherit from `ConformlyError`, providing consistent interface for debugging and programmatic handling.
|
|
@@ -648,7 +654,15 @@ print(trace.generated_value)
|
|
|
648
654
|
print(trace.seed)
|
|
649
655
|
```
|
|
650
656
|
|
|
651
|
-
|
|
657
|
+
Tracing is available for `case(..., valid=False)`. The trace describes the selected
|
|
658
|
+
target: its dotted path, violation, generated value, seed, and value source. This
|
|
659
|
+
applies to DSL and string paths as well as `"first"` and `"random"` selection.
|
|
660
|
+
Unrelated fields, defaults, and overrides do not overwrite target metadata. For
|
|
661
|
+
`MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
|
|
662
|
+
absent from the payload.
|
|
663
|
+
|
|
664
|
+
Enabling tracing leaves payloads, internal RNG consumption, and default factory
|
|
665
|
+
call counts unchanged. A reused tracer describes the latest completed generation.
|
|
652
666
|
|
|
653
667
|
|
|
654
668
|
## Development
|
|
@@ -737,3 +751,6 @@ Contributions welcome!
|
|
|
737
751
|
- Add tests for new functionality
|
|
738
752
|
- Run `uv run -m pytest` and `uv run -m ruff check .`
|
|
739
753
|
- Submit a pull request
|
|
754
|
+
|
|
755
|
+
Test collection and assertion audit findings, including deferred collection checks,
|
|
756
|
+
are documented in [the test audit](docs/test-audit.md).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Keep test functions visible to pytest collection."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_no_test_functions_are_nested_inside_functions() -> None:
|
|
8
|
+
tests_root = Path(__file__).parent
|
|
9
|
+
hidden = set()
|
|
10
|
+
function_nodes = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
11
|
+
|
|
12
|
+
for source in tests_root.rglob("test_*.py"):
|
|
13
|
+
tree = ast.parse(source.read_text(encoding="utf-8"), filename=str(source))
|
|
14
|
+
for outer in ast.walk(tree):
|
|
15
|
+
if not isinstance(outer, function_nodes):
|
|
16
|
+
continue
|
|
17
|
+
for inner in ast.walk(outer):
|
|
18
|
+
if (
|
|
19
|
+
inner is not outer
|
|
20
|
+
and isinstance(inner, function_nodes)
|
|
21
|
+
and inner.name.startswith("test_")
|
|
22
|
+
):
|
|
23
|
+
hidden.add(
|
|
24
|
+
f"{source.relative_to(tests_root)}:{inner.lineno}: {inner.name}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert not hidden, "Tests hidden from pytest collection:\n" + "\n".join(
|
|
28
|
+
sorted(hidden)
|
|
29
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/constraints.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/_validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|