mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Dependency injection is cool - it should however not be confined to the top-layer of the application. Inversion of control can help every layer and is a great way to de-couple various parts of the app.
This code brings in a framework (Lagom) to build a dependency injection framework and then adds a small function (``depends``) that adapts it to FastAPI's dependency injection mechanism ("Depends").
The advantages to this approach are numerous.
We don't need to write these little adapters for each component of the backend to adapt it FastAPI. For example look at the change to the roles API controller:
```diff
-def get_role_manager(app: UniverseApplication = Depends(get_app)) -> RoleManager:
- return app.role_manager
-
-
@cbv(router)
class FastAPIRoles:
- role_manager: RoleManager = Depends(get_role_manager)
+ role_manager: RoleManager = depends(RoleManager)
```
This is much less boilerplate. We don't need to implement & type that function get_role_manager and we don't need to bring in the import on UniverseApplication.
Additionally, we've got a clean abstraction that shields us from ``fastapi`` imports in all of our controllers. It should make it more possible to switch to new frameworks and such as the Python ecosystem matures.
Also, the same DI that is used to inject ``RoleManager`` into this contoller is used to inject app into RoleManager when it is constructed during application initialization. Any component being managed by UniverseApplication can now rely on its constructor arguments to be injected if it wants. It is easy to see the cool examples on FastAPI and think it is just a technology for controllers, but it totally is not.
I don't think there is really a way to use FastAPI's dependency injection outside the context of that framework, but even if one could Lagom is superior. All the auto-wiring is by type and requires zero framework and zero configuration (https://github.com/meadsteve/lagom#auto-wiring-with-zero-configuraton).
Having a web framework provide these framework-bound extension points for injecting stuff into controllers was the state of the art of Java like 15 years ago. Skipping that whole learning process and using the type system and auto-wiring that isn't dependent on framework annotation really jumps out to Java circa 8 years ago!
Why Lagom is an interesting question. When researching DI frameworks, I couldn't find a clear winner but Lagom focus on type annotations versus annotating by name makes it clearly more modern than a lot frameworks by much bigger names (https://github.com/meadsteve/lagom/blob/master/docs/comparison.md). The other type-centric framework that had even a nice interface that I found was punq (https://punq.readthedocs.io/en/latest/). The development just doesn't seem as active on punq. While I didn't land up using the integration Lagom seems to have async frameworks in mind (https://github.com/meadsteve/lagom/blob/master/lagom/integrations/fast_api.py), so that is another plus. Ultimately though I think I can swap between these two with like 10 lines of code switch, they do cool things with very simple interfaces and neither requires a bunch of investment in annotation on your components.
151 lines
4.6 KiB
Python
151 lines
4.6 KiB
Python
""" Module contains test fixtures meant to aide in the testing of jobs and
|
|
tool evaluation. Such extensive "fixtures" are something of an anti-pattern
|
|
so use of this should be limitted to tests of very 'extensive' classes.
|
|
"""
|
|
|
|
import os.path
|
|
import shutil
|
|
import string
|
|
import tempfile
|
|
from collections import defaultdict
|
|
|
|
import galaxy.datatypes.registry
|
|
import galaxy.model
|
|
from galaxy.tool_util.parser import get_tool_source
|
|
from galaxy.tools import create_tool_from_source
|
|
from galaxy.util.bunch import Bunch
|
|
from .unittest_utils import galaxy_mock
|
|
|
|
|
|
datatypes_registry = galaxy.datatypes.registry.Registry()
|
|
datatypes_registry.load_datatypes()
|
|
galaxy.model.set_datatypes_registry(datatypes_registry)
|
|
|
|
|
|
class UsesApp:
|
|
|
|
def setup_app(self):
|
|
self.test_directory = tempfile.mkdtemp()
|
|
self.app = galaxy_mock.MockApp()
|
|
self.app.config.new_file_path = os.path.join(self.test_directory, "new_files")
|
|
self.app.config.admin_users = "mary@example.com"
|
|
self.app.job_search = None
|
|
|
|
def tear_down_app(self):
|
|
shutil.rmtree(self.test_directory)
|
|
|
|
|
|
# Simple tool with just one text parameter and output.
|
|
SIMPLE_TOOL_CONTENTS = '''<tool id="${tool_id}" name="Test Tool" version="$version" profile="$profile">
|
|
<command>echo "$param1" < $out1</command>
|
|
<inputs>
|
|
<param type="text" name="param1" value="" />
|
|
</inputs>
|
|
<outputs>
|
|
<data name="out1" format="data" label="Output ($param1)" />
|
|
</outputs>
|
|
</tool>
|
|
'''
|
|
|
|
|
|
# A tool with data parameters (kind of like cat1) my favorite test tool :)
|
|
SIMPLE_CAT_TOOL_CONTENTS = '''<tool id="${tool_id}" name="Test Tool" version="$version" profile="$profile">
|
|
<command>cat "$param1" #for $r in $repeat# "$r.param2" #end for# < $out1</command>
|
|
<inputs>
|
|
<param type="data" format="tabular" name="param1" value="" />
|
|
<repeat name="repeat1" label="Repeat 1">
|
|
<param type="data" format="tabular" name="param2" value="" />
|
|
</repeat>
|
|
</inputs>
|
|
<outputs>
|
|
<data name="out1" format="data" />
|
|
</outputs>
|
|
</tool>
|
|
'''
|
|
|
|
|
|
class UsesTools:
|
|
|
|
def _init_tool(
|
|
self,
|
|
tool_contents=SIMPLE_TOOL_CONTENTS,
|
|
filename="tool.xml",
|
|
version="1.0",
|
|
profile="16.01",
|
|
tool_id="test_tool",
|
|
extra_file_contents=None,
|
|
extra_file_path=None,
|
|
):
|
|
self._init_app_for_tools()
|
|
self.tool_file = os.path.join(self.test_directory, filename)
|
|
contents_template = string.Template(tool_contents)
|
|
tool_contents = contents_template.safe_substitute(dict(version=version, profile=profile, tool_id=tool_id))
|
|
self.__write_tool(tool_contents)
|
|
if extra_file_contents and extra_file_path:
|
|
self.__write_tool(extra_file_contents, path=os.path.join(self.test_directory, extra_file_path))
|
|
return self.__setup_tool()
|
|
|
|
def _init_tool_for_path(self, tool_file):
|
|
self._init_app_for_tools()
|
|
self.tool_file = tool_file
|
|
return self.__setup_tool()
|
|
|
|
def _init_app_for_tools(self):
|
|
self.app.config.drmaa_external_runjob_script = ""
|
|
self.app.config.tool_secret = "testsecret"
|
|
self.app.config.track_jobs_in_database = False
|
|
self.app.job_config["get_job_tool_configurations"] = lambda ids: [Bunch(handler=Bunch())]
|
|
|
|
def __setup_tool(self):
|
|
tool_source = get_tool_source(self.tool_file)
|
|
try:
|
|
self.tool = create_tool_from_source(self.app, tool_source, config_file=self.tool_file)
|
|
self.tool.assert_finalized()
|
|
except Exception:
|
|
self.tool = None
|
|
if getattr(self, "tool_action", None and self.tool):
|
|
self.tool.tool_action = self.tool_action
|
|
return self.tool
|
|
|
|
def __write_tool(self, contents, path=None):
|
|
path = path or self.tool_file
|
|
with open(path, "w") as out:
|
|
out.write(contents)
|
|
|
|
|
|
class MockContext:
|
|
|
|
def __init__(self, model_objects=None):
|
|
self.expunged_all = False
|
|
self.flushed = False
|
|
self.model_objects = model_objects or defaultdict(lambda: {})
|
|
self.created_objects = []
|
|
self.current = self
|
|
|
|
def expunge_all(self):
|
|
self.expunged_all = True
|
|
|
|
def query(self, clazz):
|
|
return MockQuery(self.model_objects.get(clazz))
|
|
|
|
def flush(self):
|
|
self.flushed = True
|
|
|
|
def add(self, object):
|
|
self.created_objects.append(object)
|
|
|
|
|
|
class MockQuery:
|
|
|
|
def __init__(self, class_objects):
|
|
self.class_objects = class_objects
|
|
|
|
def filter_by(self, **kwds):
|
|
return Bunch(first=lambda: None)
|
|
|
|
def get(self, id):
|
|
return self.class_objects.get(id, None)
|
|
|
|
|
|
__all__ = ('UsesApp', )
|