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

Enhance --verbose #2526

Merged
merged 22 commits into from Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b9b9cb8
`--verbose` mode prints location of project root
Shivansh-007 Oct 2, 2021
96f4fba
Add changelog entry
Shivansh-007 Oct 8, 2021
d60fef5
Enhance verbose output to include relative path to root
Shivansh-007 Oct 15, 2021
27ac2f5
Update changelog to relfect change
Shivansh-007 Oct 15, 2021
4b92fd1
Merge branch 'main' into feat/project-root-verbose
Shivansh-007 Nov 2, 2021
7a5bac6
Remove redundant quotes while patching 'find_project_root'
Shivansh-007 Nov 2, 2021
44cfd6c
Document the new return value in docstring
Shivansh-007 Nov 2, 2021
b821ed8
Remove quotes from project root verbose statement
Shivansh-007 Nov 2, 2021
bd4fc39
Store project root in ctx.obj to avoid passing it around and make it …
Shivansh-007 Dec 11, 2021
e35c190
Merge remote-tracking branch 'upstream/main' into feat/project-root-v…
Shivansh-007 Dec 11, 2021
513c9ae
Don't normalize path in logs to root parent
Shivansh-007 Dec 11, 2021
8e312d6
Edit changelog to reflect new changes
Shivansh-007 Dec 11, 2021
4a81c23
Merge branch 'main' into feat/project-root-verbose
Shivansh-007 Dec 13, 2021
7317a37
Call resolve to remove relative paths when getting SRCs log
Shivansh-007 Dec 15, 2021
9d2ca6e
Move identified root log above using config log
Shivansh-007 Dec 17, 2021
12e41ba
Update docstring
Shivansh-007 Dec 17, 2021
baa73d6
Update changelog, make more explicit
Shivansh-007 Dec 17, 2021
6a5dcb4
Use normalize path ignore to resolve and format root
Shivansh-007 Dec 19, 2021
2f2fc40
Merge branch 'main' into feat/project-root-verbose
JelleZijlstra Dec 19, 2021
857eeb3
Fix tests, add a dummy root since most don't care about it
Shivansh-007 Dec 19, 2021
092675a
Return file system root as default value of `find_pyproject_toml`
Shivansh-007 Jan 10, 2022
c02122a
Merge branch 'main' into feat/project-root-verbose
Shivansh-007 Jan 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -22,6 +22,8 @@
`values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708)
- For stubs, one blank line between class attributes and methods is now kept if there's
at least one pre-existing blank line (#2736)
- Verbose mode also now describes how a project root was discovered and which paths will
be formatted. (#2526)

### Packaging

Expand Down
42 changes: 35 additions & 7 deletions src/black/__init__.py
Expand Up @@ -31,6 +31,7 @@
)

import click
from click.core import ParameterSource
from dataclasses import replace
from mypy_extensions import mypyc_attr

Expand Down Expand Up @@ -411,8 +412,37 @@ def main(
config: Optional[str],
) -> None:
"""The uncompromising code formatter."""
if config and verbose:
out(f"Using configuration from {config}.", bold=False, fg="blue")
ctx.ensure_object(dict)
root, method = find_project_root(src) if code is None else (None, None)
ctx.obj["root"] = root

if verbose:
if root:
out(
f"Identified `{root}` as project root containing a {method}.",
fg="blue",
)

normalized = [
(normalize_path_maybe_ignore(Path(source), root), source)
for source in src
]
srcs_string = ", ".join(
[
f'"{_norm}"'
if _norm
else f'\033[31m"{source} (skipping - invalid)"\033[34m'
for _norm, source in normalized
]
)
out(f"Sources to be formatted: {srcs_string}", fg="blue")

if config:
config_source = ctx.get_parameter_source("config")
if config_source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP):
out("Using configuration from project root.", fg="blue")
else:
out(f"Using configuration in '{config}'.", fg="blue")

error_msg = "Oh no! 💥 💔 💥"
if required_version and required_version != __version__:
Expand Down Expand Up @@ -516,14 +546,12 @@ def get_sources(
stdin_filename: Optional[str],
) -> Set[Path]:
"""Compute the set of files to be formatted."""
Shivansh-007 marked this conversation as resolved.
Show resolved Hide resolved

root = find_project_root(src)
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)
gitignore = get_gitignore(root)
gitignore = get_gitignore(ctx.obj["root"])
else:
gitignore = None

Expand All @@ -536,7 +564,7 @@ def get_sources(
is_stdin = False

if is_stdin or p.is_file():
normalized_path = normalize_path_maybe_ignore(p, root, report)
normalized_path = normalize_path_maybe_ignore(p, ctx.obj["root"], report)
if normalized_path is None:
continue

Expand All @@ -563,7 +591,7 @@ def get_sources(
sources.update(
gen_python_files(
p.iterdir(),
root,
ctx.obj["root"],
include,
exclude,
extend_exclude,
Expand Down
28 changes: 19 additions & 9 deletions src/black/files.py
Expand Up @@ -31,14 +31,18 @@


@lru_cache()
def find_project_root(srcs: Sequence[str]) -> Path:
def find_project_root(srcs: Sequence[str]) -> Tuple[Path, str]:
"""Return a directory containing .git, .hg, or pyproject.toml.
Shivansh-007 marked this conversation as resolved.
Show resolved Hide resolved

That directory will be a common parent of all files and directories
passed in `srcs`.

If no directory in the tree contains a marker that would specify it's the
project root, the root of the file system is returned.

Returns a two-tuple with the first element as the project root path and
the second element as a string describing the method by which the
project root was discovered.
"""
if not srcs:
srcs = [str(Path.cwd().resolve())]
Expand All @@ -58,20 +62,20 @@ def find_project_root(srcs: Sequence[str]) -> Path:

for directory in (common_base, *common_base.parents):
if (directory / ".git").exists():
return directory
return directory, ".git directory"

if (directory / ".hg").is_dir():
return directory
return directory, ".hg directory"

if (directory / "pyproject.toml").is_file():
return directory
return directory, "pyproject.toml"

return directory
return directory, "file system root"


def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
"""Find the absolute filepath to a pyproject.toml if it exists"""
path_project_root = find_project_root(path_search_start)
path_project_root, _ = find_project_root(path_search_start)
path_pyproject_toml = path_project_root / "pyproject.toml"
if path_pyproject_toml.is_file():
return str(path_pyproject_toml)
Expand Down Expand Up @@ -133,7 +137,9 @@ def get_gitignore(root: Path) -> PathSpec:


def normalize_path_maybe_ignore(
path: Path, root: Path, report: Report
path: Path,
root: Path,
report: Optional[Report] = None,
) -> Optional[str]:
"""Normalize `path`. May return `None` if `path` was ignored.

Expand All @@ -143,12 +149,16 @@ def normalize_path_maybe_ignore(
abspath = path if path.is_absolute() else Path.cwd() / path
normalized_path = abspath.resolve().relative_to(root).as_posix()
except OSError as e:
report.path_ignored(path, f"cannot be read because {e}")
if report:
report.path_ignored(path, f"cannot be read because {e}")
return None

except ValueError:
if path.is_symlink():
report.path_ignored(path, f"is a symbolic link that points outside {root}")
if report:
report.path_ignored(
path, f"is a symbolic link that points outside {root}"
)
return None

raise
Expand Down
34 changes: 23 additions & 11 deletions tests/test_black.py
Expand Up @@ -100,6 +100,8 @@ class FakeContext(click.Context):

def __init__(self) -> None:
self.default_map: Dict[str, Any] = {}
# Dummy root, since most of the tests don't care about it
self.obj: Dict[str, Any] = {"root": PROJECT_ROOT}


class FakeParameter(click.Parameter):
Expand Down Expand Up @@ -1350,10 +1352,17 @@ def test_find_project_root(self) -> None:
src_python.touch()

self.assertEqual(
black.find_project_root((src_dir, test_dir)), root.resolve()
black.find_project_root((src_dir, test_dir)),
(root.resolve(), "pyproject.toml"),
)
self.assertEqual(
black.find_project_root((src_dir,)),
(src_dir.resolve(), "pyproject.toml"),
)
self.assertEqual(
black.find_project_root((src_python,)),
(src_dir.resolve(), "pyproject.toml"),
)
self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())

@patch(
"black.files.find_user_pyproject_toml",
Expand Down Expand Up @@ -1756,6 +1765,7 @@ def assert_collected_sources(
src: Sequence[Union[str, Path]],
expected: Sequence[Union[str, Path]],
*,
ctx: Optional[FakeContext] = None,
exclude: Optional[str] = None,
include: Optional[str] = None,
extend_exclude: Optional[str] = None,
Expand All @@ -1771,7 +1781,7 @@ def assert_collected_sources(
)
gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
collected = black.get_sources(
ctx=FakeContext(),
ctx=ctx or FakeContext(),
src=gs_src,
quiet=False,
verbose=False,
Expand Down Expand Up @@ -1807,9 +1817,11 @@ def test_gitignore_used_as_default(self) -> None:
base / "b/.definitely_exclude/a.pyi",
]
src = [base / "b/"]
assert_collected_sources(src, expected, extend_exclude=r"/exclude/")
ctx = FakeContext()
ctx.obj["root"] = base
assert_collected_sources(src, expected, ctx=ctx, extend_exclude=r"/exclude/")

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_exclude_for_issue_1572(self) -> None:
# Exclude shouldn't touch files that were explicitly given to Black through the
# CLI. Exclude is supposed to only apply to the recursive discovery of files.
Expand Down Expand Up @@ -1992,13 +2004,13 @@ def test_symlink_out_of_root_directory(self) -> None:
child.is_symlink.assert_called()
assert child.is_symlink.call_count == 2

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin(self) -> None:
src = ["-"]
expected = ["-"]
assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py")

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin_filename(self) -> None:
src = ["-"]
stdin_filename = str(THIS_DIR / "data/collections.py")
Expand All @@ -2010,7 +2022,7 @@ def test_get_sources_with_stdin_filename(self) -> None:
stdin_filename=stdin_filename,
)

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
# Exclude shouldn't exclude stdin_filename since it is mimicking the
# file being passed directly. This is the same as
Expand All @@ -2026,7 +2038,7 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
stdin_filename=stdin_filename,
)

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
# Extend exclude shouldn't exclude stdin_filename since it is mimicking the
# file being passed directly. This is the same as
Expand All @@ -2042,7 +2054,7 @@ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
stdin_filename=stdin_filename,
)

@patch("black.find_project_root", lambda *args: THIS_DIR.resolve())
@patch("black.find_project_root", lambda *args: (THIS_DIR.resolve(), None))
def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
# Force exclude should exclude the file when passing it through
# stdin_filename
Expand Down