Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ jobs:
uses: actions/checkout@v7

- name: Initialize CodeQL
uses: github/codeql-action/init@v4.37.8
uses: github/codeql-action/init@v4.37.9
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql.yml
queries: +security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@v4.37.8
uses: github/codeql-action/autobuild@v4.37.9
if: ${{ matrix.language == 'python' || matrix.language == 'javascript' }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4.37.8
uses: github/codeql-action/analyze@v4.37.9
with:
category: "/language:${{ matrix.language }}"
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ repos:
- flake8-no-implicit-concat==0.3.4
- flake8-requirements==1.7.8
- repo: https://github.com/PyCQA/isort
rev: '9.0.0b5'
rev: '9.0.1'
hooks:
- id: isort
- repo: https://github.com/psf/black-pre-commit-mirror
Expand Down
1 change: 1 addition & 0 deletions CHANGES/13604.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Made Python code more typing-spec compliant -- by :user:`MarcoGorelli`.
4 changes: 4 additions & 0 deletions CHANGES/4099.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``ClientConnectorCertificateError.ssl`` now returns the value passed to the
``ssl`` parameter, matching
:attr:`ClientConnectorError.ssl <aiohttp.ClientConnectorError.ssl>`.
-- by :user:`ArockiaRajamanickam`.
1 change: 1 addition & 0 deletions CHANGES/4559.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a ``text_charset`` parameter to ``FileResponse`` and ``UrlDispatcher.add_static`` -- by :user:`Dreamsorcerer`.
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Anton Kasyanov
Anton Zhdan-Pushkin
Arcadiy Ivanov
Arie Bovenberg
Arockia Rajamanickam
Arseny Timoniq
Arsh Smith
Arshiya Tabasum
Expand Down Expand Up @@ -262,6 +263,7 @@ Manny7717
Manuel Miranda
Marat Sharafutdinov
Marc Mueller
Marco Gorelli
Marco Paolini
Marcus Campbell
Marcus Stojcevich
Expand Down
7 changes: 2 additions & 5 deletions aiohttp/client_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,13 +382,10 @@ def host(self) -> str:
def port(self) -> int | None:
return self._conn_key.port

@property
def ssl(self) -> bool:
return self._conn_key.is_ssl

def __str__(self) -> str:
return (
f"Cannot connect to host {self.host}:{self.port} ssl:{self.ssl} "
f"Cannot connect to host {self.host}:{self.port} "
f"ssl:{'default' if self.ssl is True else self.ssl} "
f"[{self.certificate_error.__class__.__name__}: "
f"{self.certificate_error.args}]"
)
Expand Down
5 changes: 3 additions & 2 deletions aiohttp/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@

_ApplicationNone = TypeVar("_ApplicationNone", Application, None)
_Request = TypeVar("_Request", bound=BaseRequest)
_ServerRequest = TypeVar("_ServerRequest", bound=BaseRequest)

REUSE_ADDRESS = os.name == "posix" and sys.platform != "cygwin"

Expand Down Expand Up @@ -237,8 +238,8 @@ def __init__(
) -> None: ...
@overload
def __init__(
self: "TestClient[_Request, None]",
server: BaseTestServer[_Request],
self: "TestClient[_ServerRequest, None]",
server: BaseTestServer[_ServerRequest],
*,
cookie_jar: AbstractCookieJar | None = None,
**kwargs: Any,
Expand Down
9 changes: 8 additions & 1 deletion aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,15 @@ def __init__(
status: int = 200,
reason: str | None = None,
headers: LooseHeaders | None = None,
text_charset: str | None = None,
) -> None:
super().__init__(status=status, reason=reason, headers=headers)

self._path = pathlib.Path(path)
self._chunk_size = chunk_size
if text_charset == "":
raise ValueError("text_charset must not be an empty string")
self._text_charset = text_charset

def _seek_and_read(self, fobj: BinaryIO, offset: int, chunk_size: int) -> bytes:
fobj.seek(offset)
Expand Down Expand Up @@ -381,7 +385,10 @@ async def _prepare_open_file(
guesser = CONTENT_TYPES.guess_file_type
else:
guesser = CONTENT_TYPES.guess_type
self.content_type = guesser(self._path)[0] or FALLBACK_CONTENT_TYPE
content_type = guesser(self._path)[0] or FALLBACK_CONTENT_TYPE
self.content_type = content_type
if self._text_charset is not None and content_type.startswith("text/"):
self.charset = self._text_charset

if file_encoding:
self._headers[hdrs.CONTENT_ENCODING] = file_encoding
Expand Down
10 changes: 9 additions & 1 deletion aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ def __init__(
name: str | None = None,
expect_handler: _ExpectHandler | None = None,
chunk_size: int = DEFAULT_CHUNK_SIZE,
text_charset: str | None = None,
show_index: bool = False,
break_symlink_sandbox: bool = False,
append_version: bool = False,
Expand All @@ -522,6 +523,9 @@ def __init__(
self._directory = directory
self._show_index = show_index
self._chunk_size = chunk_size
if text_charset == "":
raise ValueError("text_charset must not be an empty string")
self._text_charset = text_charset
self._break_symlink_sandbox = break_symlink_sandbox
self._expect_handler = expect_handler
self._append_version = append_version
Expand Down Expand Up @@ -667,7 +671,9 @@ def _resolve_path_to_response(self, unresolved_path: Path) -> StreamResponse:
raise HTTPForbidden() from error

# Return the file response, which handles all other checks.
return FileResponse(file_path, chunk_size=self._chunk_size)
return FileResponse(
file_path, chunk_size=self._chunk_size, text_charset=self._text_charset
)

def _directory_as_html(self, dir_path: Path) -> str:
"""returns directory's index as html."""
Expand Down Expand Up @@ -1136,6 +1142,7 @@ def add_static(
name: str | None = None,
expect_handler: _ExpectHandler | None = None,
chunk_size: int = DEFAULT_CHUNK_SIZE,
text_charset: str | None = None,
show_index: bool = False,
break_symlink_sandbox: bool = False,
append_version: bool = False,
Expand All @@ -1155,6 +1162,7 @@ def add_static(
name=name,
expect_handler=expect_handler,
chunk_size=chunk_size,
text_charset=text_charset,
show_index=show_index,
break_symlink_sandbox=break_symlink_sandbox,
append_version=append_version,
Expand Down
6 changes: 6 additions & 0 deletions docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2887,6 +2887,12 @@ Connection errors

Derived from :exc:`ClientOSError`

.. attribute:: ssl

The value passed as the ``ssl`` parameter of the request: an
:class:`ssl.SSLContext`, a :class:`bool`, or a
:class:`~aiohttp.Fingerprint`.

.. class:: ClientConnectorDNSError
:canonical: aiohttp.client_exceptions.ClientConnectorDNSError

Expand Down
26 changes: 25 additions & 1 deletion docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -941,7 +941,7 @@ and :ref:`aiohttp-web-signals` handlers::
:attr:`~aiohttp.StreamResponse.body`, represented as :class:`str`.


.. class:: FileResponse(*, path, chunk_size=256*1024, status=200, reason=None, headers=None)
.. class:: FileResponse(*, path, chunk_size=256*1024, status=200, reason=None, headers=None, text_charset=None)
:canonical: aiohttp.web_fileresponse.FileResponse

The response class used to send files, inherited from :class:`StreamResponse`.
Expand All @@ -966,6 +966,19 @@ and :ref:`aiohttp-web-signals` handlers::
response's ones. The ``Content-Type`` response header
will be overridden if provided.

:param str text_charset: charset to advertise for text files,
e.g. ``"utf-8"``. When the ``Content-Type``
header is guessed from the file extension and
the guessed type is ``text/*``, a ``charset``
parameter with this value is appended to it
(e.g. ``text/plain; charset=utf-8``). Other
media types never take the charset, and an
explicit ``Content-Type`` supplied via *headers*
is never modified. By default (``None``) no
charset is added.

.. versionadded:: 3.15


.. class:: WebSocketResponse(*, timeout=10.0, receive_timeout=None, \
autoclose=True, autoping=True, heartbeat=None, \
Expand Down Expand Up @@ -1895,6 +1908,7 @@ Application and Router

.. method:: add_static(prefix, path, *, name=None, expect_handler=None, \
chunk_size=256*1024, \
text_charset=None, \
show_index=False, \
break_symlink_sandbox=False, \
append_version=False)
Expand Down Expand Up @@ -1939,6 +1953,14 @@ Application and Router
say, 1Mb may increase file downloading
speed but consumes more memory.

:param str text_charset: charset appended to the guessed
``Content-Type`` of ``text/*`` files,
e.g. ``"utf-8"``; passed to
:class:`~aiohttp.web.FileResponse`. By
default (``None``) no charset is added.

.. versionadded:: 3.15

:param bool show_index: flag for allowing to show indexes of a directory,
by default it's not allowed and HTTP/403 will
be returned on directory access.
Expand Down Expand Up @@ -2538,6 +2560,7 @@ The definition is created by functions like :func:`get` or

.. function:: static(prefix, path, *, name=None, expect_handler=None, \
chunk_size=256*1024, \
text_charset=None, \
show_index=False, break_symlink_sandbox=False, \
append_version=False)
:canonical: aiohttp.web_routedef.static
Expand Down Expand Up @@ -2651,6 +2674,7 @@ A routes table definition used for describing routes by decorators

.. method:: static(prefix, path, *, name=None, expect_handler=None, \
chunk_size=256*1024, \
text_charset=None, \
show_index=False, break_symlink_sandbox=False, \
append_version=False)

Expand Down
2 changes: 1 addition & 1 deletion requirements/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ pip-tools==7.6.1
# via -r requirements/dev.in
pkgconfig==1.6.0
# via -r requirements/test-common-base.in
platformdirs==4.11.4
platformdirs==4.11.5
# via virtualenv
pluggy==1.6.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ pip-tools==7.6.1
# via -r requirements/dev.in
pkgconfig==1.6.0
# via -r requirements/test-common-base.in
platformdirs==4.11.4
platformdirs==4.11.5
# via virtualenv
pluggy==1.6.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/lint.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ packaging==26.3
# via pytest
pathspec==1.1.1
# via mypy
platformdirs==4.11.4
platformdirs==4.11.5
# via virtualenv
pluggy==1.6.0
# via pytest
Expand Down
14 changes: 10 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from http.cookies import BaseCookie
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Protocol
from unittest import mock
from uuid import uuid4

Expand Down Expand Up @@ -63,9 +63,17 @@ def pytest_configure(config: pytest.Config) -> None:

if sys.version_info >= (3, 11):
from typing import Unpack

class _RequestMaker(Protocol):
def __call__(
self, method: str, url: URL, **kwargs: Unpack[ClientRequestArgs]
) -> ClientRequest: ...

else:
from typing import Any as Unpack

_RequestMaker = Any


# We require pytest-aiohttp to avoid confusing debugging if it's not installed.
pytest_plugins = ("pytest_aiohttp.plugin", "pytester")
Expand Down Expand Up @@ -444,9 +452,7 @@ def parametrize_zlib_backend(


@pytest.fixture
async def make_client_request() -> (
AsyncIterator[Callable[[str, URL, Unpack[ClientRequestArgs]], ClientRequest]]
):
async def make_client_request() -> AsyncIterator[_RequestMaker]:
"""Fixture to help creating test ClientRequest objects with defaults."""
requests: list[ClientRequest] = []
sessions: list[ClientSession] = []
Expand Down
9 changes: 6 additions & 3 deletions tests/test_benchmarks_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import asyncio
import sys
from collections.abc import Callable
from http.cookies import BaseCookie
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Protocol

import pytest
from multidict import CIMultiDict
Expand All @@ -25,7 +24,11 @@
if sys.version_info >= (3, 11):
from typing import Unpack

_RequestMaker = Callable[[str, URL, Unpack[ClientRequestArgs]], ClientRequest]
class _RequestMaker(Protocol):
def __call__(
self, method: str, url: URL, **kwargs: Unpack[ClientRequestArgs]
) -> ClientRequest: ...

else:
_RequestMaker = Any
if TYPE_CHECKING:
Expand Down
22 changes: 19 additions & 3 deletions tests/test_client_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import errno
import pickle
import ssl
import sys

import pytest
Expand Down Expand Up @@ -176,7 +177,7 @@ def test_ctor(self) -> None:
assert err.certificate_error == certificate_error
assert err.host == "example.com"
assert err.port == 8080
assert err.ssl is False
assert err.ssl is True
if sys.version_info >= (3, 11):
assert_type(err.args, tuple[client_reqrep.ConnectionKey, Exception])

Expand All @@ -192,7 +193,7 @@ def test_pickle(self) -> None:
assert err2.certificate_error.args == ("Bad certificate",)
assert err2.host == "example.com"
assert err2.port == 8080
assert err2.ssl is False
assert err2.ssl is True
assert err2.foo == "bar"

def test_repr(self) -> None:
Expand All @@ -211,7 +212,7 @@ def test_str(self) -> None:
connection_key=self.connection_key, certificate_error=certificate_error
)
assert str(err) == (
"Cannot connect to host example.com:8080 ssl:False"
"Cannot connect to host example.com:8080 ssl:default"
" [Exception: ('Bad certificate',)]"
)

Expand All @@ -224,6 +225,21 @@ def test_oserror(self) -> None:
assert err.errno == 1
assert err.strerror == "Bad certificate"

def test_ssl_is_the_same_as_on_the_base_class(self) -> None:
context = ssl.create_default_context()
connection_key = self.connection_key._replace(is_ssl=True, ssl=context)
certificate_error = Exception("Bad certificate")

err = client.ClientConnectorCertificateError(
connection_key=connection_key, certificate_error=certificate_error
)
base_err = client.ClientConnectorError(
connection_key=connection_key, os_error=OSError(1, "Bad certificate")
)

assert err.ssl is context
assert err.ssl is base_err.ssl


class TestServerDisconnectedError:
def test_ctor(self) -> None:
Expand Down
Loading
Loading