Skip to content

Commit 6889fce

Browse files
committed
Add inline types to Requests
1 parent 0e4ae38 commit 6889fce

23 files changed

+1229
-541
lines changed

.github/workflows/publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
python -m build
4242
4343
- name: "Upload dists"
44-
uses: "actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"
44+
uses: "actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f"
4545
id: upload-artifact
4646
with:
4747
name: "dist"
@@ -61,7 +61,7 @@ jobs:
6161

6262
steps:
6363
- name: "Download dists"
64-
uses: "actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" # v8.0.0
64+
uses: "actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131" # v7.0.0
6565
with:
6666
artifact-ids: ${{ needs.build.outputs.artifact-id }}
6767
path: "dist/"
@@ -83,7 +83,7 @@ jobs:
8383

8484
steps:
8585
- name: "Download dists"
86-
uses: "actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" # v8.0.0
86+
uses: "actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131" # v7.0.0
8787
with:
8888
artifact-ids: ${{ needs.build.outputs.artifact-id }}
8989
path: "dist/"

.github/workflows/typecheck.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
13+
steps:
14+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
15+
with:
16+
persist-credentials: false
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
20+
with:
21+
python-version: "3.10"
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install pip==26.0.1
26+
python -m pip install -e . --group typecheck
27+
28+
- name: Run pyright
29+
run: python -m pyright src/requests/

docs/api.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ Lower-Level Classes
5555

5656
.. autoclass:: Response
5757
:inherited-members:
58-
:exclude-members: is_permanent_redirect
5958

6059

6160
Lower-Lower-Level Classes

docs/user/quickstart.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,7 @@ use the same key::
271271
},
272272
...
273273
}
274-
>>> # httpbin may embed non-deterministic metadata,
275-
>>> # so we only compare our submitted data here.
276-
>>> r1.json()['form'] == r2.json()['form']
274+
>>> r1.text == r2.text
277275
True
278276

279277
There are times that you may want to send data that is not form-encoded. If

pyproject.toml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,20 @@ Source = "https://github.com/psf/requests"
5151
[project.optional-dependencies]
5252
security = []
5353
socks = ["PySocks>=1.5.6, !=1.5.7"]
54-
use_chardet_on_py3 = ["chardet>=3.0.2,<8"]
54+
use_chardet_on_py3 = ["chardet>=3.0.2,<7"]
55+
56+
[dependency-groups]
5557
test = [
5658
"pytest-httpbin==2.1.0",
5759
"pytest-cov",
5860
"pytest-mock",
5961
"pytest-xdist",
6062
"PySocks>=1.5.6, !=1.5.7",
61-
"pytest>=3"
63+
"pytest>=3",
64+
]
65+
typecheck = [
66+
"pyright",
67+
"typing_extensions",
6268
]
6369

6470
[tool.setuptools]
@@ -100,3 +106,8 @@ addopts = "--doctest-modules"
100106
doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS"
101107
minversion = "6.2"
102108
testpaths = ["tests"]
109+
110+
111+
[tool.pyright]
112+
include = ["src/requests"]
113+
typeCheckingMode = "strict"

src/requests/__init__.py

Lines changed: 55 additions & 19 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(".")
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(".")
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
@@ -75,8 +81,8 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
7581
if chardet_version:
7682
major, minor, patch = chardet_version.split(".")[:3]
7783
major, minor, patch = int(major), int(minor), int(patch)
78-
# chardet_version >= 3.0.2, < 8.0.0
79-
assert (3, 0, 2) <= (major, minor, patch) < (8, 0, 0)
84+
# chardet_version >= 3.0.2, < 6.0.0
85+
assert (3, 0, 2) <= (major, minor, patch) < (7, 0, 0)
8086
elif charset_normalizer_version:
8187
major, minor, patch = charset_normalizer_version.split(".")[:3]
8288
major, minor, patch = int(major), int(minor), int(patch)
@@ -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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
TypeVar,
18+
runtime_checkable,
19+
)
20+
21+
_T_co = TypeVar("_T_co", covariant=True)
22+
23+
24+
@runtime_checkable
25+
class SupportsRead(Protocol[_T_co]):
26+
def read(self, length: int = ...) -> _T_co: ...
27+
28+
29+
@runtime_checkable
30+
class SupportsItems(Protocol):
31+
def items(self) -> Iterable[tuple[Any, Any]]: ...
32+
33+
34+
# These are needed at runtime for default_hooks() return type
35+
HookType = Callable[["Response"], Any]
36+
HooksInputType = Mapping[str, "Iterable[HookType] | HookType"]
37+
38+
39+
def is_prepared(request: PreparedRequest) -> TypeIs[_ValidatedRequest]:
40+
"""Verify a PreparedRequest has been fully prepared."""
41+
if TYPE_CHECKING:
42+
return request.url is not None and request.method is not None
43+
# noop at runtime to avoid AssertionError
44+
return True
45+
46+
47+
if TYPE_CHECKING:
48+
from typing import TypeAlias
49+
50+
from typing_extensions import TypeIs # move to typing when Python >= 3.13
51+
52+
from .auth import AuthBase
53+
from .cookies import RequestsCookieJar
54+
from .models import PreparedRequest, Response
55+
from .structures import CaseInsensitiveDict
56+
57+
class _ValidatedRequest(PreparedRequest):
58+
"""Subtype asserting a PreparedRequest has been fully prepared before calling.
59+
60+
The override suppression is required because mutable attribute types are
61+
invariant (Liskov), but we only narrow after preparation is complete. This
62+
is the explicit contract for Requests but Python's typing doesn't have a
63+
better way to represent the requirement.
64+
"""
65+
66+
url: str # type: ignore[reportIncompatibleVariableOverride]
67+
method: str # type: ignore[reportIncompatibleVariableOverride]
68+
69+
# Type aliases for core API concepts (ordered by request() signature)
70+
UriType: TypeAlias = str | bytes
71+
72+
_ParamsMappingKeyType: TypeAlias = str | bytes | int | float
73+
_ParamsMappingValueType: TypeAlias = (
74+
str | bytes | int | float | Iterable[str | bytes | int | float] | None
75+
)
76+
ParamsType: TypeAlias = (
77+
Mapping[_ParamsMappingKeyType, _ParamsMappingValueType]
78+
| tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...]
79+
| Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]]
80+
| str
81+
| bytes
82+
| None
83+
)
84+
85+
KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | Mapping[Any, Any]
86+
87+
EncodableDataType: TypeAlias = KVDataType | str | bytes | SupportsRead[str | bytes]
88+
89+
DataType: TypeAlias = (
90+
KVDataType
91+
| Iterable[bytes | str]
92+
| str
93+
| bytes
94+
| SupportsRead[str | bytes]
95+
| None
96+
)
97+
98+
BodyType: TypeAlias = (
99+
bytes | str | Iterable[bytes | str] | SupportsRead[bytes | str] | None
100+
)
101+
102+
HeadersType: TypeAlias = CaseInsensitiveDict[str] | Mapping[str, str | bytes]
103+
HeadersUpdateType: TypeAlias = Mapping[str, str | bytes | None]
104+
105+
CookiesType: TypeAlias = RequestsCookieJar | Mapping[str, str]
106+
107+
# Building blocks for FilesType
108+
_FileName: TypeAlias = str | None
109+
_FileContent: TypeAlias = SupportsRead[str | bytes] | str | bytes
110+
_FileSpecBasic: TypeAlias = tuple[_FileName, _FileContent]
111+
_FileSpecWithContentType: TypeAlias = tuple[_FileName, _FileContent, str]
112+
_FileSpecWithHeaders: TypeAlias = tuple[
113+
_FileName, _FileContent, str, CaseInsensitiveDict[str] | Mapping[str, str]
114+
]
115+
_FileSpec: TypeAlias = (
116+
_FileContent | _FileSpecBasic | _FileSpecWithContentType | _FileSpecWithHeaders
117+
)
118+
FilesType: TypeAlias = (
119+
Mapping[str, _FileSpec] | Iterable[tuple[str, _FileSpec]] | None
120+
)
121+
122+
AuthType: TypeAlias = (
123+
tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] | None
124+
)
125+
126+
TimeoutType: TypeAlias = float | tuple[float | None, float | None] | None
127+
ProxiesType: TypeAlias = MutableMapping[str, str]
128+
HooksType: TypeAlias = dict[str, list["HookType"]] | None
129+
VerifyType: TypeAlias = bool | str
130+
CertType: TypeAlias = str | tuple[str, str] | None
131+
JsonType: TypeAlias = (
132+
None | bool | int | float | str | list["JsonType"] | dict[str, "JsonType"]
133+
)

0 commit comments

Comments
 (0)