-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
snapshotter: use wcmatch.glob.globmatch function
All existing python glob match functions seem to have issues. Path.match does not accept '**'. When using fnmatch, '*' matches a / character which is not what we want. To fix this mess, I've introduced a new 3rd party library wcmatch to handle the globmatching.
- Loading branch information
Showing
9 changed files
with
143 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
""" | ||
Copyright (c) 2023 Aiven Ltd | ||
See LICENSE for details | ||
Classes for working with snapshot groups. | ||
""" | ||
from astacus.common.snapshot import SnapshotGroup | ||
from pathlib import Path | ||
from typing import Iterable, Optional, Sequence | ||
from typing_extensions import Self | ||
from wcmatch.glob import GLOBSTAR, iglob, translate | ||
|
||
import dataclasses | ||
import os | ||
import re | ||
|
||
WCMATCH_FLAGS = GLOBSTAR | ||
|
||
|
||
@dataclasses.dataclass | ||
class CompiledGroup: | ||
group: SnapshotGroup | ||
regex: re.Pattern | ||
|
||
@classmethod | ||
def compile(cls, group: SnapshotGroup) -> Self: | ||
return cls(group, glob_compile(group.root_glob)) | ||
|
||
def matches(self, relative_path: Path) -> bool: | ||
return bool(self.regex.match(str(relative_path))) and relative_path.name not in self.group.excluded_names | ||
|
||
def glob(self, root_dir: Optional[Path] = None) -> Iterable[str]: | ||
for path in iglob(self.group.root_glob, root_dir=root_dir, flags=WCMATCH_FLAGS): | ||
if os.path.basename(path) not in self.group.excluded_names: | ||
yield path | ||
|
||
|
||
@dataclasses.dataclass | ||
class CompiledGroups: | ||
groups: Sequence[CompiledGroup] | ||
|
||
@classmethod | ||
def compile(cls, groups: Sequence[SnapshotGroup]) -> Self: | ||
return cls([CompiledGroup.compile(group) for group in groups]) | ||
|
||
def get_matching(self, relative_path: Path) -> list[SnapshotGroup]: | ||
return [group.group for group in self.groups if group.matches(relative_path)] | ||
|
||
def any_match(self, relative_path: Path) -> bool: | ||
return any(group.matches(relative_path) for group in self.groups) | ||
|
||
def root_globs(self) -> list[str]: | ||
return [group.group.root_glob for group in self.groups] | ||
|
||
|
||
def glob_compile(glob: str) -> re.Pattern: | ||
return re.compile(translate(glob, flags=WCMATCH_FLAGS)[0][0]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
""" | ||
Copyright (c) 2023 Aiven Ltd | ||
See LICENSE for details | ||
""" | ||
from astacus.common.snapshot import SnapshotGroup | ||
from astacus.node.snapshot_groups import CompiledGroup, CompiledGroups, glob_compile | ||
from pathlib import Path | ||
|
||
import os | ||
|
||
POSITIVE_TEST_CASES: list[tuple[Path, str]] = [ | ||
(Path("foo"), "foo"), | ||
(Path("foo"), "*"), | ||
(Path("foo/bar"), "*/bar"), | ||
(Path("foo"), "**"), | ||
(Path("foo/bar"), "**"), | ||
(Path("foo/bar/baz"), "**/*"), | ||
(Path("foo/bar"), "**/*"), | ||
(Path("foo/bar"), "**/**"), | ||
] | ||
|
||
NEGATIVE_TEST_CASES: list[tuple[Path, str]] = [ | ||
(Path("foo/bar/baz"), "*/*"), | ||
(Path("foo"), "foobar"), | ||
(Path("foo"), "*/foo"), | ||
] | ||
|
||
|
||
def test_compile() -> None: | ||
for path, glob in POSITIVE_TEST_CASES: | ||
assert glob_compile(glob).match(str(path)) is not None | ||
for path, glob in NEGATIVE_TEST_CASES: | ||
assert glob_compile(glob).match(str(path)) is None | ||
|
||
|
||
def test_CompiledGroup_matches() -> None: | ||
for path, glob in POSITIVE_TEST_CASES: | ||
group = SnapshotGroup(root_glob=glob) | ||
assert CompiledGroup.compile(group).matches(path) | ||
group = SnapshotGroup(root_glob=glob, excluded_names=[os.path.basename(path)]) | ||
assert not CompiledGroup.compile(group).matches(path) | ||
for path, glob in NEGATIVE_TEST_CASES: | ||
group = SnapshotGroup(root_glob=glob) | ||
assert not CompiledGroup.compile(group).matches(path) | ||
|
||
|
||
def test_CompiledGroups() -> None: | ||
for path, glob in POSITIVE_TEST_CASES: | ||
group1 = SnapshotGroup(root_glob=glob) | ||
group2 = SnapshotGroup(root_glob=glob, excluded_names=[os.path.basename(path)]) | ||
group3 = SnapshotGroup(root_glob="doesntmatch") | ||
compiled = CompiledGroups.compile([group1, group2, group3]) | ||
assert compiled.any_match(path) | ||
assert compiled.get_matching(path) == [group1] | ||
|
||
|
||
def test_CompiledGroup_glob(tmp_path: Path) -> None: | ||
for p, _ in POSITIVE_TEST_CASES + NEGATIVE_TEST_CASES: | ||
p = tmp_path / p | ||
p.mkdir(parents=True, exist_ok=True) | ||
p.touch() | ||
for p, glob in POSITIVE_TEST_CASES: | ||
group = SnapshotGroup(root_glob=glob) | ||
assert str(p) in CompiledGroup.compile(group).glob(tmp_path) | ||
for p, glob in NEGATIVE_TEST_CASES: | ||
group = SnapshotGroup(root_glob=glob) | ||
assert str(p) not in CompiledGroup.compile(group).glob(tmp_path) |