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>
This commit is contained in:
2026-05-06 15:39:48 -04:00
parent 76fce2dc85
commit dd41f0c25f
5 changed files with 54 additions and 22 deletions

View File

@@ -2,16 +2,24 @@
mizan.client - Server function implementation. mizan.client - Server function implementation.
This subpackage contains everything needed to make server functions work: This subpackage contains everything needed to make server functions work:
- The @client decorator - The @client decorator (lives in mizan_core.client.function)
- ServerFunction base class - ServerFunction base class (mizan_core.client.function)
- Function execution logic - Function execution logic (.executor — Django-specific dispatch)
- JWT authentication (integral to server functions) - JWT authentication (.jwt — Django-specific session integration)
Usage: Usage:
from mizan.client import client, ServerFunction, compose from mizan.client import client, ServerFunction, compose
""" """
from .function import ( # Register the Django framework response base so view-path detection works
# in mizan_core.client.function. Has to happen before any @client-decorated
# code is evaluated.
from django.http import HttpResponseBase as _HttpResponseBase
from mizan_core.client.function import set_framework_response_base as _set_response_base
_set_response_base(_HttpResponseBase)
from mizan_core.client.function import (
# Decorator # Decorator
client, client,
# Context markers # Context markers

View File

@@ -293,7 +293,7 @@ def _register_form_as_server_functions(form_class: type) -> None:
from .schema_utils import build_form_schema from .schema_utils import build_form_schema
from .validation_utils import validate_form_instance from .validation_utils import validate_form_instance
from mizan_core.registry import register from mizan_core.registry import register
from mizan.client.function import ServerFunction from mizan_core.client.function import ServerFunction
config: mizanFormMeta = form_class.mizan config: mizanFormMeta = form_class.mizan
form_name = config.name form_name = config.name
@@ -485,7 +485,7 @@ def _register_formset_functions(
from .validation_utils import build_formset_validation from .validation_utils import build_formset_validation
from .formset_utils import forms_to_formset_post_data from .formset_utils import forms_to_formset_post_data
from mizan_core.registry import register from mizan_core.registry import register
from mizan.client.function import ServerFunction from mizan_core.client.function import ServerFunction
formset_class = formset_factory(form_class) formset_class = formset_factory(form_class)
@@ -646,7 +646,7 @@ def register_form(
Creates and registers `{name}.schema`, `{name}.validate`, and Creates and registers `{name}.schema`, `{name}.validate`, and
`{name}.submit` (if a submit_handler is provided). `{name}.submit` (if a submit_handler is provided).
""" """
from mizan.client.function import create_form_functions from mizan_core.client.function import create_form_functions
from mizan_core.registry import register from mizan_core.registry import register
schema_fn, validate_fn, submit_fn = create_form_functions( schema_fn, validate_fn, submit_fn = create_form_functions(

View File

@@ -19,7 +19,7 @@ from typing import Any
from mizan._vendor.app_visitor import DjangoAppVisitor, get_members from mizan._vendor.app_visitor import DjangoAppVisitor, get_members
from mizan_core.registry import register, get_function from mizan_core.registry import register, get_function
from mizan.client.function import ServerFunction from mizan_core.client.function import ServerFunction
class _RegisterServerFunctions: class _RegisterServerFunctions:

View File

@@ -35,10 +35,37 @@ from typing import (
get_type_hints, get_type_hints,
) )
from django.http import HttpRequest
from pydantic import BaseModel from pydantic import BaseModel
# ─── Framework-response-base hook ───────────────────────────────────────────
#
# View-path detection — distinguishing functions that return data (RPC path)
# from functions that return a framework-native response object (view path) —
# requires knowing the framework's response base class. Each backend adapter
# registers its base class here at import time.
#
# Django sets this to django.http.HttpResponseBase. FastAPI would set it to
# starlette.responses.Response. If unset, all functions are treated as RPC.
_framework_response_base: type | None = None
def set_framework_response_base(cls: type) -> None:
"""Backends register their framework's response base class for view-path detection."""
global _framework_response_base
_framework_response_base = cls
def is_framework_response(obj_or_cls: Any) -> bool:
"""True if obj_or_cls is, or is a subclass of, the registered framework response base."""
if _framework_response_base is None:
return False
if isinstance(obj_or_cls, type):
return issubclass(obj_or_cls, _framework_response_base)
return isinstance(obj_or_cls, _framework_response_base)
# ============================================================================= # =============================================================================
# REACT CONTEXT - Named context marker # REACT CONTEXT - Named context marker
# ============================================================================= # =============================================================================
@@ -116,8 +143,8 @@ class ServerFunction(ABC, Generic[TInput, TOutput]):
Input: ClassVar[type[BaseModel]] = BaseModel Input: ClassVar[type[BaseModel]] = BaseModel
Output: ClassVar[type[BaseModel]] = BaseModel Output: ClassVar[type[BaseModel]] = BaseModel
def __init__(self, request: HttpRequest): def __init__(self, request: Any):
"""Initialize with the Django request.""" """Initialize with the framework's request object (HttpRequest in Django, Request in FastAPI, etc.)."""
self.request = request self.request = request
@property @property
@@ -187,9 +214,8 @@ class _FunctionWrapper(ServerFunction):
else: else:
result = self._wrapped_fn(self.request) result = self._wrapped_fn(self.request)
# View path — return HttpResponse directly (no serialization) # View path — return a framework-native response directly (no serialization)
from django.http import HttpResponseBase if is_framework_response(result):
if isinstance(result, HttpResponseBase):
return result return result
# Wrap primitive returns in the generated output model # Wrap primitive returns in the generated output model
@@ -431,12 +457,10 @@ def _create_server_function(
if output_type is None: if output_type is None:
raise TypeError(f"Server function '{name}' must have a return type annotation") raise TypeError(f"Server function '{name}' must have a return type annotation")
# Detect view path: function returns HttpResponse (or has no return annotation # Detect view path: function returns a framework-native response type
# that maps to a model — view functions often just have -> HttpResponse) # (e.g. Django HttpResponse, FastAPI Response). View functions often just
from django.http import HttpResponseBase # have -> HttpResponse with no Pydantic model.
is_view_path = ( is_view_path = is_framework_response(output_type)
isinstance(output_type, type) and issubclass(output_type, HttpResponseBase)
)
if is_view_path: if is_view_path:
# View path — no Pydantic output wrapping needed # View path — no Pydantic output wrapping needed
@@ -776,7 +800,7 @@ class FormSchemaOutput(BaseModel):
def create_form_functions( def create_form_functions(
form_class: type, form_class: type,
name: str, name: str,
submit_handler: Callable[[HttpRequest, dict], BaseModel] | None = None, submit_handler: Callable[[Any, dict], BaseModel] | None = None,
) -> tuple[type[ServerFunction], type[ServerFunction], type[ServerFunction] | None]: ) -> tuple[type[ServerFunction], type[ServerFunction], type[ServerFunction] | None]:
""" """
Generate server functions for a Django Form. Generate server functions for a Django Form.