add gitignore support using pathspec (#878)
This commit is contained in:
parent
a6d866990e
commit
e9d4e7b67f
1
Pipfile
1
Pipfile
@ -13,6 +13,7 @@ black = {path = ".",extras = ["d"],editable = true}
|
|||||||
aiohttp-cors = "*"
|
aiohttp-cors = "*"
|
||||||
typed-ast = ">=1.3.1"
|
typed-ast = ">=1.3.1"
|
||||||
regex = ">=2019.8"
|
regex = ">=2019.8"
|
||||||
|
pathspec = ">=0.6"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pre-commit = "*"
|
pre-commit = "*"
|
||||||
|
27
black.py
27
black.py
@ -43,6 +43,7 @@
|
|||||||
import click
|
import click
|
||||||
import toml
|
import toml
|
||||||
from typed_ast import ast3, ast27
|
from typed_ast import ast3, ast27
|
||||||
|
from pathspec import PathSpec
|
||||||
|
|
||||||
# lib2to3 fork
|
# lib2to3 fork
|
||||||
from blib2to3.pytree import Node, Leaf, type_repr
|
from blib2to3.pytree import Node, Leaf, type_repr
|
||||||
@ -436,7 +437,9 @@ def main(
|
|||||||
p = Path(s)
|
p = Path(s)
|
||||||
if p.is_dir():
|
if p.is_dir():
|
||||||
sources.update(
|
sources.update(
|
||||||
gen_python_files_in_dir(p, root, include_regex, exclude_regex, report)
|
gen_python_files_in_dir(
|
||||||
|
p, root, include_regex, exclude_regex, report, get_gitignore(root)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
elif p.is_file() or s == "-":
|
elif p.is_file() or s == "-":
|
||||||
# if a file was explicitly given, we don't care about its extension
|
# if a file was explicitly given, we don't care about its extension
|
||||||
@ -3453,12 +3456,23 @@ def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]:
|
|||||||
return imports
|
return imports
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_gitignore(root: Path) -> PathSpec:
|
||||||
|
""" Return a PathSpec matching gitignore content if present."""
|
||||||
|
gitignore = root / ".gitignore"
|
||||||
|
if not gitignore.is_file():
|
||||||
|
return PathSpec.from_lines("gitwildmatch", [])
|
||||||
|
else:
|
||||||
|
return PathSpec.from_lines("gitwildmatch", gitignore.open())
|
||||||
|
|
||||||
|
|
||||||
def gen_python_files_in_dir(
|
def gen_python_files_in_dir(
|
||||||
path: Path,
|
path: Path,
|
||||||
root: Path,
|
root: Path,
|
||||||
include: Pattern[str],
|
include: Pattern[str],
|
||||||
exclude: Pattern[str],
|
exclude: Pattern[str],
|
||||||
report: "Report",
|
report: "Report",
|
||||||
|
gitignore: PathSpec,
|
||||||
) -> Iterator[Path]:
|
) -> Iterator[Path]:
|
||||||
"""Generate all files under `path` whose paths are not excluded by the
|
"""Generate all files under `path` whose paths are not excluded by the
|
||||||
`exclude` regex, but are included by the `include` regex.
|
`exclude` regex, but are included by the `include` regex.
|
||||||
@ -3469,6 +3483,12 @@ def gen_python_files_in_dir(
|
|||||||
"""
|
"""
|
||||||
assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
|
assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
|
||||||
for child in path.iterdir():
|
for child in path.iterdir():
|
||||||
|
# First ignore files matching .gitignore
|
||||||
|
if gitignore.match_file(child.as_posix()):
|
||||||
|
report.path_ignored(child, f"matches the .gitignore file content")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Then ignore with `exclude` option.
|
||||||
try:
|
try:
|
||||||
normalized_path = "/" + child.resolve().relative_to(root).as_posix()
|
normalized_path = "/" + child.resolve().relative_to(root).as_posix()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -3482,13 +3502,16 @@ def gen_python_files_in_dir(
|
|||||||
|
|
||||||
if child.is_dir():
|
if child.is_dir():
|
||||||
normalized_path += "/"
|
normalized_path += "/"
|
||||||
|
|
||||||
exclude_match = exclude.search(normalized_path)
|
exclude_match = exclude.search(normalized_path)
|
||||||
if exclude_match and exclude_match.group(0):
|
if exclude_match and exclude_match.group(0):
|
||||||
report.path_ignored(child, f"matches the --exclude regular expression")
|
report.path_ignored(child, f"matches the --exclude regular expression")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if child.is_dir():
|
if child.is_dir():
|
||||||
yield from gen_python_files_in_dir(child, root, include, exclude, report)
|
yield from gen_python_files_in_dir(
|
||||||
|
child, root, include, exclude, report, gitignore
|
||||||
|
)
|
||||||
|
|
||||||
elif child.is_file():
|
elif child.is_file():
|
||||||
include_match = include.search(normalized_path)
|
include_match = include.search(normalized_path)
|
||||||
|
1
setup.py
1
setup.py
@ -40,6 +40,7 @@ def get_long_description() -> str:
|
|||||||
"toml>=0.9.4",
|
"toml>=0.9.4",
|
||||||
"typed-ast>=1.3.1",
|
"typed-ast>=1.3.1",
|
||||||
"regex",
|
"regex",
|
||||||
|
"pathspec>=0.6, <1",
|
||||||
],
|
],
|
||||||
extras_require={"d": ["aiohttp>=3.3.2", "aiohttp-cors"]},
|
extras_require={"d": ["aiohttp>=3.3.2", "aiohttp-cors"]},
|
||||||
test_suite="tests.test_black",
|
test_suite="tests.test_black",
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
else:
|
else:
|
||||||
has_blackd_deps = True
|
has_blackd_deps = True
|
||||||
|
|
||||||
|
from pathspec import PathSpec
|
||||||
|
|
||||||
ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True)
|
ff = partial(black.format_file_in_place, mode=black.FileMode(), fast=True)
|
||||||
fs = partial(black.format_str, mode=black.FileMode())
|
fs = partial(black.format_str, mode=black.FileMode())
|
||||||
THIS_FILE = Path(__file__)
|
THIS_FILE = Path(__file__)
|
||||||
@ -1392,6 +1394,7 @@ def test_include_exclude(self) -> None:
|
|||||||
include = re.compile(r"\.pyi?$")
|
include = re.compile(r"\.pyi?$")
|
||||||
exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
|
exclude = re.compile(r"/exclude/|/\.definitely_exclude/")
|
||||||
report = black.Report()
|
report = black.Report()
|
||||||
|
gitignore = PathSpec.from_lines("gitwildmatch", [])
|
||||||
sources: List[Path] = []
|
sources: List[Path] = []
|
||||||
expected = [
|
expected = [
|
||||||
Path(path / "b/dont_exclude/a.py"),
|
Path(path / "b/dont_exclude/a.py"),
|
||||||
@ -1399,13 +1402,37 @@ def test_include_exclude(self) -> None:
|
|||||||
]
|
]
|
||||||
this_abs = THIS_DIR.resolve()
|
this_abs = THIS_DIR.resolve()
|
||||||
sources.extend(
|
sources.extend(
|
||||||
black.gen_python_files_in_dir(path, this_abs, include, exclude, report)
|
black.gen_python_files_in_dir(
|
||||||
|
path, this_abs, include, exclude, report, gitignore
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(sorted(expected), sorted(sources))
|
||||||
|
|
||||||
|
def test_gitignore_exclude(self) -> None:
|
||||||
|
path = THIS_DIR / "data" / "include_exclude_tests"
|
||||||
|
include = re.compile(r"\.pyi?$")
|
||||||
|
exclude = re.compile(r"")
|
||||||
|
report = black.Report()
|
||||||
|
gitignore = PathSpec.from_lines(
|
||||||
|
"gitwildmatch", ["exclude/", ".definitely_exclude"]
|
||||||
|
)
|
||||||
|
sources: List[Path] = []
|
||||||
|
expected = [
|
||||||
|
Path(path / "b/dont_exclude/a.py"),
|
||||||
|
Path(path / "b/dont_exclude/a.pyi"),
|
||||||
|
]
|
||||||
|
this_abs = THIS_DIR.resolve()
|
||||||
|
sources.extend(
|
||||||
|
black.gen_python_files_in_dir(
|
||||||
|
path, this_abs, include, exclude, report, gitignore
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(sorted(expected), sorted(sources))
|
self.assertEqual(sorted(expected), sorted(sources))
|
||||||
|
|
||||||
def test_empty_include(self) -> None:
|
def test_empty_include(self) -> None:
|
||||||
path = THIS_DIR / "data" / "include_exclude_tests"
|
path = THIS_DIR / "data" / "include_exclude_tests"
|
||||||
report = black.Report()
|
report = black.Report()
|
||||||
|
gitignore = PathSpec.from_lines("gitwildmatch", [])
|
||||||
empty = re.compile(r"")
|
empty = re.compile(r"")
|
||||||
sources: List[Path] = []
|
sources: List[Path] = []
|
||||||
expected = [
|
expected = [
|
||||||
@ -1422,7 +1449,12 @@ def test_empty_include(self) -> None:
|
|||||||
this_abs = THIS_DIR.resolve()
|
this_abs = THIS_DIR.resolve()
|
||||||
sources.extend(
|
sources.extend(
|
||||||
black.gen_python_files_in_dir(
|
black.gen_python_files_in_dir(
|
||||||
path, this_abs, empty, re.compile(black.DEFAULT_EXCLUDES), report
|
path,
|
||||||
|
this_abs,
|
||||||
|
empty,
|
||||||
|
re.compile(black.DEFAULT_EXCLUDES),
|
||||||
|
report,
|
||||||
|
gitignore,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(sorted(expected), sorted(sources))
|
self.assertEqual(sorted(expected), sorted(sources))
|
||||||
@ -1430,6 +1462,7 @@ def test_empty_include(self) -> None:
|
|||||||
def test_empty_exclude(self) -> None:
|
def test_empty_exclude(self) -> None:
|
||||||
path = THIS_DIR / "data" / "include_exclude_tests"
|
path = THIS_DIR / "data" / "include_exclude_tests"
|
||||||
report = black.Report()
|
report = black.Report()
|
||||||
|
gitignore = PathSpec.from_lines("gitwildmatch", [])
|
||||||
empty = re.compile(r"")
|
empty = re.compile(r"")
|
||||||
sources: List[Path] = []
|
sources: List[Path] = []
|
||||||
expected = [
|
expected = [
|
||||||
@ -1443,7 +1476,12 @@ def test_empty_exclude(self) -> None:
|
|||||||
this_abs = THIS_DIR.resolve()
|
this_abs = THIS_DIR.resolve()
|
||||||
sources.extend(
|
sources.extend(
|
||||||
black.gen_python_files_in_dir(
|
black.gen_python_files_in_dir(
|
||||||
path, this_abs, re.compile(black.DEFAULT_INCLUDES), empty, report
|
path,
|
||||||
|
this_abs,
|
||||||
|
re.compile(black.DEFAULT_INCLUDES),
|
||||||
|
empty,
|
||||||
|
report,
|
||||||
|
gitignore,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual(sorted(expected), sorted(sources))
|
self.assertEqual(sorted(expected), sorted(sources))
|
||||||
@ -1488,13 +1526,18 @@ def test_symlink_out_of_root_directory(self) -> None:
|
|||||||
include = re.compile(black.DEFAULT_INCLUDES)
|
include = re.compile(black.DEFAULT_INCLUDES)
|
||||||
exclude = re.compile(black.DEFAULT_EXCLUDES)
|
exclude = re.compile(black.DEFAULT_EXCLUDES)
|
||||||
report = black.Report()
|
report = black.Report()
|
||||||
|
gitignore = PathSpec.from_lines("gitwildmatch", [])
|
||||||
# `child` should behave like a symlink which resolved path is clearly
|
# `child` should behave like a symlink which resolved path is clearly
|
||||||
# outside of the `root` directory.
|
# outside of the `root` directory.
|
||||||
path.iterdir.return_value = [child]
|
path.iterdir.return_value = [child]
|
||||||
child.resolve.return_value = Path("/a/b/c")
|
child.resolve.return_value = Path("/a/b/c")
|
||||||
child.is_symlink.return_value = True
|
child.is_symlink.return_value = True
|
||||||
try:
|
try:
|
||||||
list(black.gen_python_files_in_dir(path, root, include, exclude, report))
|
list(
|
||||||
|
black.gen_python_files_in_dir(
|
||||||
|
path, root, include, exclude, report, gitignore
|
||||||
|
)
|
||||||
|
)
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
self.fail(f"`get_python_files_in_dir()` failed: {ve}")
|
self.fail(f"`get_python_files_in_dir()` failed: {ve}")
|
||||||
path.iterdir.assert_called_once()
|
path.iterdir.assert_called_once()
|
||||||
@ -1504,7 +1547,11 @@ def test_symlink_out_of_root_directory(self) -> None:
|
|||||||
# outside of the `root` directory.
|
# outside of the `root` directory.
|
||||||
child.is_symlink.return_value = False
|
child.is_symlink.return_value = False
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
list(black.gen_python_files_in_dir(path, root, include, exclude, report))
|
list(
|
||||||
|
black.gen_python_files_in_dir(
|
||||||
|
path, root, include, exclude, report, gitignore
|
||||||
|
)
|
||||||
|
)
|
||||||
path.iterdir.assert_called()
|
path.iterdir.assert_called()
|
||||||
self.assertEqual(path.iterdir.call_count, 2)
|
self.assertEqual(path.iterdir.call_count, 2)
|
||||||
child.resolve.assert_called()
|
child.resolve.assert_called()
|
||||||
|
Loading…
Reference in New Issue
Block a user