Skip to content

Commit 561e4b6

Browse files
authored
Add inline types to Requests (#7272)
1 parent 8f6cda9 commit 561e4b6

20 files changed

Lines changed: 1308 additions & 525 deletions

.github/workflows/typecheck.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Type Check
2+
3+
on: [push, pull_request]
4+
5+
permissions:
6+
contents: read
7+
8+
jobs:
9+
typecheck:
10+
runs-on: ubuntu-24.04
11+
timeout-minutes: 10
12+
strategy:
13+
matrix:
14+
python-version: ["3.10", "3.14"]
15+
16+
steps:
17+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
18+
with:
19+
persist-credentials: false
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install pip==26.0.1
29+
python -m pip install -e . --group typecheck
30+
31+
- name: Run pyright
32+
run: python -m pyright src/requests/

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ test = [
6464
"pytest>=3",
6565
"trustme",
6666
]
67+
typecheck = [
68+
"pyright",
69+
"typing_extensions",
70+
]
6771

6872
[tool.setuptools]
6973
license-files = ["LICENSE", "NOTICE"]
@@ -104,3 +108,8 @@ addopts = "--doctest-modules"
104108
doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS"
105109
minversion = "6.2"
106110
testpaths = ["tests"]
111+
112+
113+
[tool.pyright]
114+
include = ["src/requests"]
115+
typeCheckingMode = "strict"

src/requests/__init__.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
:license: Apache 2.0, see LICENSE for more details.
3939
"""
4040

41+
from __future__ import annotations
42+
4143
import warnings
4244

4345
import urllib3
@@ -50,21 +52,25 @@
5052
charset_normalizer_version = None
5153

5254
try:
53-
from chardet import __version__ as chardet_version
55+
from chardet import __version__ as chardet_version # type: ignore[import-not-found]
5456
except ImportError:
5557
chardet_version = None
5658

5759

58-
def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version):
59-
urllib3_version = urllib3_version.split(".")[:3]
60-
assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git.
60+
def check_compatibility(
61+
urllib3_version: str,
62+
chardet_version: str | None,
63+
charset_normalizer_version: str | None,
64+
) -> None:
65+
urllib3_version_list = urllib3_version.split(".")[:3]
66+
assert urllib3_version_list != ["dev"] # Verify urllib3 isn't installed from git.
6167

6268
# Sometimes, urllib3 only reports its version as 16.1.
63-
if len(urllib3_version) == 2:
64-
urllib3_version.append("0")
69+
if len(urllib3_version_list) == 2:
70+
urllib3_version_list.append("0")
6571

6672
# Check urllib3 for compatibility.
67-
major, minor, patch = urllib3_version # noqa: F811
73+
major, minor, patch = urllib3_version_list # noqa: F811
6874
major, minor, patch = int(major), int(minor), int(patch)
6975
# urllib3 >= 1.21.1
7076
assert major >= 1
@@ -90,28 +96,28 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
9096
)
9197

9298

93-
def _check_cryptography(cryptography_version):
99+
def _check_cryptography(cryptography_version: str) -> None:
94100
# cryptography < 1.3.4
95101
try:
96-
cryptography_version = list(map(int, cryptography_version.split(".")))
102+
cryptography_version_list = list(map(int, cryptography_version.split(".")))
97103
except ValueError:
98104
return
99105

100-
if cryptography_version < [1, 3, 4]:
101-
warning = (
102-
f"Old version of cryptography ({cryptography_version}) may cause slowdown."
103-
)
106+
if cryptography_version_list < [1, 3, 4]:
107+
warning = f"Old version of cryptography ({cryptography_version_list}) may cause slowdown."
104108
warnings.warn(warning, RequestsDependencyWarning)
105109

106110

107111
# Check imported dependencies for compatibility.
108112
try:
109113
check_compatibility(
110-
urllib3.__version__, chardet_version, charset_normalizer_version
114+
urllib3.__version__, # type: ignore[reportPrivateImportUsage]
115+
chardet_version, # type: ignore[reportUnknownArgumentType]
116+
charset_normalizer_version,
111117
)
112118
except (AssertionError, ValueError):
113119
warnings.warn(
114-
f"urllib3 ({urllib3.__version__}) or chardet "
120+
f"urllib3 ({urllib3.__version__}) or chardet " # type: ignore[reportPrivateImportUsage]
115121
f"({chardet_version})/charset_normalizer ({charset_normalizer_version}) "
116122
"doesn't match a supported version!",
117123
RequestsDependencyWarning,
@@ -132,9 +138,11 @@ def _check_cryptography(cryptography_version):
132138
pyopenssl.inject_into_urllib3()
133139

134140
# Check cryptography version
135-
from cryptography import __version__ as cryptography_version
141+
from cryptography import ( # type: ignore[reportMissingImports]
142+
__version__ as cryptography_version, # type: ignore[reportUnknownVariableType]
143+
)
136144

137-
_check_cryptography(cryptography_version)
145+
_check_cryptography(cryptography_version) # type: ignore[reportUnknownArgumentType]
138146
except ImportError:
139147
pass
140148

@@ -177,6 +185,34 @@ def _check_cryptography(cryptography_version):
177185
from .sessions import Session, session
178186
from .status_codes import codes
179187

188+
__all__ = (
189+
"ConnectionError",
190+
"ConnectTimeout",
191+
"HTTPError",
192+
"JSONDecodeError",
193+
"PreparedRequest",
194+
"ReadTimeout",
195+
"Request",
196+
"RequestException",
197+
"Response",
198+
"Session",
199+
"Timeout",
200+
"TooManyRedirects",
201+
"URLRequired",
202+
"codes",
203+
"delete",
204+
"get",
205+
"head",
206+
"options",
207+
"packages",
208+
"patch",
209+
"post",
210+
"put",
211+
"request",
212+
"session",
213+
"utils",
214+
)
215+
180216
logging.getLogger(__name__).addHandler(NullHandler())
181217

182218
# FileModeWarnings go off per the default.

src/requests/_internal_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
}
2424

2525

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

3838

39-
def unicode_is_ascii(u_string):
39+
def unicode_is_ascii(u_string: str) -> bool:
4040
"""Determine if unicode string only contains ASCII characters.
4141
4242
:param str u_string: unicode string to check. Must be unicode

src/requests/_types.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""
2+
requests._types
3+
~~~~~~~~~~~~~~~
4+
5+
This module contains type aliases used internally by the Requests library.
6+
These types are not part of the public API and must not be relied upon
7+
by external code.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from collections.abc import Callable, Iterable, Mapping, MutableMapping
13+
from typing import (
14+
TYPE_CHECKING,
15+
Any,
16+
Protocol,
17+
TypeAlias,
18+
TypeVar,
19+
runtime_checkable,
20+
)
21+
22+
_T_co = TypeVar("_T_co", covariant=True)
23+
24+
25+
@runtime_checkable
26+
class SupportsRead(Protocol[_T_co]):
27+
def read(self, length: int = ..., /) -> _T_co: ...
28+
29+
30+
@runtime_checkable
31+
class SupportsItems(Protocol):
32+
def items(self) -> Iterable[tuple[Any, Any]]: ...
33+
34+
35+
# These are needed at runtime for default_hooks() return type
36+
HookType: TypeAlias = Callable[["Response"], Any]
37+
HooksInputType: TypeAlias = Mapping[str, Iterable[HookType] | HookType]
38+
39+
40+
def is_prepared(request: PreparedRequest) -> TypeIs[_ValidatedRequest]:
41+
"""Verify a PreparedRequest has been fully prepared."""
42+
if TYPE_CHECKING:
43+
return request.url is not None and request.method is not None
44+
# noop at runtime to avoid AssertionError
45+
return True
46+
47+
48+
if TYPE_CHECKING:
49+
from http.cookiejar import CookieJar
50+
from typing import TypeAlias, TypedDict
51+
52+
from typing_extensions import (
53+
Buffer, # TODO: move to collections.abc when Python >= 3.12
54+
TypeIs, # TODO: move to typing when Python >= 3.13
55+
)
56+
57+
from .auth import AuthBase
58+
from .cookies import RequestsCookieJar
59+
from .models import PreparedRequest, Response
60+
from .structures import CaseInsensitiveDict
61+
62+
class _ValidatedRequest(PreparedRequest):
63+
"""Subtype asserting a PreparedRequest has been fully prepared before calling.
64+
65+
The override suppression is required because mutable attribute types are
66+
invariant (Liskov), but we only narrow after preparation is complete. This
67+
is the explicit contract for Requests but Python's typing doesn't have a
68+
better way to represent the requirement.
69+
"""
70+
71+
url: str # type: ignore[reportIncompatibleVariableOverride]
72+
method: str # type: ignore[reportIncompatibleVariableOverride]
73+
74+
# Type aliases for core API concepts (ordered by request() signature)
75+
UriType: TypeAlias = str | bytes
76+
77+
_ParamsMappingKeyType: TypeAlias = str | bytes | int | float
78+
_ParamsMappingValueType: TypeAlias = (
79+
str | bytes | int | float | Iterable[str | bytes | int | float] | None
80+
)
81+
ParamsType: TypeAlias = (
82+
Mapping[_ParamsMappingKeyType, _ParamsMappingValueType]
83+
| tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...]
84+
| Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]]
85+
| str
86+
| bytes
87+
| None
88+
)
89+
90+
KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | Mapping[Any, Any]
91+
92+
RawDataType: TypeAlias = KVDataType | str | bytes
93+
StreamDataType: TypeAlias = SupportsRead[str | bytes]
94+
EncodableDataType: TypeAlias = RawDataType | StreamDataType
95+
96+
DataType: TypeAlias = (
97+
KVDataType
98+
| Iterable[bytes | str]
99+
| str
100+
| bytes
101+
| Buffer
102+
| SupportsRead[str | bytes]
103+
| None
104+
)
105+
106+
BodyType: TypeAlias = (
107+
bytes | str | Iterable[bytes | str] | SupportsRead[bytes | str] | None
108+
)
109+
110+
HeadersType: TypeAlias = CaseInsensitiveDict[str] | Mapping[str, str | bytes]
111+
HeadersUpdateType: TypeAlias = Mapping[str, str | bytes | None]
112+
113+
CookiesType: TypeAlias = RequestsCookieJar | Mapping[str, str]
114+
115+
# Building blocks for FilesType
116+
_FileName: TypeAlias = str | None
117+
_FileContent: TypeAlias = SupportsRead[str | bytes] | str | bytes
118+
_FileSpecBasic: TypeAlias = tuple[_FileName, _FileContent]
119+
_FileSpecWithContentType: TypeAlias = tuple[_FileName, _FileContent, str]
120+
_FileSpecWithHeaders: TypeAlias = tuple[
121+
_FileName, _FileContent, str, CaseInsensitiveDict[str] | Mapping[str, str]
122+
]
123+
_FileSpec: TypeAlias = (
124+
_FileContent | _FileSpecBasic | _FileSpecWithContentType | _FileSpecWithHeaders
125+
)
126+
FilesType: TypeAlias = (
127+
Mapping[str, _FileSpec] | Iterable[tuple[str, _FileSpec]] | None
128+
)
129+
130+
AuthType: TypeAlias = (
131+
tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] | None
132+
)
133+
134+
TimeoutType: TypeAlias = float | tuple[float | None, float | None] | None
135+
ProxiesType: TypeAlias = MutableMapping[str, str]
136+
HooksType: TypeAlias = dict[str, list[HookType]] | None
137+
VerifyType: TypeAlias = bool | str
138+
CertType: TypeAlias = str | tuple[str, str] | None
139+
JsonType: TypeAlias = (
140+
None | bool | int | float | str | list["JsonType"] | dict[str, "JsonType"]
141+
)
142+
143+
# TypedDicts for Unpack kwargs (PEP 692)
144+
145+
class BaseRequestKwargs(TypedDict, total=False):
146+
headers: Mapping[str, str | bytes] | None
147+
cookies: RequestsCookieJar | CookieJar | dict[str, str] | None
148+
files: FilesType
149+
auth: AuthType
150+
timeout: TimeoutType
151+
allow_redirects: bool
152+
proxies: dict[str, str] | None
153+
hooks: HooksType
154+
stream: bool | None
155+
verify: VerifyType | None
156+
cert: CertType
157+
158+
class RequestKwargs(BaseRequestKwargs, total=False):
159+
"""kwargs for request(), options(), head(), delete()."""
160+
161+
params: ParamsType
162+
data: DataType
163+
json: JsonType
164+
165+
class GetKwargs(BaseRequestKwargs, total=False):
166+
data: DataType
167+
json: JsonType
168+
169+
class PostKwargs(BaseRequestKwargs, total=False):
170+
params: ParamsType
171+
172+
class DataKwargs(BaseRequestKwargs, total=False):
173+
"""kwargs for put(), patch()."""
174+
175+
params: ParamsType
176+
json: JsonType

0 commit comments

Comments
 (0)