conformly 0.7.2__tar.gz → 0.7.4__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 (109) hide show
  1. {conformly-0.7.2/src/conformly.egg-info → conformly-0.7.4}/PKG-INFO +60 -7
  2. {conformly-0.7.2 → conformly-0.7.4}/README.md +59 -6
  3. {conformly-0.7.2 → conformly-0.7.4}/pyproject.toml +2 -1
  4. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/_utils.py +7 -1
  5. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/case.py +9 -7
  6. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/cases.py +5 -4
  7. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/context.py +1 -1
  8. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/orchestration.py +118 -36
  9. conformly-0.7.4/src/conformly/_internal/generator/types/_utils.py +84 -0
  10. conformly-0.7.4/src/conformly/_internal/generator/types/dictionaries.py +137 -0
  11. conformly-0.7.4/src/conformly/_internal/generator/types/float.py +119 -0
  12. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/integer.py +11 -3
  13. conformly-0.7.4/src/conformly/_internal/generator/types/list.py +135 -0
  14. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/string.py +42 -30
  15. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/tuple.py +33 -33
  16. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/url.py +3 -1
  17. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/uuid.py +6 -6
  18. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/attrs.py +6 -1
  19. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/pydantic.py +1 -0
  20. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/typeddict.py +9 -3
  21. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/core/element.py +0 -2
  22. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/core/field.py +5 -0
  23. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/models.py +6 -1
  24. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/planner/field.py +114 -38
  25. conformly-0.7.4/src/conformly/_internal/planner/model.py +18 -0
  26. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/planner/session.py +17 -4
  27. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/models.py +9 -2
  28. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/resolve.py +49 -56
  29. conformly-0.7.4/src/conformly/_internal/resolver/semantics/_domains.py +29 -0
  30. conformly-0.7.4/src/conformly/_internal/resolver/semantics/_numeric.py +15 -0
  31. conformly-0.7.4/src/conformly/_internal/resolver/semantics/_regex.py +9 -0
  32. {conformly-0.7.2 → conformly-0.7.4/src/conformly.egg-info}/PKG-INFO +60 -7
  33. {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/SOURCES.txt +4 -0
  34. conformly-0.7.4/tests/test_collection.py +29 -0
  35. conformly-0.7.2/src/conformly/_internal/generator/types/_utils.py +0 -63
  36. conformly-0.7.2/src/conformly/_internal/generator/types/dictionaries.py +0 -122
  37. conformly-0.7.2/src/conformly/_internal/generator/types/float.py +0 -89
  38. conformly-0.7.2/src/conformly/_internal/generator/types/list.py +0 -112
  39. conformly-0.7.2/src/conformly/_internal/planner/model.py +0 -9
  40. {conformly-0.7.2 → conformly-0.7.4}/LICENSE +0 -0
  41. {conformly-0.7.2 → conformly-0.7.4}/setup.cfg +0 -0
  42. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/__init__.py +0 -0
  43. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/__init__.py +0 -0
  44. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/__init__.py +0 -0
  45. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/_errors.py +0 -0
  46. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/_path_proxy.py +0 -0
  47. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/path.py +0 -0
  48. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/__init__.py +0 -0
  49. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/base.py +0 -0
  50. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/collections.py +0 -0
  51. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/enum.py +0 -0
  52. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/keys.py +0 -0
  53. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/mapping.py +0 -0
  54. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/numeric.py +0 -0
  55. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/string.py +0 -0
  56. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/fields/__init__.py +0 -0
  57. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/fields/registry.py +0 -0
  58. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/fields/special_types.py +0 -0
  59. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/__init__.py +0 -0
  60. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/_errors.py +0 -0
  61. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/protocol.py +0 -0
  62. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/registry.py +0 -0
  63. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/__init__.py +0 -0
  64. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/boolean.py +0 -0
  65. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/email.py +0 -0
  66. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/enum.py +0 -0
  67. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv4.py +0 -0
  68. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv6.py +0 -0
  69. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipvany.py +0 -0
  70. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/__init__.py +0 -0
  71. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
  72. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
  73. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
  74. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
  75. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/registry.py +0 -0
  76. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/api.py +0 -0
  77. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/core/__init__.py +0 -0
  78. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
  79. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
  80. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/types.py +0 -0
  81. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/planner/__init__.py +0 -0
  82. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/planner/_errors.py +0 -0
  83. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/__init__.py +0 -0
  84. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/__init__.py +0 -0
  85. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
  86. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/base.py +0 -0
  87. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
  88. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
  89. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
  90. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/factory.py +0 -0
  91. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/list.py +0 -0
  92. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
  93. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/object.py +0 -0
  94. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/string.py +0 -0
  95. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
  96. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
  97. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/tracer.py +0 -0
  98. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/__init__.py +0 -0
  99. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/constants.py +0 -0
  100. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/primitives.py +0 -0
  101. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/strategies.py +0 -0
  102. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/values.py +0 -0
  103. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/exceptions.py +0 -0
  104. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/py.typed +0 -0
  105. {conformly-0.7.2 → conformly-0.7.4}/src/conformly/tracer/__init__.py +0 -0
  106. {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/dependency_links.txt +0 -0
  107. {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/requires.txt +0 -0
  108. {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/top_level.txt +0 -0
  109. {conformly-0.7.2 → conformly-0.7.4}/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.4
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.
@@ -291,17 +297,43 @@ For `case(Model, valid=False, strategy="<field>")`:
291
297
  - **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
292
298
  - **Exactly one field is targeted** (the one specified by `strategy`).
293
299
  - **The generator will violate constraints** for that field, making it invalid.
294
- - **If a field has multiple constraints**, the violated constraint may be chosen by generator logic (not necessarily the one you expect).
300
+ - **Inside collections**, planning selects a compatible dictionary key, value, or
301
+ element before generation. Fixed tuples retain their length and order; unrelated
302
+ elements, keys, values, and model fields remain valid.
303
+ - **Only the selected violation is guaranteed on its target**. Other constraints
304
+ on that same target are not guaranteed to remain satisfied. A `DUPLICATE`
305
+ violation preserves an allowed collection length; collections with a maximum
306
+ size below two cannot be targeted for duplication.
307
+ - **Explicit impossible or unsupported violations raise `PlanningError`**;
308
+ empty-only collections do not offer element violations.
295
309
  - **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
296
310
  - **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
297
311
 
298
- If you need **deterministic control** over which exact constraint to violate, that is not implemented in yet (see Roadmap).
312
+ Request an exact violation with `strategy="items::below_min"` or
313
+ `path(Model, lambda m: m.items).violate(V.BELOW_MIN)`. Otherwise, the planner uses
314
+ violation priority and chooses the first compatible key, value, or tuple element.
299
315
 
300
316
  ## Optional Fields and Defaults
301
317
 
302
- - If a field is **optional** (`Optional[T]`), valid generation may produce `None`.
303
- - If a field has a **default value**, valid generation returns the default.
304
- - Invalid generation **requires at least one constraint** on the targeted field (raises `ValueError` otherwise).
318
+ - For untargeted fields, priority is **override → nullable `None` → model default
319
+ or default factory → generation**. Nullable fields currently always return `None`
320
+ without an override, even with a non-`None` default; their factories are not called.
321
+ - Model defaults, factory results and overrides are trusted and returned without
322
+ validation. `valid=True` guarantees the validity of generated values, not of
323
+ caller-supplied data. An invalid supplied value can also make an unrelated field
324
+ invalid when generating a negative case.
325
+ - Invalid targets bypass their defaults, nullable priority and overrides. Other
326
+ fields retain the priority above. Default factories are called once per generated
327
+ payload when selected by this priority; attrs factories requiring `self` are unsupported.
328
+ - TypedDict `NotRequired[T]` controls key presence independently of whether `T`
329
+ accepts `None`. Valid generation currently includes these keys with values of `T`;
330
+ `NotRequired[int]` does not become a present `None`.
331
+ - `MISSING_FIELD` is offered only for required fields: nullable required fields can
332
+ be removed, but fields with defaults and TypedDict `NotRequired` keys cannot.
333
+ `EXTRA_FIELD` is offered for Pydantic only with `extra="forbid"`; `ignore` and
334
+ `allow` accept extra data. Other adapters treat undeclared keys as structural violations.
335
+ - A target needs an applicable semantic, type or structural violation; impossible
336
+ explicit requests raise `PlanningError`.
305
337
 
306
338
  ## Constraints
307
339
 
@@ -321,6 +353,19 @@ If you need **deterministic control** over which exact constraint to violate, th
321
353
  | Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
322
354
  | Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
323
355
 
356
+ `Pattern` uses Python regex search semantics, consistent with Pydantic: a match
357
+ anywhere in the string is sufficient. Use anchors (`^...$`) to require a whole-string
358
+ match. Pattern mismatches are verified; unsuccessful inversion raises a structured
359
+ `GenerationError` instead of returning a matching value. Obvious universal patterns
360
+ (such as `(?s).*`) are excluded from automatic planning, and explicit mismatch
361
+ requests raise `PlanningError`.
362
+
363
+ `MultipleOf` requires a finite positive divisor. For floats, a quotient within
364
+ `1e-9` of an integer is treated as a multiple (no relative tolerance). Integer
365
+ `NOT_MULTIPLE` requests for divisors such as `1` or `0.5`, which divide every integer,
366
+ raise `PlanningError` and are excluded from automatic planning. A bounded range
367
+ without a representable multiple raises `GenerationError` with code `no_multiples_of`.
368
+
324
369
  > Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
325
370
  > Use Field() parameters for constraint extraction.
326
371
 
@@ -648,7 +693,15 @@ print(trace.generated_value)
648
693
  print(trace.seed)
649
694
  ```
650
695
 
651
- The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
696
+ Tracing is available for `case(..., valid=False)`. The trace describes the selected
697
+ target: its dotted path, violation, generated value, seed, and value source. This
698
+ applies to DSL and string paths as well as `"first"` and `"random"` selection.
699
+ Unrelated fields, defaults, and overrides do not overwrite target metadata. For
700
+ `MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
701
+ absent from the payload.
702
+
703
+ Enabling tracing leaves payloads, internal RNG consumption, and default factory
704
+ call counts unchanged. A reused tracer describes the latest completed generation.
652
705
 
653
706
 
654
707
  ## Development
@@ -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.
@@ -258,17 +264,43 @@ For `case(Model, valid=False, strategy="<field>")`:
258
264
  - **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
259
265
  - **Exactly one field is targeted** (the one specified by `strategy`).
260
266
  - **The generator will violate constraints** for that field, making it invalid.
261
- - **If a field has multiple constraints**, the violated constraint may be chosen by generator logic (not necessarily the one you expect).
267
+ - **Inside collections**, planning selects a compatible dictionary key, value, or
268
+ element before generation. Fixed tuples retain their length and order; unrelated
269
+ elements, keys, values, and model fields remain valid.
270
+ - **Only the selected violation is guaranteed on its target**. Other constraints
271
+ on that same target are not guaranteed to remain satisfied. A `DUPLICATE`
272
+ violation preserves an allowed collection length; collections with a maximum
273
+ size below two cannot be targeted for duplication.
274
+ - **Explicit impossible or unsupported violations raise `PlanningError`**;
275
+ empty-only collections do not offer element violations.
262
276
  - **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
263
277
  - **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
264
278
 
265
- If you need **deterministic control** over which exact constraint to violate, that is not implemented in yet (see Roadmap).
279
+ Request an exact violation with `strategy="items::below_min"` or
280
+ `path(Model, lambda m: m.items).violate(V.BELOW_MIN)`. Otherwise, the planner uses
281
+ violation priority and chooses the first compatible key, value, or tuple element.
266
282
 
267
283
  ## Optional Fields and Defaults
268
284
 
269
- - If a field is **optional** (`Optional[T]`), valid generation may produce `None`.
270
- - If a field has a **default value**, valid generation returns the default.
271
- - Invalid generation **requires at least one constraint** on the targeted field (raises `ValueError` otherwise).
285
+ - For untargeted fields, priority is **override → nullable `None` → model default
286
+ or default factory → generation**. Nullable fields currently always return `None`
287
+ without an override, even with a non-`None` default; their factories are not called.
288
+ - Model defaults, factory results and overrides are trusted and returned without
289
+ validation. `valid=True` guarantees the validity of generated values, not of
290
+ caller-supplied data. An invalid supplied value can also make an unrelated field
291
+ invalid when generating a negative case.
292
+ - Invalid targets bypass their defaults, nullable priority and overrides. Other
293
+ fields retain the priority above. Default factories are called once per generated
294
+ payload when selected by this priority; attrs factories requiring `self` are unsupported.
295
+ - TypedDict `NotRequired[T]` controls key presence independently of whether `T`
296
+ accepts `None`. Valid generation currently includes these keys with values of `T`;
297
+ `NotRequired[int]` does not become a present `None`.
298
+ - `MISSING_FIELD` is offered only for required fields: nullable required fields can
299
+ be removed, but fields with defaults and TypedDict `NotRequired` keys cannot.
300
+ `EXTRA_FIELD` is offered for Pydantic only with `extra="forbid"`; `ignore` and
301
+ `allow` accept extra data. Other adapters treat undeclared keys as structural violations.
302
+ - A target needs an applicable semantic, type or structural violation; impossible
303
+ explicit requests raise `PlanningError`.
272
304
 
273
305
  ## Constraints
274
306
 
@@ -288,6 +320,19 @@ If you need **deterministic control** over which exact constraint to violate, th
288
320
  | Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
289
321
  | Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
290
322
 
323
+ `Pattern` uses Python regex search semantics, consistent with Pydantic: a match
324
+ anywhere in the string is sufficient. Use anchors (`^...$`) to require a whole-string
325
+ match. Pattern mismatches are verified; unsuccessful inversion raises a structured
326
+ `GenerationError` instead of returning a matching value. Obvious universal patterns
327
+ (such as `(?s).*`) are excluded from automatic planning, and explicit mismatch
328
+ requests raise `PlanningError`.
329
+
330
+ `MultipleOf` requires a finite positive divisor. For floats, a quotient within
331
+ `1e-9` of an integer is treated as a multiple (no relative tolerance). Integer
332
+ `NOT_MULTIPLE` requests for divisors such as `1` or `0.5`, which divide every integer,
333
+ raise `PlanningError` and are excluded from automatic planning. A bounded range
334
+ without a representable multiple raises `GenerationError` with code `no_multiples_of`.
335
+
291
336
  > Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
292
337
  > Use Field() parameters for constraint extraction.
293
338
 
@@ -615,7 +660,15 @@ print(trace.generated_value)
615
660
  print(trace.seed)
616
661
  ```
617
662
 
618
- The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
663
+ Tracing is available for `case(..., valid=False)`. The trace describes the selected
664
+ target: its dotted path, violation, generated value, seed, and value source. This
665
+ applies to DSL and string paths as well as `"first"` and `"random"` selection.
666
+ Unrelated fields, defaults, and overrides do not overwrite target metadata. For
667
+ `MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
668
+ absent from the payload.
669
+
670
+ Enabling tracing leaves payloads, internal RNG consumption, and default factory
671
+ call counts unchanged. A reused tracer describes the latest completed generation.
619
672
 
620
673
 
621
674
  ## Development
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "conformly"
3
- version = "0.7.2"
3
+ version = "0.7.4"
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"]
@@ -62,7 +62,13 @@ def plan_tasks(
62
62
 
63
63
  if split_by_violations:
64
64
  return [
65
- PlannedTask(path=path, allowed_violations=(violation,))
65
+ plan_violation_task(
66
+ model,
67
+ path,
68
+ allow_type_mismatch,
69
+ allow_structural_violations,
70
+ forced_violation=violation,
71
+ )
66
72
  for path in paths
67
73
  for violation in plan_violation_task(
68
74
  model, path, allow_type_mismatch, allow_structural_violations
@@ -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)
@@ -5,12 +5,19 @@ from .context import GenerationContext
5
5
  from .registry import choose_mismatch_kind, get_generator
6
6
 
7
7
  from conformly._internal.planner import PlannedTask
8
+ from conformly._internal.planner.model import CollectionTarget
8
9
  from conformly._internal.resolver import (
9
10
  ResolvedField,
10
11
  ResolvedModel,
11
12
  create_minimal_semantic,
12
13
  )
13
- from conformly._internal.resolver.semantics import ListSemantic, TupleSemantic
14
+ from conformly._internal.resolver.semantics import (
15
+ DictSemantic,
16
+ FieldSemantics,
17
+ ListSemantic,
18
+ ObjectSemantic,
19
+ TupleSemantic,
20
+ )
14
21
  from conformly._internal.tracer import Tracer, ValueSource
15
22
  from conformly._internal.types import UNSET, FieldKind, FieldPath, ViolationType
16
23
 
@@ -43,6 +50,8 @@ def generate_invalid(
43
50
  violations=task.allowed_violations,
44
51
  overrides=overrides,
45
52
  tracer=tracer,
53
+ collection_target=task.collection_target,
54
+ collection_steps=task.collection_steps,
46
55
  )
47
56
 
48
57
 
@@ -54,6 +63,9 @@ def _built_dict_with_violations(
54
63
  violations: tuple[ViolationType, ...],
55
64
  overrides: dict[FieldPath, Any] | None = None,
56
65
  tracer: Tracer | None = None,
66
+ path_names: tuple[str, ...] = (),
67
+ collection_target: CollectionTarget | None = None,
68
+ collection_steps: tuple[tuple[FieldPath, CollectionTarget], ...] = (),
57
69
  ) -> dict[str, Any]:
58
70
  result: dict[str, Any] = {}
59
71
  target_index = target_path[depth]
@@ -72,7 +84,7 @@ def _built_dict_with_violations(
72
84
 
73
85
  for i in range(0, target_index):
74
86
  field = fields[i]
75
- result[field.name] = generate_field(ctx, field, None, overrides, tracer)
87
+ result[field.name] = generate_field(ctx, field, None, overrides)
76
88
 
77
89
  if target_index < total_fields:
78
90
  field = fields[target_index]
@@ -85,7 +97,11 @@ def _built_dict_with_violations(
85
97
  field=field.name,
86
98
  )
87
99
 
88
- value = generate_field(ctx, field, violations, overrides, tracer)
100
+ if tracer:
101
+ tracer.set_target_path(".".join((*path_names, field.name)))
102
+ value = generate_field(
103
+ ctx, field, violations, overrides, tracer, collection_target
104
+ )
89
105
  if value is not UNSET:
90
106
  result[field.name] = value
91
107
 
@@ -97,7 +113,7 @@ def _built_dict_with_violations(
97
113
  field=field.name,
98
114
  )
99
115
 
100
- result[field.name] = _built_dict_with_violations(
116
+ nested_value = _built_dict_with_violations(
101
117
  ctx=ctx,
102
118
  model=field.nested_model,
103
119
  target_path=target_path,
@@ -105,18 +121,44 @@ def _built_dict_with_violations(
105
121
  violations=violations,
106
122
  overrides=overrides,
107
123
  tracer=tracer,
124
+ path_names=(*path_names, field.name),
125
+ collection_target=collection_target,
126
+ collection_steps=collection_steps,
108
127
  )
109
128
 
110
- else:
111
- for i, field in enumerate(fields):
112
- result[field.name] = generate_field(ctx, field, tracer=tracer)
129
+ if isinstance(field.semantic, (ListSemantic, DictSemantic)):
130
+ step = next(
131
+ (t for p, t in collection_steps if p == target_path[: depth + 1]),
132
+ None,
133
+ )
134
+ if step is None:
135
+ raise generation_error(
136
+ "Nested collection requires a planned target",
137
+ code="missing_collection_target",
138
+ )
139
+ container = _generate_nested_collection(
140
+ ctx, field.semantic, step, nested_value
141
+ )
142
+ result[field.name] = container
143
+ else:
144
+ result[field.name] = nested_value
113
145
 
146
+ else:
114
147
  key, value = _generate_extra_field_value(ctx)
148
+ suffix = 1
149
+ while key in result:
150
+ key = f"extra_{suffix}"
151
+ suffix += 1
115
152
  result[key] = value
153
+ if tracer:
154
+ tracer.set_target_path(".".join((*path_names, key)))
155
+ tracer.set_violation(ViolationType.EXTRA_FIELD)
156
+ tracer.set_value_source(ValueSource.GENERATED)
157
+ tracer.set_generated_value(value)
116
158
 
117
159
  for i in range(target_index + 1, total_fields):
118
160
  field = fields[i]
119
- result[field.name] = generate_field(ctx, field, tracer=tracer)
161
+ result[field.name] = generate_field(ctx, field, overrides=overrides)
120
162
 
121
163
  return result
122
164
 
@@ -127,6 +169,7 @@ def generate_field(
127
169
  violations: tuple[ViolationType, ...] | None = None,
128
170
  overrides: dict[FieldPath, Any] | None = None,
129
171
  tracer: Tracer | None = None,
172
+ collection_target: CollectionTarget | None = None,
130
173
  ) -> Any:
131
174
  if violations is None:
132
175
  if overrides and field.path in overrides:
@@ -144,18 +187,13 @@ def generate_field(
144
187
  tracer.set_value_source(ValueSource.MODEL_DEFAULT)
145
188
 
146
189
  default = field.default
147
- if callable(default):
148
- if tracer:
149
- tracer.set_generated_value(default())
150
- return default()
151
-
190
+ value = default() if callable(default) else default
152
191
  if tracer:
153
- tracer.set_generated_value(default())
154
-
155
- return default
192
+ tracer.set_generated_value(value)
193
+ return value
156
194
 
157
195
  if field.nested_model and not isinstance(
158
- field.semantic, (ListSemantic, TupleSemantic)
196
+ field.semantic, (ListSemantic, DictSemantic, TupleSemantic)
159
197
  ):
160
198
  return generate_valid(ctx, field.nested_model, overrides)
161
199
 
@@ -165,31 +203,50 @@ def generate_field(
165
203
  tracer.set_violation(violation)
166
204
  tracer.set_value_source(ValueSource.GENERATED)
167
205
 
168
- if violation == ViolationType.TYPE_MISMATCH:
169
- mismatch_kind = choose_mismatch_kind(field.semantic.kind)
170
- mismatch_semantic = create_minimal_semantic(mismatch_kind)
171
- value = get_generator(mismatch_kind).generate_value(
172
- ctx, mismatch_semantic, None
173
- )
174
- if tracer:
175
- tracer.set_generated_value(value)
176
- return value
177
-
178
- if violation == ViolationType.MISSING_FIELD:
179
- if tracer:
180
- tracer.set_generated_value(UNSET)
181
- return UNSET
182
-
183
- value = get_generator(field.semantic.kind).generate_value(
184
- ctx, field.semantic, violation
206
+ value = generate_semantic_value(
207
+ ctx, field.semantic, field.nested_model, violations, collection_target
185
208
  )
186
-
187
209
  if tracer:
188
210
  tracer.set_generated_value(value)
189
-
190
211
  return value
191
212
 
192
213
 
214
+ def generate_semantic_value(
215
+ ctx: GenerationContext,
216
+ semantic: FieldSemantics,
217
+ nested_model: ResolvedModel | None,
218
+ violations: tuple[ViolationType, ...] | None,
219
+ collection_target: CollectionTarget | None = None,
220
+ ) -> Any:
221
+ """Generate directly from resolved semantics, without synthetic field models."""
222
+ violation = _choose_violation(violations)
223
+ if violation == ViolationType.TYPE_MISMATCH:
224
+ mismatch_kind = choose_mismatch_kind(semantic.kind)
225
+ return get_generator(mismatch_kind).generate_value(
226
+ ctx, create_minimal_semantic(mismatch_kind), None
227
+ )
228
+ if violation == ViolationType.MISSING_FIELD:
229
+ return UNSET
230
+ if nested_model is not None and isinstance(semantic, ObjectSemantic):
231
+ if violation is not None:
232
+ raise generation_error(
233
+ "Nested model violation requires a field target",
234
+ code="missing_nested_target",
235
+ )
236
+ return generate_valid(ctx, nested_model)
237
+ from .types import dictionaries, list as lists, tuple as tuples
238
+
239
+ if isinstance(semantic, ListSemantic):
240
+ return lists.generate_value(ctx, semantic, violation, target=collection_target)
241
+ if isinstance(semantic, DictSemantic):
242
+ return dictionaries.generate_value(
243
+ ctx, semantic, violation, target=collection_target
244
+ )
245
+ if isinstance(semantic, TupleSemantic):
246
+ return tuples.generate_value(ctx, semantic, violation, target=collection_target)
247
+ return get_generator(semantic.kind).generate_value(ctx, semantic, violation)
248
+
249
+
193
250
  def _choose_violation(
194
251
  violations: tuple[ViolationType, ...] | None,
195
252
  ) -> ViolationType | None:
@@ -204,3 +261,28 @@ def _generate_extra_field_value(ctx: GenerationContext) -> tuple[str, Any]:
204
261
  value = get_generator(semantic.kind).generate_value(ctx, semantic, None)
205
262
  key = "extra"
206
263
  return (key, value)
264
+
265
+
266
+ def _generate_nested_collection(
267
+ ctx: GenerationContext,
268
+ semantic: ListSemantic | DictSemantic,
269
+ target: CollectionTarget,
270
+ nested_value: dict[str, Any],
271
+ ) -> Any:
272
+ from .types._utils import calculate_valid_collection_range, unique_collection_range
273
+ from .types.dictionaries import _generate_dict_of_length
274
+ from .types.list import _generate_list_of_length
275
+
276
+ lower, upper = calculate_valid_collection_range(semantic)
277
+ if isinstance(semantic, DictSemantic):
278
+ lower, upper = unique_collection_range(
279
+ lower, upper, semantic.key_semantic, collection="dict"
280
+ )
281
+ length = ctx.rng.randint(lower, upper)
282
+ return _generate_dict_of_length(
283
+ ctx, semantic, length, target=target, target_value=nested_value
284
+ )
285
+ length = ctx.rng.randint(lower, upper)
286
+ result = _generate_list_of_length(ctx, semantic, length - 1)
287
+ result.insert(target.index, nested_value)
288
+ return result
@@ -0,0 +1,84 @@
1
+ from typing import Any
2
+
3
+ from .._errors import generation_error
4
+ from ..context import GenerationContext
5
+
6
+ from conformly._internal.resolver import ResolvedModel
7
+ from conformly._internal.resolver.semantics import (
8
+ DictSemantic,
9
+ FieldSemantics,
10
+ ListSemantic,
11
+ TupleSemantic,
12
+ )
13
+ from conformly._internal.resolver.semantics._domains import finite_cardinality
14
+ from conformly._internal.types import ViolationType
15
+ from conformly.exceptions import GenerationError
16
+
17
+
18
+ def calculate_valid_collection_range(
19
+ semantic: ListSemantic | DictSemantic | TupleSemantic,
20
+ ) -> tuple[int, int]:
21
+ """Choose a small default range without overriding declared size limits."""
22
+ if semantic.length_range is None:
23
+ return 1, 3
24
+ lower = semantic.length_range.min_length
25
+ upper = semantic.length_range.max_length
26
+ if upper == 0:
27
+ return lower, 0
28
+ lower = max(1, lower)
29
+ return lower, upper if upper is not None else max(3, lower)
30
+
31
+
32
+ def unique_collection_range(
33
+ lower: int,
34
+ upper: int,
35
+ semantic: FieldSemantics,
36
+ *,
37
+ collection: str,
38
+ extra_values: int = 0,
39
+ ) -> tuple[int, int]:
40
+ cardinality = finite_cardinality(semantic)
41
+ if cardinality is not None:
42
+ cardinality += extra_values
43
+ if lower > cardinality:
44
+ raise generation_error(
45
+ "Collection size exceeds the number of distinct valid values",
46
+ code="impossible_collection_size",
47
+ collection=collection,
48
+ requested_size=lower,
49
+ cardinality=cardinality,
50
+ )
51
+ upper = min(upper, cardinality)
52
+ return lower, upper
53
+
54
+
55
+ def collection_retry_error(
56
+ *, collection: str, requested_size: int, generated_size: int, attempts: int
57
+ ) -> GenerationError:
58
+ return generation_error(
59
+ "Could not fill collection with distinct valid values",
60
+ code="collection_retry_exhausted",
61
+ collection=collection,
62
+ requested_size=requested_size,
63
+ generated_size=generated_size,
64
+ attempts=attempts,
65
+ )
66
+
67
+
68
+ def is_hashable(value: Any) -> bool:
69
+ try:
70
+ hash(value)
71
+ return True
72
+ except TypeError:
73
+ return False
74
+
75
+
76
+ def generate_collection_item(
77
+ ctx: GenerationContext,
78
+ item_semantic: FieldSemantics,
79
+ item_nested_model: ResolvedModel | None,
80
+ violations: tuple[ViolationType, ...] | None,
81
+ ) -> Any:
82
+ from ..orchestration import generate_semantic_value
83
+
84
+ return generate_semantic_value(ctx, item_semantic, item_nested_model, violations)