From 2dff7b4469c6d8836369a49297f2535081cd8b3c Mon Sep 17 00:00:00 2001 From: Alexander Egorov Date: Mon, 26 Oct 2026 20:46:38 +0100 Subject: [PATCH 1/2] feat(web)!: remove output processors, encode Page as JSON by its type An API method returns data, encoded as JSON, or a winter.web.Response. The output processors and their registry are gone: register_output_processor_resolver, IOutputProcessor, IOutputProcessorResolver, register_output_processor and winter.django.output_template. A template is rendered by the API method into a Response. Page was the only API processor of Winter, and it needed the request only for the previous and next links of its meta. Those links are dropped, so Page is encoded by a JSON encoder registered for its type, which anyone can write for their own types: winter.json.register_encoder is exported for that. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++ docs/architecture.md | 8 +-- tests/api/api_with_response.py | 19 ------ tests/api/django/__init__.py | 1 - tests/api/django/api_with_output_template.py | 14 ----- tests/conftest.py | 4 -- tests/django/test_api_with_output_template.py | 8 --- .../test_api_request_and_response_spec.py | 12 +--- tests/templates/hello.txt | 1 - tests/web/test_response.py | 8 --- tests/web/test_simple_api.py | 14 ++--- winter/django/__init__.py | 1 - winter/django/output_template.py | 24 -------- winter/json/__init__.py | 1 + winter/openapi/inspectors/page_inspector.py | 2 - winter/web/__init__.py | 1 - winter/web/initialization.py | 5 +- winter/web/output_processor.py | 61 ------------------- winter/web/pagination/__init__.py | 1 + winter/web/pagination/page_encoder.py | 24 ++++++++ winter/web/pagination/page_processor.py | 29 --------- .../web/pagination/page_processor_resolver.py | 17 ------ winter/web/pagination/utils.py | 47 -------------- winter/web/request_handler.py | 7 --- 24 files changed, 44 insertions(+), 271 deletions(-) delete mode 100644 tests/api/django/__init__.py delete mode 100644 tests/api/django/api_with_output_template.py delete mode 100644 tests/django/test_api_with_output_template.py delete mode 100644 tests/templates/hello.txt delete mode 100644 winter/django/output_template.py delete mode 100644 winter/web/output_processor.py create mode 100644 winter/web/pagination/page_encoder.py delete mode 100644 winter/web/pagination/page_processor.py delete mode 100644 winter/web/pagination/page_processor_resolver.py delete mode 100644 winter/web/pagination/utils.py diff --git a/README.md b/README.md index be3f76f5..c364d55a 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,12 @@ urlpatterns = winter.django.create_urls(app) - Interceptors belong to applications: `WebApplication(..., interceptors=[AuthorizationInterceptor, AuditInterceptor])` lists their classes in the order they run. The injector of each request creates them, so their dependencies go to `__init__`. `interceptor_registry` and `InterceptorRegistry` are gone. +- Output processors are gone: `register_output_processor_resolver`, `IOutputProcessor`, `IOutputProcessorResolver`, + `register_output_processor` and `winter.django.output_template`. An API method returns data, encoded as JSON + (`winter.json.register_encoder` adds the encoder of a type), or a `winter.web.Response`, e.g. a rendered template: + `Response(HTTPStatus.OK, render_to_string('report.html', context).encode(), {'content-type': 'text/html'})`. +- The `meta` of a `Page` has no `previous` and `next` links any more, and `winter.web.pagination.utils` with + `get_previous_page_url` and `get_next_page_url` is gone. - The request pipeline takes the injector from the application. `winter.application.set_injector` is needed only by `winter.sqlalchemy`, `winter.ddd` and `winter.messaging`. - `winter.django.create_urls` installs the Django throttling identity on its first call: replace it with diff --git a/docs/architecture.md b/docs/architecture.md index 5313599a..e583a452 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -68,7 +68,7 @@ at least two of them. subpackage can later ship as its own distribution from its own repository without any import path changing. 2. **Framework behaviour is never process-global.** Every behavioural registry (exception - handlers, interceptors, output processors, throttling configuration, OpenAPI inspectors) + handlers, interceptors, response header serializers, throttling configuration, OpenAPI inspectors) belongs to the application object an adapter builds from, such as `winter.web.WebApplication`, and the values that its methods receive come from its injector, so two applications can live in one process. @@ -85,13 +85,13 @@ at least two of them. - **Framework behaviour is still process-global.** A `winter.web.WebApplication` holds the code of a web application, its injector and its interceptors. The adapters derive its exception handlers from its definitions and handle each of its requests in a child of its injector. The other - behavioural registries of `winter.web` and `winter.openapi` (output processors, response header - serializers, native response types, throttling, OpenAPI inspectors) are module singletons, and + behavioural registries of `winter.web` and `winter.openapi` (response header serializers, + native response types, throttling, OpenAPI inspectors) are module singletons, and `winter.sqlalchemy`, `winter.ddd` and `winter.messaging` take the injector from the service locator `winter.application.get_injector`. - **Models are initialised once per process.** `winter.web.initialization.initialize`, the Django adapter's `_initialize_process` and `winter.openapi.initialization.initialize` fill those module - singletons with the standard output processors, serializers and inspectors on first use. In the + singletons with the standard serializers and inspectors on first use. In the target, a `WebApplication` is given these registries like its interceptors, and they start with the standard entries. Building from an application, an adapter adds its own entries (the native response type, the throttling identity) to that application only. The diff --git a/tests/api/api_with_response.py b/tests/api/api_with_response.py index a3dcaf03..0db93702 100644 --- a/tests/api/api_with_response.py +++ b/tests/api/api_with_response.py @@ -5,20 +5,6 @@ import winter.web from winter.web import Request from winter.web import Response -from winter.web.output_processor import IOutputProcessor -from winter.web.output_processor import register_output_processor - - -class PlainTextProcessor(IOutputProcessor): - """Renders the method result as text/plain instead of JSON.""" - - def process_output(self, output: str, request: Request) -> Response: - return Response(status_code=HTTPStatus.OK, body=output.encode(), headers={'content-type': 'text/plain'}) - - -def plain_text(func): - """Same shape as winter.django.output_template, but with an adapter-independent processor.""" - return register_output_processor(func, PlainTextProcessor()) @winter.web.route('with-response/') @@ -28,11 +14,6 @@ class APIWithResponse: def raw(self) -> Response: return Response(status_code=HTTPStatus.ACCEPTED, body=b'raw bytes', headers={'content-type': 'text/plain'}) - @winter.web.route_get('greeting/') - @plain_text - def greeting(self) -> str: - return 'Hello, Winter' - @winter.web.route_get('request-info/') def request_info(self, request: Request, django_request: HttpRequest) -> dict: return { diff --git a/tests/api/django/__init__.py b/tests/api/django/__init__.py deleted file mode 100644 index fb4dec65..00000000 --- a/tests/api/django/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .api_with_output_template import APIWithOutputTemplate diff --git a/tests/api/django/api_with_output_template.py b/tests/api/django/api_with_output_template.py deleted file mode 100644 index a8dd39af..00000000 --- a/tests/api/django/api_with_output_template.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Any -from typing import Dict - -import winter.web -import winter.django - - -@winter.web.route('with-output-template/') -class APIWithOutputTemplate: - - @winter.web.route_get('{?name}') - @winter.django.output_template('hello.txt') - def greet(self, name: str) -> Dict[str, Any]: - return {'name': name} diff --git a/tests/conftest.py b/tests/conftest.py index 5458a845..df626d11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,3 @@ -from pathlib import Path - import django import pytest from injector import CallableProvider @@ -14,13 +12,11 @@ def pytest_configure(): from django.conf import settings - app_dir = Path(__file__) settings.configure( ROOT_URLCONF='tests.urls', TEMPLATES=[ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [app_dir.parent.name + '/templates'], 'APP_DIRS': True, 'OPTIONS': { 'debug': True, # We want template errors to raise diff --git a/tests/django/test_api_with_output_template.py b/tests/django/test_api_with_output_template.py deleted file mode 100644 index 74e9509c..00000000 --- a/tests/django/test_api_with_output_template.py +++ /dev/null @@ -1,8 +0,0 @@ -from http import HTTPStatus - - -def test_api_with_output_template(api_client): - response = api_client.get('/with-output-template/?name=John') - - assert response.status_code == HTTPStatus.OK - assert response.content == b'Hello, John!' diff --git a/tests/openapi/test_api_request_and_response_spec.py b/tests/openapi/test_api_request_and_response_spec.py index 07dff3df..431a8142 100644 --- a/tests/openapi/test_api_request_and_response_spec.py +++ b/tests/openapi/test_api_request_and_response_spec.py @@ -349,11 +349,9 @@ class RequestBodyWithUndefined: 'properties': { 'limit': {'format': 'int32', 'nullable': True, 'type': 'integer'}, 'offset': {'format': 'int32', 'nullable': True, 'type': 'integer'}, - 'next': {'nullable': True, 'type': 'string'}, - 'previous': {'nullable': True, 'type': 'string'}, 'total_count': {'format': 'int32', 'type': 'integer'}, }, - 'required': ['total_count', 'limit', 'offset', 'previous', 'next'], + 'required': ['total_count', 'limit', 'offset'], 'title': 'PageMetaOfNestedDataclass', 'type': 'object', }, @@ -399,10 +397,8 @@ class RequestBodyWithUndefined: 'extra': {'type': 'string'}, 'limit': {'format': 'int32', 'nullable': True, 'type': 'integer'}, 'offset': {'format': 'int32', 'nullable': True, 'type': 'integer'}, - 'next': {'nullable': True, 'type': 'string'}, - 'previous': {'nullable': True, 'type': 'string'}, 'total_count': {'format': 'int32', 'type': 'integer'}}, - 'required': ['total_count', 'limit', 'offset', 'previous', 'next', 'extra'], + 'required': ['total_count', 'limit', 'offset', 'extra'], 'title': 'PageMetaOfInteger', 'type': 'object', }, @@ -1008,12 +1004,10 @@ def method_2(self) -> Page[str]: 'PageMetaOfString': { 'properties': { 'limit': {'format': 'int32', 'nullable': True, 'type': 'integer'}, - 'next': {'nullable': True, 'type': 'string'}, 'offset': {'format': 'int32', 'nullable': True, 'type': 'integer'}, - 'previous': {'nullable': True, 'type': 'string'}, 'total_count': {'format': 'int32', 'type': 'integer'}, }, - 'required': ['total_count', 'limit', 'offset', 'previous', 'next'], + 'required': ['total_count', 'limit', 'offset'], 'title': 'PageMetaOfString', 'type': 'object'}, 'PageOfString': { diff --git a/tests/templates/hello.txt b/tests/templates/hello.txt deleted file mode 100644 index 272c22e1..00000000 --- a/tests/templates/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, {{ name }}! \ No newline at end of file diff --git a/tests/web/test_response.py b/tests/web/test_response.py index 6c550c5d..4c6c5a7f 100644 --- a/tests/web/test_response.py +++ b/tests/web/test_response.py @@ -9,14 +9,6 @@ def test_raw_response_bypasses_json_encoding(api_client): assert response.headers['content-type'] == 'text/plain' -def test_output_processor_may_return_a_response(api_client): - response = api_client.get('/with-response/greeting/') - - assert response.status_code == HTTPStatus.OK - assert response.content == b'Hello, Winter' - assert response.headers['content-type'] == 'text/plain' - - def test_request_describes_the_http_request(api_client): response = api_client.get('/with-response/request-info/?x=1') diff --git a/tests/web/test_simple_api.py b/tests/web/test_simple_api.py index 3af2105d..364692a6 100644 --- a/tests/web/test_simple_api.py +++ b/tests/web/test_simple_api.py @@ -22,13 +22,11 @@ def test_get_response_entity(api_client): } -@pytest.mark.parametrize(('limit', 'offset', 'expected_previous', 'expected_next'), ( - (1, 1, '/winter-simple/page-response/?limit=1', '/winter-simple/page-response/?limit=1&offset=2'), - (None, None, None, None), - (2, 3, '/winter-simple/page-response/?limit=2&offset=1', '/winter-simple/page-response/?limit=2&offset=5'), - (9, 3, '/winter-simple/page-response/?limit=9', None), +@pytest.mark.parametrize(('limit', 'offset'), ( + (1, 1), + (None, None), )) -def test_page_response(api_client, limit, offset, expected_previous, expected_next): +def test_page_response(api_client, limit, offset): url = f'/winter-simple/page-response/?' query_params = [] if limit is not None: @@ -42,8 +40,6 @@ def test_page_response(api_client, limit, offset, expected_previous, expected_ne 'meta': { 'limit': limit, 'offset': offset, - 'next': expected_next, - 'previous': expected_previous, 'total_count': 10, }, } @@ -62,8 +58,6 @@ def test_custom_page_response(api_client): 'meta': { 'limit': 2, 'offset': 2, - 'next': '/winter-simple/custom-page-response/?limit=2&offset=4', - 'previous': '/winter-simple/custom-page-response/?limit=2', 'total_count': 10, 'extra': 456, }, diff --git a/winter/django/__init__.py b/winter/django/__init__.py index ef195470..325a6acd 100644 --- a/winter/django/__init__.py +++ b/winter/django/__init__.py @@ -1,5 +1,4 @@ from . import json_encoders # noqa: F401 registers the Django lazy string encoder -from .output_template import output_template from .request import DjangoRequest from .reverse import reverse from .throttling import django_user_identity diff --git a/winter/django/output_template.py b/winter/django/output_template.py deleted file mode 100644 index 6aa828d3..00000000 --- a/winter/django/output_template.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.shortcuts import render - -from winter.web import Request -from winter.web.output_processor import IOutputProcessor -from winter.web.output_processor import register_output_processor -from .request import DjangoRequest - - -class TemplateRenderer(IOutputProcessor): - - def __init__(self, template_name: str): - self._template_name = template_name - - def process_output(self, output, request: Request): - assert isinstance(request, DjangoRequest), 'output_template works with the Django adapter only' - return render(request.django_request, self._template_name, context=output) - - -def output_template(template_name: str): - def wrapper(func): - output_processor = TemplateRenderer(template_name) - return register_output_processor(func, output_processor) - - return wrapper diff --git a/winter/json/__init__.py b/winter/json/__init__.py index c392cdcf..12bd52fc 100644 --- a/winter/json/__init__.py +++ b/winter/json/__init__.py @@ -2,4 +2,5 @@ from .decoder import json_decode from .decoder import json_decoder from .encoder import JSONEncoder +from .encoder import register_encoder from .undefined import Undefined diff --git a/winter/openapi/inspectors/page_inspector.py b/winter/openapi/inspectors/page_inspector.py index b03313f0..2f092efe 100644 --- a/winter/openapi/inspectors/page_inspector.py +++ b/winter/openapi/inspectors/page_inspector.py @@ -26,8 +26,6 @@ def create_dataclass(page_type: Type) -> Type: 'total_count': int, 'limit': Optional[int], 'offset': Optional[int], - 'previous': Optional[str], - 'next': Optional[str], **{extra_field.name: extra_field.type for extra_field in extra_fields}, }, }, diff --git a/winter/web/__init__.py b/winter/web/__init__.py index 3af32435..2f5ba80d 100644 --- a/winter/web/__init__.py +++ b/winter/web/__init__.py @@ -6,7 +6,6 @@ from .interceptor import Interceptor from .media_type import InvalidMediaTypeException from .media_type import MediaType -from .output_processor import register_output_processor_resolver from .query_parameters import map_query_parameter from .query_parameters import query_parameters from .request_body_annotation import request_body diff --git a/winter/web/initialization.py b/winter/web/initialization.py index 82e4c142..5f1e6cf5 100644 --- a/winter/web/initialization.py +++ b/winter/web/initialization.py @@ -1,5 +1,3 @@ -from .output_processor import register_output_processor_resolver -from .pagination.page_processor_resolver import PageOutputProcessorResolver from .response_header_serializer import response_headers_serializer from .response_header_serializers import DateTimeResponseHeaderSerializer from .response_header_serializers import LastModifiedResponseHeaderSerializer @@ -9,7 +7,7 @@ def initialize(): """ - Registers the standard output processors and response header serializers of winter.web on the first call. + Registers the standard response header serializers of winter.web on the first call. Adapters call it before building their routes. Process-global until these registries belong to an application: docs/architecture.md, Current deviations. """ @@ -20,6 +18,5 @@ def initialize(): def _initialize_process(): - register_output_processor_resolver(PageOutputProcessorResolver()) response_headers_serializer.add_serializer(DateTimeResponseHeaderSerializer()) response_headers_serializer.add_serializer(LastModifiedResponseHeaderSerializer()) diff --git a/winter/web/output_processor.py b/winter/web/output_processor.py deleted file mode 100644 index 610fe27f..00000000 --- a/winter/web/output_processor.py +++ /dev/null @@ -1,61 +0,0 @@ -import abc -import dataclasses -from typing import Any -from typing import Callable -from typing import List -from typing import Optional - - -from winter.web.request import Request -from winter.components import ComponentMethod -from winter.components import annotate - - -class IOutputProcessor(abc.ABC): - """Process API method returned value so that it can be put to HttpResponse body. - Common usage is to serializer some DTO to dict.""" - - @abc.abstractmethod - def process_output(self, output, request: Request): # pragma: no cover - return output - - -@dataclasses.dataclass -class OutputProcessorAnnotation: - output_processor: IOutputProcessor - - -class IOutputProcessorResolver(abc.ABC): - """ - Resolves IOutputProcessor for a given body type. - Due to python dynamic typing it's called after every API request. - """ - - @abc.abstractmethod - def is_supported(self, body: Any) -> bool: # pragma: no cover - return False - - @abc.abstractmethod - def get_processor(self, body: Any) -> IOutputProcessor: # pragma: no cover - pass - - -_registered_resolvers: List[IOutputProcessorResolver] = [] - - -def register_output_processor(method: Callable, output_processor: IOutputProcessor): - return annotate(OutputProcessorAnnotation(output_processor), single=True)(method) - - -def register_output_processor_resolver(output_processor_resolver: IOutputProcessorResolver): - _registered_resolvers.append(output_processor_resolver) - - -def get_output_processor(method: ComponentMethod, body: Any) -> Optional[IOutputProcessor]: - output_processor_annotation = method.annotations.get_one_or_none(OutputProcessorAnnotation) - if output_processor_annotation is not None: - return output_processor_annotation.output_processor - for resolver in _registered_resolvers: - if resolver.is_supported(body): - return resolver.get_processor(body) - return None diff --git a/winter/web/pagination/__init__.py b/winter/web/pagination/__init__.py index ae67e8e4..59d5b4ef 100644 --- a/winter/web/pagination/__init__.py +++ b/winter/web/pagination/__init__.py @@ -1,2 +1,3 @@ +from . import page_encoder # noqa: F401 registers the JSON encoder of Page from .limits import limits from .order_by import order_by diff --git a/winter/web/pagination/page_encoder.py b/winter/web/pagination/page_encoder.py new file mode 100644 index 00000000..d62aa973 --- /dev/null +++ b/winter/web/pagination/page_encoder.py @@ -0,0 +1,24 @@ +import dataclasses + +from winter.json import register_encoder +from winter.pagination import Page + +_PAGE_FIELDS = dataclasses.fields(Page) + + +@register_encoder +def page_encoder(page: Page) -> dict: + """The fields that a subclass of Page adds go to the meta.""" + return { + 'meta': { + 'total_count': page.total_count, + 'limit': page.position.limit, + 'offset': page.position.offset, + **{ + field.name: getattr(page, field.name) + for field in dataclasses.fields(page) + if field not in _PAGE_FIELDS + }, + }, + 'objects': page.items, + } diff --git a/winter/web/pagination/page_processor.py b/winter/web/pagination/page_processor.py deleted file mode 100644 index 57cedcbb..00000000 --- a/winter/web/pagination/page_processor.py +++ /dev/null @@ -1,29 +0,0 @@ -import dataclasses -from typing import Dict - - -from winter.web.request import Request -from winter.pagination import Page -from winter.web.output_processor import IOutputProcessor -from .utils import get_next_page_url -from .utils import get_previous_page_url - - -class PageProcessor(IOutputProcessor): - - def process_output(self, output: Page, request: Request) -> Dict: - extra_fields = set(dataclasses.fields(output)) - set(dataclasses.fields(Page)) - return { - 'meta': { - 'total_count': output.total_count, - 'limit': output.position.limit, - 'offset': output.position.offset, - 'previous': get_previous_page_url(output, request), - 'next': get_next_page_url(output, request), - **{ - extra_field.name: getattr(output, extra_field.name) - for extra_field in extra_fields - }, - }, - 'objects': output.items, - } diff --git a/winter/web/pagination/page_processor_resolver.py b/winter/web/pagination/page_processor_resolver.py deleted file mode 100644 index 1df767de..00000000 --- a/winter/web/pagination/page_processor_resolver.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -from winter.pagination import Page -from winter.web.output_processor import IOutputProcessorResolver -from .page_processor import PageProcessor - - -class PageOutputProcessorResolver(IOutputProcessorResolver): - - def __init__(self): - self._page_processor = PageProcessor() - - def is_supported(self, body: Any) -> bool: - return isinstance(body, Page) - - def get_processor(self, body: Any) -> PageProcessor: - return self._page_processor diff --git a/winter/web/pagination/utils.py b/winter/web/pagination/utils.py deleted file mode 100644 index e9554096..00000000 --- a/winter/web/pagination/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Optional - -from furl import furl - -from winter.web.request import Request -from winter.pagination import Page - - -def get_previous_page_url(page: Page, request: Request) -> Optional[str]: - offset = page.position.offset - limit = page.position.limit - - if not offset or limit is None: - return None - - url = furl(request.full_path) - url.query.set([('limit', limit)]) - - previous_offset = offset - limit - - if previous_offset <= 0: - url.query.remove('offset') - else: - url.query.set([('offset', previous_offset)]) - - return url.tostr() - - -def get_next_page_url(page: Page, request: Request) -> Optional[str]: - offset = page.position.offset or 0 - limit = page.position.limit - total = page.total_count - - if limit is None: - return None - - next_offset = offset + limit - - if next_offset >= total: - return None - - url = furl(request.full_path) - url.query.set([ - ('limit', limit), - ('offset', next_offset), - ]) - return url.tostr() diff --git a/winter/web/request_handler.py b/winter/web/request_handler.py index c06d7ba0..f36a5572 100644 --- a/winter/web/request_handler.py +++ b/winter/web/request_handler.py @@ -13,7 +13,6 @@ from .exceptions import ExceptionHandlers from .exceptions import ThrottleException from .exceptions.handlers import exception_argument_name -from .output_processor import get_output_processor from .pagination.page_position_argument_resolver import PagePositionArgumentResolver from .path_parameters_argument_resolver import PathParametersArgumentResolver from .query_parameters.query_parameter_argument_resolver import QueryParameterArgumentResolver @@ -134,12 +133,6 @@ def _to_response(request: Request, result: Any, method: ComponentMethod, respons body = result status_code = get_response_status(request.method, method) - output_processor = get_output_processor(method, body) - if output_processor is not None: - body = output_processor.process_output(body, request) - if isinstance(body, Response): - return Response(status_code=body.status_code, body=body.body, headers={**headers, **body.headers}) - if _is_native(body): return Response(status_code=status_code, body=body, headers=headers) From fda387bb21d0465b37e9a63734b077079ffab947 Mon Sep 17 00:00:00 2001 From: Alexander Egorov Date: Tue, 27 Oct 2026 11:29:24 +0100 Subject: [PATCH 2/2] docs(web): drop output processors from the docstrings of the request handler Co-Authored-By: Claude Opus 5.5 --- winter/web/request_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winter/web/request_handler.py b/winter/web/request_handler.py index f36a5572..0f38fd8b 100644 --- a/winter/web/request_handler.py +++ b/winter/web/request_handler.py @@ -70,7 +70,7 @@ def handle_request( ) -> Response: """ Run the whole handling pipeline for a matched route of the application: throttling, its interceptors, argument - resolution, the API method, its exception handlers, output processing and JSON encoding. + resolution, the API method, its exception handlers and JSON encoding. Framework-agnostic: adapters build the Request from their own request object and send the returned Response. The request is handled in an injector of its own, a child of the application's one, that binds the Request and the request values of the adapter, such as its own request object. Request-scoped values live in it. @@ -146,8 +146,8 @@ def _to_response(request: Request, result: Any, method: ComponentMethod, respons def register_native_response_type(type_: type): """ - Adapters register the response types of their framework here. An API method or an output processor - may return such an object, and it reaches the adapter untouched. + Adapters register the response types of their framework here. An API method may return such an object, + and it reaches the adapter untouched. """ global _native_response_types _native_response_types = _native_response_types + (type_,)