From 4dab5a72648b955edc5eb152c7effe2182048b30 Mon Sep 17 00:00:00 2001 From: Nipunn Koorapati Date: Wed, 13 Oct 2021 14:52:53 -0700 Subject: [PATCH] Support --exclude more than once on command line The result is an "OR" of all the patterns provided. Should be fully backward compatible to existing folks. Fixes #10310 --- docs/source/command_line.rst | 4 +-- docs/source/config_file.rst | 11 +++++-- mypy/config_parser.py | 1 + mypy/main.py | 6 ++-- mypy/modulefinder.py | 17 ++++++---- mypy/options.py | 2 +- mypy/test/test_find_sources.py | 57 ++++++++++++++++++---------------- test-data/unit/cmdline.test | 54 ++++++++++++++++++++++++++++++++ 8 files changed, 113 insertions(+), 39 deletions(-) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 4b54e6a4dab0..fdf44566c398 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -59,8 +59,8 @@ for full details, see :ref:`running-mypy`. pass ``--exclude '/setup\.py$'``. Similarly, you can ignore discovering directories with a given name by e.g. ``--exclude /build/`` or those matching a subpath with ``--exclude /project/vendor/``. To ignore - multiple files / directories / paths, you can combine expressions with - ``|``, e.g ``--exclude '/setup\.py$|/build/'``. + multiple files / directories / paths, you can provide the --exclude + flag more than once, e.g ``--exclude '/setup\.py$' --exclude '/build/'``. Note that this flag only affects recursive directory tree discovery, that is, when mypy is discovering files within a directory tree or submodules of diff --git a/docs/source/config_file.rst b/docs/source/config_file.rst index 1241c40e5ac8..c34f23d9e169 100644 --- a/docs/source/config_file.rst +++ b/docs/source/config_file.rst @@ -197,12 +197,19 @@ section of the command line docs. .. confval:: exclude - :type: regular expression + :type: newline separated list of regular expressions - A regular expression that matches file names, directory names and paths + A newline list of regular expression that matches file names, directory names and paths which mypy should ignore while recursively discovering files to check. Use forward slashes on all platforms. + .. code-block:: ini + + [mypy] + exclude = + ^file1\.py$ + ^file2\.py$ + For more details, see :option:`--exclude `. This option may only be set in the global section (``[mypy]``). diff --git a/mypy/config_parser.py b/mypy/config_parser.py index d5552186ecc7..f9d9d408f067 100644 --- a/mypy/config_parser.py +++ b/mypy/config_parser.py @@ -124,6 +124,7 @@ def check_follow_imports(choice: str) -> str: 'cache_dir': expand_path, 'python_executable': expand_path, 'strict': bool, + 'exclude': lambda s: [p.strip() for p in s.split('\n') if p.strip()], } # Reuse the ini_config_types and overwrite the diff diff --git a/mypy/main.py b/mypy/main.py index 9ecd345126f4..e478d65a6fca 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -864,11 +864,13 @@ def add_invertible_flag(flag: str, group=code_group) code_group.add_argument( "--exclude", + action="append", metavar="PATTERN", - default="", + default=[], help=( "Regular expression to match file names, directory names or paths which mypy should " - "ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'" + "ignore while recursively discovering files to check, e.g. --exclude '/setup\\.py$'. " + "May be specified more than once, eg. --exclude a --exclude b" ) ) code_group.add_argument( diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index 17a700d0ce9f..4469533835e3 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -500,16 +500,21 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]: return sources -def matches_exclude(subpath: str, exclude: str, fscache: FileSystemCache, verbose: bool) -> bool: - if not exclude: +def matches_exclude(subpath: str, + excludes: List[str], + fscache: FileSystemCache, + verbose: bool) -> bool: + if not excludes: return False subpath_str = os.path.relpath(subpath).replace(os.sep, "/") if fscache.isdir(subpath): subpath_str += "/" - if re.search(exclude, subpath_str): - if verbose: - print("TRACE: Excluding {}".format(subpath_str), file=sys.stderr) - return True + for exclude in excludes: + if re.search(exclude, subpath_str): + if verbose: + print("TRACE: Excluding {} (matches pattern {})".format(subpath_str, exclude), + file=sys.stderr) + return True return False diff --git a/mypy/options.py b/mypy/options.py index 3a56add0d0ad..738e1ac71ffa 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -100,7 +100,7 @@ def __init__(self) -> None: # top-level __init__.py to your packages. self.explicit_package_bases = False # File names, directory names or subpaths to avoid checking - self.exclude: str = "" + self.exclude: List[str] = [] # disallow_any options self.disallow_any_generics = False diff --git a/mypy/test/test_find_sources.py b/mypy/test/test_find_sources.py index 7824c82208f9..53da9c384bd2 100644 --- a/mypy/test/test_find_sources.py +++ b/mypy/test/test_find_sources.py @@ -298,7 +298,7 @@ def test_find_sources_exclude(self) -> None: } # file name - options.exclude = r"/f\.py$" + options.exclude = [r"/f\.py$"] fscache = FakeFSCache(files) assert find_sources(["/"], options, fscache) == [ ("a2", "/pkg"), @@ -309,7 +309,7 @@ def test_find_sources_exclude(self) -> None: assert find_sources(["/pkg/a2/b/f.py"], options, fscache) == [('a2.b.f', '/pkg')] # directory name - options.exclude = "/a1/" + options.exclude = ["/a1/"] fscache = FakeFSCache(files) assert find_sources(["/"], options, fscache) == [ ("a2", "/pkg"), @@ -323,13 +323,13 @@ def test_find_sources_exclude(self) -> None: with pytest.raises(InvalidSourceList): find_sources(["/pkg/a1/b"], options, fscache) - options.exclude = "/a1/$" + options.exclude = ["/a1/$"] assert find_sources(["/pkg/a1"], options, fscache) == [ ('e', '/pkg/a1/b/c/d'), ('f', '/pkg/a1/b') ] # paths - options.exclude = "/pkg/a1/" + options.exclude = ["/pkg/a1/"] fscache = FakeFSCache(files) assert find_sources(["/"], options, fscache) == [ ("a2", "/pkg"), @@ -339,15 +339,17 @@ def test_find_sources_exclude(self) -> None: with pytest.raises(InvalidSourceList): find_sources(["/pkg/a1"], options, fscache) - options.exclude = "/(a1|a3)/" - fscache = FakeFSCache(files) - assert find_sources(["/"], options, fscache) == [ - ("a2", "/pkg"), - ("a2.b.c.d.e", "/pkg"), - ("a2.b.f", "/pkg"), - ] + # OR two patterns together + for orred in [["/(a1|a3)/"], ["a1", "a3"], ["a3", "a1"]]: + options.exclude = orred + fscache = FakeFSCache(files) + assert find_sources(["/"], options, fscache) == [ + ("a2", "/pkg"), + ("a2.b.c.d.e", "/pkg"), + ("a2.b.f", "/pkg"), + ] - options.exclude = "b/c/" + options.exclude = ["b/c/"] fscache = FakeFSCache(files) assert find_sources(["/"], options, fscache) == [ ("a2", "/pkg"), @@ -356,19 +358,22 @@ def test_find_sources_exclude(self) -> None: ] # nothing should be ignored as a result of this - options.exclude = "|".join(( + big_exclude1 = [ "/pkg/a/", "/2", "/1", "/pk/", "/kg", "/g.py", "/bc", "/xxx/pkg/a2/b/f.py" "xxx/pkg/a2/b/f.py", - )) - fscache = FakeFSCache(files) - assert len(find_sources(["/"], options, fscache)) == len(files) - - 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", - } - fscache = FakeFSCache(files) - assert len(find_sources(["."], options, fscache)) == len(files) + ] + big_exclude2 = ["|".join(big_exclude1)] + for big_exclude in [big_exclude1, big_exclude2]: + options.exclude = big_exclude + fscache = FakeFSCache(files) + assert len(find_sources(["/"], options, fscache)) == len(files) + + 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", + } + fscache = FakeFSCache(files) + assert len(find_sources(["."], options, fscache)) == len(files) diff --git a/test-data/unit/cmdline.test b/test-data/unit/cmdline.test index 47cc10007004..93eb893d3c00 100644 --- a/test-data/unit/cmdline.test +++ b/test-data/unit/cmdline.test @@ -1309,3 +1309,57 @@ pkg.py:1: error: "int" not callable 1() [out] pkg.py:1: error: "int" not callable + +[case testCmdlineExclude] +# cmd: mypy --exclude abc . +[file abc/apkg.py] +1() +[file b/bpkg.py] +1() +[file c/cpkg.py] +1() +[out] +c/cpkg.py:1: error: "int" not callable +b/bpkg.py:1: error: "int" not callable + +[case testCmdlineMultipleExclude] +# cmd: mypy --exclude abc --exclude b/ . +[file abc/apkg.py] +1() +[file b/bpkg.py] +1() +[file c/cpkg.py] +1() +[out] +c/cpkg.py:1: error: "int" not callable + +[case testCmdlineCfgExclude] +# cmd: mypy . +[file mypy.ini] +\[mypy] +exclude = abc +[file abc/apkg.py] +1() +[file b/bpkg.py] +1() +[file c/cpkg.py] +1() +[out] +c/cpkg.py:1: error: "int" not callable +b/bpkg.py:1: error: "int" not callable + +[case testCmdlineCfgMultipleExclude] +# cmd: mypy . +[file mypy.ini] +\[mypy] +exclude = + abc + b +[file abc/apkg.py] +1() +[file b/bpkg.py] +1() +[file c/cpkg.py] +1() +[out] +c/cpkg.py:1: error: "int" not callable