From f22e3b766b6bf5eeabedbbbdcaae0027b7118734 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 21 Jul 2024 10:43:13 +0100 Subject: [PATCH 01/21] Move type checking outside pre-commit Invoke mypy with specific directories rather than a list of files found by pre-commit in the repository. This makes it catch more errors and issues. --- .github/workflows/ci.yml | 13 +++++++++++++ .pre-commit-config.yaml | 4 ++-- noxfile.py | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) 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..1d7f8afee20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,14 +28,14 @@ repos: args: [--fix] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.1 + rev: v1.10.0 hooks: - id: mypy exclude: tests/data args: ["--pretty", "--show-error-codes"] additional_dependencies: [ 'keyring==24.2.0', - 'nox==2024.03.02', + 'nox==2023.4.22', 'pytest', 'types-docutils==0.20.0.3', 'types-setuptools==68.2.0.0', diff --git a/noxfile.py b/noxfile.py index 160e823e9f1..3c36cda88c6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -174,6 +174,29 @@ def docs_live(session: nox.Session) -> None: ) +@nox.session +def typecheck(session: nox.Session) -> None: + session.install( + "mypy", + "keyring", + "nox", + "pytest", + "types-docutils", + "types-setuptools", + "types-freezegun", + "types-pyyaml", + ) + + session.run( + "mypy", + "src/pip", + "tests", + "tools", + "noxfile.py", + "--exclude=tests/data", + ) + + @nox.session def lint(session: nox.Session) -> None: session.install("pre-commit") From 63d785cfe7488bbdd1fef347284a1e3c0fafe6cd Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Thu, 10 Jul 2025 23:20:18 -0400 Subject: [PATCH 02/21] Create full dependency group for type checking --- noxfile.py | 24 +++++++++--------------- pyproject.toml | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/noxfile.py b/noxfile.py index 3c36cda88c6..ab694220e07 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", @@ -176,15 +173,12 @@ def docs_live(session: nox.Session) -> None: @nox.session def typecheck(session: nox.Session) -> None: - session.install( - "mypy", - "keyring", - "nox", - "pytest", - "types-docutils", - "types-setuptools", - "types-freezegun", - "types-pyyaml", + # Install test and test-types dependency groups + run_with_protected_pip( + session, + "install", + "--group", + "type-check", ) session.run( @@ -290,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 90f04307f47..7e24fedfd55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,45 @@ test-common-wheels = [ "coverage >= 4.4", ] +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"] # tools/update-rtd-redirects.py + +type-check = [ + # 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) + + # Everything else that could contain type hints: + {include-group = "test"}, + {include-group = "docs"}, + {include-group = "nox"}, + {include-group = "all-optional"}, + {include-group = "update-rtd-redirects"}, +] + [tool.setuptools] package-dir = {"" = "src"} include-package-data = false From e8b735fd3788ddba71b56b549ae0c9c0c8407628 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:09:17 -0400 Subject: [PATCH 03/21] Move all vendored libraries with stubs into TYPE_CHECKING block --- pyproject.toml | 2 +- src/pip/_internal/commands/install.py | 8 +++++- src/pip/_internal/exceptions.py | 3 ++- src/pip/_internal/index/collector.py | 14 +++++++--- src/pip/_internal/locations/__init__.py | 11 +++++--- src/pip/_internal/locations/_distutils.py | 20 +++++++++++--- src/pip/_internal/network/auth.py | 16 +++++++---- src/pip/_internal/network/cache.py | 7 +++-- src/pip/_internal/network/download.py | 21 ++++++++++----- src/pip/_internal/network/lazy_wheel.py | 9 +++++-- src/pip/_internal/network/session.py | 33 +++++++++++++++-------- src/pip/_internal/network/utils.py | 9 +++++-- tests/lib/wheel.py | 10 +++++-- tests/unit/test_collector.py | 8 +++++- tests/unit/test_command_install.py | 9 +++++-- tests/unit/test_locations.py | 13 ++++++--- tests/unit/test_network_session.py | 10 ++++--- 17 files changed, 149 insertions(+), 54 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7e24fedfd55..6223cc7fff5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ docs = [ "sphinx-issues" ] -# Libraries that are not required for pip to run, +# Libraries that are not required for pip to run, # but are used in some optional features. all-optional = ["keyring"] 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..1280ecf435e 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 Request, Response from pip._internal.metadata import BaseDistribution from pip._internal.network.download import _FileDownload diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 00d66daa3bf..8904fb04960 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] diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 9f2c4fe316c..bee5f0b4773 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 @@ -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..77324d67a0d 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 typing import TYPE_CHECKING 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__) @@ -46,7 +57,8 @@ def distutils_scheme( """ Return a distutils install scheme """ - from distutils.dist import Distribution + if not TYPE_CHECKING: + from distutils.dist import Distribution dist_args: dict[str, str | list[str]] = {"name": dist_name} if isolated: diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index a42f7024c4c..e331453f227 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 Request, 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 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..705f3ff7581 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -26,15 +26,6 @@ Union, ) -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 +41,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__) 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/tests/lib/wheel.py b/tests/lib/wheel.py index 6e17fce1d54..44e81a679ab 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -15,16 +15,22 @@ from io import BytesIO, StringIO from pathlib import Path from typing import ( + TYPE_CHECKING, AnyStr, TypeVar, Union, ) from zipfile import ZipFile -from pip._vendor.requests.structures import CaseInsensitiveDict - from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution +if TYPE_CHECKING: + # Vendored libraries with type stubs + from requests.structures import CaseInsensitiveDict +else: + from pip._vendor.requests.structures import CaseInsensitiveDict + + # As would be used in metadata HeaderValue = Union[str, list[str]] 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..9987531d7e6 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,7 +133,9 @@ 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 + + if not TYPE_CHECKING: + from distutils.dist import Distribution # patch the function that returns what config files are present monkeypatch.setattr( @@ -155,7 +161,8 @@ def test_install_lib_takes_precedence( f = tmpdir / "config" / "setup.cfg" f.parent.mkdir() f.write_text("[install]\ninstall-lib=" + install_lib) - from distutils.dist import Distribution + if not TYPE_CHECKING: + from distutils.dist import Distribution # patch the function that returns what config files are present monkeypatch.setattr( diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index b48be71fc51..26ee0b6b70b 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,6 +17,12 @@ 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 From 9931fb53a19be332582510b9310d43b9bb2a8ab8 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:10:17 -0400 Subject: [PATCH 04/21] Add type ignores to distutils.command.install SCHEMES --- src/pip/_internal/locations/__init__.py | 6 +++--- src/pip/_internal/locations/_distutils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index bee5f0b4773..4b3ef478fae 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -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 diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index 77324d67a0d..a0db7d4488a 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -19,7 +19,7 @@ import logging import os import sys -from distutils.command.install import SCHEME_KEYS +from distutils.command.install import SCHEME_KEYS # type: ignore from typing import TYPE_CHECKING from pip._internal.models.scheme import Scheme From 02c8b5c742c63cd6bd71dbbddf56a723914ddc13 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:12:06 -0400 Subject: [PATCH 05/21] Fix all "Incompatible types in assignment" --- src/pip/_internal/exceptions.py | 4 ++-- src/pip/_internal/locations/_distutils.py | 4 ++-- src/pip/_internal/network/session.py | 28 +++++++++++++++-------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 1280ecf435e..bfc1a7266b1 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -28,7 +28,7 @@ from hashlib import _Hash # Vendored libraries with type stubs - from requests.models import Request, Response + from requests.models import PreparedRequest, Request, Response from pip._internal.metadata import BaseDistribution from pip._internal.network.download import _FileDownload @@ -298,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/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index a0db7d4488a..3980a8ce35f 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -20,7 +20,7 @@ import os import sys from distutils.command.install import SCHEME_KEYS # type: ignore -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from pip._internal.models.scheme import Scheme from pip._internal.utils.compat import WINDOWS @@ -77,7 +77,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/network/session.py b/src/pip/_internal/network/session.py index 705f3ff7581..26fafaa7a57 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -24,6 +24,7 @@ Any, Optional, Union, + cast, ) from pip import __version__ @@ -223,15 +224,16 @@ 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: pathname = url_to_path(request.url) resp = Response() resp.status_code = 200 + assert request.url is not None resp.url = request.url try: @@ -361,7 +363,7 @@ def __init__( self.headers["User-Agent"] = user_agent() # Attach our Authentication handler to the session - self.auth = MultiDomainBasicAuth(index_urls=index_urls) + self.auth = MultiDomainBasicAuth(index_urls=index_urls) # type: ignore[assignment] # Create our urllib3.Retry instance which will allow us to customize # how we handle retries. @@ -395,14 +397,20 @@ 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=retries, + 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=retries, + ), ) else: secure_adapter = HTTPAdapter(max_retries=retries, ssl_context=ssl_context) From ad9bf38008434dc764be2f672c73ddf03efefdcf Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:14:29 -0400 Subject: [PATCH 06/21] Add `type: ignore`s to pkg_resources --- src/pip/_internal/metadata/pkg_resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 89fce8b6e5d..43e8aaf2fb8 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 From 47d03cc976fbc4a66602af23e3e1d60d057c1687 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:17:41 -0400 Subject: [PATCH 07/21] Match MultiDomainBasicAuth.__call__ types with it's parent class --- src/pip/_internal/network/auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index e331453f227..a715c1a0b35 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: # Vendored libraries with type stubs from requests.auth import AuthBase, HTTPBasicAuth - from requests.models import Request, Response + from requests.models import PreparedRequest, Response from requests.utils import get_netrc_auth else: from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth @@ -443,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 From 74acb42159b9396eaa4118c630869a9ee531a6b6 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:38:14 -0400 Subject: [PATCH 08/21] Fix remaining `collector.py` type error --- src/pip/_internal/index/collector.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 8904fb04960..e506deae445 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -86,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) From ab0bcc312a93291b8984f05481de5abf4ccd2b5d Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:44:13 -0400 Subject: [PATCH 09/21] Fix remaining type errors in `xmlrpc.py` --- src/pip/_internal/network/xmlrpc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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( From 7eebf11e8a62a68f970ef20070cd3b2faa8c86fd Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:51:07 -0400 Subject: [PATCH 10/21] Cast to real responses in test network utils, download, and auth --- tests/unit/test_network_auth.py | 11 ++++++++--- tests/unit/test_network_download.py | 10 ++++++++-- tests/unit/test_network_utils.py | 11 +++++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) 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_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)) From b3002e38448e99c5475a852ff62513e19abb8c7d Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 00:53:50 -0400 Subject: [PATCH 11/21] Force string in get_user_agent for test network session --- tests/unit/test_network_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index 26ee0b6b70b..ee118622fac 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -28,7 +28,7 @@ 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: From 914ccdb3156477ab76d1ef9c76c659b97acc3dbd Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 01:00:13 -0400 Subject: [PATCH 12/21] cast to real WorkingSet in test metadata pkg resources --- tests/unit/metadata/test_metadata_pkg_resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/metadata/test_metadata_pkg_resources.py b/tests/unit/metadata/test_metadata_pkg_resources.py index ccb0b7dcf0f..c94f37e1ed1 100644 --- a/tests/unit/metadata/test_metadata_pkg_resources.py +++ b/tests/unit/metadata/test_metadata_pkg_resources.py @@ -18,7 +18,7 @@ ) pkg_resources = pytest.importorskip("pip._vendor.pkg_resources") - +from pip._vendor.pkg_resources import WorkingSet def _dist_is_local(dist: mock.Mock) -> bool: return dist.kind != "global" and dist.kind != "user" @@ -71,13 +71,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 From f0fce92c5ac2fbaea7b64abf5f05d6fc01963d1a Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 01:13:25 -0400 Subject: [PATCH 13/21] Remove unneeded `CaseInsensitiveDict` in wheel.py --- tests/lib/wheel.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index 44e81a679ab..ab6421bdc17 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -19,6 +19,7 @@ AnyStr, TypeVar, Union, + cast, ) from zipfile import ZipFile @@ -91,13 +92,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) @@ -123,14 +122,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) From f9af7efa930cb273b1d127b8a5dfd945bf25b3db Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 01:16:36 -0400 Subject: [PATCH 14/21] Use real Response in test_operations_prepare --- tests/unit/test_operations_prepare.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 From c0e93c19dbae166a30c5e78458f60ecfacda5e3f Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 01:18:01 -0400 Subject: [PATCH 15/21] Linting --- src/pip/_internal/metadata/pkg_resources.py | 2 +- tests/lib/wheel.py | 9 --------- tests/unit/metadata/test_metadata_pkg_resources.py | 3 ++- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 43e8aaf2fb8..70a3e9f78ed 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -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) # type: ignore + return bool(self._dist._provider.path) # type: ignore except AttributeError: return False diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index ab6421bdc17..c035a2b6c7d 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -15,23 +15,14 @@ from io import BytesIO, StringIO from pathlib import Path from typing import ( - TYPE_CHECKING, AnyStr, TypeVar, Union, - cast, ) from zipfile import ZipFile from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution -if TYPE_CHECKING: - # Vendored libraries with type stubs - from requests.structures import CaseInsensitiveDict -else: - from pip._vendor.requests.structures import CaseInsensitiveDict - - # As would be used in metadata HeaderValue = Union[str, list[str]] diff --git a/tests/unit/metadata/test_metadata_pkg_resources.py b/tests/unit/metadata/test_metadata_pkg_resources.py index c94f37e1ed1..79999664546 100644 --- a/tests/unit/metadata/test_metadata_pkg_resources.py +++ b/tests/unit/metadata/test_metadata_pkg_resources.py @@ -18,7 +18,8 @@ ) pkg_resources = pytest.importorskip("pip._vendor.pkg_resources") -from pip._vendor.pkg_resources import WorkingSet +from pip._vendor.pkg_resources import WorkingSet # noqa: E402 + def _dist_is_local(dist: mock.Mock) -> bool: return dist.kind != "global" and dist.kind != "user" From ac3d85c734531599d1e76dc016143f9402388e21 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 01:21:59 -0400 Subject: [PATCH 16/21] Add developer documentation --- docs/html/development/getting-started.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 ============================ From a20395702f87fa6a9af47a72c888aa54d433d32b Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Fri, 11 Jul 2025 01:41:16 -0400 Subject: [PATCH 17/21] Add missing PyYAML and types-PyYAML dependencies --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6223cc7fff5..8f4fa8b7a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ docs = [ all-optional = ["keyring"] nox = ["nox"] # noxfile.py -update-rtd-redirects = ["httpx", "rich"] # tools/update-rtd-redirects.py +update-rtd-redirects = ["httpx", "rich", "PyYAML"] # tools/update-rtd-redirects.py type-check = [ # Actual type checker: @@ -108,6 +108,7 @@ type-check = [ "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 # Everything else that could contain type hints: {include-group = "test"}, From eaf7836fef7823ef99043ee00d2b73f620bd5de6 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Thu, 17 Jul 2025 09:20:13 -0400 Subject: [PATCH 18/21] Simpler `session.py` fixes --- src/pip/_internal/network/session.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 26fafaa7a57..2d9aaf16241 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -229,11 +229,11 @@ def send( 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() resp.status_code = 200 - assert request.url is not None resp.url = request.url try: @@ -250,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 @@ -336,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__( @@ -363,11 +366,11 @@ def __init__( self.headers["User-Agent"] = user_agent() # Attach our Authentication handler to the session - self.auth = MultiDomainBasicAuth(index_urls=index_urls) # type: ignore[assignment] + self.auth = MultiDomainBasicAuth(index_urls=index_urls) # 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, @@ -382,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 @@ -401,7 +404,7 @@ def __init__( HTTPAdapter, CacheControlAdapter( cache=SafeFileCache(cache), - max_retries=retries, + max_retries=retry, ssl_context=ssl_context, ), ) @@ -409,11 +412,11 @@ def __init__( HTTPAdapter, InsecureCacheControlAdapter( cache=SafeFileCache(cache), - max_retries=retries, + 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) @@ -537,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 From 20c5498974ead5c9cc4897a9e89b47e118b69cd6 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Thu, 17 Jul 2025 09:24:28 -0400 Subject: [PATCH 19/21] Workaround mixed types in pyproject toml table --- noxfile.py | 2 +- pyproject.toml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index ab694220e07..a63136ba858 100644 --- a/noxfile.py +++ b/noxfile.py @@ -178,7 +178,7 @@ def typecheck(session: nox.Session) -> None: session, "install", "--group", - "type-check", + "all", ) session.run( diff --git a/pyproject.toml b/pyproject.toml index 8f4fa8b7a97..cabdd8eb63e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ all-optional = ["keyring"] nox = ["nox"] # noxfile.py update-rtd-redirects = ["httpx", "rich", "PyYAML"] # tools/update-rtd-redirects.py -type-check = [ +type-checking = [ # Actual type checker: "mypy", @@ -109,13 +109,15 @@ type-check = [ "types-setuptools", # test dependency and used in distutils_hack "types-six", # via python-dateutil via freezegun (test dependency) "types-PyYAML", # update-rtd-redirects dependency +] - # Everything else that could contain type hints: +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] From 49f5a13526a3e69c3bde28fa93308cdfff55c4d9 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Thu, 17 Jul 2025 09:37:52 -0400 Subject: [PATCH 20/21] cast test server to mock server --- tests/lib/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 32e4afafa3c972aac0c5193de9112b1ac7b4f6e4 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Thu, 17 Jul 2025 09:38:02 -0400 Subject: [PATCH 21/21] Fix merge issue in pre-commit --- .pre-commit-config.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d7f8afee20..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.10.0 - hooks: - - id: mypy - exclude: tests/data - args: ["--pretty", "--show-error-codes"] - additional_dependencies: [ - 'keyring==24.2.0', - 'nox==2023.4.22', - '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: