bottlepy / bottle

bottle.py is a fast and simple micro-framework for python web-applications.
http://bottlepy.org/
MIT License
8.37k stars 1.46k forks source link

Create bottle.py module decorator, to ease subclassing #1224

Closed ojob closed 4 years ago

ojob commented 4 years ago

Currently in bottle, the @route decorator is a Bottle instance attribute, making it harder to subclass, as following code will not work, as not syntaxically correct:

import bottle

class App(bottle.Bottle):
    @self.route('/there')   # <--- wrong syntax!
    def some_func(self): pass

So, considering some different options, and trying not to go down the bottleCBV route that seems quite weird to me, I setup a workaround in a personal project (see here), which sums up as follows:

import inspect
import bottle

def route(path: str, method: str = 'GET')
    """Decorate a method, to track it for later routing."""
    def wrapper(func):
        func.route = (path, method)
        return func
   return wrapper

class BottleApp(bottle.Bottle):
    """A parent class, taking care of setting-up the routing."""
    def __init__(*args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_routes()

    def set_routes(self):
        for meth in inspect(self, predicate=inspect.ismethod):
            if hasattr(meth, 'route'):
               # and now the magic happens
               self.route(path=meth.route.path, method=meth.route.method, callback=meth)

With just this, we can now subclass in a lean way:

class MyApp(BottleApp):
    """A class, that can be run by a WSGI server, that can bear its own attributes."""
    @route(path='/users')
    def users(self):
        return self.whatever

I'd be happy to propose a Pull Request to integrate this, what do you think about this?

Some comments and thoughts:

ojob commented 4 years ago

Note: seems related to https://github.com/bottlepy/bottle/issues/603#issuecomment-38384072, and is closer to https://github.com/bottlepy/bottle/issues/603#issuecomment-38903401.

However, having a function returning all entry points functions (like proposed in second link juste above) is kinda akward in resulting code, when many entry points are to be created.

defnull commented 4 years ago

You should not subclass bottle.Bottle, unless you want to change its behaviour. It is not designed to work as a base class for applications. You can programmatically add routes to a bottle instance, so implementing your own application base class (with a fancy meta-class that magically injects a specialized @route implementation into the local namespace during class declaration) is the way to go. No need to change bottle.Bottle for that.

defnull commented 4 years ago

There are some attempts to implement such a base class (or other ways to create bottle apps) around, if you look for them. The reusability-pattern I have seen the most is to just create the application within a function:

def create_app(**settings):
    app = bottle.Bottle()
    ...
    return app

A base-class could add a lot of metaclass-magic to the mix, for example, inject specialized decorators to be used at declatarion-time or make sure the final class is callable. But I'm not sure if that really helps. Explicit is better than implicit, after all.

ojob commented 4 years ago

Well, I did look for them, as already indicated in the initial description. I found them quirky, hence my proposal for supporting subclassing.

Thanks for your answer, I'll keep my code for me then :) Have a nice day

PyB1l commented 4 years ago

Having used an example similar to what @defnull pointed out in production projects over 4 year, in my humble opinion the most elegant and / or uniform way is to use a factory function that returns a configured bottle.Bottle instance. Some of the most common operations during this configuration - so that it useful for other use cases - include the following step below

def create_app(**configuration):
      app = bottle.Bottle()
      # register custom URL filters. (I.E a custom uuid for creating @get('/<id:uuid>'))
      # install plugins for Bottle instance 
      # bind hooks
      #  Replace / Patch bottle defaults (I.E i replace default bottle jsonDecoder with OrJSON that
      # supports Dataclasses, datetimes etc
      # bind HTTP Error handlers
      return app

A generic implementation of this factory, plus ready utils for plugins, filters, hooks could be useful to others.

josephernest commented 1 year ago

@defnull, what would be a simple and recommended way to create a class for an application? (which builds the Bottle object and starts it)
Here is an old question about this: Bottle framework and OOP, using method instead of function, I would be interested about the "canonical way", blessed by the creator of the library :)

I was thinking about something like:

import bottle

class App:
    def __init__(self, port=80):
        self.app = bottle.Bottle()
        self.port = port

    def setup_routes(self):
        @self.app.route("/foo")
        def foo():
            return("foo")

        @self.app.route("/bar")
        def bar():
            return("bar")

    def start(self):
        self.app.run(port=self.port)

a = App()
a.setup_routes()
a.start()

PS: I noted the fact that it's not a good idea to subclass bottle.Bottle (in your previous comment).