Skip to content

Commit a99f4be

Browse files
committed
2468 WHO DO WE APPRECIATE THAT'S BBNO$ HE'S ALWAYS UP TO SOEMTHIGN
1 parent 829c575 commit a99f4be

File tree

8 files changed

+354
-75
lines changed

8 files changed

+354
-75
lines changed

python/exacting/__init__.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from .core import Exact, exact
2+
from .types import ValidationError
3+
from .validators import (
4+
AnnotatedV,
5+
AnyV,
6+
BoolV,
7+
BytesV,
8+
DataclassV,
9+
DictV,
10+
FloatV,
11+
IntV,
12+
ListV,
13+
LiteralV,
14+
LooseDictV,
15+
LooseListV,
16+
NoneV,
17+
StrV,
18+
UnionV,
19+
Validator,
20+
)
21+
from .utils import unsafe
22+
23+
__all__ = [
24+
"Exact",
25+
"exact",
26+
"ValidationError",
27+
"AnnotatedV",
28+
"AnyV",
29+
"BoolV",
30+
"BytesV",
31+
"DataclassV",
32+
"DictV",
33+
"FloatV",
34+
"IntV",
35+
"ListV",
36+
"LiteralV",
37+
"LooseDictV",
38+
"LooseListV",
39+
"NoneV",
40+
"StrV",
41+
"UnionV",
42+
"Validator",
43+
"unsafe"
44+
]

python/exacting/core.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import json
2+
from dataclasses import MISSING, asdict, dataclass
3+
4+
from typing import Any, Dict, Type
5+
from typing_extensions import Self, dataclass_transform
6+
7+
from .validators import DataclassV, Validator
8+
from .validator_map import get_dc_validator
9+
from .types import DataclassType
10+
from .result import Result
11+
from .utils import get_field_value, unsafe_mode
12+
13+
from .exacting import py_to_bytes
14+
15+
16+
def get_exact_init(dc: DataclassType):
17+
setattr(dc, "__validator__", get_dc_validator(dc))
18+
19+
def init(self, **kwargs):
20+
for field in self.__dataclass_fields__.values():
21+
value = get_field_value(kwargs.get(field.name, MISSING), field)
22+
setattr(self, field.name, value)
23+
24+
validator: Validator = getattr(dc, "__validator__")
25+
res: Result = validator.validate(self)
26+
res.raise_for_err()
27+
28+
return None
29+
30+
return init
31+
32+
33+
def get_unsafe_init():
34+
@classmethod
35+
def __unsafe_init__(cls, **kwargs):
36+
if not unsafe_mode.get():
37+
raise RuntimeError("Scope is not in unsafe(), canceled operation")
38+
39+
item = cls.__new__(cls)
40+
for field in cls.__dataclass_fields__.values():
41+
setattr(
42+
item,
43+
field.name,
44+
get_field_value(kwargs.get(field.name, MISSING), field),
45+
)
46+
return item
47+
48+
return __unsafe_init__
49+
50+
51+
@dataclass_transform(kw_only_default=True)
52+
def exact(cls: Type) -> DataclassType:
53+
dc = dataclass(kw_only=True)(cls)
54+
unsafe_init = get_unsafe_init()
55+
setattr(dc, "__unsafe_init__", unsafe_init)
56+
57+
exact_init = get_exact_init(dc)
58+
setattr(dc, "__init__", exact_init)
59+
60+
return dc
61+
62+
63+
class _Internals:
64+
__validator__: DataclassV
65+
66+
@classmethod
67+
def __unsafe_init__(cls, **kwargs) -> Self:
68+
"""Unsafely initialize the dataclass with no-brain filling.
69+
70+
Example:
71+
72+
```python
73+
from exacting import unsafe
74+
75+
with unsafe():
76+
SomeDataclass.__unsafe_init__(**kwargs)
77+
```
78+
"""
79+
raise NotImplementedError()
80+
81+
82+
@dataclass_transform(kw_only_default=True)
83+
class _Dc: ...
84+
85+
86+
class Exact(_Dc, _Internals):
87+
def __init_subclass__(cls) -> None:
88+
exact(cls)
89+
90+
def exact_as_dict(self) -> Dict[str, Any]:
91+
"""Get this model instance as dictionary."""
92+
return asdict(self)
93+
94+
def exact_as_json(self) -> str:
95+
"""Get this model instance as JSON."""
96+
return json.dumps(self.exact_as_dict())
97+
98+
def exact_as_bytes(self) -> bytes:
99+
"""Get this model instance as bytes with `rkyv`."""
100+
return py_to_bytes(self.exact_as_dict())
101+
102+
@classmethod
103+
def exact_from_dict(cls, d: Dict[str, Any]) -> Self:
104+
res = cls.__validator__.validate(d, from_dict=True)
105+
res.raise_for_err()
106+
return res.unwrap()

python/exacting/result.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from collections import deque
2+
from typing import Generic, Optional, TypeVar
3+
4+
from .types import ValidationError
5+
6+
T = TypeVar("T")
7+
8+
9+
def build_error(errors: deque[str]) -> str:
10+
text = "\n"
11+
indent_level = 0
12+
13+
# drain contents
14+
while errors:
15+
error = errors.popleft()
16+
if error == "indent":
17+
indent_level += 1
18+
continue
19+
elif error == "unindent":
20+
indent_level -= 1
21+
continue
22+
23+
if indent_level:
24+
text += f"{' ' * indent_level}{error}\n"
25+
else:
26+
text += f"{error}\n"
27+
28+
return text.rstrip()
29+
30+
31+
class Result(Generic[T]):
32+
"""Represents a result."""
33+
34+
ok_data: Optional[T]
35+
errors: Optional["deque[str]"] # deque is O(1)
36+
37+
def __init__(self, okd: Optional[T], errors: Optional["deque[str]"]):
38+
self.ok_data = okd
39+
self.errors = errors
40+
41+
@classmethod
42+
def Ok(cls, data: T) -> "Result[T]":
43+
return cls(data, None)
44+
45+
@classmethod
46+
def Err(cls, *errors: str) -> "Result[T]":
47+
return cls(None, deque(errors))
48+
49+
def unwrap(self) -> T:
50+
"""Unwrap the OK data."""
51+
# cheap operation lmfao
52+
return self.ok_data # type: ignore
53+
54+
def unwrap_err(self) -> "deque[str]":
55+
"""Unwrap the Err data."""
56+
# AGAIN. lmfao! you gotta be responsible.
57+
return self.errors # type: ignore
58+
59+
def is_ok(self) -> bool:
60+
"""CALL."""
61+
return not self.errors
62+
63+
def trace(self, upper: str) -> "Result[T]":
64+
if self.errors is not None:
65+
self.errors.appendleft("indent")
66+
self.errors.appendleft(upper)
67+
self.errors.append("unindent")
68+
69+
return self
70+
71+
@classmethod
72+
def trace_below(cls, upper: str, *items: str) -> "Result[T]":
73+
errors = deque(items)
74+
errors.appendleft("indent")
75+
errors.appendleft(upper)
76+
errors.append("unindent")
77+
78+
return cls(okd=None, errors=errors)
79+
80+
def raise_for_err(self) -> None:
81+
if self.is_ok():
82+
return
83+
84+
error = build_error(self.unwrap_err())
85+
raise ValidationError(error)
86+
87+
def __repr__(self) -> str:
88+
if self.is_ok():
89+
return f"Result.Ok({self.unwrap()!r})"
90+
else:
91+
return f"Result.Err({self.unwrap_err()!r})"

python/exacting/types.py

Lines changed: 13 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,6 @@
11
import dataclasses as std_dc
22

3-
from collections import deque
4-
from typing import Any, Dict, Generic, Optional, Protocol, Type, TypeVar, Union
5-
6-
T = TypeVar("T")
7-
8-
9-
class Result(Generic[T]):
10-
"""Represents a result."""
11-
12-
ok_data: Optional[T]
13-
errors: Optional["deque[str]"] # O(1)
14-
15-
def __init__(self, okd: Optional[T], errors: Optional["deque[str]"]):
16-
self.ok_data = okd
17-
self.errors = errors
18-
19-
@classmethod
20-
def Ok(cls, data: T) -> "Result[T]":
21-
return cls(data, None)
22-
23-
@classmethod
24-
def Err(cls, *errors: str) -> "Result[T]":
25-
return cls(None, deque(errors))
26-
27-
def unwrap(self) -> T:
28-
"""Unwrap the OK data."""
29-
# cheap operation lmfao
30-
return self.ok_data # type: ignore
31-
32-
def unwrap_err(self) -> "deque[str]":
33-
"""Unwrap the Err data."""
34-
# AGAIN. lmfao! you gotta be responsible.
35-
return self.errors # type: ignore
36-
37-
def is_ok(self) -> bool:
38-
"""CALL."""
39-
return not self.errors
40-
41-
def trace(self, upper: str) -> "Result[T]":
42-
if self.errors is not None:
43-
self.errors.appendleft("indent")
44-
self.errors.appendleft(upper)
45-
self.errors.append("unindent")
46-
47-
return self
48-
49-
@classmethod
50-
def trace_below(cls, upper: str, *items: str) -> "Result[T]":
51-
errors = deque(items)
52-
errors.appendleft("indent")
53-
errors.appendleft(upper)
54-
errors.append("unindent")
55-
56-
return cls(okd=None, errors=errors)
57-
58-
def __repr__(self) -> str:
59-
if self.is_ok():
60-
return f"Result.Ok({self.unwrap()!r})"
61-
else:
62-
return f"Result.Err({self.unwrap_err()!r})"
3+
from typing import Any, Dict, Protocol, Type, Union
634

645

656
class Dataclass(Protocol):
@@ -72,6 +13,7 @@ class Dataclass(Protocol):
7213
class _Indexable:
7314
def __getitem__(self, k: str): ...
7415
def __setitem__(self, k: str, data: Any): ...
16+
def get(self, k: str): ...
7517
def as_dict(self) -> dict: ...
7618
def as_dc(self) -> Dataclass: ...
7719

@@ -80,8 +22,11 @@ class _DefinitelyDict(_Indexable):
8022
def __init__(self, d: Dict):
8123
self.data = d
8224

25+
def get(self, k: str):
26+
return self.data.get(k, std_dc.MISSING)
27+
8328
def __getitem__(self, k: str):
84-
self.data[k]
29+
return self.data[k]
8530

8631
def __setitem__(self, k: str, data: Any):
8732
self.data[k] = data
@@ -97,6 +42,9 @@ class _DefinitelyDataclass(_Indexable):
9742
def __init__(self, dc: Dataclass):
9843
self.dc = dc
9944

45+
def get(self, k: str):
46+
return getattr(self.dc, k, std_dc.MISSING)
47+
10048
def __getitem__(self, k: str):
10149
return getattr(self.dc, k)
10250

@@ -115,3 +63,7 @@ def indexable(item: Any) -> "_Indexable":
11563
return _DefinitelyDict(item)
11664
else:
11765
return _DefinitelyDataclass(item)
66+
67+
68+
class ValidationError(RuntimeError):
69+
"""Validation error for `exacting`."""

python/exacting/utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from contextlib import contextmanager
2+
from contextvars import ContextVar
3+
from dataclasses import MISSING, Field, _MISSING_TYPE
4+
from typing import TypeVar, Union
5+
6+
unsafe_mode = ContextVar("unsafe_mode", default=False)
7+
8+
9+
@contextmanager
10+
def unsafe(on: bool = True, /):
11+
"""Enable unsafe mode for unsafe operations.
12+
13+
Example:
14+
```python
15+
with unsafe():
16+
... # do stuff
17+
```
18+
"""
19+
20+
token = unsafe_mode.set(on)
21+
try:
22+
yield
23+
finally:
24+
unsafe_mode.reset(token)
25+
26+
27+
T = TypeVar("T")
28+
29+
30+
def get_field_value(item: Union[T, _MISSING_TYPE], field: Field) -> T:
31+
if item is MISSING:
32+
if field.default is not MISSING:
33+
return field.default
34+
elif field.default_factory is not MISSING:
35+
return field.default_factory()
36+
else:
37+
raise KeyError(field.name)
38+
else:
39+
return item

0 commit comments

Comments
 (0)