Skip to content
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

Add --exclude #9992

Merged
merged 22 commits into from
Feb 10, 2021
7 changes: 7 additions & 0 deletions docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ for full details, see :ref:`running-mypy`.
Asks mypy to type check the provided string as a program.


.. option:: --ignore-path

Asks mypy to ignore a given file name, directory name or subpath while
recursively discovering files to check. This flag may be repeated multiple
times.


Optional arguments
******************

Expand Down
9 changes: 9 additions & 0 deletions docs/source/config_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ section of the command line docs.

This option may only be set in the global section (``[mypy]``).

.. confval:: ignore_path

:type: comma-separated list of strings

A comma-separated list of file names, directory names or subpaths which mypy
should ignore while recursively discovering files to check.

This option may only be set in the global section (``[mypy]``).
hauntsaninja marked this conversation as resolved.
Show resolved Hide resolved

.. confval:: namespace_packages

:type: boolean
Expand Down
3 changes: 2 additions & 1 deletion docs/source/running_mypy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ to modules to type check.
- Mypy will check all paths provided that correspond to files.

- Mypy will recursively discover and check all files ending in ``.py`` or
``.pyi`` in directory paths provided.
``.pyi`` in directory paths provided, after accounting for
:option:`--ignore-path <mypy --ignore-path>`.

- For each file to be checked, mypy will attempt to associate the file (e.g.
``project/foo/bar/baz.py``) with a fully qualified module name (e.g.
Expand Down
15 changes: 6 additions & 9 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import gc
import json
import os
import pathlib
import re
import stat
import sys
Expand Down Expand Up @@ -2769,14 +2768,12 @@ def load_graph(sources: List[BuildSource], manager: BuildManager,
"Duplicate module named '%s' (also at '%s')" % (st.id, graph[st.id].xpath),
blocker=True,
)
p1 = len(pathlib.PurePath(st.xpath).parents)
p2 = len(pathlib.PurePath(graph[st.id].xpath).parents)

if p1 != p2:
manager.errors.report(
-1, -1,
"Are you missing an __init__.py?"
)
manager.errors.report(
-1, -1,
"Are you missing an __init__.py? Alternatively, consider using --ignore-path to "
"avoid checking one of them.",
severity='note'
)

manager.errors.raise_error()
graph[st.id] = st
Expand Down
1 change: 1 addition & 0 deletions mypy/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def check_follow_imports(choice: str) -> str:
'custom_typing_module': str,
'custom_typeshed_dir': expand_path,
'mypy_path': lambda s: [expand_path(p.strip()) for p in re.split('[,:]', s)],
'ignore_path': lambda s: [expand_path(p.strip()) for p in s.split(",")],
'files': split_and_match_files,
'quickstart_file': expand_path,
'junit_xml': expand_path,
Expand Down
11 changes: 9 additions & 2 deletions mypy/find_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import List, Sequence, Set, Tuple, Optional
from typing_extensions import Final

from mypy.modulefinder import BuildSource, PYTHON_EXTENSIONS, mypy_path
from mypy.modulefinder import BuildSource, PYTHON_EXTENSIONS, mypy_path, matches_ignore_pattern
from mypy.fscache import FileSystemCache
from mypy.options import Options

Expand Down Expand Up @@ -91,6 +91,7 @@ def __init__(self, fscache: FileSystemCache, options: Options) -> None:
self.fscache = fscache
self.explicit_package_bases = get_explicit_package_bases(options)
self.namespace_packages = options.namespace_packages
self.ignore_path = options.ignore_path

def is_explicit_package_base(self, path: str) -> bool:
assert self.explicit_package_bases
Expand All @@ -103,9 +104,15 @@ def find_sources_in_dir(self, path: str) -> List[BuildSource]:
names = sorted(self.fscache.listdir(path), key=keyfunc)
for name in names:
# Skip certain names altogether
if name == '__pycache__' or name.startswith('.') or name.endswith('~'):
if (
name in ("__pycache__", "site-packages", "node_modules")
or name.startswith(".")
or name.endswith("~")
):
continue
hauntsaninja marked this conversation as resolved.
Show resolved Hide resolved
subpath = os.path.join(path, name)
if any(matches_ignore_pattern(subpath, pattern) for pattern in self.ignore_path):
continue

if self.fscache.isdir(subpath):
sub_sources = self.find_sources_in_dir(subpath)
Expand Down
7 changes: 7 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,13 @@ def add_invertible_flag(flag: str,
code_group.add_argument(
'--explicit-package-bases', action='store_true',
help="Use current directory and MYPYPATH to determine module names of files passed")
code_group.add_argument(
"--ignore-path",
metavar="PATH",
action="append",
default=[],
help="File names, directory names or subpaths to avoid checking",
)
code_group.add_argument(
'-m', '--module', action='append', metavar='MODULE',
default=[],
Expand Down
22 changes: 21 additions & 1 deletion mypy/modulefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,17 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
names = sorted(self.fscache.listdir(package_path))
for name in names:
# Skip certain names altogether
if name == '__pycache__' or name.startswith('.') or name.endswith('~'):
if (
name in ("__pycache__", "site-packages", "node_modules")
or name.startswith(".")
or name.endswith("~")
hauntsaninja marked this conversation as resolved.
Show resolved Hide resolved
):
hauntsaninja marked this conversation as resolved.
Show resolved Hide resolved
continue
subpath = os.path.join(package_path, name)
if self.options and any(
matches_ignore_pattern(subpath, pattern) for pattern in self.options.ignore_path
):
continue

if self.fscache.isdir(subpath):
# Only recurse into packages
Expand All @@ -467,6 +475,18 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
return sources


def matches_ignore_pattern(path: str, pattern: str) -> bool:
path = os.path.splitdrive(path)[1]
path_components = path.split(os.sep)
pattern_components = pattern.replace(os.sep, "/").split("/")
if len(path_components) < len(pattern_components):
return False
return all(
path == pattern
for path, pattern in zip(reversed(path_components), reversed(pattern_components))
)


def verify_module(fscache: FileSystemCache, id: str, path: str, prefix: str) -> bool:
"""Check that all packages containing id have a __init__ file."""
if path.endswith(('__init__.py', '__init__.pyi')):
Expand Down
2 changes: 2 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def __init__(self) -> None:
# sufficient to determine module names for files. As a possible alternative, add a single
# top-level __init__.py to your packages.
self.explicit_package_bases = False
# File names, directory names or subpaths to avoid checking
self.ignore_path = [] # type: List[str]

# disallow_any options
self.disallow_any_generics = False
Expand Down
74 changes: 74 additions & 0 deletions mypy/test/test_find_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,77 @@ def test_find_sources_namespace_multi_dir(self) -> None:

finder = SourceFinder(FakeFSCache({"/a/pkg/a.py", "/b/pkg/b.py"}), options)
assert find_sources(finder, "/") == [("pkg.a", "/a"), ("pkg.b", "/b")]

def test_find_sources_ignore_path(self) -> None:
options = Options()
options.namespace_packages = True

# special cased name
finder = SourceFinder(FakeFSCache({"/dir/a.py", "/dir/venv/site-packages/b.py"}), options)
assert find_sources(finder, "/") == [("a", "/dir")]

files = {
"/pkg/a1/b/c/d/e.py",
"/pkg/a1/b/f.py",
"/pkg/a2/__init__.py",
"/pkg/a2/b/c/d/e.py",
"/pkg/a2/b/f.py",
}

# file name
options.ignore_path = ["f.py"]
finder = SourceFinder(FakeFSCache(files), options)
assert find_sources(finder, "/") == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("e", "/pkg/a1/b/c/d"),
]

# directory name
options.ignore_path = ["a1"]
finder = SourceFinder(FakeFSCache(files), options)
assert find_sources(finder, "/") == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("a2.b.f", "/pkg"),
]

# paths
options.ignore_path = ["/pkg/a1"]
finder = SourceFinder(FakeFSCache(files), options)
assert find_sources(finder, "/") == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("a2.b.f", "/pkg"),
]

options.ignore_path = ["b/c"]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a test case I'm not sure about.
We could complicate the behaviour, so that paths with slashes in the beginning or middle are treated as fixed paths to ignore, as opposed to patterns. That is, if there's a slash at the beginning it's just an absolute path and we ignore it. If there's a slash in the middle, then we treat it as a fixed path relative to the current directory and ignore it.
As opposed to right now, where we just treat "b/c" as a suffix and use it to ignore "CWD/x/y/b/c" as well as "CWD/b/c"

finder = SourceFinder(FakeFSCache(files), options)
assert find_sources(finder, "/") == [
("a2", "/pkg"),
("a2.b.f", "/pkg"),
("f", "/pkg/a1/b"),
]

# nothing should be ignored as a result of this
options.ignore_path = [
"/pkg/a", "2", "1", "pk", "kg", "g.py", "bc", "/b", "/xxx/pkg/a2/b/f.py"
"xxx/pkg/a2/b/f.py"
]
finder = SourceFinder(FakeFSCache(files), options)
assert len(find_sources(finder, "/")) == len(files)

# nothing should be ignored as a result of this
files = {
"pkg/a1/b/c/d/e.py",
"pkg/a1/b/f.py",
"pkg/a2/__init__.py",
"pkg/a2/b/c/d/e.py",
"pkg/a2/b/f.py",
}
options.ignore_path = [
"/pkg/a", "2", "1", "pk", "kg", "g.py", "bc", "/b", "/xxx/pkg/a2/b/f.py",
"xxx/pkg/a2/b/f.py", "/pkg/a1", "/pkg/a2"
]
finder = SourceFinder(FakeFSCache(files), options)
assert len(find_sources(finder, "/")) == len(files)
hauntsaninja marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions mypy_self_check.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ pretty = True
always_false = MYPYC
plugins = misc/proper_plugin.py
python_version = 3.5
ignore_path = mypy/typeshed
16 changes: 2 additions & 14 deletions test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ undef
undef
[out]
dir/a.py: error: Duplicate module named 'a' (also at 'dir/subdir/a.py')
dir/a.py: error: Are you missing an __init__.py?
dir/a.py: note: Are you missing an __init__.py? Alternatively, consider using --ignore-path to avoid checking one of them.
== Return code: 2

[case testCmdlineNonPackageSlash]
Expand Down Expand Up @@ -125,19 +125,7 @@ mypy: can't decode file 'a.py': unknown encoding: uft-8
# type: ignore
[out]
two/mod/__init__.py: error: Duplicate module named 'mod' (also at 'one/mod/__init__.py')
== Return code: 2

[case promptsForgotInit]
# cmd: mypy a.py one/mod/a.py
[file one/__init__.py]
# type: ignore
[file a.py]
# type: ignore
[file one/mod/a.py]
#type: ignore
[out]
one/mod/a.py: error: Duplicate module named 'a' (also at 'a.py')
one/mod/a.py: error: Are you missing an __init__.py?
two/mod/__init__.py: note: Are you missing an __init__.py? Alternatively, consider using --ignore-path to avoid checking one of them.
== Return code: 2

[case testFlagsFile]
Expand Down