Repository: psf/black

Issue:
Allow specifying `--workers` via environment variable
**Is your feature request related to a problem? Please describe.**

I ran into https://github.com/psf/black/issues/2513 in my CI system

While I can hardcode `--workers` into my invocations of black (in this case, via pre-commit), it's a little unnatural, since the number of cores differs between CI workers and say locally. E.g., I can hardcode the minimum of them, but then we leave some perf on the table.

**Describe the solution you'd like**

I'd like to specify it by environment variable, say `BLACK_NUM_WORKERS`. This isn't unprecedented, e.g. `BLACK_CACHE_DIR` exists. This does increase configuration complexity a little.

**Describe alternatives you've considered**

Just hardcoding `--workers`. It's not the biggest deal.

Discussion:
github-actions[bot]: **diff-shades** reports zero changes comparing this PR (e1de180ea3a0294414e12fe265cf5b144eb86503) to main (d1248ca9beaf0ba526d265f4108836d89cf551b7).

---

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

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index fd4d911..9717867 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -25,12 +25,18 @@
 
 <!-- Changes to how Black can be configured -->
 
+- The `--workers` argument to Black can now be specified via the `BLACK_NUM_WORKERS`
+  environment variable (#3743)
 - `.pytest_cache`, `.ruff_cache` and `.vscode` are now excluded by default (#3691)
+- Fix black not honouring `pyproject.toml` settings when running `--stdin-filename` and
+  the `pyproject.toml` found isn't in the current working directory (#3719)
 
 ### Packaging
 
 <!-- Changes to how Black is packaged, such as dependency requirements -->
 
+- Upgrade mypyc from 0.991 to 1.3 (#3697)
+
 ### Parser
 
 <!-- Changes to the parser or to version autodetection -->
diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md
index 6f3a3cf..2a46148 100644
--- a/docs/usage_and_configuration/the_basics.md
+++ b/docs/usage_and_configuration/the_basics.md
@@ -246,7 +246,8 @@ respect the `--force-exclude` option on some editors that rely on using stdin.
 #### `-W`, `--workers`
 
 When _Black_ formats multiple files, it may use a process pool to speed up formatting.
-This option controls the number of parallel workers.
+This option controls the number of parallel workers. This can also be specified via the
+`BLACK_NUM_WORKERS` environment variable.
 
 #### `-q`, `--quiet`
 
@@ -296,6 +297,19 @@ Read configuration options from a configuration file. See
 
 Show available command-line options and exit.
 
+### Environment variable options
+
+_Black_ supports the following configuration via environment variables.
+
+#### `BLACK_CACHE_DIR`
+
+The directory where _Black_ should store its cache.
+
+#### `BLACK_NUM_WORKERS`
+
+The number of parallel workers _Black_ should use. The command line option `-W` /
+`--workers` takes precedence over this environment variable.
+
 ### Code input alternatives
 
 _Black_ supports formatting code via stdin, with the result being printed to stdout.
diff --git a/pyproject.toml b/pyproject.toml
index 6803a62..d44623f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -119,8 +119,8 @@ sources = ["src"]
 [tool.hatch.build.targets.wheel.hooks.mypyc]
 enable-by-default = false
 dependencies = [
-  "hatch-mypyc>=0.13.0",
-  "mypy==0.991",
+  "hatch-mypyc>=0.16.0",
+  "mypy==1.3",
   # Required stubs to be removed when the packages support PEP 561 themselves
   "types-typed-ast>=1.4.2",
 ]
diff --git a/src/black/__init__.py b/src/black/__init__.py
index dbcb559..3451c86 100644
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -127,7 +127,9 @@ def read_pyproject_toml(
     otherwise.
     """
     if not value:
-        value = find_pyproject_toml(ctx.params.get("src", ()))
+        value = find_pyproject_toml(
+            ctx.params.get("src", ()), ctx.params.get("stdin_filename", None)
+        )
         if value is None:
             return None
 
@@ -362,6 +364,7 @@ def validate_regex(
 @click.option(
     "--stdin-filename",
     type=str,
+    is_eager=True,
     help=(
         "The name of the file when passing it through stdin. Useful to make "
         "sure Black will respect --force-exclude option on some "
@@ -373,7 +376,10 @@ def validate_regex(
     "--workers",
     type=click.IntRange(min=1),
     default=None,
-    help="Number of parallel workers [default: number of CPUs in the system]",
+    help=(
+        "Number of parallel workers [default: BLACK_NUM_WORKERS environment variable "
+        "or number of CPUs in the system]"
+    ),
 )
 @click.option(
     "-q",
diff --git a/src/black/concurrency.py b/src/black/concurrency.py
index 1598f51..893eba6 100644
--- a/src/black/concurrency.py
+++ b/src/black/concurrency.py
@@ -80,7 +80,8 @@ def reformat_many(
 
     executor: Executor
     if workers is None:
-        workers = os.cpu_count() or 1
+        workers = int(os.environ.get("BLACK_NUM_WORKERS", 0))
+        workers = workers or os.cpu_count() or 1
     if sys.platform == "win32":
         # Work around https://bugs.python.org/issue26903
         workers = min(workers, 60)
diff --git a/src/black/files.py b/src/black/files.py
index 8c01311..65b2d0a 100644
--- a/src/black/files.py
+++ b/src/black/files.py
@@ -89,9 +89,11 @@ def find_project_root(
     return directory, "file system root"
 
 
-def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
+def find_pyproject_toml(
+    path_search_start: Tuple[str, ...], stdin_filename: Optional[str] = None
+) -> Optional[str]:
     """Find the absolute filepath to a pyproject.toml if it exists"""
-    path_project_root, _ = find_project_root(path_search_start)
+    path_project_root, _ = find_project_root(path_search_start, stdin_filename)
     path_pyproject_toml = path_project_root / "pyproject.toml"
     if path_pyproject_toml.is_file():
         return str(path_pyproject_toml)


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
index 5f2e6f5..abb304a 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -104,6 +104,7 @@ class FakeContext(click.Context):
 
     def __init__(self) -> None:
         self.default_map: Dict[str, Any] = {}
+        self.params: Dict[str, Any] = {}
         # Dummy root, since most of the tests don't care about it
         self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}
 
@@ -1620,6 +1621,39 @@ def test_read_pyproject_toml(self) -> None:
         self.assertEqual(config["exclude"], r"\.pyi?$")
         self.assertEqual(config["include"], r"\.py?$")
 
+    def test_read_pyproject_toml_from_stdin(self) -> None:
+        with TemporaryDirectory() as workspace:
+            root = Path(workspace)
+
+            src_dir = root / "src"
+            src_dir.mkdir()
+
+            src_pyproject = src_dir / "pyproject.toml"
+            src_pyproject.touch()
+
+            test_toml_file = THIS_DIR / "test.toml"
+            src_pyproject.write_text(test_toml_file.read_text())
+
+            src_python = src_dir / "foo.py"
+            src_python.touch()
+
+            fake_ctx = FakeContext()
+            fake_ctx.params["src"] = ("-",)
+            fake_ctx.params["stdin_filename"] = str(src_python)
+
+            with change_directory(root):
+                black.read_pyproject_toml(fake_ctx, FakeParameter(), None)
+
+            config = fake_ctx.default_map
+            self.assertEqual(config["verbose"], "1")
+            self.assertEqual(config["check"], "no")
+            self.assertEqual(config["diff"], "y")
+            self.assertEqual(config["color"], "True")
+            self.assertEqual(config["line_length"], "79")
+            self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
+            self.assertEqual(config["exclude"], r"\.pyi?$")
+            self.assertEqual(config["include"], r"\.py?$")
+
     @pytest.mark.incompatible_with_mypyc
     def test_find_project_root(self) -> None:
         with TemporaryDirectory() as workspace: