Skip to content

Commit

Permalink
Fix false symlink detection claims in verbose output (#3385)
Browse files Browse the repository at this point in the history
When trying to format a project from the outside, the verbose output
shows says that there are symbolic links that points outside of the
project, but displays the wrong project path, meaning that these
messages are false positives.

This bug is triggered when the command is executed from outside a
project on a folder inside it, causing an inconsistency between the
path to the detected project root and the relative path to the target
contents.

The fix is to normalize the target path using the project root before
processing the sources, which removes the presence of the incorrect
messages.

---

The test attemps to emulate the behavior of the CLI as closely as
posible by patching some `pathlib.Path` methods and passing certain
reference paths to the context object and `black.get_sources`.

Before the associated fix was introduced, this test failed because
some of the captured files reported the presence of a symlink due to
an incorrectly formated path. The test also asserts that only a single
file is reported as ignored, which is part of the expected behavior.

Signed-off-by: Antonio Ossa Guerra <[email protected]>
  • Loading branch information
aaossa authored Jan 19, 2023
1 parent 7e6d3fa commit 18fb884
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@

- Verbose logging now shows the values of `pyproject.toml` configuration variables
(#3392)
- Fix false symlink detection messages in verbose output due to using an incorrect
relative path to the project root (#3385)

### _Blackd_

Expand Down
3 changes: 2 additions & 1 deletion src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,10 +673,11 @@ def get_sources(

sources.add(p)
elif p.is_dir():
p = root / normalize_path_maybe_ignore(p, ctx.obj["root"], report)
if using_default_exclude:
gitignore = {
root: root_gitignore,
root / p: get_gitignore(p),
p: get_gitignore(p),
}
sources.update(
gen_python_files(
Expand Down
47 changes: 47 additions & 0 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,53 @@ def test_tab_comment_indentation(self) -> None:
self.assertFormatEqual(contents_spc, fs(contents_spc))
self.assertFormatEqual(contents_spc, fs(contents_tab))

def test_false_positive_symlink_output_issue_3384(self) -> None:
# Emulate the behavior when using the CLI (`black ./child --verbose`), which
# involves patching some `pathlib.Path` methods. In particular, `is_dir` is
# patched only on its first call: when checking if "./child" is a directory it
# should return True. The "./child" folder exists relative to the cwd when
# running from CLI, but fails when running the tests because cwd is different
project_root = Path(THIS_DIR / "data" / "nested_gitignore_tests")
working_directory = project_root / "root"
target_abspath = working_directory / "child"
target_contents = (
src.relative_to(working_directory) for src in target_abspath.iterdir()
)

def mock_n_calls(responses: List[bool]) -> Callable[[], bool]:
def _mocked_calls() -> bool:
if responses:
return responses.pop(0)
return False

return _mocked_calls

with patch("pathlib.Path.iterdir", return_value=target_contents), patch(
"pathlib.Path.cwd", return_value=working_directory
), patch("pathlib.Path.is_dir", side_effect=mock_n_calls([True])):
ctx = FakeContext()
ctx.obj["root"] = project_root
report = MagicMock(verbose=True)
black.get_sources(
ctx=ctx,
src=("./child",),
quiet=False,
verbose=True,
include=DEFAULT_INCLUDE,
exclude=None,
report=report,
extend_exclude=None,
force_exclude=None,
stdin_filename=None,
)
assert not any(
mock_args[1].startswith("is a symbolic link that points outside")
for _, mock_args, _ in report.path_ignored.mock_calls
), "A symbolic link was reported."
report.path_ignored.assert_called_once_with(
Path("child", "b.py"), "matches a .gitignore file content"
)

def test_report_verbose(self) -> None:
report = Report(verbose=True)
out_lines = []
Expand Down

0 comments on commit 18fb884

Please sign in to comment.