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.
Files changed (101) hide show
  1. {conformly-0.7.2/src/conformly.egg-info → conformly-0.7.3}/PKG-INFO +19 -2
  2. {conformly-0.7.2 → conformly-0.7.3}/README.md +18 -1
  3. {conformly-0.7.2 → conformly-0.7.3}/pyproject.toml +2 -1
  4. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/case.py +9 -7
  5. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/cases.py +5 -4
  6. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/context.py +1 -1
  7. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/orchestration.py +18 -13
  8. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/url.py +3 -1
  9. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/uuid.py +6 -6
  10. {conformly-0.7.2 → conformly-0.7.3/src/conformly.egg-info}/PKG-INFO +19 -2
  11. {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/SOURCES.txt +1 -0
  12. conformly-0.7.3/tests/test_collection.py +29 -0
  13. {conformly-0.7.2 → conformly-0.7.3}/LICENSE +0 -0
  14. {conformly-0.7.2 → conformly-0.7.3}/setup.cfg +0 -0
  15. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/__init__.py +0 -0
  16. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/__init__.py +0 -0
  17. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/__init__.py +0 -0
  18. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/_errors.py +0 -0
  19. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/_path_proxy.py +0 -0
  20. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/_utils.py +0 -0
  21. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/api/path.py +0 -0
  22. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/__init__.py +0 -0
  23. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/base.py +0 -0
  24. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/collections.py +0 -0
  25. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/enum.py +0 -0
  26. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/keys.py +0 -0
  27. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/mapping.py +0 -0
  28. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/numeric.py +0 -0
  29. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/constraints/string.py +0 -0
  30. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/fields/__init__.py +0 -0
  31. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/fields/registry.py +0 -0
  32. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/fields/special_types.py +0 -0
  33. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/__init__.py +0 -0
  34. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/_errors.py +0 -0
  35. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/protocol.py +0 -0
  36. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/registry.py +0 -0
  37. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/__init__.py +0 -0
  38. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/_utils.py +0 -0
  39. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/boolean.py +0 -0
  40. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
  41. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/email.py +0 -0
  42. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/enum.py +0 -0
  43. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/float.py +0 -0
  44. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/integer.py +0 -0
  45. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/ipv4.py +0 -0
  46. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/ipv6.py +0 -0
  47. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/ipvany.py +0 -0
  48. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/list.py +0 -0
  49. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/string.py +0 -0
  50. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/generator/types/tuple.py +0 -0
  51. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/__init__.py +0 -0
  52. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
  53. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
  54. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
  55. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
  56. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
  57. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
  58. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/registry.py +0 -0
  59. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
  60. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/api.py +0 -0
  61. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/core/__init__.py +0 -0
  62. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/core/element.py +0 -0
  63. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/core/field.py +0 -0
  64. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
  65. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
  66. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/extractors/types.py +0 -0
  67. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/parser/models.py +0 -0
  68. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/__init__.py +0 -0
  69. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/_errors.py +0 -0
  70. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/field.py +0 -0
  71. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/model.py +0 -0
  72. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/planner/session.py +0 -0
  73. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/__init__.py +0 -0
  74. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/models.py +0 -0
  75. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/resolve.py +0 -0
  76. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/__init__.py +0 -0
  77. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
  78. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/base.py +0 -0
  79. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
  80. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
  81. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
  82. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/factory.py +0 -0
  83. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/list.py +0 -0
  84. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
  85. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/object.py +0 -0
  86. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/string.py +0 -0
  87. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
  88. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
  89. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/tracer.py +0 -0
  90. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/__init__.py +0 -0
  91. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/constants.py +0 -0
  92. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/primitives.py +0 -0
  93. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/strategies.py +0 -0
  94. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/_internal/types/values.py +0 -0
  95. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/exceptions.py +0 -0
  96. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/py.typed +0 -0
  97. {conformly-0.7.2 → conformly-0.7.3}/src/conformly/tracer/__init__.py +0 -0
  98. {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/dependency_links.txt +0 -0
  99. {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/requires.txt +0 -0
  100. {conformly-0.7.2 → conformly-0.7.3}/src/conformly.egg-info/top_level.txt +0 -0
  101. {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.2
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
- The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
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
- The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
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.2"
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 (same output for same 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
- - Applied after generation
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 (same output for same 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
- - Applied after generation
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
 
@@ -12,5 +12,5 @@ class GenerationContext:
12
12
 
13
13
 
14
14
  def create_context(seed: int | None = None) -> GenerationContext:
15
- rng = random.Random(seed) if seed else random.Random()
15
+ rng = random.Random(seed)
16
16
  return GenerationContext(rng=rng, rstr=Rstr(rng), seed=seed)
@@ -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, tracer)
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, tracer=tracer)
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(default())
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.uuid4())
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 = str(uuid.uuid4())
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 = str(uuid.uuid4())
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(str(uuid.uuid4()))
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.2
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
- The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
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).
@@ -95,4 +95,5 @@ src/conformly/_internal/types/primitives.py
95
95
  src/conformly/_internal/types/strategies.py
96
96
  src/conformly/_internal/types/values.py
97
97
  src/conformly/tracer/__init__.py
98
+ tests/test_collection.py
98
99
  tests/test_path.py
@@ -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