Skip to content

Commit

Permalink
add --force-exclude argument (#1032)
Browse files Browse the repository at this point in the history
Co-authored-by: Peter Yu <2057325+yukw777@users.noreply.github.com>
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
3 people committed May 8, 2020
1 parent 67726a7 commit 89c87d2
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 66 deletions.
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

0 comments on commit 89c87d2

Please sign in to comment.