Compare commits

..

2 Commits

Author SHA1 Message Date
Kar Petrosyan
f88fa846d9 changelog 2025-06-10 23:13:35 +04:00
Kar Petrosyan
2f737cadc1 feat: add socket_options to Client and AsyncClient classes 2025-06-10 23:03:04 +04:00
25 changed files with 77 additions and 91 deletions

View File

@ -15,9 +15,9 @@ jobs:
steps: steps:
- uses: "actions/checkout@v4" - uses: "actions/checkout@v4"
- uses: "actions/setup-python@v6" - uses: "actions/setup-python@v5"
with: with:
python-version: 3.9 python-version: 3.8
- name: "Install dependencies" - name: "Install dependencies"
run: "scripts/install" run: "scripts/install"
- name: "Build package & docs" - name: "Build package & docs"

View File

@ -14,11 +14,11 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: "actions/checkout@v4" - uses: "actions/checkout@v4"
- uses: "actions/setup-python@v6" - uses: "actions/setup-python@v5"
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
allow-prereleases: true allow-prereleases: true

View File

@ -4,15 +4,9 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [UNRELEASED] ## Development
### Removed * Add `socket_options` to `Client` and `AsyncClient` classes. (#3587)
* Drop support for Python 3.8
### Added
* Expose `FunctionAuth` from the public API. (#3699)
## 0.28.1 (6th December, 2024) ## 0.28.1 (6th December, 2024)

View File

@ -101,7 +101,7 @@ Or, to include the optional HTTP/2 support, use:
$ pip install httpx[http2] $ pip install httpx[http2]
``` ```
HTTPX requires Python 3.9+. HTTPX requires Python 3.8+.
## Documentation ## Documentation

View File

@ -29,7 +29,7 @@ import certifi
import httpx import httpx
import ssl import ssl
# This SSL context is equivalent to the default `verify=True`. # This SSL context is equivelent to the default `verify=True`.
ctx = ssl.create_default_context(cafile=certifi.where()) ctx = ssl.create_default_context(cafile=certifi.where())
client = httpx.Client(verify=ctx) client = httpx.Client(verify=ctx)
``` ```
@ -71,7 +71,19 @@ client = httpx.Client(verify=ctx)
### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR` ### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR`
`httpx` does respect the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables by default. For details, refer to [the section on the environment variables page](../environment_variables.md#ssl_cert_file). Unlike `requests`, the `httpx` package does not automatically pull in [the environment variables `SSL_CERT_FILE` or `SSL_CERT_DIR`](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_default_verify_paths.html). If you want to use these they need to be enabled explicitly.
For example...
```python
# Use `SSL_CERT_FILE` or `SSL_CERT_DIR` if configured.
# Otherwise default to certifi.
ctx = ssl.create_default_context(
cafile=os.environ.get("SSL_CERT_FILE", certifi.where()),
capath=os.environ.get("SSL_CERT_DIR"),
)
client = httpx.Client(verify=ctx)
```
### Making HTTPS requests to a local server ### Making HTTPS requests to a local server

View File

@ -23,7 +23,7 @@ To make asynchronous requests, you'll need an `AsyncClient`.
``` ```
!!! tip !!! tip
Use [IPython](https://ipython.readthedocs.io/en/stable/) or Python 3.9+ with `python -m asyncio` to try this code interactively, as they support executing `async`/`await` expressions in the console. Use [IPython](https://ipython.readthedocs.io/en/stable/) or Python 3.8+ with `python -m asyncio` to try this code interactively, as they support executing `async`/`await` expressions in the console.
## API Differences ## API Differences

View File

@ -226,7 +226,3 @@ For both query params (`params=`) and form data (`data=`), `requests` supports s
In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response. In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response.
If you are looking for more control, consider checking out [Custom Transports](advanced/transports.md#custom-transports). If you are looking for more control, consider checking out [Custom Transports](advanced/transports.md#custom-transports).
## Exceptions and Errors
`requests` exception hierarchy is slightly different to the `httpx` exception hierarchy. `requests` exposes a top level `RequestException`, where as `httpx` exposes a top level `HTTPError`. see the exceptions exposes in requests [here](https://requests.readthedocs.io/en/latest/_modules/requests/exceptions/). See the `httpx` error hierarchy [here](https://www.python-httpx.org/exceptions/).

View File

@ -51,29 +51,3 @@ python -c "import httpx; httpx.get('http://example.com')"
python -c "import httpx; httpx.get('http://127.0.0.1:5000/my-api')" python -c "import httpx; httpx.get('http://127.0.0.1:5000/my-api')"
python -c "import httpx; httpx.get('https://www.python-httpx.org')" python -c "import httpx; httpx.get('https://www.python-httpx.org')"
``` ```
## `SSL_CERT_FILE`
Valid values: a filename
If this environment variable is set then HTTPX will load
CA certificate from the specified file instead of the default
location.
Example:
```console
SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get('https://example.com')"
```
## `SSL_CERT_DIR`
Valid values: a directory following an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html).
If this environment variable is set and the directory follows an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html) (ie. you ran `c_rehash`) then HTTPX will load CA certificates from this directory instead of the default location.
Example:
```console
SSL_CERT_DIR=/path/to/ca-certs/ python -c "import httpx; httpx.get('https://example.com')"
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -145,6 +145,6 @@ To include the optional brotli and zstandard decoders support, use:
$ pip install httpx[brotli,zstd] $ pip install httpx[brotli,zstd]
``` ```
HTTPX requires Python 3.9+ HTTPX requires Python 3.8+
[sync-support]: https://github.com/encode/httpx/issues/572 [sync-support]: https://github.com/encode/httpx/issues/572

View File

@ -20,6 +20,8 @@ httpx.get("https://www.example.com")
Will send debug level output to the console, or wherever `stdout` is directed too... Will send debug level output to the console, or wherever `stdout` is directed too...
``` ```
DEBUG [2024-09-28 17:27:40] httpx - load_ssl_context verify=True cert=None
DEBUG [2024-09-28 17:27:40] httpx - load_verify_locations cafile='/Users/karenpetrosyan/oss/karhttpx/.venv/lib/python3.9/site-packages/certifi/cacert.pem'
DEBUG [2024-09-28 17:27:40] httpcore.connection - connect_tcp.started host='www.example.com' port=443 local_address=None timeout=5.0 socket_options=None DEBUG [2024-09-28 17:27:40] httpcore.connection - connect_tcp.started host='www.example.com' port=443 local_address=None timeout=5.0 socket_options=None
DEBUG [2024-09-28 17:27:41] httpcore.connection - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x101f1e8e0> DEBUG [2024-09-28 17:27:41] httpcore.connection - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x101f1e8e0>
DEBUG [2024-09-28 17:27:41] httpcore.connection - start_tls.started ssl_context=SSLContext(verify=True) server_hostname='www.example.com' timeout=5.0 DEBUG [2024-09-28 17:27:41] httpcore.connection - start_tls.started ssl_context=SSLContext(verify=True) server_hostname='www.example.com' timeout=5.0
@ -78,4 +80,4 @@ logging.config.dictConfig(LOGGING_CONFIG)
httpx.get('https://www.example.com') httpx.get('https://www.example.com')
``` ```
The exact formatting of the debug logging may be subject to change across different versions of `httpx` and `httpcore`. If you need to rely on a particular format it is recommended that you pin installation of these packages to fixed versions. The exact formatting of the debug logging may be subject to change across different versions of `httpx` and `httpcore`. If you need to rely on a particular format it is recommended that you pin installation of these packages to fixed versions.

View File

@ -191,7 +191,7 @@ You can also explicitly set the filename and content type, by using a tuple
of items for the file value: of items for the file value:
```pycon ```pycon
>>> with open('report.xls', 'rb') as report_file: >>> with open('report.xls', 'rb') report_file:
... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')} ... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')}
... r = httpx.post("https://httpbin.org/post", files=files) ... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text) >>> print(r.text)

View File

@ -24,12 +24,6 @@ Provides authentication classes to be used with HTTPX's [authentication paramete
This package adds caching functionality to HTTPX This package adds caching functionality to HTTPX
### httpx-secure
[GitHub](https://github.com/Zaczero/httpx-secure)
Drop-in SSRF protection for httpx with DNS caching and custom validation support.
### httpx-socks ### httpx-socks
[GitHub](https://github.com/romis2012/httpx-socks) [GitHub](https://github.com/romis2012/httpx-socks)

View File

@ -50,7 +50,6 @@ __all__ = [
"DecodingError", "DecodingError",
"delete", "delete",
"DigestAuth", "DigestAuth",
"FunctionAuth",
"get", "get",
"head", "head",
"Headers", "Headers",

View File

@ -16,7 +16,7 @@ if typing.TYPE_CHECKING: # pragma: no cover
from hashlib import _Hash from hashlib import _Hash
__all__ = ["Auth", "BasicAuth", "DigestAuth", "FunctionAuth", "NetRCAuth"] __all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"]
class Auth: class Auth:

View File

@ -29,7 +29,7 @@ from ._exceptions import (
from ._models import Cookies, Headers, Request, Response from ._models import Cookies, Headers, Request, Response
from ._status_codes import codes from ._status_codes import codes
from ._transports.base import AsyncBaseTransport, BaseTransport from ._transports.base import AsyncBaseTransport, BaseTransport
from ._transports.default import AsyncHTTPTransport, HTTPTransport from ._transports.default import SOCKET_OPTION, AsyncHTTPTransport, HTTPTransport
from ._types import ( from ._types import (
AsyncByteStream, AsyncByteStream,
AuthTypes, AuthTypes,
@ -653,6 +653,7 @@ class Client(BaseClient):
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False, follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
max_redirects: int = DEFAULT_MAX_REDIRECTS, max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URL | str = "", base_url: URL | str = "",
@ -693,6 +694,7 @@ class Client(BaseClient):
http2=http2, http2=http2,
limits=limits, limits=limits,
transport=transport, transport=transport,
socket_options=socket_options,
) )
self._mounts: dict[URLPattern, BaseTransport | None] = { self._mounts: dict[URLPattern, BaseTransport | None] = {
URLPattern(key): None URLPattern(key): None
@ -705,6 +707,7 @@ class Client(BaseClient):
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
socket_options=socket_options,
) )
for key, proxy in proxy_map.items() for key, proxy in proxy_map.items()
} }
@ -723,6 +726,7 @@ class Client(BaseClient):
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
transport: BaseTransport | None = None, transport: BaseTransport | None = None,
) -> BaseTransport: ) -> BaseTransport:
if transport is not None: if transport is not None:
@ -735,6 +739,7 @@ class Client(BaseClient):
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
socket_options=socket_options,
) )
def _init_proxy_transport( def _init_proxy_transport(
@ -746,6 +751,7 @@ class Client(BaseClient):
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> BaseTransport: ) -> BaseTransport:
return HTTPTransport( return HTTPTransport(
verify=verify, verify=verify,
@ -755,6 +761,7 @@ class Client(BaseClient):
http2=http2, http2=http2,
limits=limits, limits=limits,
proxy=proxy, proxy=proxy,
socket_options=socket_options,
) )
def _transport_for_url(self, url: URL) -> BaseTransport: def _transport_for_url(self, url: URL) -> BaseTransport:
@ -1366,6 +1373,7 @@ class AsyncClient(BaseClient):
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False, follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
max_redirects: int = DEFAULT_MAX_REDIRECTS, max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URL | str = "", base_url: URL | str = "",
@ -1407,6 +1415,7 @@ class AsyncClient(BaseClient):
http2=http2, http2=http2,
limits=limits, limits=limits,
transport=transport, transport=transport,
socket_options=socket_options,
) )
self._mounts: dict[URLPattern, AsyncBaseTransport | None] = { self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
@ -1420,6 +1429,7 @@ class AsyncClient(BaseClient):
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
socket_options=socket_options,
) )
for key, proxy in proxy_map.items() for key, proxy in proxy_map.items()
} }
@ -1437,6 +1447,7 @@ class AsyncClient(BaseClient):
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
transport: AsyncBaseTransport | None = None, transport: AsyncBaseTransport | None = None,
) -> AsyncBaseTransport: ) -> AsyncBaseTransport:
if transport is not None: if transport is not None:
@ -1449,6 +1460,7 @@ class AsyncClient(BaseClient):
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
socket_options=socket_options,
) )
def _init_proxy_transport( def _init_proxy_transport(
@ -1460,6 +1472,7 @@ class AsyncClient(BaseClient):
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> AsyncBaseTransport: ) -> AsyncBaseTransport:
return AsyncHTTPTransport( return AsyncHTTPTransport(
verify=verify, verify=verify,
@ -1469,6 +1482,7 @@ class AsyncClient(BaseClient):
http2=http2, http2=http2,
limits=limits, limits=limits,
proxy=proxy, proxy=proxy,
socket_options=socket_options,
) )
def _transport_for_url(self, url: URL) -> AsyncBaseTransport: def _transport_for_url(self, url: URL) -> AsyncBaseTransport:

View File

@ -331,7 +331,9 @@ class StreamClosed(StreamError):
""" """
def __init__(self) -> None: def __init__(self) -> None:
message = "Attempted to read or stream content, but the stream has been closed." message = (
"Attempted to read or stream content, but the stream has " "been closed."
)
super().__init__(message) super().__init__(message)

View File

@ -379,7 +379,7 @@ class URL:
if ":" in userinfo: if ":" in userinfo:
# Mask any password component. # Mask any password component.
userinfo = f"{userinfo.split(':')[0]}:[secure]" userinfo = f'{userinfo.split(":")[0]}:[secure]'
authority = "".join( authority = "".join(
[ [

View File

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "httpx" name = "httpx"
description = "The next generation HTTP client." description = "The next generation HTTP client."
license = "BSD-3-Clause" license = "BSD-3-Clause"
requires-python = ">=3.9" requires-python = ">=3.8"
authors = [ authors = [
{ name = "Tom Christie", email = "tom@tomchristie.com" }, { name = "Tom Christie", email = "tom@tomchristie.com" },
] ]
@ -20,6 +20,7 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
@ -43,7 +44,7 @@ brotli = [
cli = [ cli = [
"click==8.*", "click==8.*",
"pygments==2.*", "pygments==2.*",
"rich>=10,<15", "rich>=10,<14",
] ]
http2 = [ http2 = [
"h2>=3,<5", "h2>=3,<5",

View File

@ -11,19 +11,20 @@ chardet==5.2.0
# Documentation # Documentation
mkdocs==1.6.1 mkdocs==1.6.1
mkautodoc==0.2.0 mkautodoc==0.2.0
mkdocs-material==9.6.18 mkdocs-material==9.5.47
# Packaging # Packaging
build==1.3.0 build==1.2.2.post1
twine==6.1.0 twine==6.0.1
# Tests & Linting # Tests & Linting
coverage[toml]==7.10.6 coverage[toml]==7.6.1
cryptography==45.0.7 cryptography==44.0.1
mypy==1.17.1 mypy==1.13.0
pytest==8.4.1 pytest==8.3.4
ruff==0.12.11 ruff==0.8.1
trio==0.31.0 trio==0.27.0
trio-typing==0.10.0 trio-typing==0.10.0
trustme==1.2.1 trustme==1.1.0; python_version < '3.9'
uvicorn==0.35.0 trustme==1.2.0; python_version >= '3.9'
uvicorn==0.32.1

View File

@ -326,7 +326,7 @@ async def test_auth_property() -> None:
async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client:
assert client.auth is None assert client.auth is None
client.auth = ("user", "password123") client.auth = ("user", "password123") # type: ignore
assert isinstance(client.auth, httpx.BasicAuth) assert isinstance(client.auth, httpx.BasicAuth)
url = "https://example.org/" url = "https://example.org/"

View File

@ -3,35 +3,35 @@ import httpx
def test_client_base_url(): def test_client_base_url():
client = httpx.Client() client = httpx.Client()
client.base_url = "https://www.example.org/" client.base_url = "https://www.example.org/" # type: ignore
assert isinstance(client.base_url, httpx.URL) assert isinstance(client.base_url, httpx.URL)
assert client.base_url == "https://www.example.org/" assert client.base_url == "https://www.example.org/"
def test_client_base_url_without_trailing_slash(): def test_client_base_url_without_trailing_slash():
client = httpx.Client() client = httpx.Client()
client.base_url = "https://www.example.org/path" client.base_url = "https://www.example.org/path" # type: ignore
assert isinstance(client.base_url, httpx.URL) assert isinstance(client.base_url, httpx.URL)
assert client.base_url == "https://www.example.org/path/" assert client.base_url == "https://www.example.org/path/"
def test_client_base_url_with_trailing_slash(): def test_client_base_url_with_trailing_slash():
client = httpx.Client() client = httpx.Client()
client.base_url = "https://www.example.org/path/" client.base_url = "https://www.example.org/path/" # type: ignore
assert isinstance(client.base_url, httpx.URL) assert isinstance(client.base_url, httpx.URL)
assert client.base_url == "https://www.example.org/path/" assert client.base_url == "https://www.example.org/path/"
def test_client_headers(): def test_client_headers():
client = httpx.Client() client = httpx.Client()
client.headers = {"a": "b"} client.headers = {"a": "b"} # type: ignore
assert isinstance(client.headers, httpx.Headers) assert isinstance(client.headers, httpx.Headers)
assert client.headers["A"] == "b" assert client.headers["A"] == "b"
def test_client_cookies(): def test_client_cookies():
client = httpx.Client() client = httpx.Client()
client.cookies = {"a": "b"} client.cookies = {"a": "b"} # type: ignore
assert isinstance(client.cookies, httpx.Cookies) assert isinstance(client.cookies, httpx.Cookies)
mycookies = list(client.cookies.jar) mycookies = list(client.cookies.jar)
assert len(mycookies) == 1 assert len(mycookies) == 1
@ -42,7 +42,7 @@ def test_client_timeout():
expected_timeout = 12.0 expected_timeout = 12.0
client = httpx.Client() client = httpx.Client()
client.timeout = expected_timeout client.timeout = expected_timeout # type: ignore
assert isinstance(client.timeout, httpx.Timeout) assert isinstance(client.timeout, httpx.Timeout)
assert client.timeout.connect == expected_timeout assert client.timeout.connect == expected_timeout

View File

@ -17,7 +17,7 @@ def test_client_queryparams_string():
assert client.params["a"] == "b" assert client.params["a"] == "b"
client = httpx.Client() client = httpx.Client()
client.params = "a=b" client.params = "a=b" # type: ignore
assert isinstance(client.params, httpx.QueryParams) assert isinstance(client.params, httpx.QueryParams)
assert client.params["a"] == "b" assert client.params["a"] == "b"

View File

@ -1011,10 +1011,7 @@ def test_response_decode_text_using_autodetect():
assert response.status_code == 200 assert response.status_code == 200
assert response.reason_phrase == "OK" assert response.reason_phrase == "OK"
# The encoded byte string is consistent with either ISO-8859-1 or assert response.encoding == "ISO-8859-1"
# WINDOWS-1252. Versions <6.0 of chardet claim the former, while chardet
# 6.0 detects the latter.
assert response.encoding in ("ISO-8859-1", "WINDOWS-1252")
assert response.text == text assert response.text == text

View File

@ -489,18 +489,18 @@ def test_response_invalid_argument():
def test_ensure_ascii_false_with_french_characters(): def test_ensure_ascii_false_with_french_characters():
data = {"greeting": "Bonjour, ça va ?"} data = {"greeting": "Bonjour, ça va ?"}
response = httpx.Response(200, json=data) response = httpx.Response(200, json=data)
assert "ça va" in response.text, ( assert (
"ensure_ascii=False should preserve French accented characters" "ça va" in response.text
) ), "ensure_ascii=False should preserve French accented characters"
assert response.headers["Content-Type"] == "application/json" assert response.headers["Content-Type"] == "application/json"
def test_separators_for_compact_json(): def test_separators_for_compact_json():
data = {"clé": "valeur", "liste": [1, 2, 3]} data = {"clé": "valeur", "liste": [1, 2, 3]}
response = httpx.Response(200, json=data) response = httpx.Response(200, json=data)
assert response.text == '{"clé":"valeur","liste":[1,2,3]}', ( assert (
"separators=(',', ':') should produce a compact representation" response.text == '{"clé":"valeur","liste":[1,2,3]}'
) ), "separators=(',', ':') should produce a compact representation"
assert response.headers["Content-Type"] == "application/json" assert response.headers["Content-Type"] == "application/json"