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.
- {conformly-0.7.1/src/conformly.egg-info → conformly-0.7.2}/PKG-INFO +20 -2
- {conformly-0.7.1 → conformly-0.7.2}/README.md +19 -1
- {conformly-0.7.1 → conformly-0.7.2}/pyproject.toml +1 -1
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/orchestration.py +4 -2
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/registry.py +3 -0
- conformly-0.7.2/src/conformly/_internal/generator/types/tuple.py +93 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/core/field.py +24 -1
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/types.py +37 -1
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/models.py +3 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/field.py +21 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/resolve.py +51 -1
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/__init__.py +3 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/factory.py +15 -0
- conformly-0.7.2/src/conformly/_internal/resolver/semantics/tuple.py +21 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/primitives.py +1 -0
- {conformly-0.7.1 → conformly-0.7.2/src/conformly.egg-info}/PKG-INFO +20 -2
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/SOURCES.txt +2 -0
- {conformly-0.7.1 → conformly-0.7.2}/LICENSE +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/setup.cfg +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/_utils.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/case.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/cases.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/api/path.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/numeric.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/fields/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/context.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/_utils.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/float.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/integer.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/list.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/string.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/url.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/generator/types/uuid.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/core/element.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/model.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/planner/session.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/models.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/tracer.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/strategies.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/py.typed +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly/tracer/__init__.py +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.1 → conformly-0.7.2}/src/conformly.egg-info/top_level.txt +0 -0
- {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.
|
|
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=
|
|
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=
|
|
609
|
+
payload = case(User, valid=False, tracer=tracer)
|
|
592
610
|
|
|
593
611
|
trace = tracer.build()
|
|
594
612
|
|
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conformly
|
|
3
|
-
Version: 0.7.
|
|
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=
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/constraints.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{conformly-0.7.1 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/_validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|