def _get_func_decorator(f): argspec = inspect.getargspec(f) typespec_dict = {} # remember zip always truncates at the length of the shortest iterator for spec, argname in zip(typespecs_or_func, argspec.args): typespec_dict[argname] = spec for argname, spec in kwargs.items(): if argname in typespec_dict: raise TypeError("multiple type specifications given for " "argument {} to overloaded function {}()".format(argname, self.__name__)) if argname not in argspec.args: raise TypeError("type specifiation for unknown argument {} given to overloaded " "function {}()".format(argname, self.__name__)) typespec_dict[argname] = spec # fill in None for anything missing, indicating any type will do for argname in argspec.args: if argname not in typespec_dict: typespec_dict[argname] = None self._versions.append(argtypespec(f, typespec_dict)) return self
def _get_func_decorator(f): argspec = inspect.getargspec(f) typespec_dict = {} # remember zip always truncates at the length of the shortest iterator for spec, argname in zip(typespecs_or_func, argspec.args): typespec_dict[argname] = spec for argname, spec in kwargs.items(): if argname in typespec_dict: raise TypeError( "multiple type specifications given for " "argument {} to overloaded function {}()".format( argname, self.__name__)) if argname not in argspec.args: raise TypeError( "type specifiation for unknown argument {} given to overloaded " "function {}()".format(argname, self.__name__)) typespec_dict[argname] = spec # fill in None for anything missing, indicating any type will do for argname in argspec.args: if argname not in typespec_dict: typespec_dict[argname] = None self._versions.append(argtypespec(f, typespec_dict)) return self
def overload_with(self, *typespecs_or_func, **kwargs): """ Add a overloaded version of the decorated function to the available options for a given overloaded function. Types can be specified several ways. The simplest is to give the types as plain old arguments to the `overload_with` decorator. The order of the arguments given corresponds to the order of the arguments in the function definition. For example: >>> @overloaded ... def foo(): ... pass ... >>> @foo.overload_with(int, str) ... def foo(num, word): ... return word * num ... >>> foo(3, 'hello') 'hellohellohello' >>> foo('hello', 3) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: invalid function signature ... If fewer types are specified than arguments, it is assumed that each of the remaining arguments can be any type. (specifying `None` for the type has the same effect). Keyword arguments may also be given to `overload_with`, where the keyword must correspond to the argument name in the function definition. Here is an example of this specification technique (which may be mixed with the previous technique), using the overloaded function `foo` from above: >>> @foo.overload_with(int, int, c=int, string=str) ... def foo(a, b, string, c=1): ... return string*a*b*c ... >>> foo(2, 3, 'a') 'aaaaaa' Finally, types may be specified by way of Python 3 function annotations (this is the prefered technique if the code is only to be used in Python 3) by using the `overload_with` decorator without arguments. The type of a given argument need not be an instance of `type`. It can also be any callable that can be called with one argument, `None`, an `IterableOf` instance, a string, or a tuple of `type` any of these. The way these work is as follows: * If the type is a callable that does not return `False` (or anything with a truth value of False) when called with the argument value, the argument is considered to be of the correct type. * If one of the types in a sequence is `None` and the argument is `None`, the argument is considered to have the correct type. [#f2]_ * If the type is an instance if `IterableOf` (or one of its subtypes), the argument is considered to be the correct type if and only if all of the items in the container are considered the correct type when the criteria described here are applied with the `types` attribute of the `IterableOf` instance * If the type is a string, the string is evaluated in the context of the overloaded function's globals, and the return value of this evaluation is used as the type (which may, in turn, be any of the options described here except for a string). This is particularly useful when the function to be overloaded is a method that takes as an argument an instance of that method's parent class. Since the parent class will not be defined at the time of the function definition, using the parent class's name as an identifier will not work. * Finally, if a `tuple` (or any other `Iterable`) of type specifications is given, the argument is considered to have the correct type if any of the type specifications in the list lead to the correct type. For example: >>> def str_reversed(var): ... return ''.join(reversed(var)) if isinstance(var, str) else NotImplemented ... >>> @foo.overload_with( ... bar=lambda x: x == str_reversed(x), ... n=(int, lambda x: isinstance(x, float) and round(x) == x) ... ) ... def foo(bar, n): ... return bar * int(n) + ' is a palendrome!' ... >>> @foo.overload_with(baz=str) ... def foo(baz, n=3): ... return baz * int(n) + ' is not a palendrome.' ... >>> foo('racecar', 2.0) 'racecarracecar is a palendrome!' >>> foo('asdf') 'asdfasdfasdf is not a palendrome.' The above example also illustrates the order of precidence for overloaded functions: if a call is allowed to multiple versions of the function, the first to be defined is used. (Notice that the first call in the above example matches both overloaded versions of `foo` defined in the example). ..warning :: To allow for the use Python 3 function annotations, specifying a single argument to `overload_with` that is a callable will assume that the Python 3 type specification method is being used, as described above. This could cause problems if what is actually intended is that there be only one type-constrained argument which returns `True` for an arbitrary callable. To get around this, simply specify the type callable for the single argument using the keyword form or as a single item tuple. If the callable you want to use for this specification is a builtin such as `callable` or `int`, this shouldn't be an issue. .. rubric:: Footnotes .. [#f2] Note that if the type is `None` and it is not in a sequence, or a length one tuple `(None,)` is given, this is considered a match for all types. This is mostly to allow arguments to be left out of the specification (in particular, the first argument of most methods, `self`) """ # TODO check to make sure type specifications are valid if sys.version_info > (3, 0) \ and len(kwargs) == 0 \ and len(typespecs_or_func) == 1 \ and inspect.isfunction(typespecs_or_func[0]) \ and not inspect.isbuiltin(typespecs_or_func[0]): func = typespecs_or_func[0] if hasattr(func, 'func_annotations'): # Python 3 version with func_annotations: argspec = inspect.getargspec(func) # for now, ignore varargs and keywords annotations annotations = func.func_annotations typespec_dict = {} for arg in argspec: typespec_dict[arg] = func.func_annotations[arg] self._versions.append(argtypespec(func, typespec_dict)) return self else: raise TypeError("when not using Python 3 function annotations, " "{}.overload_with must\nbe called with " "type specifications as arguments".format(self.__name__)) else: def _get_func_decorator(f): argspec = inspect.getargspec(f) typespec_dict = {} # remember zip always truncates at the length of the shortest iterator for spec, argname in zip(typespecs_or_func, argspec.args): typespec_dict[argname] = spec for argname, spec in kwargs.items(): if argname in typespec_dict: raise TypeError("multiple type specifications given for " "argument {} to overloaded function {}()".format(argname, self.__name__)) if argname not in argspec.args: raise TypeError("type specifiation for unknown argument {} given to overloaded " "function {}()".format(argname, self.__name__)) typespec_dict[argname] = spec # fill in None for anything missing, indicating any type will do for argname in argspec.args: if argname not in typespec_dict: typespec_dict[argname] = None self._versions.append(argtypespec(f, typespec_dict)) return self return _get_func_decorator
def overload_with(self, *typespecs_or_func, **kwargs): """ Add a overloaded version of the decorated function to the available options for a given overloaded function. Types can be specified several ways. The simplest is to give the types as plain old arguments to the `overload_with` decorator. The order of the arguments given corresponds to the order of the arguments in the function definition. For example: >>> @overloaded ... def foo(): ... pass ... >>> @foo.overload_with(int, str) ... def foo(num, word): ... return word * num ... >>> foo(3, 'hello') 'hellohellohello' >>> foo('hello', 3) # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: invalid function signature ... If fewer types are specified than arguments, it is assumed that each of the remaining arguments can be any type. (specifying `None` for the type has the same effect). Keyword arguments may also be given to `overload_with`, where the keyword must correspond to the argument name in the function definition. Here is an example of this specification technique (which may be mixed with the previous technique), using the overloaded function `foo` from above: >>> @foo.overload_with(int, int, c=int, string=str) ... def foo(a, b, string, c=1): ... return string*a*b*c ... >>> foo(2, 3, 'a') 'aaaaaa' Finally, types may be specified by way of Python 3 function annotations (this is the prefered technique if the code is only to be used in Python 3) by using the `overload_with` decorator without arguments. The type of a given argument need not be an instance of `type`. It can also be any callable that can be called with one argument, `None`, an `IterableOf` instance, a string, or a tuple of `type` any of these. The way these work is as follows: * If the type is a callable that does not return `False` (or anything with a truth value of False) when called with the argument value, the argument is considered to be of the correct type. * If one of the types in a sequence is `None` and the argument is `None`, the argument is considered to have the correct type. [#f2]_ * If the type is an instance if `IterableOf` (or one of its subtypes), the argument is considered to be the correct type if and only if all of the items in the container are considered the correct type when the criteria described here are applied with the `types` attribute of the `IterableOf` instance * If the type is a string, the string is evaluated in the context of the overloaded function's globals, and the return value of this evaluation is used as the type (which may, in turn, be any of the options described here except for a string). This is particularly useful when the function to be overloaded is a method that takes as an argument an instance of that method's parent class. Since the parent class will not be defined at the time of the function definition, using the parent class's name as an identifier will not work. * Finally, if a `tuple` (or any other `Iterable`) of type specifications is given, the argument is considered to have the correct type if any of the type specifications in the list lead to the correct type. For example: >>> def str_reversed(var): ... return ''.join(reversed(var)) if isinstance(var, str) else NotImplemented ... >>> @foo.overload_with( ... bar=lambda x: x == str_reversed(x), ... n=(int, lambda x: isinstance(x, float) and round(x) == x) ... ) ... def foo(bar, n): ... return bar * int(n) + ' is a palendrome!' ... >>> @foo.overload_with(baz=str) ... def foo(baz, n=3): ... return baz * int(n) + ' is not a palendrome.' ... >>> foo('racecar', 2.0) 'racecarracecar is a palendrome!' >>> foo('asdf') 'asdfasdfasdf is not a palendrome.' The above example also illustrates the order of precidence for overloaded functions: if a call is allowed to multiple versions of the function, the first to be defined is used. (Notice that the first call in the above example matches both overloaded versions of `foo` defined in the example). ..warning :: To allow for the use Python 3 function annotations, specifying a single argument to `overload_with` that is a callable will assume that the Python 3 type specification method is being used, as described above. This could cause problems if what is actually intended is that there be only one type-constrained argument which returns `True` for an arbitrary callable. To get around this, simply specify the type callable for the single argument using the keyword form or as a single item tuple. If the callable you want to use for this specification is a builtin such as `callable` or `int`, this shouldn't be an issue. .. rubric:: Footnotes .. [#f2] Note that if the type is `None` and it is not in a sequence, or a length one tuple `(None,)` is given, this is considered a match for all types. This is mostly to allow arguments to be left out of the specification (in particular, the first argument of most methods, `self`) """ # TODO check to make sure type specifications are valid if sys.version_info > (3, 0) \ and len(kwargs) == 0 \ and len(typespecs_or_func) == 1 \ and inspect.isfunction(typespecs_or_func[0]) \ and not inspect.isbuiltin(typespecs_or_func[0]): func = typespecs_or_func[0] if hasattr(func, 'func_annotations'): # Python 3 version with func_annotations: argspec = inspect.getargspec(func) # for now, ignore varargs and keywords annotations annotations = func.func_annotations typespec_dict = {} for arg in argspec: typespec_dict[arg] = func.func_annotations[arg] self._versions.append(argtypespec(func, typespec_dict)) return self else: raise TypeError( "when not using Python 3 function annotations, " "{}.overload_with must\nbe called with " "type specifications as arguments".format(self.__name__)) else: def _get_func_decorator(f): argspec = inspect.getargspec(f) typespec_dict = {} # remember zip always truncates at the length of the shortest iterator for spec, argname in zip(typespecs_or_func, argspec.args): typespec_dict[argname] = spec for argname, spec in kwargs.items(): if argname in typespec_dict: raise TypeError( "multiple type specifications given for " "argument {} to overloaded function {}()".format( argname, self.__name__)) if argname not in argspec.args: raise TypeError( "type specifiation for unknown argument {} given to overloaded " "function {}()".format(argname, self.__name__)) typespec_dict[argname] = spec # fill in None for anything missing, indicating any type will do for argname in argspec.args: if argname not in typespec_dict: typespec_dict[argname] = None self._versions.append(argtypespec(f, typespec_dict)) return self return _get_func_decorator