Test infrastructure: - Django standalone test runner (pytest-django, test settings, EmailUser model) - React unit tests via Vitest with jsdom, jest compat layer, path aliases - Playwright E2E tests using generated hooks in a real Chromium browser - Docker Compose test backend (Django + Redis) for integration testing - Desktop integration test app (PyWebView + Django + uvicorn) - Makefile with test/test-django/test-react/test-integration targets Library bugs found and fixed: - hasJWT truthiness: undefined !== null was true, skipping session init - process.env crash: CSR client referenced process.env in non-Node browsers - baseUrl not forwarded: DjareaProvider didn't pass baseUrl to CSR client - Relative URL handling: new URL() failed with relative base paths - call() race condition: HTTP requests fired before CSRF cookie was set - Session init await: added sessionRef promise so call() waits for session - path_prefix on schema export: both export commands failed with URL reverse - NullBooleanField removed: referenced field doesn't exist in Django 5.0+ - lru_cache on JWT settings: get_settings() now cached as intended - Channel message routing: broadcasts now include channel name and params - httpFunctionCall: fixed URL and request body format Generator fixes: - Removed 1,100 lines of REST/OpenAPI client generation (not part of Djarea) - Generator now works for djarea-only projects without django-ninja REST APIs - Generated DjangoContext now includes ChannelProvider when channels exist - Fixed env var passthrough for schema export commands - Deduplicated fetch logic into single runDjangoCommand helper Test quality: - Fixed 33 tautological Django tests with real assertions - Found hidden bug: benchmark functions were never registered - Found hidden bug: unicode lookalike test used plain ASCII - Deleted worthless React unit tests (duplicates, shape checks, Zod-tests-Zod) - Replaced jsdom integration tests with Playwright browser tests Example apps: - example/: Integration test backend with 33 server functions, 5 forms, 4 channels covering auth variations, contexts, class-based ServerFunction, error codes, DjareaFormMixin, formsets, and JWT - desktop/: PyWebView desktop app with file system access, SQLite CRUD, system introspection, and 39 real HTTP integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""
|
|
Djarea Auto-Discovery
|
|
|
|
Scans Django apps for server functions following the 'clients' layer convention:
|
|
- <app>/clients.py
|
|
- <app>/clients/**/*.py
|
|
|
|
Usage in urls.py:
|
|
from djarea.setup.discovery import djarea_clients
|
|
|
|
djarea_clients('apps') # Scans apps/*/clients.py
|
|
djarea_clients('djarea', 'allauth') # Scans djarea/allauth/**/*.py
|
|
|
|
This replaces manual "import to register" patterns with explicit auto-discovery.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from djarea._vendor.app_visitor import DjangoAppVisitor, get_members
|
|
|
|
from .registry import register, get_function
|
|
from djarea.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 djarea_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
|
|
djarea_clients('apps') # Scans apps/*/clients.py
|
|
djarea_clients('apps', 'functions') # Scans apps/*/functions.py
|
|
"""
|
|
visitor = DjangoAppVisitor(layer=layer, apps_root=apps_root)
|
|
visitor.visit(_RegisterServerFunctions())
|
|
|
|
|
|
def djarea_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., 'djarea.integrations.allauth')
|
|
|
|
Example:
|
|
djarea_module('djarea.integrations.allauth')
|
|
djarea_module('djarea.jwt.functions')
|
|
"""
|
|
members = get_members(module_path)
|
|
handler = _RegisterServerFunctions()
|
|
handler.on_module('', [], members)
|