mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:07:17 +01:00
file_length: match repo exemptions without PurePath.full_match (3.13-only)
The TachiyomiSY fork's first CI run died with AttributeError on the runner's Python 3.12. Globs now go through a small tested translation (** spans directories, * and ? stop at a slash) that runs on any 3.x. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Yc4TiPjhbQLfH35QBPfrbP
This commit is contained in:
parent
879265b966
commit
ee276cd8e5
@ -17,9 +17,10 @@ fails closed on it (exit 2) instead of honouring it silently.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cache
|
from functools import cache
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path
|
||||||
|
|
||||||
#: Basename of the per-repo exemption list, looked up in the gate's cwd.
|
#: Basename of the per-repo exemption list, looked up in the gate's cwd.
|
||||||
EXEMPT_FILE = ".file-length-exempt"
|
EXEMPT_FILE = ".file-length-exempt"
|
||||||
@ -53,6 +54,35 @@ def parse(text: str) -> list[Exemption]:
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def glob_to_regex(pattern: str) -> re.Pattern[str]:
|
||||||
|
"""A repo-relative glob as an anchored regex.
|
||||||
|
|
||||||
|
`**` spans directories, `*` and `?` stop at a slash. Hand-rolled rather
|
||||||
|
than `PurePath.full_match` because that needs Python 3.13 and the gate
|
||||||
|
runs on whatever `python3` a CI runner ships (3.12 broke it in the
|
||||||
|
TachiyomiSY fork's first CI run).
|
||||||
|
"""
|
||||||
|
out: list[str] = []
|
||||||
|
i = 0
|
||||||
|
while i < len(pattern):
|
||||||
|
char = pattern[i]
|
||||||
|
if pattern.startswith("**", i):
|
||||||
|
out.append(".*")
|
||||||
|
i += 2
|
||||||
|
if pattern.startswith("/", i):
|
||||||
|
out[-1] = "(?:.*/)?"
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if char == "*":
|
||||||
|
out.append("[^/]*")
|
||||||
|
elif char == "?":
|
||||||
|
out.append("[^/]")
|
||||||
|
else:
|
||||||
|
out.append(re.escape(char))
|
||||||
|
i += 1
|
||||||
|
return re.compile("^" + "".join(out) + "$")
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def load(root: Path) -> tuple[Exemption, ...]:
|
def load(root: Path) -> tuple[Exemption, ...]:
|
||||||
"""The exemptions for the repo at `root`, empty when it has no file."""
|
"""The exemptions for the repo at `root`, empty when it has no file."""
|
||||||
@ -72,10 +102,10 @@ def repo_exempt_reason(path: Path, root: Path) -> str | None:
|
|||||||
if not entries:
|
if not entries:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
relative = PurePosixPath(path.relative_to(root).as_posix())
|
relative = path.relative_to(root).as_posix()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if relative.full_match(entry.pattern):
|
if glob_to_regex(entry.pattern).match(relative):
|
||||||
return f"repo exemption: {entry.reason}"
|
return f"repo exemption: {entry.reason}"
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from file_length.repo_exempt import (
|
|||||||
EXEMPT_FILE,
|
EXEMPT_FILE,
|
||||||
ExemptFileError,
|
ExemptFileError,
|
||||||
Exemption,
|
Exemption,
|
||||||
|
glob_to_regex,
|
||||||
load,
|
load,
|
||||||
parse,
|
parse,
|
||||||
repo_exempt_reason,
|
repo_exempt_reason,
|
||||||
@ -64,3 +65,21 @@ def test_reason_none_for_paths_outside_root(tmp_path: Path) -> None:
|
|||||||
write(tmp_path, EXEMPT_FILE, "**/*.kt # everything\n")
|
write(tmp_path, EXEMPT_FILE, "**/*.kt # everything\n")
|
||||||
outside = tmp_path.parent / "elsewhere.kt"
|
outside = tmp_path.parent / "elsewhere.kt"
|
||||||
assert repo_exempt_reason(outside, tmp_path) is None
|
assert repo_exempt_reason(outside, tmp_path) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("pattern", "hit", "miss"),
|
||||||
|
[
|
||||||
|
("a/*.kt", "a/x.kt", "a/b/x.kt"),
|
||||||
|
("a/**/*.kt", "a/b/c/x.kt", "b/x.kt"),
|
||||||
|
("a/**/*.kt", "a/x.kt", "a/x.kts"),
|
||||||
|
("**/tags/*.kt", "tags/x.kt", "tags/sub/x.kt"),
|
||||||
|
("a/?.kt", "a/x.kt", "a/xy.kt"),
|
||||||
|
("a/**", "a/b/c.kt", "b/a"),
|
||||||
|
("a.b", "a.b", "axb"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_glob_to_regex(pattern: str, hit: str, miss: str) -> None:
|
||||||
|
regex = glob_to_regex(pattern)
|
||||||
|
assert regex.match(hit), (pattern, hit)
|
||||||
|
assert not regex.match(miss), (pattern, miss)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user