conformly 0.7.4__tar.gz → 0.9.0__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 (112) hide show
  1. conformly-0.9.0/PKG-INFO +113 -0
  2. conformly-0.9.0/README.md +80 -0
  3. {conformly-0.7.4 → conformly-0.9.0}/pyproject.toml +1 -1
  4. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/__init__.py +3 -1
  5. conformly-0.9.0/src/conformly/_internal/api/_utils.py +162 -0
  6. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/case.py +21 -26
  7. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/cases.py +37 -57
  8. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/path.py +5 -26
  9. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/numeric.py +6 -4
  10. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/fields/__init__.py +3 -1
  11. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/orchestration.py +25 -8
  12. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/registry.py +12 -0
  13. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/_utils.py +6 -2
  14. conformly-0.9.0/src/conformly/_internal/generator/types/bytes.py +25 -0
  15. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/dictionaries.py +21 -9
  16. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/list.py +42 -13
  17. conformly-0.9.0/src/conformly/_internal/generator/types/temporal.py +59 -0
  18. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/tuple.py +17 -6
  19. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/extractors/constraints.py +6 -0
  20. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/field.py +16 -0
  21. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/session.py +25 -28
  22. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/resolve.py +25 -2
  23. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/__init__.py +6 -0
  24. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_domains.py +6 -0
  25. conformly-0.9.0/src/conformly/_internal/resolver/semantics/bytes.py +48 -0
  26. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/factory.py +6 -0
  27. conformly-0.9.0/src/conformly/_internal/resolver/semantics/temporal.py +139 -0
  28. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/primitives.py +5 -0
  29. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/strategies.py +2 -2
  30. conformly-0.9.0/src/conformly.egg-info/PKG-INFO +113 -0
  31. {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/SOURCES.txt +4 -0
  32. {conformly-0.7.4 → conformly-0.9.0}/tests/test_path.py +28 -31
  33. conformly-0.7.4/PKG-INFO +0 -792
  34. conformly-0.7.4/README.md +0 -759
  35. conformly-0.7.4/src/conformly/_internal/api/_utils.py +0 -87
  36. conformly-0.7.4/src/conformly.egg-info/PKG-INFO +0 -792
  37. {conformly-0.7.4 → conformly-0.9.0}/LICENSE +0 -0
  38. {conformly-0.7.4 → conformly-0.9.0}/setup.cfg +0 -0
  39. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/__init__.py +0 -0
  40. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/__init__.py +0 -0
  41. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/_errors.py +0 -0
  42. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/api/_path_proxy.py +0 -0
  43. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/__init__.py +0 -0
  44. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/base.py +0 -0
  45. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/collections.py +0 -0
  46. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/enum.py +0 -0
  47. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/keys.py +0 -0
  48. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/mapping.py +0 -0
  49. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/constraints/string.py +0 -0
  50. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/fields/registry.py +0 -0
  51. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/fields/special_types.py +0 -0
  52. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/__init__.py +0 -0
  53. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/_errors.py +0 -0
  54. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/context.py +0 -0
  55. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/protocol.py +0 -0
  56. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/__init__.py +0 -0
  57. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/boolean.py +0 -0
  58. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/email.py +0 -0
  59. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/enum.py +0 -0
  60. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/float.py +0 -0
  61. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/integer.py +0 -0
  62. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/ipv4.py +0 -0
  63. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/ipv6.py +0 -0
  64. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/ipvany.py +0 -0
  65. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/string.py +0 -0
  66. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/url.py +0 -0
  67. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/generator/types/uuid.py +0 -0
  68. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/__init__.py +0 -0
  69. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
  70. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/attrs.py +0 -0
  71. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
  72. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
  73. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
  74. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/pydantic.py +0 -0
  75. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/registry.py +0 -0
  76. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/adapters/typeddict.py +0 -0
  77. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/api.py +0 -0
  78. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/core/__init__.py +0 -0
  79. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/core/element.py +0 -0
  80. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/core/field.py +0 -0
  81. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
  82. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/extractors/types.py +0 -0
  83. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/parser/models.py +0 -0
  84. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/__init__.py +0 -0
  85. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/_errors.py +0 -0
  86. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/planner/model.py +0 -0
  87. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/__init__.py +0 -0
  88. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/models.py +0 -0
  89. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_numeric.py +0 -0
  90. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_regex.py +0 -0
  91. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
  92. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/base.py +0 -0
  93. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
  94. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
  95. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
  96. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/list.py +0 -0
  97. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
  98. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/object.py +0 -0
  99. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/string.py +0 -0
  100. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
  101. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
  102. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/tracer.py +0 -0
  103. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/__init__.py +0 -0
  104. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/constants.py +0 -0
  105. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/_internal/types/values.py +0 -0
  106. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/exceptions.py +0 -0
  107. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/py.typed +0 -0
  108. {conformly-0.7.4 → conformly-0.9.0}/src/conformly/tracer/__init__.py +0 -0
  109. {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/dependency_links.txt +0 -0
  110. {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/requires.txt +0 -0
  111. {conformly-0.7.4 → conformly-0.9.0}/src/conformly.egg-info/top_level.txt +0 -0
  112. {conformly-0.7.4 → conformly-0.9.0}/tests/test_collection.py +0 -0
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: conformly
3
+ Version: 0.9.0
4
+ Summary: Generate valid & invalid test data from your typed schemas
5
+ Author-email: Nikita Shabanov <nik.shabanov2024@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nashabanov/conformly
8
+ Project-URL: Repository, https://github.com/nashabanov/conformly.git
9
+ Project-URL: Issues, https://github.com/nashabanov/conformly/issues
10
+ Keywords: testing,fixtures,test-data,dataclasses,validation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: rstr>=3.2.2
23
+ Provides-Extra: pydantic
24
+ Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "pydantic"
25
+ Requires-Dist: email-validator>=2.0.0; extra == "pydantic"
26
+ Provides-Extra: attrs
27
+ Requires-Dist: attrs>=23.0.0; extra == "attrs"
28
+ Provides-Extra: all
29
+ Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "all"
30
+ Requires-Dist: email-validator>=2.0.0; extra == "all"
31
+ Requires-Dist: attrs>=23.0.0; extra == "all"
32
+ Dynamic: license-file
33
+
34
+ # conformly
35
+
36
+ [![CI](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml/badge.svg)](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml)
37
+ [![codecov](https://codecov.io/github/nashabanov/conformly/graph/badge.svg?branch=main)](https://codecov.io/github/nashabanov/conformly)
38
+ [![PyPI](https://img.shields.io/pypi/v/conformly.svg)](https://pypi.org/project/conformly/)
39
+ [![Python Versions](https://img.shields.io/pypi/pyversions/conformly.svg)](https://pypi.org/project/conformly/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
+
42
+ **Declarative test data generator for Python.**
43
+
44
+ Define constraints once in type annotations — generate both valid and minimal invalid test data automatically.
45
+ No factories, no hardcoded fixtures, no drift when schema changes.
46
+
47
+ Conformly turns typed schemas into dictionaries for fixtures and validation tests.
48
+ Use the same schema for happy-path payloads and targeted negative cases.
49
+
50
+ ## Key capabilities
51
+
52
+ - **Constraint-driven generation** from type annotations and model metadata.
53
+ - **Minimal invalid cases** targeting one field and a selected violation.
54
+ - **Schema as the single source of truth** for test data.
55
+ - Dataclasses and TypedDicts, with optional Pydantic and attrs integrations.
56
+ - Nested models, collections, reproducible seeds, and field overrides.
57
+
58
+ ## Installation
59
+
60
+ Requires Python 3.12 or newer.
61
+
62
+ ```bash
63
+ pip install conformly
64
+ # Optional integrations:
65
+ pip install 'conformly[pydantic]'
66
+ pip install 'conformly[attrs]'
67
+ ```
68
+
69
+ ## Quickstart
70
+
71
+ ```python
72
+ from dataclasses import dataclass
73
+ from typing import Annotated
74
+
75
+ from conformly import GreaterOrEqual, MaxLength, MinLength, V, case, path
76
+
77
+
78
+ @dataclass
79
+ class CreateUser:
80
+ username: Annotated[str, MinLength(3), MaxLength(32)]
81
+ age: Annotated[int, GreaterOrEqual(18)]
82
+
83
+
84
+ payload = case(CreateUser)
85
+ assert 3 <= len(payload["username"]) <= 32
86
+ assert payload["age"] >= 18
87
+
88
+ invalid = case(
89
+ CreateUser, valid=False, target=path("age").violate(V.BELOW_MIN)
90
+ )
91
+ assert invalid["age"] < 18
92
+ assert 3 <= len(invalid["username"]) <= 32
93
+ ```
94
+
95
+ Results are dictionaries, not model instances. Invalid generation targets one
96
+ field; only the selected violation is guaranteed on that target. Defaults and
97
+ overrides are trusted rather than validated. See the
98
+ [invalid generation contract](https://github.com/nashabanov/conformly/blob/main/docs/strategies.md#invalid-generation-contract).
99
+
100
+ ## Documentation and examples
101
+
102
+ Start with [Getting started](https://github.com/nashabanov/conformly/blob/main/docs/getting-started.md), browse the
103
+ [documentation index](https://github.com/nashabanov/conformly/blob/main/docs/README.md), or run the [examples](https://github.com/nashabanov/conformly/blob/main/examples/README.md).
104
+ The [API reference](https://github.com/nashabanov/conformly/blob/main/docs/api-reference.md) lists parameters and defaults.
105
+
106
+ ## Development and contributing
107
+
108
+ See [CONTRIBUTING.md](https://github.com/nashabanov/conformly/blob/main/CONTRIBUTING.md) for setup and checks, and
109
+ [CHANGELOG.md](https://github.com/nashabanov/conformly/blob/main/CHANGELOG.md) for release notes and migration guidance.
110
+
111
+ ## License
112
+
113
+ MIT — see [LICENSE](https://github.com/nashabanov/conformly/blob/main/LICENSE).
@@ -0,0 +1,80 @@
1
+ # conformly
2
+
3
+ [![CI](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml/badge.svg)](https://github.com/nashabanov/conformly/actions/workflows/ci.yaml)
4
+ [![codecov](https://codecov.io/github/nashabanov/conformly/graph/badge.svg?branch=main)](https://codecov.io/github/nashabanov/conformly)
5
+ [![PyPI](https://img.shields.io/pypi/v/conformly.svg)](https://pypi.org/project/conformly/)
6
+ [![Python Versions](https://img.shields.io/pypi/pyversions/conformly.svg)](https://pypi.org/project/conformly/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ **Declarative test data generator for Python.**
10
+
11
+ Define constraints once in type annotations — generate both valid and minimal invalid test data automatically.
12
+ No factories, no hardcoded fixtures, no drift when schema changes.
13
+
14
+ Conformly turns typed schemas into dictionaries for fixtures and validation tests.
15
+ Use the same schema for happy-path payloads and targeted negative cases.
16
+
17
+ ## Key capabilities
18
+
19
+ - **Constraint-driven generation** from type annotations and model metadata.
20
+ - **Minimal invalid cases** targeting one field and a selected violation.
21
+ - **Schema as the single source of truth** for test data.
22
+ - Dataclasses and TypedDicts, with optional Pydantic and attrs integrations.
23
+ - Nested models, collections, reproducible seeds, and field overrides.
24
+
25
+ ## Installation
26
+
27
+ Requires Python 3.12 or newer.
28
+
29
+ ```bash
30
+ pip install conformly
31
+ # Optional integrations:
32
+ pip install 'conformly[pydantic]'
33
+ pip install 'conformly[attrs]'
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from dataclasses import dataclass
40
+ from typing import Annotated
41
+
42
+ from conformly import GreaterOrEqual, MaxLength, MinLength, V, case, path
43
+
44
+
45
+ @dataclass
46
+ class CreateUser:
47
+ username: Annotated[str, MinLength(3), MaxLength(32)]
48
+ age: Annotated[int, GreaterOrEqual(18)]
49
+
50
+
51
+ payload = case(CreateUser)
52
+ assert 3 <= len(payload["username"]) <= 32
53
+ assert payload["age"] >= 18
54
+
55
+ invalid = case(
56
+ CreateUser, valid=False, target=path("age").violate(V.BELOW_MIN)
57
+ )
58
+ assert invalid["age"] < 18
59
+ assert 3 <= len(invalid["username"]) <= 32
60
+ ```
61
+
62
+ Results are dictionaries, not model instances. Invalid generation targets one
63
+ field; only the selected violation is guaranteed on that target. Defaults and
64
+ overrides are trusted rather than validated. See the
65
+ [invalid generation contract](https://github.com/nashabanov/conformly/blob/main/docs/strategies.md#invalid-generation-contract).
66
+
67
+ ## Documentation and examples
68
+
69
+ Start with [Getting started](https://github.com/nashabanov/conformly/blob/main/docs/getting-started.md), browse the
70
+ [documentation index](https://github.com/nashabanov/conformly/blob/main/docs/README.md), or run the [examples](https://github.com/nashabanov/conformly/blob/main/examples/README.md).
71
+ The [API reference](https://github.com/nashabanov/conformly/blob/main/docs/api-reference.md) lists parameters and defaults.
72
+
73
+ ## Development and contributing
74
+
75
+ See [CONTRIBUTING.md](https://github.com/nashabanov/conformly/blob/main/CONTRIBUTING.md) for setup and checks, and
76
+ [CHANGELOG.md](https://github.com/nashabanov/conformly/blob/main/CHANGELOG.md) for release notes and migration guidance.
77
+
78
+ ## License
79
+
80
+ MIT — see [LICENSE](https://github.com/nashabanov/conformly/blob/main/LICENSE).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "conformly"
3
- version = "0.7.4"
3
+ version = "0.9.0"
4
4
  description = "Generate valid & invalid test data from your typed schemas"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -18,7 +18,7 @@ from ._internal.constraints import (
18
18
  Pattern,
19
19
  UniqueItems,
20
20
  )
21
- from ._internal.fields import Email, IPv4, IPv6, IPvAny
21
+ from ._internal.fields import Email, HttpUrl, IPv4, IPv6, IPvAny, Url
22
22
  from ._internal.types import ViolationType as V
23
23
  from .exceptions import ConformlyError
24
24
 
@@ -27,6 +27,7 @@ __all__ = [
27
27
  "Email",
28
28
  "GreaterOrEqual",
29
29
  "GreaterThan",
30
+ "HttpUrl",
30
31
  "IPv4",
31
32
  "IPv6",
32
33
  "IPvAny",
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "OneOf",
41
42
  "Pattern",
42
43
  "UniqueItems",
44
+ "Url",
43
45
  "V",
44
46
  "case",
45
47
  "cases",
@@ -0,0 +1,162 @@
1
+ from typing import Any
2
+
3
+ from ._errors import api_error
4
+ from .path import PathSelector
5
+
6
+ from conformly._internal.generator import GenerationContext
7
+ from conformly._internal.parser import ModelSpec, parse_model
8
+ from conformly._internal.planner import PlannedTask, plan_violation_task, select_paths
9
+ from conformly._internal.resolver import ResolvedModel, resolve_model
10
+ from conformly._internal.types import CasesStrategy, FieldPath
11
+
12
+
13
+ def ensure_model_or_spec(
14
+ model_or_spec: ModelSpec | ResolvedModel | type,
15
+ ) -> ResolvedModel:
16
+ if isinstance(model_or_spec, ResolvedModel):
17
+ return model_or_spec
18
+
19
+ if isinstance(model_or_spec, ModelSpec):
20
+ return resolve_model(model_or_spec)
21
+
22
+ return resolve_model(parse_model(model_or_spec))
23
+
24
+
25
+ def normalize_overrides(
26
+ model: ResolvedModel, overrides: list[PathSelector] | None
27
+ ) -> dict[FieldPath, Any] | None:
28
+ if overrides is None:
29
+ return None
30
+
31
+ normalized_overrides: dict[FieldPath, Any] = {}
32
+
33
+ for v in overrides:
34
+ if (
35
+ not isinstance(v, PathSelector)
36
+ or not v.has_override
37
+ or v.forced_violation is not None
38
+ ):
39
+ raise api_error(
40
+ "Overrides must be selectors configured with .set(value)",
41
+ code="invalid_override_selector",
42
+ path=v.raw_path if isinstance(v, PathSelector) else None,
43
+ )
44
+ if v.raw_path not in model.name_to_path:
45
+ raise api_error(
46
+ "Override path not found",
47
+ code="invalid_override_path",
48
+ path=v.raw_path,
49
+ available=list(model.name_to_path),
50
+ )
51
+ path = model.name_to_path[v.raw_path]
52
+ normalized_overrides[path] = v.override
53
+
54
+ return normalized_overrides
55
+
56
+
57
+ def validate_selection(
58
+ *,
59
+ valid: bool,
60
+ strategy: CasesStrategy,
61
+ target: PathSelector | None,
62
+ allow_all: bool,
63
+ count: int = 1,
64
+ ) -> None:
65
+ if valid and strategy != "first":
66
+ raise api_error(
67
+ "Strategy selection is only applicable when valid=False",
68
+ code="invalid_strategy_for_valid",
69
+ strategy=strategy,
70
+ )
71
+ supported = (
72
+ ("first", "random", "all", "all_violations")
73
+ if allow_all
74
+ else ("first", "random")
75
+ )
76
+ if not isinstance(strategy, str) or strategy not in supported:
77
+ raise api_error(
78
+ "Unsupported strategy",
79
+ code="unsupported_strategy",
80
+ function="cases" if allow_all else "case",
81
+ strategy=strategy,
82
+ available=list(supported),
83
+ )
84
+ if target is not None:
85
+ if not isinstance(target, PathSelector):
86
+ raise api_error(
87
+ "Target must be a PathSelector; use path('field')",
88
+ code="invalid_target",
89
+ )
90
+ if valid:
91
+ raise api_error(
92
+ "Target selection is only applicable when valid=False",
93
+ code="invalid_target_for_valid",
94
+ target=target.raw_path,
95
+ )
96
+ if strategy != "first":
97
+ raise api_error(
98
+ "Explicit targets require strategy='first'",
99
+ code="invalid_strategy_for_target",
100
+ strategy=strategy,
101
+ target=target.raw_path,
102
+ )
103
+ if count != 1:
104
+ raise api_error(
105
+ "Explicit targets require count=1",
106
+ code="invalid_count_for_target",
107
+ count=count,
108
+ target=target.raw_path,
109
+ )
110
+
111
+
112
+ def plan_tasks(
113
+ model: ResolvedModel,
114
+ *,
115
+ ctx: GenerationContext,
116
+ strategy: CasesStrategy,
117
+ target: PathSelector | None = None,
118
+ allow_all: bool,
119
+ count: int = 1,
120
+ allow_type_mismatch: bool = False,
121
+ allow_structural_violations: bool = False,
122
+ split_by_violations: bool = False,
123
+ ) -> list[PlannedTask]:
124
+ forced_violation = target.forced_violation if target is not None else None
125
+
126
+ paths = select_paths(
127
+ model,
128
+ ctx=ctx,
129
+ strategy=strategy,
130
+ target=target.raw_path if target is not None else None,
131
+ forced_violation=forced_violation,
132
+ allow_all=allow_all,
133
+ count=count,
134
+ allow_type_mismatch=allow_type_mismatch,
135
+ allow_structural_violations=allow_structural_violations,
136
+ )
137
+
138
+ if split_by_violations:
139
+ return [
140
+ plan_violation_task(
141
+ model,
142
+ path,
143
+ allow_type_mismatch,
144
+ allow_structural_violations,
145
+ forced_violation=violation,
146
+ )
147
+ for path in paths
148
+ for violation in plan_violation_task(
149
+ model, path, allow_type_mismatch, allow_structural_violations
150
+ ).allowed_violations
151
+ ]
152
+
153
+ return [
154
+ plan_violation_task(
155
+ model=model,
156
+ path=path,
157
+ allow_type_mismatch=allow_type_mismatch,
158
+ allow_structural_violations=allow_structural_violations,
159
+ forced_violation=forced_violation,
160
+ )
161
+ for path in paths
162
+ ]
@@ -1,7 +1,12 @@
1
1
  from typing import Any
2
2
 
3
3
  from ._errors import api_error
4
- from ._utils import ensure_model_or_spec, normalize_overrides, plan_tasks
4
+ from ._utils import (
5
+ ensure_model_or_spec,
6
+ normalize_overrides,
7
+ plan_tasks,
8
+ validate_selection,
9
+ )
5
10
  from .path import PathSelector
6
11
 
7
12
  from conformly._internal.generator.context import create_context
@@ -16,7 +21,8 @@ def case(
16
21
  *,
17
22
  valid: bool = True,
18
23
  seed: int | None = None,
19
- strategy: CaseStrategy | PathSelector = "first",
24
+ strategy: CaseStrategy = "first",
25
+ target: PathSelector | None = None,
20
26
  overrides: list[PathSelector] | None = None,
21
27
  allow_type_mismatch: bool = False,
22
28
  tracer: Tracer | None = None,
@@ -52,16 +58,10 @@ def case(
52
58
 
53
59
  - "random": violate a random constrained field
54
60
 
55
- - "field_name":
56
- violate a specific field using a dotted path
57
- (e.g. strategy="user.email")
58
-
59
- - "field_name::violation":
60
- violate a specific field with specific type
61
- (e.g. strategy="user.profile::below_min")
62
-
63
- - PathSelector DSL:
64
- path("user.email").violate(V.TOO_SHORT)
61
+ target:
62
+ Optional selector from path("field") or path(Model, lambda m: m.field).
63
+ Only available with valid=False and strategy="first".
64
+ Use .violate(V.X) to request an exact violation.
65
65
 
66
66
  overrides:
67
67
  Optional list of `PathSelector` to set field values.
@@ -99,27 +99,22 @@ def case(
99
99
  flag="allow_type_mismatch",
100
100
  )
101
101
 
102
- if strategy != "first":
103
- raise api_error(
104
- "Strategy selection is only applicable when valid=False",
105
- code="invalid_strategy_for_valid",
106
- strategy=strategy,
107
- )
102
+ validate_selection(
103
+ valid=valid,
104
+ strategy=strategy,
105
+ target=target,
106
+ allow_all=False,
107
+ count=1,
108
+ )
108
109
 
110
+ if valid:
109
111
  return generate_valid(ctx, model, _overrides)
110
112
 
111
- if strategy == "all":
112
- raise api_error(
113
- "The 'all' strategy is not supported in 'case()' — use 'cases()' instead",
114
- code="unsupported_strategy",
115
- function="case",
116
- strategy=strategy,
117
- )
118
-
119
113
  task = plan_tasks(
120
114
  model,
121
115
  ctx=ctx,
122
116
  strategy=strategy,
117
+ target=target,
123
118
  allow_all=False,
124
119
  count=1,
125
120
  allow_type_mismatch=allow_type_mismatch,
@@ -1,8 +1,13 @@
1
1
  from typing import Any
2
2
 
3
3
  from ._errors import api_error
4
- from ._utils import ensure_model_or_spec, normalize_overrides, plan_tasks
5
- from .path import PathSelector, parse_strategy_input
4
+ from ._utils import (
5
+ ensure_model_or_spec,
6
+ normalize_overrides,
7
+ plan_tasks,
8
+ validate_selection,
9
+ )
10
+ from .path import PathSelector
6
11
 
7
12
  from conformly._internal.generator.context import create_context
8
13
  from conformly._internal.generator.orchestration import generate_invalid, generate_valid
@@ -15,7 +20,8 @@ def cases(
15
20
  *,
16
21
  valid: bool = True,
17
22
  seed: int | None = None,
18
- strategy: CasesStrategy | PathSelector = "first",
23
+ strategy: CasesStrategy = "first",
24
+ target: PathSelector | None = None,
19
25
  count: int = 1,
20
26
  allow_type_mismatch: bool = False,
21
27
  allow_structural_violations: bool = False,
@@ -60,19 +66,13 @@ def cases(
60
66
  generate one invalid case per every available violations
61
67
  including constraints, structural and type violations (ignores count)
62
68
 
63
- - "field_name":
64
- violate a specific field using a dotted path
65
- (e.g. strategy="user.email")
66
-
67
- - "field_name::violation":
68
- violate a specific field with specific type
69
- (e.g. strategy="user.profile::below_min")
70
-
71
- - PathSelector DSL:
72
- path("user.email").violate(V.TOO_SHORT)
69
+ target:
70
+ Optional selector from path("field") or path(Model, lambda m: m.field).
71
+ Only available with valid=False and strategy="first".
72
+ Use .violate(V.X) to request an exact violation.
73
73
 
74
74
  count:
75
- Number of cases to generate (ignored if strategy="all").
75
+ Number of cases; target requires 1. Enumeration ignores count.
76
76
 
77
77
  overrides:
78
78
  Optional list of `PathSelector` to set field values.
@@ -89,7 +89,7 @@ def cases(
89
89
  violations and cases with extra field in each model.
90
90
  Structural violations are available only when:
91
91
  - valid=False
92
- - strategy="all"
92
+ - strategy="all" or "all_violations", or an explicit violation
93
93
 
94
94
  Returns:
95
95
  A list of dictionaries.
@@ -122,59 +122,39 @@ def cases(
122
122
  flag="allow_structural_violations",
123
123
  )
124
124
 
125
- if strategy != "first":
126
- raise api_error(
127
- "Strategy selection is only applicable when valid=False",
128
- code="invalid_strategy_for_valid",
129
- strategy=strategy,
130
- )
125
+ validate_selection(
126
+ valid=valid,
127
+ strategy=strategy,
128
+ target=target,
129
+ allow_all=True,
130
+ count=count,
131
+ )
131
132
 
133
+ if valid:
132
134
  return [generate_valid(ctx, model, _overrides) for _ in range(count)]
133
135
 
134
- field_strategy, forced_violation = parse_strategy_input(strategy)
135
-
136
136
  if (
137
137
  allow_structural_violations
138
- and field_strategy not in ("all", "all_violations")
139
- and forced_violation is None
138
+ and strategy not in ("all", "all_violations")
139
+ and (target is None or target.forced_violation is None)
140
140
  ):
141
141
  raise api_error(
142
142
  "Structural violations require strategy='all', 'all_violations', "
143
- "or explicit '::' syntax",
143
+ "or an explicit violation on target",
144
144
  code="invalid_strategy_for_structural",
145
145
  strategy=strategy,
146
146
  )
147
147
 
148
- if strategy == "all":
149
- tasks = plan_tasks(
150
- model,
151
- ctx=ctx,
152
- strategy="all",
153
- allow_all=True,
154
- allow_type_mismatch=allow_type_mismatch,
155
- allow_structural_violations=allow_structural_violations,
156
- split_by_violations=False,
157
- )
158
-
159
- elif strategy == "all_violations":
160
- tasks = plan_tasks(
161
- model,
162
- ctx=ctx,
163
- strategy="all",
164
- allow_all=True,
165
- allow_type_mismatch=allow_type_mismatch,
166
- allow_structural_violations=allow_structural_violations,
167
- split_by_violations=True,
168
- )
169
-
170
- else:
171
- tasks = plan_tasks(
172
- model,
173
- ctx=ctx,
174
- strategy=strategy,
175
- allow_all=False,
176
- count=count,
177
- allow_type_mismatch=allow_type_mismatch,
178
- )
148
+ tasks = plan_tasks(
149
+ model,
150
+ ctx=ctx,
151
+ strategy="all" if strategy == "all_violations" else strategy,
152
+ target=target,
153
+ allow_all=True,
154
+ count=count,
155
+ allow_type_mismatch=allow_type_mismatch,
156
+ allow_structural_violations=allow_structural_violations,
157
+ split_by_violations=strategy == "all_violations",
158
+ )
179
159
 
180
160
  return [generate_invalid(ctx, model, task, _overrides) for task in tasks]
@@ -13,10 +13,11 @@ class PathSelector:
13
13
  raw_path: str
14
14
  forced_violation: ViolationType | None = None
15
15
  override: Any | None = None
16
+ has_override: bool = False
16
17
 
17
18
  def set(self, override: Any) -> "PathSelector":
18
19
  """Override field value"""
19
- return PathSelector(self.raw_path, override=override)
20
+ return PathSelector(self.raw_path, override=override, has_override=True)
20
21
 
21
22
  def violate(self, violation: ViolationType) -> "PathSelector":
22
23
  """Force violation type"""
@@ -39,7 +40,7 @@ def path[T](
39
40
 
40
41
  Supports two usage modes:
41
42
 
42
- 1. String mode (simple, backward-compatible):
43
+ 1. String mode (simple):
43
44
  path("user.email").violate(V.TOO_SHORT)
44
45
  path("bio").violate(V.TOO_LONG)
45
46
 
@@ -62,8 +63,8 @@ def path[T](
62
63
  Notes:
63
64
  - String paths are not validated immediately. Validation occurs during
64
65
  the planning stage against the resolved model structure.
65
- - Lambda paths require source code availability (.py files). In REPL
66
- or Jupyter, use the string mode.
66
+ - Lambda paths execute against an attribute proxy and work in REPLs
67
+ and notebooks.
67
68
  """
68
69
  if isinstance(target, str):
69
70
  if expr is not None:
@@ -83,25 +84,3 @@ def path[T](
83
84
 
84
85
  raw_path = extract_path_from_proxy(expr)
85
86
  return PathSelector(raw_path)
86
-
87
-
88
- def parse_strategy_input(
89
- strategy: str | PathSelector,
90
- ) -> tuple[str, ViolationType | None]:
91
- if isinstance(strategy, PathSelector):
92
- return strategy.raw_path, strategy.forced_violation
93
-
94
- if "::" in strategy:
95
- field_part, violation_part = strategy.split("::", 1)
96
- try:
97
- v_type = ViolationType(violation_part)
98
- return field_part, v_type
99
- except ValueError:
100
- available = [v.value for v in ViolationType]
101
- raise api_error(
102
- f"Unknown violation type '{violation_part}'",
103
- code="invalid_violation_type",
104
- requested=violation_part,
105
- available=available,
106
- )
107
- return strategy, None