pw_presubmit: Allow customizing black

Allow customizing the executable used for black. Also, explicitly
specify a config file for black if one is present in a couple locations.
It now looks at the following locations, in order:

* $PW_PROJECT_ROOT/.black.toml
* $PW_PROJECT_ROOT/pyproject.toml
* $PW_ROOT/.black.toml
* $PW_ROOT/pyproject.toml

Bug: b/264578594
Change-Id: Ic1ea174ca286b6f7ffd262da3dea4d83bff27c99
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126211
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
diff --git a/pyproject.toml b/.black.toml
similarity index 94%
rename from pyproject.toml
rename to .black.toml
index 8bc1b1e..c9c0c31 100644
--- a/pyproject.toml
+++ b/.black.toml
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
 #
 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
 # use this file except in compliance with the License. You may obtain a copy of
diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst
index 2fcde5b..f86a132 100644
--- a/pw_presubmit/docs.rst
+++ b/pw_presubmit/docs.rst
@@ -171,7 +171,7 @@
 ``pw_presubmit.format_code``. These include C/C++, Java, Go, Python, GN, and
 others. All of these checks can be included by adding
 ``pw_presubmit.format_code.presubmit_checks()`` to a presubmit program. These
-all use language-specific formatters like clang-format or yapf.
+all use language-specific formatters like clang-format or black.
 
 These will suggest fixes using ``pw format --fix``.
 
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index eacac23..596808c 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -39,6 +39,7 @@
     NamedTuple,
     Optional,
     Pattern,
+    Sequence,
     TextIO,
     Tuple,
     Union,
@@ -283,22 +284,37 @@
     return {}
 
 
-_BLACK_OPTS = (
-    '--skip-string-normalization',
-    '--line-length',
-    '80',
-    '--target-version',
-    'py310',
-    '--include',
-    r'\.pyi?$',
-)
+BLACK = 'black'
+
+
+def _enumerate_black_configs() -> Iterable[Path]:
+    if directory := os.environ.get('PW_PROJECT_ROOT'):
+        yield Path(directory, '.black.toml')
+        yield Path(directory, 'pyproject.toml')
+
+    if directory := os.environ.get('PW_ROOT'):
+        yield Path(directory, '.black.toml')
+        yield Path(directory, 'pyproject.toml')
+
+
+def _black_config_args() -> Sequence[Union[str, Path]]:
+    config = None
+    for config_location in _enumerate_black_configs():
+        if config_location.is_file():
+            config = config_location
+            break
+
+    config_args: Sequence[Union[str, Path]] = ()
+    if config:
+        config_args = ('--config', config)
+    return config_args
 
 
 def _black_multiple_files(ctx: _Context) -> Tuple[str, ...]:
     changed_paths: List[str] = []
     for line in (
         log_run(
-            ['black', '--check', *_BLACK_OPTS, *ctx.paths],
+            [BLACK, '--check', *_black_config_args(), *ctx.paths],
             capture_output=True,
         )
         .stderr.decode()
@@ -325,7 +341,10 @@
             build = Path(temp) / os.path.basename(path)
             build.write_bytes(data)
 
-            proc = log_run(['black', *_BLACK_OPTS, build], capture_output=True)
+            proc = log_run(
+                [BLACK, *_black_config_args(), build],
+                capture_output=True,
+            )
             if proc.returncode:
                 stderr = proc.stderr.decode(errors='replace')
                 stderr = stderr.replace(str(build), str(path))
@@ -352,7 +371,10 @@
         if not str(path).endswith(paths):
             continue
 
-        proc = log_run(['black', *_BLACK_OPTS, path], capture_output=True)
+        proc = log_run(
+            [BLACK, *_black_config_args(), path],
+            capture_output=True,
+        )
         if proc.returncode:
             errors[path] = proc.stderr.decode()
     return errors
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index be287d1..fc13943 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -647,7 +647,7 @@
 SOURCE_FILES_FILTER = presubmit.FileFilter(
     endswith=_GN_SOURCES_IN_BUILD,
     suffix=('.bazel', '.bzl', '.gn', '.gni'),
-    exclude=(r'zephyr.*/', r'android.*/', r'^pyproject.toml'),
+    exclude=(r'zephyr.*/', r'android.*/', r'^(.black|pyproject).toml'),
 )