Skip to content

Fix mypy type checking via creating a nox typecheck session #13476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bf8f8d9
Move type checking outside pre-commit
pradyunsg Jul 21, 2024
edcb9ed
Merge branch 'make-mypy-nox-session'
notatallshaw Jul 10, 2025
1127caf
Merge branch 'main' into make-mypy-nox-session
notatallshaw Jul 10, 2025
a105646
Create full dependency group for type checking
notatallshaw Jul 11, 2025
bf23507
Move all vendored libraries with stubs into TYPE_CHECKING block
notatallshaw Jul 11, 2025
71a5528
Add type ignores to distutils.command.install SCHEMES
notatallshaw Jul 11, 2025
a4c73bb
Fix all "Incompatible types in assignment"
notatallshaw Jul 11, 2025
c28a1c7
Add `type: ignore`s to pkg_resources
notatallshaw Jul 11, 2025
a06fd0c
Match MultiDomainBasicAuth.__call__ types with it's parent class
notatallshaw Jul 11, 2025
c74b380
Fix remaining type errors in `session.py`
notatallshaw Jul 11, 2025
6a8be35
Fix remaining `collector.py` type error
notatallshaw Jul 11, 2025
39c493b
Fix remaining type errors in `xmlrpc.py`
notatallshaw Jul 11, 2025
c0e4d7b
Create MockServer in server.py
notatallshaw Jul 11, 2025
c16a0e7
Cast to real responses in test network utils, download, and auth
notatallshaw Jul 11, 2025
84c6b14
Force string in get_user_agent for test network session
notatallshaw Jul 11, 2025
99789ae
cast to real WorkingSet in test metadata pkg resources
notatallshaw Jul 11, 2025
7c8bc48
Remove unneeded `CaseInsensitiveDict` in wheel.py
notatallshaw Jul 11, 2025
caa2237
Use real Response in test_operations_prepare
notatallshaw Jul 11, 2025
0e26a96
Linting
notatallshaw Jul 11, 2025
a283aed
Add developer documentation
notatallshaw Jul 11, 2025
4253245
Add missing PyYAML and types-PyYAML dependencies
notatallshaw Jul 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -252,6 +264,7 @@ jobs:

needs:
- determine-changes
- typecheck
- docs
- packaging
- tests-unix
Expand Down
16 changes: 0 additions & 16 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions docs/html/development/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
============================

Expand Down
29 changes: 23 additions & 6 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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",
"type-check",
)

session.run(
"mypy",
"src/pip",
"tests",
"tools",
"noxfile.py",
"--exclude=tests/data",
)


@nox.session
def lint(session: nox.Session) -> None:
session.install("pre-commit")
Expand Down Expand Up @@ -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")
Expand Down
40 changes: 40 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ 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", "PyYAML"] # 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)
"types-PyYAML", # update-rtd-redirects 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
Expand Down
8 changes: 7 additions & 1 deletion src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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__)


Expand Down
5 changes: 3 additions & 2 deletions src/pip/_internal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
15 changes: 11 additions & 4 deletions src/pip/_internal/index/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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)


Expand Down
17 changes: 11 additions & 6 deletions src/pip/_internal/locations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -80,7 +80,7 @@ def _looks_like_bpo_44860() -> bool:

See <https://bugs.python.org/issue44860>.
"""
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"]
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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()
Expand Down
24 changes: 18 additions & 6 deletions src/pip/_internal/locations/_distutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,28 @@
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
from pip._internal.utils.virtualenv import running_under_virtualenv

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__)


Expand All @@ -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:
Expand All @@ -65,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.
Expand Down
6 changes: 3 additions & 3 deletions src/pip/_internal/metadata/pkg_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down
Loading
Loading