Skip to content

Commit 833d7d5

Browse files
committed
lol im tired
1 parent fbeba55 commit 833d7d5

5 files changed

Lines changed: 184 additions & 1 deletion

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,44 @@ Normally, when you use the parameters passed in example (2) above, the Python `d
105105

106106
It's worth noting that error generations are *lazy*, which means once Exacting finds out about a problem about a dataclass, it raises a `ValidationError`. This saves a lot of computation time if you have a larger model.
107107

108+
![praise pydantic](https://github.com/user-attachments/assets/c322bff2-2624-479d-9967-7184580b36c1)
109+
110+
Yeah, we also got fields! Use it like how you'd expect it. Quite literally, lol.
111+
112+
```python
113+
from exacting import Exact, field
114+
115+
116+
def create_ai_slop():
117+
return "tick tock"
118+
119+
class Comment(Exact):
120+
user: str = field(regex="^@.+$")
121+
stars: int = field(minv=1, maxv=5)
122+
body: str = field(default_factory=create_ai_slop)
123+
124+
# ✅ OK, exacting is HYPED
125+
Comment(
126+
user="@waltuh",
127+
stars=5
128+
)
129+
130+
# ❌ Hell nawh, exacting holdin' you at gunpoint
131+
Comment(
132+
user="ooga booga", # Regex validation '^@.+$' on str failed
133+
stars=-1, # Expected min value of 1, got -1
134+
body=None # Expected type <class 'str'>, got <class 'NoneType'>
135+
)
136+
```
137+
138+
Woah! That's a lot of code to process. To put it simply, exacting supports:
139+
140+
- **Regex** validation on `str`
141+
- **Min/max** validation on value (e.g., `int`, `float`) or length (e.g., `str`, `list`)
142+
- **Default** values or factory! Y'know, the old `dataclasses` way, mmmMMM
143+
- This is extra, but **custom** validation! You can make your own if you like
144+
145+
![praise pydantic, exactign sucks](https://github.com/user-attachments/assets/5969c54a-14d0-4023-9f80-b89ae9ea8374)
146+
147+
Kind of, but somehow native performance is way better than Rust. Take a look at this.
108148

python/exacting/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ class _Dc: ...
8484

8585

8686
class Exact(_Dc, _Internals):
87+
"""Represents a dataclass with runtime type checks."""
88+
8789
def __init_subclass__(cls) -> None:
8890
exact(cls)
8991

python/exacting/validators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,9 @@ def validate(self, value: Any, **options) -> Result:
264264
for item in validator_items:
265265
fv_res = item.validate(field_value)
266266
if not fv_res.is_ok():
267-
return fv_res
267+
return fv_res.trace(
268+
f"During validation of dataclass {self!r} at field {name!r}, got:"
269+
)
268270
field_value = fv_res.unwrap()
269271

270272
data[name] = field_value

val_exacting.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import time
2+
import random
3+
import string
4+
from exacting import Exact, field
5+
6+
7+
def gen_str(length: int) -> str:
8+
return "".join(random.choices(string.ascii_letters, k=length))
9+
10+
11+
class Meta(Exact):
12+
liked: bool = field()
13+
flags: list[str] = field()
14+
15+
16+
class Reply(Exact):
17+
user: str = field()
18+
content: str = field()
19+
metadata: Meta
20+
21+
22+
class Comment(Exact):
23+
user: str = field()
24+
stars: int = field(minv=1, maxv=5)
25+
body: str | None
26+
replies: list[Reply]
27+
28+
29+
class Place(Exact):
30+
name: str
31+
location: str
32+
comments: list[Comment]
33+
metadata: Meta
34+
35+
36+
def gen_reply():
37+
return Reply(
38+
user="@" + gen_str(5),
39+
content=gen_str(20),
40+
metadata=Meta(
41+
liked=random.choice([True, False]),
42+
flags=[gen_str(3) for _ in range(random.randint(0, 3))],
43+
),
44+
)
45+
46+
47+
def gen_comment():
48+
return Comment(
49+
user="@" + gen_str(5),
50+
stars=random.randint(1, 5),
51+
body=gen_str(40),
52+
replies=[gen_reply() for _ in range(random.randint(1, 3))],
53+
)
54+
55+
56+
def gen_place():
57+
return Place(
58+
name=gen_str(10),
59+
location=gen_str(20),
60+
comments=[gen_comment() for _ in range(100)],
61+
metadata=Meta(liked=True, flags=["safe"]),
62+
)
63+
64+
65+
start = time.perf_counter()
66+
place = gen_place()
67+
end = time.perf_counter()
68+
69+
print(f"{(end - start) * 1000} ms")

val_pydantic.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import time
2+
import random
3+
import string
4+
from pydantic import BaseModel, Field
5+
from typing import List, Optional
6+
7+
8+
def gen_str(length: int) -> str:
9+
return "".join(random.choices(string.ascii_letters, k=length))
10+
11+
12+
class Meta(BaseModel):
13+
liked: bool
14+
flags: List[str]
15+
16+
17+
class Reply(BaseModel):
18+
user: str = Field()
19+
content: str
20+
metadata: Meta
21+
22+
23+
class Comment(BaseModel):
24+
user: str = Field()
25+
stars: int = Field(..., ge=1, le=5)
26+
body: Optional[str]
27+
replies: List[Reply]
28+
29+
30+
class Place(BaseModel):
31+
name: str
32+
location: str
33+
comments: List[Comment]
34+
metadata: Meta
35+
36+
37+
def gen_reply() -> Reply:
38+
return Reply(
39+
user="@" + gen_str(5),
40+
content=gen_str(20),
41+
metadata=Meta(
42+
liked=random.choice([True, False]),
43+
flags=[gen_str(3) for _ in range(random.randint(0, 3))],
44+
),
45+
)
46+
47+
48+
def gen_comment() -> Comment:
49+
return Comment(
50+
user="@" + gen_str(5),
51+
stars=random.randint(1, 5),
52+
body=gen_str(40),
53+
replies=[gen_reply() for _ in range(random.randint(1, 3))],
54+
)
55+
56+
57+
def gen_place() -> Place:
58+
return Place(
59+
name=gen_str(10),
60+
location=gen_str(20),
61+
comments=[gen_comment() for _ in range(100)],
62+
metadata=Meta(liked=True, flags=["safe"]),
63+
)
64+
65+
66+
start = time.perf_counter()
67+
place = gen_place()
68+
end = time.perf_counter()
69+
70+
print(f"{(end - start) * 1000} ms")

0 commit comments

Comments
 (0)