How can I manage extra modules in app factory pattern?
Asked Answered
S

2

2

I'm using flask with the app factory pattern. I do know that the app factory pattern manages configuration objects only in the factory function. (as in the following code)

def create_app(config):
    app.config.from_object(config)
    sentry.init(app)
    ...

    return app

But how do I manage the extra module that needs that configuration, but couldn't be initialized in app creating time?

So I want to do something like

def create_app(config):
    some_module_obj = Module(host=config.host, port=config.port)

    app.config.from_object(config)
    sentry.init(app)

    return some_module_obj, app

Rather than

# I don't want to use `config` outside of the `create_app` function!
some_module_obj = Module(host=config.host, port=config.port)

def create_app(config):
    app.config.from_object(config)
    sentry.init(app)

    return app
Swung answered 1/10, 2018 at 8:52 Comment(9)
If your whole module needs a configuration then you should revise and refactor it's structure. Good practice is that modules are easily importable without config, but classes inside them require conf to create instances.Kalvn
@Fian So you mean I have to do refactor the module to support kind of init_app, haven't I?Swung
By module do you mean Python's Modules? Because as far as I know you can't invoke Python Module (like Module(), you can only import it and invoke objects inside it.Kalvn
For example, rq-schedular. If you refer github.com/rq/rq-scheduler, it initialize the object as below: scheduler = Scheduler(connection=Redis(host='0.0.0.0', port=6379))Swung
Ok, I get it, what you've called module is a Python Class and it's instance initialization, actually. Why'd you want to return some_module_obj from the create_app instead of attaching it to app as an attribute? App factory should return app only.Kalvn
Yeah good question. I don't really want to return some_module_obj in create_app, but I do not want to handle configuration outside of the create_app function. Since some_module_obj needs configuration value when it's initialize, I tried to initialize inside of create_app function.Swung
@Hyunwoo did you try Inject? You can initialize all services using inject in create_app().Offutt
@DanilaGanchar Could you please give me an example? Even short snippet is fine.Swung
@Hyunwoo thanks for 'accepted answer'. good luck.Offutt
O
5

Not sure that is what you need, but you asked for a small example with inject + Flask in comments. As I understood the main problem is related with Flask + configuration + initialization. This is just an example how it works.

app.py:

from flask import Flask

from api import bp
from configurator import configure


def create_app():
    app = Flask(__name__)
    # configure Flask app config as you wish... (app.config.from_object(config))
    # just some settings for demonstration
    app.config.update(dict(
        MODULE1_TIMER=1,
        MODULE2_LIMIT=2,
    ))
    # configure inject using app context and Flask config
    with app.app_context():
        configure()
    # demo blueprint
    app.register_blueprint(bp)

    return app


if __name__ == '__main__':
    create_app().run(debug=True)

Let's imagine that we have some modules:

# mod1.py
class Module1:
    def __init__(self, timer: int) -> None:
        self._timer = timer

# mod2.py
class Module2:
    def __init__(self, limit: int) -> None:
        self._limit = limit

    def get_limit(self):
        return self._limit

# mod3.py - works with mod1 and mod2
class Module3:
    def __init__(self, module1, module2) -> None:
        self._module1 = module1
        self._module2 = module2

    def get_limit(self):
        return self._module2.get_limit()

configurator.py:

import inject
from flask import current_app

from mod1 import Module1
from mod2 import Module2
from mod3 import Module3


@inject.params(
    module1=Module1,
    module2=Module2,
)
def _init_module3(module1, module2):
    # module1 and module2 are injected instances
    return Module3(module1, module2)


def _injector_config(binder):
    # initialization of Module1 and Module2 using Flask config
    binder.bind(Module1, Module1(current_app.config['MODULE1_TIMER']))
    binder.bind(Module2, Module2(current_app.config['MODULE2_LIMIT']))
    # initialization of Module3 using injected Module1 + Module2
    # you can use bind_to_constructor + any function
    binder.bind_to_constructor(Module3, _init_module3)


def configure():
    def config(binder):
        binder.install(_injector_config)
        # one more binder.install... etc...

    inject.clear_and_configure(config)

api.py:

import inject
from flask import Blueprint, jsonify

from mod1 import Module1
from mod2 import Module2
from mod3 import Module3

bp = Blueprint('api', __name__)


@bp.route('/test')
def test():
    # get instances which was created using inject
    return jsonify(dict(
        module1=str(type(inject.instance(Module1))),
        module2=str(type(inject.instance(Module2))),
        module3=str(type(inject.instance(Module3))),
    ))


# you can inject something as arg
@bp.route('/test2')
@inject.params(module3=Module3)
def test2(module3: Module3):
    return jsonify(dict(module3=str(type(module3))))


@bp.route('/test3')
def test3():
    # you can inject something into anything
    class Example:
        module3 = inject.attr(Module3)

        @inject.params(module2=Module2)
        def __init__(self, module2: Module2) -> None:
            self.module2 = module2

    return jsonify({
        'MODULE2_LIMIT': Example.module3.get_limit(),
        'example': dir(Example()),
    })

Run server, open /test, test2, /test3.

A few words about benefits:

  • One point for initialization and configuration
  • Lower dependency on current_app, flask config / context etc.
  • Less problems with recursive imports
  • Easy to writing tests

Hope this helps.

Offutt answered 4/10, 2018 at 10:30 Comment(0)
S
0

Decided to make custom class for initialize object as factory pattern.

This is example:

class CustomFactory(metaclass=ABCMeta):
    @abstractmethod
    def init_factory(self, config):
        pass

    @property
    @abstractmethod
    def app(self):
        pass

    def __getattr__(self, item):
        return getattr(self.app, item)


class RQSchedulerFactory(CustomFactory):
    def __init__(self):
        self._app = None

    def init_factory(self, config):
        self._app = Scheduler(connection=Redis(host=config.REDIS_HOST, port=config.REDIS_PORT))

    @property
    def app(self):
        return self._app


class FireDBFactory(CustomFactory):
    @property
    def app(self):
        return self._app

    def __init__(self):
        self._app = None

    def init_factory(self, config):
        cred = credentials.Certificate(config.FIREBASE_KEY_FILE)
        firebase_admin.initialize_app(cred)

        self._app = firestore.client()

And in __init__.py (which has create_app function)

scheduler = RQSchedulerFactory()
fire_db = FireDBFactory()

And in create_app function, initialize as below:

def create_app(config):
    app.config.from_object(config)

    # Scheduler initialization
    scheduler.init_factory(config)

    # Fire store initialization
    fire_db.init_factory(config)
Swung answered 4/10, 2018 at 1:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.