Add --extend-exclude parameter (#2005)

Look ma! I contribute to open source!

Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com>
This commit is contained in:
Joshua Cannon 2021-03-01 16:07:36 -06:00 committed by GitHub
parent 858225d34d
commit beecd6fd0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 167 additions and 109 deletions

View File

@ -135,11 +135,17 @@ Options:
hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_bu
ild|buck-out|build|dist)/]
--extend-exclude TEXT Like --exclude, but adds additional files
and directories on top of the excluded
ones (useful if you simply want to add to
the default).
--force-exclude TEXT Like --exclude, but files and directories
matching this regex will be excluded even
when they are passed explicitly as
arguments.
--stdin-filename TEXT The name of the file when passing it through
stdin. Useful to make sure Black will
respect --force-exclude option on some
@ -151,7 +157,7 @@ Options:
-v, --verbose Also emit messages to stderr about files
that were not changed or were ignored due to
--exclude=.
exclusion patterns.
--version Show the version and exit.
--config FILE Read configuration from FILE path.
@ -263,7 +269,7 @@ above. What seems like a bug might be intended behaviour.
_Black_ is able to read project-specific default values for its command line options
from a `pyproject.toml` file. This is especially useful for specifying custom
`--include` and `--exclude` patterns for your project.
`--include` and `--exclude`/`--extend-exclude` patterns for your project.
**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is
"No". _Black_ is all about sensible defaults.
@ -313,25 +319,10 @@ expressions by Black. Use `[ ]` to denote a significant space character.
line-length = 88
target-version = ['py37']
include = '\.pyi?$'
exclude = '''
extend-exclude = '''
# A regex preceded with ^/ will apply only to files and directories
# in the root of the project.
^/(
(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
| foo.py # also separately exclude a file named foo.py in
# the root of the project
)
^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults)
'''
```
@ -616,6 +607,7 @@ Multiple contributions by:
- [Joseph Larson](mailto:larson.joseph@gmail.com)
- [Josh Bode](mailto:joshbode@fastmail.com)
- [Josh Holland](mailto:anowlcalledjosh@gmail.com)
- [Joshua Cannon](mailto:joshdcannon@gmail.com)
- [José Padilla](mailto:jpadilla@webapplicate.com)
- [Juan Luis Cano Rodríguez](mailto:hello@juanlu.space)
- [kaiix](mailto:kvn.hou@gmail.com)

View File

@ -28,6 +28,8 @@
- use lowercase hex strings (#1692)
- added `--extend-exclude` argument (#1571)
#### _Packaging_
- Self-contained native _Black_ binaries are now provided for releases via GitHub

View File

@ -95,6 +95,11 @@ Options:
when they are passed explicitly as
arguments.
--extend-exclude TEXT Like --exclude, but adds additional files
and directories on top of the excluded
ones. (useful if you simply want to add to
the default)
--stdin-filename TEXT The name of the file when passing it through
stdin. Useful to make sure Black will
respect --force-exclude option on some
@ -106,7 +111,7 @@ Options:
-v, --verbose Also emit messages to stderr about files
that were not changed or were ignored due to
--exclude=.
exclusion patterns.
--version Show the version and exit.
--config FILE Read configuration from FILE path.

View File

@ -4,7 +4,8 @@
_Black_ is able to read project-specific default values for its command line options
from a `pyproject.toml` file. This is especially useful for specifying custom
`--include` and `--exclude` patterns for your project.
`--include` and `--exclude`/`--force-exclude`/`--extend-exclude` patterns for your
project.
**Pro-tip**: If you're asking yourself "Do I need to configure anything?" the answer is
"No". _Black_ is all about sensible defaults.
@ -54,25 +55,10 @@ expressions by Black. Use `[ ]` to denote a significant space character.
line-length = 88
target-version = ['py37']
include = '\.pyi?$'
exclude = '''
extend-exclude = '''
# A regex preceded with ^/ will apply only to files and directories
# in the root of the project.
^/(
(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
| foo.py # also separately exclude a file named foo.py in
# the root of the project
)
^/foo.py # exclude a file named foo.py in the root of the project (in addition to the defaults)
'''
```

View File

@ -9,19 +9,8 @@
line-length = 88
target-version = ['py36', 'py37', 'py38']
include = '\.pyi?$'
exclude = '''
extend-exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
# The following are specific to Black, you probably don't want those.
| blib2to3
| tests/data

View File

@ -461,6 +461,14 @@ def target_version_option_callback(
),
show_default=True,
)
@click.option(
"--extend-exclude",
type=str,
help=(
"Like --exclude, but adds additional files and directories on top of the"
" excluded ones. (Useful if you simply want to add to the default)"
),
)
@click.option(
"--force-exclude",
type=str,
@ -493,7 +501,7 @@ def target_version_option_callback(
is_flag=True,
help=(
"Also emit messages to stderr about files that were not changed or were ignored"
" due to --exclude=."
" due to exclusion patterns."
),
)
@click.version_option(version=__version__)
@ -537,6 +545,7 @@ def main(
verbose: bool,
include: str,
exclude: str,
extend_exclude: Optional[str],
force_exclude: Optional[str],
stdin_filename: Optional[str],
src: Tuple[str, ...],
@ -570,6 +579,7 @@ def main(
verbose=verbose,
include=include,
exclude=exclude,
extend_exclude=extend_exclude,
force_exclude=force_exclude,
report=report,
stdin_filename=stdin_filename,
@ -602,6 +612,18 @@ def main(
ctx.exit(report.return_code)
def test_regex(
ctx: click.Context,
regex_name: str,
regex: Optional[str],
) -> Optional[Pattern]:
try:
return re_compile_maybe_verbose(regex) if regex is not None else None
except re.error:
err(f"Invalid regular expression for {regex_name} given: {regex!r}")
ctx.exit(2)
def get_sources(
*,
ctx: click.Context,
@ -610,28 +632,18 @@ def get_sources(
verbose: bool,
include: str,
exclude: str,
extend_exclude: Optional[str],
force_exclude: Optional[str],
report: "Report",
stdin_filename: Optional[str],
) -> Set[Path]:
"""Compute the set of files to be formatted."""
try:
include_regex = re_compile_maybe_verbose(include)
except re.error:
err(f"Invalid regular expression for include given: {include!r}")
ctx.exit(2)
try:
exclude_regex = re_compile_maybe_verbose(exclude)
except re.error:
err(f"Invalid regular expression for exclude given: {exclude!r}")
ctx.exit(2)
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)
include_regex = test_regex(ctx, "include", include)
exclude_regex = test_regex(ctx, "exclude", exclude)
assert exclude_regex is not None
extend_exclude_regex = test_regex(ctx, "extend_exclude", extend_exclude)
force_exclude_regex = test_regex(ctx, "force_exclude", force_exclude)
root = find_project_root(src)
sources: Set[Path] = set()
@ -672,6 +684,7 @@ def get_sources(
root,
include_regex,
exclude_regex,
extend_exclude_regex,
force_exclude_regex,
report,
gitignore,
@ -6112,17 +6125,27 @@ def normalize_path_maybe_ignore(
return normalized_path
def path_is_excluded(
normalized_path: str,
pattern: Optional[Pattern[str]],
) -> bool:
match = pattern.search(normalized_path) if pattern else None
return bool(match and match.group(0))
def gen_python_files(
paths: Iterable[Path],
root: Path,
include: Optional[Pattern[str]],
exclude: Pattern[str],
extend_exclude: Optional[Pattern[str]],
force_exclude: Optional[Pattern[str]],
report: "Report",
gitignore: PathSpec,
) -> Iterator[Path]:
"""Generate all files under `path` whose paths are not excluded by the
`exclude_regex` or `force_exclude` regexes, but are included by the `include` regex.
`exclude_regex`, `extend_exclude`, or `force_exclude` regexes,
but are included by the `include` regex.
Symbolic links pointing outside of the `root` directory are ignored.
@ -6139,20 +6162,22 @@ def gen_python_files(
report.path_ignored(child, "matches the .gitignore file content")
continue
# Then ignore with `--exclude` and `--force-exclude` options.
# Then ignore with `--exclude` `--extend-exclude` and `--force-exclude` options.
normalized_path = "/" + normalized_path
if child.is_dir():
normalized_path += "/"
exclude_match = exclude.search(normalized_path) if exclude else None
if exclude_match and exclude_match.group(0):
if path_is_excluded(normalized_path, exclude):
report.path_ignored(child, "matches the --exclude regular expression")
continue
force_exclude_match = (
force_exclude.search(normalized_path) if force_exclude else None
)
if force_exclude_match and force_exclude_match.group(0):
if path_is_excluded(normalized_path, extend_exclude):
report.path_ignored(
child, "matches the --extend-exclude regular expression"
)
continue
if path_is_excluded(normalized_path, force_exclude):
report.path_ignored(child, "matches the --force-exclude regular expression")
continue
@ -6162,6 +6187,7 @@ def gen_python_files(
root,
include,
exclude,
extend_exclude,
force_exclude,
report,
gitignore,

View File

@ -1346,7 +1346,14 @@ def test_include_exclude(self) -> None:
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files(
path.iterdir(), this_abs, include, exclude, None, report, gitignore
path.iterdir(),
this_abs,
include,
exclude,
None,
None,
report,
gitignore,
)
)
self.assertEqual(sorted(expected), sorted(sources))
@ -1370,6 +1377,7 @@ def test_exclude_for_issue_1572(self) -> None:
verbose=False,
include=include,
exclude=exclude,
extend_exclude=None,
force_exclude=None,
report=report,
stdin_filename=None,
@ -1392,6 +1400,7 @@ def test_get_sources_with_stdin(self) -> None:
verbose=False,
include=include,
exclude=exclude,
extend_exclude=None,
force_exclude=None,
report=report,
stdin_filename=None,
@ -1415,6 +1424,7 @@ def test_get_sources_with_stdin_filename(self) -> None:
verbose=False,
include=include,
exclude=exclude,
extend_exclude=None,
force_exclude=None,
report=report,
stdin_filename=stdin_filename,
@ -1442,6 +1452,35 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
verbose=False,
include=include,
exclude=exclude,
extend_exclude=None,
force_exclude=None,
report=report,
stdin_filename=stdin_filename,
)
)
self.assertEqual(sorted(expected), sorted(sources))
@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
# Extend exclude shouldn't exclude stdin_filename since it is mimicking the
# file being passed directly. This is the same as
# test_exclude_for_issue_1572
path = THIS_DIR / "data" / "include_exclude_tests"
include = ""
extend_exclude = r"/exclude/|a\.py"
src = "-"
report = black.Report()
stdin_filename = str(path / "b/exclude/a.py")
expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
sources = list(
black.get_sources(
ctx=FakeContext(),
src=(src,),
quiet=True,
verbose=False,
include=include,
exclude="",
extend_exclude=extend_exclude,
force_exclude=None,
report=report,
stdin_filename=stdin_filename,
@ -1467,6 +1506,7 @@ def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
verbose=False,
include=include,
exclude="",
extend_exclude=None,
force_exclude=force_exclude,
report=report,
stdin_filename=stdin_filename,
@ -1551,7 +1591,14 @@ def test_gitignore_exclude(self) -> None:
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files(
path.iterdir(), this_abs, include, exclude, None, report, gitignore
path.iterdir(),
this_abs,
include,
exclude,
None,
None,
report,
gitignore,
)
)
self.assertEqual(sorted(expected), sorted(sources))
@ -1581,33 +1628,6 @@ def test_empty_include(self) -> None:
empty,
re.compile(black.DEFAULT_EXCLUDES),
None,
report,
gitignore,
)
)
self.assertEqual(sorted(expected), sorted(sources))
def test_empty_exclude(self) -> None:
path = THIS_DIR / "data" / "include_exclude_tests"
report = black.Report()
gitignore = PathSpec.from_lines("gitwildmatch", [])
empty = re.compile(r"")
sources: List[Path] = []
expected = [
Path(path / "b/dont_exclude/a.py"),
Path(path / "b/dont_exclude/a.pyi"),
Path(path / "b/exclude/a.py"),
Path(path / "b/exclude/a.pyi"),
Path(path / "b/.definitely_exclude/a.py"),
Path(path / "b/.definitely_exclude/a.pyi"),
]
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files(
path.iterdir(),
this_abs,
re.compile(black.DEFAULT_INCLUDES),
empty,
None,
report,
gitignore,
@ -1615,8 +1635,32 @@ def test_empty_exclude(self) -> None:
)
self.assertEqual(sorted(expected), sorted(sources))
def test_invalid_include_exclude(self) -> None:
for option in ["--include", "--exclude"]:
def test_extend_exclude(self) -> None:
path = THIS_DIR / "data" / "include_exclude_tests"
report = black.Report()
gitignore = PathSpec.from_lines("gitwildmatch", [])
sources: List[Path] = []
expected = [
Path(path / "b/exclude/a.py"),
Path(path / "b/dont_exclude/a.py"),
]
this_abs = THIS_DIR.resolve()
sources.extend(
black.gen_python_files(
path.iterdir(),
this_abs,
re.compile(black.DEFAULT_INCLUDES),
re.compile(r"\.pyi$"),
re.compile(r"\.definitely_exclude"),
None,
report,
gitignore,
)
)
self.assertEqual(sorted(expected), sorted(sources))
def test_invalid_cli_regex(self) -> None:
for option in ["--include", "--exclude", "--extend-exclude", "--force-exclude"]:
self.invokeBlack(["-", option, "**()(!!*)"], exit_code=2)
def test_preserves_line_endings(self) -> None:
@ -1665,7 +1709,14 @@ def test_symlink_out_of_root_directory(self) -> None:
try:
list(
black.gen_python_files(
path.iterdir(), root, include, exclude, None, report, gitignore
path.iterdir(),
root,
include,
exclude,
None,
None,
report,
gitignore,
)
)
except ValueError as ve:
@ -1679,7 +1730,14 @@ def test_symlink_out_of_root_directory(self) -> None:
with self.assertRaises(ValueError):
list(
black.gen_python_files(
path.iterdir(), root, include, exclude, None, report, gitignore
path.iterdir(),
root,
include,
exclude,
None,
None,
report,
gitignore,
)
)
path.iterdir.assert_called()