Shoddy Decorators (or - Pain, Tears & Decorators)

Should it be possible to wrap a decorated function with another decorator?

I want to wrap some server functions with a decorator that performs some tests, specifically a load of authentication/authorisation tests that just get a little laborious to write out not to mention adding voluminous lines of code.

I can of course do this (which is more explicit) :

@anvil.server.callable
def members_only():
    if auth_and_auth() is False:
        return False

    # If here you are authed

for every function, but that’s not as neat as (something like) this, which doesn’t work :

@anvil.server.callable
@auth_and_auth
def members_only():
    # This only runs if authed.

If I put the anvil.server.callable first, I get this (kind of expected) :

anvil.server.NoServerFunctionError: 
No server function matching "auth_and_auth" has been registered

If I put it second, it just doesn’t run the decorator.

My inexperience with Python might be showing through here …

Hmmmm…
you seem to be doing exactly what I tried, yet it didn’t work for me. It just didn’t call the decorator. How interesting.

Thanks for sharing - I’m going to pick through that now…

EDIT - are you sure it’s working for you? I can’t get your app to run as it needs a modified users table so i can’t verify it. But I’m doing exactly what you’re doing and it’s just not running the decorator.

I really don’t get this.
Here’s my server module :

import anvil.server

def mydec(func):
  def wrapper():
    print("mydec before func")
    func()
    print("mydec after func")
    
  return wrapper

@mydec
@anvil.server.callable
def myfunc():
  print("Hi")

And I make the call from my form like so :

anvil.server.call('myfunc')

All I get in the output window is :

Hi

when I expect to see :

mydec before func
Hi
mydec after func

Can you see what I’m doing wrong?

I’ve found a workaround, but I think this is just an academic curiosity :slight_smile:
Don’t think it’s actually any easier or clearer than abandoning the decorator for inline code.

But, for the sake of it …

You can use what I call a “dispatcher”. So instead of calling a server function directly, you call the dispatcher with the actual function you want as a parameter (and parameters to the function as further parameters).

For example, in the form :

func_data=["fetch_data",[{"param1":"value1"}]]
anvil.server.call('dispatcher',func_data)

Then in the server module :

@anvil.server.callable
def dispatcher(p=None):
  func=p[0]
  params=p[1]
  globals()[func](params)

@decorators.admin_only
def fetch_data(p):
  print("In function, params passed : ",p)

This way, the decorator is just around the function and not the general dispatcher (which doesn’t seem to work).

Like I say, it works, but…er…probably not worth it.

1 Like

Hi folks,

I have also noticed something odd going on with decorators like this. I will take a look the next time I get five minutes spare!

1 Like

Hi all,

So, here’s what’s happening. When you add the @anvil.server.callable decorator to a function, by default it registers a server function with the name (strictly, __name__) of the function you decorated. So…

@anvil.server.callable
def my_func():
  return 42

… registers a server function called “my_func”. Unfortunately this…


def my_decorator(f):
  def wrapper():
    return 2 * f()
  
  return wrapper

@anvil.server.callable
@my_decorator
def my_func():
  return 42

… registers a server function called “wrapper”, because that’s the name of the function passed to the callable decorator. And @anvil.server.callable must come first, otherwise we won’t call our custom decorator.

The solution here is to manually specify the name of the server function, like so:


def my_decorator(f):
  def wrapper():
    return 2 * f()
  
  return wrapper

@anvil.server.callable("my_func")     # Notice manual name registration
@my_decorator
def my_func():
  return 42

We have to do this, because @anvil.server.callable can no-longer work out what the function should be called.

So, to sum up:

  • Always use @anvil.server.callable as the first decorator to your functions
  • If using multiple decorators, explicitly pass the name you want to use to call the function as an argument to @anvil.server.callable.

Hope that helps!

6 Likes

Thank you Ian, that’s perfect!

Can you get us a clone link? It should work, as long as @anvil.server.http_endpoint is the first decorator…

As an alternative you could use the functools wraps module which will preserve the __name__ of a wrapped function

from functools import wraps 

def my_decorator(f):
  @wraps(f)   # this will preserve the __name__ of the function when it's wrapped
  def wrapper():
    return 2 * f()
  
  return wrapper

@anvil.server.callable     # no need to manually set the name with functools wrap implemented
@my_decorator
def my_func():
  return 42

5 Likes