Files
mizan/backends/mizan-django/src/mizan/setup/discovery.py
Ryth Azhur dd41f0c25f Extract client/function.py to mizan-core (Tier B)
The @client decorator + ServerFunction base + composition machinery is
mostly framework-agnostic. The only Django couplings were typing
(HttpRequest in __init__ and submit_handler signatures) and runtime
view-path detection (HttpResponseBase isinstance/issubclass checks).

Replaced both with backend-extension hooks:

- HttpRequest type hints → Any. Type Protocol can be tightened later.
- HttpResponseBase view-path detection → set_framework_response_base(cls)
  hook in mizan_core.client.function. Backends register their framework's
  response base at import time. is_framework_response(obj_or_cls) handles
  both instance and subclass checks via the registered base.

mizan-django registers HttpResponseBase via mizan/client/__init__.py
before any @client-decorated code is loaded. FastAPI would similarly
register starlette.responses.Response.

Direct consumers updated:
- mizan/setup/discovery.py: ServerFunction import path
- mizan/forms/__init__.py: ServerFunction + create_form_functions imports

mizan/client/__init__.py keeps its public re-export surface stable so
'from mizan.client import client, ServerFunction, …' continues to work
for downstream Django consumers.

Verified:
- mizan-core: 15/15
- mizan-django: 348 pass, 21 skip, 0 fail
- mizan-ts edge-compat: 34/34

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:39:48 -04:00

94 lines
2.9 KiB
Python

"""
mizan Auto-Discovery
Scans Django apps for server functions following the 'clients' layer convention:
- <app>/clients.py
- <app>/clients/**/*.py
Usage in urls.py:
from mizan.setup.discovery import mizan_clients
mizan_clients('apps') # Scans apps/*/clients.py
mizan_clients('mizan', 'allauth') # Scans mizan/allauth/**/*.py
This replaces manual "import to register" patterns with explicit auto-discovery.
"""
from typing import Any
from mizan._vendor.app_visitor import DjangoAppVisitor, get_members
from mizan_core.registry import register, get_function
from mizan_core.client.function import ServerFunction
class _RegisterServerFunctions:
"""Visitor handler that registers ServerFunction subclasses."""
def on_module(
self, app_name: str, path_parts: list[str], members: list[tuple[str, Any]]
) -> None:
"""Process discovered module members."""
for name, member in members:
# Register ServerFunction subclasses
if (
isinstance(member, type)
and issubclass(member, ServerFunction)
and member is not ServerFunction
and hasattr(member, "__name__")
):
# Use the function name as registration name
fn_name = getattr(member, "name", None) or member.__name__
# Skip already registered (idempotent)
if get_function(fn_name) is member:
continue
try:
register(member, fn_name)
except ValueError:
# Already registered with different class - skip
pass
def mizan_clients(apps_root: str, layer: str = "clients") -> None:
"""
Discover and register server functions from Django apps.
Scans for the specified layer (default: 'clients') in each app:
- <app>/<layer>.py
- <app>/<layer>/**/*.py
Args:
apps_root: Root package containing Django apps (e.g., 'apps')
layer: Module name pattern to scan (default: 'clients')
Example:
# In urls.py
mizan_clients('apps') # Scans apps/*/clients.py
mizan_clients('apps', 'functions') # Scans apps/*/functions.py
"""
visitor = DjangoAppVisitor(layer=layer, apps_root=apps_root)
visitor.visit(_RegisterServerFunctions())
from .registry import validate_registry
validate_registry()
def mizan_module(module_path: str) -> None:
"""
Register server functions from a specific module.
Use this for library modules that don't follow the app convention.
Args:
module_path: Full module path (e.g., 'mizan.integrations.allauth')
Example:
mizan_module('mizan.integrations.allauth')
mizan_module('mizan.jwt.functions')
"""
members = get_members(module_path)
handler = _RegisterServerFunctions()
handler.on_module("", [], members)