file-groups 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- file_groups/config_files.py +158 -106
- file_groups/groups.py +16 -22
- file_groups/handler.py +7 -7
- file_groups/handler_compare.py +7 -8
- {file_groups-0.2.0.dist-info → file_groups-0.3.0.dist-info}/METADATA +1 -1
- file_groups-0.3.0.dist-info/RECORD +14 -0
- {file_groups-0.2.0.dist-info → file_groups-0.3.0.dist-info}/WHEEL +1 -1
- file_groups-0.2.0.dist-info/RECORD +0 -14
- {file_groups-0.2.0.dist-info → file_groups-0.3.0.dist-info}/LICENSE.txt +0 -0
- {file_groups-0.2.0.dist-info → file_groups-0.3.0.dist-info}/top_level.txt +0 -0
- {file_groups-0.2.0.dist-info → file_groups-0.3.0.dist-info}/zip-safe +0 -0
file_groups/config_files.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
import ast
|
2
2
|
import os
|
3
|
+
import errno
|
3
4
|
import re
|
4
5
|
from pathlib import Path
|
5
6
|
import itertools
|
6
7
|
from pprint import pformat
|
7
8
|
import logging
|
8
|
-
from
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from typing import Tuple, Sequence, cast
|
9
11
|
|
10
12
|
from appdirs import AppDirs # type: ignore
|
11
13
|
|
@@ -19,11 +21,49 @@ class ConfigException(Exception):
|
|
19
21
|
"""Invalid configuration"""
|
20
22
|
|
21
23
|
|
24
|
+
@dataclass
|
25
|
+
class DirConfig():
|
26
|
+
"""Hold protect config for a directory, or global (site or user) config."""
|
27
|
+
protect: dict[str, set[re.Pattern]]
|
28
|
+
config_dir: Path|None
|
29
|
+
config_files: Sequence[str]
|
30
|
+
|
31
|
+
def is_protected(self, ff: FsPath):
|
32
|
+
"""If ff id protected by a regex pattern then return the pattern, otherwise return None."""
|
33
|
+
|
34
|
+
for pattern in itertools.chain(self.protect["local"], self.protect["recursive"]):
|
35
|
+
if os.sep in str(pattern):
|
36
|
+
# Match against full path
|
37
|
+
assert os.path.isabs(ff), f"Expected absolute path, got '{ff}'"
|
38
|
+
if pattern.search(os.fspath(ff)):
|
39
|
+
return pattern
|
40
|
+
|
41
|
+
elif pattern.search(ff.name):
|
42
|
+
return pattern
|
43
|
+
|
44
|
+
return None
|
45
|
+
|
46
|
+
def __json__(self):
|
47
|
+
return {
|
48
|
+
"DirConfig": {
|
49
|
+
"protect": {key: list(str(pat) for pat in val) for key, val in self.protect.items()},
|
50
|
+
"config_dir": str(self.config_dir),
|
51
|
+
"config_files": self.config_files,
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
|
22
56
|
class ConfigFiles():
|
23
57
|
r"""Handle config files.
|
24
58
|
|
25
59
|
Config files are searched for in the standard config directories on the platform AND in any collected directory.
|
26
|
-
|
60
|
+
|
61
|
+
The 'app_dirs' default sets default config dirs and config-file names.
|
62
|
+
It is also possible to specify additional or alternative config files specific to the application using this library.
|
63
|
+
Config files must be named after the AppDirs.appname (first argument) as <appname>.conf or .<appname>.conf.
|
64
|
+
The defaults are 'file_groups.conf' and '.file_groups.conf'.
|
65
|
+
You should consider carefully before disabling loading of the default files, as an end user likely wants the protection rules to apply for any
|
66
|
+
application using this library.
|
27
67
|
|
28
68
|
The content of a conf file is a Python dict with the following structure.
|
29
69
|
|
@@ -56,14 +96,14 @@ class ConfigFiles():
|
|
56
96
|
}
|
57
97
|
}
|
58
98
|
|
59
|
-
The level one
|
60
|
-
Applications are free to add entries at this level.
|
99
|
+
The level one key is 'file_groups'.
|
100
|
+
Applications are free to add entries at this level, but not underneath. This is protect against ignored misspelled keys.
|
61
101
|
|
62
102
|
The 'file_groups' entry is a dict with a single 'protect' entry.
|
63
103
|
The 'protect' entry is a dict with at most three entries: 'local', 'recursive' and 'global'. These specify whether a directory specific
|
64
104
|
configuration will inherit and extend the parent (and global) config, or whether it is local to current directory only.
|
65
105
|
The 'local', 'recursive' and 'global' entries are lists of regex patterns to match against collected 'work_on' files.
|
66
|
-
Regexes are checked against the simple
|
106
|
+
Regexes are checked against the simple file name (i.e. not the full path) unless they contain at least one path separator (os.sep), in
|
67
107
|
which case they are checked against the absolute path.
|
68
108
|
All checks are done as regex *search* (better to protect too much than too little). Write the regex to match the full name or path if needed.
|
69
109
|
|
@@ -74,14 +114,18 @@ class ConfigFiles():
|
|
74
114
|
ignore_config_dirs_config_files: Ignore config files in standard config directories.
|
75
115
|
ignore_per_directory_config_files: Ignore config files in collected directories.
|
76
116
|
remember_configs: Store loaded and merged configs in `dir_configs` member variable.
|
117
|
+
app_dirs: Provide your own instance of AppDirs in addition to or as a replacement of the default to add config file names and path.
|
118
|
+
Configuration from later entries have higher precedence.
|
119
|
+
Note that if no AppDirs are specified, no config files will be loaded, neither from config dirs, nor from collected directories.
|
120
|
+
See: https://pypi.org/project/appdirs/
|
77
121
|
|
78
122
|
Members:
|
79
|
-
global_config: dict
|
80
123
|
remember_configs: Whether per directory resolved/merged configs are stored in `dir_configs`.
|
81
124
|
dir_configs: dict[str: dict] Mapping from dir name to directory specific config dict. Only if remember_configs is True.
|
82
125
|
"""
|
83
126
|
|
84
|
-
|
127
|
+
default_appdirs: AppDirs = AppDirs("file_groups", "Hupfeldt_IT")
|
128
|
+
|
85
129
|
_fg_key = "file_groups"
|
86
130
|
_protect_key = "protect"
|
87
131
|
_valid_dir_protect_scopes = ("local", "recursive")
|
@@ -89,104 +133,76 @@ class ConfigFiles():
|
|
89
133
|
|
90
134
|
def __init__(
|
91
135
|
self, protect: Sequence[re.Pattern] = (),
|
92
|
-
ignore_config_dirs_config_files=False, ignore_per_directory_config_files=False, remember_configs=
|
136
|
+
ignore_config_dirs_config_files=False, ignore_per_directory_config_files=False, remember_configs=True,
|
137
|
+
app_dirs: Sequence[AppDirs]|None = None,
|
138
|
+
*,
|
139
|
+
config_file: Path|None = None,
|
140
|
+
):
|
93
141
|
super().__init__()
|
94
|
-
self.remember_configs = remember_configs
|
95
142
|
|
96
|
-
self.
|
97
|
-
|
98
|
-
"
|
99
|
-
|
100
|
-
"local": set(),
|
101
|
-
"recursive": set(protect),
|
102
|
-
}
|
103
|
-
}
|
104
|
-
}
|
143
|
+
self._global_config = DirConfig({
|
144
|
+
"local": set(),
|
145
|
+
"recursive": set(protect),
|
146
|
+
}, None, ())
|
105
147
|
|
148
|
+
self.remember_configs = remember_configs
|
149
|
+
self.per_dir_configs: dict[str, DirConfig] = {} # key is abs_dir_path
|
106
150
|
self.ignore_per_directory_config_files = ignore_per_directory_config_files
|
107
151
|
|
152
|
+
app_dirs = app_dirs or (ConfigFiles.default_appdirs,)
|
153
|
+
self.conf_file_names = tuple((apd.appname + ".conf", "." + apd.appname + ".conf") for apd in app_dirs)
|
154
|
+
_LOG.debug("Conf file names: %s", self.conf_file_names)
|
155
|
+
self.config_dirs = []
|
108
156
|
if not ignore_config_dirs_config_files:
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
if not conf_dir.exists():
|
116
|
-
continue
|
117
|
-
|
118
|
-
new_config, _ = self._read_and_validate_config_file(conf_dir, self.global_config, self._valid_config_dir_protect_scopes, False)
|
119
|
-
if self.remember_configs:
|
120
|
-
self.per_dir_configs[str(conf_dir)] = new_config
|
121
|
-
|
122
|
-
fpt = new_config["file_groups"]["protect"]
|
123
|
-
cast(set, gfpt["recursive"]).update(fpt.get("global", ()))
|
124
|
-
_LOG.debug("Merged global config:\n %s", pformat(new_config))
|
125
|
-
|
126
|
-
try:
|
127
|
-
del fpt['global']
|
128
|
-
except KeyError:
|
129
|
-
pass
|
157
|
+
for appd in app_dirs:
|
158
|
+
self.config_dirs.extend(appd.site_config_dir.split(':'))
|
159
|
+
for appd in app_dirs:
|
160
|
+
self.config_dirs.append(appd.user_config_dir)
|
161
|
+
|
162
|
+
self.config_file = config_file
|
130
163
|
|
131
164
|
# self.default_config_file_example = self.default_config_file.with_suffix('.example.py')
|
132
165
|
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
num_files = 0
|
138
|
-
for cfn in self._conf_file_names:
|
139
|
-
tmp_conf_file = conf_dir/cfn
|
140
|
-
if tmp_conf_file.exists():
|
141
|
-
conf_file = tmp_conf_file
|
142
|
-
num_files += 1
|
143
|
-
|
144
|
-
if num_files == 1:
|
145
|
-
if ignore_config_files:
|
146
|
-
_LOG.debug("Ignoring config file: %s", conf_file)
|
147
|
-
return None, None
|
148
|
-
|
149
|
-
_LOG.debug("Read config file: %s", conf_file)
|
150
|
-
with open(conf_file, encoding="utf-8") as fh:
|
151
|
-
new_config = ast.literal_eval(fh.read())
|
152
|
-
_LOG.debug("%s", pformat(new_config))
|
153
|
-
return new_config, conf_file
|
154
|
-
|
155
|
-
if num_files == 0:
|
156
|
-
_LOG.debug("No config file in directory %s", conf_dir)
|
157
|
-
return None, None
|
158
|
-
|
159
|
-
msg = f"More than one config file in dir '{conf_dir}': {self._conf_file_names}."
|
160
|
-
_LOG.debug("%s", msg)
|
161
|
-
raise ConfigException(msg)
|
162
|
-
|
163
|
-
def _read_and_validate_config_file(
|
164
|
-
self, conf_dir: Path, parent_conf: dict, valid_protect_scopes: Tuple[str, ...], ignore_config_files: bool
|
165
|
-
) -> Tuple[dict, Path|None]:
|
166
|
+
def _read_and_validate_config_file_for_one_appname( # pylint: disable=too-many-locals
|
167
|
+
self, conf_dir: Path, conf_file_name_pair: Sequence[str], parent_conf: DirConfig, valid_protect_scopes: Tuple[str, ...], ignore_config_files: bool
|
168
|
+
) -> Tuple[dict[str, set[re.Pattern]], str|None]:
|
166
169
|
"""Read config file, validate keys and compile regexes and merge with parent.
|
167
170
|
|
171
|
+
Error if config files are found both with and withput '.' prefix.
|
168
172
|
Merge parent conf into conf_dir conf (if any) and return the merged dict. The parent conf is not modified.
|
169
173
|
|
170
|
-
Return: merged config dict with compiled regexes.
|
174
|
+
Return: merged config dict with compiled regexes, config file name. If no config files is found, then return inherited parent conf and None.
|
171
175
|
"""
|
172
176
|
|
173
177
|
assert conf_dir.is_absolute()
|
178
|
+
_LOG.debug("Checking for config files %s in directory: %s", conf_file_name_pair, conf_dir)
|
174
179
|
|
175
|
-
|
176
|
-
|
177
|
-
"
|
180
|
+
match [conf_dir/cfn for cfn in conf_file_name_pair if (conf_dir/cfn).exists()]:
|
181
|
+
case []:
|
182
|
+
_LOG.debug("No config file in directory %s", conf_dir)
|
183
|
+
no_conf_file: dict[str, set[re.Pattern]] = {
|
178
184
|
"local": set(),
|
179
|
-
"recursive": parent_conf
|
185
|
+
"recursive": parent_conf.protect["recursive"]
|
180
186
|
}
|
181
|
-
|
182
|
-
|
187
|
+
return no_conf_file, None
|
188
|
+
|
189
|
+
case [conf_file]:
|
190
|
+
if ignore_config_files:
|
191
|
+
_LOG.debug("Ignoring config file: %s", conf_file)
|
192
|
+
return self._global_config.protect, None
|
193
|
+
|
194
|
+
_LOG.debug("Read config file: %s", conf_file)
|
195
|
+
with open(conf_file, encoding="utf-8") as fh:
|
196
|
+
new_config = ast.literal_eval(fh.read())
|
197
|
+
_LOG.debug("%s", pformat(new_config))
|
183
198
|
|
184
|
-
|
185
|
-
|
186
|
-
|
199
|
+
case config_files:
|
200
|
+
msg = f"More than one config file in dir '{conf_dir}': {[cf.name for cf in config_files]}."
|
201
|
+
_LOG.debug("%s", msg)
|
202
|
+
raise ConfigException(msg)
|
187
203
|
|
188
204
|
try:
|
189
|
-
protect_conf = new_config[self._fg_key][self._protect_key]
|
205
|
+
protect_conf: dict[str, set[re.Pattern]] = new_config[self._fg_key][self._protect_key]
|
190
206
|
except KeyError as ex:
|
191
207
|
raise ConfigException(f"Config file '{conf_file}' is missing mandatory configuration '{self._fg_key}[{self._protect_key}]'.") from ex
|
192
208
|
|
@@ -198,7 +214,7 @@ class ConfigFiles():
|
|
198
214
|
|
199
215
|
protect_conf[key] = set(re.compile(pattern) for pattern in val)
|
200
216
|
if key == "recursive":
|
201
|
-
protect_conf[key].update(parent_conf
|
217
|
+
protect_conf[key].update(parent_conf.protect[key])
|
202
218
|
|
203
219
|
for key in self._valid_dir_protect_scopes: # Do NOT use the 'valid_protect_scopes' argument here
|
204
220
|
protect_conf.setdefault(key, set())
|
@@ -207,32 +223,68 @@ class ConfigFiles():
|
|
207
223
|
if _LOG.isEnabledFor(lvl):
|
208
224
|
_LOG.log(lvl, "Merged directory config:\n%s", pformat(new_config))
|
209
225
|
|
210
|
-
return
|
211
|
-
|
212
|
-
def
|
226
|
+
return protect_conf, conf_file.name
|
227
|
+
|
228
|
+
def _read_and_validate_config_files(
|
229
|
+
self, conf_dir: Path, parent_conf: DirConfig, valid_protect_scopes: Tuple[str, ...], ignore_config_files: bool) -> DirConfig:
|
230
|
+
cfg_merge: dict[str, set[re.Pattern]] = {}
|
231
|
+
cfg_files: list[str] = []
|
232
|
+
for conf_file_name_pair in self.conf_file_names:
|
233
|
+
cfg, cfg_file = self._read_and_validate_config_file_for_one_appname(
|
234
|
+
conf_dir, conf_file_name_pair, parent_conf, valid_protect_scopes, ignore_config_files)
|
235
|
+
for key, val in cfg.items():
|
236
|
+
cfg_merge.setdefault(key, set()).update(val)
|
237
|
+
if cfg_file:
|
238
|
+
cfg_files.append(cfg_file)
|
239
|
+
|
240
|
+
return DirConfig(cfg_merge, conf_dir, cfg_files)
|
241
|
+
|
242
|
+
def load_config_dir_files(self) -> None:
|
243
|
+
"""Load config files from platform standard directories and specified config file, if any."""
|
244
|
+
|
245
|
+
def merge_one_config_to_global(conf_dir, new_config):
|
246
|
+
if self.remember_configs:
|
247
|
+
self.per_dir_configs[str(conf_dir)] = new_config
|
248
|
+
|
249
|
+
self._global_config.protect["recursive"].update(new_config.protect.get("global", set()))
|
250
|
+
_LOG.debug("Merged global config:\n %s", new_config)
|
251
|
+
|
252
|
+
try:
|
253
|
+
del new_config.protect['global']
|
254
|
+
except KeyError:
|
255
|
+
pass
|
256
|
+
|
257
|
+
_LOG.debug("config_dirs: %s", self.config_dirs)
|
258
|
+
for conf_dir in self.config_dirs:
|
259
|
+
conf_dir = Path(conf_dir)
|
260
|
+
if not conf_dir.exists():
|
261
|
+
continue
|
262
|
+
|
263
|
+
new_config = self._read_and_validate_config_files(conf_dir, self._global_config, self._valid_config_dir_protect_scopes, False)
|
264
|
+
merge_one_config_to_global(conf_dir, new_config)
|
265
|
+
|
266
|
+
if self.config_file:
|
267
|
+
conf_dir = self.config_file.parent.absolute()
|
268
|
+
conf_name = self.config_file.name
|
269
|
+
cfg, filename = self._read_and_validate_config_file_for_one_appname(
|
270
|
+
conf_dir, (conf_name,), self._global_config, self._valid_config_dir_protect_scopes, ignore_config_files=False)
|
271
|
+
if not filename:
|
272
|
+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self.config_file))
|
273
|
+
|
274
|
+
merge_one_config_to_global(conf_dir, DirConfig(cfg, conf_dir, (cast(str, filename),)))
|
275
|
+
|
276
|
+
def dir_config(self, conf_dir: Path, parent_conf: DirConfig|None) -> DirConfig:
|
213
277
|
"""Read and merge config file from directory 'conf_dir' with 'parent_conf'.
|
214
278
|
|
215
|
-
If directory has no parent in the file_groups included dirs, then
|
279
|
+
If directory has no parent in the file_groups included dirs, then None should be supplied as parent_conf.
|
216
280
|
"""
|
217
281
|
|
218
|
-
new_config
|
219
|
-
conf_dir, parent_conf, self._valid_dir_protect_scopes, self.ignore_per_directory_config_files)
|
282
|
+
new_config = self._read_and_validate_config_files(
|
283
|
+
conf_dir, parent_conf or self._global_config, self._valid_dir_protect_scopes, self.ignore_per_directory_config_files)
|
284
|
+
_LOG.debug("new_config:\n %s", new_config)
|
285
|
+
|
220
286
|
if self.remember_configs:
|
221
287
|
self.per_dir_configs[str(conf_dir)] = new_config
|
222
|
-
|
223
|
-
|
224
|
-
def is_protected(self, ff: FsPath, dir_config: Mapping):
|
225
|
-
"""If ff id protected by a regex patterm then return the pattern, otherwise return None."""
|
226
|
-
|
227
|
-
cfg_protected = dir_config[self._fg_key][self._protect_key]
|
228
|
-
for pattern in itertools.chain(cfg_protected["local"], cfg_protected["recursive"]):
|
229
|
-
if os.sep in str(pattern):
|
230
|
-
# Match against full path
|
231
|
-
assert os.path.isabs(ff), f"Expected absolute path, got '{ff}'"
|
232
|
-
if pattern.search(os.fspath(ff)):
|
233
|
-
return pattern
|
234
|
-
|
235
|
-
elif pattern.search(ff.name):
|
236
|
-
return pattern
|
288
|
+
# _LOG.debug("per_dir_configs:\n %s", self.per_dir_configs)
|
237
289
|
|
238
|
-
return
|
290
|
+
return new_config
|
file_groups/groups.py
CHANGED
@@ -9,7 +9,7 @@ from enum import Enum
|
|
9
9
|
import logging
|
10
10
|
from typing import Sequence
|
11
11
|
|
12
|
-
from .config_files import ConfigFiles
|
12
|
+
from .config_files import DirConfig, ConfigFiles
|
13
13
|
|
14
14
|
|
15
15
|
_LOG = logging.getLogger(__name__)
|
@@ -35,14 +35,14 @@ class _Group():
|
|
35
35
|
num_directories: int = 0
|
36
36
|
num_directory_symlinks: int = 0
|
37
37
|
|
38
|
-
def add_entry_match(self, entry):
|
38
|
+
def add_entry_match(self, entry: DirEntry):
|
39
39
|
"""Abstract, but abstract and dataclass does not work with mypy. https://github.com/python/mypy/issues/500"""
|
40
40
|
|
41
41
|
@dataclass
|
42
42
|
class _IncludeMatchGroup(_Group):
|
43
43
|
include: re.Pattern|None = None
|
44
44
|
|
45
|
-
def add_entry_match(self, entry):
|
45
|
+
def add_entry_match(self, entry: DirEntry):
|
46
46
|
if not self.include:
|
47
47
|
self.files[entry.path] = entry
|
48
48
|
return
|
@@ -58,7 +58,7 @@ class _IncludeMatchGroup(_Group):
|
|
58
58
|
class _ExcludeMatchGroup(_Group):
|
59
59
|
exclude: re.Pattern|None = None
|
60
60
|
|
61
|
-
def add_entry_match(self, entry):
|
61
|
+
def add_entry_match(self, entry: DirEntry):
|
62
62
|
if not self.exclude:
|
63
63
|
self.files[entry.path] = entry
|
64
64
|
return
|
@@ -87,27 +87,21 @@ class FileGroups():
|
|
87
87
|
|
88
88
|
protect_exclude: Exclude files matching regex in the protected files (does not apply to symlinks). Default: Include ALL.
|
89
89
|
Note: Since these files are excluded from protection, it means they er NOT protected!
|
90
|
-
work_include:
|
90
|
+
work_include: ONLY include files matching regex in the may_work_on files (does not apply to symlinks). Default: Include ALL.
|
91
91
|
|
92
|
-
|
93
|
-
ignore_per_directory_config_files: Ignore config files in collected directories.
|
92
|
+
config_files: Load config files. See config_files.ConfigFiles.
|
94
93
|
"""
|
95
94
|
|
96
95
|
def __init__(
|
97
96
|
self,
|
98
97
|
protect_dirs_seq: Sequence[Path], work_dirs_seq: Sequence[Path],
|
99
98
|
*,
|
100
|
-
protect: Sequence[re.Pattern] = (),
|
101
99
|
protect_exclude: re.Pattern|None = None, work_include: re.Pattern|None = None,
|
102
|
-
|
103
|
-
remember_configs=True):
|
100
|
+
config_files: ConfigFiles|None = None):
|
104
101
|
super().__init__()
|
105
102
|
|
106
|
-
self.config_files = ConfigFiles(
|
107
|
-
|
108
|
-
ignore_config_dirs_config_files=ignore_config_dirs_config_files,
|
109
|
-
ignore_per_directory_config_files=ignore_per_directory_config_files,
|
110
|
-
remember_configs=remember_configs)
|
103
|
+
self.config_files = config_files or ConfigFiles()
|
104
|
+
self.config_files.load_config_dir_files()
|
111
105
|
|
112
106
|
# Turn all paths into absolute paths with symlinks resolved, keep referrence to original argument for messages
|
113
107
|
protect_dirs: dict[str, Path] = {os.path.abspath(os.path.realpath(kp)): kp for kp in protect_dirs_seq}
|
@@ -153,8 +147,8 @@ class FileGroups():
|
|
153
147
|
top/d2/d1
|
154
148
|
top/d2/d1/f1.jpg
|
155
149
|
|
156
|
-
When:
|
157
|
-
And:
|
150
|
+
When: work_dirs_seq is [top, top/d1/d1]
|
151
|
+
And: protect_dirs_seq is [top/d1]
|
158
152
|
|
159
153
|
Then:
|
160
154
|
|
@@ -175,7 +169,7 @@ class FileGroups():
|
|
175
169
|
|
176
170
|
checked_dirs: set[str] = set()
|
177
171
|
|
178
|
-
def find_group(abs_dir_path: str, group: _Group, other_group: _Group, parent_conf:
|
172
|
+
def find_group(abs_dir_path: str, group: _Group, other_group: _Group, parent_conf: DirConfig|None):
|
179
173
|
"""Find all files belonging to 'group'"""
|
180
174
|
_LOG.debug("find %s: %s", group.typ.name, abs_dir_path)
|
181
175
|
if abs_dir_path in checked_dirs:
|
@@ -183,7 +177,7 @@ class FileGroups():
|
|
183
177
|
return
|
184
178
|
|
185
179
|
group.num_directories += 1
|
186
|
-
dir_config
|
180
|
+
dir_config = self.config_files.dir_config(Path(abs_dir_path), parent_conf)
|
187
181
|
|
188
182
|
for entry in os.scandir(abs_dir_path):
|
189
183
|
if entry.is_dir(follow_symlinks=False):
|
@@ -195,13 +189,13 @@ class FileGroups():
|
|
195
189
|
find_group(entry.path, group, other_group, dir_config)
|
196
190
|
continue
|
197
191
|
|
198
|
-
if
|
192
|
+
if entry.name in dir_config.config_files:
|
199
193
|
continue
|
200
194
|
|
201
195
|
current_group = group
|
202
196
|
if group.typ is GroupType.MAY_WORK_ON:
|
203
197
|
# We need to check for match against configured protect patterns, if match, then the file must got to protect group instead
|
204
|
-
pattern =
|
198
|
+
pattern = dir_config.is_protected(entry)
|
205
199
|
if pattern:
|
206
200
|
_LOG.debug("find %s - '%s' is protected by regex %s, assigning to group %s instead.", group.typ.name, entry.path, pattern, other_group.typ.name)
|
207
201
|
current_group = other_group
|
@@ -233,7 +227,7 @@ class FileGroups():
|
|
233
227
|
|
234
228
|
parent_dir = parent_dir.parent
|
235
229
|
else:
|
236
|
-
parent_conf =
|
230
|
+
parent_conf = None
|
237
231
|
|
238
232
|
if any_dir in self.must_protect.dirs:
|
239
233
|
find_group(any_dir, self.must_protect, self.may_work_on, parent_conf)
|
file_groups/handler.py
CHANGED
@@ -7,6 +7,7 @@ import logging
|
|
7
7
|
from typing import Sequence
|
8
8
|
|
9
9
|
from .groups import FileGroups
|
10
|
+
from .config_files import ConfigFiles
|
10
11
|
|
11
12
|
|
12
13
|
_LOG = logging.getLogger(__name__)
|
@@ -20,9 +21,8 @@ class FileHandler(FileGroups):
|
|
20
21
|
Re-link symlinks when a file being deleted has a corresponding file.
|
21
22
|
|
22
23
|
Arguments:
|
23
|
-
protect_dirs_seq, work_dirs_seq, protect_exclude, work_include: See `FileGroups` class.
|
24
|
-
dry_run: Don't
|
25
|
-
protected_regexes: Protect files matching this from being deleted or moved.
|
24
|
+
protect_dirs_seq, work_dirs_seq, protect_exclude, work_include, config_files: See `FileGroups` class.
|
25
|
+
dry_run: Don't change any files.
|
26
26
|
delete_symlinks_instead_of_relinking: Normal operation is to re-link to a 'corresponding' or renamed file when renaming or deleting a file.
|
27
27
|
If delete_symlinks_instead_of_relinking is true, then symlinks in work_on dirs pointing to renamed/deletes files will be deleted even if
|
28
28
|
they could have logically been made to point to a file in a protect dir.
|
@@ -32,14 +32,14 @@ class FileHandler(FileGroups):
|
|
32
32
|
self,
|
33
33
|
protect_dirs_seq: Sequence[Path], work_dirs_seq: Sequence[Path],
|
34
34
|
*,
|
35
|
-
dry_run: bool,
|
36
|
-
protected_regexes: Sequence[re.Pattern],
|
37
35
|
protect_exclude: re.Pattern|None = None, work_include: re.Pattern|None = None,
|
36
|
+
config_files: ConfigFiles|None = None,
|
37
|
+
dry_run: bool,
|
38
38
|
delete_symlinks_instead_of_relinking=False):
|
39
39
|
super().__init__(
|
40
|
-
protect=protected_regexes,
|
41
40
|
protect_dirs_seq=protect_dirs_seq, work_dirs_seq=work_dirs_seq,
|
42
|
-
protect_exclude=protect_exclude, work_include=work_include
|
41
|
+
protect_exclude=protect_exclude, work_include=work_include,
|
42
|
+
config_files=config_files)
|
43
43
|
|
44
44
|
self.dry_run = dry_run
|
45
45
|
self.delete_symlinks_instead_of_relinking = delete_symlinks_instead_of_relinking
|
file_groups/handler_compare.py
CHANGED
@@ -7,6 +7,7 @@ from typing import Sequence
|
|
7
7
|
from .compare_files import CompareFiles
|
8
8
|
from .types import FsPath
|
9
9
|
from .handler import FileHandler
|
10
|
+
from .config_files import ConfigFiles
|
10
11
|
|
11
12
|
|
12
13
|
_LOG = logging.getLogger(__name__)
|
@@ -16,7 +17,7 @@ class FileHandlerCompare(FileHandler):
|
|
16
17
|
"""Extend `FileHandler` with a compare method
|
17
18
|
|
18
19
|
Arguments:
|
19
|
-
protect_dirs_seq, work_dirs_seq, protect_exclude, work_include: See `FileGroups` class.
|
20
|
+
protect_dirs_seq, work_dirs_seq, protect_exclude, work_include, config_files: See `FileGroups` class.
|
20
21
|
dry_run, protected_regexes, delete_symlinks_instead_of_relinking: See `FileHandler` class.
|
21
22
|
fcmp: Object providing compare function.
|
22
23
|
"""
|
@@ -25,17 +26,15 @@ class FileHandlerCompare(FileHandler):
|
|
25
26
|
self,
|
26
27
|
protect_dirs_seq: Sequence[Path], work_dirs_seq: Sequence[Path], fcmp: CompareFiles,
|
27
28
|
*,
|
28
|
-
dry_run: bool,
|
29
|
-
protected_regexes: Sequence[re.Pattern],
|
30
29
|
protect_exclude: re.Pattern|None = None, work_include: re.Pattern|None = None,
|
30
|
+
config_files: ConfigFiles|None = None,
|
31
|
+
dry_run: bool,
|
31
32
|
delete_symlinks_instead_of_relinking=False):
|
32
33
|
super().__init__(
|
33
|
-
protect_dirs_seq=protect_dirs_seq,
|
34
|
-
|
34
|
+
protect_dirs_seq=protect_dirs_seq, work_dirs_seq=work_dirs_seq,
|
35
|
+
protect_exclude=protect_exclude, work_include=work_include,
|
36
|
+
config_files=config_files,
|
35
37
|
dry_run=dry_run,
|
36
|
-
protected_regexes=protected_regexes,
|
37
|
-
protect_exclude=protect_exclude,
|
38
|
-
work_include=work_include,
|
39
38
|
delete_symlinks_instead_of_relinking=delete_symlinks_instead_of_relinking)
|
40
39
|
|
41
40
|
self._fcmp = fcmp
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: file-groups
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Group files into 'protect' and 'work_on' and provide operations for safe delete/move and symlink handling.
|
5
5
|
Home-page: https://github.com/lhupfeldt/file_groups.git
|
6
6
|
Author: Lars Hupfeldt Nielsen
|
@@ -0,0 +1,14 @@
|
|
1
|
+
file_groups/__init__.py,sha256=1OO3tLnRnIe51VfZSQz0sqUaUP-xS7oHxiSexnKwpEY,94
|
2
|
+
file_groups/compare_files.py,sha256=38EfZzpEpj0wH7SUruPxgyF_W8eqCbIjG6yUOx8hcFA,434
|
3
|
+
file_groups/config_files.py,sha256=kU1wTANXjk5H95x1TpxIBFZrobJFjTMbQ_mjpnewh-U,13011
|
4
|
+
file_groups/groups.py,sha256=R3wF8DV3rhMckjgz6wuycxJ9UxAB2p56zzYcdbh72M0,11072
|
5
|
+
file_groups/handler.py,sha256=X_uFNAMtuiBro_8WG6XFEq_Ys4L8175tSP_gajA01fo,10045
|
6
|
+
file_groups/handler_compare.py,sha256=J6Jev9ev9oECzYCD9M6FUlkKMMMkaP2YlygG11lKqPI,2136
|
7
|
+
file_groups/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
file_groups/types.py,sha256=giq3H58RuKpS6b7sjxnzP-sN1Xr4VTQ-rG4brLP5qMI,107
|
9
|
+
file_groups-0.3.0.dist-info/LICENSE.txt,sha256=NZc9ictLEwfoL4ywpAbLbd-n02KETGHLYQU820TJD_Q,1494
|
10
|
+
file_groups-0.3.0.dist-info/METADATA,sha256=BseS9ayqXIwuDBBUb95mPQUcLmrHbmHLO7mSGOr9IEQ,1512
|
11
|
+
file_groups-0.3.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
12
|
+
file_groups-0.3.0.dist-info/top_level.txt,sha256=mAdRf0R2TA8tpH7ftHzXP7VMRhw07p7B2mI8QKpDnjU,12
|
13
|
+
file_groups-0.3.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
14
|
+
file_groups-0.3.0.dist-info/RECORD,,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
file_groups/__init__.py,sha256=1OO3tLnRnIe51VfZSQz0sqUaUP-xS7oHxiSexnKwpEY,94
|
2
|
-
file_groups/compare_files.py,sha256=38EfZzpEpj0wH7SUruPxgyF_W8eqCbIjG6yUOx8hcFA,434
|
3
|
-
file_groups/config_files.py,sha256=pb1HXk-wUbD9R5iztuiYqsLewD_oS7rEFPDJt49Y_Ww,10007
|
4
|
-
file_groups/groups.py,sha256=6v2LpzJqBHBWEToMI2RwqVy3ktIdWm2XIpS_tX6u94E,11501
|
5
|
-
file_groups/handler.py,sha256=06_8_fBEaHndrfUdxMsFw6KZw8PRcRj4MsH4FY3I4sU,10083
|
6
|
-
file_groups/handler_compare.py,sha256=78TXl9QFMIL5gZQhiLYvbzF5OidS7kcaRz6zCCNVG6I,2120
|
7
|
-
file_groups/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
file_groups/types.py,sha256=giq3H58RuKpS6b7sjxnzP-sN1Xr4VTQ-rG4brLP5qMI,107
|
9
|
-
file_groups-0.2.0.dist-info/LICENSE.txt,sha256=NZc9ictLEwfoL4ywpAbLbd-n02KETGHLYQU820TJD_Q,1494
|
10
|
-
file_groups-0.2.0.dist-info/METADATA,sha256=fsDleiP5-XzNVRXiMK1xLx-NPbKOK3Qym18dkXA8iKQ,1512
|
11
|
-
file_groups-0.2.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
12
|
-
file_groups-0.2.0.dist-info/top_level.txt,sha256=mAdRf0R2TA8tpH7ftHzXP7VMRhw07p7B2mI8QKpDnjU,12
|
13
|
-
file_groups-0.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
14
|
-
file_groups-0.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|