conformly 0.7.0__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.0/src/conformly.egg-info → conformly-0.7.2}/PKG-INFO +47 -3
- {conformly-0.7.0 → conformly-0.7.2}/README.md +46 -2
- {conformly-0.7.0 → conformly-0.7.2}/pyproject.toml +1 -1
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/case.py +12 -1
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/orchestration.py +48 -10
- {conformly-0.7.0 → 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.0 → conformly-0.7.2}/src/conformly/_internal/parser/core/field.py +24 -1
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/types.py +37 -1
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/models.py +3 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/planner/field.py +21 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/resolve.py +51 -1
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/__init__.py +3 -0
- {conformly-0.7.0 → 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.2/src/conformly/_internal/tracer.py +63 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/types/primitives.py +1 -0
- conformly-0.7.2/src/conformly/tracer/__init__.py +3 -0
- {conformly-0.7.0 → conformly-0.7.2/src/conformly.egg-info}/PKG-INFO +47 -3
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly.egg-info/SOURCES.txt +4 -0
- {conformly-0.7.0 → conformly-0.7.2}/LICENSE +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/setup.cfg +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/_utils.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/cases.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/api/path.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/numeric.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/fields/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/context.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/_utils.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/dictionaries.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/float.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/integer.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/list.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/string.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/url.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/generator/types/uuid.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/core/element.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/planner/model.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/planner/session.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/models.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/types/strategies.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly/py.typed +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.0 → conformly-0.7.2}/src/conformly.egg-info/top_level.txt +0 -0
- {conformly-0.7.0 → 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
|
|
@@ -71,6 +71,7 @@ No factories, no hardcoded fixtures, no drift when schema changes.
|
|
|
71
71
|
- [User Cases](#use-cases)
|
|
72
72
|
- [Nested Models](#nested-models)
|
|
73
73
|
- [Collections](#collections)
|
|
74
|
+
- [Experimental](#experimental)
|
|
74
75
|
- [Development](#development)
|
|
75
76
|
- [Changelog](#changelog)
|
|
76
77
|
- [License](#license)
|
|
@@ -242,8 +243,8 @@ path("profile.name").violate(V.PATTERN_MISMATCH)
|
|
|
242
243
|
- `"first"` - violate the first constrained field (for `case`) or take the first N constrained fields (for `cases`)
|
|
243
244
|
- `"all_violations"` - generate one invalid case per every available violations including constraints, structural and type violations (ignores count)
|
|
244
245
|
- `"overrides"` - default values for fields in current generation
|
|
245
|
-
> If a field is selected for invalidation → override is ignored
|
|
246
|
-
> Otherwise → override is applied
|
|
246
|
+
> Note: If a field is selected for invalidation → override is ignored.
|
|
247
|
+
> Otherwise → override is applied.
|
|
247
248
|
```python
|
|
248
249
|
overrides=[
|
|
249
250
|
path(User, lambda u: u.full_name).set("Amogus"),
|
|
@@ -560,6 +561,24 @@ case(Model)
|
|
|
560
561
|
|
|
561
562
|
This ensures consistent output format (list) while preserving uniqueness guarantees.
|
|
562
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
|
+
|
|
563
582
|
### Dict support
|
|
564
583
|
|
|
565
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.).
|
|
@@ -606,6 +625,31 @@ invalid = case(Inventory, valid=False, strategy=path("products").violate(V.TOO_S
|
|
|
606
625
|
- Uniqueness for non-hashable elements (e.g., dicts) is best-effort (based on structural comparison fallback)
|
|
607
626
|
- Dictionary keys are restricted to `str` and `Enum` types; complex objects as keys are not supported
|
|
608
627
|
|
|
628
|
+
## Experimental
|
|
629
|
+
|
|
630
|
+
> APIs in this section are experimental and may change in future minor releases.
|
|
631
|
+
|
|
632
|
+
### Tracing
|
|
633
|
+
|
|
634
|
+
`Tracer` captures metadata about case generation, making it easier to debug, reproduce, and report generated test cases.
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
from conformly import case, Email
|
|
638
|
+
from conformly.tracer import Tracer
|
|
639
|
+
|
|
640
|
+
tracer = Tracer()
|
|
641
|
+
|
|
642
|
+
payload = case(User, valid=False, tracer=tracer)
|
|
643
|
+
|
|
644
|
+
trace = tracer.build()
|
|
645
|
+
|
|
646
|
+
print(trace.target_path)
|
|
647
|
+
print(trace.generated_value)
|
|
648
|
+
print(trace.seed)
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
|
|
652
|
+
|
|
609
653
|
|
|
610
654
|
## Development
|
|
611
655
|
|
|
@@ -38,6 +38,7 @@ No factories, no hardcoded fixtures, no drift when schema changes.
|
|
|
38
38
|
- [User Cases](#use-cases)
|
|
39
39
|
- [Nested Models](#nested-models)
|
|
40
40
|
- [Collections](#collections)
|
|
41
|
+
- [Experimental](#experimental)
|
|
41
42
|
- [Development](#development)
|
|
42
43
|
- [Changelog](#changelog)
|
|
43
44
|
- [License](#license)
|
|
@@ -209,8 +210,8 @@ path("profile.name").violate(V.PATTERN_MISMATCH)
|
|
|
209
210
|
- `"first"` - violate the first constrained field (for `case`) or take the first N constrained fields (for `cases`)
|
|
210
211
|
- `"all_violations"` - generate one invalid case per every available violations including constraints, structural and type violations (ignores count)
|
|
211
212
|
- `"overrides"` - default values for fields in current generation
|
|
212
|
-
> If a field is selected for invalidation → override is ignored
|
|
213
|
-
> Otherwise → override is applied
|
|
213
|
+
> Note: If a field is selected for invalidation → override is ignored.
|
|
214
|
+
> Otherwise → override is applied.
|
|
214
215
|
```python
|
|
215
216
|
overrides=[
|
|
216
217
|
path(User, lambda u: u.full_name).set("Amogus"),
|
|
@@ -527,6 +528,24 @@ case(Model)
|
|
|
527
528
|
|
|
528
529
|
This ensures consistent output format (list) while preserving uniqueness guarantees.
|
|
529
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
|
+
|
|
530
549
|
### Dict support
|
|
531
550
|
|
|
532
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.).
|
|
@@ -573,6 +592,31 @@ invalid = case(Inventory, valid=False, strategy=path("products").violate(V.TOO_S
|
|
|
573
592
|
- Uniqueness for non-hashable elements (e.g., dicts) is best-effort (based on structural comparison fallback)
|
|
574
593
|
- Dictionary keys are restricted to `str` and `Enum` types; complex objects as keys are not supported
|
|
575
594
|
|
|
595
|
+
## Experimental
|
|
596
|
+
|
|
597
|
+
> APIs in this section are experimental and may change in future minor releases.
|
|
598
|
+
|
|
599
|
+
### Tracing
|
|
600
|
+
|
|
601
|
+
`Tracer` captures metadata about case generation, making it easier to debug, reproduce, and report generated test cases.
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
from conformly import case, Email
|
|
605
|
+
from conformly.tracer import Tracer
|
|
606
|
+
|
|
607
|
+
tracer = Tracer()
|
|
608
|
+
|
|
609
|
+
payload = case(User, valid=False, tracer=tracer)
|
|
610
|
+
|
|
611
|
+
trace = tracer.build()
|
|
612
|
+
|
|
613
|
+
print(trace.target_path)
|
|
614
|
+
print(trace.generated_value)
|
|
615
|
+
print(trace.seed)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The trace includes generation metadata such as the target path, generated value, violation type, random seed, and value source.
|
|
619
|
+
|
|
576
620
|
|
|
577
621
|
## Development
|
|
578
622
|
|
|
@@ -7,6 +7,7 @@ from .path import PathSelector
|
|
|
7
7
|
from conformly._internal.generator.context import create_context
|
|
8
8
|
from conformly._internal.generator.orchestration import generate_invalid, generate_valid
|
|
9
9
|
from conformly._internal.parser import ModelSpec
|
|
10
|
+
from conformly._internal.tracer import Tracer
|
|
10
11
|
from conformly._internal.types import CaseStrategy
|
|
11
12
|
|
|
12
13
|
|
|
@@ -18,6 +19,7 @@ def case(
|
|
|
18
19
|
strategy: CaseStrategy | PathSelector = "first",
|
|
19
20
|
overrides: list[PathSelector] | None = None,
|
|
20
21
|
allow_type_mismatch: bool = False,
|
|
22
|
+
tracer: Tracer | None = None,
|
|
21
23
|
) -> dict[str, Any]:
|
|
22
24
|
"""
|
|
23
25
|
Generate a single example of a model.
|
|
@@ -79,6 +81,12 @@ def case(
|
|
|
79
81
|
_overrides = normalize_overrides(model, overrides)
|
|
80
82
|
|
|
81
83
|
if valid:
|
|
84
|
+
if tracer:
|
|
85
|
+
raise api_error(
|
|
86
|
+
"Tracing is only available for invalid generation",
|
|
87
|
+
code="tracing_for_valid",
|
|
88
|
+
)
|
|
89
|
+
|
|
82
90
|
if allow_type_mismatch:
|
|
83
91
|
raise api_error(
|
|
84
92
|
"Type mismatch is only available for invalid generation",
|
|
@@ -103,6 +111,9 @@ def case(
|
|
|
103
111
|
strategy=strategy,
|
|
104
112
|
)
|
|
105
113
|
|
|
114
|
+
if tracer and isinstance(strategy, PathSelector):
|
|
115
|
+
tracer.set_target_path(strategy.raw_path)
|
|
116
|
+
|
|
106
117
|
task = plan_tasks(
|
|
107
118
|
model,
|
|
108
119
|
ctx=ctx,
|
|
@@ -111,4 +122,4 @@ def case(
|
|
|
111
122
|
count=1,
|
|
112
123
|
allow_type_mismatch=allow_type_mismatch,
|
|
113
124
|
)[0]
|
|
114
|
-
return generate_invalid(ctx, model, task, _overrides)
|
|
125
|
+
return generate_invalid(ctx, model, task, _overrides, tracer)
|
|
@@ -10,7 +10,8 @@ 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
|
+
from conformly._internal.tracer import Tracer, ValueSource
|
|
14
15
|
from conformly._internal.types import UNSET, FieldKind, FieldPath, ViolationType
|
|
15
16
|
|
|
16
17
|
|
|
@@ -30,7 +31,10 @@ def generate_invalid(
|
|
|
30
31
|
model: ResolvedModel,
|
|
31
32
|
task: PlannedTask,
|
|
32
33
|
overrides: dict[FieldPath, Any] | None = None,
|
|
34
|
+
tracer: Tracer | None = None,
|
|
33
35
|
) -> dict[str, Any]:
|
|
36
|
+
if tracer:
|
|
37
|
+
tracer.set_seed(ctx.seed)
|
|
34
38
|
return _built_dict_with_violations(
|
|
35
39
|
ctx=ctx,
|
|
36
40
|
model=model,
|
|
@@ -38,6 +42,7 @@ def generate_invalid(
|
|
|
38
42
|
depth=0,
|
|
39
43
|
violations=task.allowed_violations,
|
|
40
44
|
overrides=overrides,
|
|
45
|
+
tracer=tracer,
|
|
41
46
|
)
|
|
42
47
|
|
|
43
48
|
|
|
@@ -48,6 +53,7 @@ def _built_dict_with_violations(
|
|
|
48
53
|
depth: int,
|
|
49
54
|
violations: tuple[ViolationType, ...],
|
|
50
55
|
overrides: dict[FieldPath, Any] | None = None,
|
|
56
|
+
tracer: Tracer | None = None,
|
|
51
57
|
) -> dict[str, Any]:
|
|
52
58
|
result: dict[str, Any] = {}
|
|
53
59
|
target_index = target_path[depth]
|
|
@@ -66,7 +72,7 @@ def _built_dict_with_violations(
|
|
|
66
72
|
|
|
67
73
|
for i in range(0, target_index):
|
|
68
74
|
field = fields[i]
|
|
69
|
-
result[field.name] = generate_field(ctx, field, None, overrides)
|
|
75
|
+
result[field.name] = generate_field(ctx, field, None, overrides, tracer)
|
|
70
76
|
|
|
71
77
|
if target_index < total_fields:
|
|
72
78
|
field = fields[target_index]
|
|
@@ -79,7 +85,7 @@ def _built_dict_with_violations(
|
|
|
79
85
|
field=field.name,
|
|
80
86
|
)
|
|
81
87
|
|
|
82
|
-
value = generate_field(ctx, field, violations, overrides)
|
|
88
|
+
value = generate_field(ctx, field, violations, overrides, tracer)
|
|
83
89
|
if value is not UNSET:
|
|
84
90
|
result[field.name] = value
|
|
85
91
|
|
|
@@ -98,18 +104,19 @@ def _built_dict_with_violations(
|
|
|
98
104
|
depth=depth + 1,
|
|
99
105
|
violations=violations,
|
|
100
106
|
overrides=overrides,
|
|
107
|
+
tracer=tracer,
|
|
101
108
|
)
|
|
102
109
|
|
|
103
110
|
else:
|
|
104
111
|
for i, field in enumerate(fields):
|
|
105
|
-
result[field.name] = generate_field(ctx, field)
|
|
112
|
+
result[field.name] = generate_field(ctx, field, tracer=tracer)
|
|
106
113
|
|
|
107
114
|
key, value = _generate_extra_field_value(ctx)
|
|
108
115
|
result[key] = value
|
|
109
116
|
|
|
110
117
|
for i in range(target_index + 1, total_fields):
|
|
111
118
|
field = fields[i]
|
|
112
|
-
result[field.name] = generate_field(ctx, field)
|
|
119
|
+
result[field.name] = generate_field(ctx, field, tracer=tracer)
|
|
113
120
|
|
|
114
121
|
return result
|
|
115
122
|
|
|
@@ -119,38 +126,69 @@ def generate_field(
|
|
|
119
126
|
field: ResolvedField,
|
|
120
127
|
violations: tuple[ViolationType, ...] | None = None,
|
|
121
128
|
overrides: dict[FieldPath, Any] | None = None,
|
|
129
|
+
tracer: Tracer | None = None,
|
|
122
130
|
) -> Any:
|
|
123
131
|
if violations is None:
|
|
124
132
|
if overrides and field.path in overrides:
|
|
125
|
-
|
|
133
|
+
value = overrides[field.path]
|
|
134
|
+
if tracer:
|
|
135
|
+
tracer.set_value_source(ValueSource.OVERRIDDEN)
|
|
136
|
+
tracer.set_generated_value(value)
|
|
137
|
+
return value
|
|
126
138
|
|
|
127
139
|
if field.nullable:
|
|
128
140
|
return None
|
|
129
141
|
|
|
130
142
|
if field.default is not UNSET:
|
|
143
|
+
if tracer:
|
|
144
|
+
tracer.set_value_source(ValueSource.MODEL_DEFAULT)
|
|
145
|
+
|
|
131
146
|
default = field.default
|
|
132
147
|
if callable(default):
|
|
148
|
+
if tracer:
|
|
149
|
+
tracer.set_generated_value(default())
|
|
133
150
|
return default()
|
|
134
151
|
|
|
135
|
-
|
|
152
|
+
if tracer:
|
|
153
|
+
tracer.set_generated_value(default())
|
|
154
|
+
|
|
155
|
+
return default
|
|
136
156
|
|
|
137
|
-
if field.nested_model and not isinstance(
|
|
157
|
+
if field.nested_model and not isinstance(
|
|
158
|
+
field.semantic, (ListSemantic, TupleSemantic)
|
|
159
|
+
):
|
|
138
160
|
return generate_valid(ctx, field.nested_model, overrides)
|
|
139
161
|
|
|
140
162
|
violation = _choose_violation(violations)
|
|
141
163
|
|
|
164
|
+
if tracer:
|
|
165
|
+
tracer.set_violation(violation)
|
|
166
|
+
tracer.set_value_source(ValueSource.GENERATED)
|
|
167
|
+
|
|
142
168
|
if violation == ViolationType.TYPE_MISMATCH:
|
|
143
169
|
mismatch_kind = choose_mismatch_kind(field.semantic.kind)
|
|
144
170
|
mismatch_semantic = create_minimal_semantic(mismatch_kind)
|
|
145
|
-
|
|
171
|
+
value = get_generator(mismatch_kind).generate_value(
|
|
172
|
+
ctx, mismatch_semantic, None
|
|
173
|
+
)
|
|
174
|
+
if tracer:
|
|
175
|
+
tracer.set_generated_value(value)
|
|
176
|
+
return value
|
|
146
177
|
|
|
147
178
|
if violation == ViolationType.MISSING_FIELD:
|
|
179
|
+
if tracer:
|
|
180
|
+
tracer.set_generated_value(UNSET)
|
|
148
181
|
return UNSET
|
|
149
182
|
|
|
150
|
-
|
|
183
|
+
value = get_generator(field.semantic.kind).generate_value(
|
|
151
184
|
ctx, field.semantic, violation
|
|
152
185
|
)
|
|
153
186
|
|
|
187
|
+
if tracer:
|
|
188
|
+
tracer.set_generated_value(value)
|
|
189
|
+
|
|
190
|
+
return value
|
|
191
|
+
|
|
154
192
|
|
|
155
193
|
def _choose_violation(
|
|
156
194
|
violations: tuple[ViolationType, ...] | None,
|
|
@@ -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
|