Skip to content

Instantly share code, notes, and snippets.

@hjwp
Last active August 24, 2017 15:27
Show Gist options
  • Save hjwp/6b17e8005dd92f3857ac to your computer and use it in GitHub Desktop.
Save hjwp/6b17e8005dd92f3857ac to your computer and use it in GitHub Desktop.
Baroque, the decorating decorator decorator

Harry Percival @hjwp www.obeythetestinggoat.com www.pythonanywhere.com

Decorators, don't you love them?

url: /user/<username>/my_stuff
def owner_or_admin(view):
    @wraps(view)
    def wrapped_view(request, username, *args, **kwargs):
        if request.user.username != username and not request.user.is_staff:
            return HttpResponseForbidden(render_to_string("403.html", context_instance=RequestContext(request)))
        return view(request, get_object_or_404(User, username=username), *args, **kwargs)

    return wrapped_view


@owner_or_admin
def my_private_stuff(request, owner):
    #bla

But testing:

class MyPrivateStuffViewTest(TestCase):
    def test_non_owner_cannot_view(self):
        self.client.login(non-user,whatever)
        response = self.client.get('/user/realuser/my_stuff/')
        self.assertEqual(response.status_code, 403)

    def test_non_owner_cannot_view(self):
        self.client.login(admin_user,whatever)
        self.client.get('/user/realuser/my_stuff/')
        self.assertEqual(response.status_code, 200)

For every view that's decorated! No thanks!

How about this?

def owner_or_admin(view):
    @wraps(view)
    def wrapped_view(request, username, *args, **kwargs):
        if request.user.username != username and not request.user.is_staff:
            return HttpResponseForbidden(render_to_string("403.html", context_instance=RequestContext(request)))
        wrapped_view.decorated_with_owner_or_admin = True
        return view(request, get_object_or_404(User, username=username), *args, **kwargs)


def test_view_is_decorated_with_owner_or_admin
    from app.views import my_stuff
    self.assertTrue(my_stuff.decorated_with_owner_or_admin)

Better!

But what about all my other decorators?

    @authorized_user

    @create_new_profile_if_necessary

    @definitely_not_send_all_info_to_nsa

Can you see where this is going?

PRESENTING....

@baroque, THE DECORATING DECORATOR DECORATOR
class TestBaroque(unittest.TestCase):

    def test_decorates_decorated_function_with_name_of_decorators(self):

        @baroque
        def my_decorator(func):
            return func

        @my_decorator
        def my_function():
            pass

        self.assertEquals(my_function.decorated_by, set(["my_decorator"]))
    def test_works_for_multiple_decorators(self):

        @baroque
        def my_decorator1(func):
            return func

        @baroque
        def my_decorator2(func):
            return func

        @my_decorator1
        @my_decorator2
        def my_function():
            pass

        self.assertEquals(
            my_function.decorated_by,
             set(["my_decorator1", "my_decorator2"])
        )
    def test_class_based_decorators(self):
        #...


    def test_decorators_that_take_arguments(self):
        #... this is hard!

And don't forget this test:

    def test_decorated_decorator_still_works(self):
        @baroque
        def decorator(func):
            def inner(*args):
                return func(*args) + 2
            return inner

        @decorator
        def foo(x):
            return x + 1

        self.assertEquals(foo(2), 5)

Implementation is left as an exercise for the reader....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment