From 675579699422680607108a7dd68c85ec5284220c Mon Sep 17 00:00:00 2001 From: John Parton Date: Tue, 29 Aug 2023 18:43:24 -0500 Subject: [PATCH] Remove chardet/charset-normalizer. Add fallback_charset_resolver ClientSession parameter. (#7561) Co-authored-by: Sam Bull --- .mypy.ini | 3 - CHANGES/7561.feature | 2 + CONTRIBUTORS.txt | 1 + README.rst | 6 +- aiohttp/client.py | 5 ++ aiohttp/client_reqrep.py | 52 ++++++++-------- .../cchardet-unmaintained-admonition.rst | 5 -- docs/client_advanced.rst | 30 ++++++++++ docs/client_reference.rst | 59 +++++++------------ docs/glossary.rst | 16 ----- docs/index.rst | 24 +------- requirements/base.txt | 8 +-- requirements/constraints.txt | 6 -- requirements/dev.txt | 6 -- requirements/doc-spelling.txt | 2 - requirements/doc.txt | 2 - requirements/runtime-deps.in | 2 - requirements/runtime-deps.txt | 8 +-- requirements/test.txt | 6 -- setup.cfg | 3 - tests/test_client_response.py | 44 +++----------- 21 files changed, 103 insertions(+), 187 deletions(-) create mode 100644 CHANGES/7561.feature delete mode 100644 docs/_snippets/cchardet-unmaintained-admonition.rst diff --git a/.mypy.ini b/.mypy.ini index 1841dd38a3e..103f1a601a1 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -35,9 +35,6 @@ ignore_missing_imports = True [mypy-brotli] ignore_missing_imports = True -[mypy-cchardet] -ignore_missing_imports = True - [mypy-gunicorn.*] ignore_missing_imports = True diff --git a/CHANGES/7561.feature b/CHANGES/7561.feature new file mode 100644 index 00000000000..a57914ff2a3 --- /dev/null +++ b/CHANGES/7561.feature @@ -0,0 +1,2 @@ +Replace automatic character set detection with a `fallback_charset_resolver` parameter +in `ClientSession` to allow user-supplied character set detection functions. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 503b7f129cb..c35444e3f53 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -175,6 +175,7 @@ Jesus Cea Jian Zeng Jinkyu Yi Joel Watts +John Parton Jon Nabozny Jonas Krüger Svensson Jonas Obrist diff --git a/README.rst b/README.rst index 5436adb6834..875b8fc7196 100644 --- a/README.rst +++ b/README.rst @@ -162,21 +162,17 @@ Requirements ============ - async-timeout_ -- charset-normalizer_ - multidict_ - yarl_ - frozenlist_ -Optionally you may install the cChardet_ and aiodns_ libraries (highly -recommended for sake of speed). +Optionally you may install the aiodns_ library (highly recommended for sake of speed). -.. _charset-normalizer: https://pypi.org/project/charset-normalizer .. _aiodns: https://pypi.python.org/pypi/aiodns .. _multidict: https://pypi.python.org/pypi/multidict .. _frozenlist: https://pypi.org/project/frozenlist/ .. _yarl: https://pypi.python.org/pypi/yarl .. _async-timeout: https://pypi.python.org/pypi/async_timeout -.. _cChardet: https://pypi.python.org/pypi/cchardet License ======= diff --git a/aiohttp/client.py b/aiohttp/client.py index 314e51b0190..1d9d9fe94d1 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -162,6 +162,7 @@ class ClientTimeout: DEFAULT_TIMEOUT: Final[ClientTimeout] = ClientTimeout(total=5 * 60) _RetType = TypeVar("_RetType") +_CharsetResolver = Callable[[ClientResponse, bytes], str] @final @@ -192,6 +193,7 @@ class ClientSession: "_read_bufsize", "_max_line_size", "_max_field_size", + "_resolve_charset", ) def __init__( @@ -221,6 +223,7 @@ def __init__( read_bufsize: int = 2**16, max_line_size: int = 8190, max_field_size: int = 8190, + fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8", ) -> None: if base_url is None or isinstance(base_url, URL): self._base_url: Optional[URL] = base_url @@ -291,6 +294,8 @@ def __init__( for trace_config in self._trace_configs: trace_config.freeze() + self._resolve_charset = fallback_charset_resolver + def __init_subclass__(cls: Type["ClientSession"]) -> None: raise TypeError( "Inheritance class {} from ClientSession " diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index c9864b3417e..c97fd753624 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -14,6 +14,7 @@ from typing import ( TYPE_CHECKING, Any, + Callable, Dict, Iterable, List, @@ -73,11 +74,6 @@ ssl = None # type: ignore[assignment] SSLContext = object # type: ignore[misc,assignment] -try: - import cchardet as chardet -except ImportError: # pragma: no cover - import charset_normalizer as chardet - __all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint") @@ -686,7 +682,7 @@ class ClientResponse(HeadersMixin): _raw_headers: RawHeaders = None # type: ignore[assignment] _connection = None # current connection - _source_traceback = None + _source_traceback: Optional[traceback.StackSummary] = None # set up by ClientRequest after ClientResponse object creation # post-init stage allows to not change ctor signature _closed = True # to allow __del__ for non-initialized properly response @@ -725,6 +721,15 @@ def __init__( self._loop = loop # store a reference to session #1985 self._session: Optional[ClientSession] = session + # Save reference to _resolve_charset, so that get_encoding() will still + # work after the response has finished reading the body. + if session is None: + # TODO: Fix session=None in tests (see ClientRequest.__init__). + self._resolve_charset: Callable[ + ["ClientResponse", bytes], str + ] = lambda *_: "utf-8" + else: + self._resolve_charset = session._resolve_charset if loop.get_debug(): self._source_traceback = traceback.extract_stack(sys._getframe(1)) @@ -1012,27 +1017,22 @@ def get_encoding(self) -> str: encoding = mimetype.parameters.get("charset") if encoding: - try: - codecs.lookup(encoding) - except LookupError: - encoding = None - if not encoding: - if mimetype.type == "application" and ( - mimetype.subtype == "json" or mimetype.subtype == "rdap" - ): - # RFC 7159 states that the default encoding is UTF-8. - # RFC 7483 defines application/rdap+json - encoding = "utf-8" - elif self._body is None: - raise RuntimeError( - "Cannot guess the encoding of " "a not yet read body" - ) - else: - encoding = chardet.detect(self._body)["encoding"] - if not encoding: - encoding = "utf-8" + with contextlib.suppress(LookupError): + return codecs.lookup(encoding).name + + if mimetype.type == "application" and ( + mimetype.subtype == "json" or mimetype.subtype == "rdap" + ): + # RFC 7159 states that the default encoding is UTF-8. + # RFC 7483 defines application/rdap+json + return "utf-8" + + if self._body is None: + raise RuntimeError( + "Cannot compute fallback encoding of a not yet read body" + ) - return encoding + return self._resolve_charset(self, self._body) async def text(self, encoding: Optional[str] = None, errors: str = "strict") -> str: """Read response payload and decode.""" diff --git a/docs/_snippets/cchardet-unmaintained-admonition.rst b/docs/_snippets/cchardet-unmaintained-admonition.rst deleted file mode 100644 index ec290e0e954..00000000000 --- a/docs/_snippets/cchardet-unmaintained-admonition.rst +++ /dev/null @@ -1,5 +0,0 @@ -.. warning:: - - Note that the :term:`cchardet` project is known not to support - Python 3.10 or higher. See :issue:`6819` and - :gh:`PyYoshi/cChardet/issues/77` for more details. diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index 6200c79b7ad..9e4db7fe23c 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -740,3 +740,33 @@ HTTP Pipelining --------------- aiohttp does not support HTTP/HTTPS pipelining. + + +Character Set Detection +----------------------- + +If you encounter a :exc:`UnicodeDecodeError` when using :meth:`ClientResponse.text()` +this may be because the response does not include the charset needed +to decode the body. + +If you know the correct encoding for a request, you can simply specify +the encoding as a parameter (e.g. ``resp.text("windows-1252")``). + +Alternatively, :class:`ClientSession` accepts a ``fallback_charset_resolver`` parameter which +can be used to introduce charset guessing functionality. When a charset is not found +in the Content-Type header, this function will be called to get the charset encoding. For +example, this can be used with the ``chardetng_py`` library.:: + + from chardetng_py import detect + + def charset_resolver(resp: ClientResponse, body: bytes) -> str: + tld = resp.url.host.rsplit(".", maxsplit=1)[-1] + return detect(body, allow_utf8=True, tld=tld) + + ClientSession(fallback_charset_resolver=charset_resolver) + +Or, if ``chardetng_py`` doesn't work for you, then ``charset-normalizer`` is another option:: + + from charset_normalizer import detect + + ClientSession(fallback_charset_resolver=lamba r, b: detect(b)["encoding"] or "utf-8") diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 25468e9aa66..7419fd8735f 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -51,7 +51,8 @@ The client session supports the context manager protocol for self closing. read_bufsize=2**16, \ requote_redirect_url=True, \ trust_env=False, \ - trace_configs=None) + trace_configs=None, \ + fallback_charset_resolver=lambda r, b: "utf-8") The class for creating client sessions and making requests. @@ -208,6 +209,16 @@ The client session supports the context manager protocol for self closing. disabling. See :ref:`aiohttp-client-tracing-reference` for more information. + :param Callable[[ClientResponse,bytes],str] fallback_charset_resolver: + A :term:`callable` that accepts a :class:`ClientResponse` and the + :class:`bytes` contents, and returns a :class:`str` which will be used as + the encoding parameter to :meth:`bytes.decode()`. + + This function will be called when the charset is not known (e.g. not specified in the + Content-Type header). The default function simply defaults to ``utf-8``. + + .. versionadded:: 3.8.6 + .. attribute:: closed ``True`` if the session has been closed, ``False`` otherwise. @@ -1406,12 +1417,8 @@ Response object Read response's body and return decoded :class:`str` using specified *encoding* parameter. - If *encoding* is ``None`` content encoding is autocalculated - using ``Content-Type`` HTTP header and *charset-normalizer* tool if the - header is not provided by server. - - :term:`cchardet` is used with fallback to :term:`charset-normalizer` if - *cchardet* is not available. + If *encoding* is ``None`` content encoding is determined from the + Content-Type header, or using the ``fallback_charset_resolver`` function. Close underlying connection if data reading gets an error, release connection otherwise. @@ -1420,21 +1427,11 @@ Response object ``None`` for encoding autodetection (default). - :return str: decoded *BODY* - - :raise LookupError: if the encoding detected by cchardet is - unknown by Python (e.g. VISCII). - .. note:: + :raises: :exc:`UnicodeDecodeError` if decoding fails. See also + :meth:`get_encoding`. - If response has no ``charset`` info in ``Content-Type`` HTTP - header :term:`cchardet` / :term:`charset-normalizer` is used for - content encoding autodetection. - - It may hurt performance. If page encoding is known passing - explicit *encoding* parameter might help:: - - await resp.text('ISO-8859-1') + :return str: decoded *BODY* .. method:: json(*, encoding=None, loads=json.loads, \ content_type='application/json') @@ -1442,13 +1439,9 @@ Response object Read response's body as *JSON*, return :class:`dict` using specified *encoding* and *loader*. If data is not still available - a ``read`` call will be done, + a ``read`` call will be done. - If *encoding* is ``None`` content encoding is autocalculated - using :term:`cchardet` or :term:`charset-normalizer` as fallback if - *cchardet* is not available. - - if response's `content-type` does not match `content_type` parameter + If response's `content-type` does not match `content_type` parameter :exc:`aiohttp.ContentTypeError` get raised. To disable content type check pass ``None`` value. @@ -1480,17 +1473,9 @@ Response object .. method:: get_encoding() - Automatically detect content encoding using ``charset`` info in - ``Content-Type`` HTTP header. If this info is not exists or there - are no appropriate codecs for encoding then :term:`cchardet` / - :term:`charset-normalizer` is used. - - Beware that it is not always safe to use the result of this function to - decode a response. Some encodings detected by cchardet are not known by - Python (e.g. VISCII). *charset-normalizer* is not concerned by that issue. - - :raise RuntimeError: if called before the body has been read, - for :term:`cchardet` usage + Retrieve content encoding using ``charset`` info in ``Content-Type`` HTTP header. + If no charset is present or the charset is not understood by Python, the + ``fallback_charset_resolver`` function associated with the ``ClientSession`` is called. .. versionadded:: 3.0 diff --git a/docs/glossary.rst b/docs/glossary.rst index 81bfcfa654b..4bfe7c55126 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -45,22 +45,6 @@ Any object that can be called. Use :func:`callable` to check that. - charset-normalizer - - The Real First Universal Charset Detector. - Open, modern and actively maintained alternative to Chardet. - - https://pypi.org/project/charset-normalizer/ - - cchardet - - cChardet is high speed universal character encoding detector - - binding to charsetdetect. - - https://pypi.python.org/pypi/cchardet/ - - .. include:: _snippets/cchardet-unmaintained-admonition.rst - gunicorn Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for diff --git a/docs/index.rst b/docs/index.rst index 4b8a6263a94..62e03c5baa9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,15 +33,6 @@ Library Installation $ pip install aiohttp -You may want to install *optional* :term:`cchardet` library as faster -replacement for :term:`charset-normalizer`: - -.. code-block:: bash - - $ pip install cchardet - -.. include:: _snippets/cchardet-unmaintained-admonition.rst - For speeding up DNS resolving by client API you may install :term:`aiodns` as well. This option is highly recommended: @@ -53,9 +44,9 @@ This option is highly recommended: Installing all speedups in one command -------------------------------------- -The following will get you ``aiohttp`` along with :term:`cchardet`, -:term:`aiodns` and ``Brotli`` in one bundle. No need to type -separate commands anymore! +The following will get you ``aiohttp`` along with :term:`aiodns` and ``Brotli`` in one +bundle. +No need to type separate commands anymore! .. code-block:: bash @@ -157,17 +148,8 @@ Dependencies ============ - *async_timeout* -- *charset-normalizer* - *multidict* - *yarl* -- *Optional* :term:`cchardet` as faster replacement for - :term:`charset-normalizer`. - - Install it explicitly via: - - .. code-block:: bash - - $ pip install cchardet - *Optional* :term:`aiodns` for fast DNS resolving. The library is highly recommended. diff --git a/requirements/base.txt b/requirements/base.txt index 8043da412f0..759344fc2ea 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with python 3.8 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --allow-unsafe --output-file=requirements/base.txt --resolver=backtracking --strip-extras requirements/base.in +# pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in # aiodns==3.0.0 ; sys_platform == "linux" or sys_platform == "darwin" # via -r requirements/runtime-deps.in @@ -12,12 +12,8 @@ async-timeout==4.0.3 ; python_version < "3.11" # via -r requirements/runtime-deps.in brotli==1.0.9 # via -r requirements/runtime-deps.in -cchardet==2.1.7 ; python_version < "3.10" - # via -r requirements/runtime-deps.in cffi==1.15.1 # via pycares -charset-normalizer==3.2.0 - # via -r requirements/runtime-deps.in frozenlist==1.4.0 # via # -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 63db91a290d..04a38ba3bd7 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -26,8 +26,6 @@ brotli==1.0.9 # via -r requirements/runtime-deps.in build==0.10.0 # via pip-tools -cchardet==2.1.7 ; python_version < "3.10" - # via -r requirements/runtime-deps.in certifi==2023.7.22 # via requests cffi==1.15.1 @@ -36,10 +34,6 @@ cffi==1.15.1 # pycares cfgv==3.3.1 # via pre-commit -charset-normalizer==3.2.0 - # via - # -r requirements/runtime-deps.in - # requests cherry-picker==2.1.0 # via -r requirements/dev.in click==8.1.6 diff --git a/requirements/dev.txt b/requirements/dev.txt index 0c53aafc536..2a6d301f9c9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -26,8 +26,6 @@ brotli==1.0.9 # via -r requirements/runtime-deps.in build==0.10.0 # via pip-tools -cchardet==2.1.7 ; python_version < "3.10" - # via -r requirements/runtime-deps.in certifi==2023.7.22 # via requests cffi==1.15.1 @@ -36,10 +34,6 @@ cffi==1.15.1 # pycares cfgv==3.3.1 # via pre-commit -charset-normalizer==3.2.0 - # via - # -r requirements/runtime-deps.in - # requests cherry-picker==2.1.0 # via -r requirements/dev.in click==8.1.6 diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index f286326eb8c..00324dec894 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -14,8 +14,6 @@ blockdiag==3.0.0 # via sphinxcontrib-blockdiag certifi==2023.7.22 # via requests -charset-normalizer==3.2.0 - # via requests click==8.1.6 # via # click-default-group diff --git a/requirements/doc.txt b/requirements/doc.txt index e463a69bf4d..3a780420b46 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -14,8 +14,6 @@ blockdiag==3.0.0 # via sphinxcontrib-blockdiag certifi==2023.7.22 # via requests -charset-normalizer==3.2.0 - # via requests click==8.1.6 # via # click-default-group diff --git a/requirements/runtime-deps.in b/requirements/runtime-deps.in index 3a751d0911f..9450a699447 100644 --- a/requirements/runtime-deps.in +++ b/requirements/runtime-deps.in @@ -1,6 +1,5 @@ # Extracted from `setup.cfg` via `make sync-direct-runtime-deps` -charset-normalizer >=2.0, < 4.0 multidict >=4.5, < 7.0 async-timeout >= 4.0, < 5.0 ; python_version < "3.11" yarl >= 1.0, < 2.0 @@ -8,4 +7,3 @@ frozenlist >= 1.1.1 aiosignal >= 1.1.2 aiodns >= 1.1; sys_platform=="linux" or sys_platform=="darwin" Brotli -cchardet; python_version < "3.10" diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index d0de0b4f094..30729b15d10 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with python 3.8 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # -# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --resolver=backtracking --strip-extras requirements/runtime-deps.in +# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in # aiodns==3.0.0 ; sys_platform == "linux" or sys_platform == "darwin" # via -r requirements/runtime-deps.in @@ -12,12 +12,8 @@ async-timeout==4.0.3 ; python_version < "3.11" # via -r requirements/runtime-deps.in brotli==1.0.9 # via -r requirements/runtime-deps.in -cchardet==2.1.7 ; python_version < "3.10" - # via -r requirements/runtime-deps.in cffi==1.15.1 # via pycares -charset-normalizer==3.2.0 - # via -r requirements/runtime-deps.in frozenlist==1.4.0 # via # -r requirements/runtime-deps.in diff --git a/requirements/test.txt b/requirements/test.txt index ceb71ab1ed8..fd46edff96b 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -12,18 +12,12 @@ async-timeout==4.0.3 ; python_version < "3.11" # via -r requirements/runtime-deps.in brotli==1.0.9 # via -r requirements/runtime-deps.in -cchardet==2.1.7 ; python_version < "3.10" - # via -r requirements/runtime-deps.in certifi==2023.7.22 # via requests cffi==1.15.1 # via # cryptography # pycares -charset-normalizer==3.2.0 - # via - # -r requirements/runtime-deps.in - # requests click==8.1.6 # via # typer diff --git a/setup.cfg b/setup.cfg index 819815cd148..cc3fc71808f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,6 @@ zip_safe = False include_package_data = True install_requires = - charset-normalizer >=2.0, < 4.0 multidict >=4.5, < 7.0 async-timeout >= 4.0, < 5.0 ; python_version < "3.11" yarl >= 1.0, < 2.0 @@ -64,8 +63,6 @@ speedups = # required c-ares (aiodns' backend) will not build on windows aiodns >= 1.1; sys_platform=="linux" or sys_platform=="darwin" Brotli - # cchardet is unmaintained: aio-libs/aiohttp#6819 - cchardet; python_version < "3.10" [options.packages.find] exclude = diff --git a/tests/test_client_response.py b/tests/test_client_response.py index 136d7853424..07eb1e1e747 100644 --- a/tests/test_client_response.py +++ b/tests/test_client_response.py @@ -427,7 +427,11 @@ def side_effect(*args, **kwargs): assert not response.get_encoding.called -async def test_text_detect_encoding(loop: Any, session: Any) -> None: +@pytest.mark.parametrize("content_type", ("text/plain", "text/plain;charset=invalid")) +async def test_text_charset_resolver( + content_type: str, loop: Any, session: Any +) -> None: + session._resolve_charset = lambda r, b: "cp1251" response = ClientResponse( "get", URL("http://def-cl-resp.org"), @@ -445,7 +449,7 @@ def side_effect(*args, **kwargs): fut.set_result('{"тест": "пройден"}'.encode("cp1251")) return fut - response._headers = {"Content-Type": "text/plain"} + response._headers = {"Content-Type": content_type} content = response.content = mock.Mock() content.read.side_effect = side_effect @@ -453,35 +457,7 @@ def side_effect(*args, **kwargs): res = await response.text() assert res == '{"тест": "пройден"}' assert response._connection is None - - -async def test_text_detect_encoding_if_invalid_charset(loop: Any, session: Any) -> None: - response = ClientResponse( - "get", - URL("http://def-cl-resp.org"), - request_info=mock.Mock(), - writer=mock.Mock(), - continue100=None, - timer=TimerNoop(), - traces=[], - loop=loop, - session=session, - ) - - def side_effect(*args, **kwargs): - fut = loop.create_future() - fut.set_result('{"тест": "пройден"}'.encode("cp1251")) - return fut - - response._headers = {"Content-Type": "text/plain;charset=invalid"} - content = response.content = mock.Mock() - content.read.side_effect = side_effect - - await response.read() - res = await response.text() - assert res == '{"тест": "пройден"}' - assert response._connection is None - assert response.get_encoding().lower() in ("windows-1251", "maccyrillic") + assert response.get_encoding() == "cp1251" async def test_get_encoding_body_none(loop: Any, session: Any) -> None: @@ -508,7 +484,7 @@ def side_effect(*args, **kwargs): with pytest.raises( RuntimeError, - match="^Cannot guess the encoding of a not yet read body$", + match="^Cannot compute fallback encoding of a not yet read body$", ): response.get_encoding() assert response.closed @@ -729,9 +705,7 @@ def test_get_encoding_unknown(loop: Any, session: Any) -> None: ) response._headers = {"Content-Type": "application/json"} - with mock.patch("aiohttp.client_reqrep.chardet") as m_chardet: - m_chardet.detect.return_value = {"encoding": None} - assert response.get_encoding() == "utf-8" + assert response.get_encoding() == "utf-8" def test_raise_for_status_2xx() -> None: