简体   繁体   中英

Decorator to tag functions as callable

I'm creating a function tagging system, to enable or disable functions based on tags:

def do_nothing(*args, **kwargs): pass

class Selector(set):
    def tag(self, tag):
        def decorator(func):
            if tag in self:
                return func
            else:
                return do_nothing
        return decorator

selector = Selector(['a'])

@selector.tag('a')
def foo1():
    print "I am called"

@selector.tag('b')
def foo2():
    print "I am not called"

@selector.tag('a')
@selector.tag('b')
def foo3():
    print "I want to be called, but I won't be"

foo1() #Prints "I am called"
foo2() #Does nothing
foo3() #Does nothing, even though it is tagged with 'a'

My question is about the last function, foo3. I understand why it isn't being called. I was wondering if there's a way to make it so that it is called if any of the tags are present in the selector. Ideally, the solution makes it so the tags are only checked once, not every time the function is called.

A side note: I'm doing this to select tests to run based on environment variables in unittest unit tests. My actual implementation uses unittest.skip .

EDIT: Added the decorator return.

The issue is that if you decorate it twice, one returns the function, one returns nothing.

foo3() -> @selector.tag('a') -> foo3()
foo3() -> @selector.tag('b') -> do_nothing

foo3() -> @selector.tag('b') -> do_nothing
do_nothing -> @selector.tag('a') -> do_nothing

This means, in whatever order, you will always get nothing. What you need to do is keep a set of tags on each object, and check that whole set at once. We can do this nicely without polluting namespaces with function attributes:

class Selector(set):
    def tag(self, *tags):
        tags = set(tags)
        def decorator(func):
            if hasattr(func, "_tags"):
                func._tags.update(tags)
            else:
                func._tags = tags
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs) if self & func._tags else None
            wrapper._tags = func._tags
            return wrapper
        return decorator

This gives some bonuses - it's possible to inspect all the tags the function has, and it's possible to tag with multiple decorators or by giving many tags in a single decorator.

@selector.tag('a')
@selector.tag('b')
def foo():
    ...


#Or, equivalently:
@selector.tag('a', 'b')
def foo():
    ...

The use of functools.wraps() also means the function keeps it's original 'identity' (docstrings, name, etc...).

Edit: If you wanted to do some wrapper elimination:

    def decorator(func):
        if hasattr(func, "_tagged_function"):
            func = func._tagged_function
        if hasattr(func, "_tags"):
            func._tags.update(tags)
        else:
            func._tags = tags
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs) if self & func._tags else None
        wrapper._tagged_function = func
        wrapper._tags = func._tags
        return wrapper

Would that work for you:

class Selector(set):
    def tag(self, tag_list):
        def decorator(func):
            if set(tag_list) & self:
                return func
            else:
                return do_nothing
        return decorator


@selector.tag(['a','b'])
def foo3():
    print "I want to be called, but I won't be"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM