Quickstart

Welcome to typing-exe!

Installation

pip install typing-exe

GitHub page

snimu/typing-exe

Let's begin!

Let's just show the features provided by this package, one after the other.

Assert: basic

First off: Assert. Don't forget @execute_annotations or the annotations won't do anything!

from typing_exe.annotations import Assert
from typing_exe.decorators import execute_annotations


@execute_annotations
def foo(a: Assert[lambda a: a >= 0]):
    ...

What happens when foo is called? Before the function-body is executed, the parameter a is checked with the given function (in this case a lambda, though it can be a regular function as long as it takes the parameter and returns a bool). If that function returns True, then the function-body is executed, if it returns False, a ValueError is raised that might look something like this:

<traceback>
ValueError: 
Assert failed! 
    - Callable: 
        - Name: foo
        - Module: __main__
    Assertion:
        - Name: <lambda>
        - Module: __main__
    - Parameter: 
        - Name: a
        - Value: -1
Assert: multiple assertions

It is easy to run an arbitrary number of assertions in succession:

from typing_exe.annotations import Assert
from typing_exe.decorators import execute_annotations


@execute_annotations
def foo(a: Assert[lambda a: a >= 0, lambda a: a%3 == 0]):
    ...

The assertions will be checked one after the other. If one fails, a ValueError will be raised.

Assert: between parameters

It is also easy to compare one parameter to others.

from typing_exe.annotations import Assert
from typing_exe.decorators import execute_annotations


@execute_annotations
def foo(a, b: Assert[lambda b, a: b > a]):
    ...

It is important that all names correspond to the name of the parameter they refer to. The exception is the annotated parameter, though it is bad form to give it some random name.

from typing_exe.annotations import Assert
from typing_exe.decorators import execute_annotations


# Works, good form
@execute_annotations
def foo(a, b: Assert[lambda b, a: b > a]):
    ...


# Works, bad form
@execute_annotations
def hoo(a, b: Assert[lambda bar, a: bar > a]):
    ...


# Doesn't work
@execute_annotations
def hoo(a, b: Assert[lambda b, nope: b > nope]):
    ...
Assert: returns

Of course, all of these features are available for return values (which is the reason why the first parameter to an assertion does not have to be called the same as the parameter it annotates: it would not work for return values):

from typing_exe.annotations import Assert
from typing_exe.decorators import execute_annotations


@execute_annotations
def foo(a) -> Assert[lambda r, a: r > a]:
    ...

This Assert will be executed after the function-body, and only then will the result be returned.

cleanup_annotations

These type-annotations aren't the actual types that the parameter takes; they are just a way to move constraints on a callable to a place where they are immediately visible to programmers. But what if you want to use actual typehints? Don't worry, typing-exe has you covered.

from typing_exe.decorators import execute_annotations, cleanup_annotations
from typing_exe.annotations import Assert


@cleanup_annotations
@execute_annotations
def foo(a: Assert[int, lambda a: a != 0]):
    ...

Now, the Assert will be executed, but foo.__annotations__ will be {'a': <class 'int'>}, making it useful to other packages such as strongtyping.

Modify

Modify works exactly as Assert does, except that the modification functions are expected to return the parameter—changed however you want—instead of a bool. Instead of checking the returned bool and raising a ValueError if it is False, Modify will change the actual parameter.

Of course, just like with Assert, the modification-functions don't have to be lambdas, the examples below just do that for brevity.

from typing_exe.annotations import Modify
from typing_exe.decorators import execute_annotations


@execute_annotations
def foo(a: Modify[lambda a: a + 1]):
    ...

Before the function-body of foo is executed, a is changed by adding 1 to it. The function-body then works with that modified a.

Of course, all the features of Assert are also available for Modify. This means that multiple modifications can be done in a row in one Modify-statement, Modify works in return-annotations, other parameters can be taken into account, and @cleanup_annotations will treat Modify just like Assert.

There is one additional tool that can be used with Modify, however: EarlyReturn.

EarlyReturn

Sometimes, when some condition is satisfied, you just want to return early without having to execute the rest of the function. typing-exe has you covered!

from typing_exe.early_return import EarlyReturn
from typing_exe.annotations import Modify
from typing_exe.decorators import execute_annotations


def modification(a):
    if a == 0:
        return EarlyReturn(1e100)   # something / 0 is very big... Having a dedicated inf would be better
    return 1/a

@execute_annotations
def foo(a: Modify[modification]):
    ...

Now, foo(0) will immediately return 1e100 without even executing the function-body (or any of the other, later annotations). Annotations before the EarlyReturn will be executed normally.

EarlyReturn: default value

EarlyReturn can even be used as a default value! This way, if a parameter to a function is unfilled, a default can be returned immediately.

from typing import Union
from typing_exe.early_return import EarlyReturn
from typing_exe.decorators import execute_annotations


@execute_annotations
def foo(a: Union[str, EarlyReturn] = EarlyReturn("well that was quick")):
    ...

If you are using type-checkers, EarlyReturn of course has to be allowed as a type to the parameter, as seen in the example above.

Sequence: chaining Assert and Modify statements

Do you want to assert something about a parameter, then modify it, then check the result of that modification, then modify again, and so on, and so on? typing-exe has you covered!

from typing_exe.annotations import (
    Assert,
    Modify,
    Sequence
)
from typing_exe.decorators import (
    execute_annotations,
    cleanup_annotations
)


@cleanup_annotations
@execute_annotations
def foo(
        a: Sequence[
            float,
            Assert[lambda a: a != 0],
            Modify[lambda a, b: b / a],
            Assert[lambda a: 0 < a < 10_000]
        ],
        b: float
):
    ...