Skip to content

Add new "force exclude" option #12373

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ for full details, see :ref:`running-mypy`.
is, when mypy is discovering files within a directory tree or submodules of
a package to check. If you pass a file or module explicitly it will still be
checked. For instance, ``mypy --exclude '/setup.py$'
but_still_check/setup.py``.
but_still_check/setup.py``. To ignore files and modules passed explicitly,
use :option:`--force-exclude` instead.

In particular, ``--exclude`` does not affect mypy's :ref:`import following
<follow-imports>`. You can use a per-module :confval:`follow_imports` config
Expand All @@ -81,6 +82,14 @@ for full details, see :ref:`running-mypy`.
``.pyi``.


.. option:: --force-exclude

Behavior is identical to :option:`--exclude`, except ``--force-exclude``
also applies to files and modules passed to mypy explicitly. For instance,
``mypy --force-exclude '/setup.py$' some_dir/setup.py`` will not check
``some_dir/setup.py``.


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 @@ -255,6 +255,15 @@ section of the command line docs.

See :ref:`using-a-pyproject-toml`.

.. confval:: force_exclude

:type: regular expression

A regular expression that matches file names, directory names and paths
which mypy should ignore while recursively discovering files to check.
Behavior is identical to :confval:`exclude`, except ``force_exclude`` also
applies to files and modules passed to mypy explicitly.

.. confval:: namespace_packages

:type: boolean
Expand Down
2 changes: 1 addition & 1 deletion docs/source/running_mypy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ to modules to type check.

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

- 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
1 change: 1 addition & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,7 @@ def log_configuration(manager: BuildManager, sources: List[BuildSource]) -> None
("Cache Dir", manager.options.cache_dir),
("Compiled", str(not __file__.endswith(".py"))),
("Exclude", manager.options.exclude),
("Force Exclude", manager.options.force_exclude),
]

for conf_name, conf_value in configuration_vars:
Expand Down
15 changes: 9 additions & 6 deletions mypy/find_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,19 @@ def create_source_list(paths: Sequence[str], options: Options,
sources = []
for path in paths:
path = os.path.normpath(path)
if path.endswith(PY_EXTENSIONS):
# Can raise InvalidSourceList if a directory doesn't have a valid module name.
name, base_dir = finder.crawl_up(path)
sources.append(BuildSource(path, name, None, base_dir))
elif fscache.isdir(path):
if fscache.isdir(path):
sub_sources = finder.find_sources_in_dir(path)
if not sub_sources and not allow_empty_dir:
raise InvalidSourceList(
"There are no .py[i] files in directory '{}'".format(path)
)
sources.extend(sub_sources)
elif matches_exclude(path, options.force_exclude, fscache, options.verbosity >= 2):
continue
elif path.endswith(PY_EXTENSIONS):
# Can raise InvalidSourceList if a directory doesn't have a valid module name.
name, base_dir = finder.crawl_up(path)
sources.append(BuildSource(path, name, None, base_dir))
else:
mod = os.path.basename(path) if options.scripts_are_modules else None
sources.append(BuildSource(path, mod, None))
Expand Down Expand Up @@ -92,6 +94,7 @@ def __init__(self, fscache: FileSystemCache, options: Options) -> None:
self.explicit_package_bases = get_explicit_package_bases(options)
self.namespace_packages = options.namespace_packages
self.exclude = options.exclude
self.force_exclude = options.force_exclude
self.verbosity = options.verbosity

def is_explicit_package_base(self, path: str) -> bool:
Expand All @@ -110,7 +113,7 @@ def find_sources_in_dir(self, path: str) -> List[BuildSource]:
subpath = os.path.join(path, name)

if matches_exclude(
subpath, self.exclude, self.fscache, self.verbosity >= 2
subpath, self.exclude + self.force_exclude, self.fscache, self.verbosity >= 2
):
continue

Expand Down
15 changes: 14 additions & 1 deletion mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,20 @@ def add_invertible_flag(flag: str,
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$'. "
"May be specified more than once, eg. --exclude a --exclude b"
"May be specified more than once, e.g. --exclude a --exclude b."
)
)
code_group.add_argument(
"--force-exclude",
action="append",
metavar="PATTERN",
default=[],
help=(
"Regular expression to match file names, directory names or paths which mypy should "
"ignore while recursively discovering files to check, e.g. --force-exclude "
"'/setup\\.py$'. May be specified more than once, e.g. --force-exclude a "
"--force-exclude b. Behavior is identical to --exclude, except also applies to files "
"and modules passed to mypy explicitly."
)
)
code_group.add_argument(
Expand Down
5 changes: 4 additions & 1 deletion mypy/modulefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,10 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]:
subpath = os.path.join(package_path, name)

if self.options and matches_exclude(
subpath, self.options.exclude, self.fscache, self.options.verbosity >= 2
subpath,
self.options.exclude + self.options.force_exclude,
self.fscache,
self.options.verbosity >= 2
):
continue

Expand Down
2 changes: 2 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def __init__(self) -> None:
self.explicit_package_bases = False
# File names, directory names or subpaths to avoid checking
self.exclude: List[str] = []
# File names, directory names or subpaths to avoid checking even if explicitly specified
self.force_exclude: List[str] = []

# disallow_any options
self.disallow_any_generics = False
Expand Down
93 changes: 93 additions & 0 deletions mypy/test/test_find_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,3 +377,96 @@ def test_find_sources_exclude(self) -> None:
}
fscache = FakeFSCache(files)
assert len(find_sources(["."], options, fscache)) == len(files)

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

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.force_exclude = [r"/f\.py$"]
fscache = FakeFSCache(files)
assert find_sources(["/"], options, fscache) == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("e", "/pkg/a1/b/c/d"),
]
assert find_sources(["/pkg/a1/b/f.py"], options, fscache) == []
assert find_sources(["/pkg/a2/b/f.py"], options, fscache) == []

# directory name
options.force_exclude = ["/a1/"]
fscache = FakeFSCache(files)
assert find_sources(["/"], options, fscache) == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("a2.b.f", "/pkg"),
]
with pytest.raises(InvalidSourceList):
find_sources(["/pkg/a1"], options, fscache)
with pytest.raises(InvalidSourceList):
find_sources(["/pkg/a1/"], options, fscache)
with pytest.raises(InvalidSourceList):
find_sources(["/pkg/a1/b"], options, fscache)

options.force_exclude = ["/a1/$"]
assert find_sources(["/pkg/a1"], options, fscache) == [
('e', '/pkg/a1/b/c/d'), ('f', '/pkg/a1/b')
]

# paths
options.force_exclude = ["/pkg/a1/"]
fscache = FakeFSCache(files)
assert find_sources(["/"], options, fscache) == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("a2.b.f", "/pkg"),
]
with pytest.raises(InvalidSourceList):
find_sources(["/pkg/a1"], options, fscache)

# OR two patterns together
for orred in [["/(a1|a3)/"], ["a1", "a3"], ["a3", "a1"]]:
options.force_exclude = orred
fscache = FakeFSCache(files)
assert find_sources(["/"], options, fscache) == [
("a2", "/pkg"),
("a2.b.c.d.e", "/pkg"),
("a2.b.f", "/pkg"),
]

options.force_exclude = ["b/c/"]
fscache = FakeFSCache(files)
assert find_sources(["/"], options, fscache) == [
("a2", "/pkg"),
("a2.b.f", "/pkg"),
("f", "/pkg/a1/b"),
]

# nothing should be ignored as a result of this
big_exclude1 = [
"/pkg/a/", "/2", "/1", "/pk/", "/kg", "/g.py", "/bc", "/xxx/pkg/a2/b/f.py"
"xxx/pkg/a2/b/f.py",
]
big_exclude2 = ["|".join(big_exclude1)]
for big_exclude in [big_exclude1, big_exclude2]:
options.force_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)