My code is behaving weird, and I can't get why.
Here is the code:
from django.urls import path
app_name = 'portal'
urlpatterns = []
def route(url, name=""):
def dec(f):
f_name = name or f.__name__
urlpatterns.append(
path(url, f, name=f_name)
)
return f
return dec
from . import views
# 2) The decorator call in the other file
from . import urls
@urls.route("/my_function")
def my_function():
print("Hello world")
I get an UnboundLocalError
on name
File "urls.py", line 10, in dec
if name == "":
UnboundLocalError: local variable 'name' referenced before assignment
name
should be set to ""
by default, I don't understand where the problem is.
The weird thing is if I run the same code and change the decorator to this:
urlpatterns = []
def route(url, name=""):
def dec(f):
if name == "":
print("I work!")
urlpatterns.append(
path(url, f, name=name)
)
return f
return dec
It works perfectly and output:
I work !
while the problem was supposed to come from the line if name == ""
PS: I'm programming on django, this line is in the urls.py
file.
The answer is in the part you didn't posted. Your real code actually looks something like
def route(url, name=""):
def dec(f):
if name == "":
# here's the real issue
name = "something_" + f.__name__
urlpatterns.append(
path(url, f, name=name)
)
return f
return dec
Assigning to name
makes it a local variable - Python has not variable declaration, so it's the place where a name is bound that defines its scope. In your case, assigning to name in dec
makes the name "name" local to dec
, so it's NOT looked up in the enclosing scopes. And since you test it ("reference") before you assign to it, you get the very obvious (nah, just kidding) " local variable 'name' referenced before assignment" error.
The solution here is to declare name
as "nonlocal" at the top of your dec
function, so Python knows it has to be looked up in the enclosing scope (or, more exactly, in the closure's cells which capture the dec
function's environment):
def route(url, name=""):
def dec(f):
nonlocal name
if name == "":
# here's the real issue
name = "something_" + f.__name__
urlpatterns.append(
path(url, f, name=name)
)
return f
return dec
Note that this only works for Python3 - if using Python2, you'll have to resort to a hack to emulate this behaviour:
def route(url, name=""):
# Py2 hack: wrap the "nonlocal" variable in
# a mutable container
name = [name]
def dec(f):
if name[0] == "":
name[0] = "something_" + f.__name__
urlpatterns.append(
path(url, f, name=name[0])
)
return f
return dec
The hack here bing that you mutate name
instead of rebinding it, so Python doesn't mark it as a local variable.
As a side note, I wouldn't recommand trying to port this @route(url)
idiom (Flask anyone ?) to Django. First because separating the view definitions from the url mapping is a deliberate design decision, which allow to remap a third-part apps urls to whatever we want without hacks nor forks etc, and also because most Django devs expect urls to be explicitely defined in the urls.py
module and will hate you for not following the convention. Now you're of course free to write your project as you want, but following conventions makes it easier for everyone. My 2 cents...