Skip to content

Commit 01d6eb8

Browse files
authored
Support --exclude more than once on command line (#11329)
The result is an "OR" of all the patterns provided. Should be fully backward compatible to existing folks. Fixes #10310
1 parent 9a2838d commit 01d6eb8

File tree

8 files changed

+113
-39
lines changed

8 files changed

+113
-39
lines changed

docs/source/command_line.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ for full details, see :ref:`running-mypy`.
5959
pass ``--exclude '/setup\.py$'``. Similarly, you can ignore discovering
6060
directories with a given name by e.g. ``--exclude /build/`` or
6161
those matching a subpath with ``--exclude /project/vendor/``. To ignore
62-
multiple files / directories / paths, you can combine expressions with
63-
``|``, e.g ``--exclude '/setup\.py$|/build/'``.
62+
multiple files / directories / paths, you can provide the --exclude
63+
flag more than once, e.g ``--exclude '/setup\.py$' --exclude '/build/'``.
6464

6565
Note that this flag only affects recursive directory tree discovery, that
6666
is, when mypy is discovering files within a directory tree or submodules of

docs/source/config_file.rst

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

198198
.. confval:: exclude
199199

200-
:type: regular expression
200+
:type: newline separated list of regular expressions
201201

202-
A regular expression that matches file names, directory names and paths
202+
A newline list of regular expression that matches file names, directory names and paths
203203
which mypy should ignore while recursively discovering files to check.
204204
Use forward slashes on all platforms.
205205

206+
.. code-block:: ini
207+
208+
[mypy]
209+
exclude =
210+
^file1\.py$
211+
^file2\.py$
212+
206213
For more details, see :option:`--exclude <mypy --exclude>`.
207214

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

mypy/config_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def check_follow_imports(choice: str) -> str:
126126
'cache_dir': expand_path,
127127
'python_executable': expand_path,
128128
'strict': bool,
129+
'exclude': lambda s: [p.strip() for p in s.split('\n') if p.strip()],
129130
}
130131

131132
# Reuse the ini_config_types and overwrite the diff

mypy/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,11 +864,13 @@ def add_invertible_flag(flag: str,
864864
group=code_group)
865865
code_group.add_argument(
866866
"--exclude",
867+
action="append",
867868
metavar="PATTERN",
868-
default="",
869+
default=[],
869870
help=(
870871
"Regular expression to match file names, directory names or paths which mypy should "
871-
"ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'"
872+
"ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'. "
873+
"May be specified more than once, eg. --exclude a --exclude b"
872874
)
873875
)
874876
code_group.add_argument(

mypy/modulefinder.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -500,16 +500,21 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
500500
return sources
501501

502502

503-
def matches_exclude(subpath: str, exclude: str, fscache: FileSystemCache, verbose: bool) -> bool:
504-
if not exclude:
503+
def matches_exclude(subpath: str,
504+
excludes: List[str],
505+
fscache: FileSystemCache,
506+
verbose: bool) -> bool:
507+
if not excludes:
505508
return False
506509
subpath_str = os.path.relpath(subpath).replace(os.sep, "/")
507510
if fscache.isdir(subpath):
508511
subpath_str += "/"
509-
if re.search(exclude, subpath_str):
510-
if verbose:
511-
print("TRACE: Excluding {}".format(subpath_str), file=sys.stderr)
512-
return True
512+
for exclude in excludes:
513+
if re.search(exclude, subpath_str):
514+
if verbose:
515+
print("TRACE: Excluding {} (matches pattern {})".format(subpath_str, exclude),
516+
file=sys.stderr)
517+
return True
513518
return False
514519

515520

mypy/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def __init__(self) -> None:
100100
# top-level __init__.py to your packages.
101101
self.explicit_package_bases = False
102102
# File names, directory names or subpaths to avoid checking
103-
self.exclude: str = ""
103+
self.exclude: List[str] = []
104104

105105
# disallow_any options
106106
self.disallow_any_generics = False

mypy/test/test_find_sources.py

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def test_find_sources_exclude(self) -> None:
298298
}
299299

300300
# file name
301-
options.exclude = r"/f\.py$"
301+
options.exclude = [r"/f\.py$"]
302302
fscache = FakeFSCache(files)
303303
assert find_sources(["/"], options, fscache) == [
304304
("a2", "/pkg"),
@@ -309,7 +309,7 @@ def test_find_sources_exclude(self) -> None:
309309
assert find_sources(["/pkg/a2/b/f.py"], options, fscache) == [('a2.b.f', '/pkg')]
310310

311311
# directory name
312-
options.exclude = "/a1/"
312+
options.exclude = ["/a1/"]
313313
fscache = FakeFSCache(files)
314314
assert find_sources(["/"], options, fscache) == [
315315
("a2", "/pkg"),
@@ -323,13 +323,13 @@ def test_find_sources_exclude(self) -> None:
323323
with pytest.raises(InvalidSourceList):
324324
find_sources(["/pkg/a1/b"], options, fscache)
325325

326-
options.exclude = "/a1/$"
326+
options.exclude = ["/a1/$"]
327327
assert find_sources(["/pkg/a1"], options, fscache) == [
328328
('e', '/pkg/a1/b/c/d'), ('f', '/pkg/a1/b')
329329
]
330330

331331
# paths
332-
options.exclude = "/pkg/a1/"
332+
options.exclude = ["/pkg/a1/"]
333333
fscache = FakeFSCache(files)
334334
assert find_sources(["/"], options, fscache) == [
335335
("a2", "/pkg"),
@@ -339,15 +339,17 @@ def test_find_sources_exclude(self) -> None:
339339
with pytest.raises(InvalidSourceList):
340340
find_sources(["/pkg/a1"], options, fscache)
341341

342-
options.exclude = "/(a1|a3)/"
343-
fscache = FakeFSCache(files)
344-
assert find_sources(["/"], options, fscache) == [
345-
("a2", "/pkg"),
346-
("a2.b.c.d.e", "/pkg"),
347-
("a2.b.f", "/pkg"),
348-
]
342+
# OR two patterns together
343+
for orred in [["/(a1|a3)/"], ["a1", "a3"], ["a3", "a1"]]:
344+
options.exclude = orred
345+
fscache = FakeFSCache(files)
346+
assert find_sources(["/"], options, fscache) == [
347+
("a2", "/pkg"),
348+
("a2.b.c.d.e", "/pkg"),
349+
("a2.b.f", "/pkg"),
350+
]
349351

350-
options.exclude = "b/c/"
352+
options.exclude = ["b/c/"]
351353
fscache = FakeFSCache(files)
352354
assert find_sources(["/"], options, fscache) == [
353355
("a2", "/pkg"),
@@ -356,19 +358,22 @@ def test_find_sources_exclude(self) -> None:
356358
]
357359

358360
# nothing should be ignored as a result of this
359-
options.exclude = "|".join((
361+
big_exclude1 = [
360362
"/pkg/a/", "/2", "/1", "/pk/", "/kg", "/g.py", "/bc", "/xxx/pkg/a2/b/f.py"
361363
"xxx/pkg/a2/b/f.py",
362-
))
363-
fscache = FakeFSCache(files)
364-
assert len(find_sources(["/"], options, fscache)) == len(files)
365-
366-
files = {
367-
"pkg/a1/b/c/d/e.py",
368-
"pkg/a1/b/f.py",
369-
"pkg/a2/__init__.py",
370-
"pkg/a2/b/c/d/e.py",
371-
"pkg/a2/b/f.py",
372-
}
373-
fscache = FakeFSCache(files)
374-
assert len(find_sources(["."], options, fscache)) == len(files)
364+
]
365+
big_exclude2 = ["|".join(big_exclude1)]
366+
for big_exclude in [big_exclude1, big_exclude2]:
367+
options.exclude = big_exclude
368+
fscache = FakeFSCache(files)
369+
assert len(find_sources(["/"], options, fscache)) == len(files)
370+
371+
files = {
372+
"pkg/a1/b/c/d/e.py",
373+
"pkg/a1/b/f.py",
374+
"pkg/a2/__init__.py",
375+
"pkg/a2/b/c/d/e.py",
376+
"pkg/a2/b/f.py",
377+
}
378+
fscache = FakeFSCache(files)
379+
assert len(find_sources(["."], options, fscache)) == len(files)

test-data/unit/cmdline.test

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,3 +1309,57 @@ pkg.py:1: error: "int" not callable
13091309
1()
13101310
[out]
13111311
pkg.py:1: error: "int" not callable
1312+
1313+
[case testCmdlineExclude]
1314+
# cmd: mypy --exclude abc .
1315+
[file abc/apkg.py]
1316+
1()
1317+
[file b/bpkg.py]
1318+
1()
1319+
[file c/cpkg.py]
1320+
1()
1321+
[out]
1322+
c/cpkg.py:1: error: "int" not callable
1323+
b/bpkg.py:1: error: "int" not callable
1324+
1325+
[case testCmdlineMultipleExclude]
1326+
# cmd: mypy --exclude abc --exclude b/ .
1327+
[file abc/apkg.py]
1328+
1()
1329+
[file b/bpkg.py]
1330+
1()
1331+
[file c/cpkg.py]
1332+
1()
1333+
[out]
1334+
c/cpkg.py:1: error: "int" not callable
1335+
1336+
[case testCmdlineCfgExclude]
1337+
# cmd: mypy .
1338+
[file mypy.ini]
1339+
\[mypy]
1340+
exclude = abc
1341+
[file abc/apkg.py]
1342+
1()
1343+
[file b/bpkg.py]
1344+
1()
1345+
[file c/cpkg.py]
1346+
1()
1347+
[out]
1348+
c/cpkg.py:1: error: "int" not callable
1349+
b/bpkg.py:1: error: "int" not callable
1350+
1351+
[case testCmdlineCfgMultipleExclude]
1352+
# cmd: mypy .
1353+
[file mypy.ini]
1354+
\[mypy]
1355+
exclude =
1356+
abc
1357+
b
1358+
[file abc/apkg.py]
1359+
1()
1360+
[file b/bpkg.py]
1361+
1()
1362+
[file c/cpkg.py]
1363+
1()
1364+
[out]
1365+
c/cpkg.py:1: error: "int" not callable

0 commit comments

Comments
 (0)