Repository: vispy/vispy

Issue:
Replace bundled "decorator" dependency
We currently have a dependency on the "decorator" library, but include it as a bundled module here:

`vispy/ext/_bundled/decorator.py`

and then import it here:

`vispy/ext/decorator.py`

where the `decorator` decorator is only used once here:

https://github.com/vispy/vispy/blob/397c3122d5644692e6e1ae6d281b7b72d741fae6/vispy/visuals/transforms/_util.py#L85-L86

I think this dependency could be removed if `arg_to_vec4` was rewritten to use https://docs.python.org/3/library/functools.html#functools.wraps or a similar standard library utility.

@larsoner you probably know the most about this, any concerns? Otherwise I say we let a hacktoberfest participant handle this.


Discussion:
I can't remember why we went with `decorator` for this one. I'd say get rid of it, hopefully `functools.wraps` is enough nowadays.
Hi,

I am interested in this one.
Thanks @sgaist. You have been assigned to this issue. Let me know if you change your mind. I'll try to check in later to check on your status (after we are in October for a bit).

Resolving patch:
diff --git a/vispy/ext/_bundled/decorator.py b/vispy/ext/_bundled/decorator.py
deleted file mode 100644
--- a/vispy/ext/_bundled/decorator.py
+++ /dev/null
@@ -1,426 +0,0 @@
-# #########################     LICENSE     ############################ #
-
-# Copyright (c) 2005-2017, Michele Simionato
-# All rights reserved.
-
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-
-#   Redistributions of source code must retain the above copyright
-#   notice, this list of conditions and the following disclaimer.
-#   Redistributions in bytecode form must reproduce the above copyright
-#   notice, this list of conditions and the following disclaimer in
-#   the documentation and/or other materials provided with the
-#   distribution.
-
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
-# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-# DAMAGE.
-
-"""
-Decorator module, see http://pypi.python.org/pypi/decorator
-for the documentation.
-"""
-from __future__ import print_function
-
-import re
-import sys
-import inspect
-import operator
-import itertools
-import collections
-
-__version__ = '4.1.2'
-
-if sys.version >= '3':
-    from inspect import getfullargspec
-
-    def get_init(cls):
-        return cls.__init__
-else:
-    FullArgSpec = collections.namedtuple(
-        'FullArgSpec', 'args varargs varkw defaults '
-        'kwonlyargs kwonlydefaults')
-
-    def getfullargspec(f):
-        "A quick and dirty replacement for getfullargspec for Python 2.X"
-        return FullArgSpec._make(inspect.getargspec(f) + ([], None))
-
-    def get_init(cls):
-        return cls.__init__.__func__
-
-try:
-    iscoroutinefunction = inspect.iscoroutinefunction
-except AttributeError:
-    # let's assume there are no coroutine functions in old Python
-    def iscoroutinefunction(f):
-        return False
-
-# getargspec has been deprecated in Python 3.5
-ArgSpec = collections.namedtuple(
-    'ArgSpec', 'args varargs varkw defaults')
-
-
-def getargspec(f):
-    """A replacement for inspect.getargspec"""
-    spec = getfullargspec(f)
-    return ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults)
-
-
-DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
-
-
-# basic functionality
-class FunctionMaker(object):
-    """
-    An object with the ability to create functions with a given signature.
-    It has attributes name, doc, module, signature, defaults, dict and
-    methods update and make.
-    """
-
-    # Atomic get-and-increment provided by the GIL
-    _compile_count = itertools.count()
-
-    # make pylint happy
-    args = varargs = varkw = defaults = kwonlyargs = kwonlydefaults = ()
-
-    def __init__(self, func=None, name=None, signature=None,
-                 defaults=None, doc=None, module=None, funcdict=None):
-        self.shortsignature = signature
-        if func:
-            # func can be a class or a callable, but not an instance method
-            self.name = func.__name__
-            if self.name == '<lambda>':  # small hack for lambda functions
-                self.name = '_lambda_'
-            self.doc = func.__doc__
-            self.module = func.__module__
-            if inspect.isfunction(func):
-                argspec = getfullargspec(func)
-                self.annotations = getattr(func, '__annotations__', {})
-                for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
-                          'kwonlydefaults'):
-                    setattr(self, a, getattr(argspec, a))
-                for i, arg in enumerate(self.args):
-                    setattr(self, 'arg%d' % i, arg)
-                if sys.version < '3':  # easy way
-                    self.shortsignature = self.signature = (
-                        inspect.formatargspec(
-                            formatvalue=lambda val: "", *argspec[:-2])[1:-1])
-                else:  # Python 3 way
-                    allargs = list(self.args)
-                    allshortargs = list(self.args)
-                    if self.varargs:
-                        allargs.append('*' + self.varargs)
-                        allshortargs.append('*' + self.varargs)
-                    elif self.kwonlyargs:
-                        allargs.append('*')  # single star syntax
-                    for a in self.kwonlyargs:
-                        allargs.append('%s=None' % a)
-                        allshortargs.append('%s=%s' % (a, a))
-                    if self.varkw:
-                        allargs.append('**' + self.varkw)
-                        allshortargs.append('**' + self.varkw)
-                    self.signature = ', '.join(allargs)
-                    self.shortsignature = ', '.join(allshortargs)
-                self.dict = func.__dict__.copy()
-        # func=None happens when decorating a caller
-        if name:
-            self.name = name
-        if signature is not None:
-            self.signature = signature
-        if defaults:
-            self.defaults = defaults
-        if doc:
-            self.doc = doc
-        if module:
-            self.module = module
-        if funcdict:
-            self.dict = funcdict
-        # check existence required attributes
-        assert hasattr(self, 'name')
-        if not hasattr(self, 'signature'):
-            raise TypeError('You are decorating a non function: %s' % func)
-
-    def update(self, func, **kw):
-        "Update the signature of func with the data in self"
-        func.__name__ = self.name
-        func.__doc__ = getattr(self, 'doc', None)
-        func.__dict__ = getattr(self, 'dict', {})
-        func.__defaults__ = self.defaults
-        func.__kwdefaults__ = self.kwonlydefaults or None
-        func.__annotations__ = getattr(self, 'annotations', None)
-        try:
-            frame = sys._getframe(3)
-        except AttributeError:  # for IronPython and similar implementations
-            callermodule = '?'
-        else:
-            callermodule = frame.f_globals.get('__name__', '?')
-        func.__module__ = getattr(self, 'module', callermodule)
-        func.__dict__.update(kw)
-
-    def make(self, src_templ, evaldict=None, addsource=False, **attrs):
-        "Make a new function from a given template and update the signature"
-        src = src_templ % vars(self)  # expand name and signature
-        evaldict = evaldict or {}
-        mo = DEF.search(src)
-        if mo is None:
-            raise SyntaxError('not a valid function template\n%s' % src)
-        name = mo.group(1)  # extract the function name
-        names = set([name] + [arg.strip(' *') for arg in
-                              self.shortsignature.split(',')])
-        for n in names:
-            if n in ('_func_', '_call_'):
-                raise NameError('%s is overridden in\n%s' % (n, src))
-
-        if not src.endswith('\n'):  # add a newline for old Pythons
-            src += '\n'
-
-        # Ensure each generated function has a unique filename for profilers
-        # (such as cProfile) that depend on the tuple of (<filename>,
-        # <definition line>, <function name>) being unique.
-        filename = '<decorator-gen-%d>' % (next(self._compile_count),)
-        try:
-            code = compile(src, filename, 'single')
-            exec(code, evaldict)
-        except:
-            print('Error in generated code:', file=sys.stderr)
-            print(src, file=sys.stderr)
-            raise
-        func = evaldict[name]
-        if addsource:
-            attrs['__source__'] = src
-        self.update(func, **attrs)
-        return func
-
-    @classmethod
-    def create(cls, obj, body, evaldict, defaults=None,
-               doc=None, module=None, addsource=True, **attrs):
-        """
-        Create a function from the strings name, signature and body.
-        evaldict is the evaluation dictionary. If addsource is true an
-        attribute __source__ is added to the result. The attributes attrs
-        are added, if any.
-        """
-        if isinstance(obj, str):  # "name(signature)"
-            name, rest = obj.strip().split('(', 1)
-            signature = rest[:-1]  # strip a right parens
-            func = None
-        else:  # a function
-            name = None
-            signature = None
-            func = obj
-        self = cls(func, name, signature, defaults, doc, module)
-        ibody = '\n'.join('    ' + line for line in body.splitlines())
-        caller = evaldict.get('_call_')  # when called from `decorate`
-        if caller and iscoroutinefunction(caller):
-            body = ('async def %(name)s(%(signature)s):\n' + ibody).replace(
-                'return', 'return await')
-        else:
-            body = 'def %(name)s(%(signature)s):\n' + ibody
-        return self.make(body, evaldict, addsource, **attrs)
-
-
-def decorate(func, caller):
-    """
-    decorate(func, caller) decorates a function using a caller.
-    """
-    evaldict = dict(_call_=caller, _func_=func)
-    fun = FunctionMaker.create(
-        func, "return _call_(_func_, %(shortsignature)s)",
-        evaldict, __wrapped__=func)
-    if hasattr(func, '__qualname__'):
-        fun.__qualname__ = func.__qualname__
-    return fun
-
-
-def decorator(caller, _func=None):
-    """decorator(caller) converts a caller function into a decorator"""
-    if _func is not None:  # return a decorated function
-        # this is obsolete behavior; you should use decorate instead
-        return decorate(_func, caller)
-    # else return a decorator function
-    if inspect.isclass(caller):
-        name = caller.__name__.lower()
-        doc = 'decorator(%s) converts functions/generators into ' \
-            'factories of %s objects' % (caller.__name__, caller.__name__)
-    elif inspect.isfunction(caller):
-        if caller.__name__ == '<lambda>':
-            name = '_lambda_'
-        else:
-            name = caller.__name__
-        doc = caller.__doc__
-    else:  # assume caller is an object with a __call__ method
-        name = caller.__class__.__name__.lower()
-        doc = caller.__call__.__doc__
-    evaldict = dict(_call=caller, _decorate_=decorate)
-    return FunctionMaker.create(
-        '%s(func)' % name, 'return _decorate_(func, _call)',
-        evaldict, doc=doc, module=caller.__module__,
-        __wrapped__=caller)
-
-
-# ####################### contextmanager ####################### #
-
-try:  # Python >= 3.2
-    from contextlib import _GeneratorContextManager
-except ImportError:  # Python >= 2.5
-    from contextlib import GeneratorContextManager as _GeneratorContextManager
-
-
-class ContextManager(_GeneratorContextManager):
-    def __call__(self, func):
-        """Context manager decorator"""
-        return FunctionMaker.create(
-            func, "with _self_: return _func_(%(shortsignature)s)",
-            dict(_self_=self, _func_=func), __wrapped__=func)
-
-
-init = getfullargspec(_GeneratorContextManager.__init__)
-n_args = len(init.args)
-if n_args == 2 and not init.varargs:  # (self, genobj) Python 2.7
-    def __init__(self, g, *a, **k):
-        return _GeneratorContextManager.__init__(self, g(*a, **k))
-    ContextManager.__init__ = __init__
-elif n_args == 2 and init.varargs:  # (self, gen, *a, **k) Python 3.4
-    pass
-elif n_args == 4:  # (self, gen, args, kwds) Python 3.5
-    def __init__(self, g, *a, **k):
-        return _GeneratorContextManager.__init__(self, g, a, k)
-    ContextManager.__init__ = __init__
-
-contextmanager = decorator(ContextManager)
-
-
-# ############################ dispatch_on ############################ #
-
-def append(a, vancestors):
-    """
-    Append ``a`` to the list of the virtual ancestors, unless it is already
-    included.
-    """
-    add = True
-    for j, va in enumerate(vancestors):
-        if issubclass(va, a):
-            add = False
-            break
-        if issubclass(a, va):
-            vancestors[j] = a
-            add = False
-    if add:
-        vancestors.append(a)
-
-
-# inspired from simplegeneric by P.J. Eby and functools.singledispatch
-def dispatch_on(*dispatch_args):
-    """
-    Factory of decorators turning a function into a generic function
-    dispatching on the given arguments.
-    """
-    assert dispatch_args, 'No dispatch args passed'
-    dispatch_str = '(%s,)' % ', '.join(dispatch_args)
-
-    def check(arguments, wrong=operator.ne, msg=''):
-        """Make sure one passes the expected number of arguments"""
-        if wrong(len(arguments), len(dispatch_args)):
-            raise TypeError('Expected %d arguments, got %d%s' %
-                            (len(dispatch_args), len(arguments), msg))
-
-    def gen_func_dec(func):
-        """Decorator turning a function into a generic function"""
-
-        # first check the dispatch arguments
-        argset = set(getfullargspec(func).args)
-        if not set(dispatch_args) <= argset:
-            raise NameError('Unknown dispatch arguments %s' % dispatch_str)
-
-        typemap = {}
-
-        def vancestors(*types):
-            """
-            Get a list of sets of virtual ancestors for the given types
-            """
-            check(types)
-            ras = [[] for _ in range(len(dispatch_args))]
-            for types_ in typemap:
-                for t, type_, ra in zip(types, types_, ras):
-                    if issubclass(t, type_) and type_ not in t.mro():
-                        append(type_, ra)
-            return [set(ra) for ra in ras]
-
-        def ancestors(*types):
-            """
-            Get a list of virtual MROs, one for each type
-            """
-            check(types)
-            lists = []
-            for t, vas in zip(types, vancestors(*types)):
-                n_vas = len(vas)
-                if n_vas > 1:
-                    raise RuntimeError(
-                        'Ambiguous dispatch for %s: %s' % (t, vas))
-                elif n_vas == 1:
-                    va, = vas
-                    mro = type('t', (t, va), {}).mro()[1:]
-                else:
-                    mro = t.mro()
-                lists.append(mro[:-1])  # discard t and object
-            return lists
-
-        def register(*types):
-            """
-            Decorator to register an implementation for the given types
-            """
-            check(types)
-
-            def dec(f):
-                check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
-                typemap[types] = f
-                return f
-            return dec
-
-        def dispatch_info(*types):
-            """
-            An utility to introspect the dispatch algorithm
-            """
-            check(types)
-            lst = []
-            for anc in itertools.product(*ancestors(*types)):
-                lst.append(tuple(a.__name__ for a in anc))
-            return lst
-
-        def _dispatch(dispatch_args, *args, **kw):
-            types = tuple(type(arg) for arg in dispatch_args)
-            try:  # fast path
-                f = typemap[types]
-            except KeyError:
-                pass
-            else:
-                return f(*args, **kw)
-            combinations = itertools.product(*ancestors(*types))
-            next(combinations)  # the first one has been already tried
-            for types_ in combinations:
-                f = typemap.get(types_)
-                if f is not None:
-                    return f(*args, **kw)
-
-            # else call the default implementation
-            return func(*args, **kw)
-
-        return FunctionMaker.create(
-            func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
-            dict(_f_=_dispatch), register=register, default=func,
-            typemap=typemap, vancestors=vancestors, ancestors=ancestors,
-            dispatch_info=dispatch_info, __wrapped__=func)
-
-    gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
-    return gen_func_dec
diff --git a/vispy/ext/decorator.py b/vispy/ext/decorator.py
deleted file mode 100644
--- a/vispy/ext/decorator.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) Vispy Development Team. All Rights Reserved.
-# Distributed under the (new) BSD License. See LICENSE.txt for more info.
-
-"""
-Handle loading decorator package from system or from the bundled copy
-"""
-
-try:
-    from ._bundled.decorator import *  # noqa
-except ImportError:
-    from decorator import *  # noqa
diff --git a/vispy/visuals/transforms/_util.py b/vispy/visuals/transforms/_util.py
--- a/vispy/visuals/transforms/_util.py
+++ b/vispy/visuals/transforms/_util.py
@@ -4,8 +4,9 @@
 
 from __future__ import division
 
+import functools
+
 import numpy as np
-from ...ext.decorator import decorator
 from ...util import logger
 
 
@@ -82,8 +83,7 @@ def as_vec4(obj, default=(0, 0, 0, 1)):
     return obj
 
 
-@decorator
-def arg_to_vec4(func, self_, arg, *args, **kwargs):
+def arg_to_vec4(func):
     """
     Decorator for converting argument to vec4 format suitable for 4x4 matrix
     multiplication.
@@ -105,21 +105,26 @@ def arg_to_vec4(func, self_, arg, *args, **kwargs):
     and returns a new (mapped) object.
 
     """
-    if isinstance(arg, (tuple, list, np.ndarray)):
-        arg = np.array(arg)
-        flatten = arg.ndim == 1
-        arg = as_vec4(arg)
-
-        ret = func(self_, arg, *args, **kwargs)
-        if flatten and ret is not None:
-            return ret.flatten()
-        return ret
-    elif hasattr(arg, '_transform_in'):
-        arr = arg._transform_in()
-        ret = func(self_, arr, *args, **kwargs)
-        return arg._transform_out(ret)
-    else:
-        raise TypeError("Cannot convert argument to 4D vector: %s" % arg)
+
+    @functools.wraps(func)
+    def wrapper(self_, arg, *args, **kwargs):
+        if isinstance(arg, (tuple, list, np.ndarray)):
+            arg = np.array(arg)
+            flatten = arg.ndim == 1
+            arg = as_vec4(arg)
+
+            ret = func(self_, arg, *args, **kwargs)
+            if flatten and ret is not None:
+                return ret.flatten()
+            return ret
+        elif hasattr(arg, '_transform_in'):
+            arr = arg._transform_in()
+            ret = func(self_, arr, *args, **kwargs)
+            return arg._transform_out(ret)
+        else:
+            raise TypeError("Cannot convert argument to 4D vector: %s" % arg)
+
+    return wrapper
 
 
 class TransformCache(object):


Test patch: