Skip to content

Commit

Permalink
...
Browse files Browse the repository at this point in the history
  • Loading branch information
ken-morel committed Jun 8, 2024
1 parent ce795b1 commit ca0279d
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 194 deletions.
222 changes: 37 additions & 185 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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': <class 'int'>}
```

## `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 <module>
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: <Checks(is_equal=3)> 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 <module>
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)
###
13 changes: 13 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
8 changes: 4 additions & 4 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -74,3 +71,6 @@ an easy to use tool. here some quick examples.
api
installation
whatsnew
report
genindex
modindex
9 changes: 9 additions & 0 deletions docs/report.rst
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/ken-morel/pyoload/issues/new>`_

Hope you like the docs!
Loading

0 comments on commit ca0279d

Please sign in to comment.