diff options
| author | michele.simionato <devnull@localhost> | 2008-12-03 06:58:37 +0000 |
|---|---|---|
| committer | michele.simionato <devnull@localhost> | 2008-12-03 06:58:37 +0000 |
| commit | 7b391ceaf4d46bd2fe1464379fc48d6b3c8b6b01 (patch) | |
| tree | 7abf80f40e2747beebea67cc38e9f6de4c863d1a | |
| download | python-decorator-git-7b391ceaf4d46bd2fe1464379fc48d6b3c8b6b01.tar.gz | |
Started the work on version 3.0 of the decorator module
| -rwxr-xr-x | CHANGES.txt | 52 | ||||
| -rwxr-xr-x | Makefile | 15 | ||||
| -rwxr-xr-x | README.txt | 22 | ||||
| -rwxr-xr-x | corner-cases.txt | 40 | ||||
| -rwxr-xr-x | decorator.py | 129 | ||||
| -rw-r--r-- | documentation.py | 923 | ||||
| -rwxr-xr-x | other.txt | 125 | ||||
| -rw-r--r-- | performance.sh | 16 | ||||
| -rw-r--r-- | setup.py | 37 | ||||
| -rwxr-xr-x | util.py | 14 |
10 files changed, 1373 insertions, 0 deletions
diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100755 index 0000000..5b77483 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,52 @@ +HISTORY +---------- + +2.3.2. Small optimization in the code for decorator factories. First version + with the code uploaded to PyPI (01/12/2008) +2.3.1. Set the zipsafe flag to False, since I want my users to have the source, not + a zipped egg (25/07/2008) +2.3.0. Added support for writing decorator factories with minimal effort (feature + requested by Matthew Wilson); implemented it by enhancing 'decorator' to + a Python 2.6 class decorator (10/07/2008) +2.2.0. Added a note on 'inspect.getsource' not working for decorated + functions; referenced PEP 326; highlighted the snippets in the + documentation with pygments; slightly simplified the code (31/07/2007) +2.1.0. Replaced the utility 'update_wrapper' with 'new_wrapper' and + updated the documentation accordingly; fixed and improved the + doctester argument parsing, signaled by Sam Wyse (3/07/2007) +2.0.1. Included the licence in the source code too; fixed a versioning + issue by adding the version number to the zip file and fixing + the link to it on the web page, thanks to Philip Jenvey (17/02/2007) +2.0. Rewritten and simplified the implementation; broken compatibility + with previous versions (in minor ways); added the utility function + 'update_wrapper' instead of 'newfunc' (13/01/2007) +1.1. 'decorator' instances now have attributes __name__, __doc__, + __module__ and __dict__ coming from the associated caller function; + included the licence into the documentation (02/12/2006) +1.0. Added LICENCE.txt; added a setuptools-friendly setup.py script + contributed by Luke Arno (10/08/2006) +0.8.1. Minor fixes to the documentation (21/06/2006) +0.8. Improved the documentation, added the 'caveats' section (16/06/2006) +0.7.1. Improved the tail_recursive example (15/05/2006) +0.7. Renamed 'copyfunc' into 'newfunc' and added the ability to copy + the signature from a model function; improved '_decorator' to + set the '__module__' attribute too, with the intent of improving + error messages; updated the documentation (10/05/2006) +0.6. Changed decorator.__call__ so that the module somewhat works + even for Python 2.3 (but the signature-preserving feature is + lost) (20/12/2005) +0.5.2. Minor changes to the documentation; improved 'getattr_' and + shortened 'locked' (28/06/2005) +0.5.1. Minor corrections to the documentation (20/05/2005) +0.5. Fixed a bug with out-of-the-mind signatures, added a check for reserved + names in the argument list and simplified the code (thanks to Duncan + Booth) (19/05/2005) +0.4.1. Fixed a typo in the documentation (thanks to Anthon van der Neut) + (17/05/2005) +0.4. Added getinfo, some tests and improved the documentation (12/05/2005) +0.3. Simplified copyfunc, renamed deferred to delayed and added the + nonblocking example (10/05/2005) +0.2. Added copyfunc, improved the multithreading examples, improved + the doctester program (09/05/2005) +0.1.1. Added the license specification and two docstrings (06/05/2005) +0.1. Initial release (04/05/2005) diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..738bc81 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +T = /home/micheles/trunk/ROnline/RCommon/Python/ms/tools +V = 2.3.2 + +documentation.html: documentation.txt + python $T/rst.py documentation +documentation.pdf: documentation.txt + python $T/rst.py -tp documentation +pdf: documentation.pdf + evince documentation.pdf& +decorator-$V.zip: README.txt documentation.txt documentation.html documentation.pdf \ +doctester.py decorator.py setup.py CHANGES.txt performance.sh + zip decorator-$V.zip README.txt documentation.txt documentation.html \ +documentation.pdf decorator.py setup.py doctester.py CHANGES.txt performance.sh +upload: decorator-$V.zip + scp decorator-$V.zip documentation.html alpha.phyast.pitt.edu:public_html/python diff --git a/README.txt b/README.txt new file mode 100755 index 0000000..859f89c --- /dev/null +++ b/README.txt @@ -0,0 +1,22 @@ +Decorator module
+---------------------------
+
+Dependencies:
+
+The decorator module requires Python 2.4.
+
+Installation:
+
+Unzip the archive in a directory called "decorator" in your Python path.
+For instance, on Unices you could give something like that:
+
+$ unzip decorator.zip -d decorator
+
+Testing:
+
+Just go in the package directory and give
+
+$ python doctester.py documentation.txt
+
+This will generate the main.py file containing all the examples
+discussed in the documentation, and will run the corresponding tests.
diff --git a/corner-cases.txt b/corner-cases.txt new file mode 100755 index 0000000..510addc --- /dev/null +++ b/corner-cases.txt @@ -0,0 +1,40 @@ +>>> from decorator import decorator +>>> @decorator +... def identity_dec(f, *a, **k): return f(*a, **k) +... +>>> #defarg=1 +>>> @identity_dec +... def f(f=1): print f +... +>>> f() +1 + +>>> @identity_dec +... def f(_call_): print f +... +Traceback (most recent call last): + ... +AssertionError: You cannot use _call_ or _func_ as argument names! + +>>> @identity_dec +... def f(**_func_): print f +... +Traceback (most recent call last): + ... +AssertionError: You cannot use _call_ or _func_ as argument names! + +>>> @identity_dec +... def f(name): print name +... + +>>> f("z") +z + +The decorator module also works for exotic signatures: + +>>> @identity_dec +... def f((a, (x, (y, z), w)), b): +... print a, b, x, y, z, w + +>>> f([1, [2, [3, 4], 5]], 6) +1 6 2 3 4 5 diff --git a/decorator.py b/decorator.py new file mode 100755 index 0000000..e9a35e0 --- /dev/null +++ b/decorator.py @@ -0,0 +1,129 @@ +########################## LICENCE ############################### + +## 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. +""" + +## The basic trick is to generate the source code for the decorated function +## with the right signature and to evaluate it. + +__all__ = ["decorator", "makefn", "getsignature", "upgrade_dec"] + +import os, sys, re, inspect, warnings +from tempfile import mkstemp + +DEF = re.compile('\s*def\s*([_\w][_\w\d]*)\s*\(') + +def _callermodule(level=2): + return sys._getframe(level).f_globals.get('__name__', '?') + +def getsignature(func): + "Return the signature of a function as a string" + argspec = inspect.getargspec(func) + return inspect.formatargspec(formatvalue=lambda val: "", *argspec)[1:-1] + +class FuncData(object): + def __init__(self, func=None, name=None, signature=None, + defaults=None, doc=None, module=None, funcdict=None): + if func: # func can also be a class or a callable + self.name = func.__name__ + self.doc = func.__doc__ + self.module = func.__module__ + if inspect.isfunction(func): + self.signature = getsignature(func) + self.defaults = func.func_defaults + self.dict = func.__dict__ + if name: + self.name = name + if signature: + self.signature = signature + if defaults: + self.defaults = defaults + if doc: + self.doc = doc + if module: + self.module = module + if funcdict: + self.dict = funcdict + + def update(self, func, **kw): + func.__name__ = getattr(self, 'name', 'noname') + func.__doc__ = getattr(self, 'doc', None) + func.__dict__ = getattr(self, 'dict', {}) + func.func_defaults = getattr(self, 'defaults', None) + func.__module__ = getattr(self, 'module', _callermodule()) + func.__dict__.update(kw) + return func + + def __getitem__(self, name): + return getattr(self, name) + +def makefn(src, funcdata, save_source=True, **evaldict): + src += os.linesep # add a newline just for safety + name = DEF.match(src).group(1) # extract the function name from the source + if save_source: + fhandle, fname = mkstemp() + os.write(fhandle, src) + os.close(fhandle) + else: + fname = '?' + code = compile(src, fname, 'single') + exec code in evaldict + func = evaldict[name] + return funcdata.update(func, __source__=src) + +def decorator(caller, func=None): + """ + decorator(caller) converts a caller function into a decorator; + decorator(caller, func) is akin to decorator(caller)(func). + """ + if func: + fd = FuncData(func) + name = fd.name + signature = fd.signature + for arg in signature.split(','): + argname = arg.strip(' *') + assert not argname in('_func_', '_call_'), ( + '%s is a reserved argument name!' % argname) + src = """def %(name)s(%(signature)s): + return _call_(_func_, %(signature)s)""" % locals() + return makefn(src, fd, save_source=False, _func_=func, _call_=caller) + src = 'def %s(func): return decorator(caller, func)' % caller.__name__ + return makefn(src, FuncData(caller), save_source=False, + caller=caller, decorator=decorator) + +@decorator +def deprecated(func, *args, **kw): + "A decorator for deprecated functions" + warnings.warn('Calling the deprecated function %r' % func.__name__, + DeprecationWarning, stacklevel=3) + return func(*args, **kw) + +def upgrade_dec(dec): + def new_dec(func): + fd = FuncData(func) + src = '''def %(name)s(%(signature)s): + return decorated(%(signature)s)''' % fd + return makefn(src, fd, save_source=False, decorated=dec(func)) + return FuncData(dec).update(new_dec) diff --git a/documentation.py b/documentation.py new file mode 100644 index 0000000..3d5f88a --- /dev/null +++ b/documentation.py @@ -0,0 +1,923 @@ +"""\ +The ``decorator`` module +============================================================= + +:author: Michele Simionato +:E-mail: michele.simionato@gmail.com +:version: 3.0 (11 December 2008) +:Download page: http://pypi.python.org/decorator +:Installation: ``easy_install decorator`` +:License: BSD license + +.. contents:: + +Introduction +------------------------------------------------ + +Python 2.4 decorators are an interesting example of why syntactic sugar +matters: in principle, their introduction changed nothing, since they do +not provide any new functionality which was not already present in the +language; in practice, their introduction has significantly changed the way +we structure our programs in Python. I believe the change is for the best, +and that decorators are a great idea since: + +* decorators help reducing boilerplate code; +* decorators help separation of concerns; +* decorators enhance readability and maintenability; +* decorators are very explicit. + +Still, as of now, writing custom decorators correctly requires +some experience and it is not as easy as it could be. For instance, +typical implementations of decorators involve nested functions, and +we all know that flat is better than nested. + +The aim of the ``decorator`` module it to simplify the usage of +decorators for the average programmer, and to popularize decorators +usage giving examples of useful decorators, such as ``memoize``, +``tracing``, ``redirecting_stdout``, ``locked``, etc. + +The core of this module is a decorator factory called ``decorator``. +All decorators discussed here are built as simple recipes on top +of ``decorator``. You may find their source code in the ``documentation.py`` +file. If you execute it, all the examples contained will be doctested. + +Definitions +------------------------------------ + +Technically speaking, any Python object which can be called with one argument +can be used as a decorator. However, this definition is somewhat too large +to be really useful. It is more convenient to split the generic class of +decorators in two groups: + ++ *signature-preserving* decorators, i.e. callable objects taking a + function as input and returning a function *with the same + signature* as output; + ++ *signature-changing* decorators, i.e. decorators that change + the signature of their input function, or decorators returning + non-callable objects. + +Signature-changing decorators have their use: for instance the +builtin classes ``staticmethod`` and ``classmethod`` are in this +group, since they take functions and return descriptor objects which +are not functions, nor callables. + +However, signature-preserving decorators are more common and easier to +reason about; in particular signature-preserving decorators can be +composed together whereas other +decorators in general cannot (for instance you cannot +meaningfully compose a staticmethod with a classmethod or viceversa). + +Writing signature-preserving decorators from scratch is not that +obvious, especially if one wants to define proper decorators that +can accept functions with any signature. A simple example will clarify +the issue. + +Statement of the problem +------------------------------ + +Suppose you want to trace a function: this is a typical use case +for a decorator and you can find in many places code like this: + +.. code-block:: python + + try: + from functools import update_wrapper + except ImportError: # using Python version < 2.5 + def decorator_trace(f): + def newf(*args, **kw): + print "calling %s with args %s, %s" % (f.__name__, args, kw) + return f(*args, **kw) + newf.__name__ = f.__name__ + newf.__dict__.update(f.__dict__) + newf.__doc__ = f.__doc__ + newf.__module__ = f.__module__ + return newf + else: # using Python 2.5+ + def decorator_trace(f): + def newf(*args, **kw): + print "calling %s with args %s, %s" % (f.__name__, args, kw) + return f(*args, **kw) + return update_wrapper(newf, f) + +The implementation above works in the sense that the decorator +can accept functions with generic signatures; unfortunately this +implementation does *not* define a signature-preserving decorator, since in +general ``decorator_trace`` returns a function with a +*different signature* from the original function. + +Consider for instance the following case: + +>>> @decorator_trace +... def f1(x): +... pass + +Here the original function takes a single argument named ``x``, +but the decorated function takes any number of arguments and +keyword arguments: + +>>> from inspect import getargspec +>>> print getargspec(f1) +([], 'args', 'kw', None) + +This means that introspection tools such as pydoc will give +wrong informations about the signature of ``f1``. This is pretty bad: +pydoc will tell you that the function accepts a generic signature +``*args``, ``**kw``, but when you try to call the function with more than an +argument, you will get an error: + +>>> f1(0, 1) +Traceback (most recent call last): + ... +TypeError: f1() takes exactly 1 argument (2 given) + +The solution +----------------------------------------- + +The solution is to provide a generic factory of generators, which +hides the complexity of making signature-preserving decorators +from the application programmer. The ``decorator`` factory +allows to define decorators without the need to use nested functions +or classes. As an example, here is how you can define +``decorator_trace``. + +First of all, you must import ``decorator``: + +>>> from decorator import decorator + +Then you must define an helper function with signature ``(f, *args, **kw)`` +which calls the original function ``f`` with arguments ``args`` and ``kw`` +and implements the tracing capability: + +$$_trace + +$$trace + +Therefore, you can write the following: + +>>> @trace +... def f1(x): +... pass + +It is immediate to verify that ``f1`` works + +>>> f1(0) +calling f1 with args (0,), {} + +and it that it has the correct signature: + +>>> print getargspec(f1) +(['x'], None, None, None) + +The same decorator works with functions of any signature: + +>>> @trace +... def f(x, y=1, z=2, *args, **kw): +... pass + +>>> f(0, 3) +calling f with args (0, 3, 2), {} + +>>> print getargspec(f) +(['x', 'y', 'z'], 'args', 'kw', (1, 2)) + +That includes even functions with exotic signatures like the following: + +>>> @decorator(trace) +... def exotic_signature((x, y)=(1,2)): return x+y + +>>> print getargspec(exotic_signature) +([['x', 'y']], None, None, ((1, 2),)) +>>> exotic_signature() +calling exotic_signature with args ((1, 2),), {} +3 + +Notice that exotic signatures have been disabled in Python 3.0. + +``decorator`` is a decorator +--------------------------------------------- + +``decorator`` is able to convert the helper function into a +signature-preserving decorator +object, i.e is a callable object that takes a function and returns a +decorated function with the same signature of the original function. + +The ``decorator`` factory itself can be considered as a signature-changing +decorator, just as ``classmethod`` and ``staticmethod``. +However, ``classmethod`` and ``staticmethod`` return generic +objects which are not callable, while ``decorator`` returns +signature-preserving decorators, i.e. functions of a single argument. +Therefore, you can write + +>>> @decorator +... def tracing(f, *args, **kw): +... print "calling %s with args %s, %s" % (f.func_name, args, kw) +... return f(*args, **kw) + +and this idiom is actually redefining ``tracing`` to be a decorator. +We can easily check that the signature has changed: + +>>> print getargspec(tracing) +(['func'], None, None, None) + +Therefore now ``tracing`` can be used as a decorator and +the following will work: + +>>> @tracing +... def func(): pass + +>>> func() +calling func with args (), {} + +BTW, you may use the decorator on lambda functions too: + +>>> tracing(lambda : None)() +calling <lambda> with args (), {} + +For the rest of this document, I will discuss examples of useful +decorators built on top of ``decorator``. + +``memoize`` +--------------------------------------------------------- + +This decorator implements the ``memoize`` pattern, i.e. it caches +the result of a function in a dictionary, so that the next time +the function is called with the same input parameters the result is retrieved +from the cache and not recomputed. There are many implementations of +``memoize`` in http://www.python.org/moin/PythonDecoratorLibrary, +but they do not preserve the signature. + +$$_memoize + +$$memoize + +Here is a test of usage: + +>>> @memoize +... def heavy_computation(): +... time.sleep(2) +... return "done" + +>>> print heavy_computation() # the first time it will take 2 seconds +done + +>>> print heavy_computation() # the second time it will be instantaneous +done + +As an exercise, try to implement ``memoize`` *properly* without the +``decorator`` factory. + +For sake of semplicity, my implementation only works for functions +with no keyword arguments. One can relax this requirement, and allow +keyword arguments in the signature, for instance by using ``(args, +tuple(kwargs.iteritems()))`` as key for the memoize dictionary. +Notice that in general it is impossible to memoize correctly something +that depends on mutable arguments. + +``locked`` +--------------------------------------------------------------- + +There are good use cases for decorators is in multithreaded programming. +For instance, a ``locked`` decorator can remove the boilerplate +for acquiring/releasing locks [#]_. + +.. [#] In Python 2.5, the preferred way to manage locking is via + the ``with`` statement: http://docs.python.org/lib/with-locks.html + +To show an example of usage, suppose one wants to write some data to +an external resource which can be accessed by a single user at once +(for instance a printer). Then the access to the writing function must +be locked: + +.. code-block:: python + + import time + + datalist = [] # for simplicity the written data are stored into a list. + + @locked + def write(data): + "Writing to a sigle-access resource" + time.sleep(1) + datalist.append(data) + + +Since the writing function is locked, we are guaranteed that at any given time +there is at most one writer. An example multithreaded program that invokes +``write`` and prints the datalist is shown in the next section. + +``delayed`` and ``threaded`` +-------------------------------------------- + +Often, one wants to define families of decorators, i.e. decorators depending +on one or more parameters. + +Here I will consider the example of a one-parameter family of ``delayed`` +decorators taking a procedure and converting it into a delayed procedure. +In this case the time delay is the parameter. + +A delayed procedure is a procedure that, when called, +is executed in a separate thread after a certain time +delay. The implementation is not difficult: + +$$delayed + +Notice that without the help of ``decorator``, an additional level of +nesting would have been needed. + +Delayed decorators as intended to be used on procedures, i.e. +on functions returning ``None``, since the return value of the original +function is discarded by this implementation. The decorated function returns +the current execution thread, which can be stored and checked later, for +instance to verify that the thread ``.isAlive()``. + +Delayed procedures can be useful in many situations. For instance, I have used +this pattern to start a web browser *after* the web server started, +in code such as + +>>> @delayed(2) +... def start_browser(): +... "code to open an external browser window here" + +>>> #start_browser() # will open the browser in 2 seconds +>>> #server.serve_forever() # enter the server mainloop + +The particular case in which there is no delay is important enough +to deserve a name: + +.. code-block:: python + + threaded = delayed(0) + +Threaded procedures will be executed in a separated thread as soon +as they are called. Here is an example using the ``write`` +routine defined before: + +>>> @threaded +... def writedata(data): +... write(data) + +Each call to ``writedata`` will create a new writer thread, but there will +be no synchronization problems since ``write`` is locked. + +>>> writedata("data1") +<_Timer(Thread-1, started)> + +>>> time.sleep(.1) # wait a bit, so we are sure data2 is written after data1 + +>>> writedata("data2") +<_Timer(Thread-2, started)> + +>>> time.sleep(2) # wait for the writers to complete + +>>> print datalist +['data1', 'data2'] + +``blocking`` +------------------------------------------- + +Sometimes one has to deal with blocking resources, such as ``stdin``, and +sometimes it is best to have back a "busy" message than to block everything. +This behavior can be implemented with a suitable decorator: + +$$blocking + +Functions decorated with ``blocking`` will return a busy message if +the resource is unavailable, and the intended result if the resource is +available. For instance: + +>>> @blocking("Please wait ...") +... def read_data(): +... time.sleep(3) # simulate a blocking resource +... return "some data" + +>>> print read_data() # data is not available yet +Please wait ... + +>>> time.sleep(1) +>>> print read_data() # data is not available yet +Please wait ... + +>>> time.sleep(1) +>>> print read_data() # data is not available yet +Please wait ... + +>>> time.sleep(1.1) # after 3.1 seconds, data is available +>>> print read_data() +some data + +``redirecting_stdout`` +------------------------------------------- + +Decorators help in removing the boilerplate associated to ``try .. finally`` +blocks. We saw the case of ``locked``; here is another example: + +$$redirecting_stdout + +Here is an example of usage: + +>>> from StringIO import StringIO + +>>> out = StringIO() + +>>> @redirecting_stdout(out) +... def helloworld(): +... print "hello, world!" + +>>> helloworld() + +>>> out.getvalue() +'hello, world!\n' + +Similar tricks can be used to remove the boilerplate associate with +transactional databases. I think you got the idea, so I will leave +the transactional example as an exercise for the reader. Of course +in Python 2.5 these use cases can also be addressed with the ``with`` +statement. + +Class decorators and decorator factories +-------------------------------------------------------------------- + +Starting from Python 2.6 it is possible to decorate classes. The +decorator module takes advantage of this feature to provide a facility +for writing complex decorator factories. We have already seen examples +of simple decorator factories, implemented as functions returning a +decorator. For more complex situations, it is more convenient to +implement decorator factories as classes returning callable objects +that can be used as signature-preserving decorators. To this aim, +``decorator`` can also be used as a class decorator. Given a class +with a ``.call(self, func, *args, **kw)`` method ``decorator(cls)`` adds a +suitable ``__call__`` method to the class; it raises a TypeError if +the class already has a nontrivial ``__call__`` method. + +To give an example of usage, let me +show a (simplistic) permission system based on classes. +Suppose we have a (Web) framework with the following user classes: + +$$User +$$PowerUser +$$Admin + +Suppose we have a function ``get_userclass`` returning the class of +the user logged in our system: in a Web framework ``get_userclass`` +will read the current user from the environment (i.e. from REMOTE_USER) +and will compare it with a database table to determine her user class. +For the sake of the example, let us use a trivial function: + +$$get_userclass + +We can implement the ``Restricted`` decorator factory as follows: + +$$Restricted +$$PermissionError + +An user can perform different actions according to her class: + +$$Action + +Here is an example of usage:: + + >>> a = Action() + >>> a.view() + >>> a.insert() + Traceback (most recent call last): + ... + PermissionError: User does not have the permission to run insert! + >>> a.delete() + Traceback (most recent call last): + ... + PermissionError: User does not have the permission to run delete! + + +A ``PowerUser`` could call ``.insert`` but not ``.delete``, whereas +and ``Admin`` can call all the methods. + +I could have provided the same functionality by means of a mixin class +(say ``DecoratorMixin``) providing a ``__call__`` method. Within +that design an user should have derived his decorator class from +``DecoratorMixin``. However, `I generally dislike inheritance`_ +and I do not want to force my users to inherit from a class of my +choice. Using the class decorator approach my user is free to use +any class she wants, inheriting from any class she wants, provided +the class provide a proper ``.call`` method and does not provide +a custom ``__call__`` method. In other words, I am trading (less stringent) +interface requirements for (more stringent) inheritance requirements. + +.. _I generally dislike inheritance: http://stacktrace.it/articoli/2008/06/i-pericoli-della-programmazione-con-i-mixin1 + +Dealing with third party decorators: ``new_wrapper`` +------------------------------------------------------------ + +Sometimes you find on the net some cool decorator that you would +like to include in your code. However, more often than not the cool +decorator is not signature-preserving. Therefore you may want an easy way to +upgrade third party decorators to signature-preserving decorators without +having to rewrite them in terms of ``decorator``. To this aim the +``decorator`` module provides an utility function called ``new_wrapper``. +``new_wrapper`` takes a wrapper function with a generic signature and returns +a copy of it with the right signature. +For instance, suppose you have a wrapper function ``wrapper`` (or generically +a callable object) with a "permissive" signature (say ``wrapper(*args, **kw)``) +returned by a third party non signature-preserving decorator; let ``model`` +be the original function, with a stricter signature; then +``new_wrapper(wrapper, model)`` +returns a copy of ``wrapper`` with signature copied from ``model``. +Notice that it is your responsability to make sure that the original +function and the model function have compatibile signature, i.e. that +the signature of the model is stricter (or equivalent) than the signature +of the original function. If not, you will get an error at calling +time, not at decoration time. + +With ``new_wrapper`` at your disposal, it is a breeze to define an utility +to upgrade old-style decorators to signature-preserving decorators: + +$$upgrade_dec + +``tail_recursive`` +------------------------------------------------------------ + +In order to give an example of usage for ``new_wrapper``, I will show a +pretty slick decorator that converts a tail-recursive function in an iterative +function. I have shamelessly stolen the basic idea from Kay Schluehr's recipe +in the Python Cookbook, +http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496691. + +$$TailRecursive + +$$tail_recursive + +Here the decorator is implemented as a class returning callable +objects. ``upgrade_dec`` converts that class in a factory function +returning functions. +Here is how you apply the upgraded decorator to the good old factorial: + +.. code-block:: python + + @tail_recursive + def factorial(n, acc=1): + "The good old factorial" + if n == 0: return acc + return factorial(n-1, n*acc) + + >>> print factorial(4) + 24 + +This decorator is pretty impressive, and should give you some food for +your mind ;) Notice that there is no recursion limit now, and you can +easily compute ``factorial(1001)`` or larger without filling the stack +frame. Notice also that the decorator will not work on functions which +are not tail recursive, such as + +$$fact + +(a function is tail recursive if it either returns a value without +making a recursive call, or returns directly the result of a recursive +call). + +Caveats and limitations +------------------------------------------- + +The first thing you should be aware of, it the fact that decorators +have a performance penalty. +The worse case is shown by the following example:: + + $ cat performance.sh + python -m timeit -s " + from decorator import decorator + + @decorator + def do_nothing(func, *args, **kw): + return func(*args, **kw) + + @do_nothing + def f(): + pass + " "f()" + + python -m timeit -s " + def f(): + pass + " "f()" + +On my Linux system, using the ``do_nothing`` decorator instead of the +plain function is more than four times slower:: + + $ bash performance.sh + 1000000 loops, best of 3: 1.68 usec per loop + 1000000 loops, best of 3: 0.397 usec per loop + +It should be noted that a real life function would probably do +something more useful than ``f`` here, and therefore in real life the +performance penalty could be completely negligible. As always, the +only way to know if there is +a penalty in your specific use case is to measure it. + +You should be aware that decorators will make your tracebacks +longer and more difficult to understand. Consider this example: + +>>> @tracing +... def f(): +... 1/0 + +Calling ``f()`` will give you a ``ZeroDivisionError``, but since the +function is decorated the traceback will be longer: + +>>> f() +Traceback (most recent call last): + File "<stdin>", line 1, in ? + f() + File "<string>", line 2, in f + File "<stdin>", line 4, in tracing + return f(*args, **kw) + File "<stdin>", line 3, in f + 1/0 +ZeroDivisionError: integer division or modulo by zero + +You see here the inner call to the decorator ``tracing``, which calls +``f(*args, **kw)``, and a reference to ``File "<string>", line 2, in f``. +This latter reference is due to the fact that internally the decorator +module uses ``eval`` to generate the decorated function. Notice that +``eval`` is *not* responsibile for the performance penalty, since is the +called *only once* at function decoration time, and not every time +the decorated function is called. + +Using ``eval`` means that ``inspect.getsource`` will not work for decorated +functions. This means that the usual '??' trick in IPython will give you +the (right on the spot) message +``Dynamically generated function. No source code available.``. This +however is preferable to the situation with regular decorators, where +``inspect.getsource`` gives you the wrapper source code which is probably +not what you want: + +$$identity_dec +$$example + +>>> import inspect +>>> print inspect.getsource(example) + def wrapper(*args, **kw): + return func(*args, **kw) +<BLANKLINE> + +(see bug report 1764286_ for an explanation of what is happening). + +.. _1764286: http://bugs.python.org/issue1764286 + +At present, there is no clean way to avoid ``eval``. A clean solution +would require to change the CPython implementation of functions and +add an hook to make it possible to change their signature directly. +This will happen in future versions of Python (see PEP 362_) and +then the decorator module will become obsolete. + +.. _362: http://www.python.org/dev/peps/pep-0362 + +For debugging purposes, it may be useful to know that the decorator +module also provides a ``getinfo`` utility function which returns a +dictionary containing information about a function. +For instance, for the factorial function we will get + +>>> d = getinfo(factorial) +>>> d['name'] +'factorial' +>>> d['argnames'] +['n', 'acc'] +>>> d['signature'] +'n, acc' +>>> d['defaults'] +(1,) +>>> d['doc'] +'The good old factorial' + +In the present implementation, decorators generated by ``decorator`` +can only be used on user-defined Python functions or methods, not on generic +callable objects, nor on built-in functions, due to limitations of the +``inspect`` module in the standard library. +Also, there is a restriction on the names of the arguments: if try to +call an argument ``_call_`` or ``_func_`` you will get an AssertionError: + +>>> @tracing +... def f(_func_): print f +... +Traceback (most recent call last): + ... +AssertionError: You cannot use _call_ or _func_ as argument names! + +(the existence of these two reserved names is an implementation detail). + +Moreover, the implementation is such that the decorated function contains +a copy of the original function attributes: + +>>> def f(): pass # the original function +>>> f.attr1 = "something" # setting an attribute +>>> f.attr2 = "something else" # setting another attribute + +>>> traced_f = tracing(f) # the decorated function + +>>> traced_f.attr1 +'something' +>>> traced_f.attr2 = "something different" # setting attr +>>> f.attr2 # the original attribute did not change +'something else' + +That's all folks, enjoy! + + +LICENCE +--------------------------------------------- + +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. + +If you use this software and you are happy with it, consider sending me a +note, just to gratify my ego. On the other hand, if you use this software and +you are unhappy with it, send me a patch! +""" + +import sys, threading +from decorator import * + +def _trace(f, *args, **kw): + print "calling %s with args %s, %s" % (f.func_name, args, kw) + return f(*args, **kw) +def trace(f): + return decorator(_trace, f) + +def delayed(nsec): + def call(proc, *args, **kw): + thread = threading.Timer(nsec, proc, args, kw) + thread.start() + return thread + return decorator(call) + +def identity_dec(func): + def wrapper(*args, **kw): + return func(*args, **kw) + return wrapper + +@identity_dec +def example(): pass + +def _memoize(func, *args, **kw): + # args and kw must be hashable + if kw: + key = args, frozenset(kw.items()) + else: + key = args + cache = func.cache # created at decoration time + if key in cache: + return cache[key] + else: + result = func(*args, **kw) + cache[key] = result + return result + +def memoize(f): + f.cache = {} + return decorator(_memoize, f) + +@decorator +def locked(func, *args, **kw): + lock = getattr_(func, "lock", threading.Lock) + lock.acquire() + try: + result = func(*args, **kw) + finally: + lock.release() + return result + +threaded = delayed(0) # no-delay decorator + +def blocking(not_avail="Not Available"): + def call(f, *args, **kw): + if not hasattr(f, "thread"): # no thread running + def set_result(): f.result = f(*args, **kw) + f.thread = threading.Thread(None, set_result) + f.thread.start() + return not_avail + elif f.thread.isAlive(): + return not_avail + else: # the thread is ended, return the stored result + del f.thread + return f.result + return decorator(call) + +def redirecting_stdout(new_stdout): + def call(func, *args, **kw): + save_stdout = sys.stdout + sys.stdout = new_stdout + try: + result = func(*args, **kw) + finally: + sys.stdout = save_stdout + return result + return decorator(call) + +class User(object): + "Will just be able to see a page" + +class PowerUser(User): + "Will be able to add new pages too" + +class Admin(PowerUser): + "Will be able to delete pages too" + +def get_userclass(): + return User + +class PermissionError(Exception): + pass + +class Restricted(object): + """ + Restrict public methods and functions to a given class of users. + If instantiated twice with the same userclass return the same + object. + """ + _cache = {} + def __new__(cls, userclass): + if userclass in cls._cache: + return cls._cache[userclass] + self = cls._cache[userclass] = super(Restricted, cls).__new__(cls) + self.userclass = userclass + return self + def call(self, func, *args, **kw): + userclass = get_userclass() + if issubclass(userclass, self.userclass): + return func(*args, **kw) + else: + raise PermissionError( + '%s does not have the permission to run %s!' + % (userclass.__name__, func.__name__)) + def __call__(self, func): + return decorator(self.call, func) + + +class Action(object): + @Restricted(User) + def view(self): + pass + + @Restricted(PowerUser) + def insert(self): + pass + + @Restricted(Admin) + def delete(self): + pass + +class TailRecursive(object): + """ + tail_recursive decorator based on Kay Schluehr's recipe + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496691 + """ + CONTINUE = object() # sentinel + + def __init__(self, func): + self.func = func + self.firstcall = True + + def __call__(self, *args, **kwd): + try: + if self.firstcall: # start looping + self.firstcall = False + while True: + result = self.func(*args, **kwd) + if result is self.CONTINUE: # update arguments + args, kwd = self.argskwd + else: # last call + break + else: # return the arguments of the tail call + self.argskwd = args, kwd + return self.CONTINUE + except: # reset and re-raise + self.firstcall = True + raise + else: # reset and exit + self.firstcall = True + return result + +tail_recursive = upgrade_dec(TailRecursive) + +def fact(n): # this is not tail-recursive + if n == 0: return 1 + return n * fact(n-1) diff --git a/other.txt b/other.txt new file mode 100755 index 0000000..3eddd5c --- /dev/null +++ b/other.txt @@ -0,0 +1,125 @@ +>>> from examples import deferred + +>>> fac = deferred(0.0) + +>>> results = [] # or use a Queue, if you wish + +>>> def f(x): +... "In real life, should perform some computation on x" +... results.append(x) + +>>> action["user1"] = fac(f) +>>> action["user2"] = fac(f) + +>>> f1("arg1"); print f1.thread +<_Timer(Thread-1, started)> + +>>> f2("arg2"); print f2.thread +<_Timer(Thread-2, started)> + +>>> time.sleep(.4) +>>> print results +['arg1', 'arg2'] + + #<examples.py> + + import time, threading + from decorator import decorator + + @decorator + def blocking(proc, *args, **kw): + thread = getattr(proc, "thread", None) + if thread is None: + proc.thread = threading.Thread(None, proc, args, kw) + proc.thread.start() + elif thread.isAlive(): + raise AccessError("Resource locked!") + else: + proc.thread = None + blocking.copy = True + + #</examples.py> + +>>> from examples import threaded +>>> def f(): +... print "done" + +>>> read1, read2 = blocking(readinput), blocking(readinput) +>>> read1(), read2() +done +done + +Thread factories +--------------------------------------------- + + #<examples.py> + + import threading + + @decorator + def threadfactory(f, *args, **kw): + f.created = getattr(f, "created", 0) + 1 + return threading.Thread(None, f, f.__name__ + str(f.created), args, kw) + + #</examples.py> + +Here the decorator takes a regular function and convert it into a +thread factory. The name of the generated thread is given by +the name of the function plus an integer number, counting how +many threads have been created by the factory. + +>>> from examples import threadfactory + +>>> @threadfactory +... def do_action(): +... "doing something in a separate thread." + + +>>> action1 = do_action() # create a first thread +>>> print action1 +<Thread(do_action1, initial)> + +>>> action2 = do_action() # create a second thread +>>> action2.start() # start it +>>> print action2 +<Thread(do_action2, started)> + +The ``.created`` attribute stores the total number of created threads: + +>>> print do_action.created +2 +The thread object also stores the result of the function call. + + #<main.py> + + def delayed(nsec): + def call(func, *args, **kw): + def set_result(): thread.result = func(*args, **kw) + thread = threading.Timer(nsec, set_result) + thread.result = "Not available" + thread.start() + return thread + return decorator(call) + + #</main.py> + +Here is an example of usage:: + + #<main.py> + + valuelist = [] + + def send_to_external_process(value): + time.sleep(.1) # simulate some time delay + valuelist.append(value) + + @delayed(.2) # give time to the value to reach the external process + def get_from_external_process(): + return valuelist.pop() + + + #</main.py> + +>>> send_to_external_process("value") +>>> print get_from_external_process() +value diff --git a/performance.sh b/performance.sh new file mode 100644 index 0000000..75e8928 --- /dev/null +++ b/performance.sh @@ -0,0 +1,16 @@ +python -m timeit -s " +from decorator import decorator + +@decorator +def do_nothing(func, *args, **kw): + return func(*args, **kw) + +@do_nothing +def f(): + pass +" "f()" + +python -m timeit -s " +def f(): + pass +" "f()" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e6240f6 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +setup(name='decorator', + version='2.3.2', + description=\ + 'Better living through Python with decorators.', + long_description="""\ +As of now, writing custom decorators correctly requires some experience +and it is not as easy as it could be. For instance, typical implementations +of decorators involve nested functions, and we all know +that flat is better than nested. Moreover, typical implementations +of decorators do not preserve the signature of decorated functions, +thus confusing both documentation tools and developers. + +The aim of the decorator module it to simplify the usage of decorators +for the average programmer, and to popularize decorators usage giving +examples of useful decorators, such as memoize, tracing, +redirecting_stdout, locked, etc.""", + author='Michele Simionato', + author_email='michele.simionato@gmail.com', + url='http://www.phyast.pitt.edu/~micheles/python/documentation.html', + license="BSD License", + py_modules = ['decorator'], + keywords="decorators generic utility", + classifiers=['Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries', + 'Topic :: Utilities'], + zip_safe=False) + @@ -0,0 +1,14 @@ +""" +A few useful decorators +""" + +import sys, time +from decorator import decorator + +@decorator +def traced(func, *args, **kw): + t1 = time.time() + res = func(*args,**kw) + t2 = time.time() + print >> sys.stderr, func.__name__, args, 'TIME:', t2-t1 + return res |
