file-groups 0.2.1__tar.gz → 0.3.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. {file_groups-0.2.1 → file_groups-0.3.0}/PKG-INFO +1 -1
  2. {file_groups-0.2.1 → file_groups-0.3.0}/file_groups.egg-info/PKG-INFO +1 -1
  3. {file_groups-0.2.1 → file_groups-0.3.0}/file_groups.egg-info/SOURCES.txt +5 -0
  4. file_groups-0.3.0/src/config_files.py +290 -0
  5. {file_groups-0.2.1 → file_groups-0.3.0}/src/groups.py +13 -19
  6. {file_groups-0.2.1 → file_groups-0.3.0}/src/handler.py +7 -7
  7. {file_groups-0.2.1 → file_groups-0.3.0}/src/handler_compare.py +7 -8
  8. {file_groups-0.2.1 → file_groups-0.3.0}/test/config_files_test.py +171 -153
  9. file_groups-0.3.0/test/groups/config_files_test.py +54 -0
  10. {file_groups-0.2.1 → file_groups-0.3.0}/test/groups/re_protect_test.py +2 -1
  11. {file_groups-0.2.1 → file_groups-0.3.0}/test/handler/core_test.py +32 -32
  12. {file_groups-0.2.1 → file_groups-0.3.0}/test/handler/private_test.py +7 -6
  13. {file_groups-0.2.1 → file_groups-0.3.0}/test/handler/regex_protection_test.py +9 -8
  14. {file_groups-0.2.1 → file_groups-0.3.0}/test/handler_compare_test.py +4 -4
  15. file_groups-0.3.0/test/in/configs/config_files_sys_user_config_files_additional_appdirs/home/an_app.conf +15 -0
  16. file_groups-0.3.0/test/in/configs/config_files_sys_user_config_files_additional_appdirs/home/file_groups.conf +14 -0
  17. file_groups-0.3.0/test/in/configs/config_files_sys_user_config_files_additional_appdirs/sys/file_groups.conf +16 -0
  18. file_groups-0.3.0/test/in/configs/file_groups_group_files_by_config_protect/home/file_groups.conf +17 -0
  19. file_groups-0.2.1/src/config_files.py +0 -244
  20. file_groups-0.2.1/test/groups/config_files_test.py +0 -68
  21. {file_groups-0.2.1 → file_groups-0.3.0}/.copier-answers.yml +0 -0
  22. {file_groups-0.2.1 → file_groups-0.3.0}/.gitignore +0 -0
  23. {file_groups-0.2.1 → file_groups-0.3.0}/.pylintrc +0 -0
  24. {file_groups-0.2.1 → file_groups-0.3.0}/LICENSE.txt +0 -0
  25. {file_groups-0.2.1 → file_groups-0.3.0}/README.rst +0 -0
  26. {file_groups-0.2.1 → file_groups-0.3.0}/file_groups.egg-info/dependency_links.txt +0 -0
  27. {file_groups-0.2.1 → file_groups-0.3.0}/file_groups.egg-info/requires.txt +0 -0
  28. {file_groups-0.2.1 → file_groups-0.3.0}/file_groups.egg-info/top_level.txt +0 -0
  29. {file_groups-0.2.1 → file_groups-0.3.0}/file_groups.egg-info/zip-safe +0 -0
  30. {file_groups-0.2.1 → file_groups-0.3.0}/noxfile.py +0 -0
  31. {file_groups-0.2.1 → file_groups-0.3.0}/pyproject.toml +0 -0
  32. {file_groups-0.2.1 → file_groups-0.3.0}/pytest.ini +0 -0
  33. {file_groups-0.2.1 → file_groups-0.3.0}/setup.cfg +0 -0
  34. {file_groups-0.2.1 → file_groups-0.3.0}/setup.py +0 -0
  35. {file_groups-0.2.1 → file_groups-0.3.0}/src/__init__.py +0 -0
  36. {file_groups-0.2.1 → file_groups-0.3.0}/src/compare_files.py +0 -0
  37. {file_groups-0.2.1 → file_groups-0.3.0}/src/py.typed +0 -0
  38. {file_groups-0.2.1 → file_groups-0.3.0}/src/types.py +0 -0
  39. {file_groups-0.2.1 → file_groups-0.3.0}/test/.coveragerc +0 -0
  40. {file_groups-0.2.1 → file_groups-0.3.0}/test/__init__.py +0 -0
  41. {file_groups-0.2.1 → file_groups-0.3.0}/test/compare_files_test.py +0 -0
  42. {file_groups-0.2.1 → file_groups-0.3.0}/test/conftest.py +0 -0
  43. {file_groups-0.2.1 → file_groups-0.3.0}/test/groups/__init__.py +0 -0
  44. {file_groups-0.2.1 → file_groups-0.3.0}/test/groups/core_test.py +0 -0
  45. {file_groups-0.2.1 → file_groups-0.3.0}/test/groups/directory_symlinks_test.py +0 -0
  46. {file_groups-0.2.1 → file_groups-0.3.0}/test/groups/utils.py +0 -0
  47. {file_groups-0.2.1 → file_groups-0.3.0}/test/handler/__init__.py +0 -0
  48. {file_groups-0.2.1 → file_groups-0.3.0}/test/handler/utils.py +0 -0
  49. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_inherit_global_recursive/home/file_groups.conf +0 -0
  50. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_inherit_global_recursive/sys/file_groups.conf +0 -0
  51. /file_groups-0.2.1/test/in/configs/file_groups_group_files_by_config_protect/home/file_groups.conf → /file_groups-0.3.0/test/in/configs/config_files_specified/direct.conf +0 -0
  52. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_sys_config_file_no_global/sys/file_groups.conf +0 -0
  53. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_sys_user_config_files_no_global/home/file_groups.conf +0 -0
  54. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_sys_user_config_files_no_global/sys/file_groups.conf +0 -0
  55. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_two_in_same_config_dir/home/.file_groups.conf +0 -0
  56. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_two_in_same_config_dir/home/file_groups.conf +0 -0
  57. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_unknown_protect_sub_key_config_dir/sys/file_groups.conf +0 -0
  58. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/config_files_user_config_file_no_global/home/file_groups.conf +0 -0
  59. {file_groups-0.2.1 → file_groups-0.3.0}/test/in/configs/file_groups_group_files_by_config_protect/sys/file_groups.conf +0 -0
  60. {file_groups-0.2.1 → file_groups-0.3.0}/test/mypy_requirements.txt +0 -0
  61. {file_groups-0.2.1 → file_groups-0.3.0}/test/perf/protect_home_work_on_pictures.py +0 -0
  62. {file_groups-0.2.1 → file_groups-0.3.0}/test/perf/requirements.txt +0 -0
  63. {file_groups-0.2.1 → file_groups-0.3.0}/test/pylint_requirements.txt +0 -0
  64. {file_groups-0.2.1 → file_groups-0.3.0}/test/requirements.txt +0 -0
  65. {file_groups-0.2.1 → file_groups-0.3.0}/test/version_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: file_groups
3
- Version: 0.2.1
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: file-groups
3
- Version: 0.2.1
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
@@ -47,10 +47,15 @@ test/in/configs/config_files_inherit_global_recursive_ignore_other
47
47
  test/in/configs/config_files_inherit_global_recursive_no_other
48
48
  test/in/configs/config_files_inherit_ignore_global_recursive
49
49
  test/in/configs/config_files_sys_user_and_and_other_dir_config_files_no_global_no_other_recursive
50
+ test/in/configs/config_files_sys_user_config_files_replaced_appdirs
50
51
  test/in/configs/file_groups_sys_user_config_files_no_global
51
52
  test/in/configs/config_files_inherit_global_recursive/home/file_groups.conf
52
53
  test/in/configs/config_files_inherit_global_recursive/sys/file_groups.conf
54
+ test/in/configs/config_files_specified/direct.conf
53
55
  test/in/configs/config_files_sys_config_file_no_global/sys/file_groups.conf
56
+ test/in/configs/config_files_sys_user_config_files_additional_appdirs/home/an_app.conf
57
+ test/in/configs/config_files_sys_user_config_files_additional_appdirs/home/file_groups.conf
58
+ test/in/configs/config_files_sys_user_config_files_additional_appdirs/sys/file_groups.conf
54
59
  test/in/configs/config_files_sys_user_config_files_no_global/home/file_groups.conf
55
60
  test/in/configs/config_files_sys_user_config_files_no_global/sys/file_groups.conf
56
61
  test/in/configs/config_files_two_in_same_config_dir/home/.file_groups.conf
@@ -0,0 +1,290 @@
1
+ import ast
2
+ import os
3
+ import errno
4
+ import re
5
+ from pathlib import Path
6
+ import itertools
7
+ from pprint import pformat
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Tuple, Sequence, cast
11
+
12
+ from appdirs import AppDirs # type: ignore
13
+
14
+ from .types import FsPath
15
+
16
+
17
+ _LOG = logging.getLogger(__name__)
18
+
19
+
20
+ class ConfigException(Exception):
21
+ """Invalid configuration"""
22
+
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
+
56
+ class ConfigFiles():
57
+ r"""Handle config files.
58
+
59
+ Config files are searched for in the standard config directories on the platform AND in any collected directory.
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.
67
+
68
+ The content of a conf file is a Python dict with the following structure.
69
+
70
+ {
71
+ "file_groups": { # Required
72
+ "protect": { # Optional
73
+ "local": [ # Optional
74
+ ... # Regex patterns
75
+ ],
76
+ "recursive": [ # Optional, merged with parent config dir property
77
+ ... # Regex patterns
78
+ ]
79
+ "global": [ # Optional. Only allowed in config directory files. Merged into collect dir configs 'recursive' property.
80
+ ... # Regex patterns
81
+ ],
82
+ },
83
+ }
84
+ ...
85
+ }
86
+
87
+ E.g.:
88
+
89
+ {
90
+ "file_groups": {
91
+ "protect": {
92
+ "recursive": [
93
+ r"PP.*\.jpg", # Don't mess with JPEG files starting with 'PP'.
94
+ ]
95
+ }
96
+ }
97
+ }
98
+
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.
101
+
102
+ The 'file_groups' entry is a dict with a single 'protect' entry.
103
+ The 'protect' entry is a dict with at most three entries: 'local', 'recursive' and 'global'. These specify whether a directory specific
104
+ configuration will inherit and extend the parent (and global) config, or whether it is local to current directory only.
105
+ The 'local', 'recursive' and 'global' entries are lists of regex patterns to match against collected 'work_on' files.
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
107
+ which case they are checked against the absolute path.
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.
109
+
110
+ Note that for security ast.literal_eval is used to interpret the config, so no code is allowed.
111
+
112
+ Arguments:
113
+ protect: An optional sequence of regexes to be added to protect[recursive] for all directories.
114
+ ignore_config_dirs_config_files: Ignore config files in standard config directories.
115
+ ignore_per_directory_config_files: Ignore config files in collected directories.
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/
121
+
122
+ Members:
123
+ remember_configs: Whether per directory resolved/merged configs are stored in `dir_configs`.
124
+ dir_configs: dict[str: dict] Mapping from dir name to directory specific config dict. Only if remember_configs is True.
125
+ """
126
+
127
+ default_appdirs: AppDirs = AppDirs("file_groups", "Hupfeldt_IT")
128
+
129
+ _fg_key = "file_groups"
130
+ _protect_key = "protect"
131
+ _valid_dir_protect_scopes = ("local", "recursive")
132
+ _valid_config_dir_protect_scopes = ("local", "recursive", "global")
133
+
134
+ def __init__(
135
+ self, protect: Sequence[re.Pattern] = (),
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
+ ):
141
+ super().__init__()
142
+
143
+ self._global_config = DirConfig({
144
+ "local": set(),
145
+ "recursive": set(protect),
146
+ }, None, ())
147
+
148
+ self.remember_configs = remember_configs
149
+ self.per_dir_configs: dict[str, DirConfig] = {} # key is abs_dir_path
150
+ self.ignore_per_directory_config_files = ignore_per_directory_config_files
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 = []
156
+ if not ignore_config_dirs_config_files:
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
163
+
164
+ # self.default_config_file_example = self.default_config_file.with_suffix('.example.py')
165
+
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]:
169
+ """Read config file, validate keys and compile regexes and merge with parent.
170
+
171
+ Error if config files are found both with and withput '.' prefix.
172
+ Merge parent conf into conf_dir conf (if any) and return the merged dict. The parent conf is not modified.
173
+
174
+ Return: merged config dict with compiled regexes, config file name. If no config files is found, then return inherited parent conf and None.
175
+ """
176
+
177
+ assert conf_dir.is_absolute()
178
+ _LOG.debug("Checking for config files %s in directory: %s", conf_file_name_pair, conf_dir)
179
+
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]] = {
184
+ "local": set(),
185
+ "recursive": parent_conf.protect["recursive"]
186
+ }
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))
198
+
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)
203
+
204
+ try:
205
+ protect_conf: dict[str, set[re.Pattern]] = new_config[self._fg_key][self._protect_key]
206
+ except KeyError as ex:
207
+ raise ConfigException(f"Config file '{conf_file}' is missing mandatory configuration '{self._fg_key}[{self._protect_key}]'.") from ex
208
+
209
+ for key, val in protect_conf.items():
210
+ if key not in valid_protect_scopes:
211
+ msg = f"The only keys allowed in '{self._fg_key}[{self._protect_key}]' section in the config file '{conf_file}' are: {valid_protect_scopes}. Got: '{key}'."
212
+ _LOG.debug("%s", msg)
213
+ raise ConfigException(msg)
214
+
215
+ protect_conf[key] = set(re.compile(pattern) for pattern in val)
216
+ if key == "recursive":
217
+ protect_conf[key].update(parent_conf.protect[key])
218
+
219
+ for key in self._valid_dir_protect_scopes: # Do NOT use the 'valid_protect_scopes' argument here
220
+ protect_conf.setdefault(key, set())
221
+
222
+ lvl = logging.DEBUG
223
+ if _LOG.isEnabledFor(lvl):
224
+ _LOG.log(lvl, "Merged directory config:\n%s", pformat(new_config))
225
+
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:
277
+ """Read and merge config file from directory 'conf_dir' with 'parent_conf'.
278
+
279
+ If directory has no parent in the file_groups included dirs, then None should be supplied as parent_conf.
280
+ """
281
+
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
+
286
+ if self.remember_configs:
287
+ self.per_dir_configs[str(conf_dir)] = new_config
288
+ # _LOG.debug("per_dir_configs:\n %s", self.per_dir_configs)
289
+
290
+ return new_config
@@ -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__)
@@ -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: Only include files matching regex in the may_work_on files (does not apply to symlinks). Default: Include ALL.
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
- ignore_config_dirs_config_files: Ignore config files in standard config directories.
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
- ignore_config_dirs_config_files=False, ignore_per_directory_config_files=False,
103
- remember_configs=True):
100
+ config_files: ConfigFiles|None = None):
104
101
  super().__init__()
105
102
 
106
- self.config_files = ConfigFiles(
107
- protect=protect,
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: self.work_dirs_seq is [top, top/d1/d1]
157
- And: self.protect_dirs_seq is [top/d1]
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: dict):
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, config_file = self.config_files.dir_config(Path(abs_dir_path), parent_conf)
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 config_file and entry.name == config_file.name:
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 = self.config_files.is_protected(entry, dir_config)
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 = self.config_files.global_config
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)
@@ -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 actually do anything.
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
@@ -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
- work_dirs_seq=work_dirs_seq,
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