Skip to content

Implement PEP 561 searching #4403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 105 commits into from
Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
e4b5274
Simplify find_module cache and document PEP 561
emmatyping Dec 14, 2017
6b6a97d
More work on PEP 561 impl
emmatyping Dec 15, 2017
2c936e1
Scaffold testing and fix bugs.
emmatyping Dec 16, 2017
8f64f81
Add python arg for find_module
emmatyping Dec 17, 2017
ddc526f
Get packages from Python executables
emmatyping Dec 17, 2017
329dc68
Add support for non-running Python, fix bug in impl
emmatyping Dec 21, 2017
eba98d4
Merge branch 'master' of https://github.com/python/mypy into pep561-impl
emmatyping Dec 21, 2017
5576629
Fix mypy self check
emmatyping Dec 21, 2017
2397763
Fix weird subprocess bug that works in pydevd, but not otherwise
emmatyping Dec 21, 2017
fda2a2c
Add docs for PEP561 impl
emmatyping Dec 22, 2017
59f0c49
Add initial tests for PEP 561 checking
emmatyping Dec 22, 2017
c976008
Clean up tests a bit
emmatyping Dec 22, 2017
8df8b8d
Get tests passing
emmatyping Dec 22, 2017
4ca6939
Add note about tests
emmatyping Dec 22, 2017
a96601c
Clear site-packages from cache.
emmatyping Jan 3, 2018
336fb6b
Show sample package layout, change help text
emmatyping Jan 3, 2018
8492d8b
Merge branch 'master' into pep561-impl
emmatyping Jan 3, 2018
c41eef2
Merge branch 'master' into pep561-impl
emmatyping Jan 3, 2018
1f005d7
Fix deletion of site-packages from cache
emmatyping Feb 1, 2018
45afe58
Simplify testcase file creation/deletion
emmatyping Feb 1, 2018
e7f23bb
Don't use common path and fix lint errors
emmatyping Feb 1, 2018
d74eae8
Merge branch 'master' into pep561-impl
emmatyping Feb 14, 2018
5822884
Update PEP561 implementation, docs
emmatyping Feb 15, 2018
c734bc2
Clarify python-executable purpose
emmatyping Feb 15, 2018
1d7cc7e
Document --python-executable
emmatyping Feb 15, 2018
1ac4253
Assure dir cache is always set for a key
emmatyping Feb 15, 2018
f681bf9
Fix indentation of code and command line
emmatyping Feb 15, 2018
85f233c
Fix silly indentation mistake (again)
emmatyping Feb 16, 2018
3726f18
Add check for Python packages if executable given
emmatyping Feb 16, 2018
59a2004
Finish implementation and tests for alternate executable
emmatyping Feb 16, 2018
77b3b66
Add note about advanced import methods
emmatyping Feb 16, 2018
1da6e58
Remove undocumented flag
emmatyping Feb 16, 2018
e54025f
Maintain source information in traceback
emmatyping Feb 16, 2018
056dc8f
Clean up testpackages docstrings
emmatyping Feb 16, 2018
297fac6
Even better tracebacks for package tests
emmatyping Feb 16, 2018
e35e7aa
User install on Python not executing tests
emmatyping Feb 16, 2018
ecb702e
Better debug info on failed typecheck
emmatyping Feb 16, 2018
1a41262
Check if package was installed user
emmatyping Feb 16, 2018
31dab8d
Check if user site exits
emmatyping Feb 16, 2018
ff15057
Parentheses have haunted me since Scheme
emmatyping Feb 16, 2018
9442765
Don't unconditionally set python
emmatyping Feb 16, 2018
a34e7e8
Cleanup tests
emmatyping Feb 16, 2018
0fd592a
Fix for mypy check
emmatyping Feb 16, 2018
4cbe21a
Remove debugging info
emmatyping Feb 16, 2018
f566ed0
Try to delete simple.py after all tests in case have run
emmatyping Feb 16, 2018
206f70b
Make tearDownClass a classmethod
emmatyping Feb 16, 2018
2dbff97
Don't clean up twice, as it can break stubgen tests
emmatyping Feb 16, 2018
e33ceb7
Set python version from executable
emmatyping Feb 16, 2018
644e9a5
Fix mypy self check and doc indentation
emmatyping Feb 16, 2018
af38a12
Dedent docs
emmatyping Feb 18, 2018
b7f0788
Add missed flag back
emmatyping Feb 18, 2018
0564a5c
Remove silencing of errors in installed packages
emmatyping Feb 20, 2018
d17582b
Clean up get_package_dirs
emmatyping Feb 20, 2018
085d002
Make assert a user facing error
emmatyping Feb 21, 2018
8958dc7
Refactor get_package_dirs
emmatyping Feb 21, 2018
84fd527
Make test package typesafe to avoid error
emmatyping Feb 21, 2018
4c0ff1b
Clarify docs of unsupported import features
emmatyping Feb 21, 2018
a917ff5
Reverse equality order
emmatyping Feb 21, 2018
83ab973
Correct to more sensible key type for package_dir_cache
emmatyping Feb 21, 2018
cfac326
python -> python_executable
emmatyping Feb 21, 2018
aefcb96
Set python_executable based on python_version
emmatyping Feb 21, 2018
380e301
Fix weird indentation
emmatyping Feb 21, 2018
47d8e0d
Refactor and clarify checks for package information
emmatyping Feb 21, 2018
5c19e8c
Simplify check
emmatyping Feb 21, 2018
56dfcd4
Last python -> python_executable
emmatyping Feb 21, 2018
4293dad
Use lru_cache on get_package_dirs
emmatyping Feb 21, 2018
b810d3e
Fix lint
emmatyping Feb 21, 2018
17488ad
Refactor version setting from executable
emmatyping Feb 21, 2018
e348471
Handle python executable with spaces
emmatyping Feb 21, 2018
8184937
Return early in get_package_dirs
emmatyping Feb 21, 2018
97f29cc
Refactor and clean up testpackages
emmatyping Feb 21, 2018
928683a
Fix executable flag and version inference
emmatyping Feb 21, 2018
ae65564
Clean up Python version/executable inference
emmatyping Feb 21, 2018
f6c4327
Make typedpkg python2 compatible
emmatyping Feb 21, 2018
d475d4b
Overwrite default Python version if python_executable set
emmatyping Feb 21, 2018
03cd378
Move returns into try blocks
emmatyping Feb 21, 2018
3a8ca60
Refactor python-version/executable inference
emmatyping Feb 22, 2018
e1f9494
Clarify searching for packages and mixed packages
emmatyping Feb 22, 2018
b5cbd09
Clarify default behaviour of python-executable flag
emmatyping Feb 22, 2018
fc58e4c
Comment and refactor getsitepackage usage
emmatyping Feb 22, 2018
96e2f55
Make python_executable mandatory for find_packages
emmatyping Feb 23, 2018
80f4a35
Remove py.typed file from -stubs test package
emmatyping Feb 23, 2018
dc29e51
Write testpackages to test folder
emmatyping Feb 23, 2018
be733c5
Refactor tests to not be run with package check
emmatyping Feb 23, 2018
c1264a0
Disable pep561 searching with --no-site-packages
emmatyping Feb 24, 2018
ceacbff
Fix return type to be non-optional
emmatyping Feb 24, 2018
f389f64
Set python_executable to None if not inferred
emmatyping Feb 24, 2018
1bd9f66
Set flags for tests needing 3.6
emmatyping Feb 24, 2018
806fbef
Document no-site-packages
emmatyping Feb 24, 2018
c9e3361
Fix bugs with running mypy on 3.4
emmatyping Feb 24, 2018
7be3555
Merge branch 'master' of https://github.com/python/mypy into pep561-impl
emmatyping Feb 24, 2018
5fc9d52
Disable PEP561 searching if --no-site-packages is passed
emmatyping Feb 25, 2018
ae86abd
Clean up tests
emmatyping Feb 26, 2018
0bf024f
Move no-site-packages to special-opts
emmatyping Feb 26, 2018
c9c35c1
Remove duplicate exit and error message
emmatyping Feb 26, 2018
dca9c54
Refactor name, add missing paren
emmatyping Feb 26, 2018
a929b46
Fix test import
emmatyping Feb 26, 2018
e56ffe3
Fix silly name mistake
emmatyping Feb 26, 2018
72d7b0a
Link to PEP 561 and add note about inference
emmatyping Feb 26, 2018
c9f6bc2
Fixup docs
emmatyping Feb 26, 2018
4698d23
Refactor find_module and clarify comment
emmatyping Mar 1, 2018
db0680a
Refactor find_module to not be monolithic
emmatyping Mar 5, 2018
8367e57
Key find_module_dir_cache with lib_path as well
emmatyping Mar 5, 2018
24b6742
Don't mutate dir cache with site packages
emmatyping Mar 5, 2018
afb80ea
Refactor stub package searching
emmatyping Mar 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ summary of command line flags can always be printed using the ``-h``
flag (or its long form ``--help``)::

$ mypy -h
usage: mypy [-h] [-v] [-V] [--python-version x.y] [--platform PLATFORM] [-2]
usage: mypy [-h] [-v] [-V] [--python-version x.y]
[--python-executable PYTHON_EXECUTABLE] [--platform PLATFORM] [-2]
[--ignore-missing-imports]
[--follow-imports {normal,silent,skip,error}]
[--disallow-any-{unimported,expr,decorated,explicit,generics}]
[--disallow-untyped-calls] [--disallow-untyped-defs]
[--disallow-any-unimported] [--disallow-any-expr]
[--disallow-any-decorated] [--disallow-any-explicit]
[--disallow-any-generics] [--disallow-untyped-calls]
[--disallow-untyped-defs] [--disallow-incomplete-defs]
[--check-untyped-defs] [--disallow-subclassing-any]
[--warn-incomplete-stub] [--warn-redundant-casts]
[--no-warn-no-return] [--warn-return-any] [--warn-unused-ignores]
[--warn-incomplete-stub] [--disallow-untyped-decorators]
[--warn-redundant-casts] [--no-warn-no-return] [--warn-return-any]
[--warn-unused-ignores] [--warn-unused-configs]
[--show-error-context] [--no-implicit-optional] [-i]
[--quick-and-dirty] [--cache-dir DIR] [--skip-version-check]
[--strict-optional]
[--quick-and-dirty] [--cache-dir DIR] [--cache-fine-grained]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did this option come from, and if it's introduced in this patch, should it be documented too?

Copy link
Member Author

@emmatyping emmatyping Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was introduced in #4526 but not added to the docs it seems. I suppose it would be better to have it formally documented in a seperate PR. (Guido wants me to add it)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(FWIW the reason it didn't appear in the docs right away was simply that we don't regenerate this list every time we add a flag. But we should.)

[--skip-version-check] [--strict-optional]
[--strict-optional-whitelist [GLOB [GLOB ...]]]
[--junit-xml JUNIT_XML] [--pdb] [--show-traceback] [--stats]
[--inferstats] [--custom-typing MODULE]
Expand All @@ -28,9 +32,9 @@ flag (or its long form ``--help``)::
[--shadow-file SOURCE_FILE SHADOW_FILE] [--any-exprs-report DIR]
[--cobertura-xml-report DIR] [--html-report DIR]
[--linecount-report DIR] [--linecoverage-report DIR]
[--memory-xml-report DIR]
[--txt-report DIR] [--xml-report DIR] [--xslt-html-report DIR]
[--xslt-txt-report DIR] [-m MODULE] [-c PROGRAM_TEXT] [-p PACKAGE]
[--memory-xml-report DIR] [--txt-report DIR] [--xml-report DIR]
[--xslt-html-report DIR] [--xslt-txt-report DIR] [-m MODULE]
[-c PROGRAM_TEXT] [-p PACKAGE]
[files [files ...]]

(etc., too long to show everything here)
Expand Down Expand Up @@ -366,6 +370,12 @@ Here are some more useful flags:
updates the cache, but regular incremental mode ignores cache files
written by quick mode.

- ``--python-executable EXECUTABLE`` will have mypy collect type information
from PEP 561 compliant packages installed with the given Python executable.
By default, mypy will use PEP 561 compliant packages installed for the Python
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way this is now written makes me wonder what happens if I use Python 3.6 to run mypy --python-version 3.5 -- does that use the 3.6 site-packages or none at all? (Worse if run with --py2.)

executable running mypy. See :ref:`installed_packages` for more on making
PEP 561 compliant packages.

- ``--python-version X.Y`` will make mypy typecheck your code as if it were
run under Python version X.Y. Without this option, mypy will default to using
whatever version of Python is running mypy. Note that the ``-2`` and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should mention that this is inferred from the executable somewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps combine the docs as something like

``--python-executable EXECUTABLE`` and ``--python-version X.Y`` will
have mypy will check your code as if it were run under the given python
installation.
Either argument can be used to infer the other.
If support of PEP561 packages is needed (see `--no-site-packages`), the executable
must either be specified or inferable.

Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Mypy is a static type checker for Python.
command_line
config_file
python36
installed_packages
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you add checking_installed_packages too?

Copy link
Member Author

@emmatyping emmatyping Feb 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized this file is essentially a duplicate of installed_packages.rst (must have accidentally moved and checked it in somehow).

faq
cheat_sheet
cheat_sheet_py3
Expand Down
73 changes: 73 additions & 0 deletions docs/source/installed_packages.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
.. _installed-packages:

Using Installed Packages
========================

`PEP 561 <https://www.python.org/dev/peps/pep-0561/>`_ specifies how to mark
a package as supporting type checking. Below is a summary of how to create
PEP 561 compatible packages and have mypy use them in type checking.

Making PEP 561 compatible packages
**********************************

Packages that must be imported at runtime that supply type information should
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the second 'that' into 'and'?

put a ``py.typed`` in their package directory. For example, with a directory
structure as follows:

.. code-block:: text

setup.py
package_a/
__init__.py
lib.py
py.typed

the setup.py might look like:

.. code-block:: python

from distutils.core import setup

setup(
name="SuperPackageA",
author="Me",
version="0.1",
package_data={"package_a": ["py.typed"]},
packages=["package_a"]
)

If the package is entirely made up of stub (``*.pyi``) files, the package
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key difference with the previous example isn't that it's entirely made of stubs, it's that it is not intended to be used at runtime. I'd introduce this as "stub-only packages" or something like that (what does PEP 561 call them?).

Also can you show a third example (preferably in between the former and this one) that shows a package containing .py files and (some) .pyi files? This should explain that whenever both foo.py and foo.pyi exist, foo.py is used at runtime but foo.pyi is used by mypy. And it should show what you have to do to ensure that the foo.pyi is included in the distribution. Optionally it would be nice if it also showed (by example) that you don't need a .pyi for every .py (but you do need a .py for every .pyi in this case).

should have a suffix of ``-stubs``. For example, if we had stubs for
``package_b``, we might do the following:

.. code-block:: text

setup.py
package_b-stubs/
__init__.pyi
lib.pyi

the setup.py might look like:

.. code-block:: python
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three lines need unindenting too


from distutils.core import setup

setup(
name="SuperPackageB",
author="Me",
version="0.1",
package_data={"package_b-stubs": ["__init__.pyi", "lib.pyi"]},
packages=["package_b-stubs"]
)

Using PEP 561 compatible packages with mypy
*******************************************

Generally, you do not need to do anything to use installed packages for the
Python executable used to run mypy. They should be automatically picked up by
mypy and used for type checking.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing that custom import hooks are not supported?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Custom import hooks are not supported by mypy normally, and this doesn't add support for that either. Do you have a specific need for that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, but it might be nice to note in the docs exactly where mypy stops respecting the normal import mechanics. Are zip imports supported?

Copy link
Member Author

@emmatyping emmatyping Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a note of that to the docs, and no, zip imports are not supported to my knowledge.

Copy link
Contributor

@eric-wieser eric-wieser Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do pkg_util- and PEP420- style namespace packages fare? I suppose they can always be dealt with later, once you have a release for users in the wild to test with

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #4277 for the namespace packages implementation. That will be added later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, is there a dependency between this and #4277? Which should be merged first?


If you use mypy to type check a Python other than the version running mypy, you
can use the ``--python-executable`` flag to point to the executable, and mypy
will find packages installed for that python executable.
130 changes: 109 additions & 21 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import re
import site
import stat
from subprocess import check_output, STDOUT
import sys
import time
from os.path import dirname, basename
Expand Down Expand Up @@ -683,7 +684,7 @@ def correct_rel_imp(imp: Union[ImportFrom, ImportAll]) -> str:

def is_module(self, id: str) -> bool:
"""Is there a file in the file system corresponding to module id?"""
return find_module(id, self.lib_path) is not None
return find_module(id, self.lib_path, self.options.python_executable) is not None

def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> MypyFile:
"""Parse the source of a file with the given name.
Expand Down Expand Up @@ -796,14 +797,14 @@ def remove_cwd_prefix_from_path(p: str) -> str:
return p


# Cache find_module: (id, lib_path) -> result.
find_module_cache = {} # type: Dict[Tuple[str, Tuple[str, ...]], Optional[str]]
# Cache find_module: (id) -> result.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better, drop the() around id.

find_module_cache = {} # type: Dict[str, Optional[str]]

# Cache some repeated work within distinct find_module calls: finding which
# elements of lib_path have even the subdirectory they'd need for the module
# to exist. This is shared among different module ids when they differ only
# in the last component.
find_module_dir_cache = {} # type: Dict[Tuple[str, Tuple[str, ...]], List[str]]
find_module_dir_cache = {} # type: Dict[str, List[str]]

# Cache directory listings. We assume that while one os.listdir()
# call may be more expensive than one os.stat() call, a small number
Expand All @@ -818,6 +819,9 @@ def remove_cwd_prefix_from_path(p: str) -> str:
# Cache for isdir(join(head, tail))
find_module_isdir_cache = {} # type: Dict[Tuple[str, str], bool]

# Cache packages for Python executable
package_dirs_cache = [] # type: List[str]


def find_module_clear_caches() -> None:
find_module_cache.clear()
Expand Down Expand Up @@ -861,19 +865,70 @@ def is_file(path: str) -> bool:
return res


def find_module(id: str, lib_path_arg: Iterable[str]) -> Optional[str]:
USER_SITE_PACKAGES = \
'"import site; print(site.getusersitepackages()); print(*site.getsitepackages(), sep=\'\\n\')"'
VIRTUALENV_SITE_PACKAGES = \
'"from distutils.sysconfig import get_python_lib; print(get_python_lib())"'


def call_python(python: str, command: str) -> str:
return check_output([python, '-c', command]).decode(sys.stdout.encoding)


def get_package_dirs(python: Optional[str]) -> List[str]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as everywhere I'd rename python to python_executable.

"""Find package directories for given python

This defaults to the Python running mypy."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, another docstring style nit is that if the docstring is multi-line, the closing """ should be on a line by itself. (Again, mypy is not strict about this, but in general I am. :-)

if package_dirs_cache:
return package_dirs_cache
package_dirs = [] # type: List[str]
if python:
# Use subprocess to get the package directory of given Python
# executable
check = check_output([python, '-V'], stderr=STDOUT).decode('UTF-8')
assert check.startswith('Python'), \
"Mypy could not use the Python executable: {}".format(python)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine this will be a somewhat common user error, and when people see a failing assert + traceback they will mindlessly report a crash. So can you make this print a proper error message and exit with status 2?

# If we have a working python executable, query information from it
output = call_python(python, USER_SITE_PACKAGES)
for line in output.splitlines():
if os.path.isdir(line):
package_dirs.append(line)
if not package_dirs:
# if no paths are found, we fall back on sysconfig
output = call_python(python, VIRTUALENV_SITE_PACKAGES)
for line in output.splitlines():
if os.path.isdir(line):
package_dirs.append(line)
else:
# Use running Python's package dirs
try:
user_dir = site.getusersitepackages()
package_dirs = site.getsitepackages() + [user_dir]
except AttributeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What thing above can raise AttributeError? It's such a wide net to cast, maybe it's better to use an explicit hasattr() call if there's one specific attribute that's not always there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

site won't have either getusersitepackages nor getsitepackages if it is in a virtualenv.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment? This is a bit obscure. Also if either of those exist but the call raises an unrelated AttributeError this would also hide that (though that's low probability).

package_dirs = [get_python_lib()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the one above would be clearer as an early return

package_dirs_cache[:] = package_dirs
return package_dirs


def find_module(id: str, lib_path_arg: Iterable[str],
python: Optional[str] = None) -> Optional[str]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename python -> python_executable?

"""Return the path of the module source file, or None if not found."""
lib_path = tuple(lib_path_arg)
package_dirs = get_package_dirs(python)
if python:
assert package_dirs, "Could not find package directories for Python '{}'".format(python)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like it might be a user error that deserves better than an assert -- people will file crash bugs for failed asserts even if it's their own fault.

components = id.split('.')
dir_chain = os.sep.join(components[:-1]) # e.g., 'foo/bar'
Copy link
Contributor

@eric-wieser eric-wieser Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did these two lines move outside find?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the loop below that removes site-packages from the cache.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at the very least all of this can move inside the if id not in find_module_cache: path below


def find() -> Optional[str]:
# If we're looking for a module like 'foo.bar.baz', it's likely that most of the
# many elements of lib_path don't even have a subdirectory 'foo/bar'. Discover
# that only once and cache it for when we look for modules like 'foo.bar.blah'
# that will require the same subdirectory.
components = id.split('.')
dir_chain = os.sep.join(components[:-1]) # e.g., 'foo/bar'
if (dir_chain, lib_path) not in find_module_dir_cache:
dirs = []

dirs = find_module_dir_cache.get(dir_chain, [])
Copy link
Contributor

@eric-wieser eric-wieser Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be cached by the cache be keyed by all of the enclosing arguments as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I know what you mean, could you clarify?

Copy link
Member Author

@emmatyping emmatyping Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the id, python_version is cached (which I have just changed to make the case), it seems redundant to cache them here.

Copy link
Contributor

@eric-wieser eric-wieser Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_module_dir_cache[dir_chain] is set based on python_executable, which means that the cache will be invalid if python_version changes. You need to key on at least (site_packages_dirs, dir_chain).

Copy link
Contributor

@eric-wieser eric-wieser Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better fix would be to extra find from a closure function to a top-level function with arguments - then you can once again just apply @functools.lru_cache, knowing with certainty that your cache will never be stale from previous calls

if not dirs:
# Regular packages on the PATH
for pathitem in lib_path:
# e.g., '/usr/lib/python3.4/foo/bar'
isdir = find_module_isdir_cache.get((pathitem, dir_chain))
Expand All @@ -883,8 +938,24 @@ def find() -> Optional[str]:
find_module_isdir_cache[pathitem, dir_chain] = isdir
if isdir:
dirs.append(dir)
find_module_dir_cache[dir_chain, lib_path] = dirs
candidate_base_dirs = find_module_dir_cache[dir_chain, lib_path]

# Third-party stub/typed packages
for pkg_dir in package_dirs:
stub_name = components[0] + '-stubs'
typed_file = os.path.join(pkg_dir, components[0], 'py.typed')
stub_typed_file = os.path.join(pkg_dir, stub_name, 'py.typed')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised we're looking inside foo-stubs packages for a py.typed file. The example in the docs doesn't show a need for that or even mention the possibility. Shouldn't the mere presence of foo-stubs be enough?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I should update this. and remove the py.typed file.

if os.path.isfile(stub_typed_file):
components[0] = stub_name
rest = components[:-1]
path = os.path.join(pkg_dir, *rest)
if os.path.isdir(path):
dirs.append(path)
elif os.path.isfile(typed_file):
path = os.path.join(pkg_dir, dir_chain)
dirs.append(path)

find_module_dir_cache[dir_chain] = dirs
candidate_base_dirs = find_module_dir_cache[dir_chain]

# If we're looking for a module like 'foo.bar.baz', then candidate_base_dirs now
# contains just the subdirectories 'foo/bar' that actually exist under the
Expand All @@ -897,23 +968,33 @@ def find() -> Optional[str]:
# Prefer package over module, i.e. baz/__init__.py* over baz.py*.
for extension in PYTHON_EXTENSIONS:
path = base_path + sepinit + extension
path_stubs = base_path + '_stubs' + sepinit + extension
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you forget to change _stubs to -stubs here or am I missing something?

if is_file(path) and verify_module(id, path):
return path
elif is_file(path_stubs) and verify_module(id, path_stubs):
return path_stubs
# No package, look for module.
for extension in PYTHON_EXTENSIONS:
path = base_path + extension
if is_file(path) and verify_module(id, path):
return path
return None

key = (id, lib_path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the key no longer contain lib_path? Should it contain python_executable too? Do you want to @functools.lru_cache the whole function?

Copy link
Member Author

@emmatyping emmatyping Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it because the lib_path never changes, so it was worthless to use it as a key. Also FWIW, Guido is going to be refactoring a lot of the machinery here (see #4365) after this and namespace support are merged.

Copy link
Contributor

@eric-wieser eric-wieser Mar 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is, lib_path never changes until someone decides that it does, and then your cache is broken. What if someone calls mypy.main twice with different lib_path or python_executable arguments? Now your cache is garbage.

Caching is hard, @functools.lru_cache is easy. The cache here is to save filesystem costs, so you can easily afford a few extra hash() calls by keying on everything

if key not in find_module_cache:
find_module_cache[key] = find()
return find_module_cache[key]
if id not in find_module_cache:
find_module_cache[id] = find()

# If we searched for items with a base directory of site-packages/ we need to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment seems out of date now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that is correct. Perhaps it needs to be re-worded?

# remove it to avoid searching it for non-typed ids.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a non-typed id?

Copy link
Member Author

@emmatyping emmatyping Feb 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this could be worded better. id here refers to the id argument to find_module. If site-packages/ is searched, we don't want to search all folders under site-packages/, as that includes untyped packages.

for dir in package_dirs:
if dir + os.sep in find_module_dir_cache[dir_chain]:
find_module_dir_cache[dir_chain].remove(dir + os.sep)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be moved inside find, since that's the only place that sets find_module_dir_cache?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not easily, find has several return paths, and I need to clean this up for all of them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can at least be moved inside the above if then, which only runs if find() ran


return find_module_cache[id]


def find_modules_recursive(module: str, lib_path: List[str]) -> List[BuildSource]:
module_path = find_module(module, lib_path)
def find_modules_recursive(module: str, lib_path: List[str],
python: Optional[str]) -> List[BuildSource]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python -> python_executable.

module_path = find_module(module, lib_path, python)
if not module_path:
return []
result = [BuildSource(module_path, module, None)]
Expand All @@ -933,14 +1014,14 @@ def find_modules_recursive(module: str, lib_path: List[str]) -> List[BuildSource
(os.path.isfile(os.path.join(abs_path, '__init__.py')) or
os.path.isfile(os.path.join(abs_path, '__init__.pyi'))):
hits.add(item)
result += find_modules_recursive(module + '.' + item, lib_path)
result += find_modules_recursive(module + '.' + item, lib_path, python)
elif item != '__init__.py' and item != '__init__.pyi' and \
item.endswith(('.py', '.pyi')):
mod = item.split('.')[0]
if mod not in hits:
hits.add(mod)
result += find_modules_recursive(
module + '.' + mod, lib_path)
result += find_modules_recursive(module + '.' + mod,
lib_path, python)
return result


Expand Down Expand Up @@ -1606,8 +1687,15 @@ def __init__(self,
# difference and just assume 'builtins' everywhere,
# which simplifies code.
file_id = '__builtin__'
path = find_module(file_id, manager.lib_path)
path = find_module(file_id, manager.lib_path, manager.options.python_executable)
if path:
# Installed package modules should be silenced. They are all under absolute
# paths. When 3.4 is dropped, this should just use os.path.commonpath.
if os.path.isabs(path):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use a comment. IIUC its purpose is to ignore all errors for modules given by absolute path. That also seems dangerous -- what if somehow I were to run mypy $PWD?

Copy link
Member Author

@emmatyping emmatyping Feb 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, and since you misunderstood this, it definitely deserves some explanation in a comment :)

We want to silence errors in modules that are installed packages. The best heuristic that works on all Python versions that I could find is to check that if we are have a path (eg /foo/bar/baz) and we have a single package directory /foo/bar, the path should start with /foo/bar/ (note the last seperator). I originally formulated this differently, hence the weird code, but I think

if os.path.isabs(path):
    for dir in package_dirs_cache:
         # if dir is /foo and path is /foo/bar, the next character after
         # the prefix is /
         if path.startswith(dir + os.sep):
            self.ignore_all = True

should accomplish the same task and be cleaner. The abspath check is just because all of the package directories that are installed packages (and will need to be silenced by this check) will be absolute, so we can filter relative paths.

This does have the rather unfortunate unintended side-effect of silencing errors for typeshed and anything else under eg C:\PythonXY\ on Windows. I don't think this is an issue, since typeshed probably shouldn't report errors, but I do admit it is less than ideal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I definitely don't want to silence errors in typeshed. Sometimes people pass flags that cause errors in typeshed, and those errors might explain other errors they see, so silencing them would be wrong.

Also, maybe add a comment to the effect that the isabs() check is a mere optimization? (Other than that I now understand this. Also I read it too quickly before and didn't even notice it was inside State.__init__.)

# Silence errors from module if it is in a package directory
for dir in package_dirs_cache:
if path.startswith(dir + os.sep):
self.ignore_all = True
# For non-stubs, look at options.follow_imports:
# - normal (default) -> fully analyze
# - silent -> analyze but silence errors
Expand Down
6 changes: 5 additions & 1 deletion mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ def add_invertible_flag(flag: str,
version='%(prog)s ' + __version__)
parser.add_argument('--python-version', type=parse_version, metavar='x.y',
help='use Python x.y')
parser.add_argument('--python-executable', action='store',
help="Python executable whose installed packages will be"
" used in typechecking.")
parser.add_argument('--platform', action='store', metavar='PLATFORM',
help="typecheck special-cased code for the given OS platform "
"(defaults to sys.platform).")
Expand Down Expand Up @@ -524,7 +527,8 @@ def add_invertible_flag(flag: str,
.format(special_opts.package))
options.build_type = BuildType.MODULE
lib_path = [os.getcwd()] + build.mypy_path()
targets = build.find_modules_recursive(special_opts.package, lib_path)
targets = build.find_modules_recursive(special_opts.package, lib_path,
options.python_executable)
if not targets:
fail("Can't find package '{}'".format(special_opts.package))
return targets, options
Expand Down
1 change: 1 addition & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(self) -> None:
# -- build options --
self.build_type = BuildType.STANDARD
self.python_version = defaults.PYTHON3_VERSION
self.python_executable = None # type: Optional[str]
Copy link
Contributor

@eric-wieser eric-wieser Feb 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If python_version defaults to defaults.PYTHON3_VERSION, doesn't that mean that passing --python-executable=python2.7 will lead to a mismatch in version and executable? Should these both be set to None initially?

Copy link
Member Author

@emmatyping emmatyping Feb 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I should probably change these to both default to None and modify them after option parsing. test_coherence doesn't like my current changes 😟.

self.platform = sys.platform
self.custom_typing_module = None # type: Optional[str]
self.custom_typeshed_dir = None # type: Optional[str]
Expand Down
3 changes: 2 additions & 1 deletion mypy/stubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ def find_module_path_and_all(module: str, pyversion: Tuple[int, int],
module_all = getattr(mod, '__all__', None)
else:
# Find module by going through search path.
module_path = mypy.build.find_module(module, ['.'] + search_path)
module_path = mypy.build.find_module(module, ['.'] + search_path,
interpreter)
if not module_path:
raise SystemExit(
"Can't find module '{}' (consider using --search-path)".format(module))
Expand Down
Loading