From 6a3920b7a7f8da01d8b49cae354d68f4c53f1c62 Mon Sep 17 00:00:00 2001 From: Alexander Egorov Date: Tue, 27 Oct 2026 17:16:04 +0100 Subject: [PATCH 1/2] feat!: decorators name what they declare, json_encoder and url_regexp @winter.json.register_encoder is @winter.json.json_encoder, like @winter.json.json_decoder, and @winter.web.register_url_regexp is @winter.web.url_regexp. The error for a second encoder of a type no longer refers to an unregister function that does not exist. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++- winter/django/json_encoders.py | 4 +-- winter/json/__init__.py | 2 +- winter/json/encoder.py | 46 +++++++++++++++------------ winter/web/__init__.py | 2 +- winter/web/pagination/page_encoder.py | 4 +-- winter/web/urls.py | 10 +++--- 7 files changed, 40 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 10fa732e..ca4a4377 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ urlpatterns = winter.django.create_urls(app) 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: + (`@winter.json.json_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. @@ -99,6 +99,8 @@ urlpatterns = winter.django.create_urls(app) `register_route_parameters_inspector`, `get_route_parameters_inspectors` and `winter.openapi.initialization` are gone, and the inspectors and `inspect_page` are no longer exported. A parameter that no argument stands for, such as a header read by a request-scoped provider, is added to the spec that `generate_openapi` returns. +- Decorators name what they declare: `@winter.json.register_encoder` is `@winter.json.json_encoder`, like + `@winter.json.json_decoder`, and `@winter.web.register_url_regexp` is `@winter.web.url_regexp`. ## Upgrading to Winter 33 diff --git a/winter/django/json_encoders.py b/winter/django/json_encoders.py index d98fef68..a4c32507 100644 --- a/winter/django/json_encoders.py +++ b/winter/django/json_encoders.py @@ -1,8 +1,8 @@ from django.utils.functional import Promise -from winter.json.encoder import register_encoder +from winter.json.encoder import json_encoder -@register_encoder +@json_encoder def promise_encoder(promise: Promise): return str(promise) diff --git a/winter/json/__init__.py b/winter/json/__init__.py index 12bd52fc..d2b85804 100644 --- a/winter/json/__init__.py +++ b/winter/json/__init__.py @@ -2,5 +2,5 @@ from .decoder import json_decode from .decoder import json_decoder from .encoder import JSONEncoder -from .encoder import register_encoder +from .encoder import json_encoder from .undefined import Undefined diff --git a/winter/json/encoder.py b/winter/json/encoder.py index 0f980be2..99f650e0 100644 --- a/winter/json/encoder.py +++ b/winter/json/encoder.py @@ -38,11 +38,15 @@ def default(self, obj): return super().default(obj) -def register_encoder(func: Callable = None, *, need_recursion: bool = False): +def json_encoder(func: Callable = None, *, need_recursion: bool = False): + """ + The decorated function encodes the type of its only argument, and its subclasses, as JSON. With need_recursion + the value it returns is encoded in turn. + """ if func is None: - return lambda func_: register_encoder(func_, need_recursion=need_recursion) + return lambda func_: json_encoder(func_, need_recursion=need_recursion) - assert callable(func), 'First argument in register_encoder must be callable' + assert callable(func), 'First argument in json_encoder must be callable' signature = inspect.signature(func) assert len(signature.parameters), 'Function must have only one argument' @@ -51,13 +55,13 @@ def register_encoder(func: Callable = None, *, need_recursion: bool = False): assert annotation is not parameter.empty, 'First argument in function must have annotation' assert inspect.isclass(annotation), 'Annotation must be class' assert annotation not in _encoder_map, ( - f'You can not register "{annotation.__name__}" twice. At first unregister it' + f'"{annotation.__name__}" already has a JSON encoder' ) _encoder_map[annotation] = func, need_recursion return func -@register_encoder +@json_encoder def datetime_encoder(date: datetime.datetime): representation = date.isoformat() if representation.endswith('+00:00'): @@ -65,84 +69,84 @@ def datetime_encoder(date: datetime.datetime): return representation -@register_encoder +@json_encoder def date_encoder(date: datetime.date): return date.isoformat() -@register_encoder +@json_encoder def time_encoder(time: datetime.time): if time.utcoffset() is not None: raise ValueError("JSON can't represent timezone-aware times.") return time.isoformat() -@register_encoder +@json_encoder def timedelta_encoder(timedelta: datetime.timedelta): return str(timedelta.total_seconds()) -@register_encoder +@json_encoder def decimal_encoder(number: decimal.Decimal): return str(number) -@register_encoder +@json_encoder def uuid_encoder(uid: uuid.UUID): return str(uid) -@register_encoder +@json_encoder def bytes_encoder(bytes_: bytes): return bytes_.decode('utf-8') -@register_encoder +@json_encoder def str_encoder(string: str): return string -@register_encoder +@json_encoder def int_encoder(number: int): return number -@register_encoder +@json_encoder def float_encoder(number: float): return number -@register_encoder +@json_encoder def tuple_encoder(array: tuple): return array -@register_encoder +@json_encoder def list_encoder(array: list): return array -@register_encoder +@json_encoder def set_encoder(set_: set): return list(set_) -@register_encoder +@json_encoder def frozenset_encoder(frozenset_: frozenset): return list(frozenset_) -@register_encoder +@json_encoder def generator_encoder(generator: types.GeneratorType): return list(generator) -@register_encoder(need_recursion=True) +@json_encoder(need_recursion=True) def enum_encoder(enum_: enum.Enum): return enum_.value -@register_encoder +@json_encoder def dataclass_encoder(obj: object): if not dataclasses.is_dataclass(obj): raise CannotEncode diff --git a/winter/web/__init__.py b/winter/web/__init__.py index 94fcddd7..5a215e91 100644 --- a/winter/web/__init__.py +++ b/winter/web/__init__.py @@ -29,5 +29,5 @@ from .throttling import ThrottlingMisconfigurationException from .throttling import ThrottlingStorage from .throttling import throttling -from .urls import register_url_regexp +from .urls import url_regexp from .web_application import WebApplication diff --git a/winter/web/pagination/page_encoder.py b/winter/web/pagination/page_encoder.py index d62aa973..81e1fd6d 100644 --- a/winter/web/pagination/page_encoder.py +++ b/winter/web/pagination/page_encoder.py @@ -1,12 +1,12 @@ import dataclasses -from winter.json import register_encoder +from winter.json import json_encoder from winter.pagination import Page _PAGE_FIELDS = dataclasses.fields(Page) -@register_encoder +@json_encoder def page_encoder(page: Page) -> dict: """The fields that a subclass of Page adds go to the meta.""" return { diff --git a/winter/web/urls.py b/winter/web/urls.py index bb5f0d43..ec33591a 100644 --- a/winter/web/urls.py +++ b/winter/web/urls.py @@ -11,7 +11,9 @@ _regexp = {} -def register_url_regexp(func: types.FunctionType): +def url_regexp(func: types.FunctionType): + """The decorated function gives the regexp that a path variable of the type of its only argument, or of a subclass, + matches.""" annotations = func.__annotations__.copy() annotations.pop('return', None) assert len(annotations) == 1 @@ -48,18 +50,18 @@ def get_regexp(type_=None) -> str: # noinspection PyUnusedLocal -@register_url_regexp +@url_regexp def int_regexp(cls: int): return r'\d+' # noinspection PyUnusedLocal -@register_url_regexp +@url_regexp def uuid_regexp(cls: uuid.UUID): return r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' -@register_url_regexp +@url_regexp def enum_regexp(cls: enum.Enum): values = (f'({e.value})' for e in cls) return '(' + '|'.join(values) + ')' From 233768844f483c8ea1951c22deb721d5ddda2ef7 Mon Sep 17 00:00:00 2001 From: Alexander Egorov Date: Tue, 27 Oct 2026 17:16:21 +0100 Subject: [PATCH 2/2] docs(web): multi-line docstring of url_regexp in the style of the repository Co-Authored-By: Claude Opus 5.5 --- winter/web/urls.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/winter/web/urls.py b/winter/web/urls.py index ec33591a..a8eb7f37 100644 --- a/winter/web/urls.py +++ b/winter/web/urls.py @@ -12,8 +12,10 @@ def url_regexp(func: types.FunctionType): - """The decorated function gives the regexp that a path variable of the type of its only argument, or of a subclass, - matches.""" + """ + The decorated function gives the regexp that a path variable matches when its type is the type of the only + argument of the function, or a subclass of it. + """ annotations = func.__annotations__.copy() annotations.pop('return', None) assert len(annotations) == 1