mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
Feature: Extension Framework upgrade incl. middleware, extensionsdependencies and -callbacks (#2186)
This PR brings the ability of having your own callbacks for extensions, depending on other extensions and callbacks now have a return_style either collect (default) or middleware
This commit is contained in:
parent
a1bb412451
commit
816726b935
22 changed files with 517 additions and 173 deletions
|
|
@ -9,3 +9,4 @@ Some important one is the `after_serverpy_init_app` which passes a `Scheduler` c
|
|||
|
||||
|
||||
|
||||
In the case of middleware, you can pass one object which will in turn passed to all extensions which registered that callback. Have a look at the `adjust_view_model` callback which is explained in detail in the frontend-section.
|
||||
|
|
|
|||
|
|
@ -148,8 +148,6 @@ A reasonable `mywalletdetails.jinja` would look like this:
|
|||
|
||||
## Extending certain pages or complete endpoints (don't use this for now)
|
||||
|
||||
Unfortunately this method is only able to be used once per Extension. So it's more or less unusable right now as there are already two extensions which are using those.
|
||||
|
||||
For some endpoints, there is the possibility to extend/change parts of a page or the complete page. This works by declaring the `callback_adjust_view_model` method in your extension and modify the ViewModel which got passed into the callback. As there is only one callback for all types of ViewModels, you will need to check for the type that you're expecting and only adjust this type. Here is an example:
|
||||
|
||||
```python
|
||||
|
|
@ -163,10 +161,9 @@ class ExtensionidService(Service):
|
|||
# view_model.about_redirect=url_for("spectrum_endpoint.some_enpoint_here")
|
||||
# but we do it small here and only replace a specific component:
|
||||
view_model.get_started_include = "spectrum/welcome/components/get_started.jinja"
|
||||
return view_model
|
||||
return None
|
||||
return view_model
|
||||
```
|
||||
Make sure to return `None` if the `view_model` is not the type you're interested in.
|
||||
Make sure to return the view_model in anycase. No matter whether it's the correct type or not.
|
||||
|
||||
In this example, a certain part of the page gets replaced. As you can read in the comments, you could also trigger a complete redirect to a different endpoint.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import logging
|
||||
from cryptoadvance.specter.managers.service_manager import ServiceManager
|
||||
from cryptoadvance.specter.managers.service_manager import ExtensionManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Collecting template and static files from the different services in src/cryptoadvance/specter/services
|
||||
service_template_datas = [
|
||||
(service_dir, "templates")
|
||||
for service_dir in ServiceManager.get_service_x_dirs("templates")
|
||||
for service_dir in ExtensionManager.get_service_x_dirs("templates")
|
||||
]
|
||||
service_static_datas = [
|
||||
(service_dir, "static")
|
||||
for service_dir in ServiceManager.get_service_x_dirs("static")
|
||||
for service_dir in ExtensionManager.get_service_x_dirs("static")
|
||||
]
|
||||
|
||||
# Collect Packages from the services, including service- and controller-classes
|
||||
service_packages = ServiceManager.get_service_packages()
|
||||
service_packages = ExtensionManager.get_service_packages()
|
||||
|
||||
|
||||
datas = [*service_template_datas, *service_static_datas]
|
||||
|
|
|
|||
|
|
@ -686,9 +686,9 @@ specterext-exfund==0.1.7 \
|
|||
specterext-faucet==0.1.2 \
|
||||
--hash=sha256:86db78a6c41688152cfeec14efafd6d06c97e6edd9735461ee897495f90cb2e8
|
||||
# via -r requirements.in
|
||||
specterext-stacktrack==0.2.1 \
|
||||
--hash=sha256:28729d1a981d8c061902b3d26baefc723d286aecea4d203ed729d1c89a9de267 \
|
||||
--hash=sha256:34a5e9da8a3cb7a4c8b7bc4e0a417a043150e608a7261e56d84fbfba9af15b8a
|
||||
specterext-stacktrack==0.3.0 \
|
||||
--hash=sha256:14f96f1f552f57ba017b8bc642f07343edbb1abafe09e03bbaae179d78d7ce23 \
|
||||
--hash=sha256:9e2946185730aab377951e83a27d8791a34e0f031e44f15991212b6b85722ca0
|
||||
# via -r requirements.in
|
||||
sqlalchemy==1.4.42 \
|
||||
--hash=sha256:04f2598c70ea4a29b12d429a80fad3a5202d56dce19dd4916cc46a965a5ca2e9 \
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from ..helpers import alias, calc_fullpath, load_jsons
|
|||
from ..node import Node, NonExistingNode
|
||||
from ..internal_node import InternalNode
|
||||
from ..services import callbacks
|
||||
from ..managers.service_manager import ServiceManager
|
||||
from ..managers.service_manager import ExtensionManager
|
||||
from ..util.bitcoind_setup_tasks import setup_bitcoind_thread
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -36,7 +36,7 @@ class NodeManager:
|
|||
self.only_tor = only_tor
|
||||
self.bitcoind_path = bitcoind_path
|
||||
self.internal_bitcoind_version = internal_bitcoind_version
|
||||
self.service_manager: ServiceManager = service_manager
|
||||
self.service_manager: ExtensionManager = service_manager
|
||||
self.load_from_disk(data_folder)
|
||||
internal_nodes = [
|
||||
node for node in self.nodes.values() if not node.external_node
|
||||
|
|
|
|||
|
|
@ -1 +1,5 @@
|
|||
from .service_manager import *
|
||||
# intermediate state!
|
||||
# next steps:
|
||||
# from .extension_manager import ExtensionManager
|
||||
# and then refactor the whole package to the new name ...
|
||||
from .service_manager import ExtensionManager
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
from cryptoadvance.specter.util.reflection import (
|
||||
get_template_static_folder,
|
||||
get_subclasses,
|
||||
)
|
||||
from flask import current_app as app
|
||||
from flask import url_for
|
||||
from flask.blueprints import Blueprint
|
||||
|
||||
from cryptoadvance.specter.util.specter_migrator import SpecterMigration
|
||||
|
||||
from ...services.service import Service
|
||||
from ...services.callbacks import *
|
||||
from ...services.callbacks import Callback
|
||||
from ...specter_error import SpecterInternalException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CallbackExecutor:
|
||||
"""encapsulating the complexities of the extension callbacks"""
|
||||
|
||||
def __init__(self, extensions):
|
||||
self._extensions = extensions
|
||||
|
||||
def execute_ext_callbacks(self, callback, *args, **kwargs):
|
||||
"""will execute the callback function for each extension which has defined that method
|
||||
the callback_id needs to be passed and specify why the callback has been called.
|
||||
It needs to be one of the constants defined in cryptoadvance.specter.services.callbacks
|
||||
"""
|
||||
self.check_callback(callback)
|
||||
# No debug statement here possible as this is called for every request and would flood the logs
|
||||
# logger.debug(f"Executing callback {callback_id}")
|
||||
return_values = {}
|
||||
for ext in self.services_sorted:
|
||||
if hasattr(ext, f"callback_{callback.id}"):
|
||||
# logger.debug(f"About to execute on ext {ext.id} callback_{callback.id}")
|
||||
return_values[ext.id] = getattr(ext, f"callback_{callback.id}")(
|
||||
*args, **kwargs
|
||||
)
|
||||
# logger.debug(f"returned {return_values[ext.id]}")
|
||||
if callback.return_style == "middleware":
|
||||
args = [return_values[ext.id]]
|
||||
# Filtering out all None return values
|
||||
return_values = {k: v for k, v in return_values.items() if v is not None}
|
||||
# logger.debug(f"return_values for callback {callback.id} {return_values}")
|
||||
if callback.return_style == "collect":
|
||||
return return_values
|
||||
elif callback.return_style == "middleware":
|
||||
return args[0]
|
||||
else:
|
||||
raise SpecterInternalException(
|
||||
f"""
|
||||
Unknown callback return_style {callback.return_style} for callback {callback}
|
||||
"""
|
||||
)
|
||||
|
||||
@property
|
||||
def extensions(self) -> Dict[str, Service]:
|
||||
return self._extensions or {}
|
||||
|
||||
@property
|
||||
def services_sorted(self) -> List[Service]:
|
||||
"""A list of sorted extensions. First sort-criteria is the dependency. Second one the sort-priority"""
|
||||
if hasattr(self, "_services_sorted"):
|
||||
return self._services_sorted
|
||||
exts_sorted = topological_sort(self.extensions.values())
|
||||
for ext in exts_sorted:
|
||||
ext.__class__.dependency_level = 0
|
||||
for ext in exts_sorted:
|
||||
set_dependency_level_recursive(ext.__class__, 0)
|
||||
exts_sorted.sort(
|
||||
key=lambda x: (-x.dependency_level, -getattr(x, "sort_priority", 0))
|
||||
)
|
||||
self._services_sorted = exts_sorted
|
||||
return self._services_sorted
|
||||
|
||||
@property
|
||||
def all_callbacks(self):
|
||||
return get_subclasses(Callback)
|
||||
|
||||
def check_callback(self, callback, *args, **kwargs):
|
||||
"""A callback argument needs to:
|
||||
* be a class which derives from callback
|
||||
* or a instance of such a class. This might simplify the the whole extensionframework in the future
|
||||
* If a callback has the return-type "middleware", it has to have exactly one argument (otherwise the whole thing gets to complicated)
|
||||
If one of the checks fails, it'll raise an SpecterInternalException. This behaviour should be improved over time but as long as
|
||||
the number of extensions are so small, that behaviour helps to catch issues in all extensions.
|
||||
ToDo in the future: Complaining and ignoring.
|
||||
"""
|
||||
if type(callback) != type:
|
||||
callback: Callback = callback.__class__
|
||||
if callback not in self.all_callbacks:
|
||||
raise SpecterInternalException(
|
||||
f"""
|
||||
Non existing callback_id: {callback} or your class does not inherit from Callback
|
||||
"""
|
||||
)
|
||||
if callback.return_style == "middleware":
|
||||
if len(args) > 1:
|
||||
raise SpecterInternalException(
|
||||
f"""
|
||||
The callback {callback} is using middleware but it's passing more than one argument: {args}."
|
||||
"""
|
||||
)
|
||||
|
||||
if len(kwargs.values()) > 0:
|
||||
raise SpecterInternalException(
|
||||
f"""
|
||||
The callback {callback} is using middleware but it's using named arguments: {kwargs}.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def topological_sort(instances):
|
||||
"""Sorts a list of instances so that non dependent ones come first"""
|
||||
class_map = {instance.__class__: instance for instance in instances}
|
||||
in_degree = {cls: 0 for cls in class_map.keys()}
|
||||
graph = {cls: set() for cls in class_map.keys()}
|
||||
|
||||
for cls, instance in class_map.items():
|
||||
for dep in getattr(cls, "depends", []):
|
||||
graph[dep].add(cls)
|
||||
in_degree[cls] += 1
|
||||
|
||||
no_incoming_edges = [cls for cls in class_map.keys() if in_degree[cls] == 0]
|
||||
output = []
|
||||
|
||||
while no_incoming_edges:
|
||||
node = no_incoming_edges.pop()
|
||||
output.append(class_map[node])
|
||||
|
||||
for child in graph[node]:
|
||||
in_degree[child] -= 1
|
||||
if in_degree[child] == 0:
|
||||
no_incoming_edges.append(child)
|
||||
|
||||
if len(output) != len(class_map):
|
||||
raise ValueError("Graph contains a cycle.")
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def set_dependency_level_recursive(ext, level=0):
|
||||
for dep in getattr(ext, "depends", []):
|
||||
dep.dependency_level = max(getattr(dep, "dependency_level", 0), level + 1)
|
||||
set_dependency_level_recursive(dep, level + 1)
|
||||
|
|
@ -10,7 +10,10 @@ from typing import Dict, List
|
|||
|
||||
from cryptoadvance.specter.config import ProductionConfig
|
||||
from cryptoadvance.specter.device import Device
|
||||
from cryptoadvance.specter.specter_error import SpecterError, SpecterInternalException
|
||||
from cryptoadvance.specter.managers.service_manager.callback_executor import (
|
||||
CallbackExecutor,
|
||||
)
|
||||
from cryptoadvance.specter.specter_error import SpecterError
|
||||
from cryptoadvance.specter.user import User
|
||||
from cryptoadvance.specter.util.reflection import get_template_static_folder
|
||||
from flask import current_app as app
|
||||
|
|
@ -33,7 +36,7 @@ from ...util.reflection_fs import search_dirs_in_path
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServiceManager:
|
||||
class ExtensionManager:
|
||||
"""Loads support for all Services it auto-discovers."""
|
||||
|
||||
def __init__(self, specter, devstatus_threshold):
|
||||
|
|
@ -75,14 +78,15 @@ class ServiceManager:
|
|||
self.specter.ext[clazz.id] = self._services[clazz.id]
|
||||
# maybe register the blueprint
|
||||
self.register_blueprint_for_ext(clazz, self._services[clazz.id])
|
||||
self.add_devices_for_ext(clazz, self._services[clazz.id])
|
||||
self.register_devices_from_ext(self._services[clazz.id])
|
||||
logger.info(f"Service {clazz.__name__} activated ({clazz.devstatus})")
|
||||
else:
|
||||
logger.info(
|
||||
f"Service {clazz.__name__} not activated due to devstatus ( {self.devstatus_threshold} > {clazz.devstatus} )"
|
||||
)
|
||||
logger.info("----> finished service loading")
|
||||
self.execute_ext_callbacks("afterServiceManagerInit")
|
||||
logger.info("----> finished service processing")
|
||||
self.callback_executor = CallbackExecutor(self.services)
|
||||
self.execute_ext_callbacks(callbacks.afterExtensionManagerInit)
|
||||
|
||||
@classmethod
|
||||
def register_blueprint_for_ext(cls, clazz, ext):
|
||||
|
|
@ -187,13 +191,49 @@ class ServiceManager:
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def add_devices_for_ext(cls, clazz, ext):
|
||||
if hasattr(clazz, "devices"):
|
||||
devices_modules = clazz.devices
|
||||
else:
|
||||
def register_devices_from_ext(cls, ext):
|
||||
"""extract the devices from the extension and appends all found one to
|
||||
cryptoadvance.specter.devices
|
||||
"""
|
||||
classes = cls.extract_thing_classes_from_extension("devices", Device, ext)
|
||||
if not classes:
|
||||
return
|
||||
from cryptoadvance.specter.devices import __all__ as all_devices
|
||||
|
||||
for device_class in classes:
|
||||
all_devices.append(device_class)
|
||||
logger.debug(f" Loaded Device {device_class}")
|
||||
|
||||
@classmethod
|
||||
def register_callbacks_from_ext(cls, ext):
|
||||
"""extract all callbacks from the extension and import them so that they are
|
||||
discoverable as subclass of Callback.
|
||||
"""
|
||||
# importing it is the main job here. If it's imported, it'll also be discovered
|
||||
# as a subclass of Callback.
|
||||
classes = cls.extract_thing_classes_from_extension(
|
||||
"callbacks", callbacks.Callback, ext
|
||||
)
|
||||
for callback_class in classes:
|
||||
logger.debug(f" Loaded Callback {callback_class}")
|
||||
|
||||
@classmethod
|
||||
def extract_thing_classes_from_extension(
|
||||
cls, things: str, thing_class: type, ext
|
||||
) -> List[type]:
|
||||
"""If an extension has a definition like that:
|
||||
class SomeExtension(Extension)
|
||||
things = ["someNym.specterext.some_extensions.things"]
|
||||
then this method will return a list of all the thing_class classes
|
||||
which can be found in that module:
|
||||
extract_thing_classes_from_extension("things",Thing, someExtension)
|
||||
"""
|
||||
if hasattr(ext.__class__, things):
|
||||
thing_modules: List[str] = getattr(ext.__class__, things)
|
||||
else:
|
||||
return []
|
||||
classes = []
|
||||
for module in devices_modules:
|
||||
for module in thing_modules:
|
||||
try:
|
||||
classes.extend(
|
||||
get_classlist_of_type_clazz_from_modulelist(Device, [module])
|
||||
|
|
@ -278,33 +318,9 @@ class ServiceManager:
|
|||
the callback_id needs to be passed and specify why the callback has been called.
|
||||
It needs to be one of the constants defined in cryptoadvance.specter.services.callbacks
|
||||
"""
|
||||
if callback_id not in dir(callbacks):
|
||||
raise Exception(f"Non existing callback_id: {callback_id}")
|
||||
# No debug statement here possible as this is called for every request and would flood the logs
|
||||
# logger.debug(f"Executing callback {callback_id}")
|
||||
return_values = {}
|
||||
for ext in self.services.values():
|
||||
if hasattr(ext, f"callback_{callback_id}"):
|
||||
try:
|
||||
return_values[ext.id] = getattr(ext, f"callback_{callback_id}")(
|
||||
*args, **kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
# Development should catch all errors early!
|
||||
if app.config["SPECTER_CONFIGURATION_CLASS_FULLNAME"].endswith(
|
||||
"DevelopmentConfig"
|
||||
):
|
||||
raise e
|
||||
logger.error(
|
||||
"Exception {e} while executing {callback_id} for extension {ext.id}"
|
||||
)
|
||||
logger.exception(e)
|
||||
elif hasattr(ext, "callback"):
|
||||
return_values[ext.id] = ext.callback(callback_id, *args, **kwargs)
|
||||
# Filtering out all None return values
|
||||
return_values = {k: v for k, v in return_values.items() if v is not None}
|
||||
# logger.debug(f"return_values for callback {callback_id} {return_values}")
|
||||
return return_values
|
||||
return self.callback_executor.execute_ext_callbacks(
|
||||
callback_id, *args, **kwargs
|
||||
)
|
||||
|
||||
@property
|
||||
def services(self) -> Dict[str, Service]:
|
||||
|
|
@ -418,8 +434,7 @@ class ServiceManager:
|
|||
]
|
||||
logger.info(f"Initial arr:")
|
||||
for element in arr:
|
||||
logger.info(element)
|
||||
# /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/bitcoinreserve/templates
|
||||
logger.debug(element)
|
||||
# /home/kim/src/specter-desktop/.buildenv/lib/python3.8/site-packages/cryptoadvance/specter/services/swan/templates
|
||||
|
||||
# filter only directories
|
||||
|
|
@ -474,12 +489,13 @@ class ServiceManager:
|
|||
)
|
||||
)
|
||||
logger.info(f"After extending: {arr}")
|
||||
|
||||
# Before we transform the arr into an array of strings, we iterate through all services to discover
|
||||
# the devices which might be specified in there
|
||||
devices_arr = []
|
||||
for clazz in arr:
|
||||
if hasattr(clazz, "devices"):
|
||||
logger.debug("class {clazz} has devices: {clazz.devices}")
|
||||
logger.debug(f"class {clazz.__name__} has devices: {clazz.devices}")
|
||||
for device in clazz.devices:
|
||||
try:
|
||||
import_module(device)
|
||||
|
|
@ -487,12 +503,24 @@ class ServiceManager:
|
|||
except ModuleNotFoundError as e:
|
||||
pass
|
||||
|
||||
# Same for callbacks
|
||||
callbacks_arr = []
|
||||
for clazz in arr:
|
||||
if hasattr(clazz, "callbacks"):
|
||||
logger.debug(f"class {clazz.__name__} has callbacks: {clazz.callbacks}")
|
||||
for device in clazz.callbacks:
|
||||
try:
|
||||
import_module(device)
|
||||
callbacks_arr.append(device)
|
||||
except ModuleNotFoundError as e:
|
||||
pass
|
||||
|
||||
# Transform into array of strings
|
||||
arr = [clazz.__module__ for clazz in arr]
|
||||
|
||||
# Add the devices
|
||||
arr.extend(devices_arr)
|
||||
logger.debug(f"After transforming + devices: {arr}")
|
||||
arr.extend(callbacks_arr)
|
||||
logger.debug(f"After transforming + devices + callbacks: {arr}")
|
||||
|
||||
# Controller-Packagages from the services are not imported via the service but via the baseclass
|
||||
# Therefore hiddenimport don't find them. We have to do it here.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ from distutils.core import setup
|
|||
from http.client import HTTPConnection
|
||||
from pathlib import Path
|
||||
|
||||
from cryptoadvance.specter.liquid.rpc import LiquidRPC
|
||||
from cryptoadvance.specter.managers.service_manager import ExtensionManager
|
||||
from cryptoadvance.specter.rpc import BitcoinRPC
|
||||
from cryptoadvance.specter.services import callbacks
|
||||
from cryptoadvance.specter.util.reflection import get_template_static_folder
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask, jsonify, redirect, request, session, url_for
|
||||
from flask_apscheduler import APScheduler
|
||||
|
|
@ -17,7 +22,7 @@ from werkzeug.wrappers import Response
|
|||
|
||||
from cryptoadvance.specter.hwi_rpc import HWIBridge
|
||||
from cryptoadvance.specter.liquid.rpc import LiquidRPC
|
||||
from cryptoadvance.specter.managers.service_manager import ServiceManager
|
||||
from cryptoadvance.specter.managers.service_manager import ExtensionManager
|
||||
from cryptoadvance.specter.rpc import BitcoinRPC
|
||||
from cryptoadvance.specter.services import callbacks
|
||||
from cryptoadvance.specter.util.reflection import get_template_static_folder
|
||||
|
|
@ -158,7 +163,7 @@ def init_app(app: SpecterFlask, hwibridge=False, specter=None):
|
|||
# It's an attribute to the specter but specter is not aware of it.
|
||||
# However some managers are aware of it and so we need to split
|
||||
# instantiation from initializing and in between attach the service_manager
|
||||
specter.service_manager = ServiceManager(
|
||||
specter.service_manager = ExtensionManager(
|
||||
specter=specter, devstatus_threshold=app.config["SERVICES_DEVSTATUS_THRESHOLD"]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -74,28 +74,11 @@ def wallets_overview():
|
|||
# that's why we need so many lines for just expressing:
|
||||
# "Here is a ViewModel, adjust it if you want"
|
||||
# We need to change that method to enable "middleware"
|
||||
wallets_overview_vm_dict = app.specter.service_manager.execute_ext_callbacks(
|
||||
wallets_overview_vm = app.specter.service_manager.execute_ext_callbacks(
|
||||
adjust_view_model, WalletsOverviewVm()
|
||||
)
|
||||
number_of_wallets_overview_vm = len(
|
||||
[
|
||||
wallets_overview_vm
|
||||
for wallets_overview_vm in wallets_overview_vm_dict.values()
|
||||
if type(wallets_overview_vm) == WalletsOverviewVm
|
||||
]
|
||||
)
|
||||
if number_of_wallets_overview_vm > 1:
|
||||
raise Exception(
|
||||
f"Seems that we have more than one WalletsOverviewVm Extension: {wallets_overview_vm_dict} "
|
||||
)
|
||||
if number_of_wallets_overview_vm == 1:
|
||||
wallets_overview_vm = list(wallets_overview_vm_dict.values())[0]
|
||||
else:
|
||||
wallets_overview_vm = WalletsOverviewVm()
|
||||
if wallets_overview_vm.wallets_overview_redirect != None:
|
||||
logger.info(
|
||||
f"Extension {list(wallets_overview_vm_dict.keys())[0]} redirects to {wallets_overview_vm.wallets_overview_redirect}"
|
||||
)
|
||||
logger.info(f"redirecting to {wallets_overview_vm.wallets_overview_redirect}")
|
||||
return redirect(wallets_overview_vm.wallets_overview_redirect)
|
||||
|
||||
wallet: Wallet
|
||||
|
|
|
|||
|
|
@ -53,28 +53,11 @@ def about():
|
|||
# that's why we need so many lines for just expressing:
|
||||
# "Here is a ViewModel, adjust it if you want"
|
||||
# We need to change that method to enable "middleware"
|
||||
welcome_vm_dict = app.specter.service_manager.execute_ext_callbacks(
|
||||
welcome_vm = app.specter.service_manager.execute_ext_callbacks(
|
||||
adjust_view_model, WelcomeVm()
|
||||
)
|
||||
number_of_welcome_vm = len(
|
||||
[
|
||||
wallets_overview_vm
|
||||
for wallets_overview_vm in welcome_vm_dict.values()
|
||||
if type(wallets_overview_vm) == WelcomeVm
|
||||
]
|
||||
)
|
||||
if number_of_welcome_vm > 1:
|
||||
raise SpecterInternalException(
|
||||
f"Seems that we have more than one WelcomeVm Extension: {welcome_vm_dict} "
|
||||
)
|
||||
if number_of_welcome_vm == 1:
|
||||
welcome_vm = list(welcome_vm_dict.values())[0]
|
||||
else:
|
||||
welcome_vm = WelcomeVm()
|
||||
if welcome_vm.about_redirect != None:
|
||||
logger.info(
|
||||
f"Extension {list(welcome_vm_dict.keys())[0]} redirects to {welcome_vm_dict.wallets_overview_redirect}"
|
||||
)
|
||||
logger.info(f"redirecting to {welcome_vm.about_redirect}")
|
||||
return redirect(welcome_vm.about_redirect)
|
||||
|
||||
if request.method == "POST":
|
||||
|
|
|
|||
|
|
@ -1,75 +1,119 @@
|
|||
""" Here we have some constants getting an id for extension-points/callbacks. As camelcase
|
||||
is used, we don't use CAPITAL letters to not loose the meaning of the camelcase.
|
||||
""" Here we have classes extension-points/callbacks.
|
||||
|
||||
These constants are expected as parameter to the ServiceManager.callback function
|
||||
These constants are expected as parameter to the ExtensionManager.callback function
|
||||
and it'll throw an exception if the constant does not exist.
|
||||
|
||||
Callbacks have a return_style which determines how the return values get collected:
|
||||
* "collect" will return a dict where the key is the id of the extension and the value is
|
||||
the returnvalue of that extension
|
||||
* "middleware" will expect that the extension is returning something which will in turn
|
||||
get a parameter to the next extension`s call. The last extension's returnvalue will
|
||||
then become the returnvalue of the callback.
|
||||
There are some weak naming conventions:
|
||||
1. after/before
|
||||
2. class or file
|
||||
3. method or function
|
||||
callbacks
|
||||
|
||||
"""
|
||||
|
||||
"""
|
||||
I don't know why we have this one. Doesn't seem to be used anywhere.
|
||||
"""
|
||||
afterServiceManagerInit = "afterServiceManagerInit"
|
||||
|
||||
"""
|
||||
This one is called, after the init_app method has finished. The "run" method has not
|
||||
class Callback:
|
||||
return_style = "collect"
|
||||
|
||||
|
||||
class afterExtensionManagerInit(Callback):
|
||||
"""
|
||||
I don't know why we have this one. Doesn't seem to be used anywhere.
|
||||
"""
|
||||
|
||||
id = "afterExtensionManagerInit"
|
||||
|
||||
|
||||
class after_serverpy_init_app(Callback):
|
||||
"""
|
||||
This one is called, after the init_app method has finished. The "run" method has not
|
||||
been executed yet and so urls can't be called yet.
|
||||
So this is the best place for almost all extensions to do their initializing work.
|
||||
"""
|
||||
after_serverpy_init_app = "after_serverpy_init_app"
|
||||
"""
|
||||
|
||||
""" Extensions which want to extend the settings dialog
|
||||
id = "after_serverpy_init_app"
|
||||
|
||||
|
||||
class add_settingstabs(Callback):
|
||||
"""Extensions which want to extend the settings dialog
|
||||
needs to return something like: return [{"title": "token", "endpoint":"settings_token"}]
|
||||
Check the extension-docs for a comprehensive example.
|
||||
"""
|
||||
add_settingstabs = "add_settingstabs"
|
||||
"""
|
||||
|
||||
""" Extensions which want to extend the wallet dialog
|
||||
id = "add_settingstabs"
|
||||
|
||||
|
||||
class add_wallettabs(Callback):
|
||||
"""Extensions which want to extend the wallet dialog
|
||||
needs to return something like: return [{"title": "sometitle", "endpoint":"yourendpoint"}]
|
||||
Check the extension-docs for a comprehensive example.
|
||||
"""
|
||||
add_wallettabs = "add_wallettabs"
|
||||
"""
|
||||
|
||||
""" Endpoints might define their behaviour via a ViewModel. Those Models are passed here and
|
||||
id = "add_wallettabs"
|
||||
|
||||
|
||||
class adjust_view_model(Callback):
|
||||
"""Endpoints might define their behaviour via a ViewModel. Those Models are passed here and
|
||||
extensions can modify that behaviour via Modifying that model. Currently there is only:
|
||||
cryptoadvance.specter.server_enpoints.welcome.welcome_vm.WelcomeVm
|
||||
"""
|
||||
adjust_view_model = "adjust_view_model"
|
||||
"""
|
||||
|
||||
"""
|
||||
id = "adjust_view_model"
|
||||
return_style = "middleware"
|
||||
|
||||
|
||||
class specter_persistence_callback(Callback):
|
||||
"""
|
||||
This one is called, whenever a file is persisted. To call external scripts in another
|
||||
process, you better use the SPECTER_PERSISTENCE_CALLBACK Env Var or it's asynchronous cousin
|
||||
SPECTER_PERSISTENCE_CALLBACK_ASYNC.
|
||||
"""
|
||||
specter_persistence_callback = "specter_persistence_callback"
|
||||
"""
|
||||
|
||||
"""
|
||||
id = "specter_persistence_callback"
|
||||
|
||||
|
||||
class flask_before_request(Callback):
|
||||
"""
|
||||
Will get called before every request via the Flask's @app.before_request
|
||||
"""
|
||||
flask_before_request = "flask_before_request"
|
||||
"""
|
||||
|
||||
"""
|
||||
id = "flask_before_request"
|
||||
|
||||
|
||||
class specter_added_to_flask_app(Callback):
|
||||
"""
|
||||
Will get called right after having access to app.specter
|
||||
"""
|
||||
specter_added_to_flask_app = "specter_added_to_flask_app"
|
||||
"""
|
||||
|
||||
"""
|
||||
Will get called when the server_endpoints.flash is called
|
||||
"""
|
||||
flash = "flash"
|
||||
|
||||
"""
|
||||
Callback that is not used yet, but could be implmented in server_endpoints just as flash
|
||||
"""
|
||||
create_and_show_notification = "create_and_show_notification"
|
||||
id = "specter_added_to_flask_app"
|
||||
|
||||
|
||||
"""
|
||||
class flash(Callback):
|
||||
"""Will get called if anyone is calling server_endpoionts.flash"""
|
||||
|
||||
id = "flash"
|
||||
|
||||
|
||||
class cleanup_on_exit(Callback):
|
||||
"""
|
||||
Callback that is called last in specter.cleanup_on_exit()
|
||||
"""
|
||||
cleanup_on_exit = "cleanup_on_exit"
|
||||
"""
|
||||
|
||||
id = "cleanup_on_exit"
|
||||
|
||||
|
||||
# ToDo: Think about this callback as it's specific to the notification-extension
|
||||
# We have now the possibility that callbacks are added by extensions but on the other hand
|
||||
# Core code should not execute code which got specified in extensions.
|
||||
# Maybe we could weaken that principle, though.
|
||||
class create_and_show_notification(Callback):
|
||||
"""
|
||||
Callback that is not used yet, but could be implmented in server_endpoints just as flash
|
||||
"""
|
||||
|
||||
id = "create_and_show_notification"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ services_endpoint = Blueprint(
|
|||
# All blueprint from Services are no longer loaded statically but dynamically when the service-class in initialized
|
||||
# check cryptoadvance.specter.services.service_manager.Service for doing that and
|
||||
# check cryptoadvance.specter.services/**/manifest for instances of Service-classes and
|
||||
# check cryptoadvance.specter.services.service_manager.ServiceManager.services for initialisation of ServiceClasses
|
||||
# check cryptoadvance.specter.services.service_manager.ExtensionManager.services for initialisation of ServiceClasses
|
||||
|
||||
|
||||
def user_secret_decrypted_required(func):
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class ServiceOptionality:
|
|||
opt_out = "opt_out"
|
||||
|
||||
|
||||
class Service:
|
||||
"""A base class for Services"""
|
||||
class Extension:
|
||||
"""A base class for Extensions"""
|
||||
|
||||
# These should be overrided in implementation classes
|
||||
id = None
|
||||
|
|
@ -42,7 +42,7 @@ class Service:
|
|||
desc = None # TODO: rename to "description" to be explicit
|
||||
has_blueprint = True # the default
|
||||
# If the blueprint gets a "/ext" prefix (isolated_client = True), the login cookie won't work for all specter core functionality
|
||||
isolated_client = True
|
||||
isolated_client = False
|
||||
devstatus = devstatus_alpha
|
||||
optionality = ServiceOptionality.opt_in
|
||||
visible_in_sidebar = True
|
||||
|
|
@ -246,3 +246,11 @@ class Service:
|
|||
return render_template("myext/inject_in_basejinja_body_bottom.jinja")
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Service(Extension):
|
||||
"""Deprecated! You should derive from Extension!
|
||||
This is only here for backwards compatibility and will be removed after some time
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -32,13 +32,18 @@ from .managers.config_manager import ConfigManager
|
|||
from .managers.device_manager import DeviceManager
|
||||
from .managers.node_manager import NodeManager
|
||||
from .managers.otp_manager import OtpManager
|
||||
from .managers.service_manager import ServiceManager
|
||||
from .managers.service_manager import ExtensionManager
|
||||
from .managers.user_manager import UserManager
|
||||
from .managers.wallet_manager import WalletManager
|
||||
from .node import Node
|
||||
from .persistence import read_json_file, write_json_file, write_node
|
||||
from .process_controller.bitcoind_controller import BitcoindPlainController
|
||||
from .rpc import BitcoinRPC, RpcError, get_default_datadir
|
||||
from .rpc import (
|
||||
BitcoinRPC,
|
||||
RpcError,
|
||||
get_default_datadir,
|
||||
)
|
||||
from .managers.service_manager import ExtensionManager
|
||||
from .services.service import devstatus_alpha, devstatus_beta, devstatus_prod
|
||||
from .services import callbacks
|
||||
from .specter_error import ExtProcTimeoutException, SpecterError
|
||||
|
|
|
|||
|
|
@ -187,16 +187,16 @@ def get_subclasses_for_clazz(
|
|||
continue
|
||||
try:
|
||||
module = import_module(f"{module_name}.service")
|
||||
logger.info(f" Imported {module_name}.service")
|
||||
logger.debug(f" Imported {module_name}.service")
|
||||
except ModuleNotFoundError as e:
|
||||
try:
|
||||
# Another style is orgname.specterext.extensionid, for that we have to guess the orgname:
|
||||
orgname = importer.path.split(os.path.sep)[-2]
|
||||
logger.info(f"guessing orgname: {orgname}")
|
||||
orgname = str(importer).split(os.path.sep)[-2]
|
||||
logger.debug(f"guessing orgname: {orgname}")
|
||||
module = import_module(
|
||||
f"{orgname}.specterext.{module_name}.service"
|
||||
)
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f" Imported {orgname}.specterext.{module_name}.service"
|
||||
)
|
||||
except ModuleNotFoundError as e:
|
||||
|
|
|
|||
|
|
@ -160,6 +160,4 @@ class SpectrumService(Service):
|
|||
view_model.get_started_include = (
|
||||
"spectrum/welcome/components/get_started.jinja"
|
||||
)
|
||||
else:
|
||||
return None
|
||||
return view_model
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from pathlib import PosixPath, Path
|
|||
import os
|
||||
from unittest.mock import MagicMock
|
||||
from flask import Flask
|
||||
from cryptoadvance.specter.managers.service_manager import ServiceManager
|
||||
from cryptoadvance.specter.managers.service_manager import ExtensionManager
|
||||
from cryptoadvance.specter.services.callbacks import after_serverpy_init_app
|
||||
|
||||
from cryptoadvance.specterext.swan.service import SwanService
|
||||
|
|
@ -13,12 +13,12 @@ from cryptoadvance.specterext.swan.service import SwanClient
|
|||
from cryptoadvance.specterext.devhelp.service import DevhelpService
|
||||
|
||||
|
||||
def test_ServiceManager2(mock_specter, mock_flaskapp, caplog):
|
||||
def test_ExtensionManager2(mock_specter, mock_flaskapp, caplog):
|
||||
ctx = mock_flaskapp.app_context()
|
||||
ctx.push()
|
||||
sm = ServiceManager(mock_specter, "alpha")
|
||||
sm = ExtensionManager(mock_specter, "alpha")
|
||||
# We have passed the TestConfig which is (hopefully) not existing in the Swan Extension
|
||||
# So the ServiceManager will move up the dependency tree of TestConfig until it finds
|
||||
# So the ExtensionManager will move up the dependency tree of TestConfig until it finds
|
||||
# a Config and will copy the keys into the flask-config
|
||||
assert mock_flaskapp.config["SWAN_API_URL"] == "https://api.dev.swanbitcoin.com"
|
||||
assert sm.services["swan"] != None
|
||||
|
|
@ -28,7 +28,7 @@ def test_ServiceManager2(mock_specter, mock_flaskapp, caplog):
|
|||
|
||||
def test_is_class_from_loaded_extension(mock_specter, mock_flaskapp):
|
||||
with mock_flaskapp.app_context():
|
||||
sm = ServiceManager(mock_specter, "alpha")
|
||||
sm = ExtensionManager(mock_specter, "alpha")
|
||||
assert type(sm.services_sorted[0]) == SwanService
|
||||
assert sm.is_class_from_loaded_extension(SwanClient)
|
||||
assert sm.is_class_from_loaded_extension(SwanService)
|
||||
|
|
@ -36,12 +36,12 @@ def test_is_class_from_loaded_extension(mock_specter, mock_flaskapp):
|
|||
|
||||
|
||||
@pytest.mark.skip(reason="The .buildenv directoy does not exist on the CI-Infra")
|
||||
def test_ServiceManager_get_service_x_dirs(caplog):
|
||||
def test_ExtensionManager_get_service_x_dirs(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
try:
|
||||
os.chdir("./pyinstaller")
|
||||
# THis is usefull in the pytinstaller/specterd.spec
|
||||
dirs = ServiceManager.get_service_x_dirs("templates")
|
||||
dirs = ExtensionManager.get_service_x_dirs("templates")
|
||||
# As the tests are executed in a development-environment (pip3 install -e .), the results we get back here
|
||||
# are not the same than the one we would get back when really are building. Because in that case,
|
||||
# the .buildenv would the environment and no symlinks would link to src/cryptoadavance...
|
||||
|
|
@ -56,7 +56,7 @@ def test_ServiceManager_get_service_x_dirs(caplog):
|
|||
for path in dirs:
|
||||
assert str(path).endswith("templates")
|
||||
|
||||
dirs = ServiceManager.get_service_x_dirs("static")
|
||||
dirs = ExtensionManager.get_service_x_dirs("static")
|
||||
assert f"{expected_folder}/cryptoadvance/specterext/swan/static" in [
|
||||
str(dir) for dir in dirs
|
||||
]
|
||||
|
|
@ -70,7 +70,7 @@ def test_ServiceManager_get_service_x_dirs(caplog):
|
|||
def test_ServiceManager_get_service_packages(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
packages = ServiceManager.get_service_packages()
|
||||
packages = ExtensionManager.get_service_packages()
|
||||
assert "cryptoadvance.specterext.electrum.service" in packages
|
||||
assert "cryptoadvance.specterext.electrum.devices.electrum" in packages
|
||||
assert "cryptoadvance.specterext.swan.service" in packages
|
||||
|
|
@ -97,7 +97,7 @@ def test_ServiceManager_make_path_relative(caplog):
|
|||
),
|
||||
Path("wurstbrot/something/.env/site-packages/the_rest"),
|
||||
]
|
||||
arr = [ServiceManager._make_path_relative(path) for path in arr]
|
||||
arr = [ExtensionManager._make_path_relative(path) for path in arr]
|
||||
assert arr[0] == PosixPath(
|
||||
"site-packages/cryptoadvance/specter/services/swan/templates"
|
||||
)
|
||||
|
|
@ -120,7 +120,7 @@ def mock_flaskapp(mock_specter):
|
|||
]
|
||||
flaskapp_mock.config["ISOLATED_CLIENT_EXT_URL_PREFIX"] = "/spc/ext"
|
||||
flaskapp_mock.config["EXT_URL_PREFIX"] = "/ext"
|
||||
# The ServiceManager is a flask-aware component. It will load all the services
|
||||
# The ExtensionManager is a flask-aware component. It will load all the services
|
||||
# however, in order to configure them, he needs to know about the configuration
|
||||
# of the specterApp.
|
||||
flaskapp_mock.config[
|
||||
|
|
|
|||
142
tests/test_managers_service_executor.py
Normal file
142
tests/test_managers_service_executor.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import pytest
|
||||
import logging
|
||||
from cryptoadvance.specter.managers.service_manager.callback_executor import (
|
||||
CallbackExecutor,
|
||||
)
|
||||
|
||||
from cryptoadvance.specter.managers.service_manager.callback_executor import (
|
||||
topological_sort,
|
||||
set_dependency_level_recursive,
|
||||
)
|
||||
from cryptoadvance.specter.services.callbacks import (
|
||||
flask_before_request,
|
||||
adjust_view_model,
|
||||
Callback,
|
||||
)
|
||||
from cryptoadvance.specter.specter_error import SpecterInternalException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Testclasses
|
||||
class A:
|
||||
id = "A"
|
||||
depends = []
|
||||
|
||||
def callback_adjust_view_model(self, model):
|
||||
logger.info(
|
||||
f"A.callback_adjust_view_model has been called with model='{model}'!"
|
||||
)
|
||||
return "Hello"
|
||||
|
||||
|
||||
class B:
|
||||
id = "B"
|
||||
pass
|
||||
|
||||
|
||||
class C:
|
||||
id = "C"
|
||||
depends = [A]
|
||||
|
||||
def callback_adjust_view_model(self, model):
|
||||
return model + " World"
|
||||
|
||||
|
||||
class D:
|
||||
id = "D"
|
||||
depends = [C, B]
|
||||
|
||||
|
||||
class E:
|
||||
id = "E"
|
||||
pass
|
||||
|
||||
|
||||
class F:
|
||||
id = "F"
|
||||
depends = [A, D]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def exts():
|
||||
return [F(), E(), D(), C(), B(), A()]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def callback_executor(exts):
|
||||
service_dict = {ext.__class__.__name__: ext for ext in exts}
|
||||
print(service_dict)
|
||||
ce = CallbackExecutor(service_dict)
|
||||
return ce
|
||||
|
||||
|
||||
def test_check_callback(callback_executor: CallbackExecutor):
|
||||
ce = callback_executor
|
||||
with pytest.raises(SpecterInternalException):
|
||||
ce.check_callback("muh")
|
||||
ce.check_callback(flask_before_request)
|
||||
ce.check_callback(adjust_view_model)
|
||||
|
||||
|
||||
def test_topological_sort(exts):
|
||||
|
||||
ext_sorted = topological_sort(exts)
|
||||
ext_classes = [ext.__class__ for ext in ext_sorted]
|
||||
assert ext_classes == [A, C, B, D, F, E]
|
||||
|
||||
|
||||
def test_set_dependency_level_recursive(exts):
|
||||
for ext in exts:
|
||||
ext.__class__.dependency_level = 0
|
||||
|
||||
set_dependency_level_recursive(C)
|
||||
assert A.dependency_level == 1
|
||||
set_dependency_level_recursive(D)
|
||||
assert C.dependency_level == 1
|
||||
assert B.dependency_level == 1
|
||||
assert A.dependency_level == 2
|
||||
set_dependency_level_recursive(F)
|
||||
assert C.dependency_level == 2
|
||||
assert B.dependency_level == 2
|
||||
assert A.dependency_level == 3
|
||||
assert D.dependency_level == 1
|
||||
# Doing it again does not change the numbers
|
||||
set_dependency_level_recursive(F)
|
||||
assert C.dependency_level == 2
|
||||
assert B.dependency_level == 2
|
||||
assert A.dependency_level == 3
|
||||
assert D.dependency_level == 1
|
||||
|
||||
|
||||
def test_CallbackExecutor_services_sorted(exts):
|
||||
A.sort_priority = 2
|
||||
B.sort_priority = 1
|
||||
E.sort_priority = 3
|
||||
|
||||
service_dict = {ext.__class__.__name__: ext for ext in exts}
|
||||
print(service_dict)
|
||||
ce = CallbackExecutor(service_dict)
|
||||
assert len(ce.services_sorted) == 6
|
||||
for ext in ce.services_sorted:
|
||||
print(
|
||||
f"{ext.__class__.__name__} {ext.__class__.dependency_level} {getattr(ext, 'sort_priority',9999)}"
|
||||
)
|
||||
assert [ext.__class__ for ext in ce.services_sorted] == [A, B, C, D, E, F]
|
||||
|
||||
|
||||
def test_CallbackExecutor_execute(caplog, callback_executor):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
ce = callback_executor
|
||||
rv = ce.execute_ext_callbacks(flask_before_request, "bumm", ["diedel", "bummm"])
|
||||
assert rv == {}
|
||||
assert ce.execute_ext_callbacks(adjust_view_model, "") == "Hello World"
|
||||
|
||||
|
||||
def test_topological_sort_cyclic_dependencies():
|
||||
|
||||
A.depends.append(F) # cyclic dependency
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ext_sorted = topological_sort([F(), E(), D(), C(), B(), A()])
|
||||
|
||||
del A.depends
|
||||
|
|
@ -20,7 +20,7 @@ from cryptoadvance.specter.services.service_encrypted_storage import (
|
|||
ServiceEncryptedStorageError,
|
||||
ServiceEncryptedStorageManager,
|
||||
)
|
||||
from cryptoadvance.specter.managers.service_manager import ServiceManager
|
||||
from cryptoadvance.specter.managers.service_manager import ExtensionManager
|
||||
from cryptoadvance.specter.user import User, hash_password
|
||||
|
||||
|
||||
|
|
@ -41,13 +41,13 @@ class FakeService(Service):
|
|||
|
||||
|
||||
# @patch("cryptoadvance.specter.services.service_manager.app")
|
||||
# def test_ServiceManager_loads_services(empty_data_folder, app):
|
||||
# def test_ExtensionManager_loads_services(empty_data_folder, app):
|
||||
# # app.config = MagicMock()
|
||||
# # app.config.get.return_value = "prod"
|
||||
# specter_mock = MagicMock()
|
||||
# specter_mock.data_folder.return_value = empty_data_folder
|
||||
|
||||
# service_manager = ServiceManager(specter=specter_mock, devstatus_threshold="alpha")
|
||||
# service_manager = ExtensionManager(specter=specter_mock, devstatus_threshold="alpha")
|
||||
# services = service_manager.services
|
||||
# assert "swan" in services
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import pytest
|
|||
import mock
|
||||
from mock import Mock, patch
|
||||
from cryptoadvance.specterext.swan.client import (
|
||||
SwanApiException,
|
||||
SwanApiRefreshTokenException,
|
||||
SwanClient,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -90,10 +90,7 @@ def test_get_subclasses_for_class(caplog):
|
|||
classlist = get_subclasses_for_clazz(SpecterMigration)
|
||||
assert len(classlist) >= 3
|
||||
classlist = get_subclasses_for_clazz(Service)
|
||||
assert len(classlist) >= 5
|
||||
# checking naively for certain Services would be counterproductive if you import
|
||||
# the class. THis needs to work without importing the class!
|
||||
# But we can test like this:
|
||||
assert "SwanService" in [cls.__name__ for cls in classlist]
|
||||
classlist = get_subclasses_for_clazz(Device)
|
||||
assert len(classlist) > 5
|
||||
assert len(classlist) == 5 # Happy to remove that at some point
|
||||
assert SwanService in classlist
|
||||
assert ElectrumService in classlist
|
||||
assert DevhelpService in classlist
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue