How do I use url_for if my method has multiple route annotations?

Question:

So I have a method that is accessible by multiple routes:

@app.route("/canonical/path/")
@app.route("/alternate/path/")
def foo():
    return "hi!"

Now, how can I call url_for("foo") and know that I will get the first route?

Asked By: jiggy

||

Answers:

Ok. It took some delving into the werkzeug.routing and flask.helpers.url_for code, but I’ve figured out. You just change the endpoint for the route (in other words, you name your route)

@app.route("/canonical/path/", endpoint="foo-canonical")
@app.route("/alternate/path/")
def foo():
    return "hi!"

@app.route("/wheee")
def bar():
    return "canonical path is %s, alternative is %s" % (url_for("foo-canonical"), url_for("foo"))

will produce

canonical path is /canonical/path/, alternative is /alternate/path/

There is a drawback of this approach. Flask always binds the last defined route to the endpoint defined implicitly (foo in your code). Guess what happens if you redefine the endpoint? All your url_for('old_endpoint') will throw werkzeug.routing.BuildError. So, I guess the right solution for the whole issue is defining canonical path the last and name alternative:

""" 
   since url_for('foo') will be used for canonical path
   we don't have other options rather then defining an endpoint for
   alternative path, so we can use it with url_for
"""
@app.route('/alternative/path', endpoint='foo-alternative')
""" 
   we dont wanna mess with the endpoint here - 
   we want url_for('foo') to be pointing to the canonical path
"""
@app.route('/canonical/path') 
def foo():
    pass

@app.route('/wheee')
def bar():
    return "canonical path is %s, alternative is %s" % (url_for("foo"), url_for("foo-alternative"))
Answered By: Nemoden

Rules in Flask are unique. If you define the absolute same URL to the same function it will by default clash because you’re doing something which we stop you from doing since from our perspective that is wrong.

There is one reason why you would want to have more than one URL to the absolute same endpoint and that is backwards compatibility with a rule that existed in the past. Since WZ0.8 and Flask 0.8 you can explicitly specify an alias for a route:

@app.route('/')
@app.route('/index.html', alias=True)
def index():
    return ...

In this case if the user requests /index.html Flask will automatically issue a permanently redirect to just /.

That does not mean a function could not be bound to more than one url though, but in this case you would need to change the endpoint:

@app.route('/')
def index():
    ...

app.add_url_rule('/index.html', view_func=index, endpoint='alt_index')

Or alternatively:

@app.route('/')
@app.route('/index.html', endpoint='alt_index')
def index():
    ...

In this case you can define a view a second time under a different name. However this is something you generally want to avoid because then the view function would have to check request.endpoint to see what is called. Instead better do something like this:

@app.route('/')
def index():
    return _index(alt=False)

@app.route('/index.html')
def alt_index():
    return _index(alt=True)

def _index(alt):
    ...

In both of these cases URL generation is url_for('index') or url_for('alt_index').

You can also do this on the routing system level:

@app.route('/', defaults={'alt': False})
@app.route('/index.html', defaults={'alt': True})
def index(alt):
    ...

In this case url generation is url_for('index', alt=True) or url_for('index', alt=False).

Answered By: Armin Ronacher

Additionally, for those using a catch all route constructed with variables: Flask will correctly create the url path if url_for is passed a dictionary containing the variables.

For example…

app.py:

app.route('/<path:pattern1>')
app.route('/<path:pattern1>/<path:pattern2>')
def catch_all(pattern1, pattern2=None):
    return render_template('template.html', p1=pattern1, p2=pattern2)

app.route('/test')
def test_routing:
    args = {'pattern1': 'Posts', 'pattern2': 'create'}
    return render_template('test.html', args=args)

test.html:

<a href="{{url_for('catch_all', **args)}}">click here</a>

When you click on the ‘click here’ link, you will be directed to the ‘Posts/create’ route.

Answered By: vincent