From f67acd24dca2208a376c6cbc6603a587457bfc6e Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Tue, 12 May 2026 21:48:28 +0200 Subject: [PATCH] Fix #679 --- CHANGELOG.md | 5 + blacksheep/scribe.pyx | 1 - blacksheep/server/application.py | 15 +- blacksheep/server/authentication/oidc.py | 4 +- blacksheep/server/files/dynamic.py | 13 +- blacksheep/server/openapi/docstrings.py | 2 +- tests/test_auth.py | 10 +- tests/test_bindings.py | 42 ++++-- tests/test_controllers.py | 6 +- tests/test_files_serving.py | 50 +++++++ tests/test_normalization.py | 8 +- tests/test_openapi_docstrings.py | 24 ++-- tests/test_openapi_v3.py | 170 ++++++----------------- tests/test_piccolo_admin_compat.py | 17 +-- 14 files changed, 180 insertions(+), 187 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6e42e6a..1d3b363a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 crashed with `AttributeError` when a handler returned `AsyncIterable[ServerSentEvent]` (or any `collections.abc` async-iterable generic). `_try_get_schema_for_generic` now skips types whose origin has no `__parameters__`. +- Feature [#679](https://github.com/Neoteroi/BlackSheep/issues/679): add `on_response` + async callback to `serve_files()` and `serve_files_dynamic()`. The callback receives + `(request, response)` and can mutate the response in-place (e.g. inject a dynamic + `Content-Security-Policy` header). It is called for every response produced by the + static file handler. ## [2.6.2] - 2026-02-25 :gift: diff --git a/blacksheep/scribe.pyx b/blacksheep/scribe.pyx index 80d96f16..a9d9ccb6 100644 --- a/blacksheep/scribe.pyx +++ b/blacksheep/scribe.pyx @@ -6,7 +6,6 @@ from .cookies cimport Cookie, write_cookie_for_response from .messages cimport Request, Response from .url cimport URL - MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb — Python-accessible cdef int _MAX_RESPONSE_CHUNK_SIZE = MAX_RESPONSE_CHUNK_SIZE diff --git a/blacksheep/server/application.py b/blacksheep/server/application.py index 70d270b3..495b8477 100644 --- a/blacksheep/server/application.py +++ b/blacksheep/server/application.py @@ -49,7 +49,7 @@ from blacksheep.server.env import EnvironmentSettings from blacksheep.server.errors import ServerErrorDetailsHandler from blacksheep.server.files import DefaultFileOptions -from blacksheep.server.files.dynamic import serve_files_dynamic +from blacksheep.server.files.dynamic import ResponseCallback, serve_files_dynamic from blacksheep.server.normalization import normalize_handler, normalize_middleware from blacksheep.server.process import use_shutdown_handler from blacksheep.server.remotes.scheme import configure_scheme_middleware @@ -598,6 +598,7 @@ def serve_files( fallback_document: str | None = None, allow_anonymous: bool = True, default_file_options: DefaultFileOptions | None = None, + on_response: ResponseCallback | None = None, ): """ Configures dynamic file serving from a given folder, relative to the server cwd. @@ -620,6 +621,9 @@ def serve_files( use HTML5 History API for client side routing. default_file_options: Optional options to serve the default file (index.html) + on_response: Optional async callback called for every response produced by + the static file handler, receiving (request, response). Can be used to + inject dynamic response headers such as Content-Security-Policy. """ serve_files_dynamic( self.router, @@ -633,6 +637,7 @@ def serve_files( fallback_document=fallback_document, anonymous_access=allow_anonymous, default_file_options=default_file_options, + on_response=on_response, ) def _apply_middlewares_in_routes(self): @@ -809,11 +814,9 @@ async def _handle_websocket(self, scope, receive, send) -> None: root_path = scope.get("root_path", "") path = scope["path"] if root_path and path.startswith(root_path): - path = path[len(root_path):] or "/" + path = path[len(root_path) :] or "/" - route = self.router.get_match_by_method_and_path( - RouteMethod.GET_WS, path - ) + route = self.router.get_match_by_method_and_path(RouteMethod.GET_WS, path) if route is None: await ws.close() @@ -854,7 +857,7 @@ def instantiate_request(self, scope, receive) -> Request: root_path = scope.get("root_path", "") if root_path and raw_path.startswith(root_path.encode("utf8")): - raw_path = raw_path[len(root_path.encode("utf8")):] or b"/" + raw_path = raw_path[len(root_path.encode("utf8")) :] or b"/" request = Request.incoming( scope["method"], diff --git a/blacksheep/server/authentication/oidc.py b/blacksheep/server/authentication/oidc.py index 047cd180..98a452ab 100644 --- a/blacksheep/server/authentication/oidc.py +++ b/blacksheep/server/authentication/oidc.py @@ -230,9 +230,7 @@ def build_signin_parameters(self, request: Request): if self._settings.use_pkce: code_verifier = generate_pkce_code_verifier() state["code_verifier"] = code_verifier - parameters["code_challenge"] = generate_pkce_code_challenge( - code_verifier - ) + parameters["code_challenge"] = generate_pkce_code_challenge(code_verifier) parameters["code_challenge_method"] = "S256" if self._settings.audience: diff --git a/blacksheep/server/files/dynamic.py b/blacksheep/server/files/dynamic.py index 92c67827..32d3ad82 100644 --- a/blacksheep/server/files/dynamic.py +++ b/blacksheep/server/files/dynamic.py @@ -175,6 +175,9 @@ def get_response_for_resource_path( return get_response_for_file(files_handler, request, resource_path, cache_time) +ResponseCallback = Callable[[Request, Response], Awaitable[None]] + + def get_files_route_handler( files_handler: FilesHandler, source_folder_name: str, @@ -185,6 +188,7 @@ def get_files_route_handler( index_document: str | None, fallback_document: str | None, default_file_options: DefaultFileOptions | None = None, + on_response: ResponseCallback | None = None, ) -> Callable[[Request], Awaitable[Response]]: files_list_html = get_resource_file_content("fileslist.html") source_folder_full_path = os.path.abspath(str(source_folder_name)) @@ -194,7 +198,7 @@ async def static_files_handler(request: Request) -> Response: tail = unquote(request.route_values.get("tail", "")).lstrip("/") try: - return get_response_for_resource_path( + response = get_response_for_resource_path( request, tail, files_list_html, @@ -230,7 +234,10 @@ async def static_files_handler(request: Request) -> Response: if default_file_options and index_document == fallback_document: default_file_options.handle(request, response) - return response + if on_response is not None: + await on_response(request, response) + + return response return static_files_handler @@ -258,6 +265,7 @@ def serve_files_dynamic( fallback_document: str | None, anonymous_access: bool = True, default_file_options: DefaultFileOptions | None = None, + on_response: ResponseCallback | None = None, ) -> None: """ Configures a route to serve files dynamically, using the given files handler and @@ -284,6 +292,7 @@ def serve_files_dynamic( index_document, fallback_document, default_file_options, + on_response, ) if anonymous_access: diff --git a/blacksheep/server/openapi/docstrings.py b/blacksheep/server/openapi/docstrings.py index b59c038c..cadba87c 100644 --- a/blacksheep/server/openapi/docstrings.py +++ b/blacksheep/server/openapi/docstrings.py @@ -168,7 +168,7 @@ def get_parameters_info(self, docstring: str) -> dict[str, ParameterInfo]: # @param int foo: # @param int or None foo: - (*type_parts, name) = param_name.split(" ") + *type_parts, name = param_name.split(" ") types[name] = " ".join(type_parts) param_name = name diff --git a/tests/test_auth.py b/tests/test_auth.py index 0c51cc10..0f547b7d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -304,9 +304,7 @@ async def home(): assert app.response.status == 200 content = await app.response.text() - assert ( - content - == """ + assert content == """ Example. @@ -319,7 +317,6 @@ async def home(): """ - ) async def test_authorization_supports_allow_anonymous(app): @@ -1082,11 +1079,12 @@ async def test_jwt_openid_tokens_handler_authenticate_with_refresh_token( Verifies that JWTOpenIDTokensHandler.authenticate restores the refresh token on the returned identity when the refresh token header is present. """ + from itsdangerous import URLSafeSerializer + from blacksheep.server.authentication.oidc import ( - JWTOpenIDTokensHandler, HTMLStorageType, + JWTOpenIDTokensHandler, ) - from itsdangerous import URLSafeSerializer jwt_auth = JWTBearerAuthentication( valid_audiences=["test-audience"], diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 247869e3..a1a6e45b 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -1265,7 +1265,10 @@ class MultiItem: async def test_multi_format_binder_dispatches_to_json(): binder = MultiFormatBodyBinder( - [JSONBinder(MultiItem, "body", False, True), FormBinder(MultiItem, "body", False, True)], + [ + JSONBinder(MultiItem, "body", False, True), + FormBinder(MultiItem, "body", False, True), + ], MultiItem, "body", required=True, @@ -1282,7 +1285,10 @@ async def test_multi_format_binder_dispatches_to_json(): async def test_multi_format_binder_dispatches_to_form(): binder = MultiFormatBodyBinder( - [JSONBinder(MultiItem, "body", False, True), FormBinder(MultiItem, "body", False, True)], + [ + JSONBinder(MultiItem, "body", False, True), + FormBinder(MultiItem, "body", False, True), + ], MultiItem, "body", required=True, @@ -1298,7 +1304,10 @@ async def test_multi_format_binder_dispatches_to_form(): async def test_multi_format_binder_raises_415_for_unsupported_content_type(): binder = MultiFormatBodyBinder( - [JSONBinder(MultiItem, "body", False, True), FormBinder(MultiItem, "body", False, True)], + [ + JSONBinder(MultiItem, "body", False, True), + FormBinder(MultiItem, "body", False, True), + ], MultiItem, "body", required=True, @@ -1315,7 +1324,10 @@ async def test_multi_format_binder_raises_415_for_unsupported_content_type(): async def test_multi_format_binder_returns_none_when_optional_and_no_match(): binder = MultiFormatBodyBinder( - [JSONBinder(MultiItem, "body", False, False), FormBinder(MultiItem, "body", False, False)], + [ + JSONBinder(MultiItem, "body", False, False), + FormBinder(MultiItem, "body", False, False), + ], MultiItem, "body", required=False, @@ -1365,7 +1377,6 @@ def test_multi_format_binder_content_type_combines_inner(): from blacksheep.contents import Content from blacksheep.server.bindings import FromXML, XMLBinder - XML_ITEM = b"hello7" XML_NESTED = b"1" XML_ATTR = b'attr' @@ -1386,9 +1397,9 @@ async def test_xml_binder_parses_simple_fields(): async def test_xml_binder_accepts_text_xml_content_type(): binder = XMLBinder(MultiItem, "body", required=True) - request = Request( - "POST", b"/", [(b"content-type", b"text/xml")] - ).with_content(Content(b"text/xml", XML_ITEM)) + request = Request("POST", b"/", [(b"content-type", b"text/xml")]).with_content( + Content(b"text/xml", XML_ITEM) + ) result = await binder.get_value(request) assert isinstance(result, MultiItem) @@ -1433,7 +1444,11 @@ def test_xml_binder_rejects_xxe_attack(): b"&xxe;1" ) with pytest.raises( - (defusedxml.DTDForbidden, defusedxml.EntitiesForbidden, defusedxml.ExternalReferenceForbidden) + ( + defusedxml.DTDForbidden, + defusedxml.EntitiesForbidden, + defusedxml.ExternalReferenceForbidden, + ) ): XMLBinder._parse_xml(xxe_payload) @@ -1455,9 +1470,10 @@ def test_xml_binder_rejects_billion_laughs(): def test_element_to_dict_handles_attributes(): - from blacksheep.server.bindings import _element_to_dict import xml.etree.ElementTree as ET + from blacksheep.server.bindings import _element_to_dict + root = ET.fromstring(XML_ATTR) d = _element_to_dict(root) assert d["id"] == "99" @@ -1465,9 +1481,10 @@ def test_element_to_dict_handles_attributes(): def test_element_to_dict_handles_nested(): - from blacksheep.server.bindings import _element_to_dict import xml.etree.ElementTree as ET + from blacksheep.server.bindings import _element_to_dict + root = ET.fromstring(XML_NESTED) d = _element_to_dict(root) assert isinstance(d["inner"], dict) @@ -1475,9 +1492,10 @@ def test_element_to_dict_handles_nested(): def test_element_to_dict_collects_repeated_tags_as_list(): - from blacksheep.server.bindings import _element_to_dict import xml.etree.ElementTree as ET + from blacksheep.server.bindings import _element_to_dict + root = ET.fromstring(XML_LIST) d = _element_to_dict(root) assert d["tag"] == ["a", "b"] diff --git a/tests/test_controllers.py b/tests/test_controllers.py index 644f9618..aef9f52d 100644 --- a/tests/test_controllers.py +++ b/tests/test_controllers.py @@ -284,12 +284,10 @@ class Home(Controller): @get("/") async def index(self): assert isinstance(self, Home) - return self.html( - """ + return self.html("""

Title

Lorem ipsum

- """ - ) + """) await app(get_example_scope("GET", "/"), MockReceive(), MockSend()) diff --git a/tests/test_files_serving.py b/tests/test_files_serving.py index a9df45d8..17817ff0 100644 --- a/tests/test_files_serving.py +++ b/tests/test_files_serving.py @@ -9,6 +9,7 @@ from blacksheep import Application, Request from blacksheep.common.files.asyncfs import FileContext, FilesHandler from blacksheep.exceptions import BadRequest, InvalidArgument +from blacksheep.messages import Response from blacksheep.ranges import Range, RangePart from blacksheep.server.files import ( DefaultFileOptions, @@ -1193,3 +1194,52 @@ async def test_mounted_app_without_child_prefix_returns_404_for_unknown_file(): await parent_app(scope, MockReceive(), send) assert send.messages[0]["status"] == 404 + + +async def test_serve_files_on_response_callback_sets_csp_dynamically( + app: Application, +): + """ + Demonstrates using the async on_response callback on serve_files() to inject a + dynamic Content-Security-Policy header derived from the incoming request. + """ + + async def add_csp(request: Request, response: Response) -> None: + site = request.query.get("site", ["none"])[0] + response.add_header( + b"content-security-policy", + f"frame-ancestors https://{site}.example.com".encode(), + ) + + app.serve_files( + get_folder_path("files2"), + fallback_document="index.html", + on_response=add_csp, + ) + + await app.start() + + # Direct file hit + scope = get_example_scope("GET", "/index.html", query={"site": "acme"}) + await app(scope, MockReceive(), MockSend()) + response = app.response + assert response.status == 200 + assert response.headers[b"content-security-policy"] == ( + b"frame-ancestors https://acme.example.com", + ) + + # Fallback path (SPA route not matching any file) + scope = get_example_scope("GET", "/some-spa-route", query={"site": "beta"}) + await app(scope, MockReceive(), MockSend()) + response = app.response + assert response.status == 200 + assert response.headers[b"content-security-policy"] == ( + b"frame-ancestors https://beta.example.com", + ) + + # Static asset also receives the callback + scope = get_example_scope("GET", "/scripts/main.js") + await app(scope, MockReceive(), MockSend()) + response = app.response + assert response.status == 200 + assert b"content-security-policy" in response.headers diff --git a/tests/test_normalization.py b/tests/test_normalization.py index b69efb25..5d65c890 100644 --- a/tests/test_normalization.py +++ b/tests/test_normalization.py @@ -680,7 +680,13 @@ async def example_2() -> AsyncIterable[str]: from dataclasses import dataclass from blacksheep import FormContent, JSONContent, Request -from blacksheep.server.bindings import FormBinder, FromBody, FromBodyBinder, FromForm, MultiFormatBodyBinder +from blacksheep.server.bindings import ( + FormBinder, + FromBody, + FromBodyBinder, + FromForm, + MultiFormatBodyBinder, +) @dataclass diff --git a/tests/test_openapi_docstrings.py b/tests/test_openapi_docstrings.py index 83ba1ed3..10792e3a 100644 --- a/tests/test_openapi_docstrings.py +++ b/tests/test_openapi_docstrings.py @@ -215,23 +215,19 @@ are separated by blank lines. """, DocstringInfo( - summary=collapse( - """ + summary=collapse(""" This is a paragraph. Paragraphs can span multiple lines, and can contain I{inline markup}. - """ - ), - description=collapse( - """ + """), + description=collapse(""" This is a paragraph. Paragraphs can span multiple lines, and can contain I{inline markup}. This is another paragraph. Paragraphs are separated by blank lines. - """ - ), + """), parameters={}, ), False, @@ -490,13 +486,11 @@ def test_rest_dialect(docstring, expected_info): + "culpa qui officia deserunt mollit anim id est laborum.", parameters={ "lorem": ParameterInfo( - collapse( - """ + collapse(""" A very long description spanning across multiple lines; Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute - irure dolor in reprehenderit.""" - ), + irure dolor in reprehenderit."""), value_type=str, ), }, @@ -772,8 +766,7 @@ def test_googledoc_dialect_warns_about_invalid_parameter(): ), }, return_type=None, - return_description=collapse( - """ + return_description=collapse(""" A dict mapping keys to the corresponding table row data fetched. Each row is represented as a tuple of strings. For example: @@ -785,8 +778,7 @@ def test_googledoc_dialect_warns_about_invalid_parameter(): Returned keys are always bytes. If a key from the keys argument is missing from the dictionary, then that row was not found in the table (and require_all_keys must have been False). - """ - ), + """), ), ), ], diff --git a/tests/test_openapi_v3.py b/tests/test_openapi_v3.py index ee0f2159..abb994aa 100644 --- a/tests/test_openapi_v3.py +++ b/tests/test_openapi_v3.py @@ -482,9 +482,7 @@ def test_dates_handling(docs: OpenAPIHandler, serializer: Serializer): yaml = serializer.to_yaml(docs.generate_documentation(get_app())) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -508,7 +506,6 @@ def test_dates_handling(docs: OpenAPIHandler, serializer: Serializer): nullable: false tags: [] """.strip() - ) def test_register_schema_for_generic_with_list( @@ -523,9 +520,7 @@ def test_register_schema_for_generic_with_list( yaml = serializer.to_yaml(docs.generate_documentation(get_app())) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -569,7 +564,6 @@ def test_register_schema_for_generic_with_list( nullable: false tags: [] """.strip() - ) def test_register_schema_for_multiple_generic_with_list( @@ -587,9 +581,7 @@ def test_register_schema_for_multiple_generic_with_list( yaml = serializer.to_yaml(docs.generate_documentation(get_app())) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -660,7 +652,6 @@ def test_register_schema_for_multiple_generic_with_list( nullable: false tags: [] """.strip() - ) def test_register_schema_for_generic_with_property( @@ -675,9 +666,7 @@ def test_register_schema_for_generic_with_property( yaml = serializer.to_yaml(docs.generate_documentation(get_app())) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -717,7 +706,6 @@ def test_register_schema_for_generic_with_property( nullable: false tags: [] """.strip() - ) def test_register_schema_for_generic_sub_property( @@ -733,9 +721,7 @@ def test_register_schema_for_generic_sub_property( yaml = serializer.to_yaml(docs.generate_documentation(get_app())) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -782,7 +768,6 @@ def test_register_schema_for_generic_sub_property( $ref: '#/components/schemas/ValidatedOfFoo' tags: [] """.strip() - ) async def test_register_schema_for_multi_generic( @@ -798,9 +783,7 @@ def combo_example() -> Combo[Cat, Foo]: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -862,7 +845,6 @@ def combo_example() -> Combo[Cat, Foo]: ... $ref: '#/components/schemas/Foo' tags: [] """.strip() - ) async def test_register_schema_for_generic_with_list_reusing_ref( @@ -883,9 +865,7 @@ def two() -> PaginatedSet[Cat]: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -949,7 +929,6 @@ def two() -> PaginatedSet[Cat]: ... - name: A tag - name: B tag """.strip() - ) def test_get_type_name_raises_for_invalid_object_type(docs: OpenAPIHandler): @@ -970,9 +949,7 @@ def forward_ref_example() -> ForwardRefExample: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1027,7 +1004,6 @@ def forward_ref_example() -> ForwardRefExample: ... $ref: '#/components/schemas/PaginatedSetOfCat' tags: [] """.strip() - ) async def test_handling_of_normal_class(docs: OpenAPIHandler, serializer: Serializer): @@ -1046,9 +1022,7 @@ def plain_class() -> PlainClass: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1067,7 +1041,6 @@ def plain_class() -> PlainClass: ... components: {} tags: [] """.strip() - ) async def test_handling_of_pydantic_class_with_generic( @@ -1084,9 +1057,7 @@ def home() -> PydPaginatedSetOfCat: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if PYDANTIC_VERSION == 1: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1141,11 +1112,8 @@ def home() -> PydPaginatedSetOfCat: ... - total tags: [] """.strip() - ) else: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1200,7 +1168,6 @@ def home() -> PydPaginatedSetOfCat: ... type: object tags: [] """.strip() - ) async def test_handling_of_pydantic_class_with_child_models( @@ -1217,9 +1184,7 @@ def home() -> PydTypeWithChildModels: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if PYDANTIC_VERSION == 1: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1297,11 +1262,8 @@ def home() -> PydTypeWithChildModels: ... - friend tags: [] """.strip() - ) else: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1379,7 +1341,6 @@ def home() -> PydTypeWithChildModels: ... type: object tags: [] """.strip() - ) async def test_handling_of_pydantic_class_in_generic( @@ -1396,9 +1357,7 @@ def home() -> PaginatedSet[PydCat]: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if PYDANTIC_VERSION == 1: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1453,11 +1412,8 @@ def home() -> PaginatedSet[PydCat]: ... nullable: false tags: [] """.strip() - ) else: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1512,7 +1468,6 @@ def home() -> PaginatedSet[PydCat]: ... nullable: false tags: [] """.strip() - ) async def test_handling_of_sequence(docs: OpenAPIHandler, serializer: Serializer): @@ -1526,9 +1481,7 @@ def home() -> Sequence[Cat]: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1564,7 +1517,6 @@ def home() -> Sequence[Cat]: ... nullable: false tags: [] """.strip() - ) @pytest.mark.asyncio @@ -1579,9 +1531,7 @@ def home() -> Mapping[str, Mapping[int, list[Cat]]]: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == r""" + assert yaml.strip() == r""" openapi: 3.1.0 info: title: Example @@ -1623,7 +1573,6 @@ def home() -> Mapping[str, Mapping[int, list[Cat]]]: ... nullable: false tags: [] """.strip() - ) def test_handling_of_generic_with_forward_references(docs: OpenAPIHandler): @@ -1638,9 +1587,7 @@ async def test_cats_api(docs: OpenAPIHandler, serializer: Serializer): yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -1821,7 +1768,6 @@ async def test_cats_api(docs: OpenAPIHandler, serializer: Serializer): nullable: false tags: [] """.strip() - ) async def test_cats_api_capital_operations_ids( @@ -1836,9 +1782,7 @@ async def test_cats_api_capital_operations_ids( yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -2019,7 +1963,6 @@ async def test_cats_api_capital_operations_ids( nullable: false tags: [] """.strip() - ) async def test_cats_annotated_api(docs: OpenAPIHandler, serializer: Serializer): @@ -2029,9 +1972,7 @@ async def test_cats_annotated_api(docs: OpenAPIHandler, serializer: Serializer): yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -2213,7 +2154,6 @@ async def test_cats_annotated_api(docs: OpenAPIHandler, serializer: Serializer): nullable: false tags: [] """.strip() - ) async def test_cats_annotated_api_capital_operations_ids( @@ -2228,9 +2168,7 @@ async def test_cats_annotated_api_capital_operations_ids( yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -2412,7 +2350,6 @@ async def test_cats_annotated_api_capital_operations_ids( nullable: false tags: [] """.strip() - ) async def test_handling_of_pydantic_types(docs: OpenAPIHandler, serializer: Serializer): @@ -2427,9 +2364,7 @@ def home() -> PydExampleWithSpecificTypes: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) if PYDANTIC_VERSION == 1: - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -2461,12 +2396,9 @@ def home() -> PydExampleWithSpecificTypes: ... - url tags: [] """.strip() - ) return - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -2498,7 +2430,6 @@ def home() -> PydExampleWithSpecificTypes: ... type: object tags: [] """.strip() - ) async def test_pydantic_generic(docs: OpenAPIHandler, serializer: Serializer): @@ -2833,9 +2764,7 @@ def auth_home() -> A: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -2884,7 +2813,6 @@ def auth_home() -> A: ... nullable: false tags: [] """.strip() - ) async def test_handles_ref_for_optional_type( @@ -2909,9 +2837,7 @@ def four(cat_id: UUID) -> Cat: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -3018,7 +2944,6 @@ def four(cat_id: UUID) -> Cat: ... nullable: false tags: [] """.strip() - ) async def test_handles_from_form_docs(docs: OpenAPIHandler, serializer: Serializer): @@ -3032,9 +2957,7 @@ def one(data: FromForm[CreateFooInput]) -> Foo: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -3102,7 +3025,6 @@ def one(data: FromForm[CreateFooInput]) -> Foo: ... - 3 tags: [] """.strip() - ) async def test_websockets_routes_are_ignored( @@ -3121,9 +3043,7 @@ def websocket_route() -> None: ... yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example @@ -3191,7 +3111,6 @@ def websocket_route() -> None: ... - 3 tags: [] """.strip() - ) async def test_mount_oad_generation(serializer: Serializer): @@ -3294,9 +3213,7 @@ def delete_parrot(parrot_id: str): yaml = serializer.to_yaml(docs.generate_documentation(parent)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Parent API @@ -3454,7 +3371,6 @@ def delete_parrot(parrot_id: str): nullable: false tags: [] """.strip() - ) async def test_mount_oad_generation_sub_children(serializer: Serializer): @@ -3512,9 +3428,7 @@ def child_3_home(): yaml = serializer.to_yaml(docs.generate_documentation(parent)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Parent API @@ -3550,7 +3464,6 @@ def child_3_home(): tags: - name: A Home """.strip() - ) async def test_sorting_api_controllers_tags(serializer: Serializer): @@ -3642,9 +3555,7 @@ def create_cat(self, cat: Cat) -> None: yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example API @@ -3797,7 +3708,6 @@ def create_cat(self, cat: Cat) -> None: - name: Dogs - name: Parrots """.strip() - ) @dataclass @@ -4233,9 +4143,7 @@ def create_parrot(self, parrot: Parrot) -> None: yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + assert yaml.strip() == """ openapi: 3.1.0 info: title: Example API @@ -4283,7 +4191,6 @@ def create_parrot(self, parrot: Parrot) -> None: tags: - name: TagExample """.strip() - ) async def test_handles_from_files_multipart_docs(): @@ -4728,7 +4635,9 @@ async def test_multi_format_union_body_generates_all_content_types( app = get_app() @app.router.post("/items") - def create_item(data: FromJSON[CreateFooInput] | FromForm[CreateFooInput]) -> Foo: ... + def create_item( + data: FromJSON[CreateFooInput] | FromForm[CreateFooInput], + ) -> Foo: ... docs.bind_app(app) await app.start() @@ -4747,7 +4656,12 @@ async def test_from_body_generates_json_and_form_content_types( docs: OpenAPIHandler, serializer: Serializer, monkeypatch ): """FromBody[T] should document both JSON and form content types.""" - from blacksheep.server.bindings import FormBinder, FromBody, FromBodyBinder, JSONBinder + from blacksheep.server.bindings import ( + FormBinder, + FromBody, + FromBodyBinder, + JSONBinder, + ) monkeypatch.setattr(FromBodyBinder, "binder_types", [JSONBinder, FormBinder]) @@ -4820,7 +4734,9 @@ async def test_json_xml_union_generates_all_content_types( app = get_app() @app.router.post("/items") - def create_item(data: FromJSON[CreateFooInput] | FromXML[CreateFooInput]) -> Foo: ... + def create_item( + data: FromJSON[CreateFooInput] | FromXML[CreateFooInput], + ) -> Foo: ... docs.bind_app(app) await app.start() diff --git a/tests/test_piccolo_admin_compat.py b/tests/test_piccolo_admin_compat.py index 29028edc..be052350 100644 --- a/tests/test_piccolo_admin_compat.py +++ b/tests/test_piccolo_admin_compat.py @@ -16,18 +16,19 @@ The child app is responsible for deriving its own application-relative path by stripping root_path from path. """ -import re -import pytest -from blacksheep.testing.helpers import get_example_scope -from blacksheep.testing.messages import MockReceive, MockSend -from tests.utils.application import FakeApplication +import re +import pytest from starlette.applications import Starlette from starlette.responses import HTMLResponse, PlainTextResponse from starlette.routing import Mount, Route from starlette.staticfiles import StaticFiles +from blacksheep.testing.helpers import get_example_scope +from blacksheep.testing.messages import MockReceive, MockSend +from tests.utils.application import FakeApplication + # --------------------------------------------------------------------------- # Tests: real Starlette (required dependency) # @@ -60,11 +61,11 @@ def starlette_admin_like_app(tmp_path): async def admin_root(request): return HTMLResponse( - '' + "" '' - '' + "" '' - '' + "" ) async def login_endpoint(request):