diff --git a/.flake8 b/.flake8 index 85f51cf9f05..f8dca18e7cf 100644 --- a/.flake8 +++ b/.flake8 @@ -1,8 +1,8 @@ [flake8] # B905 should be enabled when we drop support for 3.9 -ignore = E203, E266, E501, E704, W503, B905, B907 +ignore = E203, E266, E501, E701, E704, W503, B905, B907 # line length is intentionally set to 80 here because black uses Bugbear -# See https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length for more details +# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#bugbear for more details max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/CHANGES.md b/CHANGES.md index 187c59766c8..a726a91457a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,12 +14,19 @@ +- Move the `hug_parens_with_braces_and_square_brackets` feature to the unstable style + due to an outstanding crash and proposed formatting tweaks (#4198) - Checking for newline before adding one on docstring that is almost at the line limit (#4185) ### Configuration - +- _Black_ now ignores `pyproject.toml` that is missing a `tool.black` section when + discovering project root and configuration. Since _Black_ continues to use version + control as an indicator of project root, this is expected to primarily change behavior + for users in a monorepo setup (desirably). If you wish to preserve previous behavior, + simply add an empty `[tool.black]` to the previously discovered `pyproject.toml` + (#4204) ### Packaging diff --git a/docs/compatible_configs/flake8/.flake8 b/docs/compatible_configs/flake8/.flake8 index 8dd399ab55b..0d4ade348d6 100644 --- a/docs/compatible_configs/flake8/.flake8 +++ b/docs/compatible_configs/flake8/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203,E701 diff --git a/docs/compatible_configs/flake8/setup.cfg b/docs/compatible_configs/flake8/setup.cfg index 8dd399ab55b..0d4ade348d6 100644 --- a/docs/compatible_configs/flake8/setup.cfg +++ b/docs/compatible_configs/flake8/setup.cfg @@ -1,3 +1,3 @@ [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203,E701 diff --git a/docs/compatible_configs/flake8/tox.ini b/docs/compatible_configs/flake8/tox.ini index 8dd399ab55b..0d4ade348d6 100644 --- a/docs/compatible_configs/flake8/tox.ini +++ b/docs/compatible_configs/flake8/tox.ini @@ -1,3 +1,3 @@ [flake8] max-line-length = 88 -extend-ignore = E203 +extend-ignore = E203,E701 diff --git a/docs/compatible_configs/pycodestyle/.flake8 b/docs/compatible_configs/pycodestyle/.flake8 new file mode 100644 index 00000000000..34225907524 --- /dev/null +++ b/docs/compatible_configs/pycodestyle/.flake8 @@ -0,0 +1,3 @@ +[pycodestyle] +max-line-length = 88 +ignore = E203,E701 diff --git a/docs/compatible_configs/pycodestyle/setup.cfg b/docs/compatible_configs/pycodestyle/setup.cfg new file mode 100644 index 00000000000..34225907524 --- /dev/null +++ b/docs/compatible_configs/pycodestyle/setup.cfg @@ -0,0 +1,3 @@ +[pycodestyle] +max-line-length = 88 +ignore = E203,E701 diff --git a/docs/compatible_configs/pycodestyle/tox.ini b/docs/compatible_configs/pycodestyle/tox.ini new file mode 100644 index 00000000000..34225907524 --- /dev/null +++ b/docs/compatible_configs/pycodestyle/tox.ini @@ -0,0 +1,3 @@ +[pycodestyle] +max-line-length = 88 +ignore = E203,E701 diff --git a/docs/faq.md b/docs/faq.md index 124a096efac..d19ff8e7ace 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -77,13 +77,10 @@ following will not be formatted: - invalid syntax, as it can't be safely distinguished from automagics in the absence of a running `IPython` kernel. -## Why are Flake8's E203 and W503 violated? +## Why does Flake8 report warnings? -Because they go against PEP 8. E203 falsely triggers on list -[slices](the_black_code_style/current_style.md#slices), and adhering to W503 hinders -readability because operators are misaligned. Disable W503 and enable the -disabled-by-default counterpart W504. E203 should be disabled while changes are still -[discussed](https://github.com/PyCQA/pycodestyle/issues/373). +Some of Flake8's rules conflict with Black's style. We recommend disabling these rules. +See [Using _Black_ with other tools](labels/why-pycodestyle-warnings). ## Which Python versions does Black support? diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index e642a1aef33..187e3a3e6f5 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -134,10 +134,10 @@ profile = black -### Flake8 +### pycodestyle -[Flake8](https://pypi.org/p/flake8/) is a code linter. It warns you of syntax errors, -possible bugs, stylistic errors, etc. For the most part, Flake8 follows +[pycodestyle](https://pycodestyle.pycqa.org/) is a code linter. It warns you of syntax +errors, possible bugs, stylistic errors, etc. For the most part, pycodestyle follows [PEP 8](https://www.python.org/dev/peps/pep-0008/) when warning about stylistic errors. There are a few deviations that cause incompatibilities with _Black_. @@ -145,67 +145,115 @@ There are a few deviations that cause incompatibilities with _Black_. ``` max-line-length = 88 -extend-ignore = E203, E704 +ignore = E203,E701 ``` +(labels/why-pycodestyle-warnings)= + #### Why those options above? +##### `max-line-length` + +As with isort, pycodestyle should be configured to allow lines up to the length limit of +`88`, _Black_'s default. + +##### `E203` + In some cases, as determined by PEP 8, _Black_ will enforce an equal amount of -whitespace around slice operators. Due to this, Flake8 will raise -`E203 whitespace before ':'` warnings. Since this warning is not PEP 8 compliant, Flake8 -should be configured to ignore it via `extend-ignore = E203`. +whitespace around slice operators. Due to this, pycodestyle will raise +`E203 whitespace before ':'` warnings. Since this warning is not PEP 8 compliant, it +should be disabled. + +##### `E701` / `E704` + +_Black_ will collapse implementations of classes and functions consisting solely of `..` +to a single line. This matches how such examples are formatted in PEP 8. It remains true +that in all other cases Black will prevent multiple statements on the same line, in +accordance with PEP 8 generally discouraging this. + +However, `pycodestyle` does not mirror this logic and may raise +`E701 multiple statements on one line (colon)` in this situation. Its +disabled-by-default `E704 multiple statements on one line (def)` rule may also raise +warnings and should not be enabled. + +##### `W503` When breaking a line, _Black_ will break it before a binary operator. This is compliant with PEP 8 as of [April 2016](https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b#diff-64ec08cc46db7540f18f2af46037f599). There's a disabled-by-default warning in Flake8 which goes against this PEP 8 recommendation called `W503 line break before binary operator`. It should not be enabled -in your configuration. - -Also, as like with isort, flake8 should be configured to allow lines up to the length -limit of `88`, _Black_'s default. This explains `max-line-length = 88`. +in your configuration. You can use its counterpart +`W504 line break after binary operator` instead. #### Formats
-.flake8 +setup.cfg, .pycodestyle, tox.ini ```ini -[flake8] +[pycodestyle] max-line-length = 88 -extend-ignore = E203, E704 +ignore = E203,E701 ```
-
-setup.cfg +### Flake8 -```ini +[Flake8](https://pypi.org/p/flake8/) is a wrapper around multiple linters, including +pycodestyle. As such, it has many of the same issues. + +#### Bugbear + +It's recommended to use [the Bugbear plugin](https://github.com/PyCQA/flake8-bugbear) +and enable +[its B950 check](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings#:~:text=you%20expect%20it.-,B950,-%3A%20Line%20too%20long) +instead of using Flake8's E501, because it aligns with +[Black's 10% rule](labels/line-length). + +Install Bugbear and use the following config: + +``` +[flake8] +max-line-length = 80 +extend-select = B950 +extend-ignore = E203,E501,E701 +``` + +#### Minimal Configuration + +In cases where you can't or don't want to install Bugbear, you can use this minimally +compatible config: + +``` [flake8] max-line-length = 88 -extend-ignore = E203, E704 +extend-ignore = E203,E701 ``` -
+#### Why those options above? + +See [the pycodestyle section](labels/why-pycodestyle-warnings) above. + +#### Formats
-tox.ini +.flake8, setup.cfg, tox.ini ```ini [flake8] max-line-length = 88 -extend-ignore = E203, E704 +extend-ignore = E203,E701 ```
### Pylint -[Pylint](https://pypi.org/p/pylint/) is also a code linter like Flake8. It has the same -checks as flake8 and more. In particular, it has more formatting checks regarding style -conventions like variable naming. With so many checks, Pylint is bound to have some -mixed feelings about _Black_'s formatting style. +[Pylint](https://pypi.org/p/pylint/) is also a code linter like Flake8. It has many of +the same checks as Flake8 and more. It particularly has more formatting checks regarding +style conventions like variable naming. #### Configuration @@ -252,35 +300,3 @@ max-line-length = "88" ``` - -### pycodestyle - -[pycodestyle](https://pycodestyle.pycqa.org/) is also a code linter like Flake8. - -#### Configuration - -``` -max-line-length = 88 -ignore = E203 -``` - -#### Why those options above? - -pycodestyle should be configured to only complain about lines that surpass `88` -characters via `max_line_length = 88`. - -See -[Why are Flake8’s E203 and W503 violated?](https://black.readthedocs.io/en/stable/faq.html#why-are-flake8-s-e203-and-w503-violated) - -#### Formats - -
-setup.cfg - -```cfg -[pycodestyle] -ignore = E203 -max_line_length = 88 -``` - -
diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index ca5d1d4a701..586c79074af 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -143,7 +143,7 @@ significantly shorter files than sticking with 80 (the most popular), or even 79 by the standard library). In general, [90-ish seems like the wise choice](https://youtu.be/wf-BqAjZb8M?t=260). -If you're paid by the line of code you write, you can pass `--line-length` with a lower +If you're paid by the lines of code you write, you can pass `--line-length` with a lower number. _Black_ will try to respect that. However, sometimes it won't be able to without breaking other rules. In those rare cases, auto-formatted code will exceed your allotted limit. @@ -153,35 +153,10 @@ harder to work with line lengths exceeding 100 characters. It also adversely aff side-by-side diff review on typical screen resolutions. Long lines also make it harder to present code neatly in documentation or talk slides. -#### Flake8 +#### Flake8 and other linters -If you use Flake8, you have a few options: - -1. Recommended is using [Bugbear](https://github.com/PyCQA/flake8-bugbear) and enabling - its B950 check instead of using Flake8's E501, because it aligns with Black's 10% - rule. Install Bugbear and use the following config: - - ```ini - [flake8] - max-line-length = 80 - ... - select = C,E,F,W,B,B950 - extend-ignore = E203, E501, E704 - ``` - - The rationale for B950 is explained in - [Bugbear's documentation](https://github.com/PyCQA/flake8-bugbear#opinionated-warnings). - -2. For a minimally compatible config: - - ```ini - [flake8] - max-line-length = 88 - extend-ignore = E203, E704 - ``` - -An explanation of why E203 is disabled can be found in the [Slices section](#slices) of -this page. +See [Using _Black_ with other tools](../guides/using_black_with_other_tools.md) about +linter compatibility. ### Empty lines diff --git a/docs/the_black_code_style/future_style.md b/docs/the_black_code_style/future_style.md index 50dcc583ef1..d7640765b30 100644 --- a/docs/the_black_code_style/future_style.md +++ b/docs/the_black_code_style/future_style.md @@ -16,14 +16,14 @@ feature, it is demoted to the `--unstable` style. To avoid thrash when a feature demoted from the `--preview` to the `--unstable` style, users can use the `--enable-unstable-feature` flag to enable specific unstable features. +(labels/preview-features)= + Currently, the following features are included in the preview style: - `hex_codes_in_unicode_sequences`: normalize casing of Unicode escape characters in strings - `unify_docstring_detection`: fix inconsistencies in whether certain strings are detected as docstrings -- `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested - brackets ([see below](labels/hug-parens)) - `no_normalize_fmt_skip_whitespace`: whitespace before `# fmt: skip` comments is no longer normalized - `typed_params_trailing_comma`: consistently add trailing commas to typed function @@ -41,6 +41,8 @@ The unstable style additionally includes the following features: ([see below](labels/wrap-long-dict-values)) - `multiline_string_handling`: more compact formatting of expressions involving multiline strings ([see below](labels/multiline-string-handling)) +- `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested + brackets ([see below](labels/hug-parens)) (labels/hug-parens)= diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index dc9d9a64c68..61c52450165 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -456,10 +456,11 @@ of tools like [Poetry](https://python-poetry.org/), ### Where _Black_ looks for the file -By default _Black_ looks for `pyproject.toml` starting from the common base directory of -all files and directories passed on the command line. If it's not there, it looks in -parent directories. It stops looking when it finds the file, or a `.git` directory, or a -`.hg` directory, or the root of the file system, whichever comes first. +By default _Black_ looks for `pyproject.toml` containing a `[tool.black]` section +starting from the common base directory of all files and directories passed on the +command line. If it's not there, it looks in parent directories. It stops looking when +it finds the file, or a `.git` directory, or a `.hg` directory, or the root of the file +system, whichever comes first. If you're formatting standard input, _Black_ will look for configuration starting from the current working directory. diff --git a/scripts/generate_schema.py b/scripts/generate_schema.py old mode 100644 new mode 100755 index f3437cc8fee..35765750091 --- a/scripts/generate_schema.py +++ b/scripts/generate_schema.py @@ -62,7 +62,7 @@ def main(schemastore: bool, outfile: IO[str]) -> None: } if schemastore: - schema["$id"] = ("https://json.schemastore.org/partial-black.json",) + schema["$id"] = "https://json.schemastore.org/partial-black.json" # The precise list of unstable features may change frequently, so don't # bother putting it in SchemaStore schema["properties"]["enable-unstable-feature"]["items"] = {"type": "string"} diff --git a/src/black/files.py b/src/black/files.py index 1eb8745572b..960f13ee270 100644 --- a/src/black/files.py +++ b/src/black/files.py @@ -42,6 +42,12 @@ import colorama # noqa: F401 +@lru_cache +def _load_toml(path: Union[Path, str]) -> Dict[str, Any]: + with open(path, "rb") as f: + return tomllib.load(f) + + @lru_cache def find_project_root( srcs: Sequence[str], stdin_filename: Optional[str] = None @@ -84,7 +90,9 @@ def find_project_root( return directory, ".hg directory" if (directory / "pyproject.toml").is_file(): - return directory, "pyproject.toml" + pyproject_toml = _load_toml(directory / "pyproject.toml") + if "black" in pyproject_toml.get("tool", {}): + return directory, "pyproject.toml" return directory, "file system root" @@ -117,8 +125,7 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]: If parsing fails, will raise a tomllib.TOMLDecodeError. """ - with open(path_config, "rb") as f: - pyproject_toml = tomllib.load(f) + pyproject_toml = _load_toml(path_config) config: Dict[str, Any] = pyproject_toml.get("tool", {}).get("black", {}) config = {k.replace("--", "").replace("-", "_"): v for k, v in config.items()} diff --git a/src/black/mode.py b/src/black/mode.py index 3aa92c7b8bb..9593a90d170 100644 --- a/src/black/mode.py +++ b/src/black/mode.py @@ -187,6 +187,8 @@ class Preview(Enum): Preview.wrap_long_dict_values_in_parens, # See issue #4159 Preview.multiline_string_handling, + # See issue #4036 (crash), #4098, #4099 (proposed tweaks) + Preview.hug_parens_with_braces_and_square_brackets, } diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py index 47a6a0bcae6..cbbcf16d3bd 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets.py @@ -1,4 +1,4 @@ -# flags: --preview +# flags: --unstable def foo_brackets(request): return JsonResponse( { diff --git a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py index fdebdf69c20..16ebea379bc 100644 --- a/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py +++ b/tests/data/cases/preview_hug_parens_with_braces_and_square_brackets_no_ll1.py @@ -1,4 +1,4 @@ -# flags: --preview --no-preview-line-length-1 +# flags: --unstable --no-preview-line-length-1 # split out from preview_hug_parens_with_brackes_and_square_brackets, as it produces # different code on the second pass with line-length 1 in many cases. # Seems to be about whether the last string in a sequence gets wrapped in parens or not. diff --git a/tests/test_black.py b/tests/test_black.py index 123ea0bb88a..f876d365b12 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -1668,9 +1668,9 @@ def test_find_project_root(self) -> None: src_dir.mkdir() root_pyproject = root / "pyproject.toml" - root_pyproject.touch() + root_pyproject.write_text("[tool.black]", encoding="utf-8") src_pyproject = src_dir / "pyproject.toml" - src_pyproject.touch() + src_pyproject.write_text("[tool.black]", encoding="utf-8") src_python = src_dir / "foo.py" src_python.touch() @@ -1693,6 +1693,20 @@ def test_find_project_root(self) -> None: (src_dir.resolve(), "pyproject.toml"), ) + src_sub = src_dir / "sub" + src_sub.mkdir() + + src_sub_pyproject = src_sub / "pyproject.toml" + src_sub_pyproject.touch() # empty + + src_sub_python = src_sub / "bar.py" + + # we skip src_sub_pyproject since it is missing the [tool.black] section + self.assertEqual( + black.find_project_root((src_sub_python,)), + (src_dir.resolve(), "pyproject.toml"), + ) + @patch( "black.files.find_user_pyproject_toml", ) diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 00000000000..8050e0f73c6 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,74 @@ +""" + +Test that the docs are up to date. + +""" + +import re +from itertools import islice +from pathlib import Path +from typing import Optional, Sequence, Set + +import pytest + +from black.mode import UNSTABLE_FEATURES, Preview + +DOCS_PATH = Path("docs/the_black_code_style/future_style.md") + + +def check_feature_list( + lines: Sequence[str], expected_feature_names: Set[str], label: str +) -> Optional[str]: + start_index = lines.index(f"(labels/{label}-features)=\n") + if start_index == -1: + return ( + f"Could not find the {label} features list in {DOCS_PATH}. Ensure the" + " preview-features label is present." + ) + num_blank_lines_seen = 0 + seen_preview_feature_names = set() + for line in islice(lines, start_index + 1, None): + if not line.strip(): + num_blank_lines_seen += 1 + if num_blank_lines_seen == 3: + break + continue + if line.startswith("- "): + match = re.search(r"^- `([a-z\d_]+)`", line) + if match: + seen_preview_feature_names.add(match.group(1)) + + if seen_preview_feature_names - expected_feature_names: + extra = ", ".join(sorted(seen_preview_feature_names - expected_feature_names)) + return ( + f"The following features should not be in the list of {label} features:" + f" {extra}. Please remove them from the {label}-features label in" + f" {DOCS_PATH}" + ) + elif expected_feature_names - seen_preview_feature_names: + missing = ", ".join(sorted(expected_feature_names - seen_preview_feature_names)) + return ( + f"The following features are missing from the list of {label} features:" + f" {missing}. Please document them under the {label}-features label in" + f" {DOCS_PATH}" + ) + else: + return None + + +def test_feature_lists_are_up_to_date() -> None: + repo_root = Path(__file__).parent.parent + if not (repo_root / "docs").exists(): + pytest.skip("docs not found") + with (repo_root / DOCS_PATH).open(encoding="utf-8") as f: + future_style = f.readlines() + preview_error = check_feature_list( + future_style, + {feature.name for feature in set(Preview) - UNSTABLE_FEATURES}, + "preview", + ) + assert preview_error is None, preview_error + unstable_error = check_feature_list( + future_style, {feature.name for feature in UNSTABLE_FEATURES}, "unstable" + ) + assert unstable_error is None, unstable_error