Source code for flask_unchained.app_factory

import importlib
import inspect
import os
import sys

from types import ModuleType
from typing import *

from py_meta_utils import Singleton

from .bundles import AppBundle, Bundle
from .constants import DEV, PROD, STAGING, TEST, ENV_ALIASES, VALID_ENVS
from .exceptions import BundleNotFoundError
from .flask_unchained import FlaskUnchained
from .unchained import unchained
from .utils import cwd_import


def maybe_set_app_factory_from_env():
    app_factory = os.getenv('UNCHAINED_APP_FACTORY', None)
    if app_factory:
        module_name, class_name = app_factory.rsplit('.', 1)
        app_factory_cls = getattr(cwd_import(module_name), class_name)
        AppFactory.set_singleton_class(app_factory_cls)


[docs]class AppFactory(metaclass=Singleton): """ The Application Factory Pattern for Flask Unchained. """ APP_CLASS = FlaskUnchained """ Set :attr:`APP_CLASS` to use a custom subclass of :class:`~flask_unchained.FlaskUnchained`. """ REQUIRED_BUNDLES = [ # these are ordered by first to be loaded 'flask_unchained.bundles.controller', 'flask_unchained.bundles.babel', # requires controller bundle ]
[docs] def create_app(self, env: Union[DEV, PROD, STAGING, TEST], bundles: Optional[List[str]] = None, *, _config_overrides: Optional[Dict[str, Any]] = None, _load_unchained_config: bool = True, **app_kwargs, ) -> FlaskUnchained: """ Flask Unchained Application Factory. Returns an instance of :attr:`APP_CLASS` (by default, :class:`~flask_unchained.FlaskUnchained`). Example Usage:: app = AppFactory().create_app(PROD) :param env: Which environment the app should run in. Should be one of "development", "production", "staging", or "test" (you can import them: ``from flask_unchained import DEV, PROD, STAGING, TEST``) :type env: str :param bundles: An optional list of bundle modules names to use. Overrides ``unchained_config.BUNDLES`` (mainly useful for testing). :type bundles: List[str] :param app_kwargs: keyword argument overrides for the :attr:`APP_CLASS` constructor :type app_kwargs: Dict[str, Any] :param _config_overrides: a dictionary of config option overrides; meant for test fixtures (for internal use only). :param _load_unchained_config: Whether or not to try to load unchained_config (for internal use only). :return: The initialized :attr:`APP_CLASS` app instance, ready to rock'n'roll """ env = ENV_ALIASES.get(env, env) if env not in VALID_ENVS: valid_envs = [f'{x!r}' for x in VALID_ENVS] raise ValueError(f"env must be one of {', '.join(valid_envs)}") unchained_config = {} unchained_config_module = None if _load_unchained_config: unchained_config_module = self.load_unchained_config(env) unchained_config = {k: v for k, v in vars(unchained_config_module).items() if not k.startswith('_') and k.isupper()} unchained_config['_CONFIG_OVERRIDES'] = _config_overrides _, bundles = self.load_bundles( bundle_package_names=bundles or unchained_config.get('BUNDLES', []), unchained_config_module=unchained_config_module) app_import_name = (bundles[-1].module_name.split('.')[0] if bundles else ('tests' if env == TEST else 'dev_app')) app = self.APP_CLASS(app_import_name, **self.get_app_kwargs( app_kwargs, bundles, env, unchained_config)) app.env = env for bundle in bundles: bundle.before_init_app(app) unchained.init_app(app, bundles, unchained_config) for bundle in bundles: bundle.after_init_app(app) return app
[docs] @staticmethod def load_unchained_config(env: Union[DEV, PROD, STAGING, TEST]) -> ModuleType: """ Load the unchained config from the current working directory for the given environment. If ``env == "test"``, look for ``tests._unchained_config``, otherwise check the value of the ``UNCHAINED`` environment variable, falling back to loading the ``unchained_config`` module. """ if not sys.path or sys.path[0] != os.getcwd(): sys.path.insert(0, os.getcwd()) msg = None if env == TEST: try: return cwd_import('tests._unchained_config') except ImportError as e: msg = f'{e.msg}: Could not find _unchained_config.py in the tests directory' unchained_config_module_name = os.getenv('UNCHAINED', 'unchained_config') try: return cwd_import(unchained_config_module_name) except ImportError as e: if not msg: msg = f'{e.msg}: Could not find {unchained_config_module_name}.py ' \ f'in the current working directory (are you in the project root?)' e.msg = msg raise e
[docs] def get_app_kwargs(self, app_kwargs: Dict[str, Any], bundles: List[Bundle], env: Union[DEV, PROD, STAGING, TEST], unchained_config: Dict[str, Any], ) -> Dict[str, Any]: """ Returns ``app_kwargs`` with default settings applied from ``unchained_config``. """ # this is for developing standalone bundles (as opposed to regular apps) if not isinstance(bundles[-1], AppBundle) and env != TEST: app_kwargs['template_folder'] = os.path.join( os.path.dirname(__file__), 'templates') # set kwargs to pass to Flask from the unchained_config params = inspect.signature(self.APP_CLASS).parameters valid_app_kwargs = [name for i, (name, param) in enumerate(params.items()) if i > 0 and (param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.KEYWORD_ONLY)] for kw in valid_app_kwargs: config_key = kw.upper() if config_key in unchained_config: app_kwargs.setdefault(kw, unchained_config[config_key]) # set up the root static/templates folders (if any) root_path = app_kwargs.get('root_path') if not root_path and bundles: root_path = bundles[-1].root_path if not bundles[-1].is_single_module: root_path = os.path.dirname(root_path) app_kwargs['root_path'] = root_path def root_folder_or_none(folder_name): if not root_path: return None folder = os.path.join(root_path, folder_name) return folder if os.path.isdir(folder) else None app_kwargs.setdefault('template_folder', root_folder_or_none('templates')) app_kwargs.setdefault('static_folder', root_folder_or_none('static')) if app_kwargs['static_folder'] and not app_kwargs.get('static_url_path'): app_kwargs.setdefault('static_url_path', '/static') return app_kwargs
[docs] @classmethod def load_bundles(cls, bundle_package_names: Optional[List[str]] = None, unchained_config_module: Optional[ModuleType] = None, ) -> Tuple[Union[None, AppBundle], List[Bundle]]: """ Load bundle instances from the given list of bundle packages. If ``unchained_config_module`` is given and there was no app bundle listed in ``bundle_package_names``, attempt to load the app bundle from the unchained config. """ bundle_package_names = bundle_package_names or [] for bundle_name in reversed(cls.REQUIRED_BUNDLES): try: existing_index = bundle_package_names.index(bundle_name) except ValueError: pass else: bundle_package_names.pop(existing_index) bundle_package_names.insert(0, bundle_name) if not bundle_package_names: return None, [] bundles = [] for bundle_package_name in bundle_package_names: bundles.append(cls.load_bundle(bundle_package_name)) if isinstance(bundles[-1], AppBundle): return bundles[-1], bundles if unchained_config_module: single_module_app_bundle = cls.bundle_from_module(unchained_config_module) if single_module_app_bundle: bundles.append(single_module_app_bundle) return single_module_app_bundle, bundles return None, bundles
[docs] @classmethod def load_bundle(cls, bundle_package_name: str) -> Union[AppBundle, Bundle]: """ Attempt to load the bundle instance from the given package. """ for module_name in [f'{bundle_package_name}.bundle', bundle_package_name]: try: module = importlib.import_module(module_name) except ImportError as e: if f"No module named '{module_name}'" in str(e): continue raise e bundle = cls.bundle_from_module(module) if bundle: return bundle raise BundleNotFoundError( f'Unable to find a Bundle subclass in the {bundle_package_name} bundle!' ' Please make sure this bundle is installed and that there is a Bundle' ' subclass in the packages\'s __init__.py (or bundle.py) file.')
[docs] @classmethod def bundle_from_module(cls, module: ModuleType) -> Union[AppBundle, Bundle, None]: """ Attempt to instantiate the bundle class from the given module. """ try: bundle_class = inspect.getmembers(module, cls._is_bundle(module))[0][1] except IndexError: return None else: return bundle_class()
@classmethod def _is_bundle(cls, module: ModuleType) -> Callable[[Any], bool]: return lambda obj: (isinstance(obj, type) and issubclass(obj, Bundle) and obj not in {AppBundle, Bundle} and obj.__module__.startswith(module.__name__))
__all__ = [ 'AppFactory', 'maybe_set_app_factory_from_env', ]