Repository: psf/black

Issue:
Black stops looking for config at first pyproject.toml file even if there's no black section in it
<!--
Please make sure that the bug is not already fixed either in newer versions or the
current development version. To confirm this, you have three options:

1. Update Black's version if a newer release exists: `pip install -U black`
2. Use the online formatter at <https://black.vercel.app/?version=main>, which will use
   the latest main branch.
3. Or run _Black_ on your machine:
   - create a new virtualenv (make sure it's the same Python version);
   - clone this repository;
   - run `pip install -e .[d]`;
   - run `pip install -r test_requirements.txt`
   - make sure it's sane by running `python -m pytest`; and
   - run `black` like you did last time.
-->
Disclaimer: I was hesitant whether I should report this as a bug or rather a "Feature request".

**Describe the bug**

<!-- A clear and concise description of what the bug is. -->
In a project dealing with multiple sub-projects in a single repository (aka. mono-repo style) having more than one `pyproject.toml` file confuses black.
In our example we wanted to have one top-level black configuration,
while also having sub-project specific `pyproject.toml` files (without any black config) in each sub-directory.
Unfortunately, running black on a sub-directory
uses default configuration instead of the one defined in the top-level config file.

**To Reproduce**

<!--
Minimal steps to reproduce the behavior with source code and Black's configuration.
-->
Given this file structure:
```
.
|-- pyproject.toml
|-- sub1
|   |-- pyproject.toml
|   `-- test.py
`-- sub2
    `-- test.py
```

With the following contents:
```
==> ./pyproject.toml <==
[tool.black]
line-length = 20

==> ./sub1/pyproject.toml <==
[tool.isort]
profile = "black"

==> ./sub1/test.py <==
a_very_long_line = {"that should be wrapped": "to fit 20 characters line length"}

==> ./sub2/test.py <==
a_very_long_line = {"that should be wrapped": "to fit 20 characters line length"}
```

Running black on both test.py files:
```sh
$ black -v sub1/test.py
Identified `/private/tmp/black-many-configs/sub1` as project root containing a pyproject.toml.
Sources to be formatted: "test.py"
sub1/test.py already well formatted, good job.

All done! ✨ 🍰 ✨
1 file left unchanged.

$ black -v sub2/test.py
Identified `/private/tmp/black-many-configs` as project root containing a pyproject.toml.
Sources to be formatted: "sub2/test.py"
Using configuration from project root.
reformatted sub2/test.py

All done! ✨ 🍰 ✨
1 file reformatted.
```

Modifies only the file inside `sub2` directory (the one without `pyproject.toml` file)
even though `sub1/pyproject.toml` doesn't contain any black config section.

**Expected behavior**

<!-- A clear and concise description of what you expected to happen. -->
Both files should be modified
and black should continue looking for a file
that is relevant for its configuration (in this case, the one that has a `[tool.black]` section).

**Environment**

<!-- Please complete the following information: -->

- Black's version: black, 22.1.0 (compiled: yes)
- OS and Python version: OSX - Python 3.9.9

**Additional context**

<!-- Add any other context about the problem here. -->

A variation of this problem has already been reported in https://github.com/psf/black/issues/1826, where the proposed solution is to introduce support for the `config` option inside the sub-project's `pyproject.toml` config file.

Other (potentially) relevant issues: https://github.com/psf/black/issues/2537, https://github.com/psf/black/issues/2850.

Discussion:
hauntsaninja: This has some relation to #1826 in that this gives some folks affected by that issue an option (assuming it's all within the same repo, have pyproject.toml in a parent directory). However, I don't really like #1826 on its own — without #3952 I think it's a little footgun-y.
github-actions[bot]: **diff-shades** reports zero changes comparing this PR (7c504b2250a1af8cddb6176d1e8cbdfd240a4a93) to main (24c9c01e910eb566223961b00d3e2c3602c99a23).

---

[**What is this?**](https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades) | [Workflow run](https://github.com/psf/black/actions/runs/7751003586) | [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 4fd030d..a3ffba6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -16,7 +16,12 @@
 
 ### Configuration
 
-<!-- Changes to how Black can be configured -->
+- _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/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md
index dc9d9a6..61c5245 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/src/black/files.py b/src/black/files.py
index 1eb8745..960f13e 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()}
 


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
index 123ea0b..f876d36 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",
     )