file-groups 0.3.1__py3-none-any.whl → 0.4.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/__init__.py +1 -1
- file_groups/config_files.py +5 -5
- file_groups/groups.py +7 -7
- file_groups/handler.py +16 -22
- file_groups/handler_compare.py +1 -1
- file_groups/types.py +1 -2
- {file_groups-0.3.1.dist-info → file_groups-0.4.0.dist-info}/METADATA +3 -4
- file_groups-0.4.0.dist-info/RECORD +14 -0
- {file_groups-0.3.1.dist-info → file_groups-0.4.0.dist-info}/WHEEL +1 -1
- file_groups-0.3.1.dist-info/RECORD +0 -14
- {file_groups-0.3.1.dist-info → file_groups-0.4.0.dist-info}/LICENSE.txt +0 -0
- {file_groups-0.3.1.dist-info → file_groups-0.4.0.dist-info}/top_level.txt +0 -0
- {file_groups-0.3.1.dist-info → file_groups-0.4.0.dist-info}/zip-safe +0 -0
file_groups/__init__.py
CHANGED
file_groups/config_files.py
CHANGED
@@ -26,7 +26,7 @@ class ProtectConfig():
|
|
26
26
|
"""Hold global (site or user) protect config."""
|
27
27
|
protect_recursive: set[re.Pattern]
|
28
28
|
|
29
|
-
def __json__(self):
|
29
|
+
def __json__(self) -> dict[str, Any]:
|
30
30
|
return {
|
31
31
|
ProtectConfig.__name__: {
|
32
32
|
"protect_recursive": [str(pat) for pat in self.protect_recursive],
|
@@ -41,13 +41,13 @@ class DirConfig(ProtectConfig):
|
|
41
41
|
config_dir: Path|None
|
42
42
|
config_files: list[str]
|
43
43
|
|
44
|
-
def is_protected(self, ff: FsPath):
|
44
|
+
def is_protected(self, ff: FsPath) -> re.Pattern|None:
|
45
45
|
"""If ff id protected by a regex pattern then return the pattern, otherwise return None."""
|
46
46
|
|
47
47
|
# _LOG.debug("ff '%s'", ff)
|
48
48
|
for pattern in itertools.chain(self.protect_local, self.protect_recursive):
|
49
49
|
if os.sep in str(pattern):
|
50
|
-
# _LOG.debug("Pattern '%s' has path sep", pattern)
|
50
|
+
# _LOG.debug("re.Pattern '%s' has path sep", pattern)
|
51
51
|
assert os.path.isabs(ff), f"Expected absolute path, got '{ff}'"
|
52
52
|
|
53
53
|
# Search against full path
|
@@ -68,7 +68,7 @@ class DirConfig(ProtectConfig):
|
|
68
68
|
|
69
69
|
return None
|
70
70
|
|
71
|
-
def __json__(self):
|
71
|
+
def __json__(self) -> dict[str, Any]:
|
72
72
|
return {
|
73
73
|
DirConfig.__name__: super().__json__()[ProtectConfig.__name__] | {
|
74
74
|
"protect_local": [str(pat) for pat in self.protect_local],
|
@@ -159,7 +159,7 @@ class ConfigFiles():
|
|
159
159
|
|
160
160
|
def __init__( # pylint: disable=too-many-positional-arguments,too-many-arguments
|
161
161
|
self, protect: Sequence[re.Pattern] = (),
|
162
|
-
ignore_config_dirs_config_files=False, ignore_per_directory_config_files=False, remember_configs=True,
|
162
|
+
ignore_config_dirs_config_files: bool = False, ignore_per_directory_config_files: bool =False, remember_configs: bool =True,
|
163
163
|
app_dirs: Sequence[AppDirs]|None = None,
|
164
164
|
*,
|
165
165
|
config_file: Path|None = None,
|
file_groups/groups.py
CHANGED
@@ -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: DirEntry):
|
38
|
+
def add_entry_match(self, entry: DirEntry) -> None:
|
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: DirEntry):
|
45
|
+
def add_entry_match(self, entry: DirEntry) -> None:
|
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: DirEntry):
|
61
|
+
def add_entry_match(self, entry: DirEntry) -> None:
|
62
62
|
if not self.exclude:
|
63
63
|
self.files[entry.path] = entry
|
64
64
|
return
|
@@ -169,7 +169,7 @@ class FileGroups():
|
|
169
169
|
|
170
170
|
checked_dirs: set[str] = set()
|
171
171
|
|
172
|
-
def handle_entry(abs_dir_path: str, group: _Group, other_group: _Group, dir_config: DirConfig, entry: DirEntry):
|
172
|
+
def handle_entry(abs_dir_path: str, group: _Group, other_group: _Group, dir_config: DirConfig, entry: DirEntry) -> None:
|
173
173
|
"""Put entry in the correct group. Call 'find_group' if entry is a directory."""
|
174
174
|
if group.typ is GroupType.MAY_WORK_ON:
|
175
175
|
# Check for match against configured protect patterns, if match, then the file must got to protect group instead
|
@@ -207,7 +207,7 @@ class FileGroups():
|
|
207
207
|
_LOG.debug("find %s - entry name: %s", group.typ.name, entry.name)
|
208
208
|
group.add_entry_match(entry)
|
209
209
|
|
210
|
-
def find_group(abs_dir_path: str, group: _Group, other_group: _Group, parent_conf: DirConfig|None):
|
210
|
+
def find_group(abs_dir_path: str, group: _Group, other_group: _Group, parent_conf: DirConfig|None) -> None:
|
211
211
|
"""Recursively find all files belonging to 'group'"""
|
212
212
|
_LOG.debug("find %s: %s", group.typ.name, abs_dir_path)
|
213
213
|
if abs_dir_path in checked_dirs:
|
@@ -238,7 +238,7 @@ class FileGroups():
|
|
238
238
|
else:
|
239
239
|
find_group(any_dir, self.may_work_on, self.must_protect, parent_conf)
|
240
240
|
|
241
|
-
def dump(self):
|
241
|
+
def dump(self) -> None:
|
242
242
|
"""Log collected files. This may be A LOT of output for large directories."""
|
243
243
|
|
244
244
|
log = _LOG.getChild("dump")
|
@@ -280,7 +280,7 @@ class FileGroups():
|
|
280
280
|
|
281
281
|
log.log(lvl, "")
|
282
282
|
|
283
|
-
def stats(self):
|
283
|
+
def stats(self) -> None:
|
284
284
|
"""Log collection numbers."""
|
285
285
|
log = _LOG.getChild("stats")
|
286
286
|
lvl = logging.INFO
|
file_groups/handler.py
CHANGED
@@ -2,13 +2,12 @@ import os
|
|
2
2
|
from pathlib import Path
|
3
3
|
import shutil
|
4
4
|
import re
|
5
|
-
from contextlib import contextmanager
|
6
5
|
import logging
|
7
6
|
from typing import Sequence
|
8
7
|
|
9
8
|
from .groups import FileGroups
|
10
9
|
from .config_files import ConfigFiles
|
11
|
-
|
10
|
+
from .types import FsPath
|
12
11
|
|
13
12
|
_LOG = logging.getLogger(__name__)
|
14
13
|
|
@@ -35,7 +34,7 @@ class FileHandler(FileGroups):
|
|
35
34
|
protect_exclude: re.Pattern|None = None, work_include: re.Pattern|None = None,
|
36
35
|
config_files: ConfigFiles|None = None,
|
37
36
|
dry_run: bool,
|
38
|
-
delete_symlinks_instead_of_relinking=False):
|
37
|
+
delete_symlinks_instead_of_relinking: bool =False):
|
39
38
|
super().__init__(
|
40
39
|
protect_dirs_seq=protect_dirs_seq, work_dirs_seq=work_dirs_seq,
|
41
40
|
protect_exclude=protect_exclude, work_include=work_include,
|
@@ -55,7 +54,7 @@ class FileHandler(FileGroups):
|
|
55
54
|
self.num_moved = 0
|
56
55
|
self.num_relinked = 0
|
57
56
|
|
58
|
-
def reset(self):
|
57
|
+
def reset(self) -> None:
|
59
58
|
"""Reset internal housekeeping of deleted/renamed/moved files.
|
60
59
|
|
61
60
|
This makes it possible to do a 'dry_run' and an actual run without collecting files again.
|
@@ -69,7 +68,7 @@ class FileHandler(FileGroups):
|
|
69
68
|
self.num_moved = 0
|
70
69
|
self.num_relinked = 0
|
71
70
|
|
72
|
-
def _no_symlink_check_registered_delete(self, delete_path: str):
|
71
|
+
def _no_symlink_check_registered_delete(self, delete_path: str) -> None:
|
73
72
|
"""Does a registered delete without checking for symlinks, so that we can use this in the symlink handling."""
|
74
73
|
assert isinstance(delete_path, str)
|
75
74
|
assert os.path.isabs(delete_path), f"Expected absolute path, got '{delete_path}'"
|
@@ -84,7 +83,7 @@ class FileHandler(FileGroups):
|
|
84
83
|
if delete_path in self.may_work_on.symlinks:
|
85
84
|
self.deleted_symlinks.add(delete_path)
|
86
85
|
|
87
|
-
def _handle_single_symlink_chain(self, symlnk_path: str, keep_path):
|
86
|
+
def _handle_single_symlink_chain(self, symlnk_path: str, keep_path: str|FsPath|None) -> None:
|
88
87
|
"""TODO doc - Symlink will only be deleted if it is in self.may_work_on.files."""
|
89
88
|
|
90
89
|
assert os.path.isabs(symlnk_path), f"Expected an absolute path, got '{symlnk_path}'"
|
@@ -141,8 +140,12 @@ class FileHandler(FileGroups):
|
|
141
140
|
|
142
141
|
self.num_relinked += 1
|
143
142
|
|
144
|
-
def _fix_symlinks_to_deleted_or_moved_files(self, from_path: str, to_path):
|
145
|
-
"""Any symlinks pointing to 'from_path' will be change to point to 'to_path'
|
143
|
+
def _fix_symlinks_to_deleted_or_moved_files(self, from_path: str, to_path: str|FsPath|None) -> None:
|
144
|
+
"""Any symlinks pointing to 'from_path' will be change to point to 'to_path' or deleted.
|
145
|
+
|
146
|
+
If 'to_path' is None then delete full chains of symlinks in 'may_work_on' dirs.
|
147
|
+
"""
|
148
|
+
|
146
149
|
_LOG.debug("_fix_symlinks_to_deleted_or_moved_files(self, %s, %s)", from_path, to_path)
|
147
150
|
|
148
151
|
for symlnk in self.must_protect.symlinks_by_abs_points_to.get(from_path, ()):
|
@@ -153,13 +156,13 @@ class FileHandler(FileGroups):
|
|
153
156
|
_LOG.debug("_fix_symlinks_to_deleted_or_moved_files, may_work_on symlink: '%s'.", symlnk)
|
154
157
|
self._handle_single_symlink_chain(os.fspath(symlnk), to_path)
|
155
158
|
|
156
|
-
def registered_delete(self, delete_path: str, corresponding_keep_path) -> Path|None:
|
159
|
+
def registered_delete(self, delete_path: str, corresponding_keep_path: str|FsPath|None) -> Path|None:
|
157
160
|
"""Return `corresponding_keep_path` as absolute Path"""
|
158
161
|
self._no_symlink_check_registered_delete(delete_path)
|
159
162
|
self._fix_symlinks_to_deleted_or_moved_files(delete_path, corresponding_keep_path)
|
160
163
|
return Path(corresponding_keep_path).absolute() if corresponding_keep_path else None
|
161
164
|
|
162
|
-
def _registered_move_or_rename(self, from_path: str, to_path, *, is_move) -> Path:
|
165
|
+
def _registered_move_or_rename(self, from_path: str, to_path: str|FsPath, *, is_move: bool) -> Path:
|
163
166
|
"""Return `to_path` as absolute Path"""
|
164
167
|
assert isinstance(from_path, str)
|
165
168
|
assert os.path.isabs(from_path), f"Expected absolute path, got '{from_path}'"
|
@@ -189,20 +192,18 @@ class FileHandler(FileGroups):
|
|
189
192
|
self._fix_symlinks_to_deleted_or_moved_files(from_path, to_path)
|
190
193
|
return res
|
191
194
|
|
192
|
-
def registered_move(self, from_path: str, to_path) -> Path:
|
195
|
+
def registered_move(self, from_path: str, to_path: str|FsPath) -> Path:
|
193
196
|
"""Return `to_path` as absolute Path"""
|
194
197
|
return self._registered_move_or_rename(from_path, to_path, is_move=True)
|
195
198
|
|
196
|
-
def registered_rename(self, from_path: str, to_path) -> Path:
|
199
|
+
def registered_rename(self, from_path: str, to_path: str|FsPath) -> Path:
|
197
200
|
"""Return `to_path` as absolute Path"""
|
198
201
|
return self._registered_move_or_rename(from_path, to_path, is_move=False)
|
199
202
|
|
200
|
-
|
201
|
-
def stats(self):
|
203
|
+
def stats(self) -> None:
|
202
204
|
log = _LOG.getChild("stats")
|
203
205
|
lvl = logging.INFO
|
204
206
|
if not log.isEnabledFor(lvl):
|
205
|
-
yield
|
206
207
|
return
|
207
208
|
|
208
209
|
prefix = ''
|
@@ -218,10 +219,3 @@ class FileHandler(FileGroups):
|
|
218
219
|
log.log(lvl, "%srenamed: %s", prefix, self.num_renamed)
|
219
220
|
log.log(lvl, "%smoved: %s", prefix, self.num_moved)
|
220
221
|
log.log(lvl, "%srelinked: %s", prefix, self.num_relinked)
|
221
|
-
|
222
|
-
try:
|
223
|
-
yield
|
224
|
-
|
225
|
-
finally:
|
226
|
-
if self.dry_run:
|
227
|
-
log.log(lvl, "*** DRY RUN ***")
|
file_groups/handler_compare.py
CHANGED
@@ -29,7 +29,7 @@ class FileHandlerCompare(FileHandler):
|
|
29
29
|
protect_exclude: re.Pattern|None = None, work_include: re.Pattern|None = None,
|
30
30
|
config_files: ConfigFiles|None = None,
|
31
31
|
dry_run: bool,
|
32
|
-
delete_symlinks_instead_of_relinking=False):
|
32
|
+
delete_symlinks_instead_of_relinking: bool = False):
|
33
33
|
super().__init__(
|
34
34
|
protect_dirs_seq=protect_dirs_seq, work_dirs_seq=work_dirs_seq,
|
35
35
|
protect_exclude=protect_exclude, work_include=work_include,
|
file_groups/types.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: file_groups
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4.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
|
@@ -14,9 +14,8 @@ Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
18
17
|
Classifier: Topic :: Software Development :: Libraries
|
19
|
-
Requires-Python: >=3.
|
18
|
+
Requires-Python: >=3.11
|
20
19
|
Description-Content-Type: text/x-rst
|
21
20
|
License-File: LICENSE.txt
|
22
21
|
Requires-Dist: appdirs>=1.4.4
|
@@ -0,0 +1,14 @@
|
|
1
|
+
file_groups/__init__.py,sha256=FAl1D2-Fm2nQgXdM1rttTymLby24pN3ogmbS5dri770,111
|
2
|
+
file_groups/compare_files.py,sha256=38EfZzpEpj0wH7SUruPxgyF_W8eqCbIjG6yUOx8hcFA,434
|
3
|
+
file_groups/config_files.py,sha256=-sEyPkcds9gZEVvIJVnhWqXk9H4X20w_TT8BQg3_xzM,14101
|
4
|
+
file_groups/groups.py,sha256=fMn85U2QPF39sgYzma806XgtUOozOsA-CUNRjB2mC5I,11433
|
5
|
+
file_groups/handler.py,sha256=QerrcMtLEzl5NUOpwX2QbbngBUl8ssfYemcbGbaV208,10156
|
6
|
+
file_groups/handler_compare.py,sha256=zVRHdqP-2Z7u3D2jQ_6_HLF3SOUk0mSyJ7xiS5Y0Hig,2182
|
7
|
+
file_groups/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
file_groups/types.py,sha256=IEDtGNxhQhQaWn8sTHn5-jbpM-dbba7wiIfaud1XSXc,74
|
9
|
+
file_groups-0.4.0.dist-info/LICENSE.txt,sha256=NZc9ictLEwfoL4ywpAbLbd-n02KETGHLYQU820TJD_Q,1494
|
10
|
+
file_groups-0.4.0.dist-info/METADATA,sha256=hCMNdysRiHy14tnW7TKa4vxO0BKBRO_RlMHdISRDJe4,1510
|
11
|
+
file_groups-0.4.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
12
|
+
file_groups-0.4.0.dist-info/top_level.txt,sha256=mAdRf0R2TA8tpH7ftHzXP7VMRhw07p7B2mI8QKpDnjU,12
|
13
|
+
file_groups-0.4.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
14
|
+
file_groups-0.4.0.dist-info/RECORD,,
|
@@ -1,14 +0,0 @@
|
|
1
|
-
file_groups/__init__.py,sha256=XBp5563xhTer1vQY-DpKRvViAvlHaZqx9GrINiGqbeI,127
|
2
|
-
file_groups/compare_files.py,sha256=38EfZzpEpj0wH7SUruPxgyF_W8eqCbIjG6yUOx8hcFA,434
|
3
|
-
file_groups/config_files.py,sha256=VwRmZYEemKQawYI43qV9XhvPyI7ouaDjjadhc74wqNs,14021
|
4
|
-
file_groups/groups.py,sha256=Lt8QlAET8032t9OXcq9cmzLy27tZsYTnLAaGilzEtcM,11377
|
5
|
-
file_groups/handler.py,sha256=NmYfmPLGXgPsGxM9uR2r8aS3cI-8S7D5-8884EoGA1M,10083
|
6
|
-
file_groups/handler_compare.py,sha256=qbGHwT0FSvfnWtAnjYi4px6V5xIM2cStm0soflwYAi4,2174
|
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.1.dist-info/LICENSE.txt,sha256=NZc9ictLEwfoL4ywpAbLbd-n02KETGHLYQU820TJD_Q,1494
|
10
|
-
file_groups-0.3.1.dist-info/METADATA,sha256=wSlWiFxs8yMgFvY6jN3pWIBGBVbbneRhyMQM5tNcoPw,1561
|
11
|
-
file_groups-0.3.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
12
|
-
file_groups-0.3.1.dist-info/top_level.txt,sha256=mAdRf0R2TA8tpH7ftHzXP7VMRhw07p7B2mI8QKpDnjU,12
|
13
|
-
file_groups-0.3.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
14
|
-
file_groups-0.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|