Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3011b0e

Browse files
asdf
1 parent 1b7ff0e commit 3011b0e

6 files changed

Lines changed: 230 additions & 4 deletions

File tree

docs/source/running_mypy.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@ By default, :option:`--install-types <mypy --install-types>` shows a confirmatio
403403
Use :option:`--non-interactive <mypy --non-interactive>` to install all suggested
404404
stub packages without asking for confirmation *and* type check your code:
405405

406+
If you maintain a lock file (for example ``pylock.toml``), you can combine
407+
:option:`--install-types <mypy --install-types>` with
408+
``--install-types-from-pylock FILE`` to install known stub packages for locked
409+
runtime dependencies while avoiding upgrades or downgrades of runtime packages.
410+
406411
If you've already installed the relevant third-party libraries in an environment
407412
other than the one mypy is running in, you can use :option:`--python-executable
408413
<mypy --python-executable>` flag to point to the Python executable for that

mypy/installtypes.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
from collections.abc import Mapping
6+
7+
if sys.version_info >= (3, 11):
8+
import tomllib
9+
else:
10+
import tomli as tomllib
11+
12+
from mypy.stubinfo import (
13+
non_bundled_packages_flat,
14+
non_bundled_packages_namespace,
15+
stub_distribution_name,
16+
)
17+
18+
_DIST_NORMALIZE_RE = re.compile(r"[-_.]+")
19+
20+
21+
def normalize_distribution_name(name: str) -> str:
22+
return _DIST_NORMALIZE_RE.sub("-", name).lower()
23+
24+
25+
def read_locked_packages(path: str) -> dict[str, str | None]:
26+
"""Read package name/version pairs from a pylock-like TOML file.
27+
28+
Supports common lockfile layouts that use either [[package]] or
29+
[[packages]] tables with "name" and optional "version" keys.
30+
"""
31+
with open(path, "rb") as f:
32+
data = tomllib.load(f)
33+
34+
entries: list[object] = []
35+
for key in ("package", "packages"):
36+
value = data.get(key)
37+
if isinstance(value, list):
38+
entries.extend(value)
39+
40+
locked: dict[str, str | None] = {}
41+
for entry in entries:
42+
if not isinstance(entry, Mapping):
43+
continue
44+
name = entry.get("name")
45+
if not isinstance(name, str) or not name.strip():
46+
continue
47+
version_obj = entry.get("version")
48+
version = version_obj if isinstance(version_obj, str) and version_obj.strip() else None
49+
locked[normalize_distribution_name(name)] = version
50+
51+
return locked
52+
53+
54+
def resolve_stub_packages_from_lock(locked: Mapping[str, str | None]) -> list[str]:
55+
"""Map runtime packages from a lock file to known stubs packages.
56+
57+
This uses mypy's existing known typeshed mapping and intentionally skips
58+
heuristics that could cause accidental installation of unrelated packages.
59+
"""
60+
known_stubs = set(non_bundled_packages_flat.values())
61+
for namespace_packages in non_bundled_packages_namespace.values():
62+
known_stubs.update(namespace_packages.values())
63+
64+
stubs: set[str] = set()
65+
for dist_name in locked:
66+
if dist_name.startswith("types-"):
67+
continue
68+
candidates = {
69+
dist_name,
70+
dist_name.replace("-", "_"),
71+
}
72+
for module_name in candidates:
73+
stub = stub_distribution_name(module_name)
74+
if stub:
75+
stubs.add(stub)
76+
typeshed_name = f"types-{dist_name}"
77+
if typeshed_name in known_stubs:
78+
stubs.add(typeshed_name)
79+
return sorted(stubs)
80+
81+
82+
def make_runtime_constraints(locked: Mapping[str, str | None]) -> list[str]:
83+
"""Create pip constraints that pin runtime packages to locked versions."""
84+
constraints: list[str] = []
85+
for name, version in sorted(locked.items()):
86+
if version:
87+
constraints.append(f"{name}=={version}")
88+
return constraints

mypy/main.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import platform
88
import subprocess
99
import sys
10+
import tempfile
1011
import time
1112
from collections import defaultdict
1213
from collections.abc import Sequence
@@ -29,6 +30,7 @@
2930
from mypy.errors import CompileError
3031
from mypy.find_sources import InvalidSourceList, create_source_list
3132
from mypy.fscache import FileSystemCache
33+
from mypy.installtypes import make_runtime_constraints, read_locked_packages, resolve_stub_packages_from_lock
3234
from mypy.modulefinder import (
3335
BuildSource,
3436
FindModuleCache,
@@ -124,6 +126,22 @@ def main(
124126
if options.non_interactive and not options.install_types:
125127
fail("error: --non-interactive is only supported with --install-types", stderr, options)
126128

129+
if options.install_types_from_pylock is not None and not options.install_types:
130+
fail(
131+
"error: --install-types-from-pylock is only supported with --install-types",
132+
stderr,
133+
options,
134+
)
135+
136+
if options.install_types_from_pylock is not None and not os.path.isfile(
137+
options.install_types_from_pylock
138+
):
139+
fail(
140+
f"error: Can't find lock file '{options.install_types_from_pylock}'",
141+
stderr,
142+
options,
143+
)
144+
127145
if options.install_types and not options.incremental:
128146
fail(
129147
"error: --install-types not supported with incremental mode disabled", stderr, options
@@ -137,9 +155,22 @@ def main(
137155
)
138156

139157
if options.install_types and not sources:
140-
install_types(formatter, options, non_interactive=options.non_interactive)
158+
install_types(
159+
formatter,
160+
options,
161+
non_interactive=options.non_interactive,
162+
pylock_path=options.install_types_from_pylock,
163+
)
141164
return
142165

166+
if options.install_types and options.install_types_from_pylock:
167+
install_types(
168+
formatter,
169+
options,
170+
non_interactive=options.non_interactive,
171+
pylock_path=options.install_types_from_pylock,
172+
)
173+
143174
res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
144175

145176
if options.non_interactive:
@@ -1204,6 +1235,15 @@ def add_invertible_flag(
12041235
dest="special-opts:find_occurrences",
12051236
help="Print out all usages of a class member (experimental)",
12061237
)
1238+
misc_group.add_argument(
1239+
"--install-types-from-pylock",
1240+
metavar="FILE",
1241+
dest="install_types_from_pylock",
1242+
help=(
1243+
"With --install-types, read packages from a pylock TOML file and "
1244+
"install matching known stub packages"
1245+
),
1246+
)
12071247
misc_group.add_argument(
12081248
"--scripts-are-modules",
12091249
action="store_true",
@@ -1711,24 +1751,48 @@ def install_types(
17111751
*,
17121752
after_run: bool = False,
17131753
non_interactive: bool = False,
1754+
pylock_path: str | None = None,
17141755
) -> bool:
17151756
"""Install stub packages using pip if some missing stubs were detected."""
1716-
packages = read_types_packages_to_install(options.cache_dir, after_run)
1757+
constraints: list[str] = []
1758+
if pylock_path is None:
1759+
packages = read_types_packages_to_install(options.cache_dir, after_run)
1760+
else:
1761+
locked = read_locked_packages(pylock_path)
1762+
packages = resolve_stub_packages_from_lock(locked)
1763+
constraints = make_runtime_constraints(locked)
1764+
17171765
if not packages:
17181766
# If there are no missing stubs, generate no output.
17191767
return False
17201768
if after_run and not non_interactive:
17211769
print()
17221770
print("Installing missing stub packages:")
17231771
assert options.python_executable, "Python executable required to install types"
1724-
cmd = [options.python_executable, "-m", "pip", "install"] + packages
1772+
cmd = [options.python_executable, "-m", "pip", "install"]
1773+
constraints_file = None
1774+
if pylock_path is not None:
1775+
cmd.append("--no-deps")
1776+
if pylock_path is not None and constraints:
1777+
constraints_file = tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8")
1778+
with constraints_file as f:
1779+
f.write("\n".join(constraints))
1780+
f.write("\n")
1781+
cmd += ["--constraint", constraints_file.name]
1782+
cmd += packages
17251783
print(formatter.style(" ".join(cmd), "none", bold=True))
17261784
print()
17271785
if not non_interactive:
17281786
x = input("Install? [yN] ")
17291787
if not x.strip() or not x.lower().startswith("y"):
17301788
print(formatter.style("mypy: Skipping installation", "red", bold=True))
1789+
if constraints_file is not None:
1790+
os.unlink(constraints_file.name)
17311791
sys.exit(2)
17321792
print()
1733-
subprocess.run(cmd)
1793+
try:
1794+
subprocess.run(cmd)
1795+
finally:
1796+
if constraints_file is not None:
1797+
os.unlink(constraints_file.name)
17341798
return True

mypy/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ def __init__(self) -> None:
409409
# Install missing stub packages in non-interactive mode (don't prompt for
410410
# confirmation, and don't show any errors)
411411
self.non_interactive = False
412+
# Lock file used for --install-types to discover installable stub packages.
413+
self.install_types_from_pylock: str | None = None
412414
# When we encounter errors that may cause many additional errors,
413415
# skip most errors after this many messages have been reported.
414416
# -1 means unlimited.

mypy/test/testinstalltypes.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import tempfile
5+
import textwrap
6+
import unittest
7+
8+
from mypy.installtypes import (
9+
make_runtime_constraints,
10+
read_locked_packages,
11+
resolve_stub_packages_from_lock,
12+
)
13+
14+
15+
class TestInstallTypesFromPylock(unittest.TestCase):
16+
def test_read_locked_packages(self) -> None:
17+
content = textwrap.dedent(
18+
"""
19+
[[package]]
20+
name = "requests"
21+
version = "2.32.3"
22+
23+
[[packages]]
24+
name = "python-dateutil"
25+
version = "2.9.0"
26+
27+
[[package]]
28+
name = "types-requests"
29+
version = "2.32.0"
30+
"""
31+
)
32+
with tempfile.NamedTemporaryFile("w", suffix=".toml", delete=False, encoding="utf-8") as f:
33+
f.write(content)
34+
path = f.name
35+
try:
36+
locked = read_locked_packages(path)
37+
finally:
38+
os.unlink(path)
39+
40+
assert locked["requests"] == "2.32.3"
41+
assert locked["python-dateutil"] == "2.9.0"
42+
assert locked["types-requests"] == "2.32.0"
43+
44+
def test_resolve_stub_packages_from_lock(self) -> None:
45+
locked = {
46+
"requests": "2.32.3",
47+
"python-dateutil": "2.9.0",
48+
"types-requests": "2.32.0",
49+
}
50+
stubs = resolve_stub_packages_from_lock(locked)
51+
assert "types-requests" in stubs
52+
assert "types-python-dateutil" in stubs
53+
54+
def test_make_runtime_constraints(self) -> None:
55+
locked = {
56+
"requests": "2.32.3",
57+
"python-dateutil": "2.9.0",
58+
"no-version": None,
59+
}
60+
constraints = make_runtime_constraints(locked)
61+
assert constraints == ["python-dateutil==2.9.0", "requests==2.32.3"]

test-data/unit/cmdline.test

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,12 @@ pkg.py:1: error: Incompatible types in assignment (expression has type "int", va
915915
error: --non-interactive is only supported with --install-types
916916
== Return code: 2
917917

918+
[case testCmdlineInstallTypesFromPylockWithoutInstallTypes]
919+
# cmd: mypy --install-types-from-pylock pylock.toml -m pkg
920+
[out]
921+
error: --install-types-from-pylock is only supported with --install-types
922+
== Return code: 2
923+
918924
[case testCmdlineNonInteractiveInstallTypesNothingToDo]
919925
# cmd: mypy --install-types --non-interactive -m pkg
920926
[file pkg.py]

0 commit comments

Comments
 (0)