conformly 0.7.3__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.3/src/conformly.egg-info → conformly-0.7.4}/PKG-INFO +45 -9
  2. {conformly-0.7.3 → conformly-0.7.4}/README.md +44 -8
  3. {conformly-0.7.3 → conformly-0.7.4}/pyproject.toml +1 -1
  4. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/_utils.py +7 -1
  5. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/orchestration.py +100 -23
  6. conformly-0.7.4/src/conformly/_internal/generator/types/_utils.py +84 -0
  7. conformly-0.7.4/src/conformly/_internal/generator/types/dictionaries.py +137 -0
  8. conformly-0.7.4/src/conformly/_internal/generator/types/float.py +119 -0
  9. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/integer.py +11 -3
  10. conformly-0.7.4/src/conformly/_internal/generator/types/list.py +135 -0
  11. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/string.py +42 -30
  12. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/tuple.py +33 -33
  13. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/attrs.py +6 -1
  14. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/pydantic.py +1 -0
  15. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/typeddict.py +9 -3
  16. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/core/element.py +0 -2
  17. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/core/field.py +5 -0
  18. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/models.py +6 -1
  19. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/field.py +114 -38
  20. conformly-0.7.4/src/conformly/_internal/planner/model.py +18 -0
  21. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/session.py +17 -4
  22. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/models.py +9 -2
  23. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/resolve.py +49 -56
  24. conformly-0.7.4/src/conformly/_internal/resolver/semantics/_domains.py +29 -0
  25. conformly-0.7.4/src/conformly/_internal/resolver/semantics/_numeric.py +15 -0
  26. conformly-0.7.4/src/conformly/_internal/resolver/semantics/_regex.py +9 -0
  27. {conformly-0.7.3 → conformly-0.7.4/src/conformly.egg-info}/PKG-INFO +45 -9
  28. {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/SOURCES.txt +3 -0
  29. conformly-0.7.3/src/conformly/_internal/generator/types/_utils.py +0 -63
  30. conformly-0.7.3/src/conformly/_internal/generator/types/dictionaries.py +0 -122
  31. conformly-0.7.3/src/conformly/_internal/generator/types/float.py +0 -89
  32. conformly-0.7.3/src/conformly/_internal/generator/types/list.py +0 -112
  33. conformly-0.7.3/src/conformly/_internal/planner/model.py +0 -9
  34. {conformly-0.7.3 → conformly-0.7.4}/LICENSE +0 -0
  35. {conformly-0.7.3 → conformly-0.7.4}/setup.cfg +0 -0
  36. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/__init__.py +0 -0
  37. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/__init__.py +0 -0
  38. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/__init__.py +0 -0
  39. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/_errors.py +0 -0
  40. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/_path_proxy.py +0 -0
  41. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/case.py +0 -0
  42. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/cases.py +0 -0
  43. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/path.py +0 -0
  44. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/__init__.py +0 -0
  45. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/base.py +0 -0
  46. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/collections.py +0 -0
  47. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/enum.py +0 -0
  48. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/keys.py +0 -0
  49. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/mapping.py +0 -0
  50. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/numeric.py +0 -0
  51. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/string.py +0 -0
  52. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/fields/__init__.py +0 -0
  53. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/fields/registry.py +0 -0
  54. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/fields/special_types.py +0 -0
  55. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/__init__.py +0 -0
  56. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/_errors.py +0 -0
  57. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/context.py +0 -0
  58. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/protocol.py +0 -0
  59. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/registry.py +0 -0
  60. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/__init__.py +0 -0
  61. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/boolean.py +0 -0
  62. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/email.py +0 -0
  63. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/enum.py +0 -0
  64. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv4.py +0 -0
  65. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv6.py +0 -0
  66. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipvany.py +0 -0
  67. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/url.py +0 -0
  68. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/uuid.py +0 -0
  69. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/__init__.py +0 -0
  70. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
  71. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
  72. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
  73. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
  74. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/registry.py +0 -0
  75. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/api.py +0 -0
  76. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/core/__init__.py +0 -0
  77. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
  78. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
  79. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/types.py +0 -0
  80. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/__init__.py +0 -0
  81. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/_errors.py +0 -0
  82. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/__init__.py +0 -0
  83. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/__init__.py +0 -0
  84. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
  85. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/base.py +0 -0
  86. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
  87. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
  88. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
  89. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/factory.py +0 -0
  90. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/list.py +0 -0
  91. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
  92. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/object.py +0 -0
  93. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/string.py +0 -0
  94. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
  95. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
  96. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/tracer.py +0 -0
  97. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/__init__.py +0 -0
  98. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/constants.py +0 -0
  99. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/primitives.py +0 -0
  100. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/strategies.py +0 -0
  101. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/values.py +0 -0
  102. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/exceptions.py +0 -0
  103. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/py.typed +0 -0
  104. {conformly-0.7.3 → conformly-0.7.4}/src/conformly/tracer/__init__.py +0 -0
  105. {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/dependency_links.txt +0 -0
  106. {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/requires.txt +0 -0
  107. {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/top_level.txt +0 -0
  108. {conformly-0.7.3 → conformly-0.7.4}/tests/test_collection.py +0 -0
  109. {conformly-0.7.3 → 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.3
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
@@ -297,17 +297,43 @@ For `case(Model, valid=False, strategy="<field>")`:
297
297
  - **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
298
298
  - **Exactly one field is targeted** (the one specified by `strategy`).
299
299
  - **The generator will violate constraints** for that field, making it invalid.
300
- - **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.
301
309
  - **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
302
310
  - **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
303
311
 
304
- 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.
305
315
 
306
316
  ## Optional Fields and Defaults
307
317
 
308
- - If a field is **optional** (`Optional[T]`), valid generation may produce `None`.
309
- - If a field has a **default value**, valid generation returns the default.
310
- - 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`.
311
337
 
312
338
  ## Constraints
313
339
 
@@ -327,6 +353,19 @@ If you need **deterministic control** over which exact constraint to violate, th
327
353
  | Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
328
354
  | Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
329
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
+
330
369
  > Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
331
370
  > Use Field() parameters for constraint extraction.
332
371
 
@@ -751,6 +790,3 @@ Contributions welcome!
751
790
  - Add tests for new functionality
752
791
  - Run `uv run -m pytest` and `uv run -m ruff check .`
753
792
  - 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).
@@ -264,17 +264,43 @@ For `case(Model, valid=False, strategy="<field>")`:
264
264
  - **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
265
265
  - **Exactly one field is targeted** (the one specified by `strategy`).
266
266
  - **The generator will violate constraints** for that field, making it invalid.
267
- - **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.
268
276
  - **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
269
277
  - **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
270
278
 
271
- 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.
272
282
 
273
283
  ## Optional Fields and Defaults
274
284
 
275
- - If a field is **optional** (`Optional[T]`), valid generation may produce `None`.
276
- - If a field has a **default value**, valid generation returns the default.
277
- - 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`.
278
304
 
279
305
  ## Constraints
280
306
 
@@ -294,6 +320,19 @@ If you need **deterministic control** over which exact constraint to violate, th
294
320
  | Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
295
321
  | Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
296
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
+
297
336
  > Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
298
337
  > Use Field() parameters for constraint extraction.
299
338
 
@@ -718,6 +757,3 @@ Contributions welcome!
718
757
  - Add tests for new functionality
719
758
  - Run `uv run -m pytest` and `uv run -m ruff check .`
720
759
  - 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"
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"
@@ -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
@@ -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
 
@@ -55,6 +64,8 @@ def _built_dict_with_violations(
55
64
  overrides: dict[FieldPath, Any] | None = None,
56
65
  tracer: Tracer | None = None,
57
66
  path_names: tuple[str, ...] = (),
67
+ collection_target: CollectionTarget | None = None,
68
+ collection_steps: tuple[tuple[FieldPath, CollectionTarget], ...] = (),
58
69
  ) -> dict[str, Any]:
59
70
  result: dict[str, Any] = {}
60
71
  target_index = target_path[depth]
@@ -88,7 +99,9 @@ def _built_dict_with_violations(
88
99
 
89
100
  if tracer:
90
101
  tracer.set_target_path(".".join((*path_names, field.name)))
91
- value = generate_field(ctx, field, violations, overrides, tracer)
102
+ value = generate_field(
103
+ ctx, field, violations, overrides, tracer, collection_target
104
+ )
92
105
  if value is not UNSET:
93
106
  result[field.name] = value
94
107
 
@@ -100,7 +113,7 @@ def _built_dict_with_violations(
100
113
  field=field.name,
101
114
  )
102
115
 
103
- result[field.name] = _built_dict_with_violations(
116
+ nested_value = _built_dict_with_violations(
104
117
  ctx=ctx,
105
118
  model=field.nested_model,
106
119
  target_path=target_path,
@@ -109,8 +122,27 @@ def _built_dict_with_violations(
109
122
  overrides=overrides,
110
123
  tracer=tracer,
111
124
  path_names=(*path_names, field.name),
125
+ collection_target=collection_target,
126
+ collection_steps=collection_steps,
112
127
  )
113
128
 
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
145
+
114
146
  else:
115
147
  key, value = _generate_extra_field_value(ctx)
116
148
  suffix = 1
@@ -137,6 +169,7 @@ def generate_field(
137
169
  violations: tuple[ViolationType, ...] | None = None,
138
170
  overrides: dict[FieldPath, Any] | None = None,
139
171
  tracer: Tracer | None = None,
172
+ collection_target: CollectionTarget | None = None,
140
173
  ) -> Any:
141
174
  if violations is None:
142
175
  if overrides and field.path in overrides:
@@ -160,7 +193,7 @@ def generate_field(
160
193
  return value
161
194
 
162
195
  if field.nested_model and not isinstance(
163
- field.semantic, (ListSemantic, TupleSemantic)
196
+ field.semantic, (ListSemantic, DictSemantic, TupleSemantic)
164
197
  ):
165
198
  return generate_valid(ctx, field.nested_model, overrides)
166
199
 
@@ -170,31 +203,50 @@ def generate_field(
170
203
  tracer.set_violation(violation)
171
204
  tracer.set_value_source(ValueSource.GENERATED)
172
205
 
173
- if violation == ViolationType.TYPE_MISMATCH:
174
- mismatch_kind = choose_mismatch_kind(field.semantic.kind)
175
- mismatch_semantic = create_minimal_semantic(mismatch_kind)
176
- value = get_generator(mismatch_kind).generate_value(
177
- ctx, mismatch_semantic, None
178
- )
179
- if tracer:
180
- tracer.set_generated_value(value)
181
- return value
182
-
183
- if violation == ViolationType.MISSING_FIELD:
184
- if tracer:
185
- tracer.set_generated_value(UNSET)
186
- return UNSET
187
-
188
- value = get_generator(field.semantic.kind).generate_value(
189
- ctx, field.semantic, violation
206
+ value = generate_semantic_value(
207
+ ctx, field.semantic, field.nested_model, violations, collection_target
190
208
  )
191
-
192
209
  if tracer:
193
210
  tracer.set_generated_value(value)
194
-
195
211
  return value
196
212
 
197
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
+
198
250
  def _choose_violation(
199
251
  violations: tuple[ViolationType, ...] | None,
200
252
  ) -> ViolationType | None:
@@ -209,3 +261,28 @@ def _generate_extra_field_value(ctx: GenerationContext) -> tuple[str, Any]:
209
261
  value = get_generator(semantic.kind).generate_value(ctx, semantic, None)
210
262
  key = "extra"
211
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)
@@ -0,0 +1,137 @@
1
+ from typing import Any
2
+
3
+ from .._errors import generation_error
4
+ from ..context import GenerationContext
5
+ from ._utils import (
6
+ calculate_valid_collection_range,
7
+ collection_retry_error,
8
+ generate_collection_item,
9
+ is_hashable,
10
+ unique_collection_range,
11
+ )
12
+
13
+ from conformly._internal.planner.model import CollectionTarget
14
+ from conformly._internal.resolver.semantics import DictSemantic
15
+ from conformly._internal.types import UNSET, ViolationType
16
+
17
+
18
+ def generate_value(
19
+ ctx: GenerationContext,
20
+ semantic: DictSemantic,
21
+ violation: ViolationType | None = None,
22
+ *,
23
+ target: CollectionTarget | None = None,
24
+ ) -> dict[Any, Any]:
25
+ return (
26
+ _generate_valid_dict(ctx, semantic)
27
+ if violation is None
28
+ else _generate_invalid_dict(ctx, semantic, violation, target)
29
+ )
30
+
31
+
32
+ def _generate_valid_dict(
33
+ ctx: GenerationContext, semantic: DictSemantic
34
+ ) -> dict[Any, Any]:
35
+ min_len, max_len = calculate_valid_collection_range(semantic)
36
+
37
+ min_len, max_len = unique_collection_range(
38
+ min_len, max_len, semantic.key_semantic, collection="dict"
39
+ )
40
+ length = ctx.rng.randint(min_len, max_len)
41
+ return _generate_dict_of_length(ctx, semantic, length)
42
+
43
+
44
+ def _generate_dict_of_length(
45
+ ctx: GenerationContext,
46
+ semantic: DictSemantic,
47
+ length: int,
48
+ violation: ViolationType | None = None,
49
+ target: CollectionTarget | None = None,
50
+ target_value: Any = UNSET,
51
+ ) -> dict[Any, Any]:
52
+ result: dict[Any, Any] = {}
53
+ max_attempts = max(100, length * 20)
54
+ for _ in range(max_attempts):
55
+ if len(result) == length:
56
+ return result
57
+ is_target = target is not None and len(result) == target.index
58
+ key_violation = (
59
+ (violation,)
60
+ if is_target
61
+ and target is not None
62
+ and target.side == "key"
63
+ and violation is not None
64
+ else None
65
+ )
66
+ value_violation = (
67
+ (violation,)
68
+ if is_target
69
+ and target is not None
70
+ and target.side == "value"
71
+ and violation is not None
72
+ else None
73
+ )
74
+ key = generate_collection_item(ctx, semantic.key_semantic, None, key_violation)
75
+ if not is_hashable(key) or key in result:
76
+ continue
77
+ result[key] = (
78
+ target_value
79
+ if is_target and target_value is not UNSET
80
+ else generate_collection_item(
81
+ ctx,
82
+ semantic.value_semantic,
83
+ semantic.value_nested_model,
84
+ value_violation,
85
+ )
86
+ )
87
+ if len(result) == length:
88
+ return result
89
+ raise collection_retry_error(
90
+ collection="dict",
91
+ requested_size=length,
92
+ generated_size=len(result),
93
+ attempts=max_attempts,
94
+ )
95
+
96
+
97
+ def _generate_invalid_dict(
98
+ ctx: GenerationContext,
99
+ semantic: DictSemantic,
100
+ violation: ViolationType,
101
+ target: CollectionTarget | None,
102
+ ) -> dict[Any, Any]:
103
+ min_len, max_len = calculate_valid_collection_range(semantic)
104
+ if violation == ViolationType.TOO_LESS_ITEMS:
105
+ lower = semantic.length_range.min_length if semantic.length_range else min_len
106
+ length = max(0, lower - 1)
107
+ unique_collection_range(
108
+ length, length, semantic.key_semantic, collection="dict"
109
+ )
110
+ return _generate_dict_of_length(ctx, semantic, length)
111
+ if violation == ViolationType.TOO_MANY_ITEMS:
112
+ length = max_len + 1
113
+ unique_collection_range(
114
+ length, length, semantic.key_semantic, collection="dict"
115
+ )
116
+ return _generate_dict_of_length(ctx, semantic, length)
117
+ if target is None or target.side not in ("key", "value"):
118
+ raise generation_error(
119
+ "Dictionary violation requires a planned key or value target",
120
+ code="missing_collection_target",
121
+ )
122
+ min_len, max_len = unique_collection_range(
123
+ min_len,
124
+ max_len,
125
+ semantic.key_semantic,
126
+ collection="dict",
127
+ extra_values=1 if target.side == "key" else 0,
128
+ )
129
+ length = ctx.rng.randint(min_len, max_len)
130
+ if not 0 <= target.index < length:
131
+ raise generation_error(
132
+ "Dictionary target is outside collection",
133
+ code="invalid_collection_target",
134
+ index=target.index,
135
+ size=length,
136
+ )
137
+ return _generate_dict_of_length(ctx, semantic, length, violation, target)