Resolving indirect and direct (ciruclar) dependencies between extensions #5145
-
summaryI'm looking for feedback on a design that has resulted in circular dependencies between flask extensions. It currently works as intended, but I'm not really satisfied with where it ended up. While I'm looking for options to remove the circular dependency, I'd also be interested in hearing more about how extensions are supposed to deal with dependencies on other extensions backgroundI've got an API where want to be able to configure some functionality used in application blueprints for different deployments. For example, registration for production API access uses info from the org's official IdP to validate a user is eligible for access whereas a test deployment will just check that the person trying to register is one of a few specific users. Since there were a few pieces of functionality like this, I rolled a pseudo-exetension plugin registry class that could be subclassed to implement the different concrete versions of the functionality I needed. This was fine until some of those implementations started to require other extensions to work. In this case, it was needing Flask-SQLAlchemy, where I needed to use the db.session and model classes to query for info in the database. I initially imported the plugin implementations module and passed it directly to the plugin registry on construction, which eventually revealed the issue. Deferring loading the module, by passing the name as a string for the plugin registry to load on its own or setting up a flask app config default to specify the module in the app factory, got around the error so long as the plugin was created after the SQLAlchemy extension instance. example
# cicimport/__init__.py
# the rest of the __init__.py files are empty
from circimport.plugins.plugins import Type1PluginRegistry
# plugin implementation module import causes circular import
# import circimport.plugins.production as implementations
class FlaskFake:
def __init__(self) -> None:
self.config = {}
def create_db():
return lambda: 'SQLALchemy'
def create_plugin():
return Type1PluginRegistry(
plugin_module='circimport.plugins.production'
)
def create_app():
app = FlaskFake()
app.config['PLUGIN_MODULE'] = 'circimport.plugins.development'
# db.init_app(app)
plugin.init_app(app)
plugin.set_plugin('Impl1')
plugin.exec()
plugin.set_plugin('Impl2')
plugin.exec()
plugin.set_plugin('Impl3')
plugin.exec()
# initializing before db causes circular import
# plugin = create_plugin()
db = create_db()
plugin = create_plugin() # circimport/__main__.py
from circimport import create_app
create_app() # circimport/models/core.py
from circimport import db
def model():
return f'model using {db()}' # circimport/plugins/plugins.py
from abc import ABC, abstractmethod
from inspect import ismodule, isclass, getmembers
from importlib import import_module
class AbstractPluginBase(ABC):
def __init__(self, app=None):
if app is not None:
self.init_app(app)
def init_app(self, app):
pass
@abstractmethod
def exec(*args, **kwargs):
pass
class PluginRegistry:
MODULE_ENVVAR = 'PLUGIN_MODULE'
PLUGIN_ENVVAR = 'PLUGIN'
def __init__(self, base_cls, plugin_module=None, app=None) -> None:
self._registry = {}
self._selected = None
self._base_cls = base_cls
# load modules at registry instantiation
if plugin_module is not None:
self.load_plugins(plugin_module)
if app is not None:
self.init_app(app)
def load_plugins(self, module):
if not ismodule(module):
module = import_module(module)
predicate = lambda x: isclass(x) and issubclass(x, self._base_cls) and x is not self._base_cls
self._registry.update({name: cls() for name, cls in getmembers(module, predicate)})
def set_plugin(self, plugin):
self._selected = plugin
def init_app(self, app):
# load additional modules at app instantiation
if self.MODULE_ENVVAR in app.config:
self.load_plugins(app.config[self.MODULE_ENVVAR])
if self.PLUGIN_ENVVAR in app.config:
self.set_plugin(app.config[self.PLUGIN_ENVVAR])
for impl in self._registry.values():
impl.init_app(app)
def exec(self, *args, **kwargs):
print(self._selected, end=' ')
return self._registry[self._selected].exec(*args, **kwargs)
class Type1Plugin(AbstractPluginBase):
pass
class Type1PluginRegistry(PluginRegistry):
def __init__(self, plugin_module=None, app=None) -> None:
super().__init__(Type1Plugin, plugin_module, app) # circimport/plugins/development.py
from circimport.plugins.plugins import Type1Plugin
from circimport.models.core import model
class Impl3(Type1Plugin):
def exec(*args, **kwargs):
print(f'loaded on app create importing {model()}') # circimport/plugins/production.py
from circimport import db
from circimport.plugins.plugins import Type1Plugin
class Impl1(Type1Plugin):
def exec(*args, **kwargs):
print('doesn\'t touch db')
class Impl2(Type1Plugin):
def exec(*args, **kwargs):
print(f'touches {db()}') |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 3 replies
-
TBH, my first impression there is "over-engineered"... ;) My approach would be to keep core stuff in your core app, and then create plugins for your application (e.g. using setup.cfg entry points, and there's also When a plugin depends on another one it declares that dependency so the other one gets loaded first; and so far I never had to deal w/ circular dependencies but I'd simply do the imports locally within a function if it otherwise creates problems - but again, try to avoid having circular dependencies altogether. |
Beta Was this translation helpful? Give feedback.
TBH, my first impression there is "over-engineered"... ;)
My approach would be to keep core stuff in your core app, and then create plugins for your application (e.g. using setup.cfg entry points, and there's also
flask-pluginengine
(disclaimer: I'm the author)). Those plugins then import stuff directly from the core app, e.g.from yourapp.flaskstuff import db
to get the SQLAlchemy object; same for the rest.When a plugin depends on another one it declares that dependency so the other one gets loaded first; and so far I never had to deal w/ circular dependencies but I'd simply do the imports locally within a function if it otherwise creates problems - but again, try to avoid having circul…