Autodiscovery of Blueprints, Middleware, and Listeners
How do I autodiscover the components I am using to build my application?
One of the first problems someone faces when building an application, is how to structure the project. Sanic makes heavy use of decorators to register route handlers, middleware, and listeners. And, after creating blueprints, they need to be mounted to the application.
A possible solution is a single file in which everything is imported and applied to the Sanic instance. Another is passing around the Sanic instance as a global variable. Both of these solutions have their drawbacks.
An alternative is autodiscovery. You point your application at modules (already imported, or strings), and let it wire everything up.
server.py
#
from sanic import Sanic
from sanic.response import empty
import blueprints
from utility import autodiscover
app = Sanic("auto", register=True)
autodiscover(
app,
blueprints,
"parent.child",
"listeners.something",
recursive=True,
)
app.route("/")(lambda _: empty())
[2021-03-02 21:37:02 +0200] [880451] [INFO] Goin' Fast @ http://127.0.0.1:9999
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something @ nested
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something @ level1
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something @ level3
[2021-03-02 21:37:02 +0200] [880451] [DEBUG] something inside __init__.py
[2021-03-02 21:37:02 +0200] [880451] [INFO] Starting worker [880451]
utility.py
#
from glob import glob
from importlib import import_module, util
from inspect import getmembers
from pathlib import Path
from types import ModuleType
from typing import Union
from sanic.blueprints import Blueprint
def autodiscover(
app, *module_names: Union[str, ModuleType], recursive: bool = False
):
mod = app.__module__
blueprints = set()
_imported = set()
def _find_bps(module):
nonlocal blueprints
for _, member in getmembers(module):
if isinstance(member, Blueprint):
blueprints.add(member)
for module in module_names:
if isinstance(module, str):
module = import_module(module, mod)
_imported.add(module.__file__)
_find_bps(module)
if recursive:
base = Path(module.__file__).parent
for path in glob(f"{base}/**/*.py", recursive=True):
if path not in _imported:
name = "module"
if "__init__" in path:
*_, name, __ = path.split("/")
spec = util.spec_from_file_location(name, path)
specmod = util.module_from_spec(spec)
_imported.add(path)
spec.loader.exec_module(specmod)
_find_bps(specmod)
for bp in blueprints:
app.blueprint(bp)
blueprints/level1.py
#
from sanic import Blueprint
from sanic.log import logger
level1 = Blueprint("level1")
@level1.after_server_start
def print_something(app, loop):
logger.debug("something @ level1")
blueprints/one/two/level3.py
#
from sanic import Blueprint
from sanic.log import logger
level3 = Blueprint("level3")
@level3.after_server_start
def print_something(app, loop):
logger.debug("something @ level3")
listeners/something.py
#
from sanic import Sanic
from sanic.log import logger
app = Sanic.get_app("auto")
@app.after_server_start
def print_something(app, loop):
logger.debug("something")
parent/child/__init__.py
#
from sanic import Blueprint
from sanic.log import logger
bp = Blueprint("__init__")
@bp.after_server_start
def print_something(app, loop):
logger.debug("something inside __init__.py")
parent/child/nested.py
#
from sanic import Blueprint
from sanic.log import logger
nested = Blueprint("nested")
@nested.after_server_start
def print_something(app, loop):
logger.debug("something @ nested")
here is the dir tree
generate with 'find . -type d -name "__pycache__" -exec rm -rf {} +; tree'
. # run 'sanic sever -d' here
├── blueprints
│ ├── __init__.py # you need add this file, just empty
│ ├── level1.py
│ └── one
│ └── two
│ └── level3.py
├── listeners
│ └── something.py
├── parent
│ └── child
│ ├── __init__.py
│ └── nested.py
├── server.py
└── utility.py
source ./.venv/bin/activate # activate the python venv which sanic is installed in
sanic sever -d # run this in the directory containing server.py
you will see "something ***" like this:
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something inside __init__.py
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something @ level3
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something @ level1
[2023-07-12 11:23:36 +0000] [113704] [DEBUG] something @ nested