Skip to content

Commit a1a22ca

Browse files
hauntsaninjaJukkaL
authored andcommitted
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 4c3ea82 commit a1a22ca

11 files changed

+210
-40
lines changed

docs/source/command_line.rst

+24
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

+12
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,18 @@ section of the command line docs.
192192

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

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

197209
:type: boolean

docs/source/running_mypy.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,8 @@ to modules to type check.
355355
- Mypy will check all paths provided that correspond to files.
356356

357357
- Mypy will recursively discover and check all files ending in ``.py`` or
358-
``.pyi`` in directory paths provided.
358+
``.pyi`` in directory paths provided, after accounting for
359+
:option:`--exclude <mypy --exclude>`.
359360

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

mypy/build.py

+7-9
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
@@ -2552,6 +2551,7 @@ def log_configuration(manager: BuildManager, sources: List[BuildSource]) -> None
25522551
("Current Executable", sys.executable),
25532552
("Cache Dir", manager.options.cache_dir),
25542553
("Compiled", str(not __file__.endswith(".py"))),
2554+
("Exclude", manager.options.exclude),
25552555
]
25562556

25572557
for conf_name, conf_value in configuration_vars:
@@ -2751,14 +2751,12 @@ def load_graph(sources: List[BuildSource], manager: BuildManager,
27512751
"Duplicate module named '%s' (also at '%s')" % (st.id, graph[st.id].xpath),
27522752
blocker=True,
27532753
)
2754-
p1 = len(pathlib.PurePath(st.xpath).parents)
2755-
p2 = len(pathlib.PurePath(graph[st.id].xpath).parents)
2756-
2757-
if p1 != p2:
2758-
manager.errors.report(
2759-
-1, -1,
2760-
"Are you missing an __init__.py?"
2761-
)
2754+
manager.errors.report(
2755+
-1, -1,
2756+
"Are you missing an __init__.py? Alternatively, consider using --exclude to "
2757+
"avoid checking one of them.",
2758+
severity='note'
2759+
)
27622760

27632761
manager.errors.raise_error()
27642762
graph[st.id] = st

mypy/find_sources.py

+9-2
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

+9
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,15 @@ def add_invertible_flag(flag: str,
791791
code_group.add_argument(
792792
'--explicit-package-bases', action='store_true',
793793
help="Use current directory and MYPYPATH to determine module names of files passed")
794+
code_group.add_argument(
795+
"--exclude",
796+
metavar="PATTERN",
797+
default="",
798+
help=(
799+
"Regular expression to match file names, directory names or paths which mypy should "
800+
"ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'"
801+
)
802+
)
794803
code_group.add_argument(
795804
'-m', '--module', action='append', metavar='MODULE',
796805
default=[],

mypy/modulefinder.py

+22-3
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
@@ -380,10 +381,15 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
380381
names = sorted(self.fscache.listdir(package_path))
381382
for name in names:
382383
# Skip certain names altogether
383-
if name == '__pycache__' or name.startswith('.') or name.endswith('~'):
384+
if name in ("__pycache__", "site-packages", "node_modules") or name.startswith("."):
384385
continue
385386
subpath = os.path.join(package_path, name)
386387

388+
if self.options and matches_exclude(
389+
subpath, self.options.exclude, self.fscache, self.options.verbosity >= 2
390+
):
391+
continue
392+
387393
if self.fscache.isdir(subpath):
388394
# Only recurse into packages
389395
if (self.options and self.options.namespace_packages) or (
@@ -397,13 +403,26 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
397403
if stem == '__init__':
398404
continue
399405
if stem not in seen and '.' not in stem and suffix in PYTHON_EXTENSIONS:
400-
# (If we sorted names) we could probably just make the BuildSource ourselves,
401-
# but this ensures compatibility with find_module / the cache
406+
# (If we sorted names by keyfunc) we could probably just make the BuildSource
407+
# ourselves, but this ensures compatibility with find_module / the cache
402408
seen.add(stem)
403409
sources.extend(self.find_modules_recursive(module + '.' + stem))
404410
return sources
405411

406412

413+
def matches_exclude(subpath: str, exclude: str, fscache: FileSystemCache, verbose: bool) -> bool:
414+
if not exclude:
415+
return False
416+
subpath_str = os.path.abspath(subpath).replace(os.sep, "/")
417+
if fscache.isdir(subpath):
418+
subpath_str += "/"
419+
if re.search(exclude, subpath_str):
420+
if verbose:
421+
print("TRACE: Excluding {}".format(subpath_str), file=sys.stderr)
422+
return True
423+
return False
424+
425+
407426
def verify_module(fscache: FileSystemCache, id: str, path: str, prefix: str) -> bool:
408427
"""Check that all packages containing id have a __init__ file."""
409428
if path.endswith(('__init__.py', '__init__.pyi')):

mypy/options.py

+2
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)