4

So with the following...

    def makeBold(fn):
        def wrapped():
            return '<b>'+fn()+'</b>'
        return wrapped

    @makeBold
    def produceElement():
        return 'hello'

The result is

    <b>hello</b>

I would like to do something like this...

    @makeBold(attrib=val, attrib=val)
    def produceElement():
        return 'hello'

and have the result be something like...

    <b attrib=val, attrib=val>hello<b/>

Any advice would be great!

3 Answers3

3

Wrap your function with another function:

import functools
def makeBold(**kwargs):
    attribstrings = str(kwargs) # figure out what to do with the dict yourself 
    def actualdecorator(fn):
        @functools.wraps(fn)
        def wrapped():
            return '<b'+attribstrings+'>'+fn()+'</b>'
        return wrapped
    return actualdecorator

I leave working out how to build the string as an exercise for the reader.

Note that the structure of a decorator expression is @ <callable object of one parameter, w> <declaration of callable object, f>. It's effect is f = w(f). Accordingly, w (the decorator) has to return a callable of the same type as f.

In @makebold(foo)def bar(x):pass, the expression makebold(foo) is the decorator - that is, the final effect of the decorator is bar = makebold(foo)(bar), and so bar ends up holding wrapped.

The purpose of functools.wraps is to fix up properties of the decorated function to copy over metadata (such as name and docstring) from the argument function to the wrapped function, so that the whole wrapping process is transparent.

Marcin
  • 48,559
  • 18
  • 128
  • 201
  • Probably wouldn't hurt to shove a `functools.wraps` there... – Jon Clements Jul 19 '13 at 21:10
  • Can you explain the layers of decorators including the functools decorator? – Stephan Jul 19 '13 at 21:19
  • 1
    @Stephan The `functools` decorator has [documentation](http://docs.python.org/3.4/library/functools.html#functools.wraps) that explains it adequately. That said, explaining *why* the structure of the solution has to look this way might be a good idea. – millimoose Jul 19 '13 at 21:31
  • 1
    A function that returns a function that takes a function and returns a function. I call this functional programming! – rodrigo Jul 19 '13 at 21:46
  • @rodrigo Hilariosly enough, type-safe FP languages actually can't express this pattern. That is, at least in the straightforward model where every function has a fixed number of typed parameters, there is no way to express "`f` is a function that takes as its parameter a function `g -> ... -> a`, and returns a function `h -> ... -> b` where `...` is an arbitrary set of parameters. – millimoose Jul 19 '13 at 22:23
  • @rodrigo Next step, let's explore pointfree programming in python! (It's actually pretty easy). – Marcin Jul 20 '13 at 16:48
  • Whats point free python? And can you point me in the direction of a document about it... for free? :-) – Luis De Siqueira Jul 22 '13 at 22:47
  • @LuisDeSiqueira Point-free programming is a style of programming where you compose functions, so you don't refer to the arguments directly. See: http://www.haskell.org/haskellwiki/Pointfree and http://stackoverflow.com/questions/944446/what-is-point-free-style-in-functional-programming – Marcin Jul 22 '13 at 23:01
2

I'm perhaps dubious that this is a good use case for decorators, but here:

import string

SelfClosing = object()

def escapeAttr(attr):
    # WARNING: example only, security not guaranteed for any of these functions
    return attr.replace('"', '\\"')

def tag(name, content='', **attributes):
    # prepare attributes
    for attr,value in attributes.items():
        assert all(c.isalnum() for c in attr)  # probably want to check xml spec
    attrString = ' '.join('{}="{}"'.format(k,escapeAttr(v)) for k,v in attributes.items())

    if not content==SelfClosing:
        return '<{name} {attrs}>{content}</{name}>'.format(
            name = name,
            attrs = attrString,
            content = content
        )
    else:  # self-closing tag
        return '<{name} {attrs}/>'

Example:

def makeBoldWrapper(**attributes):
    def wrapWithBold(origFunc):
        def composed(*args, **kw):
            result = origFunc(*args, **kw)
            postprocessed = tag('b', content=result, **attributes)
            return postprocessed
        return composed
    return wrapWithBold

Demo:

@makeBoldWrapper(attr1='1', attr2='2')
def helloWorld(text):
    return text

>>> print( helloWorld('Hello, world!') )
<b attr2="2" attr1="1">Hello, world!</b>

The common misconception with decorators is that the parameters (attr1=...) are parameters to the decorator @myDecorator; that is not the case. Rather the result of the function call myDecoratorFactory(attr1=...) is calculated as someresult and becomes an anonymous decorator @someresult. Therefore 'decorators with arguments' are actually decorator factories that need to return a decorator as a value.

ninjagecko
  • 88,546
  • 24
  • 137
  • 145
  • You should probably use `functools.wraps` in your code samples. It's not necessary but clearly a "best practice". – millimoose Jul 19 '13 at 21:29
  • @millimoose: I personally have a love-hate relationship with `partial` since I've had issues with it in the past. I feel it's kind of klunkey and destroys method signatures, though currying is good. Even though `@wraps` exists to allegedly preserve the name of the function and docstring, if I wanted that, imho it'd be better to use a 'real' decorator framework that gets everything right. Unnecessary decorators also can create a significant amount of overhead, which I once tried to ignore but failed. Once made a framework that rewrites the function and recompiles it into what it 'should be'. – ninjagecko Jul 20 '13 at 12:59
  • @millimoose: [continued] But perhaps you're right that I should be using it, for no other reason that perhaps forward-compatibility, perhaps replacing it with another `from myfuture import wraps` that might be swapped out to do something else. Still not sure if I should use it in examples though because that might increase the required brainpower to figure out what things are doing, since it essentially does nothing (it's been a while since I used it, perhaps I'm ignoring *args or **kw convenience?). Thank you for the suggestion though. – ninjagecko Jul 20 '13 at 13:03
  • 1
    As of Python 3.3, `functools.wraps` supports preserving function signatures as visible with [`inspect.signature()`](http://docs.python.org/3/library/inspect.html#introspecting-callables-with-the-signature-object). (Unfortunately not `getargspec()`, for some reason the two mechanisms weren't unified.) So the "'real' decorator framework' you desire is, in fact, `wraps()`. (I admit I didn't know this from the top of my head, it just occured me that maybe someone on the Python dev team thought of this - turns out they did.) – millimoose Jul 20 '13 at 16:29
  • @millimoose: Ooh nice, thanks. I might then use it for when performance is not an issue. – ninjagecko Jul 20 '13 at 16:55
1

In order to do something like this, you would need a function that returns a decorator function. So in this case (assuming you want to accept arbitrary attributes), you would write

def format_attribs(kwargs):
    """Properly formats HTML attributes from a dictionary"""
    return ' '.join('{}="{}"'.format(key, val) for key,val in kwargs.iteritems())

def makeBold(**kwargs):
    attribs = format_attribs(kwargs)
    def _makeBold(fn):
        def wrapped():
            return '<b ' + attribs + '>' + fn() + '</b>'
        return wrapped
    return _makeBold

In order to make this makeBold function a little more general, you want to pass arguments through to fn and keep other information such as function name using functools.wraps:

import functools
def makeBold(**kwargs):
    attribs = format_attribs(kwargs)
    def _makeBold(fn):
        @wraps(fn)
        def wrapped(*args, **kwargs):
            return '<b ' + attribs + '>' + fn(*args, **kwargs) + '</b>'
        return wrapped
    return _makeBold
murgatroid99
  • 19,007
  • 10
  • 60
  • 95