Skip to content

Commit 829c575

Browse files
committed
rewrite
1 parent 81c0369 commit 829c575

5 files changed

Lines changed: 468 additions & 5 deletions

File tree

python/exacting/exacting.pyi

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
from typing import Any
22

3-
43
def json_to_py(json: str) -> Any:
54
"""Convert raw JSON to Python data types.
65
76
Args:
87
json (str): The JSON data.
98
"""
109

11-
1210
def jsonc_to_py(json: str) -> Any:
1311
"""Convert raw JSON to Python data bytes while allowing comments,
1412
trailing commas, object keys without quotes, single quoted strings and more.
@@ -20,7 +18,6 @@ def jsonc_to_py(json: str) -> Any:
2018
json (str): The JSONC data.
2119
"""
2220

23-
2421
class Regex:
2522
def __init__(self, m: str): ...
2623
def validate(self, input: str) -> bool:
@@ -33,10 +30,8 @@ class Regex:
3330
RuntimeError: Rust-side error.
3431
"""
3532

36-
3733
def py_to_bytes(data: Any) -> bytes:
3834
"""Convert Python data to bytes."""
3935

40-
4136
def bytes_to_py(data: bytes) -> Any:
4237
"""Convert bytes to Python data."""

python/exacting/types.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import dataclasses as std_dc
2+
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})"
63+
64+
65+
class Dataclass(Protocol):
66+
__dataclass_fields__: Dict[str, std_dc.Field]
67+
68+
69+
DataclassType = Union[Type[Dataclass], Any]
70+
71+
72+
class _Indexable:
73+
def __getitem__(self, k: str): ...
74+
def __setitem__(self, k: str, data: Any): ...
75+
def as_dict(self) -> dict: ...
76+
def as_dc(self) -> Dataclass: ...
77+
78+
79+
class _DefinitelyDict(_Indexable):
80+
def __init__(self, d: Dict):
81+
self.data = d
82+
83+
def __getitem__(self, k: str):
84+
self.data[k]
85+
86+
def __setitem__(self, k: str, data: Any):
87+
self.data[k] = data
88+
89+
def as_dict(self) -> dict:
90+
return self.data
91+
92+
def as_dc(self) -> Dataclass:
93+
raise TypeError("This indexable is not a dataclass but a dict")
94+
95+
96+
class _DefinitelyDataclass(_Indexable):
97+
def __init__(self, dc: Dataclass):
98+
self.dc = dc
99+
100+
def __getitem__(self, k: str):
101+
return getattr(self.dc, k)
102+
103+
def __setitem__(self, k: str, data: Any):
104+
setattr(self.dc, k, data)
105+
106+
def as_dict(self):
107+
raise TypeError("This indexable is not a dict but a dataclass")
108+
109+
def as_dc(self) -> Dataclass:
110+
return self.dc
111+
112+
113+
def indexable(item: Any) -> "_Indexable":
114+
if isinstance(item, dict):
115+
return _DefinitelyDict(item)
116+
else:
117+
return _DefinitelyDataclass(item)

python/exacting/validator_map.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import dataclasses
2+
from dataclasses import is_dataclass
3+
from types import NoneType, UnionType
4+
from typing import Annotated, Any, Dict, Literal, Union, get_origin
5+
from weakref import ref
6+
7+
from .validators import (
8+
AnnotatedV,
9+
AnyV,
10+
BoolV,
11+
BytesV,
12+
DataclassV,
13+
DictV,
14+
FloatV,
15+
IntV,
16+
ListV,
17+
LiteralV,
18+
NoneV,
19+
StrV,
20+
UnionV,
21+
Validator,
22+
)
23+
from .types import DataclassType
24+
25+
NONEV = NoneV()
26+
STRV = StrV()
27+
INTV = IntV()
28+
FLOATV = FloatV()
29+
BOOLV = BoolV()
30+
BYTESV = BytesV()
31+
ANYV = AnyV()
32+
33+
34+
def get_validator(typ: Any) -> Validator:
35+
if typ is None or isinstance(typ, NoneType) or typ is NoneType:
36+
return NONEV
37+
if typ is str:
38+
return STRV
39+
if typ is int:
40+
return INTV
41+
if typ is float:
42+
return FLOATV
43+
if typ is bool:
44+
return BOOLV
45+
if typ is bytes:
46+
return BYTESV
47+
if typ is Any:
48+
return ANYV
49+
50+
if is_dataclass(typ):
51+
return get_dc_validator(typ)
52+
53+
origin = get_origin(typ)
54+
if origin is list:
55+
return ListV(get_validator(typ.__args__[0]))
56+
if origin is dict:
57+
return DictV(get_validator(typ.__args__[0]), get_validator(typ.__args__[1]))
58+
if origin is Union or origin is UnionType:
59+
return union(*typ.__args__)
60+
if origin is Literal:
61+
return LiteralV(typ.__args__)
62+
if origin is Annotated:
63+
return AnnotatedV(typ.__args__[0], typ.__metadata__)
64+
65+
raise TypeError(
66+
f"Unknown type: {typ!r} (no type validator available at this moment)"
67+
)
68+
69+
70+
def union(*items) -> Validator:
71+
if len(items) == 1:
72+
return get_validator(items[0])
73+
return UnionV(get_validator(items[0]), union(*items[1:]))
74+
75+
76+
def get_map_for_dc(dc: DataclassType) -> Dict[str, Validator]:
77+
vmap = {}
78+
_FIELD = getattr(dataclasses, "_FIELD")
79+
80+
for field in dc.__dataclass_fields__.values():
81+
if getattr(field, "_field_type") is not _FIELD:
82+
raise NotImplementedError(
83+
"Currently, exacting only supports regular fields :(\n"
84+
f"...at field {field.name!r}, dataclass {dc!r}"
85+
)
86+
vmap[field.name] = get_validator(field.type)
87+
88+
return vmap
89+
90+
91+
def get_dc_validator(dc: DataclassType) -> DataclassV:
92+
return DataclassV(ref(dc), get_map_for_dc(dc))

0 commit comments

Comments
 (0)