diff --git a/stdlib/@tests/test_cases/urllib/check_request.py b/stdlib/@tests/test_cases/urllib/check_request.py new file mode 100644 index 000000000000..0c2f31542505 --- /dev/null +++ b/stdlib/@tests/test_cases/urllib/check_request.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from _typeshed import ReadableBuffer, SupportsRead +from collections.abc import Iterable +from typing_extensions import assert_type +from urllib.request import Request, urlopen + +# The type of `data` is inferred from the constructor argument. +req = Request("http://example.com", data=b"body") +assert_type(req, Request[bytes]) +assert_type(req.data, bytes) +req.data.decode() +req.data = b"other" +req.data = [b"a", b"b"] # type: ignore + + +def iterable_data(it: Iterable[bytes]) -> None: + assert_type(Request("http://example.com", it).data, Iterable[bytes]) + + +# Without `data`, any data type can be assigned later. +req2 = Request("http://example.com") +assert_type(req2.data, ReadableBuffer | SupportsRead[bytes] | Iterable[bytes] | None) +req2.data = b"body" +del req2.data + +# A plain `Request` annotation means `Request[Any]`, so specialized requests +# are accepted by functions taking a plain `Request`. +urlopen(req) diff --git a/stdlib/urllib/request.pyi b/stdlib/urllib/request.pyi index d81d3a2f8698..4d3b4b6808f5 100644 --- a/stdlib/urllib/request.pyi +++ b/stdlib/urllib/request.pyi @@ -6,7 +6,7 @@ from email.message import Message from http.client import HTTPConnection, HTTPMessage, HTTPResponse from http.cookiejar import CookieJar from re import Pattern -from typing import IO, Any, ClassVar, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only +from typing import IO, Any, ClassVar, Generic, Literal, Protocol, TypeAlias, TypeVar, overload, type_check_only from typing_extensions import Never, deprecated from urllib.error import HTTPError as HTTPError from urllib.response import addclosehook, addinfourl @@ -58,6 +58,10 @@ _T = TypeVar("_T") _UrlopenRet: TypeAlias = Any _DataType: TypeAlias = ReadableBuffer | SupportsRead[bytes] | Iterable[bytes] | None +# Default to Any so that plain `Request` annotations accept every +# specialization. A covariant type variable would require a setter that +# accepts any data type, letting incorrect assignments go unnoticed. +_DataT = TypeVar("_DataT", bound=_DataType, default=Any) if sys.version_info >= (3, 13): def urlopen( @@ -121,7 +125,7 @@ if sys.platform == "win32" or sys.platform == "darwin": else: def proxy_bypass(host: str, proxies: Mapping[str, str] | None = None) -> Any: ... # undocumented -class Request: +class Request(Generic[_DataT]): @property def full_url(self) -> str: ... @full_url.setter @@ -133,21 +137,41 @@ class Request: host: str origin_req_host: str selector: str - data: _DataType + + @property + def data(self) -> _DataT: ... + @data.setter + def data(self, data: _DataT) -> None: ... + @data.deleter + def data(self) -> None: ... + headers: MutableMapping[str, str] unredirected_hdrs: dict[str, str] unverifiable: bool method: str | None timeout: float | None # Undocumented, only set after __init__() by OpenerDirector.open() + + @overload + def __init__( + self: Request[_DataType], + url: str, + data: None = None, + headers: SupportsItems[str, str] = {}, + origin_req_host: str | None = None, + unverifiable: bool = False, + method: str | None = None, + ) -> None: ... + @overload def __init__( self, url: str, - data: _DataType = None, + data: _DataT, headers: SupportsItems[str, str] = {}, origin_req_host: str | None = None, unverifiable: bool = False, method: str | None = None, ) -> None: ... + def get_method(self) -> str: ... def add_header(self, key: str, val: str) -> None: ... def add_unredirected_header(self, key: str, val: str) -> None: ...