Flask: Add a function app.parse_route()

Created on 21 May 2020  路  7Comments  路  Source: pallets/flask

Expected Behavior

Flask should have a function, hypothetically called parse_route, that works as a reverse url_for. That is, given a path it will return the matching endpoint and its arguments. This is useful, at least for my use case, for knowing which endpoint and arguments will be matched by a path (a URL string). Or if I have a view with multiple routes applied to it, I could parse a string and know which route will be matched with which arguments. I've seen other people searching for a similar solution on the web and they came up with weird solutions trying to reverse the behavior of url_for.

Actual Behavior

Flask doesn't not seem to have any such function, but it seems to be possible to get this behavior by using some of the werkzeug internals. In particular using class werkzeug.routing.MapAdapter like this app.url_map.bind('').match(path), the match() function seems to work as expected. The problem is that this is very much buried down into the werkzeug internals and almost impossible to find for flask users like me.

Environment

  • Python version: 3.7.3
  • Flask version: 1.1.2
  • Werkzeug version: 1.0.1

Most helpful comment

@davidism I can't help but agree with @sharpaper that your response to this issue is disappointing.

Regarding your comparison with Django's resolve() function, the only reason it is better is because Flask doesn't have one! You are comparing a properly documented and supported Django function against a bunch of Flask/Werkzeug obscure/internal calls that as agreed also by @sharpaper and @ThiefMaster, are far from trivial to figure out.

Bottle.py also has a documented function for this.

I don't know much about HATEOS

First of all, it is HATEOAS, which stands for "Hypertext As The Engine of Application State". If you were familiar with it you would know that it is not a specific technique, but a general principle. You can't go look at a particular implementation and claim that if their implementation doesn't do X then X is not HATEOAS. The general idea is to expose resource URLs as first-class citizens in your API. This means that for some APIs (and I get that Eve decided not to do this) you could have clients sending resource URLs in payloads, and the server needs to decode these URLs to find out which resource is being referenced.

Lastly, I've seen people do crazy things in application code to get this information because it's really not obvious how to do this properly:

with app.test_request_context(YOUR_URL) as request_ctx:
    url_rule = request_ctx.request.url_rule

Since this is not a problem for me personally I'm going to forget about it, now that I said what I had to say about it. If eventually you come around to the idea of adding this, ping me and I'll adapt my function, which closely matches url_for(), and submit it in a PR. Thanks.

All 7 comments

As you've pointed out, this is provided by Werkzeug, it is not something that needs to be provided by Flask.

That was a rather disappointing ending. Not much for the rejection, but for the complete lack of discussion, the hurried dismissal, and for the weak argument against the proposal.
By the same logic, Flask shouldn't provide url_for either since werkzeug.routing.MapAdapter defines a .build() function that can build URLs given an endpoint name and a dictionary of arguments.
I hope you will reconsider the proposal.

Are there many usecases for it? I implemented it in my app a long time ago (code below), but it's used only in very uncommon cases so I'm not sure how useful it'd be as a part of flask...

OTOH, having a utility function that integrates nicely with flask (using the request / SERVER_NAME to bind the url map adapter, and handle RequestRedirect properly) would not necessarily be bad. Because you are right that it's not straightforward especially when you aren't familiar with Flask/Werkzeug internals...


def endpoint_for_url(url, base_url=None):
    if base_url is None:
        base_url = my_config.BASE_URL
    base_url_data = url_parse(base_url)
    url_data = url_parse(url)
    netloc = url_data.netloc or base_url_data.netloc
    # absolute url not matching our hostname
    if url_data.netloc and url_data.netloc != base_url_data.netloc:
        return None
    # application root set but the url doesn't start with it
    if base_url_data.path and not url_data.path.startswith(base_url_data.path):
        return None
    path = url_data.path[len(base_url_data.path):]
    adapter = current_app.url_map.bind(netloc)
    try:
        return adapter.match(path)
    except RequestRedirect as exc:
        return endpoint_for_url(exc.new_url)
    except HTTPException:
        return None

This is useful, at least for my use case, for knowing which endpoint and arguments will be matched by a path (a URL string).

I don't think this is a common enough use case to warrant us maintaining a specific function in Flask, especially because it's effectively solved with request.url_adapter.match() (which is the bound version of app.url_map, already provided by Flask). This does basically what ThiefMaster's function does except handling routing redirects.

Or if I have a view with multiple routes applied to it, I could parse a string and know which route will be matched with which arguments.

You know which route is matched because Flask matches and calls the endpoint associated with it. If you actually need to distinguish between different routes, then you can use different endpoint names and check request.endpoint, or different view functions entirely.

the complete lack of discussion, the hurried dismissal, and for the weak argument against the proposal

I was already sure, based on the information provided, that I didn't think this should be added to Flask. "It's not directly in Flask" seemed to be the argument for it, which is not a reason on its own to add it. There are all sorts of things that Flask does not expose directly from the things it wraps.

Flask shouldn't provide url_for either

Building URLs to link to other things is an incredibly common action in web apps, whereas I don't see matching URLs outside of the routing that already happens as particularly common.

Thanks for reopening the discussion, @davidism.

I actually use this in some of my projects. I agree with the OP that it is a useful function and would welcome an addition to Flask, because as mentioned above, it is cumbersome to do this through Werkzeug and totally non-obvious unless you are used to search through code to find what you need.

The case in which this is needed is when you create an API that follows the HATEOAS principle, which requires you to use the resource URL as its unique id. For example, in a social network type app, if user_id 57 wants to start following user_id 86 you could send a POST request to https://example.com/users/57/followers passing {"user": "https://example.com/users/86"} as payload. The server then needs to decode the resource URL to determine what resource is the client referencing.

Not that this should guide this discussion, but I want to note that Django provides this functionality in its URL resolver with the resolve() function.

Here is a link to my implementation: https://github.com/miguelgrinberg/oreilly-flask-apis-video/blob/master/orders/app/utils.py#L7-L34.

Django's resolve appears to be request.url_adapter.match(), returning an object that collects some information (the only relevant thing match doesn't return is the view function, which can be grabbed from app.view_functions). I don't know much about HATEOS, but that sounds like something appropriate for an API framework on top of Flask. Eve, which advertises HATEOS support, doesn't seem to use this sort of resolver anywhere.

@davidism I can't help but agree with @sharpaper that your response to this issue is disappointing.

Regarding your comparison with Django's resolve() function, the only reason it is better is because Flask doesn't have one! You are comparing a properly documented and supported Django function against a bunch of Flask/Werkzeug obscure/internal calls that as agreed also by @sharpaper and @ThiefMaster, are far from trivial to figure out.

Bottle.py also has a documented function for this.

I don't know much about HATEOS

First of all, it is HATEOAS, which stands for "Hypertext As The Engine of Application State". If you were familiar with it you would know that it is not a specific technique, but a general principle. You can't go look at a particular implementation and claim that if their implementation doesn't do X then X is not HATEOAS. The general idea is to expose resource URLs as first-class citizens in your API. This means that for some APIs (and I get that Eve decided not to do this) you could have clients sending resource URLs in payloads, and the server needs to decode these URLs to find out which resource is being referenced.

Lastly, I've seen people do crazy things in application code to get this information because it's really not obvious how to do this properly:

with app.test_request_context(YOUR_URL) as request_ctx:
    url_rule = request_ctx.request.url_rule

Since this is not a problem for me personally I'm going to forget about it, now that I said what I had to say about it. If eventually you come around to the idea of adding this, ping me and I'll adapt my function, which closely matches url_for(), and submit it in a PR. Thanks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tomjaguarpaw picture tomjaguarpaw  路  3Comments

davidism picture davidism  路  3Comments

stillesjo picture stillesjo  路  4Comments

xliiv picture xliiv  路  3Comments

maangulo12 picture maangulo12  路  4Comments