Skip to content

Commit 594949d

Browse files
committed
perf
1 parent 833d7d5 commit 594949d

File tree

7 files changed

+114
-97
lines changed

7 files changed

+114
-97
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Essentially... **THE** go-to option for dataclasses. heh.
99
**🔑 Key features**:
1010

1111
- **100% static typing.** Because I hate nothing too.
12-
- Up to **10x faster** than [`pydantic`](https://pydantic.dev)! (Them: 60ms, us: 6~9ms)
12+
- Generally **faster** than [`pydantic`](https://pydantic.dev)!
1313

1414
![i aint lying about static typing](https://github.com/user-attachments/assets/875517ff-5dd5-4b63-98fa-e1218ff00627)
1515

@@ -144,5 +144,6 @@ Woah! That's a lot of code to process. To put it simply, exacting supports:
144144

145145
![praise pydantic, exactign sucks](https://github.com/user-attachments/assets/5969c54a-14d0-4023-9f80-b89ae9ea8374)
146146

147-
Kind of, but somehow native performance is way better than Rust. Take a look at this.
147+
Kind of, but somehow native performance is way better than Rust. That is, exacting is *generally* faster than Pydantic on a few benchmarks. Woooosh
148148

149+
Anyway, thanks for hopping onto this quick tour, you can [read the docs](https://aweirddev.github.io/exacting) if you'd like.

_setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
name="python/exacting/",
66
ext_modules=cythonize(
77
[
8+
"python/exacting/result.py",
89
]
910
),
1011
package_dir={"": "python"},

python/exacting/core.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
from dataclasses import MISSING, asdict, dataclass
33

4-
from typing import Any, Dict, Type
4+
from typing import TYPE_CHECKING, Any, Dict, Type
55
from typing_extensions import Self, dataclass_transform
66

77
from .validators import DataclassV, Validator
@@ -21,7 +21,7 @@ def init(self, **kwargs):
2121
value = get_field_value(kwargs.get(field.name, MISSING), field)
2222
setattr(self, field.name, value)
2323

24-
validator: Validator = getattr(dc, "__validator__")
24+
validator: Validator = self.__validator__
2525
res: Result = validator.validate(self)
2626
res.raise_for_err()
2727

@@ -60,34 +60,44 @@ def exact(cls: Type) -> DataclassType:
6060
return dc
6161

6262

63-
class _Internals:
64-
__validator__: DataclassV
63+
if TYPE_CHECKING:
6564

66-
@classmethod
67-
def __unsafe_init__(cls, **kwargs) -> Self:
68-
"""Unsafely initialize the dataclass with no-brain filling.
65+
class _Internals:
66+
__validator__: DataclassV
6967

70-
Example:
68+
@classmethod
69+
def __unsafe_init__(cls, **kwargs) -> Self:
70+
"""Unsafely initialize the dataclass with no-brain filling.
7171
72-
```python
73-
from exacting import unsafe
72+
Example:
7473
75-
with unsafe():
76-
SomeDataclass.__unsafe_init__(**kwargs)
77-
```
78-
"""
79-
raise NotImplementedError()
74+
```python
75+
from exacting import unsafe
8076
77+
with unsafe():
78+
SomeDataclass.__unsafe_init__(**kwargs)
79+
```
80+
"""
81+
raise NotImplementedError()
8182

82-
@dataclass_transform(kw_only_default=True)
83-
class _Dc: ...
83+
@dataclass_transform(kw_only_default=True)
84+
class _Dc: ...
85+
86+
else:
87+
88+
class _Dc: ...
89+
90+
class _Internals: ...
8491

8592

8693
class Exact(_Dc, _Internals):
8794
"""Represents a dataclass with runtime type checks."""
8895

89-
def __init_subclass__(cls) -> None:
90-
exact(cls)
96+
def __init__(self, **kwargs):
97+
get_exact_init(self)(self, **kwargs)
98+
99+
def __init_subclass__(cls):
100+
dataclass(cls)
91101

92102
def exact_as_dict(self) -> Dict[str, Any]:
93103
"""Get this model instance as a dictionary."""

python/exacting/result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
T = TypeVar("T")
77

88

9-
def build_error(errors: deque[str]) -> str:
9+
def build_error(errors: "deque[str]") -> str:
1010
text = "\n"
1111
indent_level = 0
1212

python/exacting/validators.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ def validate(self, value: Any, **options) -> Result:
260260

261261
ef = field.metadata.get("exact")
262262
if ef:
263-
validator_items: List[Validator] = ef.validators
263+
# validator_items: List[Validator]
264+
validator_items = ef.validators
264265
for item in validator_items:
265266
fv_res = item.validate(field_value)
266267
if not fv_res.is_ok():

val_exacting.py

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,3 @@
66

77
def gen_str(length: int) -> str:
88
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: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,96 @@ def gen_str(length: int) -> str:
99
return "".join(random.choices(string.ascii_letters, k=length))
1010

1111

12-
class Meta(BaseModel):
12+
class MetaP(BaseModel):
1313
liked: bool
1414
flags: List[str]
1515

1616

17-
class Reply(BaseModel):
17+
class ReplyP(BaseModel):
1818
user: str = Field()
1919
content: str
20-
metadata: Meta
20+
metadata: MetaP
2121

2222

23-
class Comment(BaseModel):
23+
class CommentP(BaseModel):
2424
user: str = Field()
2525
stars: int = Field(..., ge=1, le=5)
2626
body: Optional[str]
27-
replies: List[Reply]
27+
replies: List[ReplyP]
28+
29+
30+
class PlaceP(BaseModel):
31+
name: str
32+
location: str
33+
comments: List[CommentP]
34+
metadata: MetaP
35+
36+
37+
def gen_reply() -> ReplyP:
38+
return ReplyP(
39+
user="@" + gen_str(5),
40+
content=gen_str(20),
41+
metadata=MetaP(
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() -> CommentP:
49+
return CommentP(
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+
2856

57+
def gen_place() -> PlaceP:
58+
return PlaceP(
59+
name=gen_str(10),
60+
location=gen_str(20),
61+
comments=[gen_comment() for _ in range(100)],
62+
metadata=MetaP(liked=True, flags=["safe"]),
63+
)
64+
65+
66+
start = time.perf_counter()
67+
for i in range(10):
68+
gen_place()
69+
end = time.perf_counter()
70+
71+
print(f"{(end - start) * 1000} ms")
72+
73+
from exacting import Exact, field
2974

30-
class Place(BaseModel):
75+
76+
class Meta(Exact):
77+
liked: bool = field()
78+
flags: list[str] = field()
79+
80+
81+
class Reply(Exact):
82+
user: str = field()
83+
content: str = field()
84+
metadata: Meta
85+
86+
87+
class Comment(Exact):
88+
user: str = field()
89+
stars: int = field(minv=1, maxv=5)
90+
body: str | None
91+
replies: list[Reply]
92+
93+
94+
class Place(Exact):
3195
name: str
3296
location: str
33-
comments: List[Comment]
97+
comments: list[Comment]
3498
metadata: Meta
3599

36100

37-
def gen_reply() -> Reply:
101+
def gen_reply2():
38102
return Reply(
39103
user="@" + gen_str(5),
40104
content=gen_str(20),
@@ -45,26 +109,27 @@ def gen_reply() -> Reply:
45109
)
46110

47111

48-
def gen_comment() -> Comment:
112+
def gen_comment2():
49113
return Comment(
50114
user="@" + gen_str(5),
51115
stars=random.randint(1, 5),
52116
body=gen_str(40),
53-
replies=[gen_reply() for _ in range(random.randint(1, 3))],
117+
replies=[gen_reply2() for _ in range(random.randint(1, 3))],
54118
)
55119

56120

57-
def gen_place() -> Place:
121+
def gen_place2():
58122
return Place(
59123
name=gen_str(10),
60124
location=gen_str(20),
61-
comments=[gen_comment() for _ in range(100)],
125+
comments=[gen_comment2() for _ in range(100)],
62126
metadata=Meta(liked=True, flags=["safe"]),
63127
)
64128

65129

66130
start = time.perf_counter()
67-
place = gen_place()
131+
for i in range(10):
132+
gen_place2()
68133
end = time.perf_counter()
69134

70135
print(f"{(end - start) * 1000} ms")

0 commit comments

Comments
 (0)