file-groups 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
file_groups/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Set __version__ property."""
2
2
 
3
- from importlib.metadata import version # type: ignore
3
+ from importlib.metadata import version
4
4
 
5
5
 
6
6
  __version__ = version("file_groups")
@@ -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
- @contextmanager
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 ***")
@@ -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,5 @@
1
1
  from os import DirEntry
2
2
  from pathlib import Path
3
- from typing import Union
4
3
 
5
4
 
6
- FsPath = Union[DirEntry, Path]
5
+ FsPath = DirEntry|Path
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: file_groups
3
- Version: 0.3.1
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.10
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,