Skip to content

Commit 1303c56

Browse files
committed
Search sys.path for PEP-561 compliant packages
Closes #5701
1 parent b44d2bc commit 1303c56

File tree

5 files changed

+63
-49
lines changed

5 files changed

+63
-49
lines changed

mypy/main.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from mypy import util
1717
from mypy.modulefinder import (
1818
BuildSource, FindModuleCache, SearchPaths,
19-
get_site_packages_dirs, mypy_path,
19+
get_search_dirs, mypy_path,
2020
)
2121
from mypy.find_sources import create_source_list, InvalidSourceList
2222
from mypy.fscache import FileSystemCache
@@ -1033,10 +1033,10 @@ def set_strict_flags() -> None:
10331033
# Set target.
10341034
if special_opts.modules + special_opts.packages:
10351035
options.build_type = BuildType.MODULE
1036-
egg_dirs, site_packages = get_site_packages_dirs(options.python_executable)
1036+
egg_dirs, site_packages, sys_path = get_search_dirs(options.python_executable)
10371037
search_paths = SearchPaths((os.getcwd(),),
10381038
tuple(mypy_path() + options.mypy_path),
1039-
tuple(egg_dirs + site_packages),
1039+
tuple(egg_dirs + site_packages + sys_path),
10401040
())
10411041
targets = []
10421042
# TODO: use the same cache that the BuildManager will

mypy/modulefinder.py

+10-38
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
'SearchPaths',
3131
[('python_path', Tuple[str, ...]), # where user code is found
3232
('mypy_path', Tuple[str, ...]), # from $MYPYPATH or config variable
33-
('package_path', Tuple[str, ...]), # from get_site_packages_dirs()
33+
('package_path', Tuple[str, ...]), # from get_search_dirs()
3434
('typeshed_path', Tuple[str, ...]), # paths in typeshed
3535
])
3636

@@ -608,28 +608,7 @@ def default_lib_path(data_dir: str,
608608

609609

610610
@functools.lru_cache(maxsize=None)
611-
def get_prefixes(python_executable: Optional[str]) -> Tuple[str, str]:
612-
"""Get the sys.base_prefix and sys.prefix for the given python.
613-
614-
This runs a subprocess call to get the prefix paths of the given Python executable.
615-
To avoid repeatedly calling a subprocess (which can be slow!) we
616-
lru_cache the results.
617-
"""
618-
if python_executable is None:
619-
return '', ''
620-
elif python_executable == sys.executable:
621-
# Use running Python's package dirs
622-
return pyinfo.getprefixes()
623-
else:
624-
# Use subprocess to get the package directory of given Python
625-
# executable
626-
return ast.literal_eval(
627-
subprocess.check_output([python_executable, pyinfo.__file__, 'getprefixes'],
628-
stderr=subprocess.PIPE).decode())
629-
630-
631-
@functools.lru_cache(maxsize=None)
632-
def get_site_packages_dirs(python_executable: Optional[str]) -> Tuple[List[str], List[str]]:
611+
def get_search_dirs(python_executable: Optional[str]) -> Tuple[List[str], List[str], List[str]]:
633612
"""Find package directories for given python.
634613
635614
This runs a subprocess call, which generates a list of the egg directories, and the site
@@ -638,17 +617,17 @@ def get_site_packages_dirs(python_executable: Optional[str]) -> Tuple[List[str],
638617
"""
639618

640619
if python_executable is None:
641-
return [], []
620+
return [], [], []
642621
elif python_executable == sys.executable:
643622
# Use running Python's package dirs
644-
site_packages = pyinfo.getsitepackages()
623+
site_packages, sys_path = pyinfo.getsearchdirs()
645624
else:
646625
# Use subprocess to get the package directory of given Python
647626
# executable
648-
site_packages = ast.literal_eval(
649-
subprocess.check_output([python_executable, pyinfo.__file__, 'getsitepackages'],
627+
site_packages, sys_path = ast.literal_eval(
628+
subprocess.check_output([python_executable, pyinfo.__file__, 'getsearchdirs'],
650629
stderr=subprocess.PIPE).decode())
651-
return expand_site_packages(site_packages)
630+
return expand_site_packages(site_packages) + (sys_path,)
652631

653632

654633
def expand_site_packages(site_packages: List[str]) -> Tuple[List[str], List[str]]:
@@ -781,10 +760,8 @@ def compute_search_paths(sources: List[BuildSource],
781760
if options.python_version[0] == 2:
782761
mypypath = add_py2_mypypath_entries(mypypath)
783762

784-
egg_dirs, site_packages = get_site_packages_dirs(options.python_executable)
785-
base_prefix, prefix = get_prefixes(options.python_executable)
786-
is_venv = base_prefix != prefix
787-
for site_dir in site_packages:
763+
egg_dirs, site_packages, sys_path = get_search_dirs(options.python_executable)
764+
for site_dir in site_packages + sys_path:
788765
assert site_dir not in lib_path
789766
if (site_dir in mypypath or
790767
any(p.startswith(site_dir + os.path.sep) for p in mypypath) or
@@ -793,15 +770,10 @@ def compute_search_paths(sources: List[BuildSource],
793770
print("See https://mypy.readthedocs.io/en/stable/running_mypy.html"
794771
"#how-mypy-handles-imports for more info", file=sys.stderr)
795772
sys.exit(1)
796-
elif site_dir in python_path and (is_venv and not site_dir.startswith(prefix)):
797-
print("{} is in the PYTHONPATH. Please change directory"
798-
" so it is not.".format(site_dir),
799-
file=sys.stderr)
800-
sys.exit(1)
801773

802774
return SearchPaths(python_path=tuple(reversed(python_path)),
803775
mypy_path=tuple(mypypath),
804-
package_path=tuple(egg_dirs + site_packages),
776+
package_path=tuple(egg_dirs + site_packages + sys_path),
805777
typeshed_path=tuple(lib_path))
806778

807779

mypy/pyinfo.py

+23-8
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
library found in Python 2. This file is run each mypy run, so it should be kept as fast as
77
possible.
88
"""
9+
import os
910
import site
1011
import sys
12+
import sysconfig
1113

1214
if __name__ == '__main__':
1315
sys.path = sys.path[1:] # we don't want to pick up mypy.types
@@ -17,12 +19,27 @@
1719
from typing import List, Tuple
1820

1921

20-
def getprefixes():
21-
# type: () -> Tuple[str, str]
22-
return getattr(sys, "base_prefix", sys.prefix), sys.prefix
22+
def getsearchdirs():
23+
# type: () -> Tuple[List[str], List[str]]
24+
site_packages = _getsitepackages()
2325

26+
# Do not include things from the standard library
27+
# because those should come from typeshed.
28+
stdlib_zip = os.path.join(
29+
sys.base_exec_prefix,
30+
getattr(sys, "platlibdir", "lib"),
31+
"python{}{}.zip".format(sys.version_info.major, sys.version_info.minor)
32+
)
33+
stdlib = sysconfig.get_path("stdlib")
34+
stdlib_ext = os.path.join(stdlib, "lib-dynload")
35+
cwd = os.path.abspath(os.getcwd())
36+
excludes = set(site_packages + [cwd, stdlib_zip, stdlib, stdlib_ext])
2437

25-
def getsitepackages():
38+
abs_sys_path = (os.path.abspath(p) for p in sys.path)
39+
return (site_packages, [p for p in abs_sys_path if p not in excludes])
40+
41+
42+
def _getsitepackages():
2643
# type: () -> List[str]
2744
res = []
2845
if hasattr(site, 'getsitepackages'):
@@ -37,10 +54,8 @@ def getsitepackages():
3754

3855

3956
if __name__ == '__main__':
40-
if sys.argv[-1] == 'getsitepackages':
41-
print(repr(getsitepackages()))
42-
elif sys.argv[-1] == 'getprefixes':
43-
print(repr(getprefixes()))
57+
if sys.argv[-1] == 'getsearchdirs':
58+
print(repr(getsearchdirs()))
4459
else:
4560
print("ERROR: incorrect argument to pyinfo.py.", file=sys.stderr)
4661
sys.exit(1)

mypy/test/testcmdline.py

+3
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
5656
fixed = [python3_path, '-m', 'mypy']
5757
env = os.environ.copy()
5858
env.pop('COLUMNS', None)
59+
extra_path = os.path.join(os.path.abspath(test_temp_dir), 'pypath')
5960
env['PYTHONPATH'] = PREFIX
61+
if os.path.isdir(extra_path):
62+
env['PYTHONPATH'] += ':' + extra_path
6063
process = subprocess.Popen(fixed + args,
6164
stdout=subprocess.PIPE,
6265
stderr=subprocess.PIPE,

test-data/unit/cmdline.test

+24
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,30 @@ main.py:6: error: Unsupported operand types for + ("int" and "str")
365365
main.py:7: error: Module has no attribute "y"
366366
main.py:8: error: Unsupported operand types for + (Module and "int")
367367

368+
[case testConfigFollowImportsSysPath]
369+
# cmd: mypy main.py
370+
[file main.py]
371+
from a import x
372+
x + 0
373+
x + '' # E
374+
import a
375+
a.x + 0
376+
a.x + '' # E
377+
a.y # E
378+
a + 0 # E
379+
[file mypy.ini]
380+
\[mypy]
381+
follow_imports = normal
382+
[file pypath/a/__init__.py]
383+
x = 0
384+
x += '' # Error reported here
385+
[file pypath/a/py.typed]
386+
[out]
387+
main.py:3: error: Unsupported operand types for + ("int" and "str")
388+
main.py:6: error: Unsupported operand types for + ("int" and "str")
389+
main.py:7: error: Module has no attribute "y"
390+
main.py:8: error: Unsupported operand types for + (Module and "int")
391+
368392
[case testConfigFollowImportsSilent]
369393
# cmd: mypy main.py
370394
[file main.py]

0 commit comments

Comments
 (0)