pallets / flask

The Python micro framework for building web applications.
https://flask.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
68.02k stars 16.21k forks source link

Nestable blueprints #593

Closed nightkr closed 3 years ago

nightkr commented 12 years ago

I'd like to be able to register "sub-blueprints" using Blueprint.register_blueprint(*args, **kwargs). This would register the nested blueprints with an app when the "parent" is registered with it. All parameters are preserved, other than url_prefix, which is handled similarly to in add_url_rule. A naíve implementation could look like this:

class Blueprint(object):
    ...

    def register_blueprint(self, blueprint, **options):
        def deferred(state):
            url_prefix = options.get('url_prefix')
            if url_prefix is None:
                url_prefix = blueprint.url_prefix
            if 'url_prefix' in options:
                del options['url_prefix']

            state.app.register_blueprint(blueprint, url_prefix, **options)
        self.record(deferred)
smartboyathome commented 12 years ago

I agree, this is something I'd like to see as well. I think the best use case would be a modular admin panel, where it is kept in a blueprint and plugins create sub-blueprints that are added to that blueprint.

nightkr commented 12 years ago

I've been using the following for now for a thing I've been working on. While there are no guarantees etc I haven't encountered any major issues with it so far.

class NestableBlueprint(Blueprint):
    """
    Hacking in support for nesting blueprints, until hopefully https://github.com/mitsuhiko/flask/issues/593 will be resolved
    """

    def register_blueprint(self, blueprint, **options):
        def deferred(state):
            url_prefix = (state.url_prefix or u"") + (options.get('url_prefix', blueprint.url_prefix) or u"")
            if 'url_prefix' in options:
                del options['url_prefix']

            state.app.register_blueprint(blueprint, url_prefix=url_prefix, **options)
        self.record(deferred)
mitsuhiko commented 12 years ago

Not sure what the semantics are for this. If someone wants this, please come up with a design for it :)

smartboyathome commented 12 years ago

I think that dontcare4free's NestableBlueprint provides a good rough design. All that would really be needed is the ability to register blueprints onto an already existing blueprint. I don't think this would really change any code in Blueprint.register() either since, from looking at that and BlueprintSetupState, the same function names are shared between Blueprint and Flask. Looks like it'd be mostly a duplication of register_blueprint from Flask to Blueprint.

Here is how I'd envision Blueprint.register_blueprint would work:

Each blueprint would have its own self.blueprints dict. When register_blueprint is called, it would check whether there is a blueprint with that name already in its internal dict. The logic on what to depending on that result do is duplicated between Flask.register_blueprint and Blueprint.register_blueprint. However, the ID of the child blueprint would have the id of the parent blueprint appended to it, with a '.' between the two. For example, if the parent blueprint was named 'config' and the child blueprint was named 'users', then ultimately its ID would be 'config.users'. Any paths in the child would be prefixed by the parent blueprint's url_prefix as well as the child's.

This is equally as rough as the dontcare4free's design above, and I haven't looked thoroughly through the code to see everywhere that this could affect, but it is possible to do.

esbullington commented 11 years ago

I'm also hoping for this. Right now, I'm implementing a RESTful API alongside a traditional web application and am ending up writing a lot of non-DRY code because I can't nest blueprints, particularly wrt API error handling (i.e., I have to override Flask's build-in error handling for each API blueprint in the same way). In fact, I'm thinking of trying out dontcare4free's NestableBlueprints subclass to see if it would help solve my problem. I'd love it if this were built in.

homeworkprod commented 11 years ago

I could use something like this as well.

Not sure about the design, though – will test-drive it in my application.

ghost commented 11 years ago

How strange - flask.Flask and flask.Blueprint are getting more and more similar. Maybe they should inherit from a common class?

untitaker commented 11 years ago

@gioi I already suggested something along these lines once in IRC and got some arguments against it (by @mitsuhiko ? i don't remember) but i can't recall the points that were made against it. I also suck at grepping the logs.

nicksloan commented 11 years ago

Personally, I like the idea of inheriting from a common class. I like to build my blueprints as fairly isolated from their siblings. It would be kind of nice if a blueprint was just a flask app itself. Perhaps there could be a distinction between blueprints that have to be bolted onto a parent, and blueprints/apps that can be run standalone. There is a lot of overlap between the two. I think bringing them closer together could really allow for some powerful uses.

homeworkprod commented 11 years ago

To elaborate on my personal use case:

A specific application contains about two dozen blueprints. Most are coupled with a single entity (one pair for users, news posts, etc.) and contain the usual view methods (create, update, delete, etc.).

However, some blueprints and entities belong together (news posts and images, gallery albums and images, etc.), and that's why I would like to group them together, but preferably without sacrificing the namespace-free view methods (as opposed to switching to create_post, create_image, etc. and combining them in the same blueprint Python module).

So what I'm actually looking for is a way to group blueprints; but there is no need to (infinitely) nest them.

ghost commented 11 years ago

@homeworkprod AFAIU, nestable blueprints would be a solution to your problem, right?

danielchatfield commented 11 years ago

@mitsuhiko Is this going anywhere? This seems like a great idea.

homeworkprod commented 11 years ago

@gioi: Well, they would be a solution, but I'm worried that this would go too far.

The initial post speaks of "sub-blueprints", and I think one additional layer would suffice (versus "nested", which, to me, sounds like multiple layers, and people could really go wild with that).

But then again I might see a problem here that doesn't exist.

ThiefMaster commented 11 years ago

Wouldn't a proper implementation of "sub-blueprints" pretty much automatically support infinite nesting of blueprints, too, anyway?

homeworkprod commented 11 years ago

Some questions to examine the current proposal:

@ThiefMaster: Well, I'd say it could get more complex, as it is usually the case with generalized implementations. But basically, yes, likely.

nightkr commented 11 years ago

@homeworkprod

  • Would this mean we'd have endpoints like admin.news.posts.tags.update?

The implementation in the OP doesn't handle this, but I think that makes the most sense.

nightkr commented 10 years ago

750 a duplicate of this?

Turbo87 commented 10 years ago

@teozkr #750 is/was a pull request, while this is an issue without commits attached

nightkr commented 10 years ago

@Turbo87 true, but @kennethreitz pretty much declined it with a back-to-the-drawing-board message.

employ commented 10 years ago

This would be extremely useful when designing applications, especially with a scenario like this (ala shopify):

Where the admin panel blueprint would be nest-able within the company page.

danielchatfield commented 10 years ago

I thought long and hard about this and whilst I absolutely agree that this would be very very nice to have it would require a rewrite of significant parts of flask (the way blueprints currently work under the hood is not very conducive of this) and I'm not sure it could be done in a completely backwards-compatible way.

razzius commented 10 years ago

@danielchatfield: I don't see why it couldn't be backwards-compatible — as Blueprint.register_blueprint is a new method, the old API would be unchanged and things could shift around under the hood as much as they needed to.

Currently, it seems that the best way to implement @employ's Shopify example with /admin consisting of multiple blueprints would be to make admin a normal Python module, implement the blueprints by importing from that module, and make the Flask app register the blueprints directly. My issue with this is that you'd have to re-enter the same "/admin" url prefix for all of its blueprints, which isn't DRY.

DasIch commented 10 years ago

This issue has now been open for almost two years. In that time frame there has not been a single serious attempt at creating a properly designed solution, which would include documentation and considering the impact a discussion on the implications of this change and potential problems.

Worse the discussion that has occured in this issue shows that fundamentally blueprints are probably a bad idea and better solutions for composing views might need to be found.

So for all those who intend to add another +1 comment, take this under consideration and post something useful, if you want to see something happen or I'm just going to unsubscribe because quite frankly I don't care at all about how many +1s this gets apart from that it's filling my inbox. A sentiment I which I'm probably not alone with.

untitaker commented 10 years ago

A way to replace blueprints should probably start with finding out (or summarizing) what is actually wrong with blueprints. They don't seem like a very elegant solution to me, but i am not sure what problems should be solved by a replacement.

I suggest deleting all :+1: comments for the sake of readability, and also to lock this issue and create a new one about a possible replacement or the future of blueprints.

iloahz commented 10 years ago

Why I need this

Say I wanna setup a admin page under /admin, and have a view for test initially, then I added a monitor view. As the test views accumulate, someday I decided to put all of them under admin/test, for unity, this operation is also done to monitor, so the desired file structure is like:

/app
    /admin
        /monitor
            __init__.py
            monitor.py
        /test
            __init__.py
            test.py
        __init__.py
        admin.py

What I expect to do now is:

from .test import test_bp
from .monitor import monitor_bp

admin_bp.register_blueprint(test_bp, url_preix='/test')
admin_bp.register_blueprint(monitor_bp, url_preix='/monitor')

rather than:

import test
import monitor

@admin_bp.add_url_rule('/test/test1', 'test1_view', test.test1_view)
@admin_bp.add_url_rule('/test/test2', 'test2_view', test.test2_view)
@admin_bp.add_url_rule('/monitor/page1', 'page1_view', monitor.page1_view)
@admin_bp.add_url_rule('/monitor/page2', 'page2_view', monitor.page2_view)

Just something like urlpattern's include in Django. As the app grows, it would definitely be a complicated sub-site under some endpoint, what one want to do of course, is to reduce the dependency.

char101 commented 10 years ago

A modification which returns the blueprint

    def add_blueprint(self, name, import_name, **kwargs):
        url_subprefix = kwargs.pop('url_subprefix', None)
        if url_subprefix:
            kwargs.setdefault('url_prefix', self.url_prefix + url_subprefix)
        bp = self.__class__(self.name + '.' + name, import_name, **kwargs)

        def later(state):
            state.app.register_blueprint(bp)
        self.record(later)

        return bp
Jaza commented 9 years ago

An alternative to sub-blueprints, is to break a one-file Blueprint into multiple py files, to import the secondary files in the main Blueprint file, and to then loop through the imported routes and call bp.add_url_rule() on them.

I created a Gist with the code for doing this:

https://gist.github.com/Jaza/61f879f577bc9d06029e

This doesn't provide sub-prefixing for the secondary routes, the way that nestable blueprints would (although in my use case I didn't want them anyway). Although, one could do auto-sub-prefixing in the for r in routes loop, with some simple concatenation. It's mainly just for splitting up a Blueprint that's grown too big to have all the routes in one py file.

chadgit commented 8 years ago

While the approach I ended up using is not as automatic as inheriting all properties of a parent blueprint except for the url_prefix, I was able to register and have nested blueprints "modules" physically located inside other blueprint "modules" directories as well by using a combination of default_config.py dictionary structure, a startup file that initialized from default_config.py the starting app module, which then auto initializes a create_app method that calls a common class file /common/classes.py makeBluePrint() factory method. Basically OnStart Read Config, then Take the "blueprints" array names and pass them all through the makeBluePrint() class which further checks config at runtime for any routes that need to be registered and registers them.

I think it would be pretty simple for me to adjust the below data structure and the bluePrintMaker() blueprint app factory method to achieve what you are asking for.

Notice that in my data dictionary in default_config.py where it lists the blueprint app names, public, admin, and admin.utilities.

Basically below in the default_config.py dictionary. "admin.utilities" is a folder/app inside of admin called "utilities".

I will adapt my data structure to support nested data elements inside of each blueprint in default_config.py with a couple switches like "inherit" or "override" with child elements / objects for template info.

In my bluePrintMaker() blueprint factory method it is able to find and register using the dot syntax and is currently how I have the module path being set by using the first data object name(s) inside of the dictionary object "blueprints"

List of files and methods: 1) a root default_config.py file holding "APP_INFO" dictionary with specific, keys, attributes and properties.

2) a /common directory in root with classes.py file containing a bluePrintMaker() class and method(s)

3) a root file in "runserver.py" that onload invokes create_app() with a nested call to classes.py 's bluePrintMaker() factory method which also creates routes and api endpoints from default_config.py dictionary data.

This structure and approach worked for me after many reviews of other approaches and code samples.

"""default_config.py"""
APP_INFO = {
  "app": {
    "properties": {
      "name": "myApp",
      "start": "public"
    },
    "blueprints": {
      "public": {
        "properties": {
          "info": {
            "name": "public",
            "path": "/public/"
          }
        },
        "views": {
          "pageCreator": {
            "urls": {
              "/<url>": "",
              "/": "{'url':'/'}"
            }
          }
        },
        "templates": {
          "properties": {
            "template_folder": "templates",
            "static_folder": "static",
            "static_url_path": "/public"
          },
          "layout": {
            "directory": "/layouts/",
            "extension": ".html"
          },
          "content": {
            "directory": "/content/",
            "extension": ".html"
          },
          "data": {
            "directory": "/templates/data/",
            "extension": ".txt"
          },
          "loader": {
            "directory": "/content/",
            "extension": ".html"
          }
        }
      },
      "admin": {
        "properties": {
          "info": {
            "name": "admin",
            "path": "/admin/"
          }
        },
        "views": {
          "checkAuth": {
            "urls": {
              "/admin": ""
            }
          }
        },
        "templates": {
          "properties": {
            "template_folder": "admin_templates",
            "static_folder": "admin_static",
            "static_url_path": "/admin"
          }
        }
      },
      "admin.utilities": {
        "properties": {
          "info": {
            "name": "utilities",
            "path": "/admin/utilities/"
          }
        },
        "views": {
          "launchUtility": {
            "urls": {
              "/admin/utilities/<utility>": "",
              "/admin/utilities": "{'utility':'/'}"
            }
          }
        },
        "templates": {
          "properties": {
            "template_folder": "utilities_templates",
            "static_folder": "utilities_static",
            "static_url_path": "/admin/utilities/"
          }
        }
      }
    }
  }
}
ThiefMaster commented 6 years ago

No it's not a "required feature", and for languages codes you certainly do not need this. Check e.g. http://flask.pocoo.org/docs/0.12/patterns/urlprocessors/

sharifzadesina commented 6 years ago

@ThiefMaster Yes, MAYBE! But tell me which one you prefer?

app.register_blueprint(blog_bp, url_prefix='/<lang>/blog')
app.register_blueprint(admin_bp, url_prefix='/<lang>/admin')
app.register_blueprint(portfolio_bp, url_prefix='/<lang>/portfolio')
# we should have `lang` or any other prefix in all over blueprints

or

main_bp = Blueprint('main_bp', url_prefix='/<lang>')
main_bp.register_blueprint(blog_bp, url_prefix='/blog')
main_bp.register_blueprint(admin_bp, url_prefix='/admin')
main_bp.register_blueprint(portfolio_bp, url_prefix='/portfolio')
# now we just need to add it on our main blueprint
davidism commented 5 years ago

Hi everyone, I appreciate that you're trying to help, but please stop posting variations of code that adds a prefix to all routes. The goal of nestable blueprints in Flask is to be fully nestable, including request.blueprint, url_for, before_request, and all other parts of the request lifecycle. This is not impossible, but it's much more than just a prefix.

If this feature is important to you, please consider #3215 as a good place to start contributing.

bekab95 commented 4 years ago

Hi everyone, I appreciate that you're trying to help, but please stop posting variations of code that adds a prefix to all routes. The goal of nestable blueprints in Flask is to be fully nestable, including request.blueprint, url_for, before_request, and all other parts of the request lifecycle. This is not impossible, but it's much more than just a prefix.

If this feature is important to you, please consider #3215 as a good place to start contributing.

@davidism does this PR https://github.com/pallets/flask/pull/3709 close the issue ?

ThiefMaster commented 4 years ago

No, AFAICT this is just refactoring to reduce similar/duplicate code between Flask and Blueprint.