Enhance --verbose (#2526)

Black would now echo the location that it determined as the root path
for the project if `--verbose` is enabled by the user, according to
which it chooses the SRC paths, i.e. the absolute path of the project
is `{root}/{src}`.

Closes #1880
This commit is contained in:
Shivansh-007 2022-01-10 19:28:35 +05:30 committed by GitHub
parent e401b6bb1e
commit 521d1b8129
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 27 deletions

View File

@ -22,6 +22,8 @@
`values: Tuple[int, ...] = 1, 2, 3`) now implies 3.8+ (#2708) `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 - For stubs, one blank line between class attributes and methods is now kept if there's
at least one pre-existing blank line (#2736) 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 ### Packaging

View File

@ -31,6 +31,7 @@
) )
import click import click
from click.core import ParameterSource
from dataclasses import replace from dataclasses import replace
from mypy_extensions import mypyc_attr from mypy_extensions import mypyc_attr
@ -411,8 +412,37 @@ def main(
config: Optional[str], config: Optional[str],
) -> None: ) -> None:
"""The uncompromising code formatter.""" """The uncompromising code formatter."""
if config and verbose: ctx.ensure_object(dict)
out(f"Using configuration from {config}.", bold=False, fg="blue") 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! 💥 💔 💥" error_msg = "Oh no! 💥 💔 💥"
if required_version and required_version != __version__: if required_version and required_version != __version__:
@ -516,14 +546,12 @@ def get_sources(
stdin_filename: Optional[str], stdin_filename: Optional[str],
) -> Set[Path]: ) -> Set[Path]:
"""Compute the set of files to be formatted.""" """Compute the set of files to be formatted."""
root = find_project_root(src)
sources: Set[Path] = set() sources: Set[Path] = set()
path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
if exclude is None: if exclude is None:
exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES)
gitignore = get_gitignore(root) gitignore = get_gitignore(ctx.obj["root"])
else: else:
gitignore = None gitignore = None
@ -536,7 +564,7 @@ def get_sources(
is_stdin = False is_stdin = False
if is_stdin or p.is_file(): 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: if normalized_path is None:
continue continue
@ -563,7 +591,7 @@ def get_sources(
sources.update( sources.update(
gen_python_files( gen_python_files(
p.iterdir(), p.iterdir(),
root, ctx.obj["root"],
include, include,
exclude, exclude,
extend_exclude, extend_exclude,

View File

@ -31,7 +31,7 @@
@lru_cache() @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. """Return a directory containing .git, .hg, or pyproject.toml.
That directory will be a common parent of all files and directories That directory will be a common parent of all files and directories
@ -39,6 +39,10 @@ def find_project_root(srcs: Sequence[str]) -> Path:
If no directory in the tree contains a marker that would specify it's the 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. 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: if not srcs:
srcs = [str(Path.cwd().resolve())] srcs = [str(Path.cwd().resolve())]
@ -58,20 +62,20 @@ def find_project_root(srcs: Sequence[str]) -> Path:
for directory in (common_base, *common_base.parents): for directory in (common_base, *common_base.parents):
if (directory / ".git").exists(): if (directory / ".git").exists():
return directory return directory, ".git directory"
if (directory / ".hg").is_dir(): if (directory / ".hg").is_dir():
return directory return directory, ".hg directory"
if (directory / "pyproject.toml").is_file(): 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]: def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]:
"""Find the absolute filepath to a pyproject.toml if it exists""" """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" path_pyproject_toml = path_project_root / "pyproject.toml"
if path_pyproject_toml.is_file(): if path_pyproject_toml.is_file():
return str(path_pyproject_toml) return str(path_pyproject_toml)
@ -133,7 +137,9 @@ def get_gitignore(root: Path) -> PathSpec:
def normalize_path_maybe_ignore( def normalize_path_maybe_ignore(
path: Path, root: Path, report: Report path: Path,
root: Path,
report: Optional[Report] = None,
) -> Optional[str]: ) -> Optional[str]:
"""Normalize `path`. May return `None` if `path` was ignored. """Normalize `path`. May return `None` if `path` was ignored.
@ -143,12 +149,16 @@ def normalize_path_maybe_ignore(
abspath = path if path.is_absolute() else Path.cwd() / path abspath = path if path.is_absolute() else Path.cwd() / path
normalized_path = abspath.resolve().relative_to(root).as_posix() normalized_path = abspath.resolve().relative_to(root).as_posix()
except OSError as e: 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 return None
except ValueError: except ValueError:
if path.is_symlink(): 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 return None
raise raise

View File

@ -100,6 +100,8 @@ class FakeContext(click.Context):
def __init__(self) -> None: def __init__(self) -> None:
self.default_map: Dict[str, Any] = {} 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): class FakeParameter(click.Parameter):
@ -1350,10 +1352,17 @@ def test_find_project_root(self) -> None:
src_python.touch() src_python.touch()
self.assertEqual( 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( @patch(
"black.files.find_user_pyproject_toml", "black.files.find_user_pyproject_toml",
@ -1756,6 +1765,7 @@ def assert_collected_sources(
src: Sequence[Union[str, Path]], src: Sequence[Union[str, Path]],
expected: Sequence[Union[str, Path]], expected: Sequence[Union[str, Path]],
*, *,
ctx: Optional[FakeContext] = None,
exclude: Optional[str] = None, exclude: Optional[str] = None,
include: Optional[str] = None, include: Optional[str] = None,
extend_exclude: Optional[str] = None, extend_exclude: Optional[str] = None,
@ -1771,7 +1781,7 @@ def assert_collected_sources(
) )
gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude) gs_force_exclude = None if force_exclude is None else compile_pattern(force_exclude)
collected = black.get_sources( collected = black.get_sources(
ctx=FakeContext(), ctx=ctx or FakeContext(),
src=gs_src, src=gs_src,
quiet=False, quiet=False,
verbose=False, verbose=False,
@ -1807,9 +1817,11 @@ def test_gitignore_used_as_default(self) -> None:
base / "b/.definitely_exclude/a.pyi", base / "b/.definitely_exclude/a.pyi",
] ]
src = [base / "b/"] 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: def test_exclude_for_issue_1572(self) -> None:
# Exclude shouldn't touch files that were explicitly given to Black through the # 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. # CLI. Exclude is supposed to only apply to the recursive discovery of files.
@ -1992,13 +2004,13 @@ def test_symlink_out_of_root_directory(self) -> None:
child.is_symlink.assert_called() child.is_symlink.assert_called()
assert child.is_symlink.call_count == 2 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: def test_get_sources_with_stdin(self) -> None:
src = ["-"] src = ["-"]
expected = ["-"] expected = ["-"]
assert_collected_sources(src, expected, include="", exclude=r"/exclude/|a\.py") 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: def test_get_sources_with_stdin_filename(self) -> None:
src = ["-"] src = ["-"]
stdin_filename = str(THIS_DIR / "data/collections.py") stdin_filename = str(THIS_DIR / "data/collections.py")
@ -2010,7 +2022,7 @@ def test_get_sources_with_stdin_filename(self) -> None:
stdin_filename=stdin_filename, 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: def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
# Exclude shouldn't exclude stdin_filename since it is mimicking the # Exclude shouldn't exclude stdin_filename since it is mimicking the
# file being passed directly. This is the same as # file being passed directly. This is the same as
@ -2026,7 +2038,7 @@ def test_get_sources_with_stdin_filename_and_exclude(self) -> None:
stdin_filename=stdin_filename, 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: def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
# Extend exclude shouldn't exclude stdin_filename since it is mimicking the # Extend exclude shouldn't exclude stdin_filename since it is mimicking the
# file being passed directly. This is the same as # file being passed directly. This is the same as
@ -2042,7 +2054,7 @@ def test_get_sources_with_stdin_filename_and_extend_exclude(self) -> None:
stdin_filename=stdin_filename, 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: def test_get_sources_with_stdin_filename_and_force_exclude(self) -> None:
# Force exclude should exclude the file when passing it through # Force exclude should exclude the file when passing it through
# stdin_filename # stdin_filename