Skip to content

Commit cb47985

Browse files
committed
Add a command for running complete type checking with nox -s typecheck, including vendored dependencies
1 parent d96a80c commit cb47985

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

noxfile.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
LOCATIONS = {
2424
"common-wheels": "tests/data/common_wheels",
2525
"protected-pip": "tools/protected_pip.py",
26+
"untracked-vendored-type-stubs": "tests/typing/untracked-vendored-stubs",
2627
}
2728

2829
AUTHORS_FILE = "AUTHORS.txt"
@@ -127,6 +128,113 @@ def test(session: nox.Session) -> None:
127128
)
128129

129130

131+
@nox.session
132+
def typecheck(session: nox.Session) -> None:
133+
runtime_typing_deps = [
134+
"types-PyYAML", # Used in noxfile (needs to be part of the mypy environment)
135+
"keyring", # An optional runtime dependency.
136+
"nox",
137+
"packaging",
138+
"pytest",
139+
"httpx",
140+
"rich",
141+
]
142+
# Install test and test-types dependency groups
143+
run_with_protected_pip(
144+
session,
145+
"install",
146+
"mypy",
147+
*runtime_typing_deps,
148+
)
149+
150+
stubs_dir = Path(LOCATIONS["untracked-vendored-type-stubs"])
151+
if stubs_dir.exists():
152+
shutil.rmtree(stubs_dir)
153+
154+
# TODO: Let's have a single place where these are defined, as we should
155+
# have the exact versions that we are vendoring.
156+
# The versions could be taken from src/pip/_vendor/vendor.txt.
157+
vendored_and_needing_stubs = [
158+
# Stub libraries that contain type hints as a separate package:
159+
"types-docutils", # via sphinx (test dependency)
160+
"types-requests", # vendored
161+
# vendored (can be removed when we upgrade to urllib3 >= 2.0)
162+
"types-urllib3==1.*",
163+
"types-setuptools", # test dependency and used in distutils_hack
164+
"types-six", # via python-dateutil via freezegun (test dependency)
165+
"types-PyYAML", # update-rtd-redirects dependency
166+
]
167+
168+
run_with_protected_pip(
169+
session,
170+
"install",
171+
f"--target={stubs_dir}",
172+
*vendored_and_needing_stubs,
173+
)
174+
175+
# Generate real pip/__init__.pyi and pip/_vendor/__init__.pyi files. We are
176+
# obliged to have these files so that mypy understands that this is the pip
177+
# package, and that it should take these stubs into account.
178+
# We use stubgen, as the __init__.pyi files must be representative of what is
179+
# in the real pip source.
180+
real_pip_init = Path("src") / "pip" / "__init__.py"
181+
real_pip_vendor_init = Path("src/pip/_vendor/__init__.py")
182+
183+
# stubgen has a problem generating for pip/_vendored/__init__.py, so we
184+
# trick it by copying it to a different path and generating from there
185+
tmp_pip_vendor_init = stubs_dir / "pip_vendor.py"
186+
shutil.copy(real_pip_vendor_init, tmp_pip_vendor_init)
187+
188+
session.run(
189+
"stubgen",
190+
str(real_pip_init),
191+
str(tmp_pip_vendor_init),
192+
"--output",
193+
str(stubs_dir),
194+
)
195+
196+
# We now make a fake pip package in the stubs dir. When mypy finds this
197+
# directory it will use any file it finds, but continue to find files from the
198+
# real pip directory. Using a `pip-stubs` directory doesn't work for mypy.
199+
pip_stubs_dir = stubs_dir / "pip"
200+
pip_vendor_dir = pip_stubs_dir / "_vendor"
201+
pip_vendor_dir.mkdir()
202+
203+
tmp_pip_vendor_init.unlink()
204+
shutil.move(stubs_dir / "pip_vendor.pyi", pip_vendor_dir / "__init__.pyi")
205+
206+
# Move the vendored stub files into the pip vendored stubpackage.
207+
for stubs_directory in stubs_dir.glob("*-stubs"):
208+
stubs_directory.rename(pip_vendor_dir / stubs_directory.name.split("-stubs")[0])
209+
210+
# Clean up anything that is left over.
211+
for item in stubs_dir.iterdir():
212+
if item != pip_stubs_dir and item.is_dir():
213+
shutil.rmtree(item)
214+
215+
# Don't track the generated stubs, so git ignore the directory.
216+
(stubs_dir / ".gitignore").write_text("*")
217+
218+
mypy_cmd = [
219+
"mypy",
220+
"--config-file=tests/typing/pyproject.toml",
221+
]
222+
if session.posargs:
223+
# Allow passing specific files/directories to be checked.
224+
mypy_cmd.extend(session.posargs)
225+
else:
226+
# Otherwise, run against all important files.
227+
mypy_cmd.extend(
228+
[
229+
"src/pip",
230+
"tests",
231+
"tools",
232+
"noxfile.py",
233+
]
234+
)
235+
session.run(*mypy_cmd)
236+
237+
130238
@nox.session
131239
def docs(session: nox.Session) -> None:
132240
session.install("--group", "docs")

tests/typing/pyproject.toml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Mypy configuration for pip type checking
2+
#
3+
# This configuration is used by `nox -s typecheck` and represents the target
4+
# mypy configuration once the pip codebase has proper type annotations.
5+
# Eventually, this configuration should be moved to the main pyproject.toml
6+
# to replace the current [tool.mypy] section.
7+
8+
[tool.mypy]
9+
mypy_path = [
10+
"tests/typing/untracked-vendored-stubs", # these are generated by nox typecheck
11+
"src",
12+
]
13+
strict = true
14+
no_implicit_reexport = false
15+
disallow_subclassing_any = false
16+
disallow_untyped_calls = false
17+
warn_return_any = true
18+
ignore_missing_imports = false
19+
warn_redundant_casts = true
20+
21+
exclude = [
22+
"src/pip/_vendor",
23+
# Tell mypy that it isn't type-checking this directory (it is just using it
24+
# for vendored library stubs)
25+
"tests/typing/untracked-vendored-stubs",
26+
"tests/data",
27+
]
28+
29+
[[tool.mypy.overrides]]
30+
module = "pip._internal.utils._jaraco_text"
31+
ignore_errors = true
32+
33+
[[tool.mypy.overrides]]
34+
module = "virtualenv"
35+
ignore_missing_imports = true
36+
37+
[[tool.mypy.overrides]]
38+
module = "pip._vendor.*"
39+
# Errors with the vendored libraries can be ignored (an upstream problem).
40+
ignore_errors = true

0 commit comments

Comments
 (0)