conformly 0.7.3__tar.gz → 0.7.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {conformly-0.7.3/src/conformly.egg-info → conformly-0.7.4}/PKG-INFO +45 -9
- {conformly-0.7.3 → conformly-0.7.4}/README.md +44 -8
- {conformly-0.7.3 → conformly-0.7.4}/pyproject.toml +1 -1
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/_utils.py +7 -1
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/orchestration.py +100 -23
- conformly-0.7.4/src/conformly/_internal/generator/types/_utils.py +84 -0
- conformly-0.7.4/src/conformly/_internal/generator/types/dictionaries.py +137 -0
- conformly-0.7.4/src/conformly/_internal/generator/types/float.py +119 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/integer.py +11 -3
- conformly-0.7.4/src/conformly/_internal/generator/types/list.py +135 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/string.py +42 -30
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/tuple.py +33 -33
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/attrs.py +6 -1
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/pydantic.py +1 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/typeddict.py +9 -3
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/core/element.py +0 -2
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/core/field.py +5 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/models.py +6 -1
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/field.py +114 -38
- conformly-0.7.4/src/conformly/_internal/planner/model.py +18 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/session.py +17 -4
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/models.py +9 -2
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/resolve.py +49 -56
- conformly-0.7.4/src/conformly/_internal/resolver/semantics/_domains.py +29 -0
- conformly-0.7.4/src/conformly/_internal/resolver/semantics/_numeric.py +15 -0
- conformly-0.7.4/src/conformly/_internal/resolver/semantics/_regex.py +9 -0
- {conformly-0.7.3 → conformly-0.7.4/src/conformly.egg-info}/PKG-INFO +45 -9
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/SOURCES.txt +3 -0
- conformly-0.7.3/src/conformly/_internal/generator/types/_utils.py +0 -63
- conformly-0.7.3/src/conformly/_internal/generator/types/dictionaries.py +0 -122
- conformly-0.7.3/src/conformly/_internal/generator/types/float.py +0 -89
- conformly-0.7.3/src/conformly/_internal/generator/types/list.py +0 -112
- conformly-0.7.3/src/conformly/_internal/planner/model.py +0 -9
- {conformly-0.7.3 → conformly-0.7.4}/LICENSE +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/setup.cfg +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/case.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/cases.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/api/path.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/numeric.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/fields/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/context.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/registry.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/url.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/generator/types/uuid.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/types.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/factory.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/tracer.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/primitives.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/strategies.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/py.typed +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly/tracer/__init__.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/src/conformly.egg-info/top_level.txt +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/tests/test_collection.py +0 -0
- {conformly-0.7.3 → conformly-0.7.4}/tests/test_path.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conformly
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Generate valid & invalid test data from your typed schemas
|
|
5
5
|
Author-email: Nikita Shabanov <nik.shabanov2024@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -297,17 +297,43 @@ For `case(Model, valid=False, strategy="<field>")`:
|
|
|
297
297
|
- **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
|
|
298
298
|
- **Exactly one field is targeted** (the one specified by `strategy`).
|
|
299
299
|
- **The generator will violate constraints** for that field, making it invalid.
|
|
300
|
-
- **
|
|
300
|
+
- **Inside collections**, planning selects a compatible dictionary key, value, or
|
|
301
|
+
element before generation. Fixed tuples retain their length and order; unrelated
|
|
302
|
+
elements, keys, values, and model fields remain valid.
|
|
303
|
+
- **Only the selected violation is guaranteed on its target**. Other constraints
|
|
304
|
+
on that same target are not guaranteed to remain satisfied. A `DUPLICATE`
|
|
305
|
+
violation preserves an allowed collection length; collections with a maximum
|
|
306
|
+
size below two cannot be targeted for duplication.
|
|
307
|
+
- **Explicit impossible or unsupported violations raise `PlanningError`**;
|
|
308
|
+
empty-only collections do not offer element violations.
|
|
301
309
|
- **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
|
|
302
310
|
- **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
|
|
303
311
|
|
|
304
|
-
|
|
312
|
+
Request an exact violation with `strategy="items::below_min"` or
|
|
313
|
+
`path(Model, lambda m: m.items).violate(V.BELOW_MIN)`. Otherwise, the planner uses
|
|
314
|
+
violation priority and chooses the first compatible key, value, or tuple element.
|
|
305
315
|
|
|
306
316
|
## Optional Fields and Defaults
|
|
307
317
|
|
|
308
|
-
-
|
|
309
|
-
|
|
310
|
-
|
|
318
|
+
- For untargeted fields, priority is **override → nullable `None` → model default
|
|
319
|
+
or default factory → generation**. Nullable fields currently always return `None`
|
|
320
|
+
without an override, even with a non-`None` default; their factories are not called.
|
|
321
|
+
- Model defaults, factory results and overrides are trusted and returned without
|
|
322
|
+
validation. `valid=True` guarantees the validity of generated values, not of
|
|
323
|
+
caller-supplied data. An invalid supplied value can also make an unrelated field
|
|
324
|
+
invalid when generating a negative case.
|
|
325
|
+
- Invalid targets bypass their defaults, nullable priority and overrides. Other
|
|
326
|
+
fields retain the priority above. Default factories are called once per generated
|
|
327
|
+
payload when selected by this priority; attrs factories requiring `self` are unsupported.
|
|
328
|
+
- TypedDict `NotRequired[T]` controls key presence independently of whether `T`
|
|
329
|
+
accepts `None`. Valid generation currently includes these keys with values of `T`;
|
|
330
|
+
`NotRequired[int]` does not become a present `None`.
|
|
331
|
+
- `MISSING_FIELD` is offered only for required fields: nullable required fields can
|
|
332
|
+
be removed, but fields with defaults and TypedDict `NotRequired` keys cannot.
|
|
333
|
+
`EXTRA_FIELD` is offered for Pydantic only with `extra="forbid"`; `ignore` and
|
|
334
|
+
`allow` accept extra data. Other adapters treat undeclared keys as structural violations.
|
|
335
|
+
- A target needs an applicable semantic, type or structural violation; impossible
|
|
336
|
+
explicit requests raise `PlanningError`.
|
|
311
337
|
|
|
312
338
|
## Constraints
|
|
313
339
|
|
|
@@ -327,6 +353,19 @@ If you need **deterministic control** over which exact constraint to violate, th
|
|
|
327
353
|
| Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
|
|
328
354
|
| Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
|
|
329
355
|
|
|
356
|
+
`Pattern` uses Python regex search semantics, consistent with Pydantic: a match
|
|
357
|
+
anywhere in the string is sufficient. Use anchors (`^...$`) to require a whole-string
|
|
358
|
+
match. Pattern mismatches are verified; unsuccessful inversion raises a structured
|
|
359
|
+
`GenerationError` instead of returning a matching value. Obvious universal patterns
|
|
360
|
+
(such as `(?s).*`) are excluded from automatic planning, and explicit mismatch
|
|
361
|
+
requests raise `PlanningError`.
|
|
362
|
+
|
|
363
|
+
`MultipleOf` requires a finite positive divisor. For floats, a quotient within
|
|
364
|
+
`1e-9` of an integer is treated as a multiple (no relative tolerance). Integer
|
|
365
|
+
`NOT_MULTIPLE` requests for divisors such as `1` or `0.5`, which divide every integer,
|
|
366
|
+
raise `PlanningError` and are excluded from automatic planning. A bounded range
|
|
367
|
+
without a representable multiple raises `GenerationError` with code `no_multiples_of`.
|
|
368
|
+
|
|
330
369
|
> Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
|
|
331
370
|
> Use Field() parameters for constraint extraction.
|
|
332
371
|
|
|
@@ -751,6 +790,3 @@ Contributions welcome!
|
|
|
751
790
|
- Add tests for new functionality
|
|
752
791
|
- Run `uv run -m pytest` and `uv run -m ruff check .`
|
|
753
792
|
- Submit a pull request
|
|
754
|
-
|
|
755
|
-
Test collection and assertion audit findings, including deferred collection checks,
|
|
756
|
-
are documented in [the test audit](docs/test-audit.md).
|
|
@@ -264,17 +264,43 @@ For `case(Model, valid=False, strategy="<field>")`:
|
|
|
264
264
|
- **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
|
|
265
265
|
- **Exactly one field is targeted** (the one specified by `strategy`).
|
|
266
266
|
- **The generator will violate constraints** for that field, making it invalid.
|
|
267
|
-
- **
|
|
267
|
+
- **Inside collections**, planning selects a compatible dictionary key, value, or
|
|
268
|
+
element before generation. Fixed tuples retain their length and order; unrelated
|
|
269
|
+
elements, keys, values, and model fields remain valid.
|
|
270
|
+
- **Only the selected violation is guaranteed on its target**. Other constraints
|
|
271
|
+
on that same target are not guaranteed to remain satisfied. A `DUPLICATE`
|
|
272
|
+
violation preserves an allowed collection length; collections with a maximum
|
|
273
|
+
size below two cannot be targeted for duplication.
|
|
274
|
+
- **Explicit impossible or unsupported violations raise `PlanningError`**;
|
|
275
|
+
empty-only collections do not offer element violations.
|
|
268
276
|
- **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
|
|
269
277
|
- **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
|
|
270
278
|
|
|
271
|
-
|
|
279
|
+
Request an exact violation with `strategy="items::below_min"` or
|
|
280
|
+
`path(Model, lambda m: m.items).violate(V.BELOW_MIN)`. Otherwise, the planner uses
|
|
281
|
+
violation priority and chooses the first compatible key, value, or tuple element.
|
|
272
282
|
|
|
273
283
|
## Optional Fields and Defaults
|
|
274
284
|
|
|
275
|
-
-
|
|
276
|
-
|
|
277
|
-
|
|
285
|
+
- For untargeted fields, priority is **override → nullable `None` → model default
|
|
286
|
+
or default factory → generation**. Nullable fields currently always return `None`
|
|
287
|
+
without an override, even with a non-`None` default; their factories are not called.
|
|
288
|
+
- Model defaults, factory results and overrides are trusted and returned without
|
|
289
|
+
validation. `valid=True` guarantees the validity of generated values, not of
|
|
290
|
+
caller-supplied data. An invalid supplied value can also make an unrelated field
|
|
291
|
+
invalid when generating a negative case.
|
|
292
|
+
- Invalid targets bypass their defaults, nullable priority and overrides. Other
|
|
293
|
+
fields retain the priority above. Default factories are called once per generated
|
|
294
|
+
payload when selected by this priority; attrs factories requiring `self` are unsupported.
|
|
295
|
+
- TypedDict `NotRequired[T]` controls key presence independently of whether `T`
|
|
296
|
+
accepts `None`. Valid generation currently includes these keys with values of `T`;
|
|
297
|
+
`NotRequired[int]` does not become a present `None`.
|
|
298
|
+
- `MISSING_FIELD` is offered only for required fields: nullable required fields can
|
|
299
|
+
be removed, but fields with defaults and TypedDict `NotRequired` keys cannot.
|
|
300
|
+
`EXTRA_FIELD` is offered for Pydantic only with `extra="forbid"`; `ignore` and
|
|
301
|
+
`allow` accept extra data. Other adapters treat undeclared keys as structural violations.
|
|
302
|
+
- A target needs an applicable semantic, type or structural violation; impossible
|
|
303
|
+
explicit requests raise `PlanningError`.
|
|
278
304
|
|
|
279
305
|
## Constraints
|
|
280
306
|
|
|
@@ -294,6 +320,19 @@ If you need **deterministic control** over which exact constraint to violate, th
|
|
|
294
320
|
| Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
|
|
295
321
|
| Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
|
|
296
322
|
|
|
323
|
+
`Pattern` uses Python regex search semantics, consistent with Pydantic: a match
|
|
324
|
+
anywhere in the string is sufficient. Use anchors (`^...$`) to require a whole-string
|
|
325
|
+
match. Pattern mismatches are verified; unsuccessful inversion raises a structured
|
|
326
|
+
`GenerationError` instead of returning a matching value. Obvious universal patterns
|
|
327
|
+
(such as `(?s).*`) are excluded from automatic planning, and explicit mismatch
|
|
328
|
+
requests raise `PlanningError`.
|
|
329
|
+
|
|
330
|
+
`MultipleOf` requires a finite positive divisor. For floats, a quotient within
|
|
331
|
+
`1e-9` of an integer is treated as a multiple (no relative tolerance). Integer
|
|
332
|
+
`NOT_MULTIPLE` requests for divisors such as `1` or `0.5`, which divide every integer,
|
|
333
|
+
raise `PlanningError` and are excluded from automatic planning. A bounded range
|
|
334
|
+
without a representable multiple raises `GenerationError` with code `no_multiples_of`.
|
|
335
|
+
|
|
297
336
|
> Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
|
|
298
337
|
> Use Field() parameters for constraint extraction.
|
|
299
338
|
|
|
@@ -718,6 +757,3 @@ Contributions welcome!
|
|
|
718
757
|
- Add tests for new functionality
|
|
719
758
|
- Run `uv run -m pytest` and `uv run -m ruff check .`
|
|
720
759
|
- Submit a pull request
|
|
721
|
-
|
|
722
|
-
Test collection and assertion audit findings, including deferred collection checks,
|
|
723
|
-
are documented in [the test audit](docs/test-audit.md).
|
|
@@ -62,7 +62,13 @@ def plan_tasks(
|
|
|
62
62
|
|
|
63
63
|
if split_by_violations:
|
|
64
64
|
return [
|
|
65
|
-
|
|
65
|
+
plan_violation_task(
|
|
66
|
+
model,
|
|
67
|
+
path,
|
|
68
|
+
allow_type_mismatch,
|
|
69
|
+
allow_structural_violations,
|
|
70
|
+
forced_violation=violation,
|
|
71
|
+
)
|
|
66
72
|
for path in paths
|
|
67
73
|
for violation in plan_violation_task(
|
|
68
74
|
model, path, allow_type_mismatch, allow_structural_violations
|
|
@@ -5,12 +5,19 @@ from .context import GenerationContext
|
|
|
5
5
|
from .registry import choose_mismatch_kind, get_generator
|
|
6
6
|
|
|
7
7
|
from conformly._internal.planner import PlannedTask
|
|
8
|
+
from conformly._internal.planner.model import CollectionTarget
|
|
8
9
|
from conformly._internal.resolver import (
|
|
9
10
|
ResolvedField,
|
|
10
11
|
ResolvedModel,
|
|
11
12
|
create_minimal_semantic,
|
|
12
13
|
)
|
|
13
|
-
from conformly._internal.resolver.semantics import
|
|
14
|
+
from conformly._internal.resolver.semantics import (
|
|
15
|
+
DictSemantic,
|
|
16
|
+
FieldSemantics,
|
|
17
|
+
ListSemantic,
|
|
18
|
+
ObjectSemantic,
|
|
19
|
+
TupleSemantic,
|
|
20
|
+
)
|
|
14
21
|
from conformly._internal.tracer import Tracer, ValueSource
|
|
15
22
|
from conformly._internal.types import UNSET, FieldKind, FieldPath, ViolationType
|
|
16
23
|
|
|
@@ -43,6 +50,8 @@ def generate_invalid(
|
|
|
43
50
|
violations=task.allowed_violations,
|
|
44
51
|
overrides=overrides,
|
|
45
52
|
tracer=tracer,
|
|
53
|
+
collection_target=task.collection_target,
|
|
54
|
+
collection_steps=task.collection_steps,
|
|
46
55
|
)
|
|
47
56
|
|
|
48
57
|
|
|
@@ -55,6 +64,8 @@ def _built_dict_with_violations(
|
|
|
55
64
|
overrides: dict[FieldPath, Any] | None = None,
|
|
56
65
|
tracer: Tracer | None = None,
|
|
57
66
|
path_names: tuple[str, ...] = (),
|
|
67
|
+
collection_target: CollectionTarget | None = None,
|
|
68
|
+
collection_steps: tuple[tuple[FieldPath, CollectionTarget], ...] = (),
|
|
58
69
|
) -> dict[str, Any]:
|
|
59
70
|
result: dict[str, Any] = {}
|
|
60
71
|
target_index = target_path[depth]
|
|
@@ -88,7 +99,9 @@ def _built_dict_with_violations(
|
|
|
88
99
|
|
|
89
100
|
if tracer:
|
|
90
101
|
tracer.set_target_path(".".join((*path_names, field.name)))
|
|
91
|
-
value = generate_field(
|
|
102
|
+
value = generate_field(
|
|
103
|
+
ctx, field, violations, overrides, tracer, collection_target
|
|
104
|
+
)
|
|
92
105
|
if value is not UNSET:
|
|
93
106
|
result[field.name] = value
|
|
94
107
|
|
|
@@ -100,7 +113,7 @@ def _built_dict_with_violations(
|
|
|
100
113
|
field=field.name,
|
|
101
114
|
)
|
|
102
115
|
|
|
103
|
-
|
|
116
|
+
nested_value = _built_dict_with_violations(
|
|
104
117
|
ctx=ctx,
|
|
105
118
|
model=field.nested_model,
|
|
106
119
|
target_path=target_path,
|
|
@@ -109,8 +122,27 @@ def _built_dict_with_violations(
|
|
|
109
122
|
overrides=overrides,
|
|
110
123
|
tracer=tracer,
|
|
111
124
|
path_names=(*path_names, field.name),
|
|
125
|
+
collection_target=collection_target,
|
|
126
|
+
collection_steps=collection_steps,
|
|
112
127
|
)
|
|
113
128
|
|
|
129
|
+
if isinstance(field.semantic, (ListSemantic, DictSemantic)):
|
|
130
|
+
step = next(
|
|
131
|
+
(t for p, t in collection_steps if p == target_path[: depth + 1]),
|
|
132
|
+
None,
|
|
133
|
+
)
|
|
134
|
+
if step is None:
|
|
135
|
+
raise generation_error(
|
|
136
|
+
"Nested collection requires a planned target",
|
|
137
|
+
code="missing_collection_target",
|
|
138
|
+
)
|
|
139
|
+
container = _generate_nested_collection(
|
|
140
|
+
ctx, field.semantic, step, nested_value
|
|
141
|
+
)
|
|
142
|
+
result[field.name] = container
|
|
143
|
+
else:
|
|
144
|
+
result[field.name] = nested_value
|
|
145
|
+
|
|
114
146
|
else:
|
|
115
147
|
key, value = _generate_extra_field_value(ctx)
|
|
116
148
|
suffix = 1
|
|
@@ -137,6 +169,7 @@ def generate_field(
|
|
|
137
169
|
violations: tuple[ViolationType, ...] | None = None,
|
|
138
170
|
overrides: dict[FieldPath, Any] | None = None,
|
|
139
171
|
tracer: Tracer | None = None,
|
|
172
|
+
collection_target: CollectionTarget | None = None,
|
|
140
173
|
) -> Any:
|
|
141
174
|
if violations is None:
|
|
142
175
|
if overrides and field.path in overrides:
|
|
@@ -160,7 +193,7 @@ def generate_field(
|
|
|
160
193
|
return value
|
|
161
194
|
|
|
162
195
|
if field.nested_model and not isinstance(
|
|
163
|
-
field.semantic, (ListSemantic, TupleSemantic)
|
|
196
|
+
field.semantic, (ListSemantic, DictSemantic, TupleSemantic)
|
|
164
197
|
):
|
|
165
198
|
return generate_valid(ctx, field.nested_model, overrides)
|
|
166
199
|
|
|
@@ -170,31 +203,50 @@ def generate_field(
|
|
|
170
203
|
tracer.set_violation(violation)
|
|
171
204
|
tracer.set_value_source(ValueSource.GENERATED)
|
|
172
205
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
mismatch_semantic = create_minimal_semantic(mismatch_kind)
|
|
176
|
-
value = get_generator(mismatch_kind).generate_value(
|
|
177
|
-
ctx, mismatch_semantic, None
|
|
178
|
-
)
|
|
179
|
-
if tracer:
|
|
180
|
-
tracer.set_generated_value(value)
|
|
181
|
-
return value
|
|
182
|
-
|
|
183
|
-
if violation == ViolationType.MISSING_FIELD:
|
|
184
|
-
if tracer:
|
|
185
|
-
tracer.set_generated_value(UNSET)
|
|
186
|
-
return UNSET
|
|
187
|
-
|
|
188
|
-
value = get_generator(field.semantic.kind).generate_value(
|
|
189
|
-
ctx, field.semantic, violation
|
|
206
|
+
value = generate_semantic_value(
|
|
207
|
+
ctx, field.semantic, field.nested_model, violations, collection_target
|
|
190
208
|
)
|
|
191
|
-
|
|
192
209
|
if tracer:
|
|
193
210
|
tracer.set_generated_value(value)
|
|
194
|
-
|
|
195
211
|
return value
|
|
196
212
|
|
|
197
213
|
|
|
214
|
+
def generate_semantic_value(
|
|
215
|
+
ctx: GenerationContext,
|
|
216
|
+
semantic: FieldSemantics,
|
|
217
|
+
nested_model: ResolvedModel | None,
|
|
218
|
+
violations: tuple[ViolationType, ...] | None,
|
|
219
|
+
collection_target: CollectionTarget | None = None,
|
|
220
|
+
) -> Any:
|
|
221
|
+
"""Generate directly from resolved semantics, without synthetic field models."""
|
|
222
|
+
violation = _choose_violation(violations)
|
|
223
|
+
if violation == ViolationType.TYPE_MISMATCH:
|
|
224
|
+
mismatch_kind = choose_mismatch_kind(semantic.kind)
|
|
225
|
+
return get_generator(mismatch_kind).generate_value(
|
|
226
|
+
ctx, create_minimal_semantic(mismatch_kind), None
|
|
227
|
+
)
|
|
228
|
+
if violation == ViolationType.MISSING_FIELD:
|
|
229
|
+
return UNSET
|
|
230
|
+
if nested_model is not None and isinstance(semantic, ObjectSemantic):
|
|
231
|
+
if violation is not None:
|
|
232
|
+
raise generation_error(
|
|
233
|
+
"Nested model violation requires a field target",
|
|
234
|
+
code="missing_nested_target",
|
|
235
|
+
)
|
|
236
|
+
return generate_valid(ctx, nested_model)
|
|
237
|
+
from .types import dictionaries, list as lists, tuple as tuples
|
|
238
|
+
|
|
239
|
+
if isinstance(semantic, ListSemantic):
|
|
240
|
+
return lists.generate_value(ctx, semantic, violation, target=collection_target)
|
|
241
|
+
if isinstance(semantic, DictSemantic):
|
|
242
|
+
return dictionaries.generate_value(
|
|
243
|
+
ctx, semantic, violation, target=collection_target
|
|
244
|
+
)
|
|
245
|
+
if isinstance(semantic, TupleSemantic):
|
|
246
|
+
return tuples.generate_value(ctx, semantic, violation, target=collection_target)
|
|
247
|
+
return get_generator(semantic.kind).generate_value(ctx, semantic, violation)
|
|
248
|
+
|
|
249
|
+
|
|
198
250
|
def _choose_violation(
|
|
199
251
|
violations: tuple[ViolationType, ...] | None,
|
|
200
252
|
) -> ViolationType | None:
|
|
@@ -209,3 +261,28 @@ def _generate_extra_field_value(ctx: GenerationContext) -> tuple[str, Any]:
|
|
|
209
261
|
value = get_generator(semantic.kind).generate_value(ctx, semantic, None)
|
|
210
262
|
key = "extra"
|
|
211
263
|
return (key, value)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _generate_nested_collection(
|
|
267
|
+
ctx: GenerationContext,
|
|
268
|
+
semantic: ListSemantic | DictSemantic,
|
|
269
|
+
target: CollectionTarget,
|
|
270
|
+
nested_value: dict[str, Any],
|
|
271
|
+
) -> Any:
|
|
272
|
+
from .types._utils import calculate_valid_collection_range, unique_collection_range
|
|
273
|
+
from .types.dictionaries import _generate_dict_of_length
|
|
274
|
+
from .types.list import _generate_list_of_length
|
|
275
|
+
|
|
276
|
+
lower, upper = calculate_valid_collection_range(semantic)
|
|
277
|
+
if isinstance(semantic, DictSemantic):
|
|
278
|
+
lower, upper = unique_collection_range(
|
|
279
|
+
lower, upper, semantic.key_semantic, collection="dict"
|
|
280
|
+
)
|
|
281
|
+
length = ctx.rng.randint(lower, upper)
|
|
282
|
+
return _generate_dict_of_length(
|
|
283
|
+
ctx, semantic, length, target=target, target_value=nested_value
|
|
284
|
+
)
|
|
285
|
+
length = ctx.rng.randint(lower, upper)
|
|
286
|
+
result = _generate_list_of_length(ctx, semantic, length - 1)
|
|
287
|
+
result.insert(target.index, nested_value)
|
|
288
|
+
return result
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .._errors import generation_error
|
|
4
|
+
from ..context import GenerationContext
|
|
5
|
+
|
|
6
|
+
from conformly._internal.resolver import ResolvedModel
|
|
7
|
+
from conformly._internal.resolver.semantics import (
|
|
8
|
+
DictSemantic,
|
|
9
|
+
FieldSemantics,
|
|
10
|
+
ListSemantic,
|
|
11
|
+
TupleSemantic,
|
|
12
|
+
)
|
|
13
|
+
from conformly._internal.resolver.semantics._domains import finite_cardinality
|
|
14
|
+
from conformly._internal.types import ViolationType
|
|
15
|
+
from conformly.exceptions import GenerationError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def calculate_valid_collection_range(
|
|
19
|
+
semantic: ListSemantic | DictSemantic | TupleSemantic,
|
|
20
|
+
) -> tuple[int, int]:
|
|
21
|
+
"""Choose a small default range without overriding declared size limits."""
|
|
22
|
+
if semantic.length_range is None:
|
|
23
|
+
return 1, 3
|
|
24
|
+
lower = semantic.length_range.min_length
|
|
25
|
+
upper = semantic.length_range.max_length
|
|
26
|
+
if upper == 0:
|
|
27
|
+
return lower, 0
|
|
28
|
+
lower = max(1, lower)
|
|
29
|
+
return lower, upper if upper is not None else max(3, lower)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def unique_collection_range(
|
|
33
|
+
lower: int,
|
|
34
|
+
upper: int,
|
|
35
|
+
semantic: FieldSemantics,
|
|
36
|
+
*,
|
|
37
|
+
collection: str,
|
|
38
|
+
extra_values: int = 0,
|
|
39
|
+
) -> tuple[int, int]:
|
|
40
|
+
cardinality = finite_cardinality(semantic)
|
|
41
|
+
if cardinality is not None:
|
|
42
|
+
cardinality += extra_values
|
|
43
|
+
if lower > cardinality:
|
|
44
|
+
raise generation_error(
|
|
45
|
+
"Collection size exceeds the number of distinct valid values",
|
|
46
|
+
code="impossible_collection_size",
|
|
47
|
+
collection=collection,
|
|
48
|
+
requested_size=lower,
|
|
49
|
+
cardinality=cardinality,
|
|
50
|
+
)
|
|
51
|
+
upper = min(upper, cardinality)
|
|
52
|
+
return lower, upper
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def collection_retry_error(
|
|
56
|
+
*, collection: str, requested_size: int, generated_size: int, attempts: int
|
|
57
|
+
) -> GenerationError:
|
|
58
|
+
return generation_error(
|
|
59
|
+
"Could not fill collection with distinct valid values",
|
|
60
|
+
code="collection_retry_exhausted",
|
|
61
|
+
collection=collection,
|
|
62
|
+
requested_size=requested_size,
|
|
63
|
+
generated_size=generated_size,
|
|
64
|
+
attempts=attempts,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_hashable(value: Any) -> bool:
|
|
69
|
+
try:
|
|
70
|
+
hash(value)
|
|
71
|
+
return True
|
|
72
|
+
except TypeError:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def generate_collection_item(
|
|
77
|
+
ctx: GenerationContext,
|
|
78
|
+
item_semantic: FieldSemantics,
|
|
79
|
+
item_nested_model: ResolvedModel | None,
|
|
80
|
+
violations: tuple[ViolationType, ...] | None,
|
|
81
|
+
) -> Any:
|
|
82
|
+
from ..orchestration import generate_semantic_value
|
|
83
|
+
|
|
84
|
+
return generate_semantic_value(ctx, item_semantic, item_nested_model, violations)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .._errors import generation_error
|
|
4
|
+
from ..context import GenerationContext
|
|
5
|
+
from ._utils import (
|
|
6
|
+
calculate_valid_collection_range,
|
|
7
|
+
collection_retry_error,
|
|
8
|
+
generate_collection_item,
|
|
9
|
+
is_hashable,
|
|
10
|
+
unique_collection_range,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from conformly._internal.planner.model import CollectionTarget
|
|
14
|
+
from conformly._internal.resolver.semantics import DictSemantic
|
|
15
|
+
from conformly._internal.types import UNSET, ViolationType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_value(
|
|
19
|
+
ctx: GenerationContext,
|
|
20
|
+
semantic: DictSemantic,
|
|
21
|
+
violation: ViolationType | None = None,
|
|
22
|
+
*,
|
|
23
|
+
target: CollectionTarget | None = None,
|
|
24
|
+
) -> dict[Any, Any]:
|
|
25
|
+
return (
|
|
26
|
+
_generate_valid_dict(ctx, semantic)
|
|
27
|
+
if violation is None
|
|
28
|
+
else _generate_invalid_dict(ctx, semantic, violation, target)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _generate_valid_dict(
|
|
33
|
+
ctx: GenerationContext, semantic: DictSemantic
|
|
34
|
+
) -> dict[Any, Any]:
|
|
35
|
+
min_len, max_len = calculate_valid_collection_range(semantic)
|
|
36
|
+
|
|
37
|
+
min_len, max_len = unique_collection_range(
|
|
38
|
+
min_len, max_len, semantic.key_semantic, collection="dict"
|
|
39
|
+
)
|
|
40
|
+
length = ctx.rng.randint(min_len, max_len)
|
|
41
|
+
return _generate_dict_of_length(ctx, semantic, length)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _generate_dict_of_length(
|
|
45
|
+
ctx: GenerationContext,
|
|
46
|
+
semantic: DictSemantic,
|
|
47
|
+
length: int,
|
|
48
|
+
violation: ViolationType | None = None,
|
|
49
|
+
target: CollectionTarget | None = None,
|
|
50
|
+
target_value: Any = UNSET,
|
|
51
|
+
) -> dict[Any, Any]:
|
|
52
|
+
result: dict[Any, Any] = {}
|
|
53
|
+
max_attempts = max(100, length * 20)
|
|
54
|
+
for _ in range(max_attempts):
|
|
55
|
+
if len(result) == length:
|
|
56
|
+
return result
|
|
57
|
+
is_target = target is not None and len(result) == target.index
|
|
58
|
+
key_violation = (
|
|
59
|
+
(violation,)
|
|
60
|
+
if is_target
|
|
61
|
+
and target is not None
|
|
62
|
+
and target.side == "key"
|
|
63
|
+
and violation is not None
|
|
64
|
+
else None
|
|
65
|
+
)
|
|
66
|
+
value_violation = (
|
|
67
|
+
(violation,)
|
|
68
|
+
if is_target
|
|
69
|
+
and target is not None
|
|
70
|
+
and target.side == "value"
|
|
71
|
+
and violation is not None
|
|
72
|
+
else None
|
|
73
|
+
)
|
|
74
|
+
key = generate_collection_item(ctx, semantic.key_semantic, None, key_violation)
|
|
75
|
+
if not is_hashable(key) or key in result:
|
|
76
|
+
continue
|
|
77
|
+
result[key] = (
|
|
78
|
+
target_value
|
|
79
|
+
if is_target and target_value is not UNSET
|
|
80
|
+
else generate_collection_item(
|
|
81
|
+
ctx,
|
|
82
|
+
semantic.value_semantic,
|
|
83
|
+
semantic.value_nested_model,
|
|
84
|
+
value_violation,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
if len(result) == length:
|
|
88
|
+
return result
|
|
89
|
+
raise collection_retry_error(
|
|
90
|
+
collection="dict",
|
|
91
|
+
requested_size=length,
|
|
92
|
+
generated_size=len(result),
|
|
93
|
+
attempts=max_attempts,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _generate_invalid_dict(
|
|
98
|
+
ctx: GenerationContext,
|
|
99
|
+
semantic: DictSemantic,
|
|
100
|
+
violation: ViolationType,
|
|
101
|
+
target: CollectionTarget | None,
|
|
102
|
+
) -> dict[Any, Any]:
|
|
103
|
+
min_len, max_len = calculate_valid_collection_range(semantic)
|
|
104
|
+
if violation == ViolationType.TOO_LESS_ITEMS:
|
|
105
|
+
lower = semantic.length_range.min_length if semantic.length_range else min_len
|
|
106
|
+
length = max(0, lower - 1)
|
|
107
|
+
unique_collection_range(
|
|
108
|
+
length, length, semantic.key_semantic, collection="dict"
|
|
109
|
+
)
|
|
110
|
+
return _generate_dict_of_length(ctx, semantic, length)
|
|
111
|
+
if violation == ViolationType.TOO_MANY_ITEMS:
|
|
112
|
+
length = max_len + 1
|
|
113
|
+
unique_collection_range(
|
|
114
|
+
length, length, semantic.key_semantic, collection="dict"
|
|
115
|
+
)
|
|
116
|
+
return _generate_dict_of_length(ctx, semantic, length)
|
|
117
|
+
if target is None or target.side not in ("key", "value"):
|
|
118
|
+
raise generation_error(
|
|
119
|
+
"Dictionary violation requires a planned key or value target",
|
|
120
|
+
code="missing_collection_target",
|
|
121
|
+
)
|
|
122
|
+
min_len, max_len = unique_collection_range(
|
|
123
|
+
min_len,
|
|
124
|
+
max_len,
|
|
125
|
+
semantic.key_semantic,
|
|
126
|
+
collection="dict",
|
|
127
|
+
extra_values=1 if target.side == "key" else 0,
|
|
128
|
+
)
|
|
129
|
+
length = ctx.rng.randint(min_len, max_len)
|
|
130
|
+
if not 0 <= target.index < length:
|
|
131
|
+
raise generation_error(
|
|
132
|
+
"Dictionary target is outside collection",
|
|
133
|
+
code="invalid_collection_target",
|
|
134
|
+
index=target.index,
|
|
135
|
+
size=length,
|
|
136
|
+
)
|
|
137
|
+
return _generate_dict_of_length(ctx, semantic, length, violation, target)
|