Skip to content

Commit

Permalink
✨ Add ignore patterns (#92)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcelo Trylesinski <[email protected]>
  • Loading branch information
lemonyte and Kludex authored Jul 18, 2023
1 parent b210d2f commit 393dc89
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 7 deletions.
4 changes: 2 additions & 2 deletions bump_pydantic/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from bump_pydantic.main import app
from bump_pydantic.main import entrypoint

if __name__ == "__main__":
app()
entrypoint()
47 changes: 47 additions & 0 deletions bump_pydantic/glob_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fnmatch
import re
from pathlib import Path
from typing import List

MATCH_SEP = r"(?:/|\\)"
MATCH_SEP_OR_END = r"(?:/|\\|\Z)"
MATCH_NON_RECURSIVE = r"[^/\\]*"
MATCH_RECURSIVE = r"(?:.*)"


def glob_to_re(pattern: str) -> str:
"""Translate a glob pattern to a regular expression for matching."""
fragments: List[str] = []
for segment in re.split(r"/|\\", pattern):
if segment == "":
continue
if segment == "**":
# Remove previous separator match, so the recursive match can match zero or more segments.
if fragments and fragments[-1] == MATCH_SEP:
fragments.pop()
fragments.append(MATCH_RECURSIVE)
elif "**" in segment:
raise ValueError("invalid pattern: '**' can only be an entire path component")
else:
fragment = fnmatch.translate(segment)
fragment = fragment.replace(r"(?s:", r"(?:")
fragment = fragment.replace(r".*", MATCH_NON_RECURSIVE)
fragment = fragment.replace(r"\Z", r"")
fragments.append(fragment)
fragments.append(MATCH_SEP)
# Remove trailing MATCH_SEP, so it can be replaced with MATCH_SEP_OR_END.
if fragments and fragments[-1] == MATCH_SEP:
fragments.pop()
fragments.append(MATCH_SEP_OR_END)
return rf"(?s:{''.join(fragments)})"


def match_glob(path: Path, pattern: str) -> bool:
"""Check if a path matches a glob pattern.
If the pattern ends with a directory separator, the path must be a directory.
"""
match = bool(re.fullmatch(glob_to_re(pattern), str(path)))
if pattern.endswith("/") or pattern.endswith("\\"):
return match and path.is_dir()
return match
20 changes: 16 additions & 4 deletions bump_pydantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@
from bump_pydantic import __version__
from bump_pydantic.codemods import Rule, gather_codemods
from bump_pydantic.codemods.class_def_visitor import ClassDefVisitor
from bump_pydantic.glob_helpers import match_glob

app = Typer(invoke_without_command=True, add_completion=False)

entrypoint = functools.partial(app, windows_expand_args=False)

P = ParamSpec("P")
T = TypeVar("T")

DEFAULT_IGNORES = [".venv/**"]


def version_callback(value: bool):
if value:
Expand All @@ -37,6 +42,7 @@ def main(
path: Path = Argument(..., exists=True, dir_okay=True, allow_dash=False),
disable: List[Rule] = Option(default=[], help="Disable a rule."),
diff: bool = Option(False, help="Show diff instead of applying changes."),
ignore: List[str] = Option(default=DEFAULT_IGNORES, help="Ignore a path glob pattern."),
log_file: Path = Option("log.txt", help="Log errors to this file."),
version: bool = Option(
None,
Expand All @@ -57,13 +63,19 @@ def main(

if os.path.isfile(path):
package = path.parent
files = [str(path.relative_to("."))]
all_files = [path]
else:
package = path
files_str = list(package.glob("**/*.py"))
files = [str(file.relative_to(".")) for file in files_str]
all_files = list(package.glob("**/*.py"))

filtered_files = [file for file in all_files if not any(match_glob(file, pattern) for pattern in ignore)]
files = [str(file.relative_to(".")) for file in filtered_files]

console.log(f"Found {len(files)} files to process")
if files:
console.log(f"Found {len(files)} files to process")
else:
console.log("No files to process.")
raise Exit()

providers = {FullyQualifiedNameProvider, ScopeProvider}
metadata_manager = FullRepoManager(".", files, providers=providers) # type: ignore[arg-type]
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Issues = "https://github.com/pydantic/bump-pydantic/issues"
Source = "https://github.com/pydantic/bump-pydantic"

[project.scripts]
bump-pydantic = "bump_pydantic.main:app"
bump-pydantic = "bump_pydantic.main:entrypoint"

[tool.hatch.version]
path = "bump_pydantic/__init__.py"
Expand Down
70 changes: 70 additions & 0 deletions tests/unit/test_glob_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations

from pathlib import Path

import pytest

from bump_pydantic.glob_helpers import glob_to_re, match_glob


class TestGlobHelpers:
match_glob_values: list[tuple[str, Path, bool]] = [
("foo", Path("foo"), True),
("foo", Path("bar"), False),
("foo", Path("foo/bar"), False),
("*", Path("foo"), True),
("*", Path("bar"), True),
("*", Path("foo/bar"), False),
("**", Path("foo"), True),
("**", Path("foo/bar"), True),
("**", Path("foo/bar/baz/qux"), True),
("foo/bar", Path("foo/bar"), True),
("foo/bar", Path("foo"), False),
("foo/bar", Path("far"), False),
("foo/bar", Path("foo/foo"), False),
("foo/*", Path("foo/bar"), True),
("foo/*", Path("foo/bar/baz"), False),
("foo/*", Path("foo"), False),
("foo/*", Path("bar"), False),
("foo/**", Path("foo/bar"), True),
("foo/**", Path("foo/bar/baz"), True),
("foo/**", Path("foo/bar/baz/qux"), True),
("foo/**", Path("foo"), True),
("foo/**", Path("bar"), False),
("foo/**/bar", Path("foo/bar"), True),
("foo/**/bar", Path("foo/baz/bar"), True),
("foo/**/bar", Path("foo/baz/qux/bar"), True),
("foo/**/bar", Path("foo/baz/qux"), False),
("foo/**/bar", Path("foo/bar/baz"), False),
("foo/**/bar", Path("foo/bar/bar"), True),
("foo/**/bar", Path("foo"), False),
("foo/**/bar", Path("bar"), False),
("foo/**/*/bar", Path("foo/bar"), False),
("foo/**/*/bar", Path("foo/baz/bar"), True),
("foo/**/*/bar", Path("foo/baz/qux/bar"), True),
("foo/**/*/bar", Path("foo/baz/qux"), False),
("foo/**/*/bar", Path("foo/bar/baz"), False),
("foo/**/*/bar", Path("foo/bar/bar"), True),
("foo/**/*/bar", Path("foo"), False),
("foo/**/*/bar", Path("bar"), False),
("foo/ba*", Path("foo/bar"), True),
("foo/ba*", Path("foo/baz"), True),
("foo/ba*", Path("foo/qux"), False),
("foo/ba*", Path("foo/baz/qux"), False),
("foo/ba*", Path("foo/bar/baz"), False),
("foo/ba*", Path("foo"), False),
("foo/ba*", Path("bar"), False),
("foo/**/ba*/*/qux", Path("foo/a/b/c/bar/a/qux"), True),
("foo/**/ba*/*/qux", Path("foo/a/b/c/baz/a/qux"), True),
("foo/**/ba*/*/qux", Path("foo/a/bar/a/qux"), True),
("foo/**/ba*/*/qux", Path("foo/baz/a/qux"), True),
("foo/**/ba*/*/qux", Path("foo/baz/qux"), False),
("foo/**/ba*/*/qux", Path("foo/a/b/c/qux/a/qux"), False),
("foo/**/ba*/*/qux", Path("foo"), False),
("foo/**/ba*/*/qux", Path("bar"), False),
]

@pytest.mark.parametrize(("pattern", "path", "expected"), match_glob_values)
def test_match_glob(self, pattern: str, path: Path, expected: bool):
expr = glob_to_re(pattern)
assert match_glob(path, pattern) == expected, f"path: {path}, pattern: {pattern}, expr: {expr}"

0 comments on commit 393dc89

Please sign in to comment.