Skip to content
Closed
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
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ dev = [
"uvicorn>=0.35",
"werkzeug>=3.1.6",
# Linting
"mypy==1.17.1",
"pyright>=1.1.409",
"ruff==0.15.13",
# Packaging
"build==1.3.0",
Expand Down Expand Up @@ -58,9 +58,10 @@ known-first-party = ["httpx2", "httpcore2"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F403", "F405"]

[tool.mypy]
ignore_missing_imports = true
strict = true
[tool.pyright]
include = ["src/httpx2/httpx2", "src/httpcore2/httpcore2"]
exclude = ["src/httpcore2/httpcore2/_sync", "tests/httpcore2/_sync"]
typeCheckingMode = "strict"

[tool.pytest.ini_options]
addopts = "-rxXs --import-mode=importlib"
Expand Down
2 changes: 1 addition & 1 deletion scripts/check
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ set -x

uv lock --check
uv run ruff format --check --diff
uv run mypy $SOURCE_FILES
uv run pyright $SOURCE_FILES
uv run ruff check
uv run python scripts/unasync.py --check
7 changes: 5 additions & 2 deletions src/httpcore2/httpcore2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,25 @@
from ._backends.anyio import AnyIOBackend
except ImportError: # pragma: no cover

class AnyIOBackend: # type: ignore
class _AnyIOBackend(AsyncNetworkBackend):
def __init__(self, *args, **kwargs): # type: ignore
msg = "Attempted to use 'httpcore2.AnyIOBackend' but 'anyio' is not installed."
raise RuntimeError(msg)

AnyIOBackend = _AnyIOBackend


# The 'httpcore2.TrioBackend' class is conditional on 'trio' being installed.
try:
from ._backends.trio import TrioBackend
except ImportError: # pragma: no cover

class TrioBackend: # type: ignore
class _TrioBackend(AsyncNetworkBackend):
def __init__(self, *args, **kwargs): # type: ignore
msg = "Attempted to use 'httpcore2.TrioBackend' but 'trio' is not installed."
raise RuntimeError(msg)

TrioBackend = _TrioBackend

__all__ = [
# top-level requests
Expand Down
2 changes: 1 addition & 1 deletion src/httpcore2/httpcore2/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def stream(
headers: HeaderTypes = None,
content: bytes | typing.Iterator[bytes] | None = None,
extensions: Extensions | None = None,
) -> typing.Iterator[Response]:
) -> typing.Generator[Response]:
"""
Sends an HTTP request, returning the response within a content manager.

Expand Down
8 changes: 6 additions & 2 deletions src/httpcore2/httpcore2/_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,29 @@
from .http2 import AsyncHTTP2Connection
except ImportError: # pragma: no cover

class AsyncHTTP2Connection: # type: ignore
class _AsyncHTTP2Connection(AsyncConnectionInterface): # type: ignore
def __init__(self, *args, **kwargs) -> None: # type: ignore
raise RuntimeError(
"Attempted to use http2 support, but the `h2` package is not "
"installed. Use 'pip install httpcore[http2]'."
)

AsyncHTTP2Connection = _AsyncHTTP2Connection


try:
from .socks_proxy import AsyncSOCKSProxy
except ImportError: # pragma: no cover

class AsyncSOCKSProxy: # type: ignore
class _AsyncSOCKSProxy(AsyncConnectionPool): # type: ignore
def __init__(self, *args, **kwargs) -> None: # type: ignore
raise RuntimeError(
"Attempted to use SOCKS support, but the `socksio` package is not "
"installed. Use 'pip install httpcore[socks]'."
)

AsyncSOCKSProxy = _AsyncSOCKSProxy


__all__ = [
"AsyncHTTPConnection",
Expand Down
7 changes: 4 additions & 3 deletions src/httpcore2/httpcore2/_async/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async def _connect(self, request: Request) -> AsyncNetworkStream:
while True:
try:
if self._uds is None:
kwargs = {
kwargs: dict[str, typing.Any] = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize retry trace kwargs before the try

With the new pyright gate enabled, this branch-local kwargs declaration still makes the check fail: pyright --help defines the positional arguments as files..., and running pyright src/httpcore2/httpcore2/_async/connection.py reports "kwargs" is possibly unbound at the retry trace on line 148. Because scripts/check now runs pyright over src/httpcore2, the replacement type-check step exits non-zero before the rest of CI can pass; hoist/initialize the retry trace kwargs (or avoid reusing the branch-local variable in the except).

Useful? React with 👍 / 👎.

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"host": self._origin.host.decode("ascii"),
"port": self._origin.port,
"local_address": self._local_address,
Expand All @@ -117,7 +117,7 @@ async def _connect(self, request: Request) -> AsyncNetworkStream:
stream = await self._network_backend.connect_tcp(**kwargs)
trace.return_value = stream
else:
kwargs = {
kwargs: dict[str, typing.Any] = {
"path": self._uds,
"timeout": timeout,
"socket_options": self._socket_options,
Expand Down Expand Up @@ -145,7 +145,8 @@ async def _connect(self, request: Request) -> AsyncNetworkStream:
raise
retries_left -= 1
delay = next(delays)
async with Trace("retry", logger, request, kwargs) as trace:
# TODO(Marcelo): Check if `kwargs` is unbound.
async with Trace("retry", logger, request, kwargs) as trace: # type: ignore[reportPossiblyUnboundVariable]
await self._network_backend.sleep(delay)

def can_handle_request(self, origin: Origin) -> bool:
Expand Down
1 change: 1 addition & 0 deletions src/httpcore2/httpcore2/_async/connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pyright: reportUnknownMemberType=false, reportAttributeAccessIssue=false, reportPrivateUsage=false
from __future__ import annotations

import ssl
Expand Down
1 change: 1 addition & 0 deletions src/httpcore2/httpcore2/_async/http11.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pyright: reportPrivateUsage=false
from __future__ import annotations

import enum
Expand Down
11 changes: 4 additions & 7 deletions src/httpcore2/httpcore2/_async/http2.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pyright: reportPrivateUsage=false
from __future__ import annotations

import enum
Expand All @@ -14,11 +15,7 @@
import h2.settings

from .._backends.base import AsyncNetworkStream
from .._exceptions import (
ConnectionNotAvailable,
LocalProtocolError,
RemoteProtocolError,
)
from .._exceptions import ConnectionNotAvailable, LocalProtocolError, RemoteProtocolError
from .._models import Origin, Request, Response
from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation
from .._trace import Trace
Expand All @@ -29,7 +26,7 @@


def has_body_headers(request: Request) -> bool:
return any(k.lower() == b"content-length" or k.lower() == b"transfer-encoding" for k, v in request.headers)
return any(k.lower() == b"content-length" or k.lower() == b"transfer-encoding" for k, _v in request.headers)


class HTTPConnectionState(enum.IntEnum):
Expand Down Expand Up @@ -276,7 +273,7 @@ async def _receive_response(self, request: Request, stream_id: int) -> tuple[int
break

status_code = 200
headers = []
headers: list[tuple[bytes, bytes]] = []
assert event.headers is not None
for k, v in event.headers:
if k == b":status":
Expand Down
2 changes: 1 addition & 1 deletion src/httpcore2/httpcore2/_async/http_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def merge_headers(
"""
default_headers = [] if default_headers is None else list(default_headers)
override_headers = [] if override_headers is None else list(override_headers)
has_override = set(key.lower() for key, value in override_headers)
has_override = set(key.lower() for key, _value in override_headers)
default_headers = [(key, value) for key, value in default_headers if key.lower() not in has_override]
return default_headers + override_headers

Expand Down
4 changes: 3 additions & 1 deletion src/httpcore2/httpcore2/_async/socks_proxy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# pyright: reportAttributeAccessIssue=false, reportUnknownMemberType=false
from __future__ import annotations

import logging
import ssl
from typing import Any

import socksio

Expand Down Expand Up @@ -214,7 +216,7 @@ async def handle_async_request(self, request: Request) -> Response:
if self._connection is None:
try:
# Connect to the proxy
kwargs = {
kwargs: dict[str, Any] = {
"host": self._proxy_origin.host.decode("ascii"),
"port": self._proxy_origin.port,
"timeout": timeout,
Expand Down
22 changes: 8 additions & 14 deletions src/httpcore2/httpcore2/_backends/anyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,10 @@
import typing

import anyio
import anyio.abc
import anyio.streams.tls

from .._exceptions import (
ConnectError,
ConnectTimeout,
ReadError,
ReadTimeout,
WriteError,
WriteTimeout,
map_exceptions,
)
from .._exceptions import ConnectError, ConnectTimeout, ReadError, ReadTimeout, WriteError, WriteTimeout, map_exceptions
from .._utils import is_socket_readable
from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream

Expand All @@ -23,7 +17,7 @@ def __init__(self, stream: anyio.abc.ByteStream) -> None:
self._stream = stream

async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
exc_map = {
exc_map: dict[type[Exception], type[Exception]] = {
TimeoutError: ReadTimeout,
anyio.BrokenResourceError: ReadError,
anyio.ClosedResourceError: ReadError,
Expand All @@ -40,7 +34,7 @@ async def write(self, buffer: bytes, timeout: float | None = None) -> None:
if not buffer:
return

exc_map = {
exc_map: dict[type[Exception], type[Exception]] = {
TimeoutError: WriteTimeout,
anyio.BrokenResourceError: WriteError,
anyio.ClosedResourceError: WriteError,
Expand All @@ -58,7 +52,7 @@ async def start_tls(
server_hostname: str | None = None,
timeout: float | None = None,
) -> AsyncNetworkStream:
exc_map = {
exc_map: dict[type[Exception], type[Exception]] = {
TimeoutError: ConnectTimeout,
anyio.BrokenResourceError: ConnectError,
anyio.EndOfStream: ConnectError,
Expand Down Expand Up @@ -105,7 +99,7 @@ async def connect_tcp(
) -> AsyncNetworkStream: # pragma: no cover
if socket_options is None:
socket_options = []
exc_map = {
exc_map: dict[type[Exception], type[Exception]] = {
TimeoutError: ConnectTimeout,
OSError: ConnectError,
anyio.BrokenResourceError: ConnectError,
Expand All @@ -130,7 +124,7 @@ async def connect_unix_socket(
) -> AsyncNetworkStream: # pragma: no cover
if socket_options is None:
socket_options = []
exc_map = {
exc_map: dict[type[Exception], type[Exception]] = {
TimeoutError: ConnectTimeout,
OSError: ConnectError,
anyio.BrokenResourceError: ConnectError,
Expand Down
4 changes: 2 additions & 2 deletions src/httpcore2/httpcore2/_backends/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def connect_tcp(
source_address=source_address,
)
for option in socket_options:
sock.setsockopt(*option) # pragma: no cover
sock.setsockopt(*option) # pragma: no cover # type: ignore[reportArgumentType]
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
return SyncStream(sock)

Expand All @@ -229,7 +229,7 @@ def connect_unix_socket(
with map_exceptions(exc_map):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
for option in socket_options:
sock.setsockopt(*option)
sock.setsockopt(*option) # type: ignore[reportArgumentType]
sock.settimeout(timeout)
sock.connect(path)
return SyncStream(sock)
4 changes: 3 additions & 1 deletion src/httpcore2/httpcore2/_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

import contextlib
import typing

ExceptionMapping = typing.Mapping[typing.Type[Exception], typing.Type[Exception]]


@contextlib.contextmanager
def map_exceptions(map: ExceptionMapping) -> typing.Iterator[None]:
def map_exceptions(map: ExceptionMapping) -> typing.Generator[None]:
Comment thread
Kludex marked this conversation as resolved.
try:
yield
except Exception as exc: # noqa: PIE786
Expand Down
17 changes: 9 additions & 8 deletions src/httpcore2/httpcore2/_models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pyright: reportAttributeAccessIssue=false
from __future__ import annotations

import base64
Expand Down Expand Up @@ -33,7 +34,7 @@ def enforce_bytes(value: bytes | str, *, name: str) -> bytes:
return value.encode("ascii")
except UnicodeEncodeError:
raise TypeError(f"{name} strings may not include unicode characters.")
elif isinstance(value, bytes):
elif isinstance(value, bytes): # type: ignore[reportUnnecessaryIsInstance]
return value

seen_type = type(value).__name__
Expand All @@ -46,7 +47,7 @@ def enforce_url(value: URL | bytes | str, *, name: str) -> URL:
"""
if isinstance(value, (bytes, str)):
return URL(value)
elif isinstance(value, URL):
elif isinstance(value, URL): # type: ignore[reportUnnecessaryIsInstance]
return value

seen_type = type(value).__name__
Expand All @@ -70,7 +71,7 @@ def enforce_headers(
)
for k, v in value.items()
]
elif isinstance(value, typing.Sequence):
elif isinstance(value, typing.Sequence): # type: ignore[reportUnnecessaryIsInstance]
return [
(
enforce_bytes(k, name="header name"),
Expand Down Expand Up @@ -113,7 +114,7 @@ def include_request_headers(
url: "URL",
content: None | bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes],
) -> list[tuple[bytes, bytes]]:
headers_set = {k.lower() for k, v in headers}
headers_set = {k.lower() for k, _v in headers}

if b"host" not in headers_set:
default_port = DEFAULT_PORTS.get(url.scheme)
Expand Down Expand Up @@ -335,7 +336,7 @@ def __init__(
self.url: URL = enforce_url(url, name="url")
self.headers: list[tuple[bytes, bytes]] = enforce_headers(headers, name="headers")
self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = enforce_stream(content, name="content")
self.extensions = {} if extensions is None else extensions
self.extensions: Extensions = {} if extensions is None else extensions

if "target" in self.extensions:
self.url = URL(
Expand Down Expand Up @@ -374,7 +375,7 @@ def __init__(
self.status: int = status
self.headers: list[tuple[bytes, bytes]] = enforce_headers(headers, name="headers")
self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = enforce_stream(content, name="content")
self.extensions = {} if extensions is None else extensions
self.extensions: Extensions = {} if extensions is None else extensions

self._stream_consumed = False

Expand Down Expand Up @@ -427,7 +428,7 @@ def close(self) -> None:
"You should use 'await response.aclose()' instead."
)
if hasattr(self.stream, "close"):
self.stream.close()
self.stream.close() # type: ignore[reportUnknownMemberType]

# Async interface...

Expand Down Expand Up @@ -465,7 +466,7 @@ async def aclose(self) -> None:
"You should use 'response.close()' instead."
)
if hasattr(self.stream, "aclose"):
await self.stream.aclose()
await self.stream.aclose() # type: ignore[reportUnknownMemberType]


class Proxy:
Expand Down
Loading
Loading