Package: pydecor
Easy-peasy Python decorators!GitHub:https://github.com/mplanchard/pydecorPyPI:https://pypi.python.org/pypi/pydecorDocs:https://pydecor.readthedocs.io/Contact:msplanchard@gmailor @msplanchard on TwitterSummaryDecorators are great, but they’re hard to write, especially if you want
to include arguments to your decorators, or use your decorators on
class methods as well as functions. I know that, no matter how many I write,
I still find myself looking up the syntax every time. And that’s just for
simple function decorators. Getting decorators to work consistently at the
class and method level is a whole ‘nother barrel of worms.PyDecor aims to make function decoration easy and straightforward, so that
developers can stop worrying about closures and syntax in triply nested
functions and instead get down to decorating!Table of ContentsSummaryQuickstartWhy PyDecor?InstallationDetailsProvided DecoratorsGenericsPrête-à-porter (ready-to-wear)CachesLRUCacheFIFOCacheTimedCacheStackingClass DecorationMethod DecorationExamplesUpdate a Function’s Args or KwargsDo Something with a Function’s Return ValueDo Something Instead of a FunctionAutomatically Log Function Calls and ResultsIntercept an Exception and Re-raise a Custom OneIntercept an Exception, Do Something, and Re-raise the OriginalIntercept an Exception, Handle, and Be Done with ItRoadmapMore Prête-à-porter DecoratorsType AnnotationsContributingContributor ConductTestsAutoformattingLintingDocsDeploymentCredits and LinksQuickstartInstallpydecor:pip install pydecorUse one of the ready-to-wear decorators:# Memoize a functionfrompydecorimportmemoize@memoize()deffibonacci(n):"""Compute the given number of the fibonacci sequence"""ifn<2:returnnreturnfibonacci(n-2)+fibonacci(n-1)print(fibonacci(150))# Intercept an error and raise a different onefromflaskimportFlaskfrompydecorimportinterceptfromwerkzeug.exceptionsimportInternalServerErrorapp=Flask(__name__)@app.route('/')@intercept(catch=Exception,reraise=InternalServerError,err_msg='The server encountered an error rendering "some_view"')defsome_view():"""The root view"""assertFalsereturn'Asserted False successfully!'client=app.test_client()response=client.get('/')assertresponse.status_code==500assert'some_view'.encode()inresp.dataUse a generic decorator to run your own functions@before,@after,
or@insteadof another function, like in the following example,
which sets a User-Agent header on a Flask response:fromflaskimportFlask,make_responsefrompydecorimportDecorated,afterapp=Flask(__name__)# `Decorated` instances are passed to your functions and contain# information about the wrapped function, including its `args`,# `kwargs`, and `result`, if it's been called.defset_user_agent(decorated:Decorated):"""Sets the user-agent header on a result from a view"""resp=make_response(decorated.result)resp.headers.set('User-Agent','my_applicatoin')returnresp@app.route('/')@after(set_user_agent)defindex_view():return'Hello, world!'client=app.test_client()response=client.get('/')assertresponse.headers.get('User-Agent')=='my_application'Or make your own decorator withconstruct_decoratorfromflaskimportrequesfrompydecorimportDecorated,construct_decoratorfromwerkzeug.exceptionsimportUnauthorizeddefcheck_auth(_decorated:Decorated,request):"""Theoretically checks auth.

    It goes without saying, but this is example code. You should
    not actually check auth this way!
    """ifrequest.host!='localhost':raiseUnauthorized('locals only!')authed=construct_decorator(before=check_auth)app=Flask(__name__)@app.route('/')# Any keyword arguments provided to any of the generic decorators are# passed directly to your callable.@authed(request=request)defsome_view():"""An authenticated view"""return'This is sensitive data!'Why PyDecor?It’s easy!With PyDecor, you can go from this:fromfunctoolsimportwrapsfromflaskimportrequestfromwerkzeug.exceptionsimportUnauthorizedfrommy_pkg.authimportauthorize_requestdefauth_decorator(request=None):"""Check the passed request for authentication"""defdecorator(decorated):@wraps(decorated)defwrapper(*args,**kwargs):ifnotauthorize_request(request):raiseUnauthorized('Not authorized!')returndecorated(*args,**kwargs)returnwrapperreturndecorated@auth_decorator(request=requst)defsome_view():return'Hello, World!'to this:fromflaskimportrequestfrompydecorimportbeforefromwerkzeug.exceptionsimportUnauthorizedfrommy_pkg.authimportauthorize_requestdefcheck_auth(_decorated,request=request):"""Ensure the request is authorized"""ifnotauthorize_request(request):raiseUnauthorized('Not authorized!')@before(check_auth,request=request)defsome_view():return'Hello, world!'Not only is it less code, but you don’t have to remember decorator
syntax or mess with nested functions. Full disclosure, I had to look
up a decorator sample to be sure I got the first example’s syntax right,
and I just spent two weeks writing a decorator library.It’s fast!PyDecor aims to make your life easier, not slower. The decoration machinery
is designed to be as efficient as is reasonable, and contributions to
speed things up are always welcome.Implicit Method Decoration!Getting a decorator to “roll down” to methods when applied to a class is
a complicated business, but all of PyDecor’s decorators provide it for
free, so rather than writing:frompydecorimportlog_callclassFullyLoggedClass(object):@log_call(level='debug')defsome_function(self,*args,**kwargs):returnargs,kwargs@log_call(level='debug')defanother_function(self,*args,**kwargs):returnNone...You can just write:frompydecorimportlog_call@log_call(level='debug')classFullyLoggedClass(object):defsome_function(self,*args,**kwargs):returnargs,kwargsdefanother_function(self,*args,**kwargs):returnNone...PyDecor ignores special methods (like__init__) so as not to interfere
with deep Python magic. By default, it works on any methods of a class,
including instance, class and static methods. It also ensures that class
attributes are preserved after decoration, so your class references
continue to behave as expected.Consistent Method Decoration!Whether you’re decorating a class, an instance method, a class method, or
a static method, you can use the same passed function.selfandclsvariables are stripped out of the method parameters passed to the provided
callable, so your functions don’t need to care about where they’re used.Lots of Tests!Seriously. Don’t believe me? Just look. We’ve got the best tests. Just
phenomenal.Installation`pydecor` 2.0 and forward supports only Python 3.6+!If you need support for an older Python, use the most recent 1.x release.To installpydecor, simply run:pip install -U pydecorTo install the current development release:pip install --pre -U pydecorYou can also install from source to get the absolute most recent
code, which may or may not be functional:git clone https://github.com/mplanchard/pydecor
pip install ./pydecorDetailsProvided DecoratorsThis package provides generic decorators, which can be used with any
function to provide extra utility to decorated resources, as well
as prête-à-porter (ready-to-wear) decorators for immediate use.While the information below is enough to get you started, I highly
recommend checking out thedecorator module docsto see all the
options and details for the various decorators!Generics@before- run a callable before the decorated function executescalled with an instance ofDecoratedand any provided kwargs@after- run a callable after the decorated function executescalled with an instance ofDecoratedand any provided kwargs@instead- run a callable in place of the decorated functioncalled with an instance ofDecoratedand any provided kwargs@decorate- specify multiple callables to be run before, after, and/or
instead of the decorated functioncallables passed todecorate’sbefore,after, orinsteadkeyword arguments will be called with the same default function signature
as described for the individual decorators, above. Extras will be
passed to all provided callables.construct_decorator- specify functions to be runbefore,after,
orinsteadof decorated functions. Returns a reusable decorator.The callable passed to a generic decorator is expected to handle at least one
positional argument, which will be an instance ofDecorated.Decoratedobjects provide the following interface:Attributes:args: a tuple of any positional arguments with which the decorated
callable was calledkwargs: a dict of any keyword arguments with which the decorated
callable was calledwrapped: a reference to the decorated callableresult: when the _wrapped_ function has been called, its return value is
stored hereMethods__call__(*args, **kwargs): a shortcut todecorated.wrapped(*args, **kwargs), calling an instance ofDecoratedcalls the underlying wrapped callable. The result of this call (or a
direct call todecorated.wrapped()) will set theresultattribute.Every generic decorator may take any number of keyword arguments, which will be
passed directly into the provided callable, so, running the code below prints
“red”:frompydecorimportbeforedefbefore_func(_decorated,label=None):print(label)@before(before_func,label='red')defred_function():passred_function()Every generic decorator takes the following keyword arguments:implicit_method_decoration- if True, decorating a class implies
decorating all of its methods.Caution:you should probably leave this
on unless you know what you are doing.instance_methods_only- if True, only instance methods (not class or
static methods) will be automatically decorated whenimplicit_method_decorationis TrueTheconstruct_decoratorfunction can be used to combine@before,@after, and@insteadcalls into one decorator, without having to
worry about unintended stacking effects. Let’s make a
decorator that announces when we’re starting an exiting a function:frompydecorimportconstruct_decoratordefbefore_func(decorated):print('Starting decorated function ''"{}"'.format(decorated.wrapped.__name__))defafter_func(decorated):print('"{}" gave result "{}"'.format(decorated.wrapped.__name__,decorated.result))my_decorator=construct_decorator(before=before_func,after=after_func)@my_decorator()defthis_function_returns_nothing():return'nothing'And the output?Starting decorated function "this_function_returns_nothing"
"this_function_returns_nothing" gave result "nothing"Maybe a more realistic example would be useful. Let’s say we want to add
headers to a Flask response.fromflaskimportFlask,Response,make_responsefrompydecorimportconstruct_decoratordef_set_app_json_header(decorated):# Ensure the response is a Response object, even if a tuple was# returned by the view function.response=make_response(decorated.result)response.headers.set('Content-Type','application/json')returnresponseapplication_json=construct_decorator(after=_set_app_json_header)# Now you can decorate any Flask view, and your headers will be set.app=Flask(__name__)# Note that you must decorate "before" (closer to) the function than the# app.route() decoration, because the route decorator must be called on# the "finalized" version of your function@app.route('/')@application_json()defroot_view():return'Hello, world!'client=app.test_client()response=app.get('/')print(response.headers)The output?..code:Content-Type: application/json
Content-Length: 13Prête-à-porter (ready-to-wear)export- add the decorated class or function to its module’s__all__list, exposing it as a “public” reference.intercept- catch the specified exception and optionally re-raise and/or
call a provided callback to handle the exceptionlog_call- automatically log the decorated function’s call signature and
resultsmemoize- memoize a function’s call and return values for re-use. Can
use any cache inpydecor.caches, which all have options for automatic
pruning to keep the memoization cache from growing too large.CachesThree caches are provided withpydecor. These are designed to be passed
to the@memoizationdecorator if you want to use something other than
the defaultLRUCache, but they are perfectly functional for use elsewhere.All caches implement the standard dictionary interface.LRUCacheA least-recently-used cache. Both getting and setting of key/value pairs
results in their having been considered most-recently-used. When the cache
reaches the specifiedmax_size, least-recently-used items are discarded.FIFOCacheA first-in, first-out cache. When the cache reaches the specifiedmax_size,
the first item that was inserted is discarded, then the second, and so on.TimedCacheA cache whose entries expire. If amax_ageis specified, any entries older
than themax_age(in seconds) will be considered invalid, and will be
removed upon access.StackingGeneric and ready-to-wear decorators may be stacked! You can stack multiple
of the same decorator, or you can mix and match. Some gotchas are listed
below.Generally, stacking works just as you might expect, but some care must be
taken when using the@insteaddecorator, or@intercept, which
uses@insteadunder the hood.Just remember that@insteadreplaces everything that comes before. So,
as long as@insteadcalls the decorated function, it’s okay to stack it.
In these cases, it will be calledbeforeany decorators specified below
it, and those decorators will be executed when it calls the decorated function.@interceptbehaves this way, too.If an@insteaddecorator doesnotcall the decorated function and
instead replaces it entirely, itmustbe specified first (at the bottom
of the stacked decorator pile), otherwise the decorators below it will not
execute.For@beforeand@after, it doesn’t matter in what order the decorators
are specified.@beforeis always called first, and@afterlast.Class DecorationClass decoration is difficult, but PyDecor aims to make it as easy and
intuitive as possible!By default, decorating a class applies that decorator to all of that class’
methods (instance, class, and static). The decoration applies to class and
static methods whether they are referenced via an instance or via a class
reference. “Extras” specified at the class level persist across calls to
different methods, allowing for things like a class level memoization
dictionary (there’s a very basic test in the test suite
that demonstrates this pattern).If you’d prefer that the decorator not apply to class and static methods,
set theinstance_methods_only=Truewhen decorating the class.If you want to decorate the class itself, andnotits methods, keep in
mind that the decorator will be triggered when the class is instantiated,
and that, if the decorator replaces or alters the return, that return will
replace the instantiated class. With those caveats in mind, settingimplicit_method_decoration=Falsewhen decorating a class enables that
functionality.NoteClass decoration, and in particular the decoration of class and static
methods, is accomplished through some pretty deep, complicated magic.
The test suite has a lot of tests trying to make sure that everything
works as expected, but please report any bugs you find so that I
can resolve them!Method DecorationDecorators can be applied to static, class, or instance methods directly, as
well. If combined with@staticmethodor@classmethoddecorators,
those decorators should always be at the “top” of the decorator stack
(furthest from the function).When decorating instance methods,selfis removed from the parameters
passed to the provided callable.When decorating class methods,clsis removed from the parameters passed
to the provided callable.Currently, the class and instance referencesdo nothave to be named"cls"and"self", respectively, in order to be removed. However,
this is not guaranteed for future releases, so try to keep your naming
standard if you can (just FYI,"self"is the more likely of the two to
wind up being required).ExamplesBelow are some examples for the generic and standard decorators. Please
check out the API Docs for more information, and also check out the
convenience decorators, which are all implemented using thebefore,after, andinsteaddecorators from this library.Update a Function’s Args or KwargsFunctions passed to@beforecan either return None, in which case nothing
happens to the decorated functions parameters, or they can return a tuple
of args (as a tuple) and kwargs (as a dict), in which case those parameters
are used in the decorated function. In this example, we sillify a very
serious function.NoteBecause kwargs are mutable, they can be updated even if the function
passed tobeforedoesn’t return anything.frompydecorimportbeforedefspamify_func(decorated):"""Mess with the function arguments"""args=tuple(['spam'for_indecorated.args])kwargs={k:'spam'forkindecorated.kwargs}returnargs,kwargs@before(spamify_func)defserious_function(serious_string,serious_kwarg='serious'):"""A very serious function"""print('A serious arg:{}'.format(serious_string))print('A serious kwarg:{}'.format(serious_kwarg))serious_function('Politics',serious_kwarg='Religion')The output?A serious arg: spam
A serious kwarg: spamDo Something with a Function’s Return ValueFunctions passed to@afterreceive the decorated function’s return value
as part of theDecoratedinstance. If@afterreturns None, the return
value is sent back unchanged. However, if@afterreturns something,
its return value is sent back as the return value of the function.In this example, we ensure that a function’s return value has been thoroughly
spammified.frompydecorimportafterdefspamify_return(decorated):"""Spamify the result of a function"""return'spam-spam-spam-spam-{}-spam-spam-spam-spam'.format(decorated.result)@after(spamify_return)defunspammed_function():"""Return a non-spammy value"""return'beef'print(unspammed_function())The output?spam-spam-spam-spam-beef-spam-spam-spam-spamDo Something Instead of a FunctionFunctions passed to@insteadalso provide wrapped context via theDecoratedobject. But, if theinsteadcallable does not call the
wrapped function, it won’t get called at all. Maybe you want to skip
a function when a certain condition is True, but you don’t want to usepytest.skipif, becausepytestcan’t be a dependency of your
production code for whatever reason.frompydecorimportinsteaddefskip(decorated,when=False):ifwhen:passelse:# Calling `decorated` calls the wrapped function.returndecorated(*decorated.args,**decorated.kwargs)@instead(skip,when=True)defuncalled_function():print("You won't see me (you won't see me)")uncalled_function()The output?(There is no output, because the function was skipped)Automatically Log Function Calls and ResultsMaybe you want to make sure your functions get logged without having to
bother with the logging boilerplate each time.@log_calltries to
automatically get a logging instance corresponding to the module
in which the decoration occurs (in the same way as if you made a call
tologging.getLogger(__name__), or you can pass it your own, fancy,
custom, spoiler-bedecked logger instance.fromloggingimportgetLogger,StreamHandlerfromsysimportstdoutfrompydecorimportlog_call# We're just getting a logger here so we can see the output. This isn't# actually necessary for @log_call to work!log=getLogger(__name__)log.setLevel('DEBUG')log.addHandler(StreamHandler(stdout))@log_call()defget_lucky(*args,**kwargs):"""We're up all night 'till the sun."""return"We're up all night for good fun."get_lucky('Too far','to give up',who_we='are')And the output?get_lucky(*('Too far', 'to give up'), **{'who_we': 'are'}) -> "We're up all night for good fun"Intercept an Exception and Re-raise a Custom OneAre you a put-upon library developer tired of constantly having to re-raise
custom exceptions so that users of your library can have one nice try/except
looking for your base exception? Let’s make that easier:frompydecorimportinterceptclassBetterException(Exception):"""Much better than all those other exceptions"""@intercept(catch=RuntimeError,reraise=BetterException)defsometimes_i_error(val):"""Sometimes, this function raises an exception"""ifval>5:raiseRuntimeError('This value is too big!')foriinrange(7):sometimes_i_error(i)The output?Traceback (most recent call last):
  File "/Users/Nautilus/Library/Preferences/PyCharm2017.1/scratches/scratch_1.py", line 88, in <module>
    sometimes_i_error(i)
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/decorators.py", line 389, in wrapper
    return fn(**fkwargs)
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/functions.py", line 58, in intercept
    raise_from(new_exc, context)
  File "<string>", line 2, in raise_from
__main__.BetterException: This value is too big!Intercept an Exception, Do Something, and Re-raise the OriginalMaybe you don’twantto raise a custom exception. Maybe the original
one was just fine. All you want to do is print a special message before
re-raising the original exception. PyDecor has you covered:frompydecorimportinterceptdefprint_exception(exc):"""Make sure stdout knows about our exceptions"""print('Houston, we have a problem:{}'.format(exc))@intercept(catch=Exception,handler=print_exception,reraise=True)defassert_false():"""All I do is assert that False is True"""assertFalse,'Turns out, False is not True'assert_false()And the output:Houston, we have a problem: Turns out, False is not True
Traceback (most recent call last):
  File "/Users/Nautilus/Library/Preferences/PyCharm2017.1/scratches/scratch_1.py", line 105, in <module>
    assert_false()
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/decorators.py", line 389, in wrapper
    return fn(**fkwargs)
  File "/Users/Nautilus/Documents/Programming/pydecor/pydecor/functions.py", line 49, in intercept
    return decorated(*decorated_args, **decorated_kwargs)
  File "/Users/Nautilus/Library/Preferences/PyCharm2017.1/scratches/scratch_1.py", line 102, in assert_false
    assert False, 'Turns out, False is not True'
AssertionError: Turns out, False is not TrueIntercept an Exception, Handle, and Be Done with ItSometimes an exception isn’t the end of the world, and it doesn’t need to
bubble up to the top of your application. In these cases, maybe just handle
it and don’t re-raise:frompydecorimportinterceptdeflet_us_know_it_happened(exc):"""Just let us know an exception happened (if we are reading stdout)"""print('This non-critical exception happened:{}'.format(exc))@intercept(catch=ValueError,handler=let_us_know_it_happened)defresilient_function(val):"""I am so resilient!"""val=int(val)print('If I get here, I have an integer:{}'.format(val))resilient_function('50')resilient_function('foo')Output:If I get here, I have an integer: 50
This non-critical exception happened: invalid literal for int() with base 10: 'foo'Note that the function doesnotcontinue running after the exception is
handled. Use this for short-circuiting under certain conditions rather
than for instituting atry/except:passblock. Maybe one day I’ll figure
out how to make this work like that, but as it stands, the decorator surrounds
the entire function, so it does not provide that fine-grained level of control.Roadmap2.?More Prête-à-porter Decoratorsskipif- similar to py.test’s decorator, skip the function if a
provided condition is TrueLet me know if you’ve got any idea for other decorators that would
be nice to have!Type AnnotationsNow that we’ve dropped support for Python 2, we can use type annotations
to properly annotate function inputs and return values and make them
available to library authors.ContributingContributions are welcome! If you find a bug or if something doesn’t
work the way you think it should, pleaseraise an issue.
If you know how to fix the bug, pleaseopen a PR!I absolutely welcome any level of contribution. If you think the docs
could be better, or if you’ve found a typo, please open up a PR to improve
and/or fix them.Contributor ConductThere is aCODE_OF_CONDUCT.mdfile with details, based on one of GitHub’s
templates, but the upshot is that I expect everyone who contributes to this
project to do their best to be helpful, friendly, and patient. Discrimination
of any kind will not be tolerated and will be promptly reported to GitHub.On a personal note, Open Source survives because of people who are willing to
contribute their time and effort for free. The least we can do is treat them
with respect.TestsTests can be run with:make testThis will use whatever your localpython3happens to be. If you have
other pythons available, you can run:make toxto try to run locally for all supported Python versions.If you have docker installed, you can run:make test-docker-{version}  # e.g. make test-docker-3.6to pull down an appropriate Docker image and run tests inside of it. You can
also run:make test-dockerto do this for all supported versions of Python.PRs that cause tests to fail will not be merged until tests pass.Any new functionality is expected to come with appropriate tests.
If you have any questions, feel free to reach out to me via email atmsplanchard@gmailor on GH via Issues.AutoformattingThis project usesblackfor autoformatting. I recommend setting your editor
up to format on save for this project, but you can also run:make fmtto format everything.LintingLinting can be run with:make lintCurrently, linting verifies that there are:No flake8 errorsNo mypy errorsNo pylint errorsNo files that need be formattedYou should ensure thatmake lintreturns 0 before opening a PR.DocsDocs are autogenerated via Sphinx. You can build them locally by running:make docsYou can then opendocs/_build/html/index.htmlin your web browser of
choice to see how the documentation will look with your changes.DeploymentDeployment is handled through pushing tags. Any tag pushed to GH causes
a push to PyPI if the current version is not yet present there.Pushing the appropriate tag is made easier through the use of:VERSION=1.0.0 make distributewhereVERSIONobviously should be the current version. This will verify
the specified version matches the package’s current version, check to be
sure that the most recent master is being distributed, prompt for a message,
and create and push a signed tag of the formatv{version}.Credits and LinksThis project was started using my genericproject templateTests are run withpytestandtoxDocumentation built withsphinxCoverage information collected withcoveragePickling of objects provided viadill