From ca0279d603e712ddc49afb3c908bedb6ad426daa Mon Sep 17 00:00:00 2001 From: ken-morel Date: Sun, 9 Jun 2024 00:06:15 +0100 Subject: [PATCH] ... --- README.md | 222 +++++++--------------------------------- docs/examples.rst | 13 +++ docs/index.rst | 8 +- docs/report.rst | 9 ++ docs/usage.rst | 92 +++++++++++++++++ docs/whatsnew.rst | 32 +++++- src/pyoload/__init__.py | 8 +- 7 files changed, 190 insertions(+), 194 deletions(-) create mode 100644 docs/report.rst diff --git a/README.md b/README.md index 7289e1c..cee4ed4 100644 --- a/README.md +++ b/README.md @@ -9,219 +9,71 @@ # pyoload -pyoload is a little initiative to integrate tools for typechecking and -casting in python functions and classes. +Hy pythonista! I'm happy to present to you `pyoload`, as from my words: -# usage + A python module for extended and recursive type checking and casting of + function arguments and class attributes during runtime -## `pyoload.annotate` +Here we use some of the beautiful and clean features offered by python, including +decorators and descriptors to help you type check during runtime -Simple decorator over functions and classes +Here are some simple usage examples to fill this pypi page. -### functions +## annotate -e.g +This decorator function uses the power of `inspect.signature` to check the arguments +passed to the function using it's annotations with support for default values, generic aliase +and annotations adding some more annotation types for convenience, lower some code. ```python from pyoload import * @annotate -def foo(bar: int) -> str: - ... - -@annotate -def bar(foo: str): +def foo( + a: str, # this has an annotation + b=3, # this uses a default value + c: int = 0 # here both +) -> None: # The return type ... ``` -raises `pyoload.AnnotationError` when type mismatch - -### classes - -When annotating a class, pyoload wraps the classes `__setattr__` with -a typechecker function which typechecks the passed value on each assignment. - -It also calls annotate on each of it's methods, except the class has a -`__annotate_norecur__` attribute. - -Newer from version 1.1.3, pyoload ignores attributes with no annotations and does not check -them. - ```python from pyoload import * @annotate -class Person: - age: 'int' - - def __init__(self: Any, age: int, name: str): - self.age = age - self.name = name - - -djamago = Person(15, 'djamago') - -print(djamago.__annotations__) # {'age': } -``` - -## `pyoload.overload` - -When decorating a function it: -- annotates the function with the special kwarg `is_overload=True` -- gets the function's name using `pyoload.get_name` and if needed - creates a new dictionarry value in - `pyoload.__overloads__[name]` where it stores all ~~overloads~~dispatches and stores a copy in - the function's `.__pyod_overloads__` attribute. - -And on each call it simply loops through each function entry, while -it catches a `pyoload.InternalAnnotationError` which is raised when -the special `is_overload` is set to true - -> [!TIP] -> you may raise `pyoload.InternalAnnotationError` inside ~~an overloaded - function~~a multimethod after carrying out some other checks and pyoload will switch to the - next oveload. - -```python -@overload -def foo(a: int): - ... - -@overload -def foo(b: str, c: float): - ... - -@foo.overload -def foo_hello(d: dict[str, list[int]]): +def foo( + b=dict[str | int, float], # here a GenericAlias + c: Cast(list[int]) = '12345' # here a recursive cast +): # No return type ... ``` +## multimethod -## type checking with `pyoload.typeMatch(val, type)` - -this function simply finds type compatibility between the passes arguments - -the type could be: -- a class -- a Union e.g `int|str` -- a generic alias e.g `dict[str, int]` -- a subclass of `pyoload.PyoloadAnnotation` as: - - `pyoload.Checks` - - `pyoload.Values` - - - -## Casting with `pyoload.Cast` - -Most pyoload decorators support `pyoload.Cast` instances, -When used as an annotation the value is casted to the specified type. - -```python -def foo(a: str): - print(repr(a)) - -foo(3.5) # '3.5' -``` - -### casting recursion - -Using recursion it supports Generic Aliases of `dict` and builtin iterable -types as `list` and `tuple`. - -```python -from pyoload import Cast - -caster = Cast(dict[str, list[tuple[float]]]) # a dictionary of names of - # places[str] to a list of their - # (x, y) coordinates - # [list[tuple[float]]] - -raw = { - 4: ( - ['1.5', 10], - [10, '1.5'], - ) -} -print(caster(raw)) # {'4': [(1.5, 10.0), (10.0, 1.5)]} -``` - -> Note - When `pyoload.Cast` receives a Union as `int|str` it tries to - cast to the listed forms in the specific order, thus if we have - `test = (3j, 11.0)` and `caster = Cast(tuple[float|str])` casting with - `caster(test)` will give `('3j', 11.0)`, since complex `3j` can not be - converted to float, and `pyoload.Cast.cast` will fallback to `str` - -## writing checks pyoload.Checks - -It provides a simple API for writing custom functions for checking. +This uses the same principles as annotate but allows multiple dispatching +(a.k.a runtime overloading?) of functions. ```python from pyoload import * -Check.register('is_equal') -def isnonecheck(param, value): - print(f'{param=}, {value=}') - if param != value: - raise Check.CheckError(f'{param!r} not equal to {value!r}') - -@annotate -def foo(bar: Checks(is_equal=3)): - pass - -foo(3) # param=3 value=3 -foo('4') - -Traceback (most recent call last): - File "C:\pyoload\src\del.py", line 77, in - foo('4') - File "C:\pyoload\src\pyoload\__init__.py", line 514, in wrapper - raise AnnotationErrors(errors) -pyoload.AnnotationErrors: [AnnotationError("Value: '4' does not match annotation: for argument 'bar' of function __main__.foo")] -``` +@multimethod +def foo(a, b): + print("two arguments") -It provides builtin checkes as: lt, gt, ge, le, eq, `func:Callable`, -`type:Any|PyoloadAnnotation` +@multimethod +def foo(a: Values((1, 2, 3))): + print('either 1, 2 or 3') -## using `pyoload.CheckedAttr` and `pyoload.CastedAttr` +@foo.overload +def _(a: Any): + raise ValueError() +``` -`pyoload` provides: -- `pyoload.CheckedAttr` A descriptor which does the type checking on - assignment, and -- `pyoload.CastedAttr` Another descriptor Which stores a casted copy of the values it is assigned +## annotations -```python -class Person: - age = CheckedAttr(gt=0) - phone = CastedAttr(tuple[int]) - - def __init__(self, age, phone): - self.age = age - self.phone = phone - -temeze = Person(17, "678936798") - -print(temeze.age) # 17 -print(temeze.phone) # (6, 7, 8, 9, 3, 6, 7, 9, 8) - -mballa = Person(0, "123456") -Traceback (most recent call last): - File "C:\pyoload\src\del.py", line 92, in - mballa = Person(0, "123456") - ^^^^^^^^^^^^^^^^^^^ - File "C:\pyoload\src\del.py", line 84, in __init__ - self.age = age - ^^^^^^^^ - File "C:\pyoload\src\pyoload\__init__.py", line 264, in __set__ - self(value) - File "C:\pyoload\src\pyoload\__init__.py", line 227, in __call__ - Check.check(name, params, val) - File "C:\pyoload\src\pyoload\__init__.py", line 132, in check - check(params, val) - File "C:\pyoload\src\pyoload\__init__.py", line 187, in gt_check - raise Check.CheckError(f'{val!r} not gt {param!r}') -pyoload.Check.CheckError: 0 not gt 0 -``` +These are what pyoload adds to the standard annotations: +> [!NOTE] +> The added annotations are still not mergeable with the standard types. -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/G2G4XYJU6) +### diff --git a/docs/examples.rst b/docs/examples.rst index 2b3b26c..86f64e2 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -29,3 +29,16 @@ AttributeError: 'Person' object has no attribute 'age' Traceback (most recent call last): ... TypeError: object of type 'int' has no len() + + + + + +-------------------------------------------------- +Adding examples +-------------------------------------------------- + +Thinking of better, more realistic or more practical examples which you may +want to retail, will be happy to add it, report it as an issue please. + +:ref:`report` diff --git a/docs/index.rst b/docs/index.rst index 92fbc86..dec82e4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,7 +27,6 @@ Welcome to pyoload v|version| documentation! :target: https://portfolio-ken-morel-projects.vercel.app/ :alt: Hit count - Hy pythonista, here is `pyoload`, what is? A python module to integrate function arguments typechecking, and multiple @@ -62,9 +61,7 @@ an easy to use tool. here some quick examples. foo(4) -* :ref:`genindex` -* :ref:`modindex` - +You could look at the title lower for more documentation. .. toctree:: :maxdepth: 1 @@ -74,3 +71,6 @@ an easy to use tool. here some quick examples. api installation whatsnew + report + genindex + modindex diff --git a/docs/report.rst b/docs/report.rst new file mode 100644 index 0000000..7d8eb59 --- /dev/null +++ b/docs/report.rst @@ -0,0 +1,9 @@ +================================================== +Report An Issue +================================================== + +Spotted a bug?, A mistake; not finding what you need +or wanting an improvement?, I will be very happy to +consider your issue, `Report it here please `_ + +Hope you like the docs! diff --git a/docs/usage.rst b/docs/usage.rst index 0566b74..b1f5b38 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -66,3 +66,95 @@ in which the object will cast. >>> object = {237: (['1.0', 5], (5.0, 6.0))} >>> caster(object ){'237': [(1.0, 5.0), (5.0, 6.0)]} + + +-------------------------------------------------- +annotables and unnanotables +-------------------------------------------------- + +Not wanting a specific function be annotated?, :ref:`pyoload.annotable` and +:ref:`pyoload.unannotable` will mark your function with a special attribute which +will prevent :ref:`pyoload.annotate` from having effect on them, and to return +the empty functions. + +`pyoload.is_annotable` is the function used by pyoload to check for the +unnanotable marks paused by :ref:`pyoload.unannotable` + +.. note:: + + The functions marked py :ref:`pyoload.unannotable` could still be annotated + if the :py:`force=True` argument specified. + +-------------------------------------------------- +Checks +-------------------------------------------------- + +`pyoload` provides this method for writing your own checks and use them +anywhere in your code. + +################################################## +howto? classes +################################################## + +to register a class as a check, simply subclass :ref:`pyoload.Check`, +The class will be instantiated, and it's instances should be callables. +- The name of the check will be taken from the instances `.name` attribute + or the classes name if the `.name` attribute not present. +- The class instance will then be used as a function and called with the value + as argument. + +.. note:: + + If the `.name` attribute is not implemented and the classes name is used. + **the class name is not lowercased** + + +.. code-block:: python + + from pyoload import * + + class MyCheck(Check): + count = 0 + def __call__(self, param, value): + MyCheck.count += 1 + assert MyCheck.count > value + + @annotate + def foo(a: Checks(MyCheck=0)): + return True + +>>> foo(1) +Traceback (most recent call last): + File "", line 1, in + File "C:\pyoload\src\pyoload\__init__.py", line 726, in wrapper + raise AnnotationErrors(errors) +pyoload.AnnotationErrors: [AnnotationError("Value: 1 does not match annotation: for argument 'a' of function __main__.foo")] +>>> foo(1) +True +>>> foo(2) +True +>>> foo(4) +Traceback (most recent call last): + File "", line 1, in + File "C:\pyoload\src\pyoload\__init__.py", line 726, in wrapper + raise AnnotationErrors(errors) +pyoload.AnnotationErrors: [AnnotationError("Value: 4 does not match annotation: for argument 'a' of function __main__.foo")] +>>> MyCheck.count +4 + +################################################## +howto? functions +################################################## + +Functions are registered with the implicit :py:`Check.register`, +here the same logic as above + +.. code-block:: python + + from pyoload import * + + count = 0 + @Check.register('MyCheck') + def _(param, val): + global count; count += 1 + assert count > val diff --git a/docs/whatsnew.rst b/docs/whatsnew.rst index 2b7a1ee..03ab4b3 100644 --- a/docs/whatsnew.rst +++ b/docs/whatsnew.rst @@ -2,8 +2,38 @@ What's new ================================================== +Lot's have been done since I started the project +to when I write this doc now, about + +.. image:: https://wakatime.com/badge/user/dbe5b692-a03c-4ea8-b663-a3e6438148b6/project/ab01ce70-02f0-4c96-9912-bafa41a0aa54.svg + + +These are the highlights + -------------------------------------------------- pyoload v2.0.0 -------------------------------------------------- -Corrected +1. Greatly worked on the docs to make them more undetsandable and increase coverage. +2. Renamed overload to multiple dispatch or multimethod as required, since + As from :ref:`Overload or multimethod`. +3. Added new options to :ref:`pyoload.Checks` such as registerring under multiple names. +4. Increased the pytest coverage to ensure the full functionality of `pyoload` + on the several supported python versions. +5. Greatly improved performance using `inspect.Signature`. Providing support + for partial annotating of function.(Yes, from v2.0.0 some annotations may be ommited). +6. Added helper methods for interacting with annotated functions, + They include + - :ref:`pyoload.annotable` + - :ref:`pyoload.unannotable` + - :ref:`pyoload.is_annotable` + - :ref:`pyoload.is_annotated` + Those methods will help you prevent some functions from being annotated. + +7. Improved support for python 3.9 and 3.10 +8. renamed functions as the previous `pyoload.typeMatch` to :ref:`pyoload.type_match` to follow + the snake case system of nomenclature. +9. :ref:`pyoload.type_match` returns a tuple of the matchin status and errors + which may have lead to type mismatch, thosse errors are added to traceback + to ease debugging. +10. Now most classes implement `__slots__` to improve memory size. diff --git a/src/pyoload/__init__.py b/src/pyoload/__init__.py index c16705a..30bf207 100644 --- a/src/pyoload/__init__.py +++ b/src/pyoload/__init__.py @@ -368,6 +368,7 @@ class Checks(PyoloadAnnotation): """ Pyoload annotation holding several checks called on typechecking. """ + __slots__ = ('checks',) def __init__( self: PyoloadAnnotation, @@ -408,7 +409,7 @@ class CheckedAttr(Checks): """ A descriptor class providing attributes which are checked on assignment """ - + __slots__ = ('name', 'value') name: str value: Any @@ -443,6 +444,7 @@ class Cast(PyoloadAnnotation): """ Holds a cast object which describes the casts to be performed """ + __slots__ = ('type',) @staticmethod def cast(val: Any, totype: Any) -> Any: @@ -528,8 +530,7 @@ class CastedAttr(Cast): """ A descriptor class providing attributes which are casted on assignment """ - - name: str + __slots__ = ('value') value: Any def __init__(self: Cast, type: Any) -> Cast: @@ -555,7 +556,6 @@ def __init__(self: Cast, type: Any) -> Cast: super().__init__(type) def __set_name__(self: Any, obj: Any, name: str, typo: Any = None): - self.name = name self.value = None def __get__(self: Any, obj: Any, type: Any):