modelfill 0.1.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.
- modelfill-0.1.0/LICENSE +21 -0
- modelfill-0.1.0/PKG-INFO +375 -0
- modelfill-0.1.0/README.md +348 -0
- modelfill-0.1.0/pyproject.toml +71 -0
- modelfill-0.1.0/pyproject.toml.orig +48 -0
- modelfill-0.1.0/src/modelfill/__init__.py +22 -0
- modelfill-0.1.0/src/modelfill/adapters.py +422 -0
- modelfill-0.1.0/src/modelfill/backend.py +350 -0
- modelfill-0.1.0/src/modelfill/errors.py +25 -0
- modelfill-0.1.0/src/modelfill/extract.py +576 -0
- modelfill-0.1.0/src/modelfill/filler.py +250 -0
- modelfill-0.1.0/src/modelfill/hypothesis.py +54 -0
- modelfill-0.1.0/src/modelfill/ir.py +152 -0
- modelfill-0.1.0/src/modelfill/py.typed +0 -0
modelfill-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 modelfill contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
modelfill-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modelfill
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Complete Pydantic models and dataclasses from their existing constraints
|
|
5
|
+
Keywords: pydantic,dataclasses,test data,constraint solving,z3,hypothesis
|
|
6
|
+
Author: Juho Lauri
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: Hypothesis
|
|
11
|
+
Classifier: Framework :: Pydantic :: 2
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: pydantic>=2.10.3,<3
|
|
20
|
+
Requires-Dist: z3-solver>=4.13,<5
|
|
21
|
+
Requires-Dist: hypothesis>=6.10.1,<7 ; extra == 'hypothesis'
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Project-URL: Repository, https://github.com/jplauri/modelfill
|
|
24
|
+
Project-URL: Issues, https://github.com/jplauri/modelfill/issues
|
|
25
|
+
Provides-Extra: hypothesis
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# modelfill
|
|
29
|
+
|
|
30
|
+
**Define validation rules once. Generate valid objects automatically.**
|
|
31
|
+
|
|
32
|
+
modelfill completes Pydantic models and standard dataclasses by translating their
|
|
33
|
+
existing validation rules into solver constraints. It solves for missing fields
|
|
34
|
+
in either direction and runs the original constructor before returning anything.
|
|
35
|
+
Models require no changes, factory classes, decorators, or duplicate rules. It
|
|
36
|
+
also plugs into [Hypothesis](#hypothesis): Hypothesis draws the fields you care
|
|
37
|
+
about, and modelfill solves the rest consistently.
|
|
38
|
+
|
|
39
|
+
modelfill reads a [documented subset](#exact-recognized-validator-language) of
|
|
40
|
+
Python: pure, deterministic validators that reject invalid input. It reports
|
|
41
|
+
logic outside that subset, and it never claims a contradiction it cannot prove.
|
|
42
|
+
|
|
43
|
+
## Install and try
|
|
44
|
+
|
|
45
|
+
modelfill needs Python 3.12 or later:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install modelfill # core
|
|
49
|
+
pip install "modelfill[hypothesis]" # with the Hypothesis strategy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
To run the example from a checkout, use [uv](https://docs.astral.sh/uv/):
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uv sync
|
|
56
|
+
uv run python examples/meeting.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Save models in a Python source file: extraction requires `inspect.getsource()`.
|
|
60
|
+
Interactive definitions without retrievable source use automatic fallback or
|
|
61
|
+
fail in strict mode.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from typing import Self
|
|
65
|
+
from pydantic import BaseModel, Field, model_validator
|
|
66
|
+
from modelfill import fill, ModelFiller, UnsatisfiableConstraints
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Meeting(BaseModel):
|
|
70
|
+
start: int = Field(ge=8, le=17)
|
|
71
|
+
end: int = Field(ge=9, le=18)
|
|
72
|
+
duration: int = Field(ge=1, le=4)
|
|
73
|
+
|
|
74
|
+
@model_validator(mode="after")
|
|
75
|
+
def check(self) -> Self:
|
|
76
|
+
if self.end != self.start + self.duration:
|
|
77
|
+
raise ValueError("Invalid meeting")
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
assert fill(Meeting, end=18, duration=4).start == 14
|
|
82
|
+
assert fill(Meeting, start=10, end=13).duration == 3
|
|
83
|
+
assert fill(Meeting, start=10, duration=2).end == 12
|
|
84
|
+
meeting = fill(Meeting)
|
|
85
|
+
assert meeting.end == meeting.start + meeting.duration
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
fill(Meeting, start=17, duration=4)
|
|
89
|
+
except UnsatisfiableConstraints:
|
|
90
|
+
pass # end would be 21, but end <= 18
|
|
91
|
+
else:
|
|
92
|
+
raise AssertionError("Expected an impossible assignment")
|
|
93
|
+
|
|
94
|
+
filler = ModelFiller(Meeting, mode="strict", seed=42)
|
|
95
|
+
assert filler.fill(end=18, duration=4).start == 14
|
|
96
|
+
meetings = filler.batch(100)
|
|
97
|
+
print(filler.explain())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The same analysis works on ordinary dataclasses:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from dataclasses import dataclass
|
|
104
|
+
from modelfill import fill
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class Interval:
|
|
109
|
+
start: int
|
|
110
|
+
end: int
|
|
111
|
+
duration: int
|
|
112
|
+
|
|
113
|
+
def __post_init__(self):
|
|
114
|
+
if self.duration < 1 or self.end != self.start + self.duration:
|
|
115
|
+
raise ValueError("Invalid interval")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
assert fill(Interval, end=18, duration=4).start == 14
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Dates, datetimes, durations and optional fields work the same way:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from datetime import datetime, timedelta
|
|
125
|
+
from pydantic import BaseModel, model_validator
|
|
126
|
+
from modelfill import fill
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Booking(BaseModel):
|
|
130
|
+
start: datetime
|
|
131
|
+
end: datetime
|
|
132
|
+
length: timedelta
|
|
133
|
+
note: str | None = None
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def check(self):
|
|
137
|
+
if self.end != self.start + self.length:
|
|
138
|
+
raise ValueError("end must equal start + length")
|
|
139
|
+
if not timedelta(minutes=30) <= self.length <= timedelta(hours=4):
|
|
140
|
+
raise ValueError("bookings last from 30 minutes to 4 hours")
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
booking = fill(Booking, end=datetime(2025, 3, 1, 18), length=timedelta(hours=2))
|
|
145
|
+
assert booking.start == datetime(2025, 3, 1, 16)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## API and input semantics
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
fill(Model, **fixed_fields)
|
|
152
|
+
ModelFiller(Model, *, mode="auto", seed=None, max_attempts=100, timeout_ms=1000)
|
|
153
|
+
filler.fill(**fixed_fields)
|
|
154
|
+
filler.batch(count, **fixed_fields)
|
|
155
|
+
filler.explain() # fresh JSON-friendly dictionary
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- All `fill()` keywords are **canonical model field names**, even if a model uses
|
|
159
|
+
input aliases. Configuration lives on `ModelFiller`; fields named `mode`,
|
|
160
|
+
`seed`, `model`, `count`, or `self` work.
|
|
161
|
+
- Fixed inputs are exact Python `int`, `bool`, `str`, `date`, naive `datetime`,
|
|
162
|
+
`timedelta`, Enum members, allowed Literal values, or `None` for optional
|
|
163
|
+
fields. There is no coercion: `"10"` or `True` for an `int` field, or an aware
|
|
164
|
+
datetime, raises `TypeError`. Unknown field names raise `ValueError`.
|
|
165
|
+
- Fields of any other type (UUIDs, floats, lists, nested models, …) are
|
|
166
|
+
**external**: modelfill passes your value to the constructor unchanged, or
|
|
167
|
+
leaves the field to the model's default. A required external field without a
|
|
168
|
+
value raises `UnsupportedConstraint`. Validators that read external fields are
|
|
169
|
+
unsupported logic.
|
|
170
|
+
- Explicit values are hard constraints. A returned object's fixed fields must
|
|
171
|
+
retain both their type and value; validator normalization cannot silently
|
|
172
|
+
replace them.
|
|
173
|
+
- Defaults are **preferences**, used when compatible with the solved constraints.
|
|
174
|
+
A missing field's zero-argument default factory runs once per fill call.
|
|
175
|
+
Standard defaults are copied. A conflicting default can be replaced.
|
|
176
|
+
- All generated fields, including selected defaults, are passed explicitly to
|
|
177
|
+
the original constructor and validated. This is stronger than Pydantic's
|
|
178
|
+
default behavior of leaving omitted defaults unchecked, and modelfill does not
|
|
179
|
+
search by omitting fields. A default Pydantic would leave unchecked can
|
|
180
|
+
therefore make a contradiction unprovable: while such a field is omitted, a
|
|
181
|
+
contradiction is reported as `GenerationExhausted`, never UNSAT.
|
|
182
|
+
`explain()["unsat_requires_explicit_fields"]` lists these fields; passing them
|
|
183
|
+
explicitly, or enabling Pydantic's `validate_default`, makes UNSAT provable.
|
|
184
|
+
- Dataclasses run their normal `__init__` and `__post_init__`. They do not gain
|
|
185
|
+
Pydantic validation; supported type domains are enforced by modelfill.
|
|
186
|
+
- Every returned object has successfully completed its original construction
|
|
187
|
+
pipeline, has canonical field types, and preserves fixed values. Neither
|
|
188
|
+
`model_construct()` nor any other validation bypass is used.
|
|
189
|
+
|
|
190
|
+
`batch()` returns a list, permits duplicates, and raises on the first failed fill.
|
|
191
|
+
It is a convenience loop over the reusable filler.
|
|
192
|
+
|
|
193
|
+
## Supported features
|
|
194
|
+
|
|
195
|
+
| Area | Support |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| Pydantic | v2 `BaseModel`, inherited validators, scalar fields |
|
|
198
|
+
| Dataclasses | Standard dataclasses, including frozen and slotted classes; pure `__post_init__` |
|
|
199
|
+
| Types | Exact `int`, `bool`, `str`, `date`, naive `datetime`, `timedelta`; ordinary `Enum`, `IntEnum`, `StrEnum`; homogeneous `Literal` subsets; any of these `\| None` (`Optional`, or `None` inside a `Literal`); other types as external fields |
|
|
200
|
+
| Numeric metadata | Integer, date, datetime and timedelta `Field(gt, ge, lt, le)`, respecting strict and inclusive bounds; integer `Field(multiple_of)`; on optional fields they constrain non-None values, as in Pydantic |
|
|
201
|
+
| String metadata | `Field(min_length, max_length)` and global `str_min_length` / `str_max_length` |
|
|
202
|
+
| Model validators | Instance `@model_validator(mode="after")`, returning the original `self` on successful paths |
|
|
203
|
+
| Field validators | `@field_validator(mode="after")`, returning the original value, including literal `info.data["earlier_field"]` reads |
|
|
204
|
+
| Aliases | Simple, distinct string input/validation aliases; API still uses field names |
|
|
205
|
+
| Defaults | Copied values and zero-argument factories as preferences |
|
|
206
|
+
| Fixed fields | Any subset, solved simultaneously; no left-to-right dependency order required |
|
|
207
|
+
|
|
208
|
+
Pydantic field metadata is read through public `model_fields`. Validator
|
|
209
|
+
discovery uses Pydantic's private `__pydantic_decorators__` registry; CI tests it
|
|
210
|
+
against the oldest supported Pydantic release. Dataclass `Annotated` metadata is
|
|
211
|
+
not a validation rule unless the dataclass's own code enforces it, so modelfill
|
|
212
|
+
does not impose it.
|
|
213
|
+
|
|
214
|
+
### Exact recognized validator language
|
|
215
|
+
|
|
216
|
+
| Construct | Accepted forms |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| Statements | `if` / `elif` / `else`, `pass`, string docstrings, `assert`, supported `raise`, supported `return`, single-name local assignments (`x = …`, `x: T = …`) outside branches, other than to validator parameters |
|
|
219
|
+
| Rejection | `raise ValueError` with any message, including f-strings (messages only run on paths that already reject), without a `from` clause; a failing `assert`, which `python -O` removes as usual |
|
|
220
|
+
| Successful return | Original `self` for model validators, original value for field validators; bare return, `return None`, or fallthrough for dataclasses |
|
|
221
|
+
| References | `self.field`; field validator value parameter; `info.data["earlier_field"]`; previously assigned locals; named constants and standard Enum members from globals/closures |
|
|
222
|
+
| Constants | Integer, Boolean, string, `None`; `date(…)`, `datetime(…)` and `timedelta(…)` calls with integer literal arguments |
|
|
223
|
+
| Arithmetic | Integer/Boolean `+`, `-`, unary `+`/`-`; multiplication with an integer literal operand; `%` and `//` by a positive integer literal; Python's date rules: `datetime ± timedelta`, `datetime - datetime`, `date ± timedelta` (whole days, like `timedelta.days`), `date - date`, `timedelta ± timedelta`, `timedelta * int literal`, `-timedelta` |
|
|
224
|
+
| Comparisons | `==`, `!=`; `<`, `<=`, `>`, `>=` between numbers or two values of one date/time type; chained comparisons; identity checks only against `None` |
|
|
225
|
+
| `None` | `x is None`, `x is not None`, `x == None`, truthiness, `x or default`; an operation that raises `TypeError` on `None` (`x < y`, `x + 1`, `len(x)`) rejects that path, so guard it as Python code does: `x is not None and x < y` |
|
|
226
|
+
| Boolean operations | `and`, `or`, `not`, with Python truthiness and short circuit meaning; operand-valued `and`/`or` when operands have the same supported type and cannot be `None` (`x or default` excepted) |
|
|
227
|
+
| Membership | `in` / `not in` a literal tuple, list or set of supported constants or Enum members, outside comparison chains |
|
|
228
|
+
| Strings | Equality, inequality, truthiness, membership in finite literal collections, builtin `len(field)` |
|
|
229
|
+
| Enums | Equality/inequality within the same Enum type, finite membership, exact member truthiness |
|
|
230
|
+
|
|
231
|
+
Early returns and nested branches are analyzed as paths. Rejection conditions
|
|
232
|
+
are not blindly negated. The entire relevant validator must be understood;
|
|
233
|
+
partially translating it would risk false claims of impossibility. Operators
|
|
234
|
+
follow Python's parsed precedence, including operand-returning `and` and `or`.
|
|
235
|
+
|
|
236
|
+
### Unsupported features and fallback
|
|
237
|
+
|
|
238
|
+
Field types modelfill cannot generate (floats, containers, nested models, UUIDs,
|
|
239
|
+
aware datetimes, other unions, mixed-type Literals, Flags, custom Enum behavior,
|
|
240
|
+
…) become external fields, described above. Unsupported **constructor shapes**
|
|
241
|
+
fail explicitly: Pydantic dataclasses, dataclass `InitVar` / `init=False`
|
|
242
|
+
fields, and complex or duplicate aliases.
|
|
243
|
+
|
|
244
|
+
Validator code outside the [language above](#exact-recognized-validator-language)
|
|
245
|
+
is unsupported **validation logic**, for example loops, calls and string methods,
|
|
246
|
+
`try`, `with` and `match`, mutation, other exception classes and transformed
|
|
247
|
+
return values. So are async, generator and wrapped validators, validators
|
|
248
|
+
without retrievable source, and these Pydantic features: `before`, `plain` or
|
|
249
|
+
`wrap` validators; legacy validators; Annotated validation callbacks;
|
|
250
|
+
unrecognized field metadata such as patterns; transforming model config; custom
|
|
251
|
+
initialization, core schema, attribute hooks and a custom `model_post_init`
|
|
252
|
+
(private attributes are fine); and data-dependent default factories.
|
|
253
|
+
|
|
254
|
+
In **strict** mode, unsupported validation logic raises `UnsupportedConstraint`
|
|
255
|
+
when the filler is created. In **auto** mode it appears in
|
|
256
|
+
`explain()["unsupported"]`. If any model behavior is unsupported, auto mode
|
|
257
|
+
demotes the extracted validator predicates to guidance, since unknown code may
|
|
258
|
+
transform values before another validator runs. Some attempts prefer candidates
|
|
259
|
+
that satisfy that guidance, but it never rules a candidate out, so misleading
|
|
260
|
+
guidance cannot starve the search. Field constraints remain candidate bounds;
|
|
261
|
+
normal construction still runs every original validator. It tries at most
|
|
262
|
+
`max_attempts` candidates and never claims UNSAT in this fallback mode. This
|
|
263
|
+
bounded search can miss valid solutions, especially narrow conditions or
|
|
264
|
+
transformations.
|
|
265
|
+
|
|
266
|
+
Only `ValueError` (including Pydantic `ValidationError`) and `AssertionError`
|
|
267
|
+
from ordinary construction are treated as candidate rejection. Other Python
|
|
268
|
+
exceptions propagate so programming errors are visible. Default-factory
|
|
269
|
+
exceptions also propagate. Validation is not sandboxed: do not use side effects
|
|
270
|
+
or nondeterministic validators. Solver timeouts cannot interrupt Python code.
|
|
271
|
+
|
|
272
|
+
Z3 string values cover U+0000–U+2FFFF, including ordinary text and emoji. Fixed
|
|
273
|
+
strings outside this range are explicitly unsupported. Such literals in a
|
|
274
|
+
validator cause strict rejection or automatic fallback; such defaults are simply
|
|
275
|
+
not preferred, and a `Literal` with such a choice makes its field external.
|
|
276
|
+
Arbitrary Python string behavior is not modeled.
|
|
277
|
+
|
|
278
|
+
## Failure categories
|
|
279
|
+
|
|
280
|
+
| Exception | Meaning |
|
|
281
|
+
| --- | --- |
|
|
282
|
+
| `UnsatisfiableConstraints` | A clean solver check proves the supported constraints plus canonical fixed assignments contradictory. No random preferences, blocked candidates or artificial bounds contribute to this result. When the solver isolates the conflict, the message names the fixed inputs, field constraints and validators involved, such as `start=17, duration=4` against `end <= 18` and `Meeting.check`. |
|
|
283
|
+
| `UnsupportedConstraint` | Unsupported constructor shape, a required external field without a value, or unsupported validation in strict mode. |
|
|
284
|
+
| `GenerationExhausted` | The bounded search did not find a normally validated object; also used for contradictions when unknown behavior or omitted unchecked defaults prevent an impossibility claim. |
|
|
285
|
+
| `SolverTimeout` | A solver query exhausted its time budget. Subclass of `SolverUnknown`. |
|
|
286
|
+
| `SolverUnknown` | Z3 could not decide SAT/UNSAT for another reason. |
|
|
287
|
+
|
|
288
|
+
These exceptions inherit from `ModelFillError`. Incorrect API arguments raise
|
|
289
|
+
`TypeError` or `ValueError`. `timeout_ms` is a **per-query** budget; a fill can make
|
|
290
|
+
several solver calls per field and attempt. This is not a wall-clock deadline.
|
|
291
|
+
|
|
292
|
+
Diagnostics include field domains, defaults, each validator's extraction status,
|
|
293
|
+
unsupported reasons, whether extracted predicates are active, sampling settings
|
|
294
|
+
and proof eligibility. In a fallback model, successfully extracted validators
|
|
295
|
+
report the status `guidance` rather than `modeled`.
|
|
296
|
+
|
|
297
|
+
## Sampling and performance
|
|
298
|
+
|
|
299
|
+
The solver's hard domains have no arbitrary integer or string-length bounds.
|
|
300
|
+
Sampling tries seeded soft preferences and discards infeasible ones. Integer,
|
|
301
|
+
date and time values spread across each field's feasible range and often sit on
|
|
302
|
+
its bounds, where tests usually find bugs; a field the solver cannot bound
|
|
303
|
+
samples near typical values instead. Assignments are varied but **nonuniform**,
|
|
304
|
+
with duplicates allowed; for distribution control and shrinking, use the
|
|
305
|
+
[Hypothesis strategy](#hypothesis).
|
|
306
|
+
|
|
307
|
+
The same seed, model, dependency versions and serial per-thread call order give
|
|
308
|
+
repeatable sequences. Cross-version or cross-thread ordering is not guaranteed.
|
|
309
|
+
Each thread owns a separate Z3 context, solver and RNG; immutable compiled
|
|
310
|
+
information is shared. Separate threads with the same seed may repeat sequences.
|
|
311
|
+
Compiled models and the convenience `fill()` fillers live in bounded caches.
|
|
312
|
+
Treat model definitions and the Enum classes and constants their validators read
|
|
313
|
+
as immutable after first compilation; restart the process after monkeypatching
|
|
314
|
+
or rebuilding a model.
|
|
315
|
+
|
|
316
|
+
```sh
|
|
317
|
+
uv run python -m benchmarks.benchmark --count 100 --repeat 5
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Measured results, the design and comparisons with Polyfactory and CrossHair are
|
|
321
|
+
in [the design document](https://github.com/jplauri/modelfill/blob/master/docs/architecture.md).
|
|
322
|
+
|
|
323
|
+
## Hypothesis
|
|
324
|
+
|
|
325
|
+
`modelfill.hypothesis.from_model` is a Hypothesis strategy. Pass strategies for
|
|
326
|
+
the fields you want Hypothesis to drive; required external fields default to
|
|
327
|
+
`st.from_type`, and modelfill solves the rest from the validators. To set
|
|
328
|
+
`mode`, `max_attempts` or `timeout_ms`, pass a configured `ModelFiller` in place
|
|
329
|
+
of the class, so options never collide with field names:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from datetime import datetime, timedelta
|
|
333
|
+
from uuid import UUID
|
|
334
|
+
|
|
335
|
+
from hypothesis import given, strategies as st
|
|
336
|
+
from pydantic import BaseModel, model_validator
|
|
337
|
+
from modelfill.hypothesis import from_model
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class Shift(BaseModel):
|
|
341
|
+
id: UUID # External: drawn with st.from_type(UUID).
|
|
342
|
+
start: datetime
|
|
343
|
+
end: datetime
|
|
344
|
+
break_minutes: int
|
|
345
|
+
|
|
346
|
+
@model_validator(mode="after")
|
|
347
|
+
def check(self):
|
|
348
|
+
if not self.start < self.end <= self.start + timedelta(hours=10):
|
|
349
|
+
raise ValueError("shifts last up to ten hours")
|
|
350
|
+
if not 0 <= self.break_minutes <= 60:
|
|
351
|
+
raise ValueError("breaks last up to an hour")
|
|
352
|
+
return self
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@given(from_model(Shift, start=st.datetimes(), break_minutes=st.integers(-30, 90)))
|
|
356
|
+
def test_shift_rules(shift):
|
|
357
|
+
assert shift.end > shift.start
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Draws that make the rules impossible (here `break_minutes` outside 0–60) are
|
|
361
|
+
rejected rather than failing the test, and cross-field rules never need
|
|
362
|
+
`.filter()`. Each example uses a fresh solver seeded from the draw, so failing
|
|
363
|
+
examples replay and the drawn fields shrink as usual.
|
|
364
|
+
|
|
365
|
+
## Development
|
|
366
|
+
|
|
367
|
+
```sh
|
|
368
|
+
uv sync
|
|
369
|
+
uv run pytest
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
[CI](https://github.com/jplauri/modelfill/blob/master/.github/workflows/ci.yml) runs
|
|
373
|
+
the full set of checks.
|
|
374
|
+
|
|
375
|
+
MIT licensed. See [architecture and tradeoffs](https://github.com/jplauri/modelfill/blob/master/docs/architecture.md).
|