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:
parent
e401b6bb1e
commit
521d1b8129
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user