Skip to content

Commit 9ab6936

Browse files
emmatypinggvanrossum
authored andcommitted
Support egg/setuptools packages for PEP 561 searching. (#5282)
This adds support for setuptool's egg format, which includes support for editable installs (`pip install -e .`/`python setup.py develop`)! Setuptools creates its own directory to put the package we want to find. These directories are listed in `easy-install.pth` in a site-package directory. Fixes #5007.
1 parent c77e27a commit 9ab6936

File tree

6 files changed

+73
-15
lines changed

6 files changed

+73
-15
lines changed

docs/source/installed_packages.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ If you would like to publish a library package to a package repository (e.g.
4343
PyPI) for either internal or external use in type checking, packages that
4444
supply type information via type comments or annotations in the code should put
4545
a ``py.typed`` in their package directory. For example, with a directory
46-
structure as follows:
46+
structure as follows
4747

4848
.. code-block:: text
4949
@@ -53,7 +53,7 @@ structure as follows:
5353
lib.py
5454
py.typed
5555
56-
the setup.py might look like:
56+
the setup.py might look like
5757

5858
.. code-block:: python
5959
@@ -67,8 +67,13 @@ the setup.py might look like:
6767
packages=["package_a"]
6868
)
6969
70+
.. note::
71+
72+
If you use setuptools, you must pass the option ``zip_safe=False`` to
73+
``setup()``, or mypy will not be able to find the installed package.
74+
7075
Some packages have a mix of stub files and runtime files. These packages also
71-
require a ``py.typed`` file. An example can be seen below:
76+
require a ``py.typed`` file. An example can be seen below
7277

7378
.. code-block:: text
7479

mypy/build.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,26 +199,43 @@ def default_flush_errors(new_messages: List[str], is_serious: bool) -> None:
199199

200200

201201
@functools.lru_cache(maxsize=None)
202-
def _get_site_packages_dirs(python_executable: Optional[str]) -> List[str]:
202+
def _get_site_packages_dirs(python_executable: Optional[str],
203+
fscache: FileSystemCache) -> List[str]:
203204
"""Find package directories for given python.
204205
205206
This runs a subprocess call, which generates a list of the site package directories.
206207
To avoid repeatedly calling a subprocess (which can be slow!) we lru_cache the results."""
208+
def make_abspath(path: str, root: str) -> str:
209+
"""Take a path and make it absolute relative to root if not already absolute."""
210+
if os.path.isabs(path):
211+
return os.path.normpath(path)
212+
else:
213+
return os.path.join(root, os.path.normpath(path))
214+
207215
if python_executable is None:
208216
return []
209217
if python_executable == sys.executable:
210218
# Use running Python's package dirs
211-
return sitepkgs.getsitepackages()
219+
site_packages = sitepkgs.getsitepackages()
212220
else:
213221
# Use subprocess to get the package directory of given Python
214222
# executable
215-
return ast.literal_eval(subprocess.check_output([python_executable, sitepkgs.__file__],
216-
stderr=subprocess.PIPE).decode())
223+
site_packages = ast.literal_eval(
224+
subprocess.check_output([python_executable, sitepkgs.__file__],
225+
stderr=subprocess.PIPE).decode())
226+
egg_dirs = []
227+
for dir in site_packages:
228+
pth = os.path.join(dir, 'easy-install.pth')
229+
if fscache.isfile(pth):
230+
with open(pth) as f:
231+
egg_dirs.extend([make_abspath(d.rstrip(), dir) for d in f.readlines()])
232+
return egg_dirs + site_packages
217233

218234

219235
def compute_search_paths(sources: List[BuildSource],
220236
options: Options,
221237
data_dir: str,
238+
fscache: FileSystemCache,
222239
alt_lib_path: Optional[str] = None) -> SearchPaths:
223240
"""Compute the search paths as specified in PEP 561.
224241
@@ -275,7 +292,7 @@ def compute_search_paths(sources: List[BuildSource],
275292
if alt_lib_path:
276293
mypypath.insert(0, alt_lib_path)
277294

278-
package_path = tuple(_get_site_packages_dirs(options.python_executable))
295+
package_path = tuple(_get_site_packages_dirs(options.python_executable, fscache))
279296
for site_dir in package_path:
280297
assert site_dir not in lib_path
281298
if site_dir in mypypath:
@@ -306,7 +323,7 @@ def _build(sources: List[BuildSource],
306323
data_dir = default_data_dir(bin_dir)
307324
fscache = fscache or FileSystemCache()
308325

309-
search_paths = compute_search_paths(sources, options, data_dir, alt_lib_path)
326+
search_paths = compute_search_paths(sources, options, data_dir, fscache, alt_lib_path)
310327

311328
reports = Reports(data_dir, options.report_dirs)
312329
source_set = BuildSourceSet(sources)

mypy/dmypy_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def fine_grained_increment(self, sources: List[mypy.build.BuildSource]) -> Dict[
327327
self.update_sources(sources)
328328
changed, removed = self.find_changed(sources)
329329
manager.search_paths = mypy.build.compute_search_paths(
330-
sources, manager.options, manager.data_dir)
330+
sources, manager.options, manager.data_dir, mypy.build.FileSystemCache())
331331
t1 = time.time()
332332
messages = self.fine_grained_manager.update(changed, removed)
333333
t2 = time.time()

mypy/test/testpep561.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from unittest import TestCase, main
77

88
import mypy.api
9-
from mypy.build import _get_site_packages_dirs
9+
from mypy.build import _get_site_packages_dirs, FileSystemCache
1010
from mypy.test.config import package_path
1111
from mypy.test.helpers import run_command
1212
from mypy.util import try_find_python2_interpreter
@@ -67,10 +67,22 @@ def virtualenv(self,
6767
yield venv_dir, os.path.abspath(os.path.join(venv_dir, 'bin', 'python'))
6868

6969
def install_package(self, pkg: str,
70-
python_executable: str = sys.executable) -> None:
70+
python_executable: str = sys.executable,
71+
use_pip: bool = True,
72+
editable: bool = False) -> None:
7173
"""Context manager to temporarily install a package from test-data/packages/pkg/"""
7274
working_dir = os.path.join(package_path, pkg)
73-
install_cmd = [python_executable, '-m', 'pip', 'install', '.']
75+
if use_pip:
76+
install_cmd = [python_executable, '-m', 'pip', 'install']
77+
if editable:
78+
install_cmd.append('-e')
79+
install_cmd.append('.')
80+
else:
81+
install_cmd = [python_executable, 'setup.py']
82+
if editable:
83+
install_cmd.append('develop')
84+
else:
85+
install_cmd.append('install')
7486
returncode, lines = run_command(install_cmd, cwd=working_dir)
7587
if returncode != 0:
7688
self.fail('\n'.join(lines))
@@ -92,7 +104,7 @@ def tearDown(self) -> None:
92104

93105
def test_get_pkg_dirs(self) -> None:
94106
"""Check that get_package_dirs works."""
95-
dirs = _get_site_packages_dirs(sys.executable)
107+
dirs = _get_site_packages_dirs(sys.executable, FileSystemCache())
96108
assert dirs
97109

98110
def test_typedpkg_stub_package(self) -> None:
@@ -155,6 +167,28 @@ def test_typedpkg_python2(self) -> None:
155167
venv_dir=venv_dir,
156168
)
157169

170+
def test_typedpkg_egg(self) -> None:
171+
with self.virtualenv() as venv:
172+
venv_dir, python_executable = venv
173+
self.install_package('typedpkg', python_executable, use_pip=False)
174+
check_mypy_run(
175+
[self.tempfile],
176+
python_executable,
177+
expected_out=self.msg_tuple,
178+
venv_dir=venv_dir,
179+
)
180+
181+
def test_typedpkg_editable(self) -> None:
182+
with self.virtualenv() as venv:
183+
venv_dir, python_executable = venv
184+
self.install_package('typedpkg', python_executable, editable=True)
185+
check_mypy_run(
186+
[self.tempfile],
187+
python_executable,
188+
expected_out=self.msg_tuple,
189+
venv_dir=venv_dir,
190+
)
191+
158192

159193
if __name__ == '__main__':
160194
main()

test-data/packages/typedpkg/setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
This setup file installs packages to test mypy's PEP 561 implementation
33
"""
44

5-
from distutils.core import setup
5+
from setuptools import setup
66

77
setup(
88
name='typedpkg',
@@ -11,4 +11,5 @@
1111
package_data={'typedpkg': ['py.typed']},
1212
packages=['typedpkg'],
1313
include_package_data=True,
14+
zip_safe=False,
1415
)

test-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ typed-ast>=1.1.0,<1.2.0
1010
typing>=3.5.2; python_version < '3.5'
1111
py>=1.5.2
1212
virtualenv
13+
setuptools

0 commit comments

Comments
 (0)