Skip to content

Commit 6bfc2db

Browse files
authored
Add --exclude (#9992)
Resolves #4675, resolves #9981. Additionally, we always ignore site-packages and node_modules, and directories starting with a dot. Also note that this doesn't really affect import discovery; it only directly affects passing files or packages to mypy. The additional check before suggesting "are you missing an __init__.py" didn't make any sense to me, so I removed it, appended to the message and downgraded the severity to note. Co-authored-by: hauntsaninja <>
1 parent 11d4fb2 commit 6bfc2db

File tree

11 files changed

+210
-40
lines changed

11 files changed

+210
-40
lines changed

docs/source/command_line.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,30 @@ for full details, see :ref:`running-mypy`.
4949
Asks mypy to type check the provided string as a program.
5050

5151

52+
.. option:: --exclude
53+
54+
A regular expression that matches file names, directory names and paths
55+
which mypy should ignore while recursively discovering files to check.
56+
Use forward slashes on all platforms.
57+
58+
For instance, to avoid discovering any files named `setup.py` you could
59+
pass ``--exclude '/setup\.py$'``. Similarly, you can ignore discovering
60+
directories with a given name by e.g. ``--exclude /build/`` or
61+
those matching a subpath with ``--exclude /project/vendor/``.
62+
63+
Note that this flag only affects recursive discovery, that is, when mypy is
64+
discovering files within a directory tree or submodules of a package to
65+
check. If you pass a file or module explicitly it will still be checked. For
66+
instance, ``mypy --exclude '/setup.py$' but_still_check/setup.py``.
67+
68+
Note that mypy will never recursively discover files and directories named
69+
"site-packages", "node_modules" or "__pycache__", or those whose name starts
70+
with a period, exactly as ``--exclude
71+
'/(site-packages|node_modules|__pycache__|\..*)/$'`` would. Mypy will also
72+
never recursively discover files with extensions other than ``.py`` or
73+
``.pyi``.
74+
75+
5276
Optional arguments
5377
******************
5478

docs/source/config_file.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ section of the command line docs.
197197

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

200+
.. confval:: exclude
201+
202+
:type: regular expression
203+
204+
A regular expression that matches file names, directory names and paths
205+
which mypy should ignore while recursively discovering files to check.
206+
Use forward slashes on all platforms.
207+
208+
For more details, see :option:`--exclude <mypy --exclude>`.
209+
210+
This option may only be set in the global section (``[mypy]``).
211+
200212
.. confval:: namespace_packages
201213

202214
:type: boolean

docs/source/running_mypy.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,8 @@ to modules to type check.
390390
- Mypy will check all paths provided that correspond to files.
391391

392392
- Mypy will recursively discover and check all files ending in ``.py`` or
393-
``.pyi`` in directory paths provided.
393+
``.pyi`` in directory paths provided, after accounting for
394+
:option:`--exclude <mypy --exclude>`.
394395

395396
- For each file to be checked, mypy will attempt to associate the file (e.g.
396397
``project/foo/bar/baz.py``) with a fully qualified module name (e.g.

mypy/build.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import gc
1616
import json
1717
import os
18-
import pathlib
1918
import re
2019
import stat
2120
import sys
@@ -2572,6 +2571,7 @@ def log_configuration(manager: BuildManager, sources: List[BuildSource]) -> None
25722571
("Current Executable", sys.executable),
25732572
("Cache Dir", manager.options.cache_dir),
25742573
("Compiled", str(not __file__.endswith(".py"))),
2574+
("Exclude", manager.options.exclude),
25752575
]
25762576

25772577
for conf_name, conf_value in configuration_vars:
@@ -2771,14 +2771,12 @@ def load_graph(sources: List[BuildSource], manager: BuildManager,
27712771
"Duplicate module named '%s' (also at '%s')" % (st.id, graph[st.id].xpath),
27722772
blocker=True,
27732773
)
2774-
p1 = len(pathlib.PurePath(st.xpath).parents)
2775-
p2 = len(pathlib.PurePath(graph[st.id].xpath).parents)
2776-
2777-
if p1 != p2:
2778-
manager.errors.report(
2779-
-1, -1,
2780-
"Are you missing an __init__.py?"
2781-
)
2774+
manager.errors.report(
2775+
-1, -1,
2776+
"Are you missing an __init__.py? Alternatively, consider using --exclude to "
2777+
"avoid checking one of them.",
2778+
severity='note'
2779+
)
27822780

27832781
manager.errors.raise_error()
27842782
graph[st.id] = st

mypy/find_sources.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import List, Sequence, Set, Tuple, Optional
77
from typing_extensions import Final
88

9-
from mypy.modulefinder import BuildSource, PYTHON_EXTENSIONS, mypy_path
9+
from mypy.modulefinder import BuildSource, PYTHON_EXTENSIONS, mypy_path, matches_exclude
1010
from mypy.fscache import FileSystemCache
1111
from mypy.options import Options
1212

@@ -91,6 +91,8 @@ def __init__(self, fscache: FileSystemCache, options: Options) -> None:
9191
self.fscache = fscache
9292
self.explicit_package_bases = get_explicit_package_bases(options)
9393
self.namespace_packages = options.namespace_packages
94+
self.exclude = options.exclude
95+
self.verbosity = options.verbosity
9496

9597
def is_explicit_package_base(self, path: str) -> bool:
9698
assert self.explicit_package_bases
@@ -103,10 +105,15 @@ def find_sources_in_dir(self, path: str) -> List[BuildSource]:
103105
names = sorted(self.fscache.listdir(path), key=keyfunc)
104106
for name in names:
105107
# Skip certain names altogether
106-
if name == '__pycache__' or name.startswith('.') or name.endswith('~'):
108+
if name in ("__pycache__", "site-packages", "node_modules") or name.startswith("."):
107109
continue
108110
subpath = os.path.join(path, name)
109111

112+
if matches_exclude(
113+
subpath, self.exclude, self.fscache, self.verbosity >= 2
114+
):
115+
continue
116+
110117
if self.fscache.isdir(subpath):
111118
sub_sources = self.find_sources_in_dir(subpath)
112119
if sub_sources:

mypy/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,15 @@ def add_invertible_flag(flag: str,
808808
code_group.add_argument(
809809
'--explicit-package-bases', action='store_true',
810810
help="Use current directory and MYPYPATH to determine module names of files passed")
811+
code_group.add_argument(
812+
"--exclude",
813+
metavar="PATTERN",
814+
default="",
815+
help=(
816+
"Regular expression to match file names, directory names or paths which mypy should "
817+
"ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'"
818+
)
819+
)
811820
code_group.add_argument(
812821
'-m', '--module', action='append', metavar='MODULE',
813822
default=[],

mypy/modulefinder.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import collections
88
import functools
99
import os
10+
import re
1011
import subprocess
1112
import sys
1213
from enum import Enum
@@ -443,10 +444,15 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
443444
names = sorted(self.fscache.listdir(package_path))
444445
for name in names:
445446
# Skip certain names altogether
446-
if name == '__pycache__' or name.startswith('.') or name.endswith('~'):
447+
if name in ("__pycache__", "site-packages", "node_modules") or name.startswith("."):
447448
continue
448449
subpath = os.path.join(package_path, name)
449450

451+
if self.options and matches_exclude(
452+
subpath, self.options.exclude, self.fscache, self.options.verbosity >= 2
453+
):
454+
continue
455+
450456
if self.fscache.isdir(subpath):
451457
# Only recurse into packages
452458
if (self.options and self.options.namespace_packages) or (
@@ -460,13 +466,26 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
460466
if stem == '__init__':
461467
continue
462468
if stem not in seen and '.' not in stem and suffix in PYTHON_EXTENSIONS:
463-
# (If we sorted names) we could probably just make the BuildSource ourselves,
464-
# but this ensures compatibility with find_module / the cache
469+
# (If we sorted names by keyfunc) we could probably just make the BuildSource
470+
# ourselves, but this ensures compatibility with find_module / the cache
465471
seen.add(stem)
466472
sources.extend(self.find_modules_recursive(module + '.' + stem))
467473
return sources
468474

469475

476+
def matches_exclude(subpath: str, exclude: str, fscache: FileSystemCache, verbose: bool) -> bool:
477+
if not exclude:
478+
return False
479+
subpath_str = os.path.abspath(subpath).replace(os.sep, "/")
480+
if fscache.isdir(subpath):
481+
subpath_str += "/"
482+
if re.search(exclude, subpath_str):
483+
if verbose:
484+
print("TRACE: Excluding {}".format(subpath_str), file=sys.stderr)
485+
return True
486+
return False
487+
488+
470489
def verify_module(fscache: FileSystemCache, id: str, path: str, prefix: str) -> bool:
471490
"""Check that all packages containing id have a __init__ file."""
472491
if path.endswith(('__init__.py', '__init__.pyi')):

mypy/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ def __init__(self) -> None:
9797
# sufficient to determine module names for files. As a possible alternative, add a single
9898
# top-level __init__.py to your packages.
9999
self.explicit_package_bases = False
100+
# File names, directory names or subpaths to avoid checking
101+
self.exclude = "" # type: str
100102

101103
# disallow_any options
102104
self.disallow_any_generics = False

0 commit comments

Comments
 (0)