Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blueprint options from register_blueprint #612

Closed
mizhi opened this issue Oct 16, 2012 · 16 comments
Closed

blueprint options from register_blueprint #612

mizhi opened this issue Oct 16, 2012 · 16 comments

Comments

@mizhi
Copy link

mizhi commented Oct 16, 2012

I've written a flask app that uses a blueprint to implement an API. When I create the server, I use register_blueprint to make the api available. I added a url_prefix parameter as a parameter to the call to register_blueprint. Thus, in theory, I could add multiple versions of this api for different instances.

As part of the blueprint, there is a particular method that needs to send a redirect to the requesting browser. This redirect is to another part of the blueprint. The problem is that url_prefix does not seem to be available within the request or current_app objects. At least, not in anyway that I've been able to see.

Here's some code that illustrates the problem:

# server.py
blueprint = flask.Blueprint("mybp", __name__)

@blueprint.before_request
def before_request():
    if not flask.request.url.endswith("/auth/login/"):
        return flask.redirect( "/auth/login/")

And here's the part where the app server is created:

app = flask.Flask("myapp")
app.register_blueprint(server.blueprint, url_prefix="/instance1")
app.register_blueprint(server.blueprint, url_prefix="/instance2")

The correct behaviors would be to go to "/instance1/auth/login" or "/instance2/auth/login." The problem is that the redirect code doesn't have a way (that I can see) of getting the url_prefix that was used to register the blueprint.

My initial thought was to see if I could get a hold of the Blueprint object that was registered on the app and maybe it would have the url_prefix that I could prepend on the redirect line. This would look something like:

@blueprint.before_request
def before_request():
    if not flask.request.url.endswith("/auth/login/"):
        url_prefix = flask.current_app.blueprints[flask.request.blueprint].url_prefix
        return flask.redirect(url_prefix + "/auth/login/")

The problem is that the way blueprint is stored in Flask.register_blueprint (https://github.com/mitsuhiko/flask/blob/master/flask/app.py#L867):

self.blueprints[blueprint.name] = blueprint

means that each separate registration of blueprint shares the same blueprint object and url_prefix may not be valid between registrations. E.g. it's not enough to simply set url_prefix on the blueprint.

My solution to this was to create a factory method:

def make_blueprint(name, url_prefix):
    blueprint = flask.Blueprint(name, __name__, url_prefix=url_prefix)

    #
    # Blueprint @route definitions follow
    #

    return blueprint

and in the server creation code:

app = flask.Flask("myapp")
app.register_blueprint(server.make_blueprint("instance1", "/instance1"))
app.register_blueprint(server.make_blueprint("instance2", "/instance2"))

This works, and I have access to url_prefix in the instances. It wasn't critical for my project, but it was a surprising property of Flask.

It seems awkward, and a violation of the principle-of-least surprise. Why wouldn't the blueprint methods have access to the url_prefix (or any other options passed in during blueprint registration)? Are there any more elegant solutions than the one above?

@ghost
Copy link

ghost commented Oct 17, 2012

Why not to use url_for? A relative url (.login) should work...

@mizhi
Copy link
Author

mizhi commented Oct 17, 2012

Unfortunately, not. According to the docs, that should work because it returns the url relative to the blueprint. When I actually test it out, however, I get different behavior.

Consider this code:

# server.py
blueprint = flask.Blueprint("mybp", __name__)

@blueprint.before_request
def before_request():
    print flask.url_for(".login")

@blueprint.route("/auth/login/", methods=["GET"])
def login():
    flask.current_app.logger.debug("Going to login screen.")
    return flask.make_response("Login screen")

And

app = flask.Flask("myapp")
app.register_blueprint(server.blueprint, url_prefix="/instance1")
app.register_blueprint(server.blueprint, url_prefix="/instance2")

When I hit /instance1/something then this code should print out /instance1/auth/login
When I hit /instance2/something then this code should print out /instance2/auth/login

Instead, for both endpoints, it prints out /instance1/auth/login for both endpoints. The behavior depends on the order in which the blueprint is registered, so for example:

app = flask.Flask("myapp")
app.register_blueprint(server.blueprint, url_prefix="/instance2")
app.register_blueprint(server.blueprint, url_prefix="/instance1")

The url printed out would be /instance2/auth/login

This seems to be unintended behavior for a blueprint that has been attached to multiple prefixes in an application.

@ghost
Copy link

ghost commented Oct 17, 2012

Ok, just checked the source. I have no clue what to do.
I don't like too much the fact we need that __name__ at creation time.
Can we do something like this:

blueprint = flask.Blueprint("mybp")

@blueprint.before_request
def before_request():
    print flask.url_for(".login")

@blueprint.route("/auth/login/", methods=["GET"])
def login():
    flask.current_app.logger.debug("Going to login screen.")
    return flask.make_response("Login screen")

and then add a custom name while registering like this:

app = flask.Flask("myapp")
app.register_blueprint(server.blueprint, url_prefix="/instance2", name="instance2")
app.register_blueprint(server.blueprint, url_prefix="/instance1", name="instance1")

I think this is more elegant than other solutions and not so difficult to implement. We could simply defer the initialization of _PackageBoundObject at registration time, right?

@mizhi
Copy link
Author

mizhi commented Oct 17, 2012

Since Blueprint is a subclass of _PackageBoundObject, I'm not sure that would work well. The key problem is that the blueprints can't really be used to create multiple endpoints with distinct properties. Currently, you can create multiple endpoints, but they share too many commonalities. E.g. url_prefix. Thus there's no way do things like redirect within the same blueprint correctly. The factory function I wrote above gets around this, but essentially by creating a new blueprint for every url_prefix. Seems that somehow a distinction needs to be drawn between blueprint properties and blueprint registration properties.

@ghost
Copy link

ghost commented Oct 17, 2012

Actually, I'm pretty sure it's possible. You can simply do:

class Dummy(object):
    def __init__(self):
        print 'This will be printed only calling initialize_dummy.'

class MyClass(Dummy):
    def __init__(self):
        print 'Initializing MyClass'

    def initialize_dummy(self):
        Dummy.__init__(self)

MyClass().initialize_dummy()

But do blueprints need to access _PackageBoundObject attributes/methods before the registration? This is my question.

The problem you're having is this: Flask uses that __name__ as unique identifier. You can't use the same name twice, because it's used by the routing system to determine the relative path. Being able to see url_prefix within a blueprint could be a solution, but there will be problems using the url_for system externally: url_for('server.login') can resolve to instance1.login and instance2.login. This is the same problem you where having using url_for('.login').

My idea banally makes you able to define the name at registration time, thus you can use the same blueprint object twice by changing that unique identifier. This would allow to use url_for('instance1.login') and url_for('instance2.login'). That's a bit more flasky, I think, to solve routing problems, but this is not the right way if you'd like to use custom configuration for each blueprint. Mixins come to mind...

@mizhi
Copy link
Author

mizhi commented Oct 17, 2012

Oh, it would work mechanically - never said it wouldn't. It's a question of the semantics of leaving an object partially init'ed after instantiation. Somehow that seems... wrong. :)

The problem is that __name__ isn't what's actually used to set the app.blueprints dictionary. The first parameter is what's used to identify the blueprint. The second, __name__, is used to construct the fully qualified endpoint names for the werkzeug Rules. Taking that out would likely break things.

Regardless, after noodling on this a bit - and becoming very familiar with the internals of flask - I think the best, least abrasive option is to simply use the factory pattern above. I may take a whack at writing code that allows access to the options passed in during blueprint registration at a later time.

@ghost
Copy link

ghost commented Oct 18, 2012

Ok, you're indeed right ;-) BTW, you don't have to use __name__, of course!

@cbsmith
Copy link

cbsmith commented Mar 26, 2013

I'm not clear on how this was resolved. I am trying to have the same blueprint registered with multiple prefixes, and to have render various non-relative URL's I need to generate use the prefix_url to generically (i.e. I only need one function to handle all of the prefixes). At this point I'm left with extracting the prefix from the request object and/or not using blueprint prefixes and just registering my functions with the "prefix" being a parameter for the function. Both seem to violate the principles of what blueprints are ostensibly about.

@ghost
Copy link

ghost commented Mar 26, 2013

Well, this is still unresolved.

@mitsuhiko
Copy link
Contributor

You can already do this easily. The basic version is this:

from flask import Flask, Blueprint, url_for

bp = Blueprint('whatever', __name__)

@bp.route('/')
def index(name):
    return 'I am %s (%s)' % (name, url_for('.index', name=name))

app = Flask(__name__)
app.register_blueprint(bp, url_prefix='/foo1', url_defaults={'name': 'Foo1'})
app.register_blueprint(bp, url_prefix='/foo2', url_defaults={'name': 'Foo2'})

c = app.test_client()
assert c.get('/foo1/').data == 'I am Foo1 (/foo1/)'
assert c.get('/foo2/').data == 'I am Foo2 (/foo2/)'

The more complicated version that gives a nicer API:

from flask import Flask, Blueprint, url_for, g

bp = Blueprint('whatever', __name__)

@bp.url_defaults
def bp_url_defaults(endpoint, values):
    name = getattr(g, 'name', None)
    if name is not None:
        values.setdefault('name', name)

@bp.url_value_preprocessor
def bp_url_value_preprocessor(endpoint, values):
    g.name = values.pop('name')

@bp.route('/')
def index():
    return 'I am %s (%s)' % (g.name, url_for('.index'))

app = Flask(__name__)
app.register_blueprint(bp, url_prefix='/foo1', url_defaults={'name': 'Foo1'})
app.register_blueprint(bp, url_prefix='/foo2', url_defaults={'name': 'Foo2'})

c = app.test_client()
assert c.get('/foo1/').data == 'I am Foo1 (/foo1/)'
assert c.get('/foo2/').data == 'I am Foo2 (/foo2/)'

@mitsuhiko
Copy link
Contributor

As a second step you can use the values passed in the view args then to find specific blueprint configuration etc. If you need that information available outside the routing system you can create multiple blueprint objects with different names from a factory function.

@ghost
Copy link

ghost commented Mar 26, 2013

One of the ugliest hacks I've ever seen. It should be easier.

@mitsuhiko
Copy link
Contributor

One of the ugliest hacks I've ever seen. It should be easier.

I am accepting a nice proposal on how a nicer solution is supposed to look like. Also not sure how that's a hack, it was designed to work this way.

@mitsuhiko
Copy link
Contributor

(There already is a way to keep blueprints apart: their name. You can just generate a blueprint in a factory and then refer to the blueprint through closures. I don't quite see where the issue is to be honest)

@ghost
Copy link

ghost commented Mar 26, 2013

In modern computing terminology, a kludge (or often a "hack") is a solution to a problem, doing a task, or fixing a system that is inefficient, inelegant, or even unfathomable, but which nevertheless (more or less) works.
(Wikipedia)

URL pre-processors should work as URL pre-processors, not as a strange way to create identifiers. That's IMHO a hack.

Do you want some proposals? Here you are.

  1. Stop considering blueprints as temporary objects, but as "views", like if they were registered using app.add_url_rule('/mount_point/<path:route', blueprint.as_view()). Also, this would make easier to support nested blueprints.
  2. There are two "type identifiers" for blueprints: __name__ and a "given name". Are you sure we need both?

@cbsmith
Copy link

cbsmith commented Mar 27, 2013

I'd like to be able to bind the same blueprint to two different url_prefix's. There aren't a lot of good ways to do it.
If I create the blueprint without a url_prefix, then url_for() doesn't seem to work at all. I could create separate blueprint objects, but then I can't find a clean way to do declarative routing using blueprint.route decorators (the code only gets loaded once, so the second blueprint doesn't bind).

The only way I could figure out how to make it work write was to either a) treat the url_prefix as an argument in the route:

@blueprint.route('/<prefix>/foo')
def my_handler(prefix):
    pass

or to have all the handlers be tied to one giant closure:

def make_blueprint(name, prefix):
    blueprint = Blueprint(name, __name__, url_prefix=prefix)
    @blueprint.route('/foo'):
    def my_handler(prefix):
        pass
    return blueprint

or to have a method which procedurally adds the routes:

HANDLER_MAP = {'/foo': my_handler}
def add_routes_for_blueprint(bp):
    for rule, handler in handler_functions.iteritems():
        bp.add_url_rule(rule, handler.__name__, handler)

blueprint = Blueprint('foo1', __name__)
add_routes_for_blueprint(blueprint)
app.register_blueprint(blueprint)
blueprint2 = Blueprint('foo2', __name__)
add_routes_for_blueprint(blueprint2)
app.register_blueprint(blueprint2)

It seems like being able to bind a blueprint to multiple places ought to be the default capability for blueprints, but it feels like it's a special case that breaks odd things in odd places, and generally requires extra work. Either I'm doing it wrong or there is an opportunity to make this far more elegant.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 14, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants