diff --git a/backends/mizan-django/src/mizan/client/__init__.py b/backends/mizan-django/src/mizan/client/__init__.py index 5c80b93..65083b9 100644 --- a/backends/mizan-django/src/mizan/client/__init__.py +++ b/backends/mizan-django/src/mizan/client/__init__.py @@ -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 diff --git a/backends/mizan-django/src/mizan/forms/__init__.py b/backends/mizan-django/src/mizan/forms/__init__.py index 155c7b0..30d6017 100644 --- a/backends/mizan-django/src/mizan/forms/__init__.py +++ b/backends/mizan-django/src/mizan/forms/__init__.py @@ -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( diff --git a/backends/mizan-django/src/mizan/setup/discovery.py b/backends/mizan-django/src/mizan/setup/discovery.py index 6283624..cd7043f 100644 --- a/backends/mizan-django/src/mizan/setup/discovery.py +++ b/backends/mizan-django/src/mizan/setup/discovery.py @@ -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: diff --git a/cores/mizan-python/src/mizan_core/client/__init__.py b/cores/mizan-python/src/mizan_core/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backends/mizan-django/src/mizan/client/function.py b/cores/mizan-python/src/mizan_core/client/function.py similarity index 93% rename from backends/mizan-django/src/mizan/client/function.py rename to cores/mizan-python/src/mizan_core/client/function.py index f458ad1..ef84ec6 100644 --- a/backends/mizan-django/src/mizan/client/function.py +++ b/cores/mizan-python/src/mizan_core/client/function.py @@ -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.