Skip to content

Commit 12cf0bf

Browse files
committed
Don't suggest incompatible stub packages (#10610)
Keep track of supported Python versions of legacy bundled packages, and only suggest a stub package if the major Python version is compatible. Fixes #10602.
1 parent 8f51fbf commit 12cf0bf

File tree

7 files changed

+166
-88
lines changed

7 files changed

+166
-88
lines changed

mypy/build.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
from mypy.renaming import VariableRenameVisitor
5959
from mypy.config_parser import parse_mypy_comments
6060
from mypy.freetree import free_tree
61-
from mypy.stubinfo import legacy_bundled_packages
61+
from mypy.stubinfo import legacy_bundled_packages, is_legacy_bundled_package
6262
from mypy import errorcodes as codes
6363

6464

@@ -2449,7 +2449,9 @@ def find_module_and_diagnose(manager: BuildManager,
24492449
# otherwise updating mypy can silently result in new false
24502450
# negatives.
24512451
global_ignore_missing_imports = manager.options.ignore_missing_imports
2452-
if ((top_level in legacy_bundled_packages or second_level in legacy_bundled_packages)
2452+
py_ver = options.python_version[0]
2453+
if ((is_legacy_bundled_package(top_level, py_ver)
2454+
or is_legacy_bundled_package(second_level, py_ver))
24532455
and global_ignore_missing_imports
24542456
and not options.ignore_missing_imports_per_module):
24552457
ignore_missing_imports = False
@@ -2558,10 +2560,10 @@ def module_not_found(manager: BuildManager, line: int, caller_state: State,
25582560
top_level = second_level
25592561
for note in notes:
25602562
if '{stub_dist}' in note:
2561-
note = note.format(stub_dist=legacy_bundled_packages[top_level])
2563+
note = note.format(stub_dist=legacy_bundled_packages[top_level].name)
25622564
errors.report(line, 0, note, severity='note', only_once=True, code=codes.IMPORT)
25632565
if reason is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED:
2564-
manager.missing_stub_packages.add(legacy_bundled_packages[top_level])
2566+
manager.missing_stub_packages.add(legacy_bundled_packages[top_level].name)
25652567
errors.set_import_context(save_import_context)
25662568

25672569

mypy/modulefinder.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from mypy.fscache import FileSystemCache
1919
from mypy.options import Options
20-
from mypy.stubinfo import legacy_bundled_packages
20+
from mypy.stubinfo import is_legacy_bundled_package
2121
from mypy import sitepkgs
2222

2323
# Paths to be searched in find_module().
@@ -136,7 +136,7 @@ def __init__(self,
136136
if options:
137137
custom_typeshed_dir = options.custom_typeshed_dir
138138
self.stdlib_py_versions = load_stdlib_py_versions(custom_typeshed_dir)
139-
self.python2 = options and options.python_version[0] == 2
139+
self.python_major_ver = 3 if options is None else options.python_version[0]
140140

141141
def clear(self) -> None:
142142
self.results.clear()
@@ -187,7 +187,7 @@ def get_toplevel_possibilities(self, lib_path: Tuple[str, ...], id: str) -> List
187187
name = os.path.splitext(name)[0]
188188
components.setdefault(name, []).append(dir)
189189

190-
if self.python2:
190+
if self.python_major_ver == 2:
191191
components = {id: filter_redundant_py2_dirs(dirs)
192192
for id, dirs in components.items()}
193193

@@ -230,8 +230,8 @@ def _find_module_non_stub_helper(self, components: List[str],
230230
elif not plausible_match and (self.fscache.isdir(dir_path)
231231
or self.fscache.isfile(dir_path + ".py")):
232232
plausible_match = True
233-
if (components[0] in legacy_bundled_packages
234-
or '.'.join(components[:2]) in legacy_bundled_packages):
233+
if (is_legacy_bundled_package(components[0], self.python_major_ver)
234+
or is_legacy_bundled_package('.'.join(components[:2]), self.python_major_ver)):
235235
return ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
236236
elif plausible_match:
237237
return ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS
@@ -280,7 +280,7 @@ def _find_module(self, id: str, use_typeshed: bool) -> ModuleSearchResult:
280280
for pkg_dir in self.search_paths.package_path:
281281
stub_name = components[0] + '-stubs'
282282
stub_dir = os.path.join(pkg_dir, stub_name)
283-
if self.python2:
283+
if self.python_major_ver == 2:
284284
alt_stub_name = components[0] + '-python2-stubs'
285285
alt_stub_dir = os.path.join(pkg_dir, alt_stub_name)
286286
if fscache.isdir(alt_stub_dir):
@@ -348,7 +348,7 @@ def _find_module(self, id: str, use_typeshed: bool) -> ModuleSearchResult:
348348
for extension in PYTHON_EXTENSIONS:
349349
path = base_path + sepinit + extension
350350
suffix = '-stubs'
351-
if self.python2:
351+
if self.python_major_ver == 2:
352352
if os.path.isdir(base_path + '-python2-stubs'):
353353
suffix = '-python2-stubs'
354354
path_stubs = base_path + suffix + sepinit + extension
@@ -432,7 +432,7 @@ def _is_compatible_stub_package(self, stub_dir: str) -> bool:
432432
import toml
433433
with open(metadata_fnam, 'r') as f:
434434
metadata = toml.load(f)
435-
if self.python2:
435+
if self.python_major_ver == 2:
436436
return bool(metadata.get('python2', False))
437437
else:
438438
return bool(metadata.get('python3', True))

mypy/stubinfo.py

Lines changed: 93 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,100 @@
1+
from typing import Optional
2+
3+
4+
class StubInfo:
5+
def __init__(self, name: str, py_version: Optional[int] = None) -> None:
6+
self.name = name
7+
# If None, compatible with py2+py3, if 2/3, only compatible with py2/py3
8+
self.py_version = py_version
9+
10+
11+
def is_legacy_bundled_package(prefix: str, py_version: int) -> bool:
12+
if prefix not in legacy_bundled_packages:
13+
return False
14+
package_ver = legacy_bundled_packages[prefix].py_version
15+
return package_ver is None or package_ver == py_version
16+
17+
118
# Stubs for these third-party packages used to be shipped with mypy.
219
#
320
# Map package name to PyPI stub distribution name.
421
#
522
# Package name can have one or two components ('a' or 'a.b').
623
legacy_bundled_packages = {
7-
'aiofiles': 'types-aiofiles',
8-
'atomicwrites': 'types-atomicwrites',
9-
'attr': 'types-attrs',
10-
'backports': 'types-backports',
11-
'backports_abc': 'types-backports_abc',
12-
'bleach': 'types-bleach',
13-
'boto': 'types-boto',
14-
'cachetools': 'types-cachetools',
15-
'certifi': 'types-certifi',
16-
'characteristic': 'types-characteristic',
17-
'chardet': 'types-chardet',
18-
'click': 'types-click',
19-
'click_spinner': 'types-click-spinner',
20-
'concurrent': 'types-futures',
21-
'contextvars': 'types-contextvars',
22-
'croniter': 'types-croniter',
23-
'cryptography': 'types-cryptography',
24-
'dataclasses': 'types-dataclasses',
25-
'dateparser': 'types-dateparser',
26-
'datetimerange': 'types-DateTimeRange',
27-
'dateutil': 'types-python-dateutil',
28-
'decorator': 'types-decorator',
29-
'deprecated': 'types-Deprecated',
30-
'docutils': 'types-docutils',
31-
'emoji': 'types-emoji',
32-
'enum': 'types-enum34',
33-
'fb303': 'types-fb303',
34-
'filelock': 'types-filelock',
35-
'first': 'types-first',
36-
'flask': 'types-Flask',
37-
'freezegun': 'types-freezegun',
38-
'frozendict': 'types-frozendict',
39-
'geoip2': 'types-geoip2',
40-
'gflags': 'types-python-gflags',
41-
'google.protobuf': 'types-protobuf',
42-
'ipaddress': 'types-ipaddress',
43-
'itsdangerous': 'types-itsdangerous',
44-
'jinja2': 'types-Jinja2',
45-
'jwt': 'types-jwt',
46-
'kazoo': 'types-kazoo',
47-
'markdown': 'types-Markdown',
48-
'markupsafe': 'types-MarkupSafe',
49-
'maxminddb': 'types-maxminddb',
50-
'mock': 'types-mock',
51-
'OpenSSL': 'types-openssl-python',
52-
'orjson': 'types-orjson',
53-
'paramiko': 'types-paramiko',
54-
'pathlib2': 'types-pathlib2',
55-
'pkg_resources': 'types-pkg_resources',
56-
'polib': 'types-polib',
57-
'pycurl': 'types-pycurl',
58-
'pymssql': 'types-pymssql',
59-
'pymysql': 'types-PyMySQL',
60-
'pyrfc3339': 'types-pyRFC3339',
61-
'python2': 'types-six',
62-
'pytz': 'types-pytz',
63-
'pyVmomi': 'types-pyvmomi',
64-
'redis': 'types-redis',
65-
'requests': 'types-requests',
66-
'retry': 'types-retry',
67-
'routes': 'types-Routes',
68-
'scribe': 'types-scribe',
69-
'simplejson': 'types-simplejson',
70-
'singledispatch': 'types-singledispatch',
71-
'six': 'types-six',
72-
'slugify': 'types-python-slugify',
73-
'tabulate': 'types-tabulate',
74-
'termcolor': 'types-termcolor',
75-
'toml': 'types-toml',
76-
'tornado': 'types-tornado',
77-
'typed_ast': 'types-typed-ast',
78-
'tzlocal': 'types-tzlocal',
79-
'ujson': 'types-ujson',
80-
'waitress': 'types-waitress',
81-
'werkzeug': 'types-Werkzeug',
82-
'yaml': 'types-PyYAML',
24+
'aiofiles': StubInfo('types-aiofiles', py_version=3),
25+
'atomicwrites': StubInfo('types-atomicwrites'),
26+
'attr': StubInfo('types-attrs'),
27+
'backports': StubInfo('types-backports'),
28+
'backports_abc': StubInfo('types-backports_abc'),
29+
'bleach': StubInfo('types-bleach'),
30+
'boto': StubInfo('types-boto'),
31+
'cachetools': StubInfo('types-cachetools'),
32+
'certifi': StubInfo('types-certifi'),
33+
'characteristic': StubInfo('types-characteristic'),
34+
'chardet': StubInfo('types-chardet'),
35+
'click': StubInfo('types-click'),
36+
'click_spinner': StubInfo('types-click-spinner'),
37+
'concurrent': StubInfo('types-futures', py_version=2),
38+
'contextvars': StubInfo('types-contextvars', py_version=3),
39+
'croniter': StubInfo('types-croniter'),
40+
'cryptography': StubInfo('types-cryptography'),
41+
'dataclasses': StubInfo('types-dataclasses', py_version=3),
42+
'dateparser': StubInfo('types-dateparser'),
43+
'datetimerange': StubInfo('types-DateTimeRange'),
44+
'dateutil': StubInfo('types-python-dateutil'),
45+
'decorator': StubInfo('types-decorator'),
46+
'deprecated': StubInfo('types-Deprecated'),
47+
'docutils': StubInfo('types-docutils', py_version=3),
48+
'emoji': StubInfo('types-emoji'),
49+
'enum': StubInfo('types-enum34', py_version=2),
50+
'fb303': StubInfo('types-fb303', py_version=2),
51+
'filelock': StubInfo('types-filelock', py_version=3),
52+
'first': StubInfo('types-first'),
53+
'flask': StubInfo('types-Flask'),
54+
'freezegun': StubInfo('types-freezegun', py_version=3),
55+
'frozendict': StubInfo('types-frozendict', py_version=3),
56+
'geoip2': StubInfo('types-geoip2'),
57+
'gflags': StubInfo('types-python-gflags'),
58+
'google.protobuf': StubInfo('types-protobuf'),
59+
'ipaddress': StubInfo('types-ipaddress', py_version=2),
60+
'itsdangerous': StubInfo('types-itsdangerous'),
61+
'jinja2': StubInfo('types-Jinja2'),
62+
'jwt': StubInfo('types-jwt'),
63+
'kazoo': StubInfo('types-kazoo', py_version=2),
64+
'markdown': StubInfo('types-Markdown'),
65+
'markupsafe': StubInfo('types-MarkupSafe'),
66+
'maxminddb': StubInfo('types-maxminddb'),
67+
'mock': StubInfo('types-mock'),
68+
'OpenSSL': StubInfo('types-openssl-python', py_version=2),
69+
'orjson': StubInfo('types-orjson', py_version=3),
70+
'paramiko': StubInfo('types-paramiko'),
71+
'pathlib2': StubInfo('types-pathlib2', py_version=2),
72+
'pkg_resources': StubInfo('types-pkg_resources', py_version=3),
73+
'polib': StubInfo('types-polib'),
74+
'pycurl': StubInfo('types-pycurl'),
75+
'pymssql': StubInfo('types-pymssql', py_version=2),
76+
'pymysql': StubInfo('types-PyMySQL'),
77+
'pyrfc3339': StubInfo('types-pyRFC3339', py_version=3),
78+
'python2': StubInfo('types-six'),
79+
'pytz': StubInfo('types-pytz'),
80+
'pyVmomi': StubInfo('types-pyvmomi'),
81+
'redis': StubInfo('types-redis'),
82+
'requests': StubInfo('types-requests'),
83+
'retry': StubInfo('types-retry'),
84+
'routes': StubInfo('types-Routes', py_version=2),
85+
'scribe': StubInfo('types-scribe', py_version=2),
86+
'simplejson': StubInfo('types-simplejson'),
87+
'singledispatch': StubInfo('types-singledispatch'),
88+
'six': StubInfo('types-six'),
89+
'slugify': StubInfo('types-python-slugify'),
90+
'tabulate': StubInfo('types-tabulate'),
91+
'termcolor': StubInfo('types-termcolor'),
92+
'toml': StubInfo('types-toml'),
93+
'tornado': StubInfo('types-tornado', py_version=2),
94+
'typed_ast': StubInfo('types-typed-ast', py_version=3),
95+
'tzlocal': StubInfo('types-tzlocal'),
96+
'ujson': StubInfo('types-ujson'),
97+
'waitress': StubInfo('types-waitress', py_version=3),
98+
'werkzeug': StubInfo('types-Werkzeug'),
99+
'yaml': StubInfo('types-PyYAML'),
83100
}

mypy/test/testpythoneval.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None
7272
interpreter = python3_path
7373
mypy_cmdline.append('--python-version={}'.format('.'.join(map(str, PYTHON3_VERSION))))
7474

75+
m = re.search('# flags: (.*)$', '\n'.join(testcase.input), re.MULTILINE)
76+
if m:
77+
mypy_cmdline.extend(m.group(1).split())
78+
7579
# Write the program to a file.
7680
program = '_' + testcase.name + '.py'
7781
program_path = os.path.join(test_temp_dir, program)

mypy/test/teststubinfo.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import unittest
2+
3+
from mypy.stubinfo import is_legacy_bundled_package
4+
5+
6+
class TestStubInfo(unittest.TestCase):
7+
def test_is_legacy_bundled_packages(self) -> None:
8+
assert not is_legacy_bundled_package('foobar_asdf', 2)
9+
assert not is_legacy_bundled_package('foobar_asdf', 3)
10+
11+
assert is_legacy_bundled_package('click', 2)
12+
assert is_legacy_bundled_package('click', 3)
13+
14+
assert is_legacy_bundled_package('scribe', 2)
15+
assert not is_legacy_bundled_package('scribe', 3)
16+
17+
assert not is_legacy_bundled_package('dataclasses', 2)
18+
assert is_legacy_bundled_package('dataclasses', 3)

test-data/unit/python2eval.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,17 @@ _testDefaultDictInference.py:5: note: Revealed type is "collections.defaultdict[
433433
from collections import abc
434434
[out]
435435
_testIgnorePython3StdlibStubs_python2.py:1: error: Module "collections" has no attribute "abc"
436+
437+
[case testNoApprovedPython2StubInstalled_python2]
438+
# flags: --ignore-missing-imports
439+
import scribe
440+
from scribe import x
441+
import maxminddb
442+
import foobar_asdf
443+
[out]
444+
_testNoApprovedPython2StubInstalled_python2.py:2: error: Library stubs not installed for "scribe" (or incompatible with Python 2.7)
445+
_testNoApprovedPython2StubInstalled_python2.py:2: note: Hint: "python3 -m pip install types-scribe"
446+
_testNoApprovedPython2StubInstalled_python2.py:2: note: (or run "mypy --install-types" to install all missing stub packages)
447+
_testNoApprovedPython2StubInstalled_python2.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
448+
_testNoApprovedPython2StubInstalled_python2.py:4: error: Library stubs not installed for "maxminddb" (or incompatible with Python 2.7)
449+
_testNoApprovedPython2StubInstalled_python2.py:4: note: Hint: "python3 -m pip install types-maxminddb"

test-data/unit/pythoneval.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,3 +1508,26 @@ x = 0
15081508
[out]
15091509
mypy: "tmp/typing.py" shadows library module "typing"
15101510
note: A user-defined top-level module with name "typing" is not supported
1511+
1512+
[case testIgnoreImportIfNoPython3StubAvailable]
1513+
# flags: --ignore-missing-imports
1514+
import scribe # No Python 3 stubs available for scribe
1515+
from scribe import x
1516+
import maxminddb # Python 3 stubs available for maxminddb
1517+
import foobar_asdf
1518+
[out]
1519+
_testIgnoreImportIfNoPython3StubAvailable.py:4: error: Library stubs not installed for "maxminddb" (or incompatible with Python 3.6)
1520+
_testIgnoreImportIfNoPython3StubAvailable.py:4: note: Hint: "python3 -m pip install types-maxminddb"
1521+
_testIgnoreImportIfNoPython3StubAvailable.py:4: note: (or run "mypy --install-types" to install all missing stub packages)
1522+
_testIgnoreImportIfNoPython3StubAvailable.py:4: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
1523+
1524+
[case testNoPython3StubAvailable]
1525+
import scribe
1526+
from scribe import x
1527+
import maxminddb
1528+
[out]
1529+
_testNoPython3StubAvailable.py:1: error: Cannot find implementation or library stub for module named "scribe"
1530+
_testNoPython3StubAvailable.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
1531+
_testNoPython3StubAvailable.py:3: error: Library stubs not installed for "maxminddb" (or incompatible with Python 3.6)
1532+
_testNoPython3StubAvailable.py:3: note: Hint: "python3 -m pip install types-maxminddb"
1533+
_testNoPython3StubAvailable.py:3: note: (or run "mypy --install-types" to install all missing stub packages)

0 commit comments

Comments
 (0)