Tighten up top-level API to only expose public API (#608)
* Tighten up top-level API to only expose public API * Leave HTTPProxyMode for backwards compat, raising warnings. * Add missing import
This commit is contained in:
parent
e1f5b8ba57
commit
d15dc0b1f8
@ -243,7 +243,7 @@ Proxies can be configured to have different behavior such as forwarding or tunne
|
||||
```python
|
||||
proxy = httpx.HTTPProxy(
|
||||
proxy_url="https://127.0.0.1",
|
||||
proxy_mode=httpx.HTTPProxyMode.TUNNEL_ONLY
|
||||
proxy_mode="TUNNEL_ONLY" # May be "TUNNEL_ONLY" or "FORWARD_ONLY". Defaults to "DEFAULT".
|
||||
)
|
||||
async with httpx.Client(proxies=proxy) as client:
|
||||
# This request will be tunneled instead of forwarded.
|
||||
|
||||
@ -2,23 +2,11 @@ from .__version__ import __description__, __title__, __version__
|
||||
from .api import delete, get, head, options, patch, post, put, request, stream
|
||||
from .auth import BasicAuth, DigestAuth
|
||||
from .client import Client
|
||||
from .concurrency.asyncio import AsyncioBackend
|
||||
from .concurrency.base import BasePoolSemaphore, BaseSocketStream, ConcurrencyBackend
|
||||
from .config import (
|
||||
USER_AGENT,
|
||||
CertTypes,
|
||||
PoolLimits,
|
||||
SSLConfig,
|
||||
Timeout,
|
||||
TimeoutConfig,
|
||||
TimeoutTypes,
|
||||
VerifyTypes,
|
||||
)
|
||||
from .dispatch.base import Dispatcher
|
||||
from .dispatch.connection import HTTPConnection
|
||||
from .dispatch.connection_pool import ConnectionPool
|
||||
from .config import TimeoutConfig # For 0.8 backwards compat.
|
||||
from .config import PoolLimits, Timeout
|
||||
from .dispatch.proxy_http import HTTPProxy, HTTPProxyMode
|
||||
from .exceptions import (
|
||||
ConnectionClosed,
|
||||
ConnectTimeout,
|
||||
CookieConflict,
|
||||
DecodingError,
|
||||
@ -38,23 +26,7 @@ from .exceptions import (
|
||||
TooManyRedirects,
|
||||
WriteTimeout,
|
||||
)
|
||||
from .models import (
|
||||
URL,
|
||||
AuthTypes,
|
||||
Cookies,
|
||||
CookieTypes,
|
||||
Headers,
|
||||
HeaderTypes,
|
||||
Origin,
|
||||
QueryParams,
|
||||
QueryParamTypes,
|
||||
Request,
|
||||
RequestData,
|
||||
RequestFiles,
|
||||
Response,
|
||||
ResponseContent,
|
||||
URLTypes,
|
||||
)
|
||||
from .models import URL, Cookies, Headers, Origin, QueryParams, Request, Response
|
||||
from .status_codes import StatusCode, codes
|
||||
|
||||
__all__ = [
|
||||
@ -71,24 +43,18 @@ __all__ = [
|
||||
"put",
|
||||
"request",
|
||||
"stream",
|
||||
"codes",
|
||||
"BasicAuth",
|
||||
"Client",
|
||||
"DigestAuth",
|
||||
"AsyncioBackend",
|
||||
"USER_AGENT",
|
||||
"CertTypes",
|
||||
"PoolLimits",
|
||||
"SSLConfig",
|
||||
"Timeout",
|
||||
"TimeoutConfig",
|
||||
"VerifyTypes",
|
||||
"HTTPConnection",
|
||||
"BasePoolSemaphore",
|
||||
"ConnectionPool",
|
||||
"TimeoutConfig", # For 0.8 backwards compat.
|
||||
"HTTPProxy",
|
||||
"HTTPProxyMode",
|
||||
"HTTPProxyMode", # For 0.8 backwards compat.
|
||||
"ConnectTimeout",
|
||||
"CookieConflict",
|
||||
"ConnectionClosed",
|
||||
"DecodingError",
|
||||
"HTTPError",
|
||||
"InvalidURL",
|
||||
@ -106,25 +72,14 @@ __all__ = [
|
||||
"WriteTimeout",
|
||||
"BaseSocketStream",
|
||||
"ConcurrencyBackend",
|
||||
"Dispatcher",
|
||||
"URL",
|
||||
"URLTypes",
|
||||
"StatusCode",
|
||||
"codes",
|
||||
"TimeoutTypes",
|
||||
"AuthTypes",
|
||||
"Cookies",
|
||||
"CookieTypes",
|
||||
"Headers",
|
||||
"HeaderTypes",
|
||||
"Origin",
|
||||
"QueryParams",
|
||||
"QueryParamTypes",
|
||||
"Request",
|
||||
"RequestData",
|
||||
"TimeoutException",
|
||||
"Response",
|
||||
"ResponseContent",
|
||||
"RequestFiles",
|
||||
"DigestAuth",
|
||||
]
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import enum
|
||||
import typing
|
||||
import warnings
|
||||
from base64 import b64encode
|
||||
|
||||
from ..concurrency.base import ConcurrencyBackend
|
||||
@ -14,11 +15,18 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HTTPProxyMode(enum.Enum):
|
||||
# This enum is pending deprecation in order to reduce API surface area,
|
||||
# but is currently still around for 0.8 backwards compat.
|
||||
DEFAULT = "DEFAULT"
|
||||
FORWARD_ONLY = "FORWARD_ONLY"
|
||||
TUNNEL_ONLY = "TUNNEL_ONLY"
|
||||
|
||||
|
||||
DEFAULT_MODE = "DEFAULT"
|
||||
FORWARD_ONLY = "FORWARD_ONLY"
|
||||
TUNNEL_ONLY = "TUNNEL_ONLY"
|
||||
|
||||
|
||||
class HTTPProxy(ConnectionPool):
|
||||
"""A proxy that sends requests to the recipient server
|
||||
on behalf of the connecting client.
|
||||
@ -29,7 +37,7 @@ class HTTPProxy(ConnectionPool):
|
||||
proxy_url: URLTypes,
|
||||
*,
|
||||
proxy_headers: HeaderTypes = None,
|
||||
proxy_mode: HTTPProxyMode = HTTPProxyMode.DEFAULT,
|
||||
proxy_mode: str = "DEFAULT",
|
||||
verify: VerifyTypes = True,
|
||||
cert: CertTypes = None,
|
||||
trust_env: bool = None,
|
||||
@ -38,6 +46,15 @@ class HTTPProxy(ConnectionPool):
|
||||
backend: typing.Union[str, ConcurrencyBackend] = "auto",
|
||||
):
|
||||
|
||||
if isinstance(proxy_mode, HTTPProxyMode):
|
||||
warnings.warn(
|
||||
"The 'HTTPProxyMode' enum is pending deprecation. "
|
||||
"Use a plain string instead. proxy_mode='FORWARD_ONLY', or "
|
||||
"proxy_mode='TUNNEL_ONLY'."
|
||||
)
|
||||
proxy_mode = proxy_mode.value
|
||||
assert proxy_mode in ("DEFAULT", "FORWARD_ONLY", "TUNNEL_ONLY")
|
||||
|
||||
super(HTTPProxy, self).__init__(
|
||||
verify=verify,
|
||||
cert=cert,
|
||||
@ -162,8 +179,8 @@ class HTTPProxy(ConnectionPool):
|
||||
tunnel all 'HTTPS' requests.
|
||||
"""
|
||||
return (
|
||||
self.proxy_mode == HTTPProxyMode.DEFAULT and not origin.is_ssl
|
||||
) or self.proxy_mode == HTTPProxyMode.FORWARD_ONLY
|
||||
self.proxy_mode == DEFAULT_MODE and not origin.is_ssl
|
||||
) or self.proxy_mode == FORWARD_ONLY
|
||||
|
||||
async def send(
|
||||
self,
|
||||
|
||||
@ -4,18 +4,9 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
URL,
|
||||
CertTypes,
|
||||
Client,
|
||||
DigestAuth,
|
||||
Dispatcher,
|
||||
ProtocolError,
|
||||
Request,
|
||||
Response,
|
||||
TimeoutTypes,
|
||||
VerifyTypes,
|
||||
)
|
||||
from httpx import URL, Client, DigestAuth, ProtocolError, Request, Response
|
||||
from httpx.config import CertTypes, TimeoutTypes, VerifyTypes
|
||||
from httpx.dispatch.base import Dispatcher
|
||||
|
||||
|
||||
class MockDispatch(Dispatcher):
|
||||
|
||||
@ -3,16 +3,9 @@ from http.cookiejar import Cookie, CookieJar
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
CertTypes,
|
||||
Client,
|
||||
Cookies,
|
||||
Dispatcher,
|
||||
Request,
|
||||
Response,
|
||||
TimeoutTypes,
|
||||
VerifyTypes,
|
||||
)
|
||||
from httpx import Client, Cookies, Request, Response
|
||||
from httpx.config import CertTypes, TimeoutTypes, VerifyTypes
|
||||
from httpx.dispatch.base import Dispatcher
|
||||
|
||||
|
||||
class MockDispatch(Dispatcher):
|
||||
|
||||
@ -4,17 +4,9 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
CertTypes,
|
||||
Client,
|
||||
Dispatcher,
|
||||
Request,
|
||||
Response,
|
||||
TimeoutTypes,
|
||||
VerifyTypes,
|
||||
__version__,
|
||||
models,
|
||||
)
|
||||
from httpx import Client, Headers, Request, Response, __version__
|
||||
from httpx.config import CertTypes, TimeoutTypes, VerifyTypes
|
||||
from httpx.dispatch.base import Dispatcher
|
||||
|
||||
|
||||
class MockDispatch(Dispatcher):
|
||||
@ -132,7 +124,7 @@ async def test_header_update():
|
||||
|
||||
|
||||
def test_header_does_not_exist():
|
||||
headers = models.Headers({"foo": "bar"})
|
||||
headers = Headers({"foo": "bar"})
|
||||
with pytest.raises(KeyError):
|
||||
del headers["baz"]
|
||||
|
||||
|
||||
@ -40,7 +40,6 @@ def test_proxies_has_same_properties_as_dispatch():
|
||||
pool = client.dispatch
|
||||
proxy = client.proxies["all"]
|
||||
|
||||
assert isinstance(pool, httpx.ConnectionPool)
|
||||
assert isinstance(proxy, httpx.HTTPProxy)
|
||||
|
||||
for prop in [
|
||||
@ -101,7 +100,6 @@ def test_dispatcher_for_request(url, proxies, expected):
|
||||
dispatcher = client.dispatcher_for_url(httpx.URL(url))
|
||||
|
||||
if expected is None:
|
||||
assert isinstance(dispatcher, httpx.ConnectionPool)
|
||||
assert dispatcher is client.dispatch
|
||||
else:
|
||||
assert isinstance(dispatcher, httpx.HTTPProxy)
|
||||
|
||||
@ -2,17 +2,9 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
CertTypes,
|
||||
Client,
|
||||
Dispatcher,
|
||||
QueryParams,
|
||||
Request,
|
||||
Response,
|
||||
TimeoutTypes,
|
||||
VerifyTypes,
|
||||
)
|
||||
from httpx.models import URL
|
||||
from httpx import URL, Client, QueryParams, Request, Response
|
||||
from httpx.config import CertTypes, TimeoutTypes, VerifyTypes
|
||||
from httpx.dispatch.base import Dispatcher
|
||||
|
||||
|
||||
class MockDispatch(Dispatcher):
|
||||
|
||||
@ -5,19 +5,17 @@ import pytest
|
||||
|
||||
from httpx import (
|
||||
URL,
|
||||
CertTypes,
|
||||
Client,
|
||||
Dispatcher,
|
||||
NotRedirectResponse,
|
||||
RedirectBodyUnavailable,
|
||||
RedirectLoop,
|
||||
Request,
|
||||
Response,
|
||||
TimeoutTypes,
|
||||
TooManyRedirects,
|
||||
VerifyTypes,
|
||||
codes,
|
||||
)
|
||||
from httpx.config import CertTypes, TimeoutTypes, VerifyTypes
|
||||
from httpx.dispatch.base import Dispatcher
|
||||
|
||||
|
||||
class MockDispatch(Dispatcher):
|
||||
|
||||
@ -9,7 +9,7 @@ import typing
|
||||
|
||||
import trio
|
||||
|
||||
from httpx import AsyncioBackend
|
||||
from httpx.concurrency.asyncio import AsyncioBackend
|
||||
from httpx.concurrency.trio import TrioBackend
|
||||
|
||||
|
||||
|
||||
@ -17,7 +17,8 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.main import Server
|
||||
|
||||
from httpx import URL, AsyncioBackend
|
||||
from httpx import URL
|
||||
from httpx.concurrency.asyncio import AsyncioBackend
|
||||
from httpx.concurrency.trio import TrioBackend
|
||||
|
||||
ENVIRONMENT_VARIABLES = {
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import httpx
|
||||
from httpx.dispatch.connection_pool import ConnectionPool
|
||||
|
||||
|
||||
async def test_keepalive_connections(server, backend):
|
||||
"""
|
||||
Connections should default to staying in a keep-alive state.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -21,7 +22,7 @@ async def test_differing_connection_keys(server, backend):
|
||||
"""
|
||||
Connections to differing connection keys should result in multiple connections.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -39,7 +40,7 @@ async def test_soft_limit(server, backend):
|
||||
"""
|
||||
pool_limits = httpx.PoolLimits(soft_limit=1)
|
||||
|
||||
async with httpx.ConnectionPool(pool_limits=pool_limits, backend=backend) as http:
|
||||
async with ConnectionPool(pool_limits=pool_limits, backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -55,7 +56,7 @@ async def test_streaming_response_holds_connection(server, backend):
|
||||
"""
|
||||
A streaming request should hold the connection open until the response is read.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
assert len(http.active_connections) == 1
|
||||
assert len(http.keepalive_connections) == 0
|
||||
@ -70,7 +71,7 @@ async def test_multiple_concurrent_connections(server, backend):
|
||||
"""
|
||||
Multiple conncurrent requests should open multiple conncurrent connections.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response_a = await http.request("GET", server.url)
|
||||
assert len(http.active_connections) == 1
|
||||
assert len(http.keepalive_connections) == 0
|
||||
@ -93,7 +94,7 @@ async def test_close_connections(server, backend):
|
||||
Using a `Connection: close` header should close the connection.
|
||||
"""
|
||||
headers = [(b"connection", b"close")]
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url, headers=headers)
|
||||
await response.read()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -104,7 +105,7 @@ async def test_standard_response_close(server, backend):
|
||||
"""
|
||||
A standard close should keep the connection open.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.read()
|
||||
await response.close()
|
||||
@ -116,7 +117,7 @@ async def test_premature_response_close(server, backend):
|
||||
"""
|
||||
A premature close should close the connection.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.close()
|
||||
assert len(http.active_connections) == 0
|
||||
@ -130,7 +131,7 @@ async def test_keepalive_connection_closed_by_server_is_reestablished(
|
||||
Upon keep-alive connection closed by remote a new connection
|
||||
should be reestablished.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.read()
|
||||
|
||||
@ -150,7 +151,7 @@ async def test_keepalive_http2_connection_closed_by_server_is_reestablished(
|
||||
Upon keep-alive connection closed by remote a new connection
|
||||
should be reestablished.
|
||||
"""
|
||||
async with httpx.ConnectionPool(backend=backend) as http:
|
||||
async with ConnectionPool(backend=backend) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
await response.read()
|
||||
|
||||
@ -168,7 +169,7 @@ async def test_connection_closed_free_semaphore_on_acquire(server, restart, back
|
||||
Verify that max_connections semaphore is released
|
||||
properly on a disconnected connection.
|
||||
"""
|
||||
async with httpx.ConnectionPool(
|
||||
async with ConnectionPool(
|
||||
pool_limits=httpx.PoolLimits(hard_limit=1), backend=backend
|
||||
) as http:
|
||||
response = await http.request("GET", server.url)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from httpx import HTTPConnection, exceptions
|
||||
import httpx
|
||||
from httpx.dispatch.connection import HTTPConnection
|
||||
|
||||
|
||||
async def test_get(server, backend):
|
||||
@ -18,7 +19,7 @@ async def test_post(server, backend):
|
||||
|
||||
|
||||
async def test_premature_close(server, backend):
|
||||
with pytest.raises(exceptions.ConnectionClosed):
|
||||
with pytest.raises(httpx.ConnectionClosed):
|
||||
async with HTTPConnection(origin=server.url, backend=backend) as conn:
|
||||
response = await conn.request(
|
||||
"GET", server.url.copy_with(path="/premature_close")
|
||||
|
||||
@ -22,9 +22,7 @@ async def test_proxy_tunnel_success(backend):
|
||||
backend=backend,
|
||||
)
|
||||
async with httpx.HTTPProxy(
|
||||
proxy_url="http://127.0.0.1:8000",
|
||||
backend=raw_io,
|
||||
proxy_mode=httpx.HTTPProxyMode.TUNNEL_ONLY,
|
||||
proxy_url="http://127.0.0.1:8000", backend=raw_io, proxy_mode="TUNNEL_ONLY",
|
||||
) as proxy:
|
||||
response = await proxy.request("GET", "http://example.com")
|
||||
|
||||
@ -60,9 +58,7 @@ async def test_proxy_tunnel_non_2xx_response(backend, status_code):
|
||||
|
||||
with pytest.raises(httpx.ProxyError) as e:
|
||||
async with httpx.HTTPProxy(
|
||||
proxy_url="http://127.0.0.1:8000",
|
||||
backend=raw_io,
|
||||
proxy_mode=httpx.HTTPProxyMode.TUNNEL_ONLY,
|
||||
proxy_url="http://127.0.0.1:8000", backend=raw_io, proxy_mode="TUNNEL_ONLY",
|
||||
) as proxy:
|
||||
await proxy.request("GET", "http://example.com")
|
||||
|
||||
@ -112,9 +108,7 @@ async def test_proxy_tunnel_start_tls(backend):
|
||||
backend=backend,
|
||||
)
|
||||
async with httpx.HTTPProxy(
|
||||
proxy_url="http://127.0.0.1:8000",
|
||||
backend=raw_io,
|
||||
proxy_mode=httpx.HTTPProxyMode.TUNNEL_ONLY,
|
||||
proxy_url="http://127.0.0.1:8000", backend=raw_io, proxy_mode="TUNNEL_ONLY",
|
||||
) as proxy:
|
||||
resp = await proxy.request("GET", "https://example.com")
|
||||
|
||||
@ -150,9 +144,7 @@ async def test_proxy_tunnel_start_tls(backend):
|
||||
assert recv[4].startswith(b"GET /target HTTP/1.1\r\nhost: example.com\r\n")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"proxy_mode", [httpx.HTTPProxyMode.FORWARD_ONLY, httpx.HTTPProxyMode.DEFAULT]
|
||||
)
|
||||
@pytest.mark.parametrize("proxy_mode", ["FORWARD_ONLY", "DEFAULT"])
|
||||
async def test_proxy_forwarding(backend, proxy_mode):
|
||||
raw_io = MockRawSocketBackend(
|
||||
data_to_send=(
|
||||
@ -204,11 +196,11 @@ def test_proxy_repr():
|
||||
proxy = httpx.HTTPProxy(
|
||||
"http://127.0.0.1:1080",
|
||||
proxy_headers={"Custom": "Header"},
|
||||
proxy_mode=httpx.HTTPProxyMode.DEFAULT,
|
||||
proxy_mode="DEFAULT",
|
||||
)
|
||||
|
||||
assert repr(proxy) == (
|
||||
"HTTPProxy(proxy_url=URL('http://127.0.0.1:1080') "
|
||||
"proxy_headers=Headers({'custom': 'Header'}) "
|
||||
"proxy_mode=<HTTPProxyMode.DEFAULT: 'DEFAULT'>)"
|
||||
"proxy_mode='DEFAULT')"
|
||||
)
|
||||
|
||||
@ -5,7 +5,9 @@ import h2.config
|
||||
import h2.connection
|
||||
import h2.events
|
||||
|
||||
from httpx import AsyncioBackend, BaseSocketStream, Request, Timeout
|
||||
from httpx import Request, Timeout
|
||||
from httpx.concurrency.asyncio import AsyncioBackend
|
||||
from httpx.concurrency.base import BaseSocketStream
|
||||
from tests.concurrency import sleep
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from httpx import URL, Origin
|
||||
from httpx.exceptions import InvalidURL
|
||||
from httpx import URL, InvalidURL, Origin
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import pytest
|
||||
import trio
|
||||
|
||||
from httpx import AsyncioBackend, SSLConfig, Timeout
|
||||
from httpx import Timeout
|
||||
from httpx.concurrency.asyncio import AsyncioBackend
|
||||
from httpx.concurrency.trio import TrioBackend
|
||||
from httpx.config import SSLConfig
|
||||
from tests.concurrency import run_concurrently, sleep
|
||||
|
||||
|
||||
|
||||
@ -7,23 +7,24 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
import httpx
|
||||
from httpx.config import SSLConfig
|
||||
|
||||
|
||||
def test_load_ssl_config():
|
||||
ssl_config = httpx.SSLConfig()
|
||||
ssl_config = SSLConfig()
|
||||
context = ssl_config.load_ssl_context()
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
|
||||
assert context.check_hostname is True
|
||||
|
||||
|
||||
def test_load_ssl_config_verify_non_existing_path():
|
||||
ssl_config = httpx.SSLConfig(verify="/path/to/nowhere")
|
||||
ssl_config = SSLConfig(verify="/path/to/nowhere")
|
||||
with pytest.raises(IOError):
|
||||
ssl_config.load_ssl_context()
|
||||
|
||||
|
||||
def test_load_ssl_config_verify_existing_file():
|
||||
ssl_config = httpx.SSLConfig(verify=httpx.config.DEFAULT_CA_BUNDLE_PATH)
|
||||
ssl_config = SSLConfig(verify=httpx.config.DEFAULT_CA_BUNDLE_PATH)
|
||||
context = ssl_config.load_ssl_context()
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
|
||||
assert context.check_hostname is True
|
||||
@ -36,7 +37,7 @@ def test_load_ssl_config_verify_env_file(https_server, ca_cert_pem_file, config)
|
||||
if config.endswith("_FILE")
|
||||
else str(Path(ca_cert_pem_file).parent)
|
||||
)
|
||||
ssl_config = httpx.SSLConfig(trust_env=True)
|
||||
ssl_config = SSLConfig(trust_env=True)
|
||||
context = ssl_config.load_ssl_context()
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
|
||||
assert context.check_hostname is True
|
||||
@ -55,14 +56,14 @@ def test_load_ssl_config_verify_env_file(https_server, ca_cert_pem_file, config)
|
||||
|
||||
def test_load_ssl_config_verify_directory():
|
||||
path = httpx.config.DEFAULT_CA_BUNDLE_PATH.parent
|
||||
ssl_config = httpx.SSLConfig(verify=path)
|
||||
ssl_config = SSLConfig(verify=path)
|
||||
context = ssl_config.load_ssl_context()
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
|
||||
assert context.check_hostname is True
|
||||
|
||||
|
||||
def test_load_ssl_config_cert_and_key(cert_pem_file, cert_private_key_file):
|
||||
ssl_config = httpx.SSLConfig(cert=(cert_pem_file, cert_private_key_file))
|
||||
ssl_config = SSLConfig(cert=(cert_pem_file, cert_private_key_file))
|
||||
context = ssl_config.load_ssl_context()
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
|
||||
assert context.check_hostname is True
|
||||
@ -72,7 +73,7 @@ def test_load_ssl_config_cert_and_key(cert_pem_file, cert_private_key_file):
|
||||
def test_load_ssl_config_cert_and_encrypted_key(
|
||||
cert_pem_file, cert_encrypted_private_key_file, password
|
||||
):
|
||||
ssl_config = httpx.SSLConfig(
|
||||
ssl_config = SSLConfig(
|
||||
cert=(cert_pem_file, cert_encrypted_private_key_file, password)
|
||||
)
|
||||
context = ssl_config.load_ssl_context()
|
||||
@ -83,7 +84,7 @@ def test_load_ssl_config_cert_and_encrypted_key(
|
||||
def test_load_ssl_config_cert_and_key_invalid_password(
|
||||
cert_pem_file, cert_encrypted_private_key_file
|
||||
):
|
||||
ssl_config = httpx.SSLConfig(
|
||||
ssl_config = SSLConfig(
|
||||
cert=(cert_pem_file, cert_encrypted_private_key_file, "password1")
|
||||
)
|
||||
|
||||
@ -92,13 +93,13 @@ def test_load_ssl_config_cert_and_key_invalid_password(
|
||||
|
||||
|
||||
def test_load_ssl_config_cert_without_key_raises(cert_pem_file):
|
||||
ssl_config = httpx.SSLConfig(cert=cert_pem_file)
|
||||
ssl_config = SSLConfig(cert=cert_pem_file)
|
||||
with pytest.raises(ssl.SSLError):
|
||||
ssl_config.load_ssl_context()
|
||||
|
||||
|
||||
def test_load_ssl_config_no_verify():
|
||||
ssl_config = httpx.SSLConfig(verify=False)
|
||||
ssl_config = SSLConfig(verify=False)
|
||||
context = ssl_config.load_ssl_context()
|
||||
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
|
||||
assert context.check_hostname is False
|
||||
@ -106,7 +107,7 @@ def test_load_ssl_config_no_verify():
|
||||
|
||||
def test_load_ssl_context():
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_config = httpx.SSLConfig(verify=ssl_context)
|
||||
ssl_config = SSLConfig(verify=ssl_context)
|
||||
|
||||
assert ssl_config.verify is True
|
||||
assert ssl_config.ssl_context is ssl_context
|
||||
@ -114,20 +115,20 @@ def test_load_ssl_context():
|
||||
|
||||
|
||||
def test_ssl_repr():
|
||||
ssl = httpx.SSLConfig(verify=False)
|
||||
ssl = SSLConfig(verify=False)
|
||||
assert repr(ssl) == "SSLConfig(cert=None, verify=False)"
|
||||
|
||||
|
||||
def test_ssl_eq():
|
||||
ssl = SSLConfig(verify=False)
|
||||
assert ssl == SSLConfig(verify=False)
|
||||
|
||||
|
||||
def test_limits_repr():
|
||||
limits = httpx.PoolLimits(hard_limit=100)
|
||||
assert repr(limits) == "PoolLimits(soft_limit=None, hard_limit=100)"
|
||||
|
||||
|
||||
def test_ssl_eq():
|
||||
ssl = httpx.SSLConfig(verify=False)
|
||||
assert ssl == httpx.SSLConfig(verify=False)
|
||||
|
||||
|
||||
def test_limits_eq():
|
||||
limits = httpx.PoolLimits(hard_limit=100)
|
||||
assert limits == httpx.PoolLimits(hard_limit=100)
|
||||
@ -196,7 +197,7 @@ def test_ssl_config_support_for_keylog_file(tmpdir, monkeypatch): # pragma: noc
|
||||
with monkeypatch.context() as m:
|
||||
m.delenv("SSLKEYLOGFILE", raising=False)
|
||||
|
||||
ssl_config = httpx.SSLConfig(trust_env=True)
|
||||
ssl_config = SSLConfig(trust_env=True)
|
||||
ssl_config.load_ssl_context()
|
||||
|
||||
assert ssl_config.ssl_context.keylog_filename is None
|
||||
@ -206,12 +207,12 @@ def test_ssl_config_support_for_keylog_file(tmpdir, monkeypatch): # pragma: noc
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("SSLKEYLOGFILE", filename)
|
||||
|
||||
ssl_config = httpx.SSLConfig(trust_env=True)
|
||||
ssl_config = SSLConfig(trust_env=True)
|
||||
ssl_config.load_ssl_context()
|
||||
|
||||
assert ssl_config.ssl_context.keylog_filename == filename
|
||||
|
||||
ssl_config = httpx.SSLConfig(trust_env=False)
|
||||
ssl_config = SSLConfig(trust_env=False)
|
||||
ssl_config.load_ssl_context()
|
||||
|
||||
assert ssl_config.ssl_context.keylog_filename is None
|
||||
|
||||
@ -108,7 +108,7 @@ def test_decoding_errors(header_value):
|
||||
headers = [(b"Content-Encoding", header_value)]
|
||||
body = b"test 123"
|
||||
compressed_body = brotli.compress(body)[3:]
|
||||
with pytest.raises(httpx.exceptions.DecodingError):
|
||||
with pytest.raises(httpx.DecodingError):
|
||||
response = httpx.Response(200, headers=headers, content=compressed_body)
|
||||
response.content
|
||||
|
||||
|
||||
@ -6,34 +6,28 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
CertTypes,
|
||||
Client,
|
||||
Dispatcher,
|
||||
Request,
|
||||
Response,
|
||||
TimeoutTypes,
|
||||
VerifyTypes,
|
||||
multipart,
|
||||
)
|
||||
import httpx
|
||||
from httpx.config import CertTypes, TimeoutTypes, VerifyTypes
|
||||
from httpx.dispatch.base import Dispatcher
|
||||
from httpx.multipart import _format_param, multipart_encode
|
||||
|
||||
|
||||
class MockDispatch(Dispatcher):
|
||||
async def send(
|
||||
self,
|
||||
request: Request,
|
||||
request: httpx.Request,
|
||||
verify: VerifyTypes = None,
|
||||
cert: CertTypes = None,
|
||||
timeout: TimeoutTypes = None,
|
||||
) -> Response:
|
||||
) -> httpx.Response:
|
||||
content = await request.read()
|
||||
return Response(200, content=content)
|
||||
return httpx.Response(200, content=content)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("value,output"), (("abc", b"abc"), (b"abc", b"abc")))
|
||||
@pytest.mark.asyncio
|
||||
async def test_multipart(value, output):
|
||||
client = Client(dispatch=MockDispatch())
|
||||
client = httpx.Client(dispatch=MockDispatch())
|
||||
|
||||
# Test with a single-value 'data' argument, and a plain file 'files' argument.
|
||||
data = {"text": value}
|
||||
@ -57,7 +51,7 @@ async def test_multipart(value, output):
|
||||
@pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None))
|
||||
@pytest.mark.asyncio
|
||||
async def test_multipart_invalid_key(key):
|
||||
client = Client(dispatch=MockDispatch())
|
||||
client = httpx.Client(dispatch=MockDispatch())
|
||||
data = {key: "abc"}
|
||||
files = {"file": io.BytesIO(b"<file content>")}
|
||||
with pytest.raises(TypeError) as e:
|
||||
@ -68,7 +62,7 @@ async def test_multipart_invalid_key(key):
|
||||
@pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"}))
|
||||
@pytest.mark.asyncio
|
||||
async def test_multipart_invalid_value(value):
|
||||
client = Client(dispatch=MockDispatch())
|
||||
client = httpx.Client(dispatch=MockDispatch())
|
||||
data = {"text": value}
|
||||
files = {"file": io.BytesIO(b"<file content>")}
|
||||
with pytest.raises(TypeError) as e:
|
||||
@ -78,7 +72,7 @@ async def test_multipart_invalid_value(value):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multipart_file_tuple():
|
||||
client = Client(dispatch=MockDispatch())
|
||||
client = httpx.Client(dispatch=MockDispatch())
|
||||
|
||||
# Test with a list of values 'data' argument, and a tuple style 'files' argument.
|
||||
data = {"text": ["abc"]}
|
||||
@ -111,7 +105,7 @@ def test_multipart_encode():
|
||||
|
||||
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
||||
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
||||
body, content_type = multipart.multipart_encode(data=data, files=files)
|
||||
body, content_type = multipart_encode(data=data, files=files)
|
||||
assert content_type == f"multipart/form-data; boundary={boundary}"
|
||||
assert body == (
|
||||
'--{0}\r\nContent-Disposition: form-data; name="a"\r\n\r\n1\r\n'
|
||||
@ -135,7 +129,7 @@ def test_multipart_encode_files_allows_filenames_as_none():
|
||||
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
||||
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
||||
|
||||
body, content_type = multipart.multipart_encode(data={}, files=files)
|
||||
body, content_type = multipart_encode(data={}, files=files)
|
||||
|
||||
assert content_type == f"multipart/form-data; boundary={boundary}"
|
||||
assert body == (
|
||||
@ -160,7 +154,7 @@ def test_multipart_encode_files_guesses_correct_content_type(
|
||||
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
||||
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
||||
|
||||
body, content_type = multipart.multipart_encode(data={}, files=files)
|
||||
body, content_type = multipart_encode(data={}, files=files)
|
||||
|
||||
assert content_type == f"multipart/form-data; boundary={boundary}"
|
||||
assert body == (
|
||||
@ -176,7 +170,7 @@ def test_multipart_encode_files_allows_str_content():
|
||||
with mock.patch("os.urandom", return_value=os.urandom(16)):
|
||||
boundary = binascii.hexlify(os.urandom(16)).decode("ascii")
|
||||
|
||||
body, content_type = multipart.multipart_encode(data={}, files=files)
|
||||
body, content_type = multipart_encode(data={}, files=files)
|
||||
|
||||
assert content_type == f"multipart/form-data; boundary={boundary}"
|
||||
assert body == (
|
||||
@ -190,17 +184,17 @@ def test_multipart_encode_files_allows_str_content():
|
||||
|
||||
class TestHeaderParamHTML5Formatting:
|
||||
def test_unicode(self):
|
||||
param = multipart._format_param("filename", "n\u00e4me")
|
||||
param = _format_param("filename", "n\u00e4me")
|
||||
assert param == b'filename="n\xc3\xa4me"'
|
||||
|
||||
def test_ascii(self):
|
||||
param = multipart._format_param("filename", b"name")
|
||||
param = _format_param("filename", b"name")
|
||||
assert param == b'filename="name"'
|
||||
|
||||
def test_unicode_escape(self):
|
||||
param = multipart._format_param("filename", "hello\\world\u0022")
|
||||
param = _format_param("filename", "hello\\world\u0022")
|
||||
assert param == b'filename="hello\\\\world%22"'
|
||||
|
||||
def test_unicode_with_control_character(self):
|
||||
param = multipart._format_param("filename", "hello\x1A\x1B\x1C")
|
||||
param = _format_param("filename", "hello\x1A\x1B\x1C")
|
||||
assert param == b'filename="hello%1A\x1B%1C"'
|
||||
|
||||
@ -1,49 +1,41 @@
|
||||
import pytest
|
||||
|
||||
from httpx import (
|
||||
Client,
|
||||
ConnectTimeout,
|
||||
PoolLimits,
|
||||
PoolTimeout,
|
||||
ReadTimeout,
|
||||
Timeout,
|
||||
WriteTimeout,
|
||||
)
|
||||
import httpx
|
||||
|
||||
|
||||
async def test_read_timeout(server, backend):
|
||||
timeout = Timeout(read_timeout=1e-6)
|
||||
timeout = httpx.Timeout(read_timeout=1e-6)
|
||||
|
||||
async with Client(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(ReadTimeout):
|
||||
async with httpx.Client(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(httpx.ReadTimeout):
|
||||
await client.get(server.url.copy_with(path="/slow_response"))
|
||||
|
||||
|
||||
async def test_write_timeout(server, backend):
|
||||
timeout = Timeout(write_timeout=1e-6)
|
||||
timeout = httpx.Timeout(write_timeout=1e-6)
|
||||
|
||||
async with Client(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(WriteTimeout):
|
||||
async with httpx.Client(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(httpx.WriteTimeout):
|
||||
data = b"*" * 1024 * 1024 * 100
|
||||
await client.put(server.url.copy_with(path="/slow_response"), data=data)
|
||||
|
||||
|
||||
async def test_connect_timeout(server, backend):
|
||||
timeout = Timeout(connect_timeout=1e-6)
|
||||
timeout = httpx.Timeout(connect_timeout=1e-6)
|
||||
|
||||
async with Client(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(ConnectTimeout):
|
||||
async with httpx.Client(timeout=timeout, backend=backend) as client:
|
||||
with pytest.raises(httpx.ConnectTimeout):
|
||||
# See https://stackoverflow.com/questions/100841/
|
||||
await client.get("http://10.255.255.1/")
|
||||
|
||||
|
||||
async def test_pool_timeout(server, backend):
|
||||
pool_limits = PoolLimits(hard_limit=1)
|
||||
timeout = Timeout(pool_timeout=1e-4)
|
||||
pool_limits = httpx.PoolLimits(hard_limit=1)
|
||||
timeout = httpx.Timeout(pool_timeout=1e-4)
|
||||
|
||||
async with Client(
|
||||
async with httpx.Client(
|
||||
pool_limits=pool_limits, timeout=timeout, backend=backend
|
||||
) as client:
|
||||
async with client.stream("GET", server.url):
|
||||
with pytest.raises(PoolTimeout):
|
||||
with pytest.raises(httpx.PoolTimeout):
|
||||
await client.get("http://localhost:8000/")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user