-
Notifications
You must be signed in to change notification settings - Fork 2
magic_constraints.decorator
magic_constrains provides following decorators for parameter and return type declaration:
function_constraintsmethod_constraintsclass_initialization_constraints
function_constraints is a function decorator supporting three forms of invocations:
function_constraints(function)function_constraints(*type_objects, return_type=None)function_constraints(*contraints)
Example:
# py3 annotation.
@function_constraints
def func1(foo: str, bar: Sequence[int]) -> Mapping[str, Sequence[int]]:
return {foo: bar}
# py2 annotation hack.
# NOT RECOMMENDED. Use the forms described later instead.
def func2(foo, bar):
return {foo: bar}
func2.__annotations__ = {
'foo': str,
'bar': Sequence[int],
'return': Mapping[str, Sequence[int]],
}
func2 = function_constraints(func2)Each parameter should be bound with a type annotation. If missing, a SyntaxError would be raised. Return type can be omitted. If return type is omitted, it defaults to Any.
# func1 is equivalent to func2.
@function_constraints
def func1():
pass
@function_constraints
def func2() -> Any:
passType checking on the default value happens during function inspection. If default value is not an instance of corresponding type annotation, a TypeError will be raised.
Example:
@function_constraints(
str, Sequence[int],
# return value could be None or a sequence of ints.
return_type=Optional[Mapping[str, Sequence[int]]],
)
def func2(foo, bar):
return {foo: bar}In this case, type_objects should be an n-tuple of type objects, n equals to the
number of parameters in the decorated function. Keyword-only parameter return_type accepts a type object to indicate the type of return value. If omitted, return_type defaults to Any, meaning that there's no restriction on the return value.
There are rules should be followed:
- Only parameters with the the kind of
POSITIONAL_ONLYorPOSITIONAL_OR_KEYWORDare accepted, see inspect.Parameter.kind for more information. - If default value exists and the default value is not an instance of corresponding type, a
TypeErrorwill be raised.
As a special case, type_objects could be Ellipsis to indicate the function accept arbitrary arguments, in other words, there's no checking on the function's parameters. When type_objects is Ellipsis, there's no limitation on the kind of function's parameters.
@function_constraints(
...,
# return value could be None or a sequence of ints.
return_type=Optional[Mapping[str, Sequence[int]]],
)
def func2(foo, bar):
return {foo: bar}
@function_constraints(...)
def func3(*args, **kwargs):
return 'whatever you want.'Notice that:
-
In Python 2,
...is only supported in slicing. Passing...as the argument would cause aSyntaxError. For Python 2 user, useEllipsisinstead of...:@function_constraints(Ellipsis, return_type=int) def func3(*args, **kwargs): return 42
-
function_constraints(...)(decorated_function)makes no sense.
Example:
# explicitly declare Parameter and ReturnType.
@function_constraints(
Parameter('foo', str),
# bar accepts None or a sequence of ints.
Parameter('bar', Optional[Sequence[int]], default=[1, 2, 3]),
ReturnType(Mapping[str, Sequence[int]]),
)
def func3(args):
return {args.foo: args.bar}In this case, contraints accepts one or more instances of Parameter and ReturnType, with following restrictions:
-
contraintsshould not be empty. -
contraintscould only contains instances ofParameterandReturnType, otherwise aTypeErrorwill be raised. - Instance of
ReturnTypecan be omitted. If omitted, there's no restriction on the return value. If not omitted, instance ofReturnTypemust be placed as the last element ofcontraints, otherwise aSyntaxErrorwill be raised.
After checking the input arguments in runtime, those arguments will
be bound to a single object as its attributes. Hence, user-defined function, that is, the one decorated by function_constraints
should accept only one POSITIONAL_ONLY argument.
-
nameis name of parameter.namemust follows the rule of defining identifier of Python. -
type_defines the type valid argument, should be a type object. - (optional)
defaultdefines the default value of parameter. If omitted and there is no argument could be bound to the parameter in the runtime, aSyntaxErrorwill be raised. - (optional)
validatoraccepts a callable with a single positional argument and returns a boolean value. If defined,validatorwill be invoked after the type introspection. IfvalidatorreturnsFalse, aTypeErrorwill be raised.
-
ReturnTypeaccepts less arguments thanParameter. The meaning ofReturnType's parameter is identical toParameter, seeParameterfor the details.
Due to the side effect of touching Iterable, Iterator and Callable, type checking on such argument is deferred to the time that evaluation really happens. For example:
# f should be a callable accepting an int argument.
@function_constraints
def function(f: Callable[[int], Any]):
# ok.
f(42)
# type error.
f(42.0)Actually, there's no way to check f when calling function. In other to solve the problem, magic-constraints defer the type checking by tranforming f to Callable[[int], Any](f). Similar strategy is applied to Iterable and Iterator.
method_constraints is a method decorator supporting three forms of invocations:
method_constraints(method)method_constraints(*type_objects, return_type=None)method_constraints(*contraints)
method_constraints is almost identical to function_constraints, except that method_constraints decorates method instead of function. Make sure you understand what the method is. See function_constraints for more details.
Here's the example of usage:
from magic_constraints import method_constraints, Parameter
class Example(object):
@method_constraints
def method1(self, foo: int, bar: float) -> float:
return foo + bar
@classmethod
@method_constraints(
int, float, int, Optional[str],
)
def method2(cls, a, b, c=42, d=None):
return a, b, c, d
@method_constraints(
Parameter('a', int),
Parameter('b', float),
Parameter('c', int, default=42),
Parameter('d', Optional[str], default=None),
)
def method3(self, args):
return args.a, args.b, args.c, args.dclass_initialization_constraints is a class decorator requires a class with INIT_PARAMETERS attribute. INIT_PARAMETERS should be a sequence contains one or more instances of Parameter and ReturnType. Restriction of INIT_PARAMETERS is identical to the contraints introduced in function_constraints(*contraints) section.
After decoration, class_initialization_constraints will inject a __init__ for argument processing. After type/value checking, accepted arguments will be bound to self as its attributes. User-defined __init__, within the decorated class or the superclass, will be invoked with a single argument self within the injected __init__. As a consequence, user-defined __init__ should not define any parameter except for self.
Example:
from magic_constraints import class_initialization_constraints, Parameter
@class_initialization_constraints
class Example(object):
INIT_PARAMETERS = [
Parameter('a', int),
]
def __init__(self):
assert self.a == 1