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:
@@ -2,16 +2,24 @@
|
||||
mizan.client - Server function implementation.
|
||||
|
||||
This subpackage contains everything needed to make server functions work:
|
||||
- The @client decorator
|
||||
- ServerFunction base class
|
||||
- Function execution logic
|
||||
- JWT authentication (integral to server functions)
|
||||
- The @client decorator (lives in mizan_core.client.function)
|
||||
- ServerFunction base class (mizan_core.client.function)
|
||||
- Function execution logic (.executor — Django-specific dispatch)
|
||||
- JWT authentication (.jwt — Django-specific session integration)
|
||||
|
||||
Usage:
|
||||
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
|
||||
client,
|
||||
# Context markers
|
||||
|
||||
@@ -293,7 +293,7 @@ def _register_form_as_server_functions(form_class: type) -> None:
|
||||
from .schema_utils import build_form_schema
|
||||
from .validation_utils import validate_form_instance
|
||||
from mizan_core.registry import register
|
||||
from mizan.client.function import ServerFunction
|
||||
from mizan_core.client.function import ServerFunction
|
||||
|
||||
config: mizanFormMeta = form_class.mizan
|
||||
form_name = config.name
|
||||
@@ -485,7 +485,7 @@ def _register_formset_functions(
|
||||
from .validation_utils import build_formset_validation
|
||||
from .formset_utils import forms_to_formset_post_data
|
||||
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)
|
||||
|
||||
@@ -646,7 +646,7 @@ def register_form(
|
||||
Creates and registers `{name}.schema`, `{name}.validate`, and
|
||||
`{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
|
||||
|
||||
schema_fn, validate_fn, submit_fn = create_form_functions(
|
||||
|
||||
@@ -19,7 +19,7 @@ from typing import Any
|
||||
from mizan._vendor.app_visitor import DjangoAppVisitor, get_members
|
||||
|
||||
from mizan_core.registry import register, get_function
|
||||
from mizan.client.function import ServerFunction
|
||||
from mizan_core.client.function import ServerFunction
|
||||
|
||||
|
||||
class _RegisterServerFunctions:
|
||||
|
||||
@@ -35,10 +35,37 @@ from typing import (
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from django.http import HttpRequest
|
||||
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
|
||||
# =============================================================================
|
||||
@@ -116,8 +143,8 @@ class ServerFunction(ABC, Generic[TInput, TOutput]):
|
||||
Input: ClassVar[type[BaseModel]] = BaseModel
|
||||
Output: ClassVar[type[BaseModel]] = BaseModel
|
||||
|
||||
def __init__(self, request: HttpRequest):
|
||||
"""Initialize with the Django request."""
|
||||
def __init__(self, request: Any):
|
||||
"""Initialize with the framework's request object (HttpRequest in Django, Request in FastAPI, etc.)."""
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
@@ -187,9 +214,8 @@ class _FunctionWrapper(ServerFunction):
|
||||
else:
|
||||
result = self._wrapped_fn(self.request)
|
||||
|
||||
# View path — return HttpResponse directly (no serialization)
|
||||
from django.http import HttpResponseBase
|
||||
if isinstance(result, HttpResponseBase):
|
||||
# View path — return a framework-native response directly (no serialization)
|
||||
if is_framework_response(result):
|
||||
return result
|
||||
|
||||
# Wrap primitive returns in the generated output model
|
||||
@@ -431,12 +457,10 @@ def _create_server_function(
|
||||
if output_type is None:
|
||||
raise TypeError(f"Server function '{name}' must have a return type annotation")
|
||||
|
||||
# Detect view path: function returns HttpResponse (or has no return annotation
|
||||
# that maps to a model — view functions often just have -> HttpResponse)
|
||||
from django.http import HttpResponseBase
|
||||
is_view_path = (
|
||||
isinstance(output_type, type) and issubclass(output_type, HttpResponseBase)
|
||||
)
|
||||
# Detect view path: function returns a framework-native response type
|
||||
# (e.g. Django HttpResponse, FastAPI Response). View functions often just
|
||||
# have -> HttpResponse with no Pydantic model.
|
||||
is_view_path = is_framework_response(output_type)
|
||||
|
||||
if is_view_path:
|
||||
# View path — no Pydantic output wrapping needed
|
||||
@@ -776,7 +800,7 @@ class FormSchemaOutput(BaseModel):
|
||||
def create_form_functions(
|
||||
form_class: type,
|
||||
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]:
|
||||
"""
|
||||
Generate server functions for a Django Form.
|
||||
Reference in New Issue
Block a user