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