Skip to content

Commit 78b4aac

Browse files
Merge pull request #9542 from mr-raj12/cleanup-patterns-todos-master
patterns: clean up TODOs, move is_include_cmd to IECommand property, fixes #9442
2 parents 857db05 + 60596bd commit 78b4aac

File tree

2 files changed

+69
-26
lines changed

2 files changed

+69
-26
lines changed

src/borg/patterns.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import re
44
import sys
55
import unicodedata
6+
import warnings
67
from collections import namedtuple
78
from enum import Enum
9+
from pathlib import Path
810

911
from .helpers import clean_lines, shellpattern
1012
from .helpers.argparsing import Action, ArgumentTypeError
@@ -89,15 +91,13 @@ def __init__(self, fallback=None):
8991
# False when calling match().
9092
self.recurse_dir = None
9193

92-
# whether to recurse into directories when no match is found
93-
# TODO: allow modification as a config option?
94+
# Whether to recurse into directories when no match is found.
95+
# This must be True so that include patterns inside excluded directories
96+
# work correctly (e.g. "+ /excluded_dir/important" inside "- /excluded_dir").
9497
self.recurse_dir_default = True
9598

9699
self.include_patterns = []
97100

98-
# TODO: move this info to parse_inclexcl_command and store in PatternBase subclass?
99-
self.is_include_cmd = {IECommand.Exclude: False, IECommand.ExcludeNoRecurse: False, IECommand.Include: True}
100-
101101
def empty(self):
102102
return not len(self._items) and not len(self._path_full_patterns)
103103

@@ -150,13 +150,13 @@ def match(self, path):
150150
if value is not non_existent:
151151
# we have a full path match!
152152
self.recurse_dir = command_recurses_dir(value)
153-
return self.is_include_cmd[value]
153+
return value.is_include
154154

155155
# this is the slow way, if we have many patterns in self._items:
156156
for pattern, cmd in self._items:
157157
if pattern.match(path, normalize=False):
158158
self.recurse_dir = pattern.recurse_dir
159-
return self.is_include_cmd[cmd]
159+
return cmd.is_include
160160

161161
# by default we will recurse if there is no match
162162
self.recurse_dir = self.recurse_dir_default
@@ -314,10 +314,17 @@ class IECommand(Enum):
314314
Exclude = 4
315315
ExcludeNoRecurse = 5
316316

317+
@property
318+
def is_include(self):
319+
return self is IECommand.Include
320+
317321

318322
def command_recurses_dir(cmd):
319-
# TODO?: raise error or return None if *cmd* is RootPath or PatternStyle
320-
return cmd not in [IECommand.ExcludeNoRecurse]
323+
if cmd is IECommand.ExcludeNoRecurse:
324+
return False
325+
if cmd is IECommand.Include or cmd is IECommand.Exclude:
326+
return True
327+
raise ValueError(f"command_recurses_dir: unexpected command: {cmd!r}")
321328

322329

323330
def get_pattern_class(prefix):
@@ -368,7 +375,14 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern):
368375
raise ArgumentTypeError("A pattern/command must have a value part.")
369376

370377
if cmd is IECommand.RootPath:
371-
# TODO: validate string?
378+
if not Path(remainder_str).is_absolute():
379+
warnings.warn(
380+
f"Root path {remainder_str!r} is not absolute, it is recommended to use an absolute path",
381+
UserWarning,
382+
stacklevel=2,
383+
)
384+
if not Path(remainder_str).exists():
385+
warnings.warn(f"Root path {remainder_str!r} does not exist", UserWarning, stacklevel=2)
372386
val = remainder_str
373387
elif cmd is IECommand.PatternStyle:
374388
# then remainder_str is something like 're' or 'sh'

src/borg/testsuite/patterns_test.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import io
22
import os.path
33
import sys
4+
import warnings
45

56
import pytest
67

78
from ..helpers.argparsing import ArgumentTypeError
89
from ..patterns import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern
9-
from ..patterns import load_exclude_file, load_pattern_file
10-
from ..patterns import parse_pattern, PatternMatcher
10+
from ..patterns import IECommand, load_exclude_file, load_pattern_file
11+
from ..patterns import command_recurses_dir, parse_inclexcl_command, parse_pattern, PatternMatcher
1112
from ..patterns import get_regex_from_pattern
1213

1314

@@ -605,25 +606,53 @@ def test_pattern_matcher():
605606
for i in ["", "foo", "bar"]:
606607
assert pm.match(i) is None
607608

608-
# add extra entries to aid in testing
609-
for target in ["A", "B", "Empty", "FileNotFound"]:
610-
pm.is_include_cmd[target] = target
609+
pm.add([RegexPattern("^a")], IECommand.Include)
610+
pm.add([RegexPattern("^b"), RegexPattern("^z")], IECommand.Exclude)
611+
pm.add([RegexPattern("^$")], IECommand.ExcludeNoRecurse)
612+
pm.fallback = False
611613

612-
pm.add([RegexPattern("^a")], "A")
613-
pm.add([RegexPattern("^b"), RegexPattern("^z")], "B")
614-
pm.add([RegexPattern("^$")], "Empty")
615-
pm.fallback = "FileNotFound"
616-
617-
assert pm.match("") == "Empty"
618-
assert pm.match("aaa") == "A"
619-
assert pm.match("bbb") == "B"
620-
assert pm.match("ccc") == "FileNotFound"
621-
assert pm.match("xyz") == "FileNotFound"
622-
assert pm.match("z") == "B"
614+
assert pm.match("") is False # ExcludeNoRecurse -> not include
615+
assert pm.match("aaa") is True # Include
616+
assert pm.match("bbb") is False # Exclude
617+
assert pm.match("ccc") is False # fallback
618+
assert pm.match("xyz") is False # fallback
619+
assert pm.match("z") is False # Exclude (matches ^z)
623620

624621
assert PatternMatcher(fallback="hey!").fallback == "hey!"
625622

626623

624+
def test_command_recurses_dir():
625+
assert command_recurses_dir(IECommand.Include) is True
626+
assert command_recurses_dir(IECommand.Exclude) is True
627+
assert command_recurses_dir(IECommand.ExcludeNoRecurse) is False
628+
with pytest.raises(ValueError, match="unexpected command"):
629+
command_recurses_dir(IECommand.RootPath)
630+
with pytest.raises(ValueError, match="unexpected command"):
631+
command_recurses_dir(IECommand.PatternStyle)
632+
633+
634+
def test_root_path_validation(tmp_path):
635+
# absolute path that exists: no warnings
636+
with warnings.catch_warnings():
637+
warnings.simplefilter("error")
638+
parse_inclexcl_command(f"R {tmp_path}")
639+
640+
# absolute path that doesn't exist: only "does not exist" warning
641+
nonexistent = str(tmp_path / "nonexistent_subdir_12345")
642+
with pytest.warns(UserWarning) as warning_list:
643+
parse_inclexcl_command(f"R {nonexistent}")
644+
messages = [str(w.message) for w in warning_list]
645+
assert any("does not exist" in m for m in messages)
646+
assert not any("absolute" in m for m in messages)
647+
648+
# relative path that doesn't exist: warns about both
649+
with pytest.warns(UserWarning) as warning_list:
650+
parse_inclexcl_command("R relative/nonexistent/path/xyz123")
651+
messages = [str(w.message) for w in warning_list]
652+
assert any("absolute" in m for m in messages)
653+
assert any("does not exist" in m for m in messages)
654+
655+
627656
@pytest.mark.parametrize(
628657
"pattern, regex",
629658
[

0 commit comments

Comments
 (0)