Skip to content
Draft
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
29 changes: 29 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Type Check

on: [push, pull_request]

permissions:
contents: read

jobs:
typecheck:
runs-on: ubuntu-24.04
timeout-minutes: 10

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install pip==26.0.1
python -m pip install -e . --group typecheck
- name: Run pyright
run: python -m pyright src/requests/
15 changes: 13 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,20 @@ Source = "https://github.com/psf/requests"
[project.optional-dependencies]
security = []
socks = ["PySocks>=1.5.6, !=1.5.7"]
use_chardet_on_py3 = ["chardet>=3.0.2,<8"]
use_chardet_on_py3 = ["chardet>=3.0.2,<7"]

[dependency-groups]
test = [
"pytest-httpbin==2.1.0",
"pytest-cov",
"pytest-mock",
"pytest-xdist",
"PySocks>=1.5.6, !=1.5.7",
"pytest>=3"
"pytest>=3",
]
typecheck = [
"pyright",
"typing_extensions",
]

[tool.setuptools]
Expand Down Expand Up @@ -100,3 +106,8 @@ addopts = "--doctest-modules"
doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS"
minversion = "6.2"
testpaths = ["tests"]


[tool.pyright]
include = ["src/requests"]
typeCheckingMode = "strict"
70 changes: 53 additions & 17 deletions src/requests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
:license: Apache 2.0, see LICENSE for more details.
"""

from __future__ import annotations

import warnings

import urllib3
Expand All @@ -50,21 +52,25 @@
charset_normalizer_version = None

try:
from chardet import __version__ as chardet_version
from chardet import __version__ as chardet_version # type: ignore[import-not-found]
except ImportError:
chardet_version = None


def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version):
urllib3_version = urllib3_version.split(".")
assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git.
def check_compatibility(
urllib3_version: str,
chardet_version: str | None,
charset_normalizer_version: str | None,
) -> None:
urllib3_version_list = urllib3_version.split(".")
assert urllib3_version_list != ["dev"] # Verify urllib3 isn't installed from git.

# Sometimes, urllib3 only reports its version as 16.1.
if len(urllib3_version) == 2:
urllib3_version.append("0")
if len(urllib3_version_list) == 2:
urllib3_version_list.append("0")

# Check urllib3 for compatibility.
major, minor, patch = urllib3_version # noqa: F811
major, minor, patch = urllib3_version_list # noqa: F811
major, minor, patch = int(major), int(minor), int(patch)
# urllib3 >= 1.21.1
assert major >= 1
Expand All @@ -90,28 +96,28 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
)


def _check_cryptography(cryptography_version):
def _check_cryptography(cryptography_version: str) -> None:
# cryptography < 1.3.4
try:
cryptography_version = list(map(int, cryptography_version.split(".")))
cryptography_version_list = list(map(int, cryptography_version.split(".")))
except ValueError:
return

if cryptography_version < [1, 3, 4]:
warning = (
f"Old version of cryptography ({cryptography_version}) may cause slowdown."
)
if cryptography_version_list < [1, 3, 4]:
warning = f"Old version of cryptography ({cryptography_version_list}) may cause slowdown."
warnings.warn(warning, RequestsDependencyWarning)


# Check imported dependencies for compatibility.
try:
check_compatibility(
urllib3.__version__, chardet_version, charset_normalizer_version
urllib3.__version__, # type: ignore[reportPrivateImportUsage]
chardet_version, # type: ignore[reportUnknownArgumentType]
charset_normalizer_version,
)
except (AssertionError, ValueError):
warnings.warn(
f"urllib3 ({urllib3.__version__}) or chardet "
f"urllib3 ({urllib3.__version__}) or chardet " # type: ignore[reportPrivateImportUsage]
f"({chardet_version})/charset_normalizer ({charset_normalizer_version}) "
"doesn't match a supported version!",
RequestsDependencyWarning,
Expand All @@ -132,9 +138,11 @@ def _check_cryptography(cryptography_version):
pyopenssl.inject_into_urllib3()

# Check cryptography version
from cryptography import __version__ as cryptography_version
from cryptography import ( # type: ignore[reportMissingImports]
__version__ as cryptography_version, # type: ignore[reportUnknownVariableType]
)

_check_cryptography(cryptography_version)
_check_cryptography(cryptography_version) # type: ignore[reportUnknownArgumentType]
except ImportError:
pass

Expand Down Expand Up @@ -177,6 +185,34 @@ def _check_cryptography(cryptography_version):
from .sessions import Session, session
from .status_codes import codes

__all__ = (
"ConnectionError",
"ConnectTimeout",
"HTTPError",
"JSONDecodeError",
"PreparedRequest",
"ReadTimeout",
"Request",
"RequestException",
"Response",
"Session",
"Timeout",
"TooManyRedirects",
"URLRequired",
"codes",
"delete",
"get",
"head",
"options",
"packages",
"patch",
"post",
"put",
"request",
"session",
"utils",
)

logging.getLogger(__name__).addHandler(NullHandler())

# FileModeWarnings go off per the default.
Expand Down
4 changes: 2 additions & 2 deletions src/requests/_internal_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
}


def to_native_string(string, encoding="ascii"):
def to_native_string(string: str | bytes, encoding: str = "ascii") -> str:
"""Given a string object, regardless of type, returns a representation of
that string in the native string type, encoding and decoding where
necessary. This assumes ASCII unless told otherwise.
Expand All @@ -36,7 +36,7 @@ def to_native_string(string, encoding="ascii"):
return out


def unicode_is_ascii(u_string):
def unicode_is_ascii(u_string: str) -> bool:
"""Determine if unicode string only contains ASCII characters.
:param str u_string: unicode string to check. Must be unicode
Expand Down
133 changes: 133 additions & 0 deletions src/requests/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
requests._types
~~~~~~~~~~~~~~~
This module contains type aliases used internally by the Requests library.
These types are not part of the public API and must not be relied upon
by external code.
"""

from __future__ import annotations

from collections.abc import Callable, Iterable, Mapping, MutableMapping
from typing import (
TYPE_CHECKING,
Any,
Protocol,
TypeVar,
runtime_checkable,
)

_T_co = TypeVar("_T_co", covariant=True)


@runtime_checkable
class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ...) -> _T_co: ...


@runtime_checkable
class SupportsItems(Protocol):
def items(self) -> Iterable[tuple[Any, Any]]: ...


# These are needed at runtime for default_hooks() return type
HookType = Callable[["Response"], Any]
HooksInputType = Mapping[str, "Iterable[HookType] | HookType"]


def is_prepared(request: PreparedRequest) -> TypeIs[_ValidatedRequest]:
"""Verify a PreparedRequest has been fully prepared."""
if TYPE_CHECKING:
return request.url is not None and request.method is not None
# noop at runtime to avoid AssertionError
return True


if TYPE_CHECKING:
from typing import TypeAlias

from typing_extensions import TypeIs # move to typing when Python >= 3.13

from .auth import AuthBase
from .cookies import RequestsCookieJar
from .models import PreparedRequest, Response
from .structures import CaseInsensitiveDict

class _ValidatedRequest(PreparedRequest):
"""Subtype asserting a PreparedRequest has been fully prepared before calling.
The override suppression is required because mutable attribute types are
invariant (Liskov), but we only narrow after preparation is complete. This
is the explicit contract for Requests but Python's typing doesn't have a
better way to represent the requirement.
"""

url: str # type: ignore[reportIncompatibleVariableOverride]
method: str # type: ignore[reportIncompatibleVariableOverride]

# Type aliases for core API concepts (ordered by request() signature)
UriType: TypeAlias = str | bytes

_ParamsMappingKeyType: TypeAlias = str | bytes | int | float
_ParamsMappingValueType: TypeAlias = (
str | bytes | int | float | Iterable[str | bytes | int | float] | None
)
ParamsType: TypeAlias = (
Mapping[_ParamsMappingKeyType, _ParamsMappingValueType]
| tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...]
| Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]]
| str
| bytes
| None
)

KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | Mapping[Any, Any]

EncodableDataType: TypeAlias = KVDataType | str | bytes | SupportsRead[str | bytes]

DataType: TypeAlias = (
KVDataType
| Iterable[bytes | str]
| str
| bytes
| SupportsRead[str | bytes]
| None
)

BodyType: TypeAlias = (
bytes | str | Iterable[bytes | str] | SupportsRead[bytes | str] | None
)

HeadersType: TypeAlias = CaseInsensitiveDict[str] | Mapping[str, str | bytes]
HeadersUpdateType: TypeAlias = Mapping[str, str | bytes | None]

CookiesType: TypeAlias = RequestsCookieJar | Mapping[str, str]

# Building blocks for FilesType
_FileName: TypeAlias = str | None
_FileContent: TypeAlias = SupportsRead[str | bytes] | str | bytes
_FileSpecBasic: TypeAlias = tuple[_FileName, _FileContent]
_FileSpecWithContentType: TypeAlias = tuple[_FileName, _FileContent, str]
_FileSpecWithHeaders: TypeAlias = tuple[
_FileName, _FileContent, str, CaseInsensitiveDict[str] | Mapping[str, str]
]
_FileSpec: TypeAlias = (
_FileContent | _FileSpecBasic | _FileSpecWithContentType | _FileSpecWithHeaders
)
FilesType: TypeAlias = (
Mapping[str, _FileSpec] | Iterable[tuple[str, _FileSpec]] | None
)

AuthType: TypeAlias = (
tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] | None
)

TimeoutType: TypeAlias = float | tuple[float | None, float | None] | None
ProxiesType: TypeAlias = MutableMapping[str, str]
HooksType: TypeAlias = dict[str, list["HookType"]] | None
VerifyType: TypeAlias = bool | str
CertType: TypeAlias = str | tuple[str, str] | None
JsonType: TypeAlias = (
None | bool | int | float | str | list["JsonType"] | dict[str, "JsonType"]
)
Loading
Loading