Skip to content
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

add --force-exclude argument #1032

Merged
merged 15 commits into from May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
164 changes: 115 additions & 49 deletions black.py
Expand Up @@ -34,6 +34,7 @@
Pattern,
Sequence,
Set,
Sized,
Tuple,
Type,
TypeVar,
Expand Down Expand Up @@ -424,6 +425,14 @@ def target_version_option_callback(
),
show_default=True,
)
@click.option(
"--force-exclude",
type=str,
help=(
"Like --exclude, but files and directories matching this regex will be "
"excluded even when they are passed explicitly as arguments"
),
)
@click.option(
"-q",
"--quiet",
Expand Down Expand Up @@ -482,6 +491,7 @@ def main(
verbose: bool,
include: str,
exclude: str,
force_exclude: Optional[str],
src: Tuple[str, ...],
config: Optional[str],
) -> None:
Expand Down Expand Up @@ -513,6 +523,57 @@ def main(
if code is not None:
print(format_str(code, mode=mode))
ctx.exit(0)
report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
sources = get_sources(
ctx=ctx,
src=src,
quiet=quiet,
verbose=verbose,
include=include,
exclude=exclude,
force_exclude=force_exclude,
report=report,
)

path_empty(
sources,
"No Python files are present to be formatted. Nothing to do 😴",
quiet,
verbose,
ctx,
)

if len(sources) == 1:
reformat_one(
src=sources.pop(),
fast=fast,
write_back=write_back,
mode=mode,
report=report,
)
else:
reformat_many(
sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
)

if verbose or not quiet:
out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
click.secho(str(report), err=True)
ctx.exit(report.return_code)


def get_sources(
*,
ctx: click.Context,
src: Tuple[str, ...],
quiet: bool,
verbose: bool,
include: str,
exclude: str,
force_exclude: Optional[str],
report: "Report",
) -> Set[Path]:
"""Compute the set of files to be formatted."""
try:
include_regex = re_compile_maybe_verbose(include)
except re.error:
Expand All @@ -523,56 +584,56 @@ def main(
except re.error:
err(f"Invalid regular expression for exclude given: {exclude!r}")
ctx.exit(2)
report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
try:
force_exclude_regex = (
re_compile_maybe_verbose(force_exclude) if force_exclude else None
)
except re.error:
err(f"Invalid regular expression for force_exclude given: {force_exclude!r}")
ctx.exit(2)

root = find_project_root(src)
sources: Set[Path] = set()
path_empty(src, quiet, verbose, ctx)
path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
exclude_regexes = [exclude_regex]
if force_exclude_regex is not None:
exclude_regexes.append(force_exclude_regex)

for s in src:
p = Path(s)
if p.is_dir():
sources.update(
gen_python_files_in_dir(
p, root, include_regex, exclude_regex, report, get_gitignore(root)
gen_python_files(
p.iterdir(),
root,
include_regex,
exclude_regexes,
report,
get_gitignore(root),
)
)
elif p.is_file() or s == "-":
# if a file was explicitly given, we don't care about its extension
elif s == "-":
sources.add(p)
elif p.is_file():
sources.update(
gen_python_files(
[p], root, None, exclude_regexes, report, get_gitignore(root)
)
)
else:
err(f"invalid path: {s}")
if len(sources) == 0:
if verbose or not quiet:
out("No Python files are present to be formatted. Nothing to do 😴")
ctx.exit(0)

if len(sources) == 1:
reformat_one(
src=sources.pop(),
fast=fast,
write_back=write_back,
mode=mode,
report=report,
)
else:
reformat_many(
sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
)

if verbose or not quiet:
out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
click.secho(str(report), err=True)
ctx.exit(report.return_code)
return sources


def path_empty(
src: Tuple[str, ...], quiet: bool, verbose: bool, ctx: click.Context
src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context
) -> None:
"""
Exit if there is no `src` provided for formatting
"""
if not src:
if len(src) == 0:
if verbose or not quiet:
out("No Path provided. Nothing to do 😴")
out(msg)
ctx.exit(0)


Expand Down Expand Up @@ -5708,11 +5769,11 @@ def get_gitignore(root: Path) -> PathSpec:
return PathSpec.from_lines("gitwildmatch", lines)


def gen_python_files_in_dir(
path: Path,
def gen_python_files(
paths: Iterable[Path],
root: Path,
include: Pattern[str],
exclude: Pattern[str],
include: Optional[Pattern[str]],
exclude_regexes: Iterable[Pattern[str]],
report: "Report",
gitignore: PathSpec,
) -> Iterator[Path]:
Expand All @@ -5724,19 +5785,13 @@ def gen_python_files_in_dir(
`report` is where output about exclusions goes.
"""
assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
for child in path.iterdir():
# First ignore files matching .gitignore
if gitignore.match_file(child.as_posix()):
report.path_ignored(child, "matches the .gitignore file content")
continue

for child in paths:
# Then ignore with `exclude` option.
try:
normalized_path = "/" + child.resolve().relative_to(root).as_posix()
normalized_path = child.resolve().relative_to(root).as_posix()
except OSError as e:
report.path_ignored(child, f"cannot be read because {e}")
continue

except ValueError:
if child.is_symlink():
report.path_ignored(
Expand All @@ -5746,21 +5801,32 @@ def gen_python_files_in_dir(

raise

# First ignore files matching .gitignore
if gitignore.match_file(normalized_path):
report.path_ignored(child, "matches the .gitignore file content")
continue

normalized_path = "/" + normalized_path
if child.is_dir():
normalized_path += "/"

exclude_match = exclude.search(normalized_path)
if exclude_match and exclude_match.group(0):
report.path_ignored(child, "matches the --exclude regular expression")
is_excluded = False
for exclude in exclude_regexes:
exclude_match = exclude.search(normalized_path) if exclude else None
if exclude_match and exclude_match.group(0):
report.path_ignored(child, "matches the --exclude regular expression")
is_excluded = True
break
if is_excluded:
continue

if child.is_dir():
yield from gen_python_files_in_dir(
child, root, include, exclude, report, gitignore
yield from gen_python_files(
child.iterdir(), root, include, exclude_regexes, report, gitignore
)

elif child.is_file():
include_match = include.search(normalized_path)
include_match = include.search(normalized_path) if include else True
if include_match:
yield child

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/reference_functions.rst
Expand Up @@ -61,7 +61,7 @@ File operations

.. autofunction:: black.find_project_root

.. autofunction:: black.gen_python_files_in_dir
.. autofunction:: black.gen_python_files

.. autofunction:: black.read_pyproject_toml

Expand Down
36 changes: 20 additions & 16 deletions tests/test_black.py
Expand Up @@ -157,9 +157,13 @@ def invokeBlack(
) -> None:
runner = BlackRunner()
if ignore_config:
args = ["--config", str(THIS_DIR / "empty.toml"), *args]
args = ["--verbose", "--config", str(THIS_DIR / "empty.toml"), *args]
result = runner.invoke(black.main, args)
self.assertEqual(result.exit_code, exit_code, msg=runner.stderr_bytes.decode())
self.assertEqual(
result.exit_code,
exit_code,
msg=f"Failed with args: {args}. Stderr: {runner.stderr_bytes.decode()!r}",
)

@patch("black.dump_to_file", dump_to_stderr)
def checkSourceFile(self, name: str) -> None:
Expand Down Expand Up @@ -1537,8 +1541,8 @@ def test_include_exclude(self) -> None:
]
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files_in_dir(
path, this_abs, include, exclude, report, gitignore
black.gen_python_files(
path.iterdir(), this_abs, include, [exclude], report, gitignore
)
)
self.assertEqual(sorted(expected), sorted(sources))
Expand All @@ -1558,8 +1562,8 @@ def test_gitignore_exclude(self) -> None:
]
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files_in_dir(
path, this_abs, include, exclude, report, gitignore
black.gen_python_files(
path.iterdir(), this_abs, include, [exclude], report, gitignore
)
)
self.assertEqual(sorted(expected), sorted(sources))
Expand All @@ -1583,11 +1587,11 @@ def test_empty_include(self) -> None:
]
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files_in_dir(
path,
black.gen_python_files(
path.iterdir(),
this_abs,
empty,
re.compile(black.DEFAULT_EXCLUDES),
[re.compile(black.DEFAULT_EXCLUDES)],
report,
gitignore,
)
Expand All @@ -1610,11 +1614,11 @@ def test_empty_exclude(self) -> None:
]
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files_in_dir(
path,
black.gen_python_files(
path.iterdir(),
this_abs,
re.compile(black.DEFAULT_INCLUDES),
empty,
[empty],
report,
gitignore,
)
Expand Down Expand Up @@ -1670,8 +1674,8 @@ def test_symlink_out_of_root_directory(self) -> None:
child.is_symlink.return_value = True
try:
list(
black.gen_python_files_in_dir(
path, root, include, exclude, report, gitignore
black.gen_python_files(
path.iterdir(), root, include, exclude, report, gitignore
)
)
except ValueError as ve:
Expand All @@ -1684,8 +1688,8 @@ def test_symlink_out_of_root_directory(self) -> None:
child.is_symlink.return_value = False
with self.assertRaises(ValueError):
list(
black.gen_python_files_in_dir(
path, root, include, exclude, report, gitignore
black.gen_python_files(
path.iterdir(), root, include, exclude, report, gitignore
)
)
path.iterdir.assert_called()
Expand Down