Skip to content

Commit

Permalink
Code Flag Options (#2259)
Browse files Browse the repository at this point in the history
Properly handles the diff, color, and fast option when black is run with
 the `--code` option.

Closes #2104, closes #1801.
  • Loading branch information
HassanAbouelela committed Jun 2, 2021
1 parent fdc4b67 commit 7567cdf
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 42 deletions.
1 change: 1 addition & 0 deletions AUTHORS.md
Expand Up @@ -68,6 +68,7 @@ Multiple contributions by:
- Gustavo Camargo
- hauntsaninja
- [Hadi Alqattan](mailto:alqattanhadizaki@gmail.com)
- [Hassan Abouelela](mailto:hassan@hassanamr.com)
- [Heaford](mailto:dan@heaford.com)
- [Hugo Barrera](mailto::hugo@barrera.io)
- Hugo van Kemenade
Expand Down
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -15,6 +15,7 @@
- Respect `.gitignore` files in all levels, not only `root/.gitignore` file (apply
`.gitignore` rules like `git` does) (#2225)
- Restored compatibility with Click 8.0 on Python 3.6 when LANG=C used (#2227)
- Fixed option usage when using the `--code` flag (#2259)
- Add extra uvloop install + import support if in python env (#2258)
- Fix --experimental-string-processing crash when matching parens are not found (#2283)
- Make sure to split lines that start with a string operator (#2286)
Expand Down Expand Up @@ -43,6 +44,7 @@
- Fix typos discovered by codespell (#2228)
- Fix Vim plugin installation instructions. (#2235)
- Add new Frequently Asked Questions page (#2247)
- Removed safety checks warning for the `--code` option (#2259)
- Fix encoding + symlink issues preventing proper build on Windows (#2262)

## 21.5b1
Expand Down
7 changes: 0 additions & 7 deletions docs/usage_and_configuration/the_basics.md
Expand Up @@ -68,13 +68,6 @@ $ black --code "print ( 'hello, world' )"
print("hello, world")
```

```{warning}
--check, --diff, and --safe / --fast have no effect when using -c / --code. Safety
checks normally turned on by default that verify _Black_'s output are disabled as well.
This is a bug which we intend to fix eventually. More details can be found in this [bug
report](https://github.com/psf/black/issues/2104).
```

### Writeback and reporting

By default _Black_ reformats the files given and/or found in place. Sometimes you need
Expand Down
119 changes: 85 additions & 34 deletions src/black/__init__.py
Expand Up @@ -384,47 +384,61 @@ def main(
)
if config and verbose:
out(f"Using configuration from {config}.", bold=False, fg="blue")

if code is not None:
print(format_str(code, mode=mode))
ctx.exit(0)
report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
sources = get_sources(
ctx=ctx,
src=src,
quiet=quiet,
verbose=verbose,
include=include,
exclude=exclude,
extend_exclude=extend_exclude,
force_exclude=force_exclude,
report=report,
stdin_filename=stdin_filename,
)
# Run in quiet mode by default with -c; the extra output isn't useful.
# You can still pass -v to get verbose output.
quiet = True

path_empty(
sources,
"No Python files are present to be formatted. Nothing to do 😴",
quiet,
verbose,
ctx,
)
report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)

if len(sources) == 1:
reformat_one(
src=sources.pop(),
fast=fast,
write_back=write_back,
mode=mode,
report=report,
if code is not None:
reformat_code(
content=code, fast=fast, write_back=write_back, mode=mode, report=report
)
else:
reformat_many(
sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
sources = get_sources(
ctx=ctx,
src=src,
quiet=quiet,
verbose=verbose,
include=include,
exclude=exclude,
extend_exclude=extend_exclude,
force_exclude=force_exclude,
report=report,
stdin_filename=stdin_filename,
)

path_empty(
sources,
"No Python files are present to be formatted. Nothing to do 😴",
quiet,
verbose,
ctx,
)

if len(sources) == 1:
reformat_one(
src=sources.pop(),
fast=fast,
write_back=write_back,
mode=mode,
report=report,
)
else:
reformat_many(
sources=sources,
fast=fast,
write_back=write_back,
mode=mode,
report=report,
)

if verbose or not quiet:
out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
click.secho(str(report), err=True)
if code is None:
click.secho(str(report), err=True)
ctx.exit(report.return_code)


Expand Down Expand Up @@ -512,6 +526,30 @@ def path_empty(
ctx.exit(0)


def reformat_code(
content: str, fast: bool, write_back: WriteBack, mode: Mode, report: Report
) -> None:
"""
Reformat and print out `content` without spawning child processes.
Similar to `reformat_one`, but for string content.
`fast`, `write_back`, and `mode` options are passed to
:func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
"""
path = Path("<string>")
try:
changed = Changed.NO
if format_stdin_to_stdout(
content=content, fast=fast, write_back=write_back, mode=mode
):
changed = Changed.YES
report.done(path, changed)
except Exception as exc:
if report.verbose:
traceback.print_exc()
report.failed(path, str(exc))


def reformat_one(
src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
) -> None:
Expand Down Expand Up @@ -720,16 +758,27 @@ def format_file_in_place(


def format_stdin_to_stdout(
fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
fast: bool,
*,
content: Optional[str] = None,
write_back: WriteBack = WriteBack.NO,
mode: Mode,
) -> bool:
"""Format file on stdin. Return True if changed.
If content is None, it's read from sys.stdin.
If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
write a diff to stdout. The `mode` argument is passed to
:func:`format_file_contents`.
"""
then = datetime.utcnow()
src, encoding, newline = decode_bytes(sys.stdin.buffer.read())

if content is None:
src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
else:
src, encoding, newline = content, "utf-8", ""

dst = src
try:
dst = format_file_contents(src, fast=fast, mode=mode)
Expand All @@ -743,6 +792,8 @@ def format_stdin_to_stdout(
sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True
)
if write_back == WriteBack.YES:
# Make sure there's a newline after the content
dst += "" if dst[-1] == "\n" else "\n"
f.write(dst)
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
now = datetime.utcnow()
Expand Down
2 changes: 1 addition & 1 deletion src/black/files.py
Expand Up @@ -38,7 +38,7 @@ def find_project_root(srcs: Sequence[str]) -> Path:
project root, the root of the file system is returned.
"""
if not srcs:
return Path("/").resolve()
srcs = [str(Path.cwd().resolve())]

path_srcs = [Path(Path.cwd(), src).resolve() for src in srcs]

Expand Down
144 changes: 144 additions & 0 deletions tests/test_black.py
Expand Up @@ -34,6 +34,7 @@
from black import Feature, TargetVersion
from black.cache import get_cache_file
from black.debug import DebugVisitor
from black.output import diff, color_diff
from black.report import Report
import black.files

Expand Down Expand Up @@ -63,6 +64,9 @@
T = TypeVar("T")
R = TypeVar("R")

# Match the time output in a diff, but nothing else
DIFF_TIME = re.compile(r"\t[\d-:+\. ]+")


@contextmanager
def cache_dir(exists: bool = True) -> Iterator[Path]:
Expand Down Expand Up @@ -2069,6 +2073,146 @@ def test_docstring_reformat_for_py27(self) -> None:
actual = result.output
self.assertFormatEqual(actual, expected)

@staticmethod
def compare_results(
result: click.testing.Result, expected_value: str, expected_exit_code: int
) -> None:
"""Helper method to test the value and exit code of a click Result."""
assert (
result.output == expected_value
), "The output did not match the expected value."
assert result.exit_code == expected_exit_code, "The exit code is incorrect."

def test_code_option(self) -> None:
"""Test the code option with no changes."""
code = 'print("Hello world")\n'
args = ["--code", code]
result = CliRunner().invoke(black.main, args)

self.compare_results(result, code, 0)

def test_code_option_changed(self) -> None:
"""Test the code option when changes are required."""
code = "print('hello world')"
formatted = black.format_str(code, mode=DEFAULT_MODE)

args = ["--code", code]
result = CliRunner().invoke(black.main, args)

self.compare_results(result, formatted, 0)

def test_code_option_check(self) -> None:
"""Test the code option when check is passed."""
args = ["--check", "--code", 'print("Hello world")\n']
result = CliRunner().invoke(black.main, args)
self.compare_results(result, "", 0)

def test_code_option_check_changed(self) -> None:
"""Test the code option when changes are required, and check is passed."""
args = ["--check", "--code", "print('hello world')"]
result = CliRunner().invoke(black.main, args)
self.compare_results(result, "", 1)

def test_code_option_diff(self) -> None:
"""Test the code option when diff is passed."""
code = "print('hello world')"
formatted = black.format_str(code, mode=DEFAULT_MODE)
result_diff = diff(code, formatted, "STDIN", "STDOUT")

args = ["--diff", "--code", code]
result = CliRunner().invoke(black.main, args)

# Remove time from diff
output = DIFF_TIME.sub("", result.output)

assert output == result_diff, "The output did not match the expected value."
assert result.exit_code == 0, "The exit code is incorrect."

def test_code_option_color_diff(self) -> None:
"""Test the code option when color and diff are passed."""
code = "print('hello world')"
formatted = black.format_str(code, mode=DEFAULT_MODE)

result_diff = diff(code, formatted, "STDIN", "STDOUT")
result_diff = color_diff(result_diff)

args = ["--diff", "--color", "--code", code]
result = CliRunner().invoke(black.main, args)

# Remove time from diff
output = DIFF_TIME.sub("", result.output)

assert output == result_diff, "The output did not match the expected value."
assert result.exit_code == 0, "The exit code is incorrect."

def test_code_option_safe(self) -> None:
"""Test that the code option throws an error when the sanity checks fail."""
# Patch black.assert_equivalent to ensure the sanity checks fail
with patch.object(black, "assert_equivalent", side_effect=AssertionError):
code = 'print("Hello world")'
error_msg = f"{code}\nerror: cannot format <string>: \n"

args = ["--safe", "--code", code]
result = CliRunner().invoke(black.main, args)

self.compare_results(result, error_msg, 123)

def test_code_option_fast(self) -> None:
"""Test that the code option ignores errors when the sanity checks fail."""
# Patch black.assert_equivalent to ensure the sanity checks fail
with patch.object(black, "assert_equivalent", side_effect=AssertionError):
code = 'print("Hello world")'
formatted = black.format_str(code, mode=DEFAULT_MODE)

args = ["--fast", "--code", code]
result = CliRunner().invoke(black.main, args)

self.compare_results(result, formatted, 0)

def test_code_option_config(self) -> None:
"""
Test that the code option finds the pyproject.toml in the current directory.
"""
with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
# Make sure we are in the project root with the pyproject file
if not Path("tests").exists():
os.chdir("..")

args = ["--code", "print"]
CliRunner().invoke(black.main, args)

pyproject_path = Path(Path().cwd(), "pyproject.toml").resolve()
assert (
len(parse.mock_calls) >= 1
), "Expected config parse to be called with the current directory."

_, call_args, _ = parse.mock_calls[0]
assert (
call_args[0].lower() == str(pyproject_path).lower()
), "Incorrect config loaded."

def test_code_option_parent_config(self) -> None:
"""
Test that the code option finds the pyproject.toml in the parent directory.
"""
with patch.object(black, "parse_pyproject_toml", return_value={}) as parse:
# Make sure we are in the tests directory
if Path("tests").exists():
os.chdir("tests")

args = ["--code", "print"]
CliRunner().invoke(black.main, args)

pyproject_path = Path(Path().cwd().parent, "pyproject.toml").resolve()
assert (
len(parse.mock_calls) >= 1
), "Expected config parse to be called with the current directory."

_, call_args, _ = parse.mock_calls[0]
assert (
call_args[0].lower() == str(pyproject_path).lower()
), "Incorrect config loaded."


with open(black.__file__, "r", encoding="utf-8") as _bf:
black_source_lines = _bf.readlines()
Expand Down

0 comments on commit 7567cdf

Please sign in to comment.