diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 815617e1c19..a9797ac9057 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,18 @@ jobs: - run: pip install nox - run: nox -s docs + typecheck: + name: typecheck + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - run: pip install nox + - run: nox -s typecheck + determine-changes: runs-on: ubuntu-22.04 outputs: @@ -252,6 +264,7 @@ jobs: needs: - determine-changes + - typecheck - docs - packaging - tests-unix diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c43dc5ef175..6b49a35af3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,22 +27,6 @@ repos: - id: ruff-check args: [--fix] -- repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 - hooks: - - id: mypy - exclude: tests/data - args: ["--pretty", "--show-error-codes"] - additional_dependencies: [ - 'keyring==24.2.0', - 'nox==2024.03.02', - 'pytest', - 'types-docutils==0.20.0.3', - 'types-setuptools==68.2.0.0', - 'types-freezegun==1.1.10', - 'types-pyyaml==6.0.12.12', - ] - - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index bc483997a64..a17d35d3832 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -148,6 +148,23 @@ To use linters locally, run: readability problems. +Type Checking +============= + +pip uses :pypi:`mypy` for static type checking to help catch type errors early. +Type checking is configured to run across the entire codebase to ensure type safety. + +To run type checking locally: + +.. code-block:: console + + $ nox -s typecheck + +This will run mypy on the ``src/pip``, ``tests``, and ``tools`` directories, +as well as ``noxfile.py``. Type checking helps maintain code quality by +catching type-related issues before they make it into the codebase. + + Running pip under a debugger ============================ diff --git a/noxfile.py b/noxfile.py index 160e823e9f1..a63136ba858 100644 --- a/noxfile.py +++ b/noxfile.py @@ -24,9 +24,6 @@ "common-wheels": "tests/data/common_wheels", "protected-pip": "tools/protected_pip.py", } -REQUIREMENTS = { - "docs": "docs/requirements.txt", -} AUTHORS_FILE = "AUTHORS.txt" VERSION_FILE = "src/pip/__init__.py" @@ -132,7 +129,7 @@ def test(session: nox.Session) -> None: @nox.session def docs(session: nox.Session) -> None: - session.install("-r", REQUIREMENTS["docs"]) + session.install("--group", "docs") def get_sphinx_build_command(kind: str) -> list[str]: # Having the conf.py in the docs/html is weird but needed because we @@ -161,7 +158,7 @@ def get_sphinx_build_command(kind: str) -> list[str]: @nox.session(name="docs-live") def docs_live(session: nox.Session) -> None: - session.install("-r", REQUIREMENTS["docs"], "sphinx-autobuild") + session.install("--group", "docs", "sphinx-autobuild") session.run( "sphinx-autobuild", @@ -174,6 +171,26 @@ def docs_live(session: nox.Session) -> None: ) +@nox.session +def typecheck(session: nox.Session) -> None: + # Install test and test-types dependency groups + run_with_protected_pip( + session, + "install", + "--group", + "all", + ) + + session.run( + "mypy", + "src/pip", + "tests", + "tools", + "noxfile.py", + "--exclude=tests/data", + ) + + @nox.session def lint(session: nox.Session) -> None: session.install("pre-commit") @@ -267,7 +284,7 @@ def coverage(session: nox.Session) -> None: run_with_protected_pip(session, "install", ".") # Install test dependencies - run_with_protected_pip(session, "install", "-r", REQUIREMENTS["tests"]) + run_with_protected_pip(session, "install", "--group", "docs") if not os.path.exists(".coverage-output"): os.mkdir(".coverage-output") diff --git a/pyproject.toml b/pyproject.toml index 019cab5eb3f..f64d95df8a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,48 @@ test-common-wheels = [ "pytest-subket >= 0.8.1", ] +docs = [ + "sphinx ~= 7.0", + # currently incompatible with sphinxcontrib-towncrier + # https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 + "towncrier < 24", + "furo", + "myst_parser", + "sphinx-copybutton", + "sphinx-inline-tabs", + "sphinxcontrib-towncrier >= 0.2.0a0", + "sphinx-issues" +] + +# Libraries that are not required for pip to run, +# but are used in some optional features. +all-optional = ["keyring"] + +nox = ["nox"] # noxfile.py +update-rtd-redirects = ["httpx", "rich", "PyYAML"] # tools/update-rtd-redirects.py + +type-checking = [ + # Actual type checker: + "mypy", + + # Stub libraries that contain type hints as a separate package: + "types-docutils", # via sphinx (test dependency) + "types-requests", # vendored + "types-urllib3", # vendored (can be removed when we upgrade to urllib3 >= 2.0) + "types-setuptools", # test dependency and used in distutils_hack + "types-six", # via python-dateutil via freezegun (test dependency) + "types-PyYAML", # update-rtd-redirects dependency +] + +all = [ + {include-group = "test"}, + {include-group = "docs"}, + {include-group = "nox"}, + {include-group = "all-optional"}, + {include-group = "update-rtd-redirects"}, + {include-group = "type-checking"}, +] + [tool.setuptools] package-dir = {"" = "src"} include-package-data = false diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 056eb8866a0..dae7211fbb8 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -7,9 +7,9 @@ import shutil import site from optparse import SUPPRESS_HELP, Values +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.requests.exceptions import InvalidProxyURL from pip._vendor.rich import print_json # Eagerly import self_outdated_check to avoid crashes. Otherwise, @@ -60,6 +60,12 @@ ) from pip._internal.wheel_builder import build, should_build_for_install_command +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.exceptions import InvalidProxyURL +else: + from pip._vendor.requests.exceptions import InvalidProxyURL + logger = getLogger(__name__) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 98f95494c62..bfc1a7266b1 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -27,7 +27,8 @@ if TYPE_CHECKING: from hashlib import _Hash - from pip._vendor.requests.models import Request, Response + # Vendored libraries with type stubs + from requests.models import PreparedRequest, Request, Response from pip._internal.metadata import BaseDistribution from pip._internal.network.download import _FileDownload @@ -297,7 +298,7 @@ def __init__( self, error_msg: str, response: Response | None = None, - request: Request | None = None, + request: Request | PreparedRequest | None = None, ) -> None: """ Initialize NetworkConnectionError with `request` and `response` diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 00d66daa3bf..e506deae445 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -18,15 +18,12 @@ from html.parser import HTMLParser from optparse import Values from typing import ( + TYPE_CHECKING, Callable, NamedTuple, Protocol, ) -from pip._vendor import requests -from pip._vendor.requests import Response -from pip._vendor.requests.exceptions import RetryError, SSLError - from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope @@ -38,6 +35,15 @@ from .sources import CandidatesFromPage, LinkSource, build_source +if TYPE_CHECKING: + # Vendored libraries with type stubs + import requests + from requests import Response + from requests.exceptions import RetryError, SSLError +else: + from pip._vendor import requests + from pip._vendor.requests.exceptions import RetryError, SSLError + logger = logging.getLogger(__name__) ResponseHeaders = MutableMapping[str, str] @@ -80,6 +86,7 @@ def _ensure_api_header(response: Response) -> None: ): return + assert response.request.method is not None raise _NotAPIContent(content_type, response.request.method) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 9f2c4fe316c..4b3ef478fae 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -6,7 +6,7 @@ import pathlib import sys import sysconfig -from typing import Any +from typing import TYPE_CHECKING, Any from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.compat import WINDOWS @@ -80,7 +80,7 @@ def _looks_like_bpo_44860() -> bool: See . """ - from distutils.command.install import INSTALL_SCHEMES + from distutils.command.install import INSTALL_SCHEMES # type: ignore try: unix_user_platlib = INSTALL_SCHEMES["unix_user"]["platlib"] @@ -105,7 +105,7 @@ def _looks_like_red_hat_lib() -> bool: This is the only way I can see to tell a Red Hat-patched Python. """ - from distutils.command.install import INSTALL_SCHEMES + from distutils.command.install import INSTALL_SCHEMES # type: ignore return all( k in INSTALL_SCHEMES @@ -117,7 +117,7 @@ def _looks_like_red_hat_lib() -> bool: @functools.cache def _looks_like_debian_scheme() -> bool: """Debian adds two additional schemes.""" - from distutils.command.install import INSTALL_SCHEMES + from distutils.command.install import INSTALL_SCHEMES # type: ignore return "deb_system" in INSTALL_SCHEMES and "unix_local" in INSTALL_SCHEMES @@ -131,8 +131,13 @@ def _looks_like_red_hat_scheme() -> bool: (fortunately?) done quite unconditionally, so we create a default command object without any configuration to detect this. """ - from distutils.command.install import install - from distutils.dist import Distribution + if TYPE_CHECKING: + # Vendored libraries with type stubs + from setuptools._distutils.command.install import install + from setuptools._distutils.dist import Distribution + else: + from distutils.command.install import install + from distutils.dist import Distribution cmd: Any = install(Distribution()) cmd.finalize_options() diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index 28c066bcee6..dc5addbb775 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -19,10 +19,8 @@ import logging import os import sys -from distutils.cmd import Command as DistutilsCommand -from distutils.command.install import SCHEME_KEYS -from distutils.command.install import install as distutils_install_command -from distutils.sysconfig import get_python_lib +from distutils.command.install import SCHEME_KEYS # type: ignore +from typing import TYPE_CHECKING, cast from pip._internal.models.scheme import Scheme from pip._internal.utils.compat import WINDOWS @@ -30,6 +28,19 @@ from .base import get_major_minor_version +if TYPE_CHECKING: + # Vendored libraries with type stubs + from setuptools._distutils.cmd import Command as DistutilsCommand + from setuptools._distutils.command.install import ( + install as distutils_install_command, + ) + from setuptools._distutils.dist import Distribution # noqa: F401 + from setuptools._distutils.sysconfig import get_python_lib +else: + from distutils.command.install import install as distutils_install_command + from distutils.sysconfig import get_python_lib + + logger = logging.getLogger(__name__) @@ -65,7 +76,7 @@ def distutils_scheme( obj: DistutilsCommand | None = None obj = d.get_command_obj("install", create=True) assert obj is not None - i: distutils_install_command = obj + i = cast(distutils_install_command, obj) # NOTE: setting user or home has the side-effect of creating the home dir # or user base for installations during finalize_options() # ideally, we'd prefer a scheme class that has no side-effects. diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 89fce8b6e5d..70a3e9f78ed 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -125,7 +125,7 @@ def from_metadata_file_contents( } dist = pkg_resources.DistInfoDistribution( location=filename, - metadata=InMemoryMetadata(metadata_dict, filename), + metadata=InMemoryMetadata(metadata_dict, filename), # type: ignore[arg-type] project_name=project_name, ) return cls(dist) @@ -146,7 +146,7 @@ def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution: raise UnsupportedWheel(f"{name} has an invalid wheel, {e}") dist = pkg_resources.DistInfoDistribution( location=wheel.location, - metadata=InMemoryMetadata(metadata_dict, wheel.location), + metadata=InMemoryMetadata(metadata_dict, wheel.location), # type: ignore[arg-type] project_name=name, ) return cls(dist) @@ -176,7 +176,7 @@ def installed_by_distutils(self) -> bool: # provider has a "path" attribute not present anywhere else. Not the # best introspection logic, but pip has been doing this for a long time. try: - return bool(self._dist._provider.path) + return bool(self._dist._provider.path) # type: ignore except AttributeError: return False diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index a42f7024c4c..a715c1a0b35 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -17,11 +17,7 @@ from functools import cache from os.path import commonprefix from pathlib import Path -from typing import Any, NamedTuple - -from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth -from pip._vendor.requests.models import Request, Response -from pip._vendor.requests.utils import get_netrc_auth +from typing import TYPE_CHECKING, Any, NamedTuple from pip._internal.utils.logging import getLogger from pip._internal.utils.misc import ( @@ -33,6 +29,16 @@ ) from pip._internal.vcs.versioncontrol import AuthInfo +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.auth import AuthBase, HTTPBasicAuth + from requests.models import PreparedRequest, Response + from requests.utils import get_netrc_auth +else: + from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth + from pip._vendor.requests.utils import get_netrc_auth + + logger = getLogger(__name__) KEYRING_DISABLED = False @@ -437,8 +443,9 @@ def _get_url_and_credentials( return url, username, password - def __call__(self, req: Request) -> Request: + def __call__(self, req: PreparedRequest) -> PreparedRequest: # Get credentials for this request + assert req.url is not None, f"{req} URL should not be None" url, username, password = self._get_url_and_credentials(req.url) # Set the url of the request to the url without any credentials diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index 0c5961c45b4..01b4968350f 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -7,15 +7,18 @@ from collections.abc import Generator from contextlib import contextmanager from datetime import datetime -from typing import Any, BinaryIO, Callable +from typing import TYPE_CHECKING, Any, BinaryIO, Callable from pip._vendor.cachecontrol.cache import SeparateBodyBaseCache from pip._vendor.cachecontrol.caches import SeparateBodyFileCache -from pip._vendor.requests.models import Response from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import ensure_dir +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.models import Response + def is_from_cache(response: Response) -> bool: return getattr(response, "from_cache", False) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 9881cc285fa..f0dbfcf216b 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -9,13 +9,7 @@ from collections.abc import Iterable, Mapping from dataclasses import dataclass from http import HTTPStatus -from typing import BinaryIO - -from pip._vendor.requests import PreparedRequest -from pip._vendor.requests.models import Response -from pip._vendor.urllib3 import HTTPResponse as URLlib3Response -from pip._vendor.urllib3._collections import HTTPHeaderDict -from pip._vendor.urllib3.exceptions import ReadTimeoutError +from typing import TYPE_CHECKING, BinaryIO from pip._internal.cli.progress_bars import BarType, get_download_progress_renderer from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError @@ -26,6 +20,19 @@ from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests import PreparedRequest + from requests.models import Response + from urllib3 import HTTPResponse as URLlib3Response + from urllib3._collections import HTTPHeaderDict + from urllib3.exceptions import ReadTimeoutError +else: + from pip._vendor.requests import PreparedRequest + from pip._vendor.urllib3 import HTTPResponse as URLlib3Response + from pip._vendor.urllib3._collections import HTTPHeaderDict + from pip._vendor.urllib3.exceptions import ReadTimeoutError + logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index ac3ebe63c9b..152e85ab2b2 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -8,16 +8,21 @@ from collections.abc import Generator from contextlib import contextmanager from tempfile import NamedTemporaryFile -from typing import Any +from typing import TYPE_CHECKING, Any from zipfile import BadZipFile, ZipFile from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution from pip._internal.network.session import PipSession from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.models import CONTENT_CHUNK_SIZE, Response +else: + from pip._vendor.requests.models import CONTENT_CHUNK_SIZE + class HTTPRangeRequestUnsupported(Exception): pass diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index a1f9444e37b..2d9aaf16241 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -24,17 +24,9 @@ Any, Optional, Union, + cast, ) -from pip._vendor import requests, urllib3 -from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter -from pip._vendor.requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter -from pip._vendor.requests.adapters import HTTPAdapter as _BaseHTTPAdapter -from pip._vendor.requests.models import PreparedRequest, Response -from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._vendor.urllib3.connectionpool import ConnectionPool -from pip._vendor.urllib3.exceptions import InsecureRequestWarning - from pip import __version__ from pip._internal.metadata import get_default_environment from pip._internal.models.link import Link @@ -50,8 +42,28 @@ if TYPE_CHECKING: from ssl import SSLContext - from pip._vendor.urllib3.poolmanager import PoolManager - from pip._vendor.urllib3.proxymanager import ProxyManager + # Vendored libraries with type stubs + import requests + import urllib3 + from requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter + from requests.adapters import HTTPAdapter as _BaseHTTPAdapter + from requests.models import PreparedRequest, Response + from requests.structures import CaseInsensitiveDict + from urllib3 import ProxyManager + from urllib3.connectionpool import ConnectionPool + from urllib3.exceptions import InsecureRequestWarning + from urllib3.poolmanager import PoolManager + + from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter + +else: + from pip._vendor import requests, urllib3 + from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter + from pip._vendor.requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter + from pip._vendor.requests.adapters import HTTPAdapter as _BaseHTTPAdapter + from pip._vendor.requests.models import PreparedRequest, Response + from pip._vendor.requests.structures import CaseInsensitiveDict + from pip._vendor.urllib3.exceptions import InsecureRequestWarning logger = logging.getLogger(__name__) @@ -212,11 +224,12 @@ def send( self, request: PreparedRequest, stream: bool = False, - timeout: float | tuple[float, float] | None = None, + timeout: float | tuple[float, float] | tuple[float, None] | None = None, verify: bool | str = True, - cert: str | tuple[str, str] | None = None, + cert: bytes | str | tuple[bytes | str, bytes | str] | None = None, proxies: Mapping[str, str] | None = None, ) -> Response: + assert request.url is not None pathname = url_to_path(request.url) resp = Response() @@ -237,13 +250,13 @@ def send( resp.headers = CaseInsensitiveDict( { "Content-Type": content_type, - "Content-Length": stats.st_size, + "Content-Length": str(stats.st_size), "Last-Modified": modified, } ) resp.raw = open(pathname, "rb") - resp.close = resp.raw.close + resp.close = resp.raw.close # type: ignore[method-assign] return resp @@ -323,6 +336,9 @@ def cert_verify( class PipSession(requests.Session): + # Let the type checker know that we are using a custom auth handler. + auth: MultiDomainBasicAuth + timeout: int | None = None def __init__( @@ -354,7 +370,7 @@ def __init__( # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. - retries = urllib3.Retry( + retry = urllib3.Retry( # Set the total number of retries that a particular request can # have. total=retries, @@ -369,14 +385,14 @@ def __init__( # Add a small amount of back off between failed requests in # order to prevent hammering the service. backoff_factor=0.25, - ) # type: ignore + ) # Our Insecure HTTPAdapter disables HTTPS validation. It does not # support caching so we'll use it for all http:// URLs. # If caching is disabled, we will also use it for # https:// hosts that we've marked as ignoring # TLS errors for (trusted-hosts). - insecure_adapter = InsecureHTTPAdapter(max_retries=retries) + insecure_adapter = InsecureHTTPAdapter(max_retries=retry) # We want to _only_ cache responses on securely fetched origins or when # the host is specified as trusted. We do this because @@ -384,17 +400,23 @@ def __init__( # origin, and we don't want someone to be able to poison the cache and # require manual eviction from the cache to fix it. if cache: - secure_adapter = CacheControlAdapter( - cache=SafeFileCache(cache), - max_retries=retries, - ssl_context=ssl_context, + secure_adapter = cast( + HTTPAdapter, + CacheControlAdapter( + cache=SafeFileCache(cache), + max_retries=retry, + ssl_context=ssl_context, + ), ) - self._trusted_host_adapter = InsecureCacheControlAdapter( - cache=SafeFileCache(cache), - max_retries=retries, + self._trusted_host_adapter = cast( + HTTPAdapter, + InsecureCacheControlAdapter( + cache=SafeFileCache(cache), + max_retries=retry, + ), ) else: - secure_adapter = HTTPAdapter(max_retries=retries, ssl_context=ssl_context) + secure_adapter = HTTPAdapter(max_retries=retry, ssl_context=ssl_context) self._trusted_host_adapter = insecure_adapter self.mount("https://", secure_adapter) @@ -518,7 +540,9 @@ def is_secure_origin(self, location: Link) -> bool: return False - def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response: + def request( # type: ignore[override] + self, method: str, url: str, *args: Any, **kwargs: Any + ) -> Response: # Allow setting a default timeout on a session kwargs.setdefault("timeout", self.timeout) # Allow setting a default proxies on a session diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index 74d3111cff0..6f49d759bed 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -1,9 +1,14 @@ -from collections.abc import Generator +from __future__ import annotations -from pip._vendor.requests.models import Response +from collections.abc import Generator +from typing import TYPE_CHECKING from pip._internal.exceptions import NetworkConnectionError +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.models import Response + # The following comments and HTTP headers were originally added by # Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03. # diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index f4bddb48a1d..b82868fbaa1 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -3,7 +3,8 @@ import logging import urllib.parse import xmlrpc.client -from typing import TYPE_CHECKING +from http.client import HTTPResponse +from typing import TYPE_CHECKING, cast from pip._internal.exceptions import NetworkConnectionError from pip._internal.network.session import PipSession @@ -44,13 +45,13 @@ def request( headers = {"Content-Type": "text/xml"} response = self._session.post( url, - data=request_body, + data=cast(bytes, request_body), headers=headers, stream=True, ) raise_for_status(response) self.verbose = verbose - return self.parse_response(response.raw) + return self.parse_response(cast(HTTPResponse, response.raw)) except NetworkConnectionError as exc: assert exc.response logger.critical( diff --git a/tests/lib/server.py b/tests/lib/server.py index 4dfa03179e7..24d553ae74d 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -5,7 +5,7 @@ from collections.abc import Iterable, Iterator from contextlib import ExitStack, contextmanager from textwrap import dedent -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, cast from unittest.mock import Mock from werkzeug.serving import BaseWSGIServer, WSGIRequestHandler @@ -99,7 +99,7 @@ def make_mock_server(**kwargs: Any) -> _MockServer: mock = Mock() app = _mock_wsgi_adapter(mock) - server = _make_server("localhost", 0, app=app, **kwargs) + server = cast(_MockServer, _make_server("localhost", 0, app=app, **kwargs)) server.mock = mock return server diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index 6e17fce1d54..c035a2b6c7d 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -21,8 +21,6 @@ ) from zipfile import ZipFile -from pip._vendor.requests.structures import CaseInsensitiveDict - from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution # As would be used in metadata @@ -85,13 +83,11 @@ def make_metadata_file( if value is not _default: return File(path, ensure_binary(value)) - metadata = CaseInsensitiveDict( - { - "Metadata-Version": "2.1", - "Name": name, - "Version": version, - } - ) + metadata: dict[str, HeaderValue] = { + "Metadata-Version": "2.1", + "Name": name, + "Version": version, + } if updates is not _default: metadata.update(updates) @@ -117,14 +113,12 @@ def make_wheel_metadata_file( if value is not _default: return File(path, ensure_binary(value)) - metadata = CaseInsensitiveDict( - { - "Wheel-Version": "1.0", - "Generator": "pip-test-suite", - "Root-Is-Purelib": "true", - "Tag": ["-".join(parts) for parts in tags], - } - ) + metadata: dict[str, HeaderValue] = { + "Wheel-Version": "1.0", + "Generator": "pip-test-suite", + "Root-Is-Purelib": "true", + "Tag": ["-".join(parts) for parts in tags], + } if updates is not _default: metadata.update(updates) diff --git a/tests/unit/metadata/test_metadata_pkg_resources.py b/tests/unit/metadata/test_metadata_pkg_resources.py index ccb0b7dcf0f..79999664546 100644 --- a/tests/unit/metadata/test_metadata_pkg_resources.py +++ b/tests/unit/metadata/test_metadata_pkg_resources.py @@ -18,6 +18,7 @@ ) pkg_resources = pytest.importorskip("pip._vendor.pkg_resources") +from pip._vendor.pkg_resources import WorkingSet # noqa: E402 def _dist_is_local(dist: mock.Mock) -> bool: @@ -71,13 +72,13 @@ def require(self, name: str) -> None: ) def test_get_distribution(ws: _MockWorkingSet, req_name: str) -> None: """Ensure get_distribution() finds all kinds of distributions.""" - dist = Environment(ws).get_distribution(req_name) + dist = Environment(cast(WorkingSet, ws)).get_distribution(req_name) assert dist is not None assert cast(Distribution, dist)._dist.project_name == req_name def test_get_distribution_nonexist() -> None: - dist = Environment(workingset).get_distribution("non-exist") + dist = Environment(cast(WorkingSet, workingset)).get_distribution("non-exist") assert dist is None diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index d95fa10fea3..f5bcf02b3ba 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -8,11 +8,11 @@ import uuid from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING from unittest import mock import pytest -from pip._vendor import requests from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import NetworkConnectionError @@ -47,6 +47,12 @@ skip_needs_old_urlun_behavior_win, ) +if TYPE_CHECKING: + # Vendored libraries with type stubs + import requests +else: + from pip._vendor import requests + ACCEPT = ", ".join( [ "application/vnd.pypi.simple.v1+json", diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index e28edf6449c..2f5755e520c 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,13 +1,18 @@ import errno +from typing import TYPE_CHECKING from unittest import mock import pytest -from pip._vendor.requests.exceptions import InvalidProxyURL - from pip._internal.commands import install from pip._internal.commands.install import create_os_error_message, decide_user_install +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.exceptions import InvalidProxyURL +else: + from pip._vendor.requests.exceptions import InvalidProxyURL + class TestDecideUserInstall: @mock.patch("site.ENABLE_USER_SITE", True) diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index 485d27476be..70ba78147ec 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -10,13 +10,17 @@ import sysconfig import tempfile from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import Mock import pytest from pip._internal.locations import SCHEME_KEYS, _should_use_sysconfig, get_scheme +if TYPE_CHECKING: + # Vendored libraries with type stubs + from setuptools._distutils.dist import Distribution # noqa: F401 + if sys.platform == "win32": pwd = Mock() else: @@ -129,6 +133,7 @@ def test_distutils_config_file_read( f = tmpdir / "config" / "setup.cfg" f.parent.mkdir() f.write_text("[install]\ninstall-scripts=" + install_scripts) + from distutils.dist import Distribution # patch the function that returns what config files are present diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py index 97faa5dfbc4..10cc627729e 100644 --- a/tests/unit/test_network_auth.py +++ b/tests/unit/test_network_auth.py @@ -5,7 +5,7 @@ import subprocess import sys from collections.abc import Iterable -from typing import Any +from typing import TYPE_CHECKING, Any, cast import pytest @@ -14,6 +14,11 @@ from tests.lib.requests_mocks import MockConnection, MockRequest, MockResponse +if TYPE_CHECKING: + from requests.models import Response +else: + from pip._vendor.requests.models import Response + @pytest.fixture(autouse=True) def reset_keyring() -> Iterable[None]: @@ -320,7 +325,7 @@ def _send(sent_req: MockRequest, **kwargs: Any) -> MockResponse: resp.status_code = 401 resp.connection = connection - auth.handle_401(resp) + auth.handle_401(cast("Response", resp)) if expect_save: assert keyring.saved_passwords == [("example.com", creds[0], creds[1])] @@ -536,7 +541,7 @@ def _send(sent_req: MockRequest, **kwargs: Any) -> MockResponse: resp.status_code = 401 resp.connection = connection - auth.handle_401(resp) + auth.handle_401(cast("Response", resp)) if expect_save: assert keyring.saved_passwords == [("example.com", creds[0], creds[1])] diff --git a/tests/unit/test_network_download.py b/tests/unit/test_network_download.py index 7fb6b7fe64b..d614879048a 100644 --- a/tests/unit/test_network_download.py +++ b/tests/unit/test_network_download.py @@ -3,6 +3,7 @@ import logging import sys from pathlib import Path +from typing import TYPE_CHECKING, cast from unittest.mock import MagicMock, call, patch import pytest @@ -21,6 +22,11 @@ from tests.lib.requests_mocks import MockResponse +if TYPE_CHECKING: + from requests.models import Response +else: + from pip._vendor.requests.models import Response + @pytest.mark.parametrize( "url, headers, from_cache, range_start, expected", @@ -91,9 +97,9 @@ def test_log_download( if from_cache: resp.from_cache = from_cache link = Link(url) - total_length = _get_http_response_size(resp) + total_length = _get_http_response_size(cast("Response", resp)) _log_download( - resp, + cast("Response", resp), link, progress_bar="on", total_length=total_length, diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index b48be71fc51..ee118622fac 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -3,14 +3,12 @@ import logging import os from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse from urllib.request import getproxies import pytest -from pip._vendor import requests - from pip import __version__ from pip._internal.models.link import Link from pip._internal.network.session import ( @@ -19,12 +17,18 @@ user_agent, ) +if TYPE_CHECKING: + # Vendored libraries with type stubs + import requests +else: + from pip._vendor import requests + def get_user_agent() -> str: # These tests are testing the computation of the user agent, so we want to # avoid reusing cached values. user_agent.cache_clear() - return PipSession().headers["User-Agent"] + return str(PipSession().headers["User-Agent"]) def test_user_agent() -> None: diff --git a/tests/unit/test_network_utils.py b/tests/unit/test_network_utils.py index 5911583feec..ca9d1956b78 100644 --- a/tests/unit/test_network_utils.py +++ b/tests/unit/test_network_utils.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING, cast + import pytest from pip._internal.exceptions import NetworkConnectionError @@ -5,6 +7,11 @@ from tests.lib.requests_mocks import MockResponse +if TYPE_CHECKING: + from requests.models import Response +else: + from pip._vendor.requests.models import Response + @pytest.mark.parametrize( "status_code, error_type", @@ -20,7 +27,7 @@ def test_raise_for_status_raises_exception(status_code: int, error_type: str) -> resp.url = "http://www.example.com/whatever.tgz" resp.reason = "Network Error" with pytest.raises(NetworkConnectionError) as excinfo: - raise_for_status(resp) + raise_for_status(cast("Response", resp)) assert str(excinfo.value) == ( f"{status_code} {error_type}: Network Error for url:" " http://www.example.com/whatever.tgz" @@ -33,4 +40,4 @@ def test_raise_for_status_does_not_raises_exception() -> None: resp.status_code = 201 resp.url = "http://www.example.com/whatever.tgz" resp.reason = "No error" - raise_for_status(resp) + raise_for_status(cast("Response", resp)) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 7d58ec61648..9535d6247ad 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -3,7 +3,7 @@ from pathlib import Path from shutil import rmtree from tempfile import mkdtemp -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import Mock, patch import pytest @@ -18,6 +18,11 @@ from tests.lib import TestData from tests.lib.requests_mocks import MockResponse +if TYPE_CHECKING: + from requests.models import Response +else: + from pip._vendor.requests.models import Response + def test_unpack_url_with_urllib_response_without_content_type(data: TestData) -> None: """ @@ -25,7 +30,7 @@ def test_unpack_url_with_urllib_response_without_content_type(data: TestData) -> """ _real_session = PipSession() - def _fake_session_get(*args: Any, **kwargs: Any) -> dict[str, str]: + def _fake_session_get(*args: Any, **kwargs: Any) -> Response: resp = _real_session.get(*args, **kwargs) del resp.headers["Content-Type"] return resp