Repository: psf/black

Issue:
Make SRC/CODE mandatory and mutually exclusive
**Is your feature request related to a problem? Please describe.**
From #2356, which made (assuming it got merged) SRC appear as a mandatory CLI parameter: Strictly speaking, SRC is optional both in the way that Black accepts no arguments (and then prints "nothing to do") and with `--code ""`. I think accepting SRC and then skipping it when `-c` is passed is a bit confusing.

**Describe the solution you'd like**
I'll propose we make `SRC` and `--code` mutually exclusive and require one of them.

So, something like this:
```
> black
BLACK IS MAD: specify one of SRC / --code

> black --code "" .
BLACK IS FURIOUS: are you trying to confuse me?
```

I wonder if Click has some way of documenting call modes. Maybe something like:

```
> black --help

black [opts] src
black [opts] --code CODE

Options:
...
```

The "nothing to do" message could be emitted when the supplied path doesn't contain any valid files, like it is currently.

*Ideally* though, we would have a flag to turn the source to code I think. If I'm thinking about it from a documentation perspective, it would be much easier to explain:

```
> black --help

black [opts] src

SRC     Sources to be formatted as directories or files.
        If --code was passed, treat SRC as Python text.

Options:
code    Treat SRC as Python text.
```

I don't know what multiple sources of Python text would do then. Maybe different lines, or we could require only one source in that case. Anyways, this is kind of an addon to my proposition, not the main thing.

**Additional context**
Also, relevant discussion in #861: SRC is currently undocumented on the CLI help. We should definitely document it while we're at it (if it doesn't get documented earlier).

Discussion:
github-actions[bot]: **diff-shades** reports zero changes comparing this PR (5cb3d19ef816753374f9b2af18a277523338648b) to main (3905173cb32922b580bad184e724586f359c8c7e).

---

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

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 634db79..458d48c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -38,6 +38,7 @@
   `--preview` (#2789)
 - Enable Python 3.10+ by default, without any extra need to specify
   `--target-version=py310`. (#2758)
+- Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804)
 
 ### Packaging
 
diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md
index fd39b6c..b82cef4 100644
--- a/docs/usage_and_configuration/the_basics.md
+++ b/docs/usage_and_configuration/the_basics.md
@@ -4,11 +4,11 @@ Foundational knowledge on using and configuring Black.
 
 _Black_ is a well-behaved Unix-style command-line tool:
 
-- it does nothing if no sources are passed to it;
+- it does nothing if it finds no sources to format;
 - it will read from standard input and write to standard output if `-` is used as the
   filename;
 - it only outputs messages to users on standard error;
-- exits with code 0 unless an internal error occurred (or `--check` was used).
+- exits with code 0 unless an internal error occurred or a CLI option prompted it.
 
 ## Usage
 
diff --git a/src/black/__init__.py b/src/black/__init__.py
index eaf72f9..7024c9d 100644
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -431,6 +431,17 @@ def main(
 ) -> None:
     """The uncompromising code formatter."""
     ctx.ensure_object(dict)
+
+    if src and code is not None:
+        out(
+            main.get_usage(ctx)
+            + "\n\n'SRC' and 'code' cannot be passed simultaneously."
+        )
+        ctx.exit(1)
+    if not src and code is None:
+        out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.")
+        ctx.exit(1)
+
     root, method = find_project_root(src) if code is None else (None, None)
     ctx.obj["root"] = root
 
@@ -569,7 +580,6 @@ def get_sources(
 ) -> Set[Path]:
     """Compute the set of files to be formatted."""
     sources: Set[Path] = set()
-    path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
 
     if exclude is None:
         exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES)


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
index 5596909..8d691d2 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -972,10 +972,13 @@ def test_check_diff_use_together(self) -> None:
             # Multi file command.
             self.invokeBlack([str(src1), str(src2), "--diff", "--check"], exit_code=1)
 
-    def test_no_files(self) -> None:
+    def test_no_src_fails(self) -> None:
         with cache_dir():
-            # Without an argument, black exits with error code 0.
-            self.invokeBlack([])
+            self.invokeBlack([], exit_code=1)
+
+    def test_src_and_code_fails(self) -> None:
+        with cache_dir():
+            self.invokeBlack([".", "-c", "0"], exit_code=1)
 
     def test_broken_symlink(self) -> None:
         with cache_dir() as workspace:
@@ -1229,13 +1232,18 @@ def test_invalid_cli_regex(self) -> None:
 
     def test_required_version_matches_version(self) -> None:
         self.invokeBlack(
-            ["--required-version", black.__version__], exit_code=0, ignore_config=True
+            ["--required-version", black.__version__, "-c", "0"],
+            exit_code=0,
+            ignore_config=True,
         )
 
     def test_required_version_does_not_match_version(self) -> None:
-        self.invokeBlack(
-            ["--required-version", "20.99b"], exit_code=1, ignore_config=True
+        result = BlackRunner().invoke(
+            black.main,
+            ["--required-version", "20.99b", "-c", "0"],
         )
+        self.assertEqual(result.exit_code, 1)
+        self.assertIn("required version", result.stderr)
 
     def test_preserves_line_endings(self) -> None:
         with TemporaryDirectory() as workspace: