Skip to content

Commit 50aa4ca

Browse files
committed
Speed up stubs suggestions (#17965)
See #17948 This is starting to show up on profiles - 1.01x faster on clean (below noise) - 1.02x faster on long - 1.02x faster on openai - 1.01x faster on openai incremental I had a dumb bug that was preventing the optimisation for a while, I'll see if I can make it even faster. Currently it's a small improvement We could also get rid of the legacy stuff in mypy 2.0
1 parent 7c27808 commit 50aa4ca

File tree

4 files changed

+93
-35
lines changed

4 files changed

+93
-35
lines changed

mypy/build.py

+7-14
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
from mypy.plugins.default import DefaultPlugin
9393
from mypy.renaming import LimitedVariableRenameVisitor, VariableRenameVisitor
9494
from mypy.stats import dump_type_stats
95-
from mypy.stubinfo import legacy_bundled_packages, non_bundled_packages, stub_distribution_name
95+
from mypy.stubinfo import is_module_from_legacy_bundled_package, stub_distribution_name
9696
from mypy.types import Type
9797
from mypy.typestate import reset_global_state, type_state
9898
from mypy.util import json_dumps, json_loads
@@ -2658,17 +2658,13 @@ def find_module_and_diagnose(
26582658

26592659
ignore_missing_imports = options.ignore_missing_imports
26602660

2661-
id_components = id.split(".")
26622661
# Don't honor a global (not per-module) ignore_missing_imports
26632662
# setting for modules that used to have bundled stubs, as
26642663
# otherwise updating mypy can silently result in new false
26652664
# negatives. (Unless there are stubs but they are incomplete.)
26662665
global_ignore_missing_imports = manager.options.ignore_missing_imports
26672666
if (
2668-
any(
2669-
".".join(id_components[:i]) in legacy_bundled_packages
2670-
for i in range(len(id_components), 0, -1)
2671-
)
2667+
is_module_from_legacy_bundled_package(id)
26722668
and global_ignore_missing_imports
26732669
and not options.ignore_missing_imports_per_module
26742670
and result is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
@@ -2789,18 +2785,15 @@ def module_not_found(
27892785
code = codes.IMPORT
27902786
errors.report(line, 0, msg.format(module=target), code=code)
27912787

2792-
components = target.split(".")
2793-
for i in range(len(components), 0, -1):
2794-
module = ".".join(components[:i])
2795-
if module in legacy_bundled_packages or module in non_bundled_packages:
2796-
break
2797-
2788+
dist = stub_distribution_name(target)
27982789
for note in notes:
27992790
if "{stub_dist}" in note:
2800-
note = note.format(stub_dist=stub_distribution_name(module))
2791+
assert dist is not None
2792+
note = note.format(stub_dist=dist)
28012793
errors.report(line, 0, note, severity="note", only_once=True, code=code)
28022794
if reason is ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED:
2803-
manager.missing_stub_packages.add(stub_distribution_name(module))
2795+
assert dist is not None
2796+
manager.missing_stub_packages.add(dist)
28042797
errors.set_import_context(save_import_context)
28052798

28062799

mypy/modulefinder.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,8 @@ def _find_module_non_stub_helper(
331331
# If this is not a directory then we can't traverse further into it
332332
if not self.fscache.isdir(dir_path):
333333
break
334-
for i in range(len(components), 0, -1):
335-
if approved_stub_package_exists(".".join(components[:i])):
336-
return ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
334+
if approved_stub_package_exists(".".join(components)):
335+
return ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED
337336
if plausible_match:
338337
return ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS
339338
else:

mypy/stubinfo.py

+46-14
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,53 @@
11
from __future__ import annotations
22

33

4-
def is_legacy_bundled_package(prefix: str) -> bool:
5-
return prefix in legacy_bundled_packages
4+
def is_module_from_legacy_bundled_package(module: str) -> bool:
5+
top_level = module.split(".", 1)[0]
6+
return top_level in legacy_bundled_packages
67

78

8-
def approved_stub_package_exists(prefix: str) -> bool:
9-
return is_legacy_bundled_package(prefix) or prefix in non_bundled_packages
9+
def approved_stub_package_exists(module: str) -> bool:
10+
top_level = module.split(".", 1)[0]
11+
if top_level in legacy_bundled_packages:
12+
return True
13+
if top_level in non_bundled_packages_flat:
14+
return True
15+
if top_level in non_bundled_packages_namespace:
16+
namespace = non_bundled_packages_namespace[top_level]
17+
components = module.split(".")
18+
for i in range(len(components), 0, -1):
19+
module = ".".join(components[:i])
20+
if module in namespace:
21+
return True
22+
return False
1023

1124

12-
def stub_distribution_name(prefix: str) -> str:
13-
return legacy_bundled_packages.get(prefix) or non_bundled_packages[prefix]
25+
def stub_distribution_name(module: str) -> str | None:
26+
top_level = module.split(".", 1)[0]
27+
28+
dist = legacy_bundled_packages.get(top_level)
29+
if dist:
30+
return dist
31+
dist = non_bundled_packages_flat.get(top_level)
32+
if dist:
33+
return dist
34+
35+
if top_level in non_bundled_packages_namespace:
36+
namespace = non_bundled_packages_namespace[top_level]
37+
components = module.split(".")
38+
for i in range(len(components), 0, -1):
39+
module = ".".join(components[:i])
40+
dist = namespace.get(module)
41+
if dist:
42+
return dist
43+
44+
return None
1445

1546

1647
# Stubs for these third-party packages used to be shipped with mypy.
1748
#
1849
# Map package name to PyPI stub distribution name.
19-
legacy_bundled_packages = {
50+
legacy_bundled_packages: dict[str, str] = {
2051
"aiofiles": "types-aiofiles",
2152
"bleach": "types-bleach",
2253
"boto": "types-boto",
@@ -32,7 +63,6 @@ def stub_distribution_name(prefix: str) -> str:
3263
"docutils": "types-docutils",
3364
"first": "types-first",
3465
"gflags": "types-python-gflags",
35-
"google.protobuf": "types-protobuf",
3666
"markdown": "types-Markdown",
3767
"mock": "types-mock",
3868
"OpenSSL": "types-pyOpenSSL",
@@ -66,20 +96,17 @@ def stub_distribution_name(prefix: str) -> str:
6696
# include packages that have a release that includes PEP 561 type
6797
# information.
6898
#
69-
# Package name can have one or two components ('a' or 'a.b').
70-
#
7199
# Note that these packages are omitted for now:
72100
# pika: typeshed's stubs are on PyPI as types-pika-ts.
73101
# types-pika already exists on PyPI, and is more complete in many ways,
74102
# but is a non-typeshed stubs package.
75-
non_bundled_packages = {
103+
non_bundled_packages_flat: dict[str, str] = {
76104
"MySQLdb": "types-mysqlclient",
77105
"PIL": "types-Pillow",
78106
"PyInstaller": "types-pyinstaller",
79107
"Xlib": "types-python-xlib",
80108
"aws_xray_sdk": "types-aws-xray-sdk",
81109
"babel": "types-babel",
82-
"backports.ssl_match_hostname": "types-backports.ssl_match_hostname",
83110
"braintree": "types-braintree",
84111
"bs4": "types-beautifulsoup4",
85112
"bugbear": "types-flake8-bugbear",
@@ -107,7 +134,6 @@ def stub_distribution_name(prefix: str) -> str:
107134
"flask_migrate": "types-Flask-Migrate",
108135
"fpdf": "types-fpdf2",
109136
"gdb": "types-gdb",
110-
"google.cloud.ndb": "types-google-cloud-ndb",
111137
"hdbcli": "types-hdbcli",
112138
"html5lib": "types-html5lib",
113139
"httplib2": "types-httplib2",
@@ -123,7 +149,6 @@ def stub_distribution_name(prefix: str) -> str:
123149
"oauthlib": "types-oauthlib",
124150
"openpyxl": "types-openpyxl",
125151
"opentracing": "types-opentracing",
126-
"paho.mqtt": "types-paho-mqtt",
127152
"parsimonious": "types-parsimonious",
128153
"passlib": "types-passlib",
129154
"passpy": "types-passpy",
@@ -171,3 +196,10 @@ def stub_distribution_name(prefix: str) -> str:
171196
"pandas": "pandas-stubs", # https://github.com/pandas-dev/pandas-stubs
172197
"lxml": "lxml-stubs", # https://github.com/lxml/lxml-stubs
173198
}
199+
200+
201+
non_bundled_packages_namespace: dict[str, dict[str, str]] = {
202+
"backports": {"backports.ssl_match_hostname": "types-backports.ssl_match_hostname"},
203+
"google": {"google.cloud.ndb": "types-google-cloud-ndb", "google.protobuf": "types-protobuf"},
204+
"paho": {"paho.mqtt": "types-paho-mqtt"},
205+
}

mypy/test/teststubinfo.py

+38-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,45 @@
22

33
import unittest
44

5-
from mypy.stubinfo import is_legacy_bundled_package
5+
from mypy.stubinfo import (
6+
approved_stub_package_exists,
7+
is_module_from_legacy_bundled_package,
8+
legacy_bundled_packages,
9+
non_bundled_packages_flat,
10+
stub_distribution_name,
11+
)
612

713

814
class TestStubInfo(unittest.TestCase):
915
def test_is_legacy_bundled_packages(self) -> None:
10-
assert not is_legacy_bundled_package("foobar_asdf")
11-
assert is_legacy_bundled_package("pycurl")
12-
assert is_legacy_bundled_package("dataclasses")
16+
assert not is_module_from_legacy_bundled_package("foobar_asdf")
17+
assert not is_module_from_legacy_bundled_package("PIL")
18+
assert is_module_from_legacy_bundled_package("pycurl")
19+
assert is_module_from_legacy_bundled_package("dataclasses")
20+
21+
def test_approved_stub_package_exists(self) -> None:
22+
assert not approved_stub_package_exists("foobar_asdf")
23+
assert approved_stub_package_exists("pycurl")
24+
assert approved_stub_package_exists("babel")
25+
assert approved_stub_package_exists("google.cloud.ndb")
26+
assert approved_stub_package_exists("google.cloud.ndb.submodule")
27+
assert not approved_stub_package_exists("google.cloud.unknown")
28+
assert approved_stub_package_exists("google.protobuf")
29+
assert approved_stub_package_exists("google.protobuf.submodule")
30+
assert not approved_stub_package_exists("google")
31+
32+
def test_stub_distribution_name(self) -> None:
33+
assert stub_distribution_name("foobar_asdf") is None
34+
assert stub_distribution_name("pycurl") == "types-pycurl"
35+
assert stub_distribution_name("babel") == "types-babel"
36+
assert stub_distribution_name("google.cloud.ndb") == "types-google-cloud-ndb"
37+
assert stub_distribution_name("google.cloud.ndb.submodule") == "types-google-cloud-ndb"
38+
assert stub_distribution_name("google.cloud.unknown") is None
39+
assert stub_distribution_name("google.protobuf") == "types-protobuf"
40+
assert stub_distribution_name("google.protobuf.submodule") == "types-protobuf"
41+
assert stub_distribution_name("google") is None
42+
43+
def test_period_in_top_level(self) -> None:
44+
for packages in (non_bundled_packages_flat, legacy_bundled_packages):
45+
for top_level_module in packages:
46+
assert "." not in top_level_module

0 commit comments

Comments
 (0)