Skip to content

Commit 4c83590

Browse files
author
atollk
committed
Reverted the change of fs.wildcard that was done to introduce the *_glob parameters.
Instead, fs.glob was extended to offer the same functionality.
1 parent 062108d commit 4c83590

File tree

7 files changed

+258
-182
lines changed

7 files changed

+258
-182
lines changed

fs/base.py

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import six
2323

24-
from . import copy, errors, fsencode, iotools, move, tools, walk, wildcard
24+
from . import copy, errors, fsencode, iotools, move, tools, walk, wildcard, glob
2525
from .copy import copy_modified_time
2626
from .glob import BoundGlobber
2727
from .mode import validate_open_mode
@@ -1641,6 +1641,49 @@ def match(self, patterns, name, accept_prefix=False):
16411641
be case sensitive (``*.py`` and ``*.PY`` will match different
16421642
names).
16431643
1644+
Arguments:
1645+
patterns (list, optional): A list of patterns, e.g.
1646+
``['*.py']``, or `None` to match everything.
1647+
name (str): A file or directory name (not a path)
1648+
1649+
Returns:
1650+
bool: `True` if ``name`` matches any of the patterns.
1651+
1652+
Raises:
1653+
TypeError: If ``patterns`` is a single string instead of
1654+
a list (or `None`).
1655+
1656+
Example:
1657+
>>> my_fs.match(['*.py'], '__init__.py')
1658+
True
1659+
>>> my_fs.match(['*.jpg', '*.png'], 'foo.gif')
1660+
False
1661+
1662+
Note:
1663+
If ``patterns`` is `None` (or ``['*']``), then this
1664+
method will always return `True`.
1665+
1666+
"""
1667+
if patterns is None:
1668+
return True
1669+
if isinstance(patterns, six.text_type):
1670+
raise TypeError("patterns must be a list or sequence")
1671+
case_sensitive = not typing.cast(
1672+
bool, self.getmeta().get("case_insensitive", False)
1673+
)
1674+
matcher = wildcard.get_matcher(patterns, case_sensitive)
1675+
return matcher(name)
1676+
1677+
def match_glob(self, patterns, name, accept_prefix=False):
1678+
# type: (Optional[Iterable[Text]], Text, bool) -> bool
1679+
"""Check if a path matches any of a list of glob patterns.
1680+
1681+
If a filesystem is case *insensitive* (such as Windows) then
1682+
this method will perform a case insensitive match (i.e. ``*.py``
1683+
will match the same names as ``*.PY``). Otherwise the match will
1684+
be case sensitive (``*.py`` and ``*.PY`` will match different
1685+
names).
1686+
16441687
Arguments:
16451688
patterns (list, optional): A list of patterns, e.g.
16461689
``['*.py']``, or `None` to match everything.
@@ -1657,13 +1700,13 @@ def match(self, patterns, name, accept_prefix=False):
16571700
a list (or `None`).
16581701
16591702
Example:
1660-
>>> my_fs.match(['*.py'], '__init__.py')
1703+
>>> my_fs.match_glob(['*.py'], '__init__.py')
16611704
True
1662-
>>> my_fs.match(['*.jpg', '*.png'], 'foo.gif')
1705+
>>> my_fs.match_glob(['*.jpg', '*.png'], 'foo.gif')
16631706
False
1664-
>>> my_fs.match(['dir/file.txt'], 'dir/', accept_prefix=True)
1707+
>>> my_fs.match_glob(['dir/file.txt'], 'dir/', accept_prefix=True)
16651708
True
1666-
>>> my_fs.match(['dir/file.txt'], 'dir/gile.txt', accept_prefix=True)
1709+
>>> my_fs.match_glob(['dir/file.txt'], 'dir/gile.txt', accept_prefix=True)
16671710
False
16681711
16691712
Note:
@@ -1678,7 +1721,7 @@ def match(self, patterns, name, accept_prefix=False):
16781721
case_sensitive = not typing.cast(
16791722
bool, self.getmeta().get("case_insensitive", False)
16801723
)
1681-
matcher = wildcard.get_matcher(
1724+
matcher = glob.get_matcher(
16821725
patterns, case_sensitive, accept_prefix=accept_prefix
16831726
)
16841727
return matcher(name)

fs/glob.py

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections import namedtuple
77
import re
88
import typing
9+
from functools import partial
910

1011
from .lrucache import LRUCache
1112
from ._repr import make_repr
@@ -17,7 +18,16 @@
1718
LineCounts = namedtuple("LineCounts", ["lines", "non_blank"])
1819

1920
if typing.TYPE_CHECKING:
20-
from typing import Iterator, List, Optional, Pattern, Text, Tuple
21+
from typing import (
22+
Iterator,
23+
List,
24+
Optional,
25+
Pattern,
26+
Text,
27+
Tuple,
28+
Iterable,
29+
Callable,
30+
)
2131
from .base import FS
2232

2333

@@ -26,6 +36,26 @@
2636
) # type: LRUCache[Tuple[Text, bool], Tuple[int, bool, Pattern]]
2737

2838

39+
def _split_pattern_by_rec(pattern):
40+
# type: (Text) -> List[Text]
41+
"""Split a glob pattern at its directory seperators (/).
42+
43+
Takes into account escaped cases like [/].
44+
"""
45+
indices = [-1]
46+
bracket_open = False
47+
for i, c in enumerate(pattern):
48+
if c == "/" and not bracket_open:
49+
indices.append(i)
50+
elif c == "[":
51+
bracket_open = True
52+
elif c == "]":
53+
bracket_open = False
54+
55+
indices.append(len(pattern))
56+
return [pattern[i + 1 : j] for i, j in zip(indices[:-1], indices[1:])]
57+
58+
2959
def _translate(pattern, case_sensitive=True):
3060
# type: (Text, bool) -> Text
3161
"""Translate a wildcard pattern to a regular expression.
@@ -43,14 +73,14 @@ def _translate(pattern, case_sensitive=True):
4373
if not case_sensitive:
4474
pattern = pattern.lower()
4575
i, n = 0, len(pattern)
46-
res = ""
76+
res = []
4777
while i < n:
4878
c = pattern[i]
4979
i = i + 1
5080
if c == "*":
51-
res = res + "[^/]*"
81+
res.append("[^/]*")
5282
elif c == "?":
53-
res = res + "."
83+
res.append("[^/]")
5484
elif c == "[":
5585
j = i
5686
if j < n and pattern[j] == "!":
@@ -60,28 +90,30 @@ def _translate(pattern, case_sensitive=True):
6090
while j < n and pattern[j] != "]":
6191
j = j + 1
6292
if j >= n:
63-
res = res + "\\["
93+
res.append("\\[")
6494
else:
6595
stuff = pattern[i:j].replace("\\", "\\\\")
6696
i = j + 1
6797
if stuff[0] == "!":
6898
stuff = "^" + stuff[1:]
6999
elif stuff[0] == "^":
70100
stuff = "\\" + stuff
71-
res = "%s[%s]" % (res, stuff)
101+
res.append("[%s]" % stuff)
72102
else:
73-
res = res + re.escape(c)
74-
return res
103+
res.append(re.escape(c))
104+
return "".join(res)
75105

76106

77107
def _translate_glob(pattern, case_sensitive=True):
78108
levels = 0
79109
recursive = False
80110
re_patterns = [""]
81111
for component in iteratepath(pattern):
82-
if component == "**":
83-
re_patterns.append(".*/?")
112+
if "**" in component:
84113
recursive = True
114+
split = component.split("**")
115+
split_re = [_translate(s, case_sensitive=case_sensitive) for s in split]
116+
re_patterns.append("/?" + ".*/?".join(split_re))
85117
else:
86118
re_patterns.append(
87119
"/" + _translate(component, case_sensitive=case_sensitive)
@@ -141,6 +173,88 @@ def imatch(pattern, path):
141173
return bool(re_pattern.match(path))
142174

143175

176+
def match_any(patterns, path):
177+
# type: (Iterable[Text], Text) -> bool
178+
"""Test if a path matches any of a list of patterns.
179+
180+
Will return `True` if ``patterns`` is an empty list.
181+
182+
Arguments:
183+
patterns (list): A list of wildcard pattern, e.g ``["*.py",
184+
"*.pyc"]``
185+
name (str): A filename.
186+
187+
Returns:
188+
bool: `True` if the path matches at least one of the patterns.
189+
190+
"""
191+
if not patterns:
192+
return True
193+
return any(match(pattern, path) for pattern in patterns)
194+
195+
196+
def imatch_any(patterns, path):
197+
# type: (Iterable[Text], Text) -> bool
198+
"""Test if a path matches any of a list of patterns (case insensitive).
199+
200+
Will return `True` if ``patterns`` is an empty list.
201+
202+
Arguments:
203+
patterns (list): A list of wildcard pattern, e.g ``["*.py",
204+
"*.pyc"]``
205+
name (str): A filename.
206+
207+
Returns:
208+
bool: `True` if the path matches at least one of the patterns.
209+
210+
"""
211+
if not patterns:
212+
return True
213+
return any(imatch(pattern, path) for pattern in patterns)
214+
215+
216+
def get_matcher(patterns, case_sensitive, accept_prefix=False):
217+
# type: (Iterable[Text], bool, bool) -> Callable[[Text], bool]
218+
"""Get a callable that matches paths against the given patterns.
219+
220+
Arguments:
221+
patterns (list): A list of wildcard pattern. e.g. ``["*.py",
222+
"*.pyc"]``
223+
case_sensitive (bool): If ``True``, then the callable will be case
224+
sensitive, otherwise it will be case insensitive.
225+
accept_prefix (bool): If ``True``, the name is
226+
not required to match the wildcards themselves
227+
but only need to be a prefix of a string that does.
228+
229+
Returns:
230+
callable: a matcher that will return `True` if the paths given as
231+
an argument matches any of the given patterns.
232+
233+
Example:
234+
>>> from fs import wildcard
235+
>>> is_python = wildcard.get_matcher(['*.py'], True)
236+
>>> is_python('__init__.py')
237+
True
238+
>>> is_python('foo.txt')
239+
False
240+
241+
"""
242+
if not patterns:
243+
return lambda name: True
244+
245+
if accept_prefix:
246+
new_patterns = []
247+
for pattern in patterns:
248+
split = _split_pattern_by_rec(pattern)
249+
for i in range(len(split)):
250+
new_pattern = "/".join(split[: i + 1])
251+
new_patterns.append(new_pattern)
252+
patterns = new_patterns
253+
254+
matcher = match_any if case_sensitive else imatch_any
255+
return partial(matcher, patterns)
256+
257+
144258
class Globber(object):
145259
"""A generator of glob results."""
146260

fs/walk.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,13 @@ def _check_open_dir(self, fs, path, info):
218218
full_path = ("" if path == "/" else path) + "/" + info.name
219219
if self.exclude_dirs is not None and fs.match(self.exclude_dirs, info.name):
220220
return False
221-
if self.exclude_glob is not None and fs.match(self.exclude_glob, full_path):
222-
return False
223-
if self.filter_dirs is not None and not fs.match(
224-
self.filter_dirs, info.name, accept_prefix=True
221+
if self.exclude_glob is not None and fs.match_glob(
222+
self.exclude_glob, full_path
225223
):
226224
return False
227-
if self.filter_glob is not None and not fs.match(
225+
if self.filter_dirs is not None and not fs.match(self.filter_dirs, info.name):
226+
return False
227+
if self.filter_glob is not None and not fs.match_glob(
228228
self.filter_glob, full_path, accept_prefix=True
229229
):
230230
return False
@@ -281,13 +281,13 @@ def _check_file(self, fs, dir_path, info):
281281
if Walker._check_file == type(self)._check_file:
282282
if self.exclude is not None and fs.match(self.exclude, info.name):
283283
return False
284-
if self.exclude_glob is not None and fs.match(
284+
if self.exclude_glob is not None and fs.match_glob(
285285
self.exclude_glob, dir_path + "/" + info.name
286286
):
287287
return False
288288
if self.filter is not None and not fs.match(self.filter, info.name):
289289
return False
290-
if self.filter_glob is not None and not fs.match(
290+
if self.filter_glob is not None and not fs.match_glob(
291291
self.filter_glob, dir_path + "/" + info.name, accept_prefix=True
292292
):
293293
return False

0 commit comments

Comments
 (0)