.. Threecheck documentation master file, created by sphinx-quickstart on Sun Jan 11 15:14:15 2009. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Threecheck's documentation! ====================================== Threecheck is a simple typechecking system written for Python 3000, which is hosted at `http://threecheck.sourceforge.net `_ (you can download it at `http://www.sourceforge.net/projects/threecheck `_) . Its features are: - base type checking (int, float, str, ...) - list, set, dict and tuple nested type checking (e.g.: list of int) - contract checkers: callables, iterables, generic attribute checker - boolean operators: and, or, not, xor - support for \*varargs and \*\*kwargs - fixed value check - clear interface for extending with new checkers: criteria checker and more powerful :class:`Checker` class subclassing Here's a simple example of its capabilities, which shows how to annotate functions: >>> import threecheck as tc >>> @tc.typecheck ... def pow(base: tc.Number, exp: int) -> tc.Number: ... res = 1 ... for i in range(exp): res=res*base ... return res >>> @tc.typecheck ... def reverseDict(d: {str: int}) -> {int: str}: ... return {v: k for k, v in d.items()} >>> @tc.typecheck ... def mult(const: tc.Or(str, int), lst: tc.Iterable(int)) -> tc.Iterable(int): ... return [v * int(const) for v in lst] Tutorial ======== Here's a walkthrough to all Threecheck capabilities, skipping the technicalities behind the system. Base typechecking ----------------- The minimum framework to use for typechecking a function or method is the :func:`typecheck` decorator and some annotations on parameters and return values: >>> import threecheck as tc >>> @tc.typecheck ... def sum(a: int, b: int) -> int: ... return a+b The :func:`typecheck` decorator enables the typechecking system, and the annotation describe the types of the parameters and return value. The most basic type of annotation you can put, as we have seen, is the type to match. Any type or class is valid. Subclasses are matched too: >>> class Base: pass ... >>> class Derived(Base): pass ... >>> @tc.typecheck ... def f(x: Base): ... pass ... >>> f(Derived()) The function executes without any error, because a ``Derived`` instance is also a ``Base`` instance. In case of typechecking error, a :exc:`TypeCheckFailed` exception is raised. Compound types -------------- Complex data types are easily matched too. If we want to simply match a list of any object, we already have the instruments to do it: >>> @tc.typecheck ... def f(lst: list): ... pass But we can also express a list of :keyword:`int`\ s with this syntax: >>> @tc.typecheck ... def f(lst: [int]): ... pass The following examples match sets or dictionaries: >>> @tc.typecheck ... def f(s: {float}): ... pass ... @tc.typecheck ... def g(d: {int: str}): ... pass The function ``f`` accepts a set of floats, whereas the function ``g`` accepts a dictionary which has :keyword:`int` as keys and :keyword:`str` as values. Tuples are considered too. Let's say we want to match tuples whose first element is an :keyword:`int`, the second one a :keyword:`str`, and the third one a :keyword:`float`\ : >>> @tc.typecheck ... def f(x: (int, str, float)): ... pass This way ``f((3, "hello", 4.2))`` is accepted, whereas ``f((3, "hello", "hello2"))`` raise a :exc:`TypeCheckFailed` exception. If you want instead a tuple made of many elements of the same type, you have to use the :class:`VarTuple` class: >>> @tc.typecheck ... def f(x: tc.VarTuple(int)): ... pass This way ``f((1, 2))``, ``f((1, 4, 3, 5))`` and ``f((8, 5, 6))`` are accepted, whereas ``f((1, 2, "hello"))`` is not. More base typechecking ---------------------- Typecheckers can be combined to express more powerful concepts. One way of combining them is through the boolean operators: :class:`And`, :class:`Or`, :class:`Not` and :class:`Xor`: >>> @tc.typecheck ... def sum(values: [tc.Or(int, str)]) -> int: ... return sum(map(int, values)) In this example we can see that builtin types and complex types are interchangeable. Using bitwise python operators (``&`` for :class:`And`, ``|`` for :class:`Or`, ``~`` for :class:`Not` and ``^`` for :class:`Xor`), we can express the boolean operators more clearly. The problem is that builtin python types don't support such operands, and thus must be wrapped using the :class:`Type` operator, as in the following example, which is perfectly equivalent to the previous one: >>> @tc.typecheck ... def sum(values: list(tc.Type(int) | tc.Type(str))) -> int: ... return sum(map(int, values)) This time, the expression got longer, but clearer. It's up to your taste decide which expression suits you better. Any other ``threecheck`` operator already supports the bitwise operators overloading. Threecheck offers also a set of types to express typical situations. Ranges (minimum and maximum are included): >>> @tc.typecheck ... def processDiceRoll(value: tc.Range(1, 6)): ... # ... Constant values: >>> @tc.typecheck ... def printNot10(value: tc.Type(int) & ~tc.Value(10)): ... print(value) Nullables and not nullables: >>> @tc.typecheck ... def getCurrentUser() -> tc.Nullable(User): ... # ... >>> @tc.typecheck ... def setContent(content: tc.NotNone(object)): ... # ... And some more contract-based checkers, like iterables: >>> @tc.typecheck ... def skipLast(i: tc.Iterable) -> tc.Iterable: ... return list(i)[:-1] And callables: >>> @tc.typecheck ... def apply(func: tc.Callable, args: list, kwargs: {str: object}): ... return func(*args, **kwargs) #* Advanced typechecking --------------------- Type checking of classes poses some more problems, due to the fact that during the definition of a class you can't refer to the class itself: >>> class Merger: ... @tc.typecheck ... def merge(self, other: Merger) -> Merger: ... # doesn't work: 'Merger' not known at this time Traceback (most recent call last): ... NameError: name 'Merger' is not defined And that mutual references are impossibile to express right away: >>> class Parent: ... @tc.typecheck ... def getChild(self) -> Child: ... # doesn't work: 'Child' not known at this time ... Traceback (most recent call last): ... NameError: name 'Child' is not defined >>> class Child: ... @tc.typecheck ... def getParent(self) -> Parent: ... # this works fine, since 'Parent' has already been defined To solve the first problem, you can use the special :class:`Self`: >>> class Merger: ... @tc.typecheck ... def merge(self, other: tc.Self) -> tc.Self: ... # it's ok Whereas in the second case, you can use the :class:`Type` class in the "lazy" format, which use the name of the type -- instead of the type itself -- so to resolve it at a later time: >>> Child = tc.Type("Child") >>> class Parent: ... @tc.typecheck ... def getChild(self) -> Child: ... # now works fine ... >>> class Child: ... @tc.typecheck ... def getParent(self) -> Parent: ... # this works fine, since 'Parent' has already been defined You can also use this :class:`Type` construct inline if you prefer: >>> class Parent: ... @tc.typecheck ... def getChild(self) -> tc.Type("Child"): ... # now works fine ... >>> class Child: ... @tc.typecheck ... def getParent(self) -> Parent: ... # this works fine, since 'Parent' has already been defined This lazy use of the :class:`Type` construct can also solve the first problem, you decide the form you prefer. Threecheck supports also a basic type of generators, the one which always satisfy a single constraint. This is expressed just like any other checker: >>> @tc.typecheck ... def getSquares(maximum) -> int: ... for x in range(maximum): ... v = x*x ... if v < maximum: ... yield v ... else: ... break Threecheck system recognize it's been applied to a generator, and thus apply the checker to *every* returned value. Future versions will implement also more complex patterns of return values. Full builtin checker list ========================= The checker system is completely based on the :class:`Checker` subclasses, whose instances perform the true checking. Some values which are not subclasses of Checker are also admitted, like the list form (``[int]``), and they generate a corresponding Checker instance. .. class:: And(\*checkers) Matches a value only if the two sub-checkers are matched. A shorthand for this checker is the ``&`` operator. .. class:: Any() Matches any value. Not putting any annotation is the same. >>> @tc.typecheck ... def doubleId(o: tc.Any) -> int: ... return id(o)*2 .. class:: Attrs(\*attributes) Check that the value has certain attributes defined. This is a way to perform duck-typing, in contrast with the classical typing given by the :class:`Type` checker. >>> @tc.typecheck ... def quack(duck: tc.Attrs('walk', 'quack')): ... duck.walk() ... duck.quack() .. data:: Callable Check the value is a callable. .. class:: Criteria(criteria) An user-definable criteria to check the values with: this is the simplest way to extend the threecheck system. The checker function must return a truth value. A shorthand for this class is the callable itself. >>> def even(x): return x % 2 == 0 >>> @tc.typecheck ... def doubleEven(x: even) -> tc.Not(even): ... return x+1 .. class:: Dict(keys, values) Check the value is a dictionary. The dictionary "``{k: v}``" operator is a shorthand for this checker. .. class:: Iterable(subtype = None) Check whether a value is an iterable. Can take a subchecker applied to each value. Notice that the iterable will be consumed, if it's not a sequence, during the checker, in this subcase! .. function:: List(subtype = None) Check a value is a list. Can take a subchecker which is applied to each value. The list "``[...]``" operator is a shorthand for this checker. .. class:: Not(checker) Invert the logic of the inner matcher. .. function:: NotNone(checker = Any) Check a value is not ``None``. .. function:: Nullable(checker = Any) Check the value is ``None``, or match the inner checker. .. data:: Number Check a value is of a number type, that is, either ``int``, ``float`` or ``complex``. .. data:: Positive Check the value is a number which is zero or greater (complex values are not valid for this checker). .. class:: Or(\*checkers) Matches a value if at least one of the two sub-checkers is matched. A shorthand for this checker is the ``|`` operator. .. class:: Range(min = None, max = None) Matches a certain range of values, extremes included. You can skip an extreme to say you ignore that limit. >>> @tc.typecheck ... def absTan(x: tc.Range(0, math.pi/2)) -> tc.Range(min=0): ... return math.abs(math.tan(x)) .. function:: Set(subtype = None) Matches a set. The set operator "``{...}``" is a shorthand for this operator. .. class:: Tuple(\*subtypes) Matches a tuple of *n* components. .. class:: Type(t) Matches a specific type. It can either be used in the non-lazy form, giving the type to check as argument, or the lazy form, giving the *name* of the type to check as argument. In this case, the first time the typechecking is required, the type is resolved looking between the global and local symbols known at the scope of definition. A shorthand for the :class:`Type` checker is the type itself. .. class:: Value(value) Check that a type has a specific value. Usually it's useful in combinations with the boolean operators. >>> @tc.typecheck ... def invertBinary(x: tc.Value(0) | tc.Value(1)) -> tc.Value(0) | tc.Value(1): ... return 1-x .. class:: VarTuple(subtype = None) Matches a tuple made of a single type repeated many times. .. function:: Xor(\*checkers) Matches a value if just one of the two sub-checkers is matched. A shorthand for this checker is the ``^`` operator. Extending checker system ======================== There are two ways to extend the Threecheck typing system: using the :class:`Criteria` class, which is simple, or using the :class:`Checker` class, which is more powerful. But before making your own checker from scratch, you can think about building it from the base operators by combining them. Combining existing operators ---------------------------- A couple of examples can be taken from the library itself. In example, the :data:`Positive` checker is expressed as follows: >>> Positive = Or(int, float) & Range(min=0) Which tells you that a positive number is either an ``int`` or a ``float`` with a minimum value or zero. A more complex example is the :func:`Xor` operator, which needs arguments too, and thus is a function: >>> def Xor(\*checkers): ... return Or(\*[ ... And(\*([checker] + [ ... Not(checker2) ... for checker2 ... in checkers ... if checker is not checker2])) ... for checker ... in checkers]) Anyway, if combining existing operators isn't enough for you, you can use the following classes. :class:`Criteria` class ----------------------- The :class:`Criteria` checker accepts any function which act as a checker. Every callable is automatically wrapped inside a :class:`Criteria` checker, and thus can be directly used: >>> def even(x): return x % 2 == 0 >>> @tc.typecheck ... def doubleEven(x: even) -> tc.Not(even): ... return x+1 :class:`Checker` class ---------------------- .. class:: Checker() The most powerful system to extend the checker system is to build upon the :class:`Checker` class. The added value is that you have more control over the error messages and informations not only about the argument checked itself, but also about the other arguments which can sometimes be useful. The Checker class methods to implements follows. .. method:: error(value, args, kwargs) Check if a value is ok for this checker. *value* is the value to check, whether *args* is a tuple containing all the non-keyword arguments passed to the function, and *kwargs* is a dict between keyword arguments and values. If the check is passed, must return an empty string, otherwise a string containing a message error. .. method:: ok_message() Return a message which explains why this checker has succeeded (in case it succeeds). E.g.: we have a checker for type `int`, and its ok_message implementation can be: >>> def ok_message(self): ... return 'is an int' Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search`