1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
# standard imports
from collections.abc import Callable, Collection, Hashable
import abc
import os
import typing
# external imports
import magic
# exports
__all__: typing.Sequence[str] = []
## code ##
# abstract nodes
class Matcher(abc.ABC, Hashable, Callable, Collection): # type: ignore [misc] # Invalid base class Callable
"""Matcher node base class."""
# child expressions or terminals
_childs: typing.Set[typing.Any]
def __init__(self, *childs: typing.Any):
if len(childs) == 1 and isinstance(childs[0], (list, tuple, set)):
self._childs = set(childs[0])
else:
self._childs = set(childs)
def __contains__(self, needle: typing.Any) -> bool:
return needle in self._childs
def __iter__(self) -> typing.Iterator[typing.Any]:
return iter(self._childs)
def __len__(self) -> int:
return len(self._childs)
def __repr__(self) -> str:
return f'{type(self).__name__}({self._childs})'
def __hash__(self) -> int:
return hash((type(self), tuple(set(self._childs))))
def __eq__(self, other: typing.Any) -> bool:
return isinstance(other, type(self)) \
and self._childs == other._childs
@abc.abstractmethod
def __call__(self, path: str) -> bool: # pylint: disable=arguments-differ
"""Check if *path* satisfies the conditions set by the Matcher instance."""
class NOT(Matcher):
"""Invert a matcher result."""
def __init__(self, expr: Matcher):
super().__init__(expr)
def __call__(self, path: str) -> bool:
return not next(iter(self._childs))(path)
# aggregate nodes
class Aggregate(Matcher): # pylint: disable=too-few-public-methods # Yeah, it's an interface...
"""Aggregation function base class (And, Or)."""
class And(Aggregate):
"""Accept only if all conditions are satisfied."""
def __call__(self, path: str) -> bool:
for itm in self:
if not itm(path):
return False
return True
class Or(Aggregate):
"""Accept only if at least one condition is satisfied."""
def __call__(self, path: str) -> bool:
for itm in self:
if itm(path):
return True
return False
# criteria nodes
class Criterion(Matcher):
"""Criterion base class. Limits acceptance to certain values."""
def accepted(self) -> typing.Set[typing.Any]:
"""Return a set of accepted values."""
return self._childs
# criteria w/o value (valueless)
class Any(Criterion):
"""Accepts anything."""
def __call__(self, path: str) -> bool:
return True
class Nothing(Criterion):
"""Accepts nothing."""
def __call__(self, path: str) -> bool:
return False
class Exists(Criterion):
"""Filters by existence."""
def __call__(self, path: str) -> bool:
return os.path.exists(path)
class IsFile(Criterion):
"""Checks if the path is a regular file."""
def __call__(self, path: str) -> bool:
return os.path.isfile(path)
class IsDir(Criterion):
"""Checks if the path is a directory."""
def __call__(self, path: str) -> bool:
return os.path.isdir(path)
class IsLink(Criterion):
"""Checks if the path is a link."""
def __call__(self, path: str) -> bool:
return os.path.islink(path)
class IsAbs(Criterion):
"""Checks if the path is an absolute path."""
def __call__(self, path: str) -> bool:
return os.path.isabs(path)
class IsRel(Criterion):
"""Checks if the path is a relative path."""
def __call__(self, path: str) -> bool:
return not os.path.isabs(path)
class IsMount(Criterion):
"""Checks if the path is a mount point."""
def __call__(self, path: str) -> bool:
return os.path.ismount(path)
class IsEmpty(Criterion):
"""Checks if the path is an empty file."""
def __call__(self, path: str) -> bool:
return os.path.exists(path) and os.stat(path).st_size == 0
class IsReadable(Criterion):
"""Checks if the path is readable."""
def __call__(self, path: str) -> bool:
return os.path.exists(path) and os.access(path, os.R_OK)
class IsWritable(Criterion):
"""Checks if the path is writable."""
def __call__(self, path: str) -> bool:
return os.path.exists(path) and os.access(path, os.W_OK)
class IsExecutable(Criterion):
"""Checks if the path is executable."""
def __call__(self, path: str) -> bool:
return os.path.exists(path) and os.access(path, os.X_OK)
# criteria w/ value
class Extension(Criterion):
"""Filters by file extension (without the dot)."""
def __call__(self, path: str) -> bool:
_, ext = os.path.splitext(path)
return ext[1:] in self.accepted()
class Mime(Criterion):
"""Filters by mime type."""
def __call__(self, path: str) -> bool:
try:
return magic.from_file(path, mime=True).lower() in self.accepted()
except FileNotFoundError:
return False
## EOF ##
|