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:
I'm giving this a bump because of our stable efforts.

The way I see it, this is not very impactful because arguably it's the user's problem if they try to pass code *and* format files. But I like correctness, and even my optimal suggestion could be fairly easy to implement. So any thoughts?
Your suggested change seems fine, thanks for bringing this up! I don't think it's urgent though.
At the same time we might have to reconsider what we claim to be Black's basic CLI behavior. The current (relevant) items [on our docs](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#the-basics) are:

- it does nothing if no sources are passed to it;
- it will read from standard input and write to standard output if - is used as the filename;
- exits with code 0 unless an internal error occurred (or --check was used).

This is problematic, not least because we definitely exit with error codes if arguments are invalid and might do so in other cases as well. So firstly I might argue that we *should* exit with non-zero on user errors (as we do already). Extending that, and keeping in mind previous discussions about running Black without arguments (#704, #740, #861), I'd argue that an invocation without arguments *should* be considered a user error. Allowing no sources but making Black do nothing is a bit confusing. It's a different story if a source is passed but no Python files are found.

Quick question: historically, why does `--code` exist when stdin can be used in similar situations? I might just not be able to think of the use cases here.

Anyways, *given* that we'll keep `--code` I'd propose the following:

- it fails if no sources are passed to it;
- it will read from stdin if `-` is passed as source
- it exits with 0 on success, 1 on fails, user errors and `--check` with reformats, 123 on internal error
- it treats `SRC ...` items as lines of code if `--code` was passed (new with this issue)

Is this too dense? 😄
After testing things: having SRC be also code is pretty hard and annoying to pull off, because we have a bunch of path validation and automatic config extraction. Not to mention that it can be a bit confusing to users. So let's stick with two arguments and just print a proper usage error message with Click ourselves.

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- 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
--- 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: