Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a stdin-filename to allow stdin to respect force-exclude rules #1780

Merged
merged 10 commits into from Nov 13, 2020
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -135,6 +135,11 @@ Options:
matching this regex will be excluded even
when they are passed explicitly as arguments.

--stdin-filename TEXT The name of the file when passing it through
stdin. Useful to make sure black will respect
bellini666 marked this conversation as resolved.
Show resolved Hide resolved
--force-exclude option on some editors that
rely on using stdin.

-q, --quiet Don't emit non-error messages to stderr.
Errors are still emitted; silence those with
2>/dev/null.
Expand Down
42 changes: 37 additions & 5 deletions src/black/__init__.py
Expand Up @@ -68,6 +68,7 @@
DEFAULT_EXCLUDES = r"/(\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|\.svn|_build|buck-out|build|dist)/" # noqa: B950
DEFAULT_INCLUDES = r"\.pyi?$"
CACHE_DIR = Path(user_cache_dir("black", version=__version__))
STDIN_PLACEHOLDER = "__BLACK_STDIN_FILENAME__"

STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters.

Expand Down Expand Up @@ -457,6 +458,15 @@ def target_version_option_callback(
"excluded even when they are passed explicitly as arguments."
),
)
@click.option(
"--stdin-filename",
type=str,
help=(
"The name of the file when passing it through stdin. Useful to make "
"sure black will respect --force-exclude option on some "
bellini666 marked this conversation as resolved.
Show resolved Hide resolved
"editors that rely on using stdin."
),
)
@click.option(
"-q",
"--quiet",
Expand Down Expand Up @@ -516,6 +526,7 @@ def main(
include: str,
exclude: str,
force_exclude: Optional[str],
stdin_filename: Optional[str],
src: Tuple[str, ...],
config: Optional[str],
) -> None:
Expand Down Expand Up @@ -548,6 +559,7 @@ def main(
exclude=exclude,
force_exclude=force_exclude,
report=report,
stdin_filename=stdin_filename,
)

path_empty(
Expand Down Expand Up @@ -587,6 +599,7 @@ def get_sources(
exclude: str,
force_exclude: Optional[str],
report: "Report",
stdin_filename: Optional[str],
) -> Set[Path]:
"""Compute the set of files to be formatted."""
try:
Expand All @@ -613,7 +626,13 @@ def get_sources(
gitignore = get_gitignore(root)

for s in src:
p = Path(s)
if s == "-" and stdin_filename:
p = Path(stdin_filename)
is_stdin = True
else:
p = Path(s)
is_stdin = False

if p.is_dir():
bellini666 marked this conversation as resolved.
Show resolved Hide resolved
sources.update(
gen_python_files(
Expand All @@ -626,9 +645,7 @@ def get_sources(
gitignore,
)
)
elif s == "-":
sources.add(p)
elif p.is_file():
elif p.is_file() or is_stdin:
bellini666 marked this conversation as resolved.
Show resolved Hide resolved
normalized_path = normalize_path_maybe_ignore(p, root, report)
if normalized_path is None:
continue
Expand All @@ -643,6 +660,11 @@ def get_sources(
report.path_ignored(p, "matches the --force-exclude regular expression")
continue

if is_stdin:
p = Path(f"{STDIN_PLACEHOLDER}{str(p)}")

sources.add(p)
elif s == "-":
sources.add(p)
else:
err(f"invalid path: {s}")
Expand Down Expand Up @@ -670,7 +692,17 @@ def reformat_one(
"""
try:
changed = Changed.NO
if not src.is_file() and str(src) == "-":
is_stdin = False

if str(src) == "-":
is_stdin = True
elif str(src).startswith(STDIN_PLACEHOLDER):
is_stdin = True
# Use the original name again in case we want to print something
# to the user
src = Path(str(src)[len(STDIN_PLACEHOLDER) :])

if not src.is_file() and is_stdin:
bellini666 marked this conversation as resolved.
Show resolved Hide resolved
if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode):
changed = Changed.YES
else:
Expand Down
135 changes: 135 additions & 0 deletions tests/test_black.py
Expand Up @@ -1730,10 +1730,145 @@ def test_exclude_for_issue_1572(self) -> None:
exclude=exclude,
force_exclude=None,
report=report,
stdin_filename=None,
)
)
self.assertEqual(sorted(expected), sorted(sources))

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
def test_get_sources_with_stdin(self) -> None:
include = ""
exclude = r"/exclude/|a\.py"
src = "-"
report = black.Report()
expected = [Path("-")]
sources = list(
black.get_sources(
ctx=FakeContext(),
src=(src,),
quiet=True,
verbose=False,
include=include,
exclude=exclude,
force_exclude=None,
report=report,
stdin_filename=None,
)
)
self.assertEqual(sorted(expected), sorted(sources))

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
def test_get_sources_with_stdin_filename(self) -> None:
include = ""
exclude = r"/exclude/|a\.py"
src = "-"
report = black.Report()
stdin_filename = str(THIS_DIR / "data/collections.py")
expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
sources = list(
black.get_sources(
ctx=FakeContext(),
src=(src,),
quiet=True,
verbose=False,
include=include,
exclude=exclude,
force_exclude=None,
report=report,
stdin_filename=stdin_filename,
)
)
self.assertEqual(sorted(expected), sorted(sources))

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
# Exclude shouldn't exclude stdin_filename since it is mimicing the
# file being passed directly. This is the same as
# test_exclude_for_issue_1572
path = THIS_DIR / "data" / "include_exclude_tests"
include = ""
exclude = r"/exclude/|a\.py"
src = "-"
report = black.Report()
stdin_filename = str(path / "b/exclude/a.py")
expected = [Path(f"__BLACK_STDIN_FILENAME__{stdin_filename}")]
sources = list(
black.get_sources(
ctx=FakeContext(),
src=(src,),
quiet=True,
verbose=False,
include=include,
exclude=exclude,
force_exclude=None,
report=report,
stdin_filename=stdin_filename,
)
)
self.assertEqual(sorted(expected), sorted(sources))

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
# Force exclude should exclude the file when passing it through
# stdin_filename
path = THIS_DIR / "data" / "include_exclude_tests"
include = ""
force_exclude = r"/exclude/|a\.py"
src = "-"
report = black.Report()
stdin_filename = str(path / "b/exclude/a.py")
sources = list(
black.get_sources(
ctx=FakeContext(),
src=(src,),
quiet=True,
verbose=False,
include=include,
exclude="",
force_exclude=force_exclude,
report=report,
stdin_filename=stdin_filename,
)
)
self.assertEqual([], sorted(sources))

def test_reformat_one_with_stdin(self) -> None:
with patch(
"black.format_stdin_to_stdout",
return_value=lambda *args, **kwargs: black.Changed.YES,
) as fsts:
report = MagicMock()
path = Path("-")
black.reformat_one(
path,
fast=True,
write_back=black.WriteBack.YES,
mode=DEFAULT_MODE,
report=report,
)
fsts.assert_called_once()
report.done.assert_called_with(path, black.Changed.YES)

def test_reformat_one_with_stdin_filename(self) -> None:
with patch(
"black.format_stdin_to_stdout",
return_value=lambda *args, **kwargs: black.Changed.YES,
) as fsts:
report = MagicMock()
p = "foo.py"
path = Path(f"__BLACK_STDIN_FILENAME__{p}")
expected = Path(p)
black.reformat_one(
path,
fast=True,
write_back=black.WriteBack.YES,
mode=DEFAULT_MODE,
report=report,
)
fsts.assert_called_once()
# __BLACK_STDIN_FILENAME__ should have been striped
report.done.assert_called_with(expected, black.Changed.YES)

def test_gitignore_exclude(self) -> None:
path = THIS_DIR / "data" / "include_exclude_tests"
include = re.compile(r"\.pyi?$")
Expand Down