import importlib
import os
from types import FunctionType
from typing import *
from ..flask_unchained import FlaskUnchained
from ..string_utils import right_replace, slugify, snake_case
from ..unchained import unchained
def _normalize_module_name(module_name):
if module_name.endswith('.bundle'):
return right_replace(module_name, '.bundle', '')
return module_name
class BundleMetaclass(type):
def __new__(mcs, name, bases, clsdict):
# check if the user explicitly set module_name
module_name = clsdict.get('module_name')
if isinstance(module_name, str):
clsdict['module_name'] = _normalize_module_name(module_name)
return super().__new__(mcs, name, bases, clsdict)
class _BundleModuleNameDescriptor:
def __get__(self, instance, cls):
return _normalize_module_name(cls.__module__)
def __set__(self, instance, value):
raise AttributeError
class _BundleIsSingleModuleDescriptor:
def __get__(self, instance, cls):
return not importlib.util.find_spec(cls.module_name).submodule_search_locations
def __set__(self, instance, value):
raise AttributeError
class _BundleRootPathDescriptor:
def __get__(self, instance, cls):
module = importlib.import_module(cls.module_name)
return os.path.dirname(module.__file__)
def __set__(self, instance, value):
raise AttributeError
class _BundleNameDescriptor:
def __init__(self, *, strip_bundle_suffix: bool = False):
self.strip_bundle_suffix = strip_bundle_suffix
def __get__(self, instance, cls):
if self.strip_bundle_suffix:
return snake_case(right_replace(cls.__name__, 'Bundle', ''))
return snake_case(cls.__name__)
class _BundleStaticFolderDescriptor:
def __get__(self, instance, cls):
if cls.is_single_module and issubclass(cls, AppBundle):
return None # this would be the same as the top-level static folder registered with Flask
if not hasattr(instance, '_static_folder'):
instance._static_folder = os.path.join(instance.root_path, 'static')
if not os.path.exists(instance._static_folder):
instance._static_folder = None
return instance._static_folder
class _BundleStaticUrlPathDescriptor:
def __get__(self, instance, cls):
if instance._static_folders:
return f'/{slugify(cls.name)}/static'
class _BundleTemplateFolderDescriptor:
def __get__(self, instance, cls):
if not hasattr(instance, '_template_folder'):
instance._template_folder = os.path.join(instance.root_path, 'templates')
if not os.path.exists(instance._template_folder):
instance._template_folder = None
return instance._template_folder
[docs]class Bundle(metaclass=BundleMetaclass):
"""
Base class for bundles.
Should be placed in your package's root or its ``bundle`` module::
# your_bundle_package/__init__.py or your_bundle_package/bundle.py
class YourBundle(Bundle):
pass
"""
name: str = _BundleNameDescriptor(strip_bundle_suffix=False)
"""
Name of the bundle. Defaults to the snake_cased class name.
"""
module_name: str = _BundleModuleNameDescriptor()
"""
Top-level module name of the bundle (dot notation).
Automatically determined; read-only.
"""
root_path: str = _BundleRootPathDescriptor()
"""
Root directory path of the bundle's package.
Automatically determined; read-only.
"""
template_folder: Optional[str] = _BundleTemplateFolderDescriptor()
"""
Root directory path of the bundle's template folder. By default, if there exists
a folder named ``templates`` in the bundle package
:attr:`~flask_unchained.Bundle.root_path`, it will be used, otherwise ``None``.
"""
static_folder: Optional[str] = _BundleStaticFolderDescriptor()
"""
Root directory path of the bundle's static assets folder. By default, if there exists
a folder named ``static`` in the bundle package
:attr:`~flask_unchained.Bundle.root_path`, it will be used, otherwise ``None``.
"""
static_url_path: Optional[str] = _BundleStaticUrlPathDescriptor()
"""
Url path where this bundle's static assets will be served from. If
:attr:`~flask_unchained.Bundle.static_folder` is set, this will default to
``/<bundle.name>/static``, otherwise ``None``.
"""
is_single_module: bool = _BundleIsSingleModuleDescriptor()
"""
Whether or not the bundle is a single module (Python file).
Automatically determined; read-only.
"""
default_load_from_module_name: Optional[str] = None
"""
The default module name for hooks to load from. Set hooks' bundle modules override
attributes for the modules you want in separate files.
.. admonition:: WARNING - EXPERIMENTAL
:class: danger
Using this feature may cause mysterious exceptions to be thrown!!
Best practice is to organize your code in separate modules.
"""
_deferred_functions: List[FunctionType] = []
"""
Deferred functions to be registered with the
:class:`~flask_unchained.bundles.controller.bundle_blueprint.BundleBlueprint`
that gets created for this bundle.
The :class:`~flask_unchained.Unchained` extension copies these values from the
:class:`DeferredBundleBlueprintFunctions` instance it created for this bundle.
"""
[docs] def before_init_app(self, app: FlaskUnchained) -> None:
"""
Override this method to perform actions on the
:class:`~flask_unchained.FlaskUnchained` app instance *before* the
``unchained`` extension has initialized the application.
"""
pass
[docs] def after_init_app(self, app: FlaskUnchained) -> None:
"""
Override this method to perform actions on the
:class:`~flask_unchained.FlaskUnchained` app instance *after* the
``unchained`` extension has initialized the application.
"""
pass
def _iter_class_hierarchy(self, include_self: bool = True, mro: bool = False):
"""
Iterate over the bundle classes in the hierarchy. Yields base-most
instances first (aka opposite of Method Resolution Order).
For internal use only.
:param include_self: Whether or not to yield the top-level bundle.
:param mro: Pass True to yield bundles in Method Resolution Order.
"""
supers = self.__class__.__mro__[(0 if include_self else 1):]
for bundle_cls in (supers if mro else reversed(supers)):
if bundle_cls not in {object, AppBundle, Bundle}:
if bundle_cls == self.__class__:
yield self
else:
yield bundle_cls()
@property
def _has_views(self) -> bool:
"""
Returns True if any of the bundles in the hierarchy has a views module.
For internal use only.
"""
if self.is_single_module and isinstance(self, AppBundle):
return True
from ..hooks.views_hook import ViewsHook
for bundle in self._iter_class_hierarchy():
if ViewsHook.import_bundle_modules(bundle):
return True
return False
@property
def _blueprint_name(self) -> str:
"""
Get the name to use for the blueprint for this bundle.
For internal use only.
"""
if self._is_top_bundle or not self._has_hierarchy_name_conflicts:
return self.name
for i, bundle in enumerate(self._iter_class_hierarchy()):
if bundle.__class__ == self.__class__:
return f'{self.name}_{i}'
@property
def _static_folders(self) -> List[str]:
"""
Get the list of static folders for this bundle.
For internal use only.
"""
if not self._has_hierarchy_name_conflicts:
return [self.static_folder] if self.static_folder else []
elif not self._is_top_bundle:
return []
return [b.static_folder for b in self._iter_class_hierarchy(mro=True)
if b.static_folder and b.name == self.name]
@property
def _is_top_bundle(self) -> bool:
"""
Whether or not this bundle is the top-most bundle in the hierarchy.
For internal use only.
"""
return not self.__class__.__subclasses__()
@property
def _has_hierarchy_name_conflicts(self) -> bool:
"""
Whether or not there are any name conflicts between bundles in the hierarchy.
For internal use only.
"""
top_bundle = self.__class__
subclasses = top_bundle.__subclasses__()
while subclasses:
top_bundle = subclasses[0]
subclasses = top_bundle.__subclasses__()
return any(b.name == self.name and b.__class__ != self.__class__
for b in top_bundle()._iter_class_hierarchy())
def __getattr__(self, name):
if name in {'before_request', 'after_request', 'teardown_request',
'context_processor', 'url_defaults', 'url_value_preprocessor',
'errorhandler'}:
from warnings import warn
warn('The app has already been initialized. Please register '
f'{name} sooner.')
return
raise AttributeError(name)
def __repr__(self) -> str:
return (f'<{self.__class__.__name__} '
f'name={self.name!r} '
f'module={self.module_name!r}>')
class AppBundleMetaclass(BundleMetaclass):
"""
Metaclass for :class:`~flask_unchained.AppBundle` to automatically set the
user's subclass on the :class:`~flask_unchained.Unchained` extension instance.
"""
def __init__(cls, name, bases, clsdict):
super().__init__(name, bases, clsdict)
unchained._app_bundle_cls = cls
[docs]class AppBundle(Bundle, metaclass=AppBundleMetaclass):
"""
Like :class:`~flask_unchained.Bundle`, except used for the top-most
application bundle.
"""
name: str = _BundleNameDescriptor(strip_bundle_suffix=True)
"""
Name of the bundle. Defaults to the snake_cased class name, excluding any
"Bundle" suffix.
"""
__all__ = [
'AppBundle',
'AppBundleMetaclass',
'Bundle',
'BundleMetaclass',
]