Flask: Cannot lazily load views in 0.10.1

Created on 13 Feb 2014  路  12Comments  路  Source: pallets/flask

I was lazily loading views in 0.9, and some views needed to use the same function, e.g.

url('/<lang:lang>/blog/', 'blog.views.blog_index', defaults={'page': 1})
url('/<lang:lang>/blog/page/<int:page>', 'blog.views.blog_index')

Upon updating Flask to 0.10.1, I now get the following error:

AssertionError: View function mapping is overwriting an existing endpoint function

The cause is the same as #796, but your current documentation still says this is a valid way of having a central URL map.

Is there a way around this, beyond just storing every single LazyView in a dict and looking it up each time url() is called? The above documentation needs to be updated to reflect the answer.

Most helpful comment

Working on this at pycon 2016 sprint.

When assigning multiple routes to a function using the decorator, the function signature is the same and the assertionerror in add_url_rule() is avoided. When using the LazyView object, different object signatures are returned, which causes the assertionerror to be raised.

We discussed several options for fixing this:

  1. Modify the example in the docs to use metaclasses to cache and return the same object when called with the same import_name. Object signatures would match in add_url_rule() and the assertionerror would be avoided.
  2. Modify the example in the docs to specify a endpoint in the LazyView:

python app.add_url_rule('/exampleTwo/', endpoint='exampleTwo()', view_func=LazyView('mytest.views.exampleTwo')) app.add_url_rule('/exampleTwo/<name>', endpoint='exampleTwo(name)', view_func=LazyView('mytest.views.exampleTwo'))

@davidism

All 12 comments

The documentation however does not include functions with the same name.. See views.index vs views.user. Right now, I am starting to build a small application for the first time using Flask and had a function based view and was using the route-decorator. I then read about class based views and wanted to switch, using http://flask.pocoo.org/docs/views/ as a guideline. Then this error was raised when I switched from

# feed is my blueprint
@feed.route('/', defaults={'page': 'index'})
@feed.route('/<page>')
def list(page):

to

feed.add_url_rule('/', defaults={'page': 'index'}, view_func=List.as_view('list'))
feed.add_url_rule('/<page>', view_func=List.as_view('list'))

The first example creates the following map:

Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
 <Rule '/<page>' (HEAD, OPTIONS, GET) -> feed.list>,
 <Rule '/' (HEAD, OPTIONS, GET) -> feed.list>])

Which creates two rules for a single endpoint: feed.list.

But using add_url_rule raises AssertionError: View function mapping is overwriting an existing endpoint function: feed.list

I guess there is no way around it, you could try using the add_url_rule function instead and provide different names for the endpoints instead. I'd assume you have to update some url_for-calls in your templates for when you do/don't provide page numbers.

I suggest you to try my extension named Flask-LazyViews, cause there already fixed same issue and you can register multiple lazy views pointing to one function at same time

Working on this at pycon 2016 sprint.

When assigning multiple routes to a function using the decorator, the function signature is the same and the assertionerror in add_url_rule() is avoided. When using the LazyView object, different object signatures are returned, which causes the assertionerror to be raised.

We discussed several options for fixing this:

  1. Modify the example in the docs to use metaclasses to cache and return the same object when called with the same import_name. Object signatures would match in add_url_rule() and the assertionerror would be avoided.
  2. Modify the example in the docs to specify a endpoint in the LazyView:

python app.add_url_rule('/exampleTwo/', endpoint='exampleTwo()', view_func=LazyView('mytest.views.exampleTwo')) app.add_url_rule('/exampleTwo/<name>', endpoint='exampleTwo(name)', view_func=LazyView('mytest.views.exampleTwo'))

@davidism

Did you make a decision here on which solution to add to the docs @dcfix @davidism?

Personally, since the linked doc is an entire page devoted to lazy views, I would just add a section mentioning the problem and example code for both solutions, and leave it up to the end user to pick their desired solution. Not a strongly held opinion, just what I'd do if it were me.

@dcfix IIRC, you were planning to put together a PR on this... I'd encourage you to start on it while it's still fresh in your mind, since I know you did a lot of great work digging into the internals here...

Enhance the error message that occurs in add_url_rule. Showing something like originally registered with <MyClass at 12345>, new callable is <MyClass at 67890> would be a big hint about what went wrong, along with some documentation.

If you want to register a lazy view for two rules, create the view and then pass it to both rules.

hello_view = LazyView('hello')
app.add_url_rule('/', view_func=hello_view)
app.add_url_rule('/hello', view_func=hello_view)

This avoids using custom endpoint names, since technically they are still the same endpoint. A similar blurb should be added to the class based views docs, since the same issue occurs.


Modifying the LazyView class to cache instantiation is too much of a hack to include in the docs. But here's how it would work anyway.

class LazyView(object):
    _cache = {}

    def __new__(cls, name):
        if name not in cls._cache:
            cls._cache[name] = object.__new__(cls)

        return cls._cache[name]

    ...

LazyView('hello') is LazyView('hello')  # True

Modifying the LazyView class to cache instantiation is too much of a hack to include in the docs. But here's how it would work anyway.

I've been doing a variation of this (but in the url() function instead of the LazyView class) in the interim to solve the problem I originally created this issue for.

Ok, I've got a solution that will give us what we need by changing a few lines in the lazyloading.rst file. I've tested it and it works like a charm, but I've got to get Sphinx up and running on my system so that I can test it before creating a pr. Hopefully tonight or tomorrow.

Hmm, this solution doesn't handle default values. Perhaps passing in a list of tuples of (path, **options)? But that's not as clean...

@str4d , I wasn't able to replicate the problem with default values that you mentioned above, using the proposed sample code. I was looking at the code base that you referenced, and it seems much more complex than our simple example.

In order to test my proposed change, I had to set up the function to accept the default. i.e.

def index(page=1):
    return render_template('index.html', page=page)

I then set up my url statement as follows:

url('981_test.views.index', ['/'], defaults={'page':3})

When I went to the page, the original default of 1 was overwritten with the value of 3.

So, it seems to work for me, but I'm not even sure if I understand the problem fully. Would you mind taking a look at the solution I've proposed with our simple example in the doc?

Thanks

I was meaning that different defaults for different URLs is not possible. But yes, if the defaults are the same for the same view handler for all possible URLs, then yes it works as-is.

For me, the hardest part of writing this solution was deciding where to stop adding edge cases for a self-proclaimed micro framework. To me, your solution has advanced way past our simple lazy loading pattern. Do you think we should create and submit another pattern for your use case?

Maybe. I don't actually _use_ my use case above; what I should have said was, the documentation should note that any provided options are applied to all URLs, so people don't trip up on it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rkomorn picture rkomorn  路  3Comments

ghost picture ghost  路  3Comments

sungjinp11 picture sungjinp11  路  3Comments

ghostbod99 picture ghostbod99  路  4Comments

jab picture jab  路  4Comments