Skip to content

Commit f175525

Browse files
committed
Search sys.path for PEP-561 compliant packages
Closes python#5701
1 parent c6e8a0b commit f175525

File tree

5 files changed

+63
-49
lines changed

5 files changed

+63
-49
lines changed

mypy/main.py

Lines changed: 3 additions & 3 deletions
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
@@ -1019,10 +1019,10 @@ def set_strict_flags() -> None:
10191019
# Set target.
10201020
if special_opts.modules + special_opts.packages:
10211021
options.build_type = BuildType.MODULE
1022-
egg_dirs, site_packages = get_site_packages_dirs(options.python_executable)
1022+
egg_dirs, site_packages, sys_path = get_search_dirs(options.python_executable)
10231023
search_paths = SearchPaths((os.getcwd(),),
10241024
tuple(mypy_path() + options.mypy_path),
1025-
tuple(egg_dirs + site_packages),
1025+
tuple(egg_dirs + site_packages + sys_path),
10261026
())
10271027
targets = []
10281028
# TODO: use the same cache that the BuildManager will

mypy/modulefinder.py

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
'SearchPaths',
2626
[('python_path', Tuple[str, ...]), # where user code is found
2727
('mypy_path', Tuple[str, ...]), # from $MYPYPATH or config variable
28-
('package_path', Tuple[str, ...]), # from get_site_packages_dirs()
28+
('package_path', Tuple[str, ...]), # from get_search_dirs()
2929
('typeshed_path', Tuple[str, ...]), # paths in typeshed
3030
])
3131

@@ -585,28 +585,7 @@ def default_lib_path(data_dir: str,
585585

586586

587587
@functools.lru_cache(maxsize=None)
588-
def get_prefixes(python_executable: Optional[str]) -> Tuple[str, str]:
589-
"""Get the sys.base_prefix and sys.prefix for the given python.
590-
591-
This runs a subprocess call to get the prefix paths of the given Python executable.
592-
To avoid repeatedly calling a subprocess (which can be slow!) we
593-
lru_cache the results.
594-
"""
595-
if python_executable is None:
596-
return '', ''
597-
elif python_executable == sys.executable:
598-
# Use running Python's package dirs
599-
return pyinfo.getprefixes()
600-
else:
601-
# Use subprocess to get the package directory of given Python
602-
# executable
603-
return ast.literal_eval(
604-
subprocess.check_output([python_executable, pyinfo.__file__, 'getprefixes'],
605-
stderr=subprocess.PIPE).decode())
606-
607-
608-
@functools.lru_cache(maxsize=None)
609-
def get_site_packages_dirs(python_executable: Optional[str]) -> Tuple[List[str], List[str]]:
588+
def get_search_dirs(python_executable: Optional[str]) -> Tuple[List[str], List[str], List[str]]:
610589
"""Find package directories for given python.
611590
612591
This runs a subprocess call, which generates a list of the egg directories, and the site
@@ -615,17 +594,17 @@ def get_site_packages_dirs(python_executable: Optional[str]) -> Tuple[List[str],
615594
"""
616595

617596
if python_executable is None:
618-
return [], []
597+
return [], [], []
619598
elif python_executable == sys.executable:
620599
# Use running Python's package dirs
621-
site_packages = pyinfo.getsitepackages()
600+
site_packages, sys_path = pyinfo.getsearchdirs()
622601
else:
623602
# Use subprocess to get the package directory of given Python
624603
# executable
625-
site_packages = ast.literal_eval(
626-
subprocess.check_output([python_executable, pyinfo.__file__, 'getsitepackages'],
604+
site_packages, sys_path = ast.literal_eval(
605+
subprocess.check_output([python_executable, pyinfo.__file__, 'getsearchdirs'],
627606
stderr=subprocess.PIPE).decode())
628-
return expand_site_packages(site_packages)
607+
return expand_site_packages(site_packages) + (sys_path,)
629608

630609

631610
def expand_site_packages(site_packages: List[str]) -> Tuple[List[str], List[str]]:
@@ -758,10 +737,8 @@ def compute_search_paths(sources: List[BuildSource],
758737
if options.python_version[0] == 2:
759738
mypypath = add_py2_mypypath_entries(mypypath)
760739

761-
egg_dirs, site_packages = get_site_packages_dirs(options.python_executable)
762-
base_prefix, prefix = get_prefixes(options.python_executable)
763-
is_venv = base_prefix != prefix
764-
for site_dir in site_packages:
740+
egg_dirs, site_packages, sys_path = get_search_dirs(options.python_executable)
741+
for site_dir in site_packages + sys_path:
765742
assert site_dir not in lib_path
766743
if (site_dir in mypypath or
767744
any(p.startswith(site_dir + os.path.sep) for p in mypypath) or
@@ -770,15 +747,10 @@ def compute_search_paths(sources: List[BuildSource],
770747
print("See https://mypy.readthedocs.io/en/stable/running_mypy.html"
771748
"#how-mypy-handles-imports for more info", file=sys.stderr)
772749
sys.exit(1)
773-
elif site_dir in python_path and (is_venv and not site_dir.startswith(prefix)):
774-
print("{} is in the PYTHONPATH. Please change directory"
775-
" so it is not.".format(site_dir),
776-
file=sys.stderr)
777-
sys.exit(1)
778750

779751
return SearchPaths(python_path=tuple(reversed(python_path)),
780752
mypy_path=tuple(mypypath),
781-
package_path=tuple(egg_dirs + site_packages),
753+
package_path=tuple(egg_dirs + site_packages + sys_path),
782754
typeshed_path=tuple(lib_path))
783755

784756

mypy/pyinfo.py

Lines changed: 23 additions & 8 deletions
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 sys.base_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
if hasattr(site, 'getusersitepackages') and hasattr(site, 'getsitepackages'):
2845
user_dir = site.getusersitepackages()
@@ -33,10 +50,8 @@ def getsitepackages():
3350

3451

3552
if __name__ == '__main__':
36-
if sys.argv[-1] == 'getsitepackages':
37-
print(repr(getsitepackages()))
38-
elif sys.argv[-1] == 'getprefixes':
39-
print(repr(getprefixes()))
53+
if sys.argv[-1] == 'getsearchdirs':
54+
print(repr(getsearchdirs()))
4055
else:
4156
print("ERROR: incorrect argument to pyinfo.py.", file=sys.stderr)
4257
sys.exit(1)

mypy/test/testcmdline.py

Lines changed: 3 additions & 0 deletions
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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,30 @@ main.py:6: error: Unsupported operand types for + ("int" and "str")
363363
main.py:7: error: Module has no attribute "y"
364364
main.py:8: error: Unsupported operand types for + (Module and "int")
365365

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

0 commit comments

Comments
 (0)