conformly 0.7.2__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.2/src/conformly.egg-info → conformly-0.7.4}/PKG-INFO +60 -7
- {conformly-0.7.2 → conformly-0.7.4}/README.md +59 -6
- {conformly-0.7.2 → conformly-0.7.4}/pyproject.toml +2 -1
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/_utils.py +7 -1
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/case.py +9 -7
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/cases.py +5 -4
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/context.py +1 -1
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/orchestration.py +118 -36
- 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.2 → 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.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/string.py +42 -30
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/tuple.py +33 -33
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/url.py +3 -1
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/uuid.py +6 -6
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/attrs.py +6 -1
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/pydantic.py +1 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/typeddict.py +9 -3
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/core/element.py +0 -2
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/core/field.py +5 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/models.py +6 -1
- {conformly-0.7.2 → 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.2 → conformly-0.7.4}/src/conformly/_internal/planner/session.py +17 -4
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/models.py +9 -2
- {conformly-0.7.2 → 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.2 → conformly-0.7.4/src/conformly.egg-info}/PKG-INFO +60 -7
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/SOURCES.txt +4 -0
- conformly-0.7.4/tests/test_collection.py +29 -0
- conformly-0.7.2/src/conformly/_internal/generator/types/_utils.py +0 -63
- conformly-0.7.2/src/conformly/_internal/generator/types/dictionaries.py +0 -122
- conformly-0.7.2/src/conformly/_internal/generator/types/float.py +0 -89
- conformly-0.7.2/src/conformly/_internal/generator/types/list.py +0 -112
- conformly-0.7.2/src/conformly/_internal/planner/model.py +0 -9
- {conformly-0.7.2 → conformly-0.7.4}/LICENSE +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/setup.cfg +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/_errors.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/_path_proxy.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/api/path.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/base.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/collections.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/enum.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/keys.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/mapping.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/numeric.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/constraints/string.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/fields/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/fields/registry.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/fields/special_types.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/_errors.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/protocol.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/registry.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/boolean.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/email.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/enum.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv4.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipv6.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/generator/types/ipvany.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/bootstrap.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/dataclass.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/protocol.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/adapters/registry.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/api.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/core/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/constraints.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/parser/extractors/types.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/planner/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/planner/_errors.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/_validators.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/base.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/boolean.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/dict.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/enum.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/factory.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/list.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/numeric.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/object.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/string.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/tuple.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/resolver/semantics/uuid.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/tracer.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/constants.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/primitives.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/strategies.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/_internal/types/values.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/exceptions.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/py.typed +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly/tracer/__init__.py +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/dependency_links.txt +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/requires.txt +0 -0
- {conformly-0.7.2 → conformly-0.7.4}/src/conformly.egg-info/top_level.txt +0 -0
- {conformly-0.7.2 → 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
|
|
@@ -252,6 +252,12 @@ overrides=[
|
|
|
252
252
|
]
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
+
Integer seeds, including `0` and negative values, make `case()` and `cases()`
|
|
256
|
+
reproducible for internal generation, including nested models and UUID values.
|
|
257
|
+
The same model, options, and seed produce the same payloads. User-provided default
|
|
258
|
+
factories run once per generated field and must themselves be deterministic;
|
|
259
|
+
the seed does not control their randomness or external state.
|
|
260
|
+
|
|
255
261
|
## Error Handling
|
|
256
262
|
|
|
257
263
|
All `conformly` errors inherit from `ConformlyError`, providing consistent interface for debugging and programmatic handling.
|
|
@@ -291,17 +297,43 @@ For `case(Model, valid=False, strategy="<field>")`:
|
|
|
291
297
|
- **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
|
|
292
298
|
- **Exactly one field is targeted** (the one specified by `strategy`).
|
|
293
299
|
- **The generator will violate constraints** for that field, making it invalid.
|
|
294
|
-
- **
|
|
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.
|
|
295
309
|
- **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
|
|
296
310
|
- **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
|
|
297
311
|
|
|
298
|
-
|
|
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.
|
|
299
315
|
|
|
300
316
|
## Optional Fields and Defaults
|
|
301
317
|
|
|
302
|
-
-
|
|
303
|
-
|
|
304
|
-
|
|
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`.
|
|
305
337
|
|
|
306
338
|
## Constraints
|
|
307
339
|
|
|
@@ -321,6 +353,19 @@ If you need **deterministic control** over which exact constraint to violate, th
|
|
|
321
353
|
| Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
|
|
322
354
|
| Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
|
|
323
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
|
+
|
|
324
369
|
> Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
|
|
325
370
|
> Use Field() parameters for constraint extraction.
|
|
326
371
|
|
|
@@ -648,7 +693,15 @@ print(trace.generated_value)
|
|
|
648
693
|
print(trace.seed)
|
|
649
694
|
```
|
|
650
695
|
|
|
651
|
-
|
|
696
|
+
Tracing is available for `case(..., valid=False)`. The trace describes the selected
|
|
697
|
+
target: its dotted path, violation, generated value, seed, and value source. This
|
|
698
|
+
applies to DSL and string paths as well as `"first"` and `"random"` selection.
|
|
699
|
+
Unrelated fields, defaults, and overrides do not overwrite target metadata. For
|
|
700
|
+
`MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
|
|
701
|
+
absent from the payload.
|
|
702
|
+
|
|
703
|
+
Enabling tracing leaves payloads, internal RNG consumption, and default factory
|
|
704
|
+
call counts unchanged. A reused tracer describes the latest completed generation.
|
|
652
705
|
|
|
653
706
|
|
|
654
707
|
## Development
|
|
@@ -219,6 +219,12 @@ overrides=[
|
|
|
219
219
|
]
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
Integer seeds, including `0` and negative values, make `case()` and `cases()`
|
|
223
|
+
reproducible for internal generation, including nested models and UUID values.
|
|
224
|
+
The same model, options, and seed produce the same payloads. User-provided default
|
|
225
|
+
factories run once per generated field and must themselves be deterministic;
|
|
226
|
+
the seed does not control their randomness or external state.
|
|
227
|
+
|
|
222
228
|
## Error Handling
|
|
223
229
|
|
|
224
230
|
All `conformly` errors inherit from `ConformlyError`, providing consistent interface for debugging and programmatic handling.
|
|
@@ -258,17 +264,43 @@ For `case(Model, valid=False, strategy="<field>")`:
|
|
|
258
264
|
- **If `allow_structural_violations=True`**, generator may substitute field missing in place of any other violations (avaliable only with `strategy="all"`)
|
|
259
265
|
- **Exactly one field is targeted** (the one specified by `strategy`).
|
|
260
266
|
- **The generator will violate constraints** for that field, making it invalid.
|
|
261
|
-
- **
|
|
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.
|
|
262
276
|
- **For numeric bounds**, invalid values may violate the lower or upper bound (e.g., `age > 120` or `age < 18`).
|
|
263
277
|
- **For float bounds**, invalid generation may produce `inf` when violating the upper boundary.
|
|
264
278
|
|
|
265
|
-
|
|
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.
|
|
266
282
|
|
|
267
283
|
## Optional Fields and Defaults
|
|
268
284
|
|
|
269
|
-
-
|
|
270
|
-
|
|
271
|
-
|
|
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`.
|
|
272
304
|
|
|
273
305
|
## Constraints
|
|
274
306
|
|
|
@@ -288,6 +320,19 @@ If you need **deterministic control** over which exact constraint to violate, th
|
|
|
288
320
|
| Collection | `MaxItems(n)` | `max_length=n` (for lists in `Field(...)`) |
|
|
289
321
|
| Collection | `UniqueItems(bool)` | `set[T]`, `frozenset[T]` |
|
|
290
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
|
+
|
|
291
336
|
> Important: Pydantic's constr(), conint(), and functional validators are not interpreted as constraints.
|
|
292
337
|
> Use Field() parameters for constraint extraction.
|
|
293
338
|
|
|
@@ -615,7 +660,15 @@ print(trace.generated_value)
|
|
|
615
660
|
print(trace.seed)
|
|
616
661
|
```
|
|
617
662
|
|
|
618
|
-
|
|
663
|
+
Tracing is available for `case(..., valid=False)`. The trace describes the selected
|
|
664
|
+
target: its dotted path, violation, generated value, seed, and value source. This
|
|
665
|
+
applies to DSL and string paths as well as `"first"` and `"random"` selection.
|
|
666
|
+
Unrelated fields, defaults, and overrides do not overwrite target metadata. For
|
|
667
|
+
`MISSING_FIELD`, the value is the internal `UNSET` sentinel because the field is
|
|
668
|
+
absent from the payload.
|
|
669
|
+
|
|
670
|
+
Enabling tracing leaves payloads, internal RNG consumption, and default factory
|
|
671
|
+
call counts unchanged. A reused tracer describes the latest completed generation.
|
|
619
672
|
|
|
620
673
|
|
|
621
674
|
## Development
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "conformly"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
4
4
|
description = "Generate valid & invalid test data from your typed schemas"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -90,6 +90,7 @@ line-ending = "auto"
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
[tool.pytest.ini_options]
|
|
93
|
+
xfail_strict = true
|
|
93
94
|
markers = ["benchmark: performance benchmarks"]
|
|
94
95
|
|
|
95
96
|
addopts = ["-v", "--strict-markers"]
|
|
@@ -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
|
|
@@ -38,9 +38,11 @@ def case(
|
|
|
38
38
|
If False, generate an invalid one.
|
|
39
39
|
|
|
40
40
|
seed:
|
|
41
|
-
Random seed for reproducible generation.
|
|
41
|
+
Random seed for reproducible internal generation.
|
|
42
42
|
- `None` (default): Use system randomness (different output each run).
|
|
43
|
-
- `int`: Initialize RNG with fixed seed
|
|
43
|
+
- `int`: Initialize RNG with a fixed seed, including 0 and negatives.
|
|
44
|
+
User-provided default factories must be deterministic for identical
|
|
45
|
+
payloads; their randomness is not controlled by this seed.
|
|
44
46
|
|
|
45
47
|
strategy:
|
|
46
48
|
Define which field to violate when valid=False.
|
|
@@ -63,14 +65,17 @@ def case(
|
|
|
63
65
|
|
|
64
66
|
overrides:
|
|
65
67
|
Optional list of `PathSelector` to set field values.
|
|
66
|
-
-
|
|
67
|
-
- Override generated values
|
|
68
|
+
- Used instead of generated values and model defaults
|
|
68
69
|
- With valid=False: act as defaults (ignored if field is violated)
|
|
69
70
|
|
|
70
71
|
allow_type_mismatch:
|
|
71
72
|
If True fields could be type mismatched.
|
|
72
73
|
Availiable only when valid=False.
|
|
73
74
|
|
|
75
|
+
tracer:
|
|
76
|
+
Optional tracer for invalid generation. Records the selected target
|
|
77
|
+
path, violation, and value without changing generation or factory calls.
|
|
78
|
+
|
|
74
79
|
Returns:
|
|
75
80
|
A dictionary representing the instance.
|
|
76
81
|
"""
|
|
@@ -111,9 +116,6 @@ def case(
|
|
|
111
116
|
strategy=strategy,
|
|
112
117
|
)
|
|
113
118
|
|
|
114
|
-
if tracer and isinstance(strategy, PathSelector):
|
|
115
|
-
tracer.set_target_path(strategy.raw_path)
|
|
116
|
-
|
|
117
119
|
task = plan_tasks(
|
|
118
120
|
model,
|
|
119
121
|
ctx=ctx,
|
|
@@ -38,9 +38,11 @@ def cases(
|
|
|
38
38
|
If False, generate invalid ones.
|
|
39
39
|
|
|
40
40
|
seed:
|
|
41
|
-
Random seed for reproducible generation.
|
|
41
|
+
Random seed for reproducible internal generation.
|
|
42
42
|
- `None` (default): Use system randomness (different output each run).
|
|
43
|
-
- `int`: Initialize RNG with fixed seed
|
|
43
|
+
- `int`: Initialize RNG with a fixed seed, including 0 and negatives.
|
|
44
|
+
User-provided default factories must be deterministic for identical
|
|
45
|
+
payloads; their randomness is not controlled by this seed.
|
|
44
46
|
|
|
45
47
|
strategy:
|
|
46
48
|
Define how fields are selected for violation when valid=False.
|
|
@@ -74,8 +76,7 @@ def cases(
|
|
|
74
76
|
|
|
75
77
|
overrides:
|
|
76
78
|
Optional list of `PathSelector` to set field values.
|
|
77
|
-
-
|
|
78
|
-
- Override generated values
|
|
79
|
+
- Used instead of generated values and model defaults
|
|
79
80
|
- With valid=False: act as defaults (ignored if field is violated)
|
|
80
81
|
|
|
81
82
|
|
|
@@ -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
|
|
|
@@ -54,6 +63,9 @@ def _built_dict_with_violations(
|
|
|
54
63
|
violations: tuple[ViolationType, ...],
|
|
55
64
|
overrides: dict[FieldPath, Any] | None = None,
|
|
56
65
|
tracer: Tracer | None = None,
|
|
66
|
+
path_names: tuple[str, ...] = (),
|
|
67
|
+
collection_target: CollectionTarget | None = None,
|
|
68
|
+
collection_steps: tuple[tuple[FieldPath, CollectionTarget], ...] = (),
|
|
57
69
|
) -> dict[str, Any]:
|
|
58
70
|
result: dict[str, Any] = {}
|
|
59
71
|
target_index = target_path[depth]
|
|
@@ -72,7 +84,7 @@ def _built_dict_with_violations(
|
|
|
72
84
|
|
|
73
85
|
for i in range(0, target_index):
|
|
74
86
|
field = fields[i]
|
|
75
|
-
result[field.name] = generate_field(ctx, field, None, overrides
|
|
87
|
+
result[field.name] = generate_field(ctx, field, None, overrides)
|
|
76
88
|
|
|
77
89
|
if target_index < total_fields:
|
|
78
90
|
field = fields[target_index]
|
|
@@ -85,7 +97,11 @@ def _built_dict_with_violations(
|
|
|
85
97
|
field=field.name,
|
|
86
98
|
)
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
if tracer:
|
|
101
|
+
tracer.set_target_path(".".join((*path_names, field.name)))
|
|
102
|
+
value = generate_field(
|
|
103
|
+
ctx, field, violations, overrides, tracer, collection_target
|
|
104
|
+
)
|
|
89
105
|
if value is not UNSET:
|
|
90
106
|
result[field.name] = value
|
|
91
107
|
|
|
@@ -97,7 +113,7 @@ def _built_dict_with_violations(
|
|
|
97
113
|
field=field.name,
|
|
98
114
|
)
|
|
99
115
|
|
|
100
|
-
|
|
116
|
+
nested_value = _built_dict_with_violations(
|
|
101
117
|
ctx=ctx,
|
|
102
118
|
model=field.nested_model,
|
|
103
119
|
target_path=target_path,
|
|
@@ -105,18 +121,44 @@ def _built_dict_with_violations(
|
|
|
105
121
|
violations=violations,
|
|
106
122
|
overrides=overrides,
|
|
107
123
|
tracer=tracer,
|
|
124
|
+
path_names=(*path_names, field.name),
|
|
125
|
+
collection_target=collection_target,
|
|
126
|
+
collection_steps=collection_steps,
|
|
108
127
|
)
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
113
145
|
|
|
146
|
+
else:
|
|
114
147
|
key, value = _generate_extra_field_value(ctx)
|
|
148
|
+
suffix = 1
|
|
149
|
+
while key in result:
|
|
150
|
+
key = f"extra_{suffix}"
|
|
151
|
+
suffix += 1
|
|
115
152
|
result[key] = value
|
|
153
|
+
if tracer:
|
|
154
|
+
tracer.set_target_path(".".join((*path_names, key)))
|
|
155
|
+
tracer.set_violation(ViolationType.EXTRA_FIELD)
|
|
156
|
+
tracer.set_value_source(ValueSource.GENERATED)
|
|
157
|
+
tracer.set_generated_value(value)
|
|
116
158
|
|
|
117
159
|
for i in range(target_index + 1, total_fields):
|
|
118
160
|
field = fields[i]
|
|
119
|
-
result[field.name] = generate_field(ctx, field,
|
|
161
|
+
result[field.name] = generate_field(ctx, field, overrides=overrides)
|
|
120
162
|
|
|
121
163
|
return result
|
|
122
164
|
|
|
@@ -127,6 +169,7 @@ def generate_field(
|
|
|
127
169
|
violations: tuple[ViolationType, ...] | None = None,
|
|
128
170
|
overrides: dict[FieldPath, Any] | None = None,
|
|
129
171
|
tracer: Tracer | None = None,
|
|
172
|
+
collection_target: CollectionTarget | None = None,
|
|
130
173
|
) -> Any:
|
|
131
174
|
if violations is None:
|
|
132
175
|
if overrides and field.path in overrides:
|
|
@@ -144,18 +187,13 @@ def generate_field(
|
|
|
144
187
|
tracer.set_value_source(ValueSource.MODEL_DEFAULT)
|
|
145
188
|
|
|
146
189
|
default = field.default
|
|
147
|
-
if callable(default)
|
|
148
|
-
if tracer:
|
|
149
|
-
tracer.set_generated_value(default())
|
|
150
|
-
return default()
|
|
151
|
-
|
|
190
|
+
value = default() if callable(default) else default
|
|
152
191
|
if tracer:
|
|
153
|
-
tracer.set_generated_value(
|
|
154
|
-
|
|
155
|
-
return default
|
|
192
|
+
tracer.set_generated_value(value)
|
|
193
|
+
return value
|
|
156
194
|
|
|
157
195
|
if field.nested_model and not isinstance(
|
|
158
|
-
field.semantic, (ListSemantic, TupleSemantic)
|
|
196
|
+
field.semantic, (ListSemantic, DictSemantic, TupleSemantic)
|
|
159
197
|
):
|
|
160
198
|
return generate_valid(ctx, field.nested_model, overrides)
|
|
161
199
|
|
|
@@ -165,31 +203,50 @@ def generate_field(
|
|
|
165
203
|
tracer.set_violation(violation)
|
|
166
204
|
tracer.set_value_source(ValueSource.GENERATED)
|
|
167
205
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
mismatch_semantic = create_minimal_semantic(mismatch_kind)
|
|
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
|
|
177
|
-
|
|
178
|
-
if violation == ViolationType.MISSING_FIELD:
|
|
179
|
-
if tracer:
|
|
180
|
-
tracer.set_generated_value(UNSET)
|
|
181
|
-
return UNSET
|
|
182
|
-
|
|
183
|
-
value = get_generator(field.semantic.kind).generate_value(
|
|
184
|
-
ctx, field.semantic, violation
|
|
206
|
+
value = generate_semantic_value(
|
|
207
|
+
ctx, field.semantic, field.nested_model, violations, collection_target
|
|
185
208
|
)
|
|
186
|
-
|
|
187
209
|
if tracer:
|
|
188
210
|
tracer.set_generated_value(value)
|
|
189
|
-
|
|
190
211
|
return value
|
|
191
212
|
|
|
192
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
|
+
|
|
193
250
|
def _choose_violation(
|
|
194
251
|
violations: tuple[ViolationType, ...] | None,
|
|
195
252
|
) -> ViolationType | None:
|
|
@@ -204,3 +261,28 @@ def _generate_extra_field_value(ctx: GenerationContext) -> tuple[str, Any]:
|
|
|
204
261
|
value = get_generator(semantic.kind).generate_value(ctx, semantic, None)
|
|
205
262
|
key = "extra"
|
|
206
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)
|