From beecd6fd0a9103aa91b1019dcf8fc774b667ea6c Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 1 Mar 2021 16:07:36 -0600 Subject: [PATCH] Add --extend-exclude parameter (#2005) Look ma! I contribute to open source! Co-authored-by: Richard Si <63936253+ichard26@users.noreply.github.com> --- README.md | 30 +++++------- docs/change_log.md | 2 + docs/installation_and_usage.md | 7 ++- docs/pyproject_toml.md | 22 ++------- pyproject.toml | 13 +---- src/black/__init__.py | 78 ++++++++++++++++++++---------- tests/test_black.py | 86 ++++++++++++++++++++++++++++------ 7 files changed, 148 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 178f763..5f8b52c 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/change_log.md b/docs/change_log.md index 066be76..01c2755 100644 --- a/docs/change_log.md +++ b/docs/change_log.md @@ -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 diff --git a/docs/installation_and_usage.md b/docs/installation_and_usage.md index ee45c93..bb554eb 100644 --- a/docs/installation_and_usage.md +++ b/docs/installation_and_usage.md @@ -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. diff --git a/docs/pyproject_toml.md b/docs/pyproject_toml.md index 453f533..9acc4c0 100644 --- a/docs/pyproject_toml.md +++ b/docs/pyproject_toml.md @@ -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) ''' ``` diff --git a/pyproject.toml b/pyproject.toml index 9d4da0b..7f632f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/src/black/__init__.py b/src/black/__init__.py index a1d16d9..e21e2af 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -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, diff --git a/tests/test_black.py b/tests/test_black.py index 9c3cc64..ba1869a 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1346,7 +1346,14 @@ class BlackTestCase(BlackBaseTestCase): 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 @@ class BlackTestCase(BlackBaseTestCase): verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1392,6 +1400,7 @@ class BlackTestCase(BlackBaseTestCase): verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=None, @@ -1415,6 +1424,7 @@ class BlackTestCase(BlackBaseTestCase): verbose=False, include=include, exclude=exclude, + extend_exclude=None, force_exclude=None, report=report, stdin_filename=stdin_filename, @@ -1442,6 +1452,35 @@ class BlackTestCase(BlackBaseTestCase): 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 @@ class BlackTestCase(BlackBaseTestCase): verbose=False, include=include, exclude="", + extend_exclude=None, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, @@ -1551,7 +1591,14 @@ class BlackTestCase(BlackBaseTestCase): 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,25 +1628,21 @@ class BlackTestCase(BlackBaseTestCase): empty, re.compile(black.DEFAULT_EXCLUDES), None, + None, report, gitignore, ) ) self.assertEqual(sorted(expected), sorted(sources)) - def test_empty_exclude(self) -> None: + def test_extend_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"), + Path(path / "b/dont_exclude/a.py"), ] this_abs = THIS_DIR.resolve() sources.extend( @@ -1607,7 +1650,8 @@ class BlackTestCase(BlackBaseTestCase): path.iterdir(), this_abs, re.compile(black.DEFAULT_INCLUDES), - empty, + re.compile(r"\.pyi$"), + re.compile(r"\.definitely_exclude"), None, report, gitignore, @@ -1615,8 +1659,8 @@ class BlackTestCase(BlackBaseTestCase): ) self.assertEqual(sorted(expected), sorted(sources)) - def test_invalid_include_exclude(self) -> None: - for option in ["--include", "--exclude"]: + 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 @@ class BlackTestCase(BlackBaseTestCase): 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 @@ class BlackTestCase(BlackBaseTestCase): 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() -- 2.39.5