Skip to content

Commit

Permalink
aded "_" neg option to pyoload.Checks and corrected recursive annotat…
Browse files Browse the repository at this point in the history
…e_class, not to interfere with callable descriptor attributes
  • Loading branch information
ken-morel committed Jun 26, 2024
1 parent 7485ab4 commit a6ca2d2
Show file tree
Hide file tree
Showing 10 changed files with 3,394 additions and 3,951 deletions.
7,219 changes: 3,302 additions & 3,917 deletions pyoload.sublime-workspace

Large diffs are not rendered by default.

26 changes: 20 additions & 6 deletions src/build/lib/pyoload/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ def __init__(
:returns: self
"""
if __check_func__ is not None:
checks['func'] = __check_func__
checks["func"] = __check_func__
self.checks = checks

def __call__(self: PyoloadAnnotation, val: Any) -> None:
Expand Down Expand Up @@ -483,6 +483,9 @@ def cast(val: Any, totype: Any) -> Any:
elif len(args) == 1:
kt, vt = args[0], Any
return {Cast.cast(k, kt): Cast.cast(v, vt) for k, v in val.items()}
elif get_origin(totype) == tuple:
args = get_args(totype)
return tuple(Cast.cast(val, ann) for val, ann in zip(args, val))
else:
sub = args[0]
return get_origin(totype)([Cast.cast(v, sub) for v in val])
Expand Down Expand Up @@ -626,6 +629,13 @@ def type_match(val: Any, spec: Union[Type, PyoloadAnnotation]) -> tuple:
return (False, e)
else:
return (True, None)
elif orig == tuple:
args = get_args(spec)
vals = zip(args, val)
for val, ann in vals:
b, e = type_match(val, ann)
if not b:
return b, e
else:
sub = get_args(spec)[0]
for val in val:
Expand All @@ -644,7 +654,7 @@ def resove_annotations(obj: Callable) -> None:
:returns: None
"""
if not hasattr(obj, '__annotations__'):
if not hasattr(obj, "__annotations__"):
raise AnnotationResolutionError(
f"object {obj=!r} does not have `.__annotations__`",
)
Expand All @@ -666,7 +676,7 @@ def resove_annotations(obj: Callable) -> None:
obj.__annotations__[k] = eval(
v,
dict(vars(getmodule(obj))),
dict(vars(obj)) if hasattr(obj, '__dict__') else None,
dict(vars(obj)) if hasattr(obj, "__dict__") else None,
)
except Exception as e:
raise AnnotationResolutionError(
Expand Down Expand Up @@ -696,6 +706,8 @@ def annotate(
"""
if isinstance(func, bool):
return partial(annotate, force=True)
if not callable(func):
return func
if not hasattr(func, "__annotations__"):
return func
if is_annoted(func):
Expand Down Expand Up @@ -889,9 +901,7 @@ def annotate_class(cls: Any, recur: bool = True):
setattr(
cls,
x,
annotate(
vars(cls).get(x)
),
annotate(vars(cls).get(x)),
)

@wraps(cls.__setattr__)
Expand Down Expand Up @@ -921,6 +931,7 @@ def new_setter(self: Any, name: str, value: Any) -> Any:

__all__ = [
"annotate",
"annotate_class",
"overload",
"multimethod",
"Checks",
Expand All @@ -937,6 +948,9 @@ def new_setter(self: Any, name: str, value: Any) -> Any:
"Values",
"AnnotationResolutionError",
"AnnotationError",
"Type",
"Any",
"type_match",
]

__version__ = "2.0.1"
Expand Down
55 changes: 44 additions & 11 deletions src/pyoload/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def register(
"""
names = [x.strip() for x in name.split(" ") if x.strip() != ""]
for name in names:
if name in cls.checks_list:
if name.lstrip("_") in cls.checks_list:
raise Check.CheckNameAlreadyExistsError(name)

def inner(func: Callable) -> Callable:
Expand All @@ -233,18 +233,33 @@ def check(cls: Any, name: str, params: Any, val: Any) -> None:
:param cls: pyoload.Check class
:param name: One of the registerred name of the check
if preceded by an underscore, it will be negated
:param params: The parameters to pass to the check
:param val: The value to check
:returns: :py:`None`
"""
neg = False
if name.startswith("_"):
name = name[1:]
neg = True
check = cls.checks_list.get(name)
if check is None:
raise Check.CheckDoesNotExistError(name)
try:
check(params, val)
except (AssertionError, TypeError) as e:
raise Check.CheckError(e) from e
except (AssertionError, TypeError, ValueError) as e:
if not neg:
raise Check.CheckError(e) from e
except Check.CheckError:
if not neg:
raise
else:
if neg:
raise Check.CheckError(
f"check {name} did not fail on: {val!r}"
f" for params: {params!r}"
)

class CheckNameAlreadyExistsError(ValueError):
"""
Expand Down Expand Up @@ -393,7 +408,7 @@ def __init__(
:returns: self
"""
if __check_func__ is not None:
checks['func'] = __check_func__
checks["func"] = __check_func__
self.checks = checks

def __call__(self: PyoloadAnnotation, val: Any) -> None:
Expand Down Expand Up @@ -440,13 +455,16 @@ def __init__(
super().__init__(**checks)

def __set_name__(self: Any, obj: Any, name: str, typo: Any = None):
print("...", self, obj, name, typo)
self.name = name
self.value = None

def __get__(self: Any, obj: Any, type: Any):
print("getting")
return self.value

def __set__(self: Any, obj: Any, value: Any):
print("ran checks", self)
self(value)
self.value = value

Expand Down Expand Up @@ -483,6 +501,9 @@ def cast(val: Any, totype: Any) -> Any:
elif len(args) == 1:
kt, vt = args[0], Any
return {Cast.cast(k, kt): Cast.cast(v, vt) for k, v in val.items()}
elif get_origin(totype) == tuple and len(args := get_args(totype)) > 1:
args = get_args(totype)
return tuple(Cast.cast(val, ann) for val, ann in zip(val, args))
else:
sub = args[0]
return get_origin(totype)([Cast.cast(v, sub) for v in val])
Expand Down Expand Up @@ -626,6 +647,14 @@ def type_match(val: Any, spec: Union[Type, PyoloadAnnotation]) -> tuple:
return (False, e)
else:
return (True, None)
elif orig == tuple and len(args := get_args(spec)) > 1:
vals = zip(val, args)
for val, ann in vals:
b, e = type_match(val, ann)
if not b:
return b, e
else:
return (True, None)
else:
sub = get_args(spec)[0]
for val in val:
Expand All @@ -644,7 +673,7 @@ def resove_annotations(obj: Callable) -> None:
:returns: None
"""
if not hasattr(obj, '__annotations__'):
if not hasattr(obj, "__annotations__"):
raise AnnotationResolutionError(
f"object {obj=!r} does not have `.__annotations__`",
)
Expand All @@ -666,7 +695,7 @@ def resove_annotations(obj: Callable) -> None:
obj.__annotations__[k] = eval(
v,
dict(vars(getmodule(obj))),
dict(vars(obj)) if hasattr(obj, '__dict__') else None,
dict(vars(obj)) if hasattr(obj, "__dict__") else None,
)
except Exception as e:
raise AnnotationResolutionError(
Expand Down Expand Up @@ -696,6 +725,8 @@ def annotate(
"""
if isinstance(func, bool):
return partial(annotate, force=True)
if not callable(func):
return func
if not hasattr(func, "__annotations__"):
return func
if is_annoted(func):
Expand Down Expand Up @@ -883,15 +914,13 @@ def annotate_class(cls: Any, recur: bool = True):
setter = cls.__setattr__
if recur:
for x in vars(cls):
if x[:2] == x[-2:] == "__":
if x[:2] == x[-2:] == "__" and x != "__init__":
continue
if hasattr(vars(cls).get(x), "__annotations__"):
if hasattr(getattr(cls, x), "__annotations__"):
setattr(
cls,
x,
annotate(
vars(cls).get(x)
),
annotate(vars(cls).get(x)),
)

@wraps(cls.__setattr__)
Expand Down Expand Up @@ -921,6 +950,7 @@ def new_setter(self: Any, name: str, value: Any) -> Any:

__all__ = [
"annotate",
"annotate_class",
"overload",
"multimethod",
"Checks",
Expand All @@ -937,6 +967,9 @@ def new_setter(self: Any, name: str, value: Any) -> Any:
"Values",
"AnnotationResolutionError",
"AnnotationError",
"Type",
"Any",
"type_match",
]

__version__ = "2.0.1"
Expand Down
14 changes: 7 additions & 7 deletions src/tests/logs.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type_match vs isinstance on int:True 1.9651758000000001ms
type_match vs isinstance on int:False 2.6587863ms
type_match on dict[str, int]*50:True 264.9008229ms
type_match on dict[str, int]*50:False 17.8607409ms
Cast str->int: 10.8863359ms
Cast complex->int | str: 0.5212234499999999ms
Cast dict[int,list[str]*10]*10->dict[str,tuple[float]]: 11.70674105ms
type_match vs isinstance on int:True 1.523942ms
type_match vs isinstance on int:False 1.8651287ms
type_match on dict[str, int]*50:True 272.9914228ms
type_match on dict[str, int]*50:False 17.931477400000002ms
Cast str->int: 10.8411041ms
Cast complex->int | str: 0.52918507ms
Cast dict[int,list[str]*10]*10->dict[str,tuple[float]]: 12.7820111ms
3 changes: 2 additions & 1 deletion src/tests/test_annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pyoload import unannotable
from pyoload import unannotate

assert pyoload.__version__ == "2.0.0"
assert pyoload.__version__ == "2.0.1"


@annotate
Expand Down Expand Up @@ -116,6 +116,7 @@ def fooar(a: 'str', b: 'int'):
assert type_match(3, dict[str | int])
assert type_match({3: '4'}, dict[int, int])
assert type_match({'3': 4}, dict[int, int])
assert type_match((3, 4.0), tuple[int, float])


if __name__ == "__main__":
Expand Down
3 changes: 2 additions & 1 deletion src/tests/test_cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pyoload import annotate
from pyoload import type_match

assert pyoload.__version__ == "2.0.0"
assert pyoload.__version__ == "2.0.1"


@annotate
Expand Down Expand Up @@ -37,6 +37,7 @@ def test_cast():
assert Cast(dict[int])({'3': '7'}) == {3: '7'}
assert Cast(dict[Any, int])({'3': '7'}) == {'3': 7}
assert Cast(tuple[int | str])(('3', 2.5, 1j, '/6')) == (3, 2, '1j', '/6')
assert Cast(tuple[float, int | str])((3, 3j)) == (3.0, str(3j))

try:
@annotate
Expand Down
19 changes: 14 additions & 5 deletions src/tests/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
from pyoload import Check
from pyoload import annotate

assert pyoload.__version__ == "2.0.0"
assert pyoload.__version__ == "2.0.1"


@annotate
class foo:
foo = CheckedAttr(len=slice(3, None))
foow = CheckedAttr(len=(3, None))
foow = CheckedAttr(len=slice(3, None))
bar: Checks(ge=3)

def __init__(self: Any, bar: Checks(func=bool)) -> Any:
def __init__(self: Any, bar: Checks(bool)) -> Any:
pass


Expand All @@ -29,7 +29,7 @@ class IsInt(Check):
name = "isint"

def __call__(self, a, b):
return a == isinstance(b, int)
assert a == isinstance(b, int)


def test_check():
Expand All @@ -43,6 +43,7 @@ def test_check():
obj = foo(2)
obj.bar = 3
obj.foo = ('1', 2, 3)
Check.check("_isint", True, None)
try:
obj.foow = None
except Exception:
Expand Down Expand Up @@ -97,7 +98,7 @@ def test_check():
Checks(test1=3)(3)
Checks(test2=4)(4)
Checks(ge=2, gt=1, lt=2.1, le=2, eq=2)(2)
print(Checks(ge=-2.5, gt=-3, lt=-2, le=2, eq=-2.5)(-2.5))
# print(Checks(ge=-2.5, gt=-3, lt=-2, le=2, eq=-2.5)(-2.5))
Checks(len=slice(2, 5))("abcd")
Checks(type=dict[str | int, tuple[int]])(
{
Expand All @@ -107,9 +108,17 @@ def test_check():
)
Checks(isinstance=float)(1.5)
Checks(isint=True)(5)
Checks(_isint=False)(5)
try:
Checks(_isint=True)(5)
except Check.CheckError:
pass
else:
raise Exception("did not fail")

for name, check in pyoload.Check.checks_list.items():
try:
print(pyoload.get_name(check))
if pyoload.get_name(check).split(".")[0] == "tests":
continue
pyoload.Checks(**{name: NotImplemented})(24)
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pyoload


assert pyoload.__version__ == "2.0.0"
assert pyoload.__version__ == "2.0.1"


errors = (
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test_overload.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pyoload import get_name
from pyoload import overload

assert pyoload.__version__ == "2.0.0"
assert pyoload.__version__ == "2.0.1"


@overload
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pyoload


assert pyoload.__version__ == "2.0.0"
assert pyoload.__version__ == "2.0.1"


@annotate
Expand Down

0 comments on commit a6ca2d2

Please sign in to comment.