Status: canonical. Single source of truth for the four-function module shape, the registry, and the
module.run()lifecycle. The previous per-function reference (handbook/specs/modules.md, 1678 lines) is superseded; the design sketch inmodule_registration.mdis historical context only.
bbsengine6.module is the runtime plugin loader, validator,
access controller, and execution framework for bbsengine6. Every
BBS package (the engine itself, casino, empyre, murdermotel, bed,
…) is a module from this package's perspective: a Python object
exposing init, access, buildargs, and main.
Every bbsengine6 module exposes exactly four callables. They are
called in order by module.run():
def init(args, **kwargs) -> bool:
"""Initialize the module. Called once when the module is loaded."""
def access(args, op: str, **kwargs) -> bool:
"""Check whether the current session is permitted to perform ``op``.
``op`` is a domain verb (the operation the caller wants to perform),
not a wire-protocol message type. Examples: ``"run"``, ``"edit"``,
``"init"``, ``"buildargs"``, ``"subscribe"``, ``"list_pending"``.
"""
def buildargs(args, **kwargs) -> argparse.ArgumentParser | None:
"""Build and validate arguments for ``main()``.
Returns a configured ``ArgumentParser``; ``None`` if the module has
no CLI flags. The first line of the module's docstring becomes the
parser description.
"""
def main(args, **kwargs) -> Any:
"""Execute the module."""
All four must accept **kwargs — the module loader threads
context (pool, conn, kwargs from the caller) through every call.
Modules are discovered via importlib.import_module() against a
fully-qualified Python module name (e.g. "bbsengine6.console.member"
or "bbsengine6.console.checkroles"). There is no
bbsengine6/modules/ directory and no plugin manifest — packages
are added to PYTHONPATH and discovered by Python's standard
import system.
The registry (register_module / unregister_module /
is_module_registered / get_module / get_module_api) layers
a name-keyed API index on top of importlib so callers can
discover which API a module exports (its access policy, its
init callable, etc.) without doing another importlib.import_module
hop. The registry is opt-in via set_require_registration(True);
without it, module.check accepts any importable module.
load(args, modulepath, package=None) -> ModuleType
Loads a module via importlib.import_module(). When args.debug
is True, the module is importlib.reload-ed on every call so
edits to the source are picked up without restarting the process.
package= resolves bare names and relative dotted names:
modulepath |
package |
Resolves to |
|---|---|---|
"checkfunctions" |
None |
importlib.import_module("checkfunctions") |
"checkfunctions" |
"bbsengine6.backend" |
bbsengine6.backend.checkfunctions |
"checkfunctions" |
".backend" |
bbsengine6.backend.checkfunctions (caller's package is bbsengine6) |
".stage_one" |
".backend" |
bbsengine6.backend.stage_one (PEP 328, caller is bbsengine6.backend) |
"bbsengine6.console.member" |
None |
bbsengine6.console.member (absolute; package ignored) |
Relative package= values are resolved against the calling frame's
__package__ via _caller_package() so cross-package calls work
without the caller hard-coding the anchor.
is_importable(modulepath) -> bool — returns True iff
import_module(modulepath) succeeds, without leaving the module
on sys.modules.
get(module_input, args=None, package=None) -> ModuleType —
accepts either a dotted name (string) or an already-imported
module object; returns the module object.
files(module_ref) -> pathlib.Path — returns the module's
on-disk directory (like importlib.files). folder(module_ref, name) -> pathlib.Path | None returns the named subdirectory.
file(module_ref, subdir, name) -> pathlib.Path | None returns
a specific file inside a subdirectory.
check(args, modulename, op="run", *, package=None, **kwargs) -> bool
check is the entry point used by module.run. It runs the
following gates, in order:
Registration check. If get_require_registration() is True,
the module must already be in the registry. If not, check
returns False.
Import. get(actual_modpath, args, package=package) —
resolves the registered module_path (or the input modulename
if unregistered) and imports it.
Required callables. init, access, buildargs, main
must all exist and be callable. Each is checked via
_check_func_signature(func, _stub_X) against the matching
stub from OP_TO_STUB:
def _stub_access(args, op, **kwargs) -> bool | None: pass
def _stub_init(args, **kwargs) -> bool | None: pass
def _stub_buildargs(args, **kwargs) -> argparse.ArgumentParser | None: pass
def _stub_main(args, **kwargs) -> bool | None: pass
def _stub_version(args, **kwargs) -> str | None: return None
version is optional; if present it must match _stub_version.
Access. m.access(args, op, **kwargs) is invoked. A
non-True return value fails the check.
_check_func_signature(func, stub, *, name=None, allow_extra=True, enforce_return=True) accepts the module's callable and the stub,
then enforces:
_stub_X) require positional-only real
functions;args, op) must be present in the
real function with no default;allow_extra=False);Optional[T] | Union[T, None]
treated as compatible with T)._check_params(func_name, params, required, optional_kwargs=False)
is the simpler parameter-name check used by older check() paths.
check_func(mod_ref, func_name, required_signature, *, allow_extra=True, enforce_return=True, silent=False) -> bool is the standalone
validator exported for callers that want to check a specific
function on a specific module. validate_function(*args, **kwargs)
is a backward-compatible alias.
bbsengine6.module keeps a process-global registry of ModuleAPI
records keyed by module name. ModuleAPI is a frozen dataclass:
@dataclass(frozen=True)
class ModuleAPI:
version: str
apis: dict[str, Callable]
module_path: str
| Function | Signature | Notes |
|---|---|---|
register_module(name, module_path, version, apis) |
Thread-safe UPSERT | Use in init() to expose a module's API |
unregister_module(name) |
Thread-safe DELETE | |
is_module_registered(name) |
Thread-safe contains | |
get_module(name) -> ModuleAPI | None |
Thread-safe read | |
get_module_api(name, api_name) -> Callable | None |
Thread-safe dict lookup | The hot-path accessor for bbsengine6.get_module_api |
set_require_registration(bool) |
Thread-safe flag | When True, module.check requires registration |
get_require_registration() -> bool |
Thread-safe flag read | |
get_all_modules() -> list[str] |
Snapshot of registered names |
The registry is created by _create_registry() which returns a
tuple of eight operations bound to a fresh RegistryState and
threading.RLock. The module-default registry is the first
instance created at import time.
The registry is what makes bbsengine6.message.init callable from
the engine startup path:
from bbsengine6 import register_module
register_module(
name="bbsengine6.message",
module_path="bbsengine6.message",
version=__version__,
apis={"access": access},
)
module.run(args, modulename, **kwargs) -> Any
run is the canonical lifecycle orchestrator. The order is:
run(args, "modulename", **kwargs)
├─ actual_modpath = get_module(modulename).module_path or modulename
├─ get(actual_modpath, args, package=kwargs.pop("package", None))
├─ check(args, modulename, package=package, **kwargs)
├─ runcallback(args, m.init, **kwargs) # module.init()
├─ argv = kwargs.get("argv", [])
├─ if _is_help_request(argv):
│ runcallback(args, m.buildargs, **kwargs)
│ print_help() and return True
├─ prgargparser = runcallback(args, m.buildargs, **kwargs)
├─ if prgargparser is not None:
│ if argv empty and _has_subparser_info(args): use args directly
│ elif argv and _has_subparser_info(args): warn + prgargparser.parse_args(argv)
│ else: prgargparser.parse_args(argv)
│ return runcallback(prgargs, m.main, **kwargs)
└─ return runcallback(args, m.main, **kwargs)
runmodule is an alias for run (kept for backward compat with
older call sites).
runcallback(args, callback, optional=False, **kwargs):
callback is None and optional=False, returns None.callback is callable, invokes callback(args, **kwargs).callback is a string "mod.func", loads the module and
invokes m.func(args, **kwargs).callback is a bare string "funcname", looks up funcname
in the caller's globals (no eval()).io.echo_traceback(),
returning None.| Function | Signature | Purpose |
|---|---|---|
check |
(args, modulename, op="run", *, package=None, **kwargs) |
Verify a module is valid and accessible |
is_importable |
(modulepath) -> bool |
Import test that does not pollute sys.modules |
load |
(args, modulepath, *, package=None) -> ModuleType |
Import (or reload in debug mode) |
get |
(module_input, args=None, *, package=None) -> ModuleType |
Resolve a string-or-module reference |
files |
(module_ref) -> pathlib.Path |
Module directory |
folder |
(module_ref, name) -> pathlib.Path | None |
Subdirectory |
file |
(module_ref, subdir, name) -> pathlib.Path | None |
Subdirectory + filename |
get_op |
(module_ref, op, args=None) -> Callable | None |
Return the op function from the module if it matches its stub |
runcallback |
(args, callback, optional=False, **kwargs) |
Invoke a callable or "module.func" string |
run |
(args, modulename, **kwargs) |
Full lifecycle |
runmodule |
alias for run |
|
register_module / unregister_module / is_module_registered / get_module / get_module_api / get_all_modules / set_require_registration / get_require_registration |
registry surface | See The registry |
check_func / validate_function |
signature validators | See Signature validation |
SignatureError is a dataclass carrying func_name, expected,
found, and optional reason fields. It is constructed by
_check_func_signature and surfaced through io.echo(..., level="debug").
bbsengine6.console.lib.runmodule(args, submodule, *, package="bbsengine6.console", **kwargs)
is a one-liner: module.runmodule(args, f"{package}.{submodule}", **kwargs).
The console __main__.py builds a subcommand parser from
CONSOLE_SUBCOMMANDS and BACKEND_SUBCOMMANDS, then routes the
user's choice to handle_subcommand(args, subcommand). Backend
subcommands dispatch with package="bbsengine6.backend"; console
subcommands stay with the default package="bbsengine6.console".
This is the same module.run lifecycle every BBS package uses; the
console is just a thin argparse wrapper around it.
bbsengine6.startup.lib.runmodule(args, submodule, **kwargs)
delegates to bbsengine6.console.lib.runmodule(args, submodule, package="bbsengine6.startup", **kwargs). The startup main loop
runs ("stage_zero", "stage_one", "engine", "bank") via this
helper, threading conn=conn through so every stage shares the
same transaction. See bbsengine6/TODO_BACKEND.md for the
canonical bring-up sequence.
ModuleNotFoundError from import_module: caught in check
via _check_func_signature's traceback path; surfaces via
io.echo_traceback() and returns None/False.level="debug" with
expected / found signature side by side.access() returning non-True: check returns False.argparse.ArgumentError / SystemExit from parse_args:
caught in run; SystemExit returns e.code == 0 if set,
argparse.ArgumentError returns False.init / main: caught by
runcallback and surfaced via io.echo_traceback().