conformly 0.7.1__tar.gz → 0.7.2__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 (100) hide show
  1. {conformly-0.7.1/src/conformly.egg-info → conformly-0.7.2}/PKG-INFO +20 -2
  2. {conformly-0.7.1 → conformly-0.7.2}/README.md +19 -1
  3. {conformly-0.7.1 → conformly-0.7.2}/pyproject.toml +1 -1
  4. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/orchestration.py +4 -2
  5. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/registry.py +3 -0
  6. conformly-0.7.2/src/conformly/_internal/generator/types/tuple.py +93 -0
  7. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/core/field.py +24 -1
  8. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/types.py +37 -1
  9. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/models.py +3 -0
  10. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/field.py +21 -0
  11. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/resolve.py +51 -1
  12. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/__init__.py +3 -0
  13. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/factory.py +15 -0
  14. conformly-0.7.2/src/conformly/_internal/resolver/semantics/tuple.py +21 -0
  15. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/primitives.py +1 -0
  16. {conformly-0.7.1 → conformly-0.7.2/src/conformly.egg-info}/PKG-INFO +20 -2
  17. {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/SOURCES.txt +2 -0
  18. {conformly-0.7.1 → conformly-0.7.2}/LICENSE +0 -0
  19. {conformly-0.7.1 → conformly-0.7.2}/setup.cfg +0 -0
  20. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/__init__.py +0 -0
  21. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/__init__.py +0 -0
  22. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/__init__.py +0 -0
  23. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/_errors.py +0 -0
  24. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/_path_proxy.py +0 -0
  25. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/_utils.py +0 -0
  26. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/case.py +0 -0
  27. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/cases.py +0 -0
  28. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/path.py +0 -0
  29. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/__init__.py +0 -0
  30. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/base.py +0 -0
  31. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/collections.py +0 -0
  32. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/enum.py +0 -0
  33. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/keys.py +0 -0
  34. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/mapping.py +0 -0
  35. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/numeric.py +0 -0
  36. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/string.py +0 -0
  37. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/fields/__init__.py +0 -0
  38. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/fields/registry.py +0 -0
  39. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/fields/special_types.py +0 -0
  40. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/__init__.py +0 -0
  41. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/_errors.py +0 -0
  42. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/context.py +0 -0
  43. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/protocol.py +0 -0
  44. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/__init__.py +0 -0
  45. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/_utils.py +0 -0
  46. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/boolean.py +0 -0
  47. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
  48. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/email.py +0 -0
  49. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/enum.py +0 -0
  50. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/float.py +0 -0
  51. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/integer.py +0 -0
  52. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipv4.py +0 -0
  53. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipv6.py +0 -0
  54. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipvany.py +0 -0
  55. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/list.py +0 -0
  56. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/string.py +0 -0
  57. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/url.py +0 -0
  58. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/uuid.py +0 -0
  59. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/__init__.py +0 -0
  60. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
  61. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
  62. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
  63. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
  64. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
  65. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
  66. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/registry.py +0 -0
  67. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
  68. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/api.py +0 -0
  69. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/core/__init__.py +0 -0
  70. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/core/element.py +0 -0
  71. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
  72. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
  73. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/__init__.py +0 -0
  74. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/_errors.py +0 -0
  75. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/model.py +0 -0
  76. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/session.py +0 -0
  77. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/__init__.py +0 -0
  78. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/models.py +0 -0
  79. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
  80. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/base.py +0 -0
  81. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
  82. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
  83. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
  84. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/list.py +0 -0
  85. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
  86. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/object.py +0 -0
  87. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/string.py +0 -0
  88. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
  89. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/tracer.py +0 -0
  90. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/__init__.py +0 -0
  91. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/constants.py +0 -0
  92. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/strategies.py +0 -0
  93. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/values.py +0 -0
  94. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/exceptions.py +0 -0
  95. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/py.typed +0 -0
  96. {conformly-0.7.1 → conformly-0.7.2}/src/conformly/tracer/__init__.py +0 -0
  97. {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/dependency_links.txt +0 -0
  98. {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/requires.txt +0 -0
  99. {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/top_level.txt +0 -0
  100. {conformly-0.7.1 → conformly-0.7.2}/tests/test_path.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conformly
3
- Version: 0.7.1
3
+ Version: 0.7.2
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
@@ -561,6 +561,24 @@ case(Model)
561
561
 
562
562
  This ensures consistent output format (list) while preserving uniqueness guarantees.
563
563
 
564
+ ### Tuple support
565
+
566
+ Fixed tuples and variadic tuples (`tuple[T, ...]`) are generated in their declared
567
+ order. Element annotations and nested models are handled independently:
568
+
569
+ ```python
570
+ @dataclass
571
+ class Event:
572
+ point: tuple[float, float]
573
+ labels: tuple[str, ...]
574
+
575
+ case(Event, valid=True)
576
+ # -> {"point": (1.2, 3.4), "labels": ("abc", "def")}
577
+ ```
578
+
579
+ For invalid generation, a constrained tuple element is selected and invalidated
580
+ without changing the other elements.
581
+
564
582
  ### Dict support
565
583
 
566
584
  `conformly` supports generation of `dict[K, V]`, where K must be a hashable type (`str` or `Enum`) and `V` can be any supported type (primitives, nested models, etc.).
@@ -621,7 +639,7 @@ from conformly.tracer import Tracer
621
639
 
622
640
  tracer = Tracer()
623
641
 
624
- payload = case(User, valid=True, tracer=tracer)
642
+ payload = case(User, valid=False, tracer=tracer)
625
643
 
626
644
  trace = tracer.build()
627
645
 
@@ -528,6 +528,24 @@ case(Model)
528
528
 
529
529
  This ensures consistent output format (list) while preserving uniqueness guarantees.
530
530
 
531
+ ### Tuple support
532
+
533
+ Fixed tuples and variadic tuples (`tuple[T, ...]`) are generated in their declared
534
+ order. Element annotations and nested models are handled independently:
535
+
536
+ ```python
537
+ @dataclass
538
+ class Event:
539
+ point: tuple[float, float]
540
+ labels: tuple[str, ...]
541
+
542
+ case(Event, valid=True)
543
+ # -> {"point": (1.2, 3.4), "labels": ("abc", "def")}
544
+ ```
545
+
546
+ For invalid generation, a constrained tuple element is selected and invalidated
547
+ without changing the other elements.
548
+
531
549
  ### Dict support
532
550
 
533
551
  `conformly` supports generation of `dict[K, V]`, where K must be a hashable type (`str` or `Enum`) and `V` can be any supported type (primitives, nested models, etc.).
@@ -588,7 +606,7 @@ from conformly.tracer import Tracer
588
606
 
589
607
  tracer = Tracer()
590
608
 
591
- payload = case(User, valid=True, tracer=tracer)
609
+ payload = case(User, valid=False, tracer=tracer)
592
610
 
593
611
  trace = tracer.build()
594
612
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "conformly"
3
- version = "0.7.1"
3
+ version = "0.7.2"
4
4
  description = "Generate valid & invalid test data from your typed schemas"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -10,7 +10,7 @@ from conformly._internal.resolver import (
10
10
  ResolvedModel,
11
11
  create_minimal_semantic,
12
12
  )
13
- from conformly._internal.resolver.semantics import ListSemantic
13
+ from conformly._internal.resolver.semantics import ListSemantic, TupleSemantic
14
14
  from conformly._internal.tracer import Tracer, ValueSource
15
15
  from conformly._internal.types import UNSET, FieldKind, FieldPath, ViolationType
16
16
 
@@ -154,7 +154,9 @@ def generate_field(
154
154
 
155
155
  return default
156
156
 
157
- if field.nested_model and not isinstance(field.semantic, ListSemantic):
157
+ if field.nested_model and not isinstance(
158
+ field.semantic, (ListSemantic, TupleSemantic)
159
+ ):
158
160
  return generate_valid(ctx, field.nested_model, overrides)
159
161
 
160
162
  violation = _choose_violation(violations)
@@ -12,6 +12,7 @@ from .types import (
12
12
  ipvany,
13
13
  list,
14
14
  string,
15
+ tuple,
15
16
  url,
16
17
  uuid,
17
18
  )
@@ -30,6 +31,7 @@ _GENERATORS: dict[FieldKind, TypeGeneratorProtocol] = {
30
31
  FieldKind.IPvAny: ipvany,
31
32
  FieldKind.LIST: list,
32
33
  FieldKind.DICT: dictionaries,
34
+ FieldKind.TUPLE: tuple,
33
35
  FieldKind.UUID: uuid,
34
36
  FieldKind.URL: url,
35
37
  FieldKind.HTTPURL: url,
@@ -51,6 +53,7 @@ _MISMATCH_MAPPING: dict[FieldKind, FieldKind] = {
51
53
  FieldKind.UUID: FieldKind.FLOAT,
52
54
  FieldKind.URL: FieldKind.FLOAT,
53
55
  FieldKind.HTTPURL: FieldKind.INTEGER,
56
+ FieldKind.TUPLE: FieldKind.INTEGER,
54
57
  }
55
58
 
56
59
 
@@ -0,0 +1,93 @@
1
+ from typing import Any
2
+
3
+ from ..context import GenerationContext
4
+ from ._utils import generate_collection_item
5
+
6
+ from conformly._internal.resolver.semantics import TupleSemantic
7
+ from conformly._internal.types import ViolationType
8
+ from conformly.exceptions import PlanningError
9
+
10
+
11
+ def _supports_violation(semantic: Any, violation: ViolationType) -> bool:
12
+ from conformly._internal.planner.field import _define_allowed_violation_types
13
+
14
+ try:
15
+ return violation in _define_allowed_violation_types(semantic)
16
+ except PlanningError:
17
+ return False
18
+
19
+
20
+ def generate_value(
21
+ ctx: GenerationContext,
22
+ semantic: TupleSemantic,
23
+ violation: ViolationType | None = None,
24
+ ) -> tuple[Any, ...]:
25
+ if violation is None:
26
+ return _generate_valid_tuple(ctx, semantic)
27
+
28
+ return _generate_invalid_tuple(ctx, semantic, violation)
29
+
30
+
31
+ def _generate_valid_tuple(
32
+ ctx: GenerationContext, semantic: TupleSemantic
33
+ ) -> tuple[Any, ...]:
34
+ if not semantic.is_variadic:
35
+ elements = semantic.elements_semantics
36
+ else:
37
+ min_len = semantic.length_range.min_length if semantic.length_range else 1
38
+ max_len = semantic.length_range.max_length if semantic.length_range else 3
39
+ length = ctx.rng.randint(min_len or 1, max_len or 3)
40
+ elements = semantic.elements_semantics * length
41
+
42
+ return tuple(
43
+ generate_collection_item(ctx, item_semantic, nested_model, None)
44
+ for item_semantic, nested_model in elements
45
+ )
46
+
47
+
48
+ def _generate_invalid_tuple(
49
+ ctx: GenerationContext, semantic: TupleSemantic, violation: ViolationType
50
+ ) -> tuple[Any, ...]:
51
+ if violation in (ViolationType.TOO_LESS_ITEMS, ViolationType.TOO_MANY_ITEMS):
52
+ if not semantic.is_variadic:
53
+ return _generate_valid_tuple(ctx, semantic)
54
+
55
+ min_len = semantic.length_range.min_length if semantic.length_range else 1
56
+ max_len = semantic.length_range.max_length if semantic.length_range else 3
57
+ length = (
58
+ max(0, min_len - 1)
59
+ if violation == ViolationType.TOO_LESS_ITEMS
60
+ else (max_len or 3) + 1
61
+ )
62
+ return tuple(
63
+ generate_collection_item(ctx, *semantic.elements_semantics[0], None)
64
+ for _ in range(length)
65
+ )
66
+
67
+ if semantic.is_variadic:
68
+ min_len = semantic.length_range.min_length if semantic.length_range else 1
69
+ max_len = semantic.length_range.max_length if semantic.length_range else 3
70
+ length = ctx.rng.randint(min_len or 1, max_len or 3)
71
+ elements = semantic.elements_semantics * length
72
+ else:
73
+ elements = semantic.elements_semantics
74
+
75
+ candidates = [
76
+ i
77
+ for i, (item_semantic, _) in enumerate(elements)
78
+ if _supports_violation(item_semantic, violation)
79
+ ]
80
+ violate_idx = (
81
+ ctx.rng.choice(candidates)
82
+ if candidates
83
+ else ctx.rng.randint(0, len(elements) - 1)
84
+ )
85
+ return tuple(
86
+ generate_collection_item(
87
+ ctx,
88
+ item_semantic,
89
+ nested_model,
90
+ (violation,) if i == violate_idx else None,
91
+ )
92
+ for i, (item_semantic, nested_model) in enumerate(elements)
93
+ )
@@ -2,7 +2,14 @@ from collections.abc import Callable
2
2
  from typing import Any
3
3
 
4
4
  from ..extractors.constraints import split_collection_constraints
5
- from ..extractors.types import DictNode, ListNode, ScalarNode, TypeNode, normalize_type
5
+ from ..extractors.types import (
6
+ DictNode,
7
+ ListNode,
8
+ ScalarNode,
9
+ TupleNode,
10
+ TypeNode,
11
+ normalize_type,
12
+ )
6
13
  from ..models import ElementSpec, FieldSpec
7
14
 
8
15
  from conformly._internal.constraints import Constraint
@@ -41,6 +48,22 @@ def build_field_spec(
41
48
  nullable=node.nullable,
42
49
  )
43
50
 
51
+ case TupleNode():
52
+ element_constraints, collection_constraints = split_collection_constraints(
53
+ external_constraints
54
+ )
55
+
56
+ return FieldSpec(
57
+ name=name,
58
+ collection_type=node.origin,
59
+ collection_constraints=(*node.constraints, *collection_constraints),
60
+ items=tuple(
61
+ resolve_element(i, name, element_constraints) for i in node.items
62
+ ),
63
+ default=default,
64
+ nullable=node.nullable,
65
+ )
66
+
44
67
  case DictNode():
45
68
  return FieldSpec(
46
69
  name=name,
@@ -37,6 +37,16 @@ class ListNode:
37
37
  item: "TypeNode"
38
38
 
39
39
 
40
+ @dataclass(frozen=True, slots=True)
41
+ class TupleNode:
42
+ kind: Literal["tuple"]
43
+ runtime_type: Any
44
+ origin: type
45
+ nullable: bool
46
+ constraints: tuple[Constraint, ...]
47
+ items: tuple["TypeNode", ...]
48
+
49
+
40
50
  @dataclass(frozen=True, slots=True)
41
51
  class DictNode:
42
52
  kind: Literal["dict"]
@@ -47,7 +57,7 @@ class DictNode:
47
57
  value: "TypeNode"
48
58
 
49
59
 
50
- TypeNode = ScalarNode | ListNode | DictNode
60
+ TypeNode = ScalarNode | ListNode | DictNode | TupleNode
51
61
 
52
62
 
53
63
  def normalize_type(field_type: Any, field_name: str) -> TypeNode:
@@ -116,6 +126,32 @@ def _build_node(
116
126
  item=item,
117
127
  )
118
128
 
129
+ if origin is tuple:
130
+ if not args:
131
+ raise SchemaError(
132
+ f"Field '{field_name}': '{origin.__name__}' must be parameterized",
133
+ context={
134
+ "code": "empty_collection",
135
+ "field_name": field_name,
136
+ "field_type": repr(t),
137
+ },
138
+ )
139
+ items: list[TypeNode] = []
140
+ if len(args) == 2 and args[1] is Ellipsis:
141
+ items.append(normalize_type(args[0], field_name))
142
+ else:
143
+ for i, _ in enumerate(args):
144
+ item = normalize_type(args[i], field_name)
145
+ items.append(item)
146
+ return TupleNode(
147
+ kind="tuple",
148
+ runtime_type=origin,
149
+ origin=origin,
150
+ nullable=nullable,
151
+ constraints=constraints,
152
+ items=tuple(items),
153
+ )
154
+
119
155
  if origin is dict:
120
156
  if len(args) != 2:
121
157
  raise SchemaError(
@@ -23,6 +23,7 @@ class FieldSpec:
23
23
  collection_type: type | None = None
24
24
  collection_constraints: tuple[Constraint, ...] = ()
25
25
  item: ElementSpec | None = None
26
+ items: tuple[ElementSpec, ...] | None = None
26
27
  key: ElementSpec | None = None
27
28
  value: ElementSpec | None = None
28
29
  nullable: bool = False
@@ -38,6 +39,7 @@ class FieldSpec:
38
39
  return (
39
40
  (self.element is not None and len(self.element.constraints) != 0)
40
41
  or (self.item is not None and len(self.item.constraints) != 0)
42
+ or any(len(item.constraints) != 0 for item in (self.items or ()))
41
43
  or (self.key is not None and len(self.key.constraints) != 0)
42
44
  or (self.value is not None and len(self.value.constraints) != 0)
43
45
  or len(self.collection_constraints) != 0
@@ -47,6 +49,7 @@ class FieldSpec:
47
49
  return (
48
50
  (self.element is not None and self.element.nested_model is not None)
49
51
  or (self.item is not None and self.item.nested_model is not None)
52
+ or any(item.nested_model is not None for item in (self.items or ()))
50
53
  or (self.key is not None and self.key.nested_model is not None)
51
54
  or (self.value is not None and self.value.nested_model is not None)
52
55
  )
@@ -11,6 +11,7 @@ from conformly._internal.resolver.semantics import (
11
11
  NumericSemantic,
12
12
  ObjectSemantic,
13
13
  StringSemantic,
14
+ TupleSemantic,
14
15
  UUIDSemantic,
15
16
  )
16
17
  from conformly._internal.resolver.semantics.dict import DictSemantic
@@ -123,6 +124,9 @@ def _define_allowed_violation_types(
123
124
  case DictSemantic(kind=FieldKind.DICT):
124
125
  violations = _define_dict_violations(semantic)
125
126
 
127
+ case TupleSemantic(kind=FieldKind.TUPLE):
128
+ violations = _define_tuple_violations(semantic)
129
+
126
130
  case EnumSemantic(kind=FieldKind.ENUM):
127
131
  violations = [ViolationType.NOT_ALLOWED_VALUE]
128
132
 
@@ -252,3 +256,20 @@ def _safe_define(semantic: FieldSemantics) -> list[ViolationType]:
252
256
  return list(_define_allowed_violation_types(semantic, False, False))
253
257
  except PlanningError:
254
258
  return []
259
+
260
+
261
+ def _define_tuple_violations(semantic: TupleSemantic) -> list[ViolationType]:
262
+ result: list[ViolationType] = []
263
+
264
+ if semantic.is_variadic and semantic.length_range is not None:
265
+ if semantic.length_range.min_length > 0:
266
+ result.append(ViolationType.TOO_LESS_ITEMS)
267
+ if semantic.length_range.max_length is not None:
268
+ result.append(ViolationType.TOO_MANY_ITEMS)
269
+
270
+ for item_semantic, _ in semantic.elements_semantics:
271
+ for violation in _safe_define(item_semantic):
272
+ if violation not in result:
273
+ result.append(violation)
274
+
275
+ return result
@@ -13,6 +13,7 @@ from .semantics import (
13
13
  NumericSemantic,
14
14
  ObjectSemantic,
15
15
  StringSemantic,
16
+ TupleSemantic,
16
17
  UUIDSemantic,
17
18
  )
18
19
 
@@ -74,7 +75,7 @@ def resolve_field(field_spec: FieldSpec, path: FieldPath) -> ResolvedField:
74
75
  nested_model=nested,
75
76
  )
76
77
 
77
- if field_spec.collection_type in (list, set, frozenset, tuple) and field_spec.item:
78
+ if field_spec.collection_type in (list, set, frozenset) and field_spec.item:
78
79
  item_semantic, item_nested = resolve_element(field_spec.item, field_spec, path)
79
80
 
80
81
  list_semantic = create_list_semantics(
@@ -90,6 +91,24 @@ def resolve_field(field_spec: FieldSpec, path: FieldPath) -> ResolvedField:
90
91
  nested_model=item_nested,
91
92
  )
92
93
 
94
+ if field_spec.collection_type is tuple and field_spec.items is not None:
95
+ elements: list[tuple[FieldSemantics, ResolvedModel | None]] = []
96
+ for i, item in enumerate(field_spec.items):
97
+ item_semantic, item_nested = resolve_element(item, field_spec, (*path, i))
98
+ elements.append((item_semantic, item_nested))
99
+
100
+ return ResolvedField(
101
+ field_spec=field_spec,
102
+ path=path,
103
+ semantic=create_tuple_semantic(
104
+ constraints=field_spec.collection_constraints,
105
+ elements_semantics=tuple(elements),
106
+ is_variadic=len(field_spec.items) == 1,
107
+ has_constraints=field_spec.has_constraints(),
108
+ ),
109
+ nested_model=None,
110
+ )
111
+
93
112
  if field_spec.collection_type is dict and field_spec.value and field_spec.key:
94
113
  if field_spec.key.field_type not in (str, ENUMERATED_TYPE):
95
114
  raise ResolutionError(
@@ -544,6 +563,37 @@ def create_list_semantics(
544
563
  )
545
564
 
546
565
 
566
+ def create_tuple_semantic(
567
+ constraints: tuple[Constraint, ...],
568
+ elements_semantics: tuple[tuple[FieldSemantics, ResolvedModel | None], ...],
569
+ is_variadic: bool,
570
+ has_constraints: bool,
571
+ ) -> TupleSemantic:
572
+ _, length_range, _ = _resolve_collection_constraints(constraints)
573
+
574
+ if not is_variadic:
575
+ fixed_length = len(elements_semantics)
576
+ min_length = length_range.min_length or fixed_length
577
+ max_length = (
578
+ length_range.max_length
579
+ if length_range.max_length is not None
580
+ else fixed_length
581
+ )
582
+ if min_length > fixed_length or max_length < fixed_length:
583
+ raise SchemaError(
584
+ "Tuple length constraints do not match fixed tuple length",
585
+ context={"code": "invalid_tuple_length_range", "length": fixed_length},
586
+ )
587
+ length_range = LengthRange(fixed_length, fixed_length)
588
+
589
+ return TupleSemantic(
590
+ elements_semantics=elements_semantics,
591
+ length_range=length_range,
592
+ is_variadic=is_variadic,
593
+ has_constraints=has_constraints,
594
+ )
595
+
596
+
547
597
  def create_dict_semantic(
548
598
  constraints: tuple[Constraint, ...],
549
599
  key_semantic: FieldSemantics,
@@ -5,6 +5,7 @@ from .list import ListSemantic
5
5
  from .numeric import NumericSemantic
6
6
  from .object import ObjectSemantic
7
7
  from .string import StringSemantic
8
+ from .tuple import TupleSemantic
8
9
  from .uuid import UUIDSemantic
9
10
 
10
11
  FieldSemantics = (
@@ -16,6 +17,7 @@ FieldSemantics = (
16
17
  | ListSemantic
17
18
  | UUIDSemantic
18
19
  | DictSemantic
20
+ | TupleSemantic
19
21
  )
20
22
 
21
23
 
@@ -28,5 +30,6 @@ __all__ = [
28
30
  "NumericSemantic",
29
31
  "ObjectSemantic",
30
32
  "StringSemantic",
33
+ "TupleSemantic",
31
34
  "UUIDSemantic",
32
35
  ]
@@ -6,6 +6,7 @@ from .list import ListSemantic
6
6
  from .numeric import NumericSemantic
7
7
  from .object import ObjectSemantic
8
8
  from .string import StringSemantic
9
+ from .tuple import TupleSemantic
9
10
  from .uuid import UUIDSemantic
10
11
 
11
12
  from conformly._internal.types import FieldKind, LengthRange, Range
@@ -66,5 +67,19 @@ def create_minimal_semantic(kind: FieldKind) -> FieldSemantics:
66
67
  )
67
68
  case FieldKind.UUID:
68
69
  return UUIDSemantic()
70
+ case FieldKind.TUPLE:
71
+ return TupleSemantic(
72
+ elements_semantics=(
73
+ (
74
+ StringSemantic(
75
+ kind=FieldKind.STRING,
76
+ length_range=LengthRange(0, None),
77
+ pattern=None,
78
+ ),
79
+ None,
80
+ ),
81
+ ),
82
+ is_variadic=True,
83
+ )
69
84
  case _:
70
85
  raise ValueError(f"Unsupported FieldKind: {kind}")
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+ from typing import TYPE_CHECKING
3
+
4
+ from .base import BaseSemantic
5
+
6
+ from conformly._internal.types import FieldKind, LengthRange
7
+
8
+ if TYPE_CHECKING:
9
+ from ..models import ResolvedModel
10
+ from . import FieldSemantics
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class TupleSemantic(BaseSemantic):
15
+ elements_semantics: tuple[tuple["FieldSemantics", "ResolvedModel | None"], ...]
16
+ length_range: LengthRange | None = None
17
+ is_variadic: bool = False
18
+
19
+ @property
20
+ def kind(self) -> FieldKind:
21
+ return FieldKind.TUPLE
@@ -23,6 +23,7 @@ class FieldKind(Enum):
23
23
  # collections
24
24
  LIST = "list"
25
25
  DICT = "dict"
26
+ TUPLE = "tuple"
26
27
 
27
28
 
28
29
  class ViolationType(Enum):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conformly
3
- Version: 0.7.1
3
+ Version: 0.7.2
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
@@ -561,6 +561,24 @@ case(Model)
561
561
 
562
562
  This ensures consistent output format (list) while preserving uniqueness guarantees.
563
563
 
564
+ ### Tuple support
565
+
566
+ Fixed tuples and variadic tuples (`tuple[T, ...]`) are generated in their declared
567
+ order. Element annotations and nested models are handled independently:
568
+
569
+ ```python
570
+ @dataclass
571
+ class Event:
572
+ point: tuple[float, float]
573
+ labels: tuple[str, ...]
574
+
575
+ case(Event, valid=True)
576
+ # -> {"point": (1.2, 3.4), "labels": ("abc", "def")}
577
+ ```
578
+
579
+ For invalid generation, a constrained tuple element is selected and invalidated
580
+ without changing the other elements.
581
+
564
582
  ### Dict support
565
583
 
566
584
  `conformly` supports generation of `dict[K, V]`, where K must be a hashable type (`str` or `Enum`) and `V` can be any supported type (primitives, nested models, etc.).
@@ -621,7 +639,7 @@ from conformly.tracer import Tracer
621
639
 
622
640
  tracer = Tracer()
623
641
 
624
- payload = case(User, valid=True, tracer=tracer)
642
+ payload = case(User, valid=False, tracer=tracer)
625
643
 
626
644
  trace = tracer.build()
627
645
 
@@ -48,6 +48,7 @@ src/conformly/_internal/generator/types/ipv6.py
48
48
  src/conformly/_internal/generator/types/ipvany.py
49
49
  src/conformly/_internal/generator/types/list.py
50
50
  src/conformly/_internal/generator/types/string.py
51
+ src/conformly/_internal/generator/types/tuple.py
51
52
  src/conformly/_internal/generator/types/url.py
52
53
  src/conformly/_internal/generator/types/uuid.py
53
54
  src/conformly/_internal/parser/__init__.py
@@ -86,6 +87,7 @@ src/conformly/_internal/resolver/semantics/list.py
86
87
  src/conformly/_internal/resolver/semantics/numeric.py
87
88
  src/conformly/_internal/resolver/semantics/object.py
88
89
  src/conformly/_internal/resolver/semantics/string.py
90
+ src/conformly/_internal/resolver/semantics/tuple.py
89
91
  src/conformly/_internal/resolver/semantics/uuid.py
90
92
  src/conformly/_internal/types/__init__.py
91
93
  src/conformly/_internal/types/constants.py
File without changes
File without changes
File without changes