diff --git a/.flake8 b/.flake8 index 3f958e89..27e0807c 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E203, E266, W503 +ignore = E203, E266, W503, E704, E701 max-line-length = 88 max-complexity = 18 per-file-ignores = diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a7bba413..8a1d16a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,7 +93,7 @@ jobs: - name: Install dependencies run: | pip install -r requirements.txt - pip install black isort flake8 + pip install black==24.2.0 isort==5.13.2 flake8==7.0.0 - name: Compile Cython extensions run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e54c891..88b638ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.7] - 2024-02-17 :tulip: + +- Fixes bug [#38](https://github.com/Neoteroi/BlackSheep-Docs/issues/38), + to support properly `list[T]` and `tuple[T]` when defining query string + parameters. Reported by @ranggakd. +- Passes annotated origin type to build OpenAPI docs (#475), by @tyzhnenko. +- Fixes #481, disabling signal handling by default to avoid negative side + effects. Handling signals is now opt-in and can be achieved using the env + variable `APP_SIGNAL_HANDLER=1`. The `is_stopping` function is modified to + work only when the option is enabled. Issue reported by @netanel-haber. +- Upgrades `black` version and format files accordingly. + ## [2.0.6] - 2024-01-17 :kr: :heart: - Adds built-in support for [Server-Sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). diff --git a/blacksheep/__init__.py b/blacksheep/__init__.py index 2bf06bc8..bce86946 100644 --- a/blacksheep/__init__.py +++ b/blacksheep/__init__.py @@ -2,8 +2,9 @@ Root module of the framework. This module re-exports the most commonly used types to reduce the verbosity of the imports statements. """ + __author__ = "Roberto Prevato " -__version__ = "2.0.6" +__version__ = "2.0.7" from .contents import Content as Content from .contents import FormContent as FormContent diff --git a/blacksheep/baseapp.pyi b/blacksheep/baseapp.pyi index fc61cf4f..436b8bd4 100644 --- a/blacksheep/baseapp.pyi +++ b/blacksheep/baseapp.pyi @@ -18,6 +18,7 @@ class BaseApplication: self.router = router self.exceptions_handlers = self.init_exceptions_handlers() self.show_error_details = show_error_details + def init_exceptions_handlers(self) -> ExceptionHandlersType: ... async def handle(self, request: Request) -> Response: ... async def handle_internal_server_error( diff --git a/blacksheep/common/types.py b/blacksheep/common/types.py index a69d3214..5e335272 100644 --- a/blacksheep/common/types.py +++ b/blacksheep/common/types.py @@ -1,6 +1,7 @@ """ Common types annotations and functions. """ + from typing import AnyStr, Dict, Iterable, List, Optional, Tuple, Union from blacksheep.url import URL diff --git a/blacksheep/contents.pyi b/blacksheep/contents.pyi index 2e9003d8..5741c7c9 100644 --- a/blacksheep/contents.pyi +++ b/blacksheep/contents.pyi @@ -16,8 +16,10 @@ class Content: self.type = content_type self.body = data self.length = len(data) + async def read(self) -> bytes: return self.body + def dispose(self) -> None: ... class StreamedContent(Content): @@ -31,6 +33,7 @@ class StreamedContent(Content): self.body = None self.length = data_length self.generator = data_provider + async def get_parts(self) -> AsyncIterable[bytes]: ... class ASGIContent(Content): @@ -39,6 +42,7 @@ class ASGIContent(Content): self.body = None self.length = -1 self.receive = receive + def dispose(self): ... async def stream(self) -> AsyncIterable[bytes]: ... async def read(self) -> bytes: ... @@ -85,6 +89,7 @@ class FormPart: self.file_name = file_name self.content_type = content_type self.charset = charset + def __repr__(self): return f"" diff --git a/blacksheep/cookies.pyi b/blacksheep/cookies.pyi index d4ee6fef..281b473b 100644 --- a/blacksheep/cookies.pyi +++ b/blacksheep/cookies.pyi @@ -36,6 +36,7 @@ class Cookie: self.secure = secure self.max_age = max_age self.same_site = same_site + def clone(self) -> "Cookie": ... def __eq__(self, other: Union[str, bytes, "Cookie"]) -> bool: ... def __repr__(self) -> str: diff --git a/blacksheep/headers.pyi b/blacksheep/headers.pyi index cebca5e7..82ff221f 100644 --- a/blacksheep/headers.pyi +++ b/blacksheep/headers.pyi @@ -5,6 +5,7 @@ class Header: def __init__(self, name: bytes, value: bytes): self.name = name self.value = value + def __repr__(self) -> str: ... def __iter__(self) -> Generator[bytes, None, None]: ... def __eq__(self, other: object) -> bool: ... @@ -14,6 +15,7 @@ HeaderType = Tuple[bytes, bytes] class Headers: def __init__(self, values: Optional[List[HeaderType]] = None): self.values = values + def get(self, name: bytes) -> Tuple[HeaderType]: ... def get_tuples(self, name: bytes) -> List[HeaderType]: ... def get_first(self, key: bytes) -> Optional[bytes]: ... diff --git a/blacksheep/messages.pyi b/blacksheep/messages.pyi index 8e327f9b..c9b8cf2d 100644 --- a/blacksheep/messages.pyi +++ b/blacksheep/messages.pyi @@ -38,11 +38,13 @@ class Message: otherwise kept as raw bytes. In case of ambiguity, use the dedicated `multiparts()` method. """ + async def multipart(self) -> List[FormPart]: """ Returns parts read from multipart/form-data, if present, otherwise None """ + def declares_content_type(self, type: bytes) -> bool: ... def declares_json(self) -> bool: ... def declares_xml(self) -> bool: ... @@ -68,6 +70,7 @@ class Request(Message): self.user: Optional[Identity] = ... self.scope: ASGIScopeInterface = ... self._session: Optional[Session] + @classmethod def incoming( cls, method: str, path: bytes, query: bytes, headers: List[HeaderType] @@ -143,6 +146,7 @@ class Response(Message): ) -> None: self.status = status self.content = content + def __repr__(self) -> str: ... @property def cookies(self) -> Cookies: ... diff --git a/blacksheep/server/authentication/oidc.py b/blacksheep/server/authentication/oidc.py index 805084d1..29767a02 100644 --- a/blacksheep/server/authentication/oidc.py +++ b/blacksheep/server/authentication/oidc.py @@ -2,6 +2,7 @@ This module provides classes to handle OpenID Connect authentication through integration with OAuth applications, supporting Authorization Code Grant and Hybrid flows. """ + import logging from abc import ABC, abstractmethod from dataclasses import dataclass diff --git a/blacksheep/server/bindings.py b/blacksheep/server/bindings.py index ce031ecc..74429399 100644 --- a/blacksheep/server/bindings.py +++ b/blacksheep/server/bindings.py @@ -7,6 +7,7 @@ See: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2 """ + from abc import abstractmethod from base64 import urlsafe_b64decode from collections.abc import Iterable as IterableAbc @@ -442,8 +443,8 @@ def _get_default_converter_single(self, expected_type): if expected_type is bytes: # note: the code is optimized for strings here, not bytes # since most of times the user will want to handle strings - return ( - lambda value: urlsafe_b64decode(value.encode("utf8")).decode("utf8") + return lambda value: ( + urlsafe_b64decode(value.encode("utf8")).decode("utf8") if value else None ) @@ -645,8 +646,8 @@ def _get_default_converter_single(self, expected_type): return lambda value: dateutil_parser(unquote(value)) if value else None if expected_type is date: - return ( - lambda value: dateutil_parser(unquote(value)).date() if value else None + return lambda value: ( + dateutil_parser(unquote(value)).date() if value else None ) raise MissingConverterError(expected_type, self.__class__) @@ -684,10 +685,8 @@ def _get_default_converter(self, expected_type): return lambda value: dateutil_parser(unquote(value[0])) if value else None if expected_type is date: - return ( - lambda value: dateutil_parser(unquote(value[0])).date() - if value - else None + return lambda value: ( + dateutil_parser(unquote(value[0])).date() if value else None ) raise MissingConverterError(expected_type, self.__class__) diff --git a/blacksheep/server/env.py b/blacksheep/server/env.py index 7e46cfb2..030617fd 100644 --- a/blacksheep/server/env.py +++ b/blacksheep/server/env.py @@ -40,4 +40,4 @@ def __init__(self) -> None: self.show_error_details = truthy(os.environ.get("APP_SHOW_ERROR_DETAILS", "")) self.mount_auto_events = truthy(os.environ.get("APP_MOUNT_AUTO_EVENTS", "1")) self.use_default_router = truthy(os.environ.get("APP_DEFAULT_ROUTER", "1")) - self.add_signal_handler = truthy(os.environ.get("APP_SIGNAL_HANDLER", "1")) + self.add_signal_handler = truthy(os.environ.get("APP_SIGNAL_HANDLER", "")) diff --git a/blacksheep/server/normalization.py b/blacksheep/server/normalization.py index 51bc5138..9c8dd759 100644 --- a/blacksheep/server/normalization.py +++ b/blacksheep/server/normalization.py @@ -136,8 +136,7 @@ def ensure_response(result) -> Response: return result -class NormalizationError(Exception): - ... +class NormalizationError(Exception): ... class UnsupportedSignatureError(NormalizationError): @@ -238,6 +237,23 @@ def __init__(self, parameter_name, route): Tuple[UUID], } +try: + # Note: try catch here is to support Python 3.8 + # it can be removed when support for Python 3.8 is dropped + _types_handled_with_query |= { + list[str], + list[int], + list[float], + list[bool], + list[UUID], + tuple[str], + tuple[int], + tuple[float], + tuple[bool], + } +except TypeError: + pass + def _check_union( parameter: ParamInfo, annotation: Any, method: Callable[..., Any] diff --git a/blacksheep/server/openapi/common.py b/blacksheep/server/openapi/common.py index 3e5dba0c..cc05fb31 100644 --- a/blacksheep/server/openapi/common.py +++ b/blacksheep/server/openapi/common.py @@ -6,6 +6,7 @@ OpenAPI Documentation v2 and v3 (currently only v3 is supported) from these types, and potentially in the future v4, if it will be so different from v3. """ + import json from abc import ABC, abstractmethod from dataclasses import dataclass diff --git a/blacksheep/server/openapi/docstrings.py b/blacksheep/server/openapi/docstrings.py index a333d68b..5551ddc2 100644 --- a/blacksheep/server/openapi/docstrings.py +++ b/blacksheep/server/openapi/docstrings.py @@ -17,6 +17,7 @@ * tests/test_openapi_docstrings.py """ + import re import warnings from abc import ABC, abstractmethod @@ -212,9 +213,9 @@ def parse_docstring(self, docstring: str) -> DocstringInfo: description=collapse(description), parameters=self.get_parameters_info(docstring), return_type=return_type, - return_description=collapse(return_description) - if return_description - else None, + return_description=( + collapse(return_description) if return_description else None + ), ) @@ -401,9 +402,9 @@ def parse_docstring(self, docstring: str) -> DocstringInfo: description=collapse(info.description), parameters=self.get_parameters_info(docstring), return_type=return_info.return_type if return_info else None, - return_description=collapse(return_info.return_description) - if return_info - else None, + return_description=( + collapse(return_info.return_description) if return_info else None + ), ) @@ -482,9 +483,9 @@ def parse_docstring(self, docstring: str) -> DocstringInfo: description=collapse(info.description), parameters=self.get_parameters_info(docstring), return_type=return_info.return_type if return_info else None, - return_description=collapse(return_info.return_description) - if return_info - else None, + return_description=( + collapse(return_info.return_description) if return_info else None + ), ) diff --git a/blacksheep/server/openapi/ui.py b/blacksheep/server/openapi/ui.py index d6b32adb..03f049d0 100644 --- a/blacksheep/server/openapi/ui.py +++ b/blacksheep/server/openapi/ui.py @@ -59,8 +59,7 @@ def get_ui_handler(self) -> Callable[[Request], Response]: """ @property - def default_ui_files(self) -> UIFilesOptions: - ... + def default_ui_files(self) -> UIFilesOptions: ... class SwaggerUIProvider(UIProvider): diff --git a/blacksheep/server/openapi/v3.py b/blacksheep/server/openapi/v3.py index c38f5359..920d041f 100644 --- a/blacksheep/server/openapi/v3.py +++ b/blacksheep/server/openapi/v3.py @@ -399,9 +399,9 @@ def __init__( DataClassTypeHandler(), PydanticModelTypeHandler(), ] - self._binder_docs: Dict[ - Type[Binder], Iterable[Union[Parameter, Reference]] - ] = {} + self._binder_docs: Dict[Type[Binder], Iterable[Union[Parameter, Reference]]] = ( + {} + ) self.security_schemes = security_schemes or {} @property @@ -915,9 +915,11 @@ def get_parameters( param_info.source or ParameterSource.QUERY ), required=param_info.required, - schema=self.get_schema_by_type(param_info.value_type) - if param_info.value_type - else None, + schema=( + self.get_schema_by_type(param_info.value_type) + if param_info.value_type + else None + ), description=param_info.description, example=param_info.example, ) @@ -975,9 +977,9 @@ def _get_content_from_response_info( for content in response_content: if content.content_type not in oad_content: - oad_content[ - content.content_type - ] = self._get_media_type_from_content_doc(content) + oad_content[content.content_type] = ( + self._get_media_type_from_content_doc(content) + ) else: raise DuplicatedContentTypeDocsException(content.content_type) diff --git a/blacksheep/server/process.py b/blacksheep/server/process.py index c201869b..cb2f5941 100644 --- a/blacksheep/server/process.py +++ b/blacksheep/server/process.py @@ -1,9 +1,13 @@ """ Provides functions related to the server process. """ + +import os import signal from typing import TYPE_CHECKING +from blacksheep.utils import truthy + if TYPE_CHECKING: from blacksheep.server.application import Application @@ -15,6 +19,10 @@ def is_stopping() -> bool: Returns a value indicating whether the server process received a SIGINT or a SIGTERM signal, and therefore the application is stopping. """ + if not truthy(os.environ.get("APP_SIGNAL_HANDLER", "")): + raise RuntimeError( + "This function can only be used if the env variable `APP_SIGNAL_HANDLER=1`" + ) return _STOPPING diff --git a/blacksheep/server/sse.py b/blacksheep/server/sse.py index b919c483..c3a961d6 100644 --- a/blacksheep/server/sse.py +++ b/blacksheep/server/sse.py @@ -1,6 +1,7 @@ """ This module offer built-in functions for Server Sent Events. """ + from typing import AsyncIterable, Callable, List, Optional, Tuple from blacksheep.contents import ServerSentEvent, StreamedContent diff --git a/blacksheep/testing/messages.py b/blacksheep/testing/messages.py index 8de46d64..7f73024e 100644 --- a/blacksheep/testing/messages.py +++ b/blacksheep/testing/messages.py @@ -41,9 +41,9 @@ async def __call__(self): return { "body": message, "type": "http.message", - "more_body": False - if (len(self.messages) == self.index or not message) - else True, + "more_body": ( + False if (len(self.messages) == self.index or not message) else True + ), } diff --git a/itests/app_2.py b/itests/app_2.py index 723cbb85..149ef22f 100644 --- a/itests/app_2.py +++ b/itests/app_2.py @@ -453,8 +453,7 @@ def get_cats( """ @get("foos") - def get_foos() -> FooList: - ... + def get_foos() -> FooList: ... @get("cats2") def get_cats_alt2( @@ -575,8 +574,7 @@ async def update_foo2( @docs.ignore() @get("/ignored") - def secret_api(self): - ... + def secret_api(self): ... @docs.summary("Some deprecated API") @docs.deprecated() @@ -668,26 +666,21 @@ def magic_cat3(self, example: Example) -> Response: """ @post("forward_ref") - def magic_cat4(self, example: Example2) -> Response: - ... + def magic_cat4(self, example: Example2) -> Response: ... @post("poor-use-of-list-annotation") - def magic_cat5(self, example: list) -> Response: - ... + def magic_cat5(self, example: list) -> Response: ... @post("poor-use-of-set-annotation2") - def magic_cat6(self, example: Set) -> Response: - ... + def magic_cat6(self, example: Set) -> Response: ... @post("/polymorph-example") @docs(on_created=on_polymorph_example_docs_created) - def polymorph_example(self) -> Response: - ... + def polymorph_example(self) -> Response: ... @post("/polymorph-example-pydantic") @docs(on_created=on_polymorph_example_docs_created_pydantic) - def polymorph_example_pydantic(self) -> Response: - ... + def polymorph_example_pydantic(self) -> Response: ... if __name__ == "__main__": diff --git a/itests/test_auth_oidc.py b/itests/test_auth_oidc.py index 8c62e6af..ae6bec6b 100644 --- a/itests/test_auth_oidc.py +++ b/itests/test_auth_oidc.py @@ -2,6 +2,7 @@ This module is under integration tests folder because it uses a partially faked authorization server in a running Flask server. """ + import json import re from datetime import datetime @@ -709,16 +710,13 @@ async def test_redirect_state_includes_original_path( ) @app.router.get("/") - async def home(): - ... + async def home(): ... @app.router.get("/account") - async def account_page(): - ... + async def account_page(): ... @app.router.get("/product/{category}/{name}") - async def product_details(): - ... + async def product_details(): ... await app.start() await app( @@ -786,8 +784,7 @@ async def test_raises_for_nonce_mismatch(app: FakeApplication): ) @app.router.get("/") - async def home(): - ... + async def home(): ... await app.start() await app( @@ -1264,8 +1261,7 @@ async def test_default_openid_connect_handler_redirects_unauthenticated_users( ) @app.router.get("/account") - def get_account_details(): - ... + def get_account_details(): ... @app.router.get("/requirement") def example(): diff --git a/tests/client/__init__.py b/tests/client/__init__.py index a2f6fc52..5b768eb8 100644 --- a/tests/client/__init__.py +++ b/tests/client/__init__.py @@ -1,5 +1,6 @@ """These classes implement the interface used by BlackSheep HTTP client implementation, to simplify testing on the ClientSession object; including handling of connections and requests timeouts; redirects, etc.""" + import asyncio diff --git a/tests/test_application.py b/tests/test_application.py index 8967c896..e1123639 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1948,8 +1948,7 @@ async def home(files: FromFiles): @pytest.mark.asyncio async def test_handler_from_json_parameter_missing_property(app): @app.router.post("/") - async def home(item: FromJSON[Item]): - ... + async def home(item: FromJSON[Item]): ... # Note: the following example missing one of the properties # required by the constructor @@ -2044,8 +2043,7 @@ async def get_lorem(): async def test_handler_from_json_parameter_missing_property_complex_type(app): @inject() @app.router.post("/") - async def home(item: FromJSON[Foo]): - ... + async def home(item: FromJSON[Foo]): ... # Note: the following example missing one of the properties # required by the constructor @@ -2070,8 +2068,7 @@ async def home(item: FromJSON[Foo]): @pytest.mark.asyncio async def test_handler_from_json_parameter_missing_property_array(app): @app.router.post("/") - async def home(item: FromJSON[List[Item]]): - ... + async def home(item: FromJSON[List[Item]]): ... # Note: the following example missing one of the properties # required by the constructor @@ -2806,8 +2803,7 @@ async def home(foo: FromQuery[List[int]]): @pytest.mark.asyncio async def test_invalid_query_parameter_int(app): @app.router.get("/") - async def home(request, foo: FromQuery[int]): - ... + async def home(request, foo: FromQuery[int]): ... app.normalize_handlers() @@ -2855,8 +2851,7 @@ async def home(request, foo: FromQuery[int]): @pytest.mark.asyncio async def test_invalid_query_parameter_float(app): @app.router.get("/") - async def home(request, foo: FromQuery[float]): - ... + async def home(request, foo: FromQuery[float]): ... app.normalize_handlers() @@ -2904,8 +2899,7 @@ async def home(request, foo: FromQuery[float]): @pytest.mark.asyncio async def test_invalid_query_parameter_bool(app): @app.router.get("/") - async def home(request, foo: FromQuery[bool]): - ... + async def home(request, foo: FromQuery[bool]): ... app.normalize_handlers() @@ -3736,12 +3730,10 @@ def on_middlewares_configuration_2(application: FakeApplication) -> None: @pytest.mark.asyncio async def test_app_events_decorator_args_support(app: Application): @app.on_start - async def before_start_1(application: FakeApplication) -> None: - ... + async def before_start_1(application: FakeApplication) -> None: ... @app.on_start() - async def before_start_2(application: FakeApplication) -> None: - ... + async def before_start_2(application: FakeApplication) -> None: ... @pytest.mark.asyncio @@ -4009,8 +4001,7 @@ class CreateCatInput(BaseModel): type: str @app.router.post("/api/cat") - async def create_cat(data: CreateCatInput): - ... + async def create_cat(data: CreateCatInput): ... # invalid JSON: content = b'{"foo":"not valid"}' diff --git a/tests/test_controllers.py b/tests/test_controllers.py index d88d62fa..45d24a97 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -488,13 +488,11 @@ async def test_controllers_with_duplicate_routes_throw( class A(Controller): @get(first_pattern) - async def index(self, request: Request): - ... + async def index(self, request: Request): ... class B(Controller): @get(second_pattern) - async def index(self, request: Request): - ... + async def index(self, request: Request): ... with pytest.raises(RouteDuplicate) as context: app.use_controllers() @@ -674,13 +672,11 @@ class A(Controller): route = "home" @get(first_pattern) - async def index(self, request: Request): - ... + async def index(self, request: Request): ... class B(Controller): @get(second_pattern) - async def index(self, request: Request): - ... + async def index(self, request: Request): ... with pytest.raises(RouteDuplicate): app.use_controllers() @@ -703,12 +699,10 @@ class A(Controller): route = "home" @get(first_pattern) - async def index(self, request: Request): - ... + async def index(self, request: Request): ... @app.router.route(second_pattern) - async def home(): - ... + async def home(): ... with pytest.raises(RouteDuplicate): app.use_controllers() diff --git a/tests/test_cors.py b/tests/test_cors.py index 6613910c..e7c782d3 100644 --- a/tests/test_cors.py +++ b/tests/test_cors.py @@ -134,8 +134,7 @@ async def home(): return text("Hello, World") @app.router.put("/") - async def put_something(): - ... + async def put_something(): ... await app.start() @@ -322,8 +321,7 @@ async def home(): return text("Hello, World") @app.router.delete("/") - async def delete_example(): - ... + async def delete_example(): ... await app.start() @@ -460,8 +458,7 @@ async def home(): return text("Hello, World") @app.router.post("/") - async def post_example(): - ... + async def post_example(): ... await app.start() @@ -492,8 +489,7 @@ async def home(): return text("Hello, World") @app.router.post("/") - async def post_example(): - ... + async def post_example(): ... await app.start() @@ -601,8 +597,7 @@ async def home(): return text("Hello, World") @app.router.post("/another") - async def another(): - ... + async def another(): ... await app.start() diff --git a/tests/test_files_serving.py b/tests/test_files_serving.py index c588705c..a207de99 100644 --- a/tests/test_files_serving.py +++ b/tests/test_files_serving.py @@ -849,8 +849,7 @@ async def not_found_handler(*args): @pytest.mark.asyncio async def test_serve_files_index_html_options(files2_index_contents, app: Application): - def on_response(request, response): - ... + def on_response(request, response): ... mock = create_autospec(on_response, return_value=None) @@ -894,8 +893,7 @@ def on_response(request, response): async def test_serve_files_index_html_options_fallback( files2_index_contents, app: Application ): - def on_response(request, response): - ... + def on_response(request, response): ... mock = create_autospec(on_response, return_value=None) diff --git a/tests/test_normalization.py b/tests/test_normalization.py index cefa5457..b06e8d98 100644 --- a/tests/test_normalization.py +++ b/tests/test_normalization.py @@ -46,12 +46,10 @@ def __init__(self, name): self.name = name -class Cat(Pet): - ... +class Cat(Pet): ... -class Dog(Pet): - ... +class Dog(Pet): ... @dataclass @@ -60,8 +58,7 @@ class SomeService: def test_parameters_get_binders_default_query(): - def handler(a, b, c): - ... + def handler(a, b, c): ... binders = get_binders(Route(b"/", handler), Container()) @@ -75,8 +72,7 @@ def handler(a, b, c): "annotation_type", [Identity, User, Optional[User], Optional[Identity]] ) def test_identity_binder_by_param_type(annotation_type): - async def handler(param): - ... + async def handler(param): ... handler.__annotations__["param"] = annotation_type @@ -86,8 +82,7 @@ async def handler(param): def test_parameters_get_binders_from_route(): - def handler(a, b, c): - ... + def handler(a, b, c): ... binders = get_binders(Route(b"/:a/:b/:c", handler), Container()) @@ -98,8 +93,7 @@ def handler(a, b, c): def test_parameters_get_binders_from_services_by_name(): - def handler(a, b, c): - ... + def handler(a, b, c): ... binders = get_binders( Route(b"/", handler), {"a": object(), "b": object(), "c": object()} @@ -112,8 +106,7 @@ def handler(a, b, c): def test_parameters_get_binders_from_services_by_type(): - def handler(a: str, b: int, c: Cat): - ... + def handler(a: str, b: int, c: Cat): ... binders = get_binders( Route(b"/", handler), {str: object(), int: object(), Cat: object()} @@ -126,8 +119,7 @@ def handler(a: str, b: int, c: Cat): def test_parameters_get_binders_from_body(): - def handler(a: Cat): - ... + def handler(a: Cat): ... binders = get_binders(Route("/", handler), Container()) assert len(binders) == 1 @@ -139,8 +131,7 @@ def handler(a: Cat): def test_parameters_get_binders_from_body_optional(): - def handler(a: Optional[Cat]): - ... + def handler(a: Optional[Cat]): ... binders = get_binders(Route(b"/", handler), Container()) assert len(binders) == 1 @@ -152,8 +143,7 @@ def handler(a: Optional[Cat]): def test_parameters_get_binders_simple_types_default_from_query(): - def handler(a: str, b: int, c: bool): - ... + def handler(a: str, b: int, c: bool): ... binders = get_binders(Route(b"/", handler), Container()) @@ -167,8 +157,7 @@ def handler(a: str, b: int, c: bool): def test_parameters_get_binders_list_types_default_from_query(): - def handler(a: List[str], b: List[int], c: List[bool]): - ... + def handler(a: List[str], b: List[int], c: List[bool]): ... binders = get_binders(Route(b"/", handler), Container()) @@ -182,8 +171,7 @@ def handler(a: List[str], b: List[int], c: List[bool]): def test_parameters_get_binders_list_types_default_from_query_optional(): - def handler(a: Optional[List[str]]): - ... + def handler(a: Optional[List[str]]): ... binders = get_binders(Route(b"/", handler), Container()) @@ -194,8 +182,7 @@ def handler(a: Optional[List[str]]): def test_parameters_get_binders_list_types_default_from_query_required(): - def handler(a: List[str]): - ... + def handler(a: List[str]): ... binders = get_binders(Route(b"/", handler), Container()) @@ -204,8 +191,7 @@ def handler(a: List[str]): def test_parameters_get_binders_sequence_types_default_from_query(): - def handler(a: Sequence[str], b: Sequence[int], c: Sequence[bool]): - ... + def handler(a: Sequence[str], b: Sequence[int], c: Sequence[bool]): ... binders = get_binders(Route(b"/", handler), Container()) @@ -219,31 +205,26 @@ def handler(a: Sequence[str], b: Sequence[int], c: Sequence[bool]): def test_throw_for_ambiguous_binder_multiple_from_body(): - def handler(a: Cat, b: Dog): - ... + def handler(a: Cat, b: Dog): ... with pytest.raises(AmbiguousMethodSignatureError): get_binders(Route(b"/", handler), Container()) def test_does_not_throw_for_forward_ref(): - def handler(a: "Cat"): - ... + def handler(a: "Cat"): ... get_binders(Route(b"/", handler), Container()) - def handler(a: List["str"]): - ... + def handler(a: List["str"]): ... get_binders(Route(b"/", handler), Container()) - def handler(a: Optional[List["Cat"]]): - ... + def handler(a: Optional[List["Cat"]]): ... get_binders(Route(b"/", handler), Container()) - def handler(a: FromQuery["str"]): - ... + def handler(a: FromQuery["str"]): ... get_binders(Route(b"/", handler), Container()) @@ -255,8 +236,7 @@ def handler( c: FromJSON[Cat], d: FromRoute[str], e: FromHeader[str], - ): - ... + ): ... container = Container() container.register(Dog, instance=Dog("Snoopy")) @@ -275,8 +255,7 @@ def handler( def test_implicit_from_services_only_when_annotation_is_none(): - def handler(dog): - ... + def handler(dog): ... container = Container() container.register("dog", instance=Dog("Snoopy")) @@ -285,8 +264,7 @@ def handler(dog): assert isinstance(binders[0], ServiceBinder) - def handler(dog: str): - ... + def handler(dog: str): ... binders = get_binders(Route(b"/", handler), container) @@ -298,8 +276,7 @@ class FromExampleQuery(FromQuery[str]): name = "example" @inject() - def handler(a: FromExampleQuery): - ... + def handler(a: FromExampleQuery): ... binders = get_binders(Route(b"/", handler), Container()) binder = binders[0] @@ -311,8 +288,7 @@ def handler(a: FromExampleQuery): def test_from_query_unspecified_type(): - def handler(a: FromQuery): - ... + def handler(a: FromQuery): ... binders = get_binders(Route(b"/", handler), Container()) binder = binders[0] @@ -324,8 +300,7 @@ def handler(a: FromQuery): def test_from_query_optional_type(): - def handler(a: FromQuery[Optional[str]]): - ... + def handler(a: FromQuery[Optional[str]]): ... binders = get_binders(Route(b"/", handler), Container()) binder = binders[0] @@ -337,8 +312,7 @@ def handler(a: FromQuery[Optional[str]]): def test_from_query_optional_type_with_union(): - def handler(a: FromQuery[Union[None, str]]): - ... + def handler(a: FromQuery[Union[None, str]]): ... binders = get_binders(Route(b"/", handler), Container()) binder = binders[0] @@ -371,8 +345,7 @@ def test_check_union_or_none(): def test_from_query_optional_list_type(): - def handler(a: FromQuery[Optional[List[str]]]): - ... + def handler(a: FromQuery[Optional[List[str]]]): ... binders = get_binders(Route(b"/", handler), Container()) binder = binders[0] @@ -388,8 +361,7 @@ class FromExampleHeader(FromHeader[str]): name = "example" @inject() - def handler(a: FromExampleHeader): - ... + def handler(a: FromExampleHeader): ... binders = get_binders(Route(b"/", handler), Container()) @@ -403,8 +375,7 @@ class AcceptLanguageHeader(FromHeader[str]): name = "accept-language" @inject() - def handler(a: AcceptLanguageHeader): - ... + def handler(a: AcceptLanguageHeader): ... binders = get_binders(Route(b"/", handler), Container()) @@ -414,32 +385,28 @@ def handler(a: AcceptLanguageHeader): def test_raises_for_route_mismatch(): - def handler(a: FromRoute[str]): - ... + def handler(a: FromRoute[str]): ... with raises(RouteBinderMismatch): get_binders(Route(b"/", handler), Container()) def test_raises_for_route_mismatch_2(): - def handler(a: FromRoute[str]): - ... + def handler(a: FromRoute[str]): ... with raises(RouteBinderMismatch): get_binders(Route(b"/:b", handler), Container()) def test_raises_for_unsupported_union(): - def handler(a: FromRoute[Union[str, int]]): - ... + def handler(a: FromRoute[Union[str, int]]): ... with raises(NormalizationError): get_binders(Route(b"/:b", handler), Container()) def test_request_binding(): - def handler(request): - ... + def handler(request): ... binders = get_binders(Route(b"/", handler), Container()) @@ -613,8 +580,7 @@ async def middleware(request, handler): def test_get_raw_bound_value_type_fallsback_to_str(): - class Foo: - ... + class Foo: ... assert _get_raw_bound_value_type(Foo) is str # type: ignore @@ -624,8 +590,7 @@ def handler( foo_id: str, data: FromJSON[Cat], some_service: SomeService, - ): - ... + ): ... container = Container() container.add_transient(SomeService) @@ -644,8 +609,7 @@ def handler( def test_camel_case_route_parameter(): - def handler(statusKey: FromRoute[str]): - ... + def handler(statusKey: FromRoute[str]): ... binders = get_binders(Route(b"/:statusKey", handler), Container()) @@ -654,8 +618,7 @@ def handler(statusKey: FromRoute[str]): def test_pascal_case_route_parameter(): - def handler(StatusKey: FromRoute[str]): - ... + def handler(StatusKey: FromRoute[str]): ... binders = get_binders(Route(b"/:StatusKey", handler), Container()) @@ -669,8 +632,7 @@ async def test_binder_through_di(): Tests support for dependency injection when resolving custom binders. """ - class Foo: - ... + class Foo: ... class FromExample(BoundValue[str]): pass diff --git a/tests/test_openapi_v3.py b/tests/test_openapi_v3.py index 6d3c294c..cbddde4c 100644 --- a/tests/test_openapi_v3.py +++ b/tests/test_openapi_v3.py @@ -237,20 +237,16 @@ def get_cats_api() -> Application: delete = app.router.delete @get("/api/cats") - def get_cats() -> PaginatedSet[Cat]: - ... + def get_cats() -> PaginatedSet[Cat]: ... @get("/api/cats/{cat_id}") - def get_cat_details(cat_id: int) -> CatDetails: - ... + def get_cat_details(cat_id: int) -> CatDetails: ... @post("/api/cats") - def create_cat(input: CreateCatInput) -> Cat: - ... + def create_cat(input: CreateCatInput) -> Cat: ... @delete("/api/cats/{cat_id}") - def delete_cat(cat_id: int) -> None: - ... + def delete_cat(cat_id: int) -> None: ... return app @@ -296,8 +292,7 @@ async def test_raises_for_duplicated_content_example(docs): 200: ResponseInfo("Example", content=[ContentInfo(Foo), ContentInfo(Foo)]) } ) - async def example(): - ... + async def example(): ... with pytest.raises(DuplicatedContentTypeDocsException): docs.bind_app(app) @@ -757,8 +752,7 @@ async def test_register_schema_for_multi_generic( app = get_app() @app.router.route("/combo") - def combo_example() -> Combo[Cat, Foo]: - ... + def combo_example() -> Combo[Cat, Foo]: ... docs.bind_app(app) await app.start() @@ -841,13 +835,11 @@ async def test_register_schema_for_generic_with_list_reusing_ref( @docs(tags=["B tag"]) @app.router.route("/one") - def one() -> PaginatedSet[Cat]: - ... + def one() -> PaginatedSet[Cat]: ... @docs(tags=["A tag"]) @app.router.route("/two") - def two() -> PaginatedSet[Cat]: - ... + def two() -> PaginatedSet[Cat]: ... await app.start() @@ -934,8 +926,7 @@ async def test_handling_of_forward_references( app = get_app() @app.router.route("/") - def forward_ref_example() -> ForwardRefExample: - ... + def forward_ref_example() -> ForwardRefExample: ... docs.bind_app(app) await app.start() @@ -1012,8 +1003,7 @@ async def test_handling_of_normal_class(docs: OpenAPIHandler, serializer: Serial app = get_app() @app.router.route("/") - def plain_class() -> PlainClass: - ... + def plain_class() -> PlainClass: ... docs.bind_app(app) await app.start() @@ -1051,8 +1041,7 @@ async def test_handling_of_pydantic_class_with_generic( app = get_app() @app.router.route("/") - def home() -> PydPaginatedSetOfCat: - ... + def home() -> PydPaginatedSetOfCat: ... docs.bind_app(app) await app.start() @@ -1180,8 +1169,7 @@ async def test_handling_of_pydantic_class_with_child_models( app = get_app() @app.router.route("/") - def home() -> PydTypeWithChildModels: - ... + def home() -> PydTypeWithChildModels: ... docs.bind_app(app) await app.start() @@ -1351,8 +1339,7 @@ async def test_handling_of_pydantic_class_in_generic( app = get_app() @app.router.route("/") - def home() -> PaginatedSet[PydCat]: - ... + def home() -> PaginatedSet[PydCat]: ... docs.bind_app(app) await app.start() @@ -1478,8 +1465,7 @@ async def test_handling_of_sequence(docs: OpenAPIHandler, serializer: Serializer app = get_app() @app.router.route("/") - def home() -> Sequence[Cat]: - ... + def home() -> Sequence[Cat]: ... docs.bind_app(app) await app.start() @@ -1860,8 +1846,7 @@ async def test_handling_of_pydantic_types(docs: OpenAPIHandler, serializer: Seri app = get_app() @app.router.route("/") - def home() -> PydExampleWithSpecificTypes: - ... + def home() -> PydExampleWithSpecificTypes: ... docs.bind_app(app) await app.start() @@ -1909,8 +1894,7 @@ async def test_pydantic_generic(docs: OpenAPIHandler, serializer: Serializer): app = get_app() @app.router.route("/") - def home() -> PydResponse[PydCat]: - ... + def home() -> PydResponse[PydCat]: ... docs.bind_app(app) await app.start() @@ -2181,8 +2165,7 @@ async def test_pydantic_constrained_types(docs: OpenAPIHandler, serializer: Seri app = get_app() @app.router.route("/") - def home() -> PydConstrained: - ... + def home() -> PydConstrained: ... docs.bind_app(app) await app.start() @@ -2377,14 +2360,12 @@ async def test_schema_registration(docs: OpenAPIHandler, serializer: Serializer) }, ) ) - class A: - ... + class A: ... app = get_app() @app.router.route("/") - def home() -> A: - ... + def home() -> A: ... @docs( security=[ @@ -2393,8 +2374,7 @@ def home() -> A: ] ) @app.router.route("/", methods=["POST"]) - def auth_home() -> A: - ... + def auth_home() -> A: ... docs.bind_app(app) await app.start() @@ -2462,20 +2442,16 @@ async def test_handles_ref_for_optional_type( app = get_app() @app.router.route("/cats") - def one() -> PaginatedSet[Cat]: - ... + def one() -> PaginatedSet[Cat]: ... @app.router.route("/cats/{cat_id}") - def two(cat_id: int) -> Optional[Cat]: - ... + def two(cat_id: int) -> Optional[Cat]: ... @app.router.route("/cats_alt/{cat_id}") - def three(cat_id: int) -> Cat: - ... + def three(cat_id: int) -> Cat: ... @app.router.route("/cats_value_pattern/{uuid:cat_id}") - def four(cat_id: UUID) -> Cat: - ... + def four(cat_id: UUID) -> Cat: ... docs.bind_app(app) await app.start() @@ -2599,8 +2575,7 @@ async def test_handles_from_form_docs(docs: OpenAPIHandler, serializer: Serializ app = get_app() @app.router.post("/foo") - def one(data: FromForm[CreateFooInput]) -> Foo: - ... + def one(data: FromForm[CreateFooInput]) -> Foo: ... docs.bind_app(app) await app.start() @@ -2687,12 +2662,10 @@ async def test_websockets_routes_are_ignored( app = get_app() @app.router.post("/foo") - def one(data: FromForm[CreateFooInput]) -> Foo: - ... + def one(data: FromForm[CreateFooInput]) -> Foo: ... @app.router.ws("/ws") - def websocket_route() -> None: - ... + def websocket_route() -> None: ... docs.bind_app(app) await app.start() @@ -3461,8 +3434,7 @@ async def test_any_of_dataclasses(docs: OpenAPIHandler, serializer: Serializer): docs.bind_app(app) @app.router.post("/one") - def one(data: AnyOfTestClass) -> AnyOfResponseTestClass: - ... + def one(data: AnyOfTestClass) -> AnyOfResponseTestClass: ... await app.start() @@ -3534,8 +3506,7 @@ async def test_any_of_pydantic_models(docs: OpenAPIHandler, serializer: Serializ docs.bind_app(app) @app.router.post("/one") - def one(data: AnyOfTestClassPyd) -> AnyOfResponseTestClassPyd: - ... + def one(data: AnyOfTestClassPyd) -> AnyOfResponseTestClassPyd: ... await app.start() diff --git a/tests/test_pathutils.py b/tests/test_pathutils.py index c64e4cf1..2c9d5158 100644 --- a/tests/test_pathutils.py +++ b/tests/test_pathutils.py @@ -30,9 +30,11 @@ def test_get_file_extension_from_name(full_path, expected_result): ("example.png", "image/png"), ( "example.js", - "text/javascript" - if sys.version_info >= (3, 12) - else "application/javascript", + ( + "text/javascript" + if sys.version_info >= (3, 12) + else "application/javascript" + ), ), ("example.json", "application/json"), ("example.woff2", "font/woff2"), diff --git a/tests/test_router.py b/tests/test_router.py index 6b8c29f6..f534f8ef 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -315,11 +315,9 @@ def home(): def test_router_match_any_by_extension(): router = Router() - def a(): - ... + def a(): ... - def b(): - ... + def b(): ... router.add_get("/a/*.js", a) router.add_get("/b/*.css", b) @@ -341,17 +339,13 @@ def b(): def test_router_match_any_below(): router = Router() - def a(): - ... + def a(): ... - def b(): - ... + def b(): ... - def c(): - ... + def c(): ... - def d(): - ... + def d(): ... router.add_get("/a/*", a) router.add_get("/b/*", b) @@ -404,32 +398,23 @@ def d(): def test_router_match_among_many(): router = Router() - def home(): - ... + def home(): ... - def home_verbose(): - ... + def home_verbose(): ... - def home_options(): - ... + def home_options(): ... - def home_connect(): - ... + def home_connect(): ... - def get_foo(): - ... + def get_foo(): ... - def create_foo(): - ... + def create_foo(): ... - def patch_foo(): - ... + def patch_foo(): ... - def delete_foo(): - ... + def delete_foo(): ... - def ws(): - ... + def ws(): ... router.add_trace("/", home_verbose) router.add_options("/", home_options) @@ -484,11 +469,9 @@ def ws(): def test_router_match_ws_get_sharing_path(): router = Router() - def home(): - ... + def home(): ... - def ws(): - ... + def ws(): ... router.add_get("/", home) router.add_ws("/", ws) @@ -506,36 +489,28 @@ def test_router_match_among_many_decorators(): router = Router() @router.get("/") - def home(): - ... + def home(): ... @router.trace("/") - def home_verbose(): - ... + def home_verbose(): ... @router.options("/") - def home_options(): - ... + def home_options(): ... @router.connect("/") - def home_connect(): - ... + def home_connect(): ... @router.get("/foo") - def get_foo(): - ... + def get_foo(): ... @router.post("/foo") - def create_foo(): - ... + def create_foo(): ... @router.patch("/foo") - def patch_foo(): - ... + def patch_foo(): ... @router.delete("/foo") - def delete_foo(): - ... + def delete_foo(): ... m = router.get_match_by_method_and_path(RouteMethod.GET, b"/") assert m is not None @@ -576,11 +551,9 @@ def delete_foo(): def test_router_match_with_trailing_slash(): router = Router() - def get_foo(): - ... + def get_foo(): ... - def create_foo(): - ... + def create_foo(): ... router.add_get("/foo", get_foo) router.add_post("/foo", create_foo) @@ -659,11 +632,9 @@ def __call__(self): def test_duplicate_pattern_raises(first_route, second_route): router = Router() - def home(): - ... + def home(): ... - def another(): - ... + def another(): ... router.add_get(first_route, home) @@ -674,11 +645,9 @@ def another(): def test_duplicate_pattern_star_raises(): router = Router() - def home(): - ... + def home(): ... - def another(): - ... + def another(): ... router.add_get("*", home) @@ -689,8 +658,7 @@ def another(): def test_more_than_one_star_raises(): router = Router() - def home(): - ... + def home(): ... with pytest.raises(RouteException): router.add_get("*/*", home) @@ -700,12 +668,10 @@ def test_automatic_pattern_with_ellipsis(): router = Router() @router.get(...) - def home(): - ... + def home(): ... @router.get(...) - def another(): - ... + def another(): ... match = router.get_match_by_method_and_path("GET", "/") assert match is None @@ -725,8 +691,7 @@ def test_automatic_pattern_with_ellipsis_name_normalization(): router = Router() @router.get(...) - def hello_world(): - ... + def hello_world(): ... match = router.get_match_by_method_and_path("GET", "/hello_world") @@ -742,8 +707,7 @@ def test_automatic_pattern_with_ellipsis_index_name(): router = Router() @router.get(...) - def index(): - ... + def index(): ... match = router.get_match_by_method_and_path("GET", "/") @@ -755,16 +719,13 @@ def test_router_iterable(): router = Router() @router.get("/") - def home(): - ... + def home(): ... @router.trace("/") - def home_verbose(): - ... + def home_verbose(): ... @router.options("/") - def home_options(): - ... + def home_options(): ... routes = list(router) assert len(routes) == 3 @@ -774,8 +735,7 @@ def home_options(): for route in routes: assert route.handler in handlers - def fallback(): - ... + def fallback(): ... router.fallback = fallback @@ -874,8 +834,7 @@ def test_route_filter_headers_1(): router = Router(headers={"X-Test": "Test"}) @router.get("/") - def home(): - ... + def home(): ... match = router.get_match(Request("GET", b"/", [])) @@ -894,11 +853,9 @@ def test_route_filter_fallback(): router = Router(headers={"X-Test": "Test"}) @router.get("/") - def home(): - ... + def home(): ... - def fallback(): - ... + def fallback(): ... router.fallback = fallback match = router.get_match(Request("GET", b"/", [])) @@ -912,12 +869,10 @@ def test_route_filter_headers_2(): router = Router(sub_routers=[test_router]) @router.get("/") - def home(): - ... + def home(): ... @test_router.get("/") - def test_home(): - ... + def test_home(): ... match = router.get_match(Request("GET", b"/", [])) @@ -934,8 +889,7 @@ def test_route_filter_params_1(): router = Router(params={"foo": "1"}) @router.get("/") - def home(): - ... + def home(): ... match = router.get_match(Request("GET", b"/", [])) @@ -954,8 +908,7 @@ def test_route_filter_host(): router = Router(host="neoteroi.dev") @router.get("/") - def test_home(): - ... + def test_home(): ... match = router.get_match(Request("GET", b"/", [(b"host", b"localhost")])) @@ -981,8 +934,7 @@ def handle(self, request: Request) -> bool: router = Router(filters=[TimeFilter()]) @router.get("/") - def test_home(): - ... + def test_home(): ... for i in range(5): match = router.get_match(Request("GET", b"/", [])) @@ -1017,20 +969,16 @@ def test_sub_routers_iter(): router = Router(sub_routers=[test_router]) @router.get("/") - def home(): - ... + def home(): ... @router.post("/foo") - def post_foo(): - ... + def post_foo(): ... @test_router.get("/") - def test_home(): - ... + def test_home(): ... @test_router.post("/cats") - def post_cat(): - ... + def post_cat(): ... routes = list(router) assert len(routes) == 4 @@ -1043,20 +991,16 @@ def test_sub_routers_sort(): router = Router(sub_routers=[test_router]) @router.get("/") - def home(): - ... + def home(): ... @router.post("/foo") - def post_foo(): - ... + def post_foo(): ... @test_router.get("/") - def test_home(): - ... + def test_home(): ... @test_router.post("/cats") - def post_cat(): - ... + def post_cat(): ... router.sort_routes() @@ -1075,20 +1019,16 @@ def test_routes_with_filters_can_have_duplicates(): router = Router(sub_routers=[test_router]) @router.get("/") - def home(): - ... + def home(): ... @router.post("/foo") - def post_foo(): - ... + def post_foo(): ... @test_router.get("/") - def test_home(): - ... + def test_home(): ... @test_router.post("/cats") - def post_cat(): - ... + def post_cat(): ... routes = list(router) assert len(routes) == 4