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

✨ Add ignore patterns #92

Merged
merged 16 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this be added to the app instead of using this partial?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither Typer nor Click seem to have an option to pass this argument during initialization. The only way to pass it is during invocation of the app through the __call__() or main() methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a reference for this?

Copy link
Contributor Author

@lemonyte lemonyte Jul 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR pallets/click#1918 added the parameter, only to the main method.
The only mention of this parameter in Click's documentation is under the main method here.
pallets/click#1918 (comment) explains how to use it in an entrypoint.


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}"