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 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,,