superpathlib 2.0.3__tar.gz → 2.0.5__tar.gz

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.
Files changed (29) hide show
  1. {superpathlib-2.0.3/src/superpathlib.egg-info → superpathlib-2.0.5}/PKG-INFO +3 -3
  2. {superpathlib-2.0.3 → superpathlib-2.0.5}/pyproject.toml +11 -10
  3. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/base.py +7 -7
  4. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/cached_content.py +4 -4
  5. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/common_folders.py +1 -1
  6. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/content_properties.py +7 -6
  7. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/encryption.py +17 -9
  8. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/extra_functionality.py +82 -72
  9. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/metadata_properties.py +14 -13
  10. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/override.py +12 -8
  11. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/tags.py +1 -1
  12. {superpathlib-2.0.3 → superpathlib-2.0.5/src/superpathlib.egg-info}/PKG-INFO +3 -3
  13. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib.egg-info/requires.txt +2 -2
  14. {superpathlib-2.0.3 → superpathlib-2.0.5}/tests/test_cached_content.py +9 -5
  15. {superpathlib-2.0.3 → superpathlib-2.0.5}/tests/test_common_folders.py +3 -2
  16. {superpathlib-2.0.3 → superpathlib-2.0.5}/tests/test_content.py +4 -3
  17. {superpathlib-2.0.3 → superpathlib-2.0.5}/tests/test_encrypted_content.py +2 -1
  18. {superpathlib-2.0.3 → superpathlib-2.0.5}/tests/test_functionality.py +20 -9
  19. {superpathlib-2.0.3 → superpathlib-2.0.5}/tests/test_metadata.py +3 -2
  20. {superpathlib-2.0.3 → superpathlib-2.0.5}/LICENSE +0 -0
  21. {superpathlib-2.0.3 → superpathlib-2.0.5}/README.md +0 -0
  22. {superpathlib-2.0.3 → superpathlib-2.0.5}/setup.cfg +0 -0
  23. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/__init__.py +0 -0
  24. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/path.py +0 -0
  25. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/py.typed +0 -0
  26. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib/utils.py +0 -0
  27. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib.egg-info/SOURCES.txt +0 -0
  28. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib.egg-info/dependency_links.txt +0 -0
  29. {superpathlib-2.0.3 → superpathlib-2.0.5}/src/superpathlib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: superpathlib
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: Extended Pathlib
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
6
  License: MIT
@@ -11,7 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: simple-classproperty<5,>=4.0.2
12
12
  Provides-Extra: full
13
13
  Requires-Dist: dirhash<1,>=0.2.1; extra == "full"
14
- Requires-Dist: numpy<2,>=1.26.4; extra == "full"
14
+ Requires-Dist: numpy<3,>=1.26.4; extra == "full"
15
15
  Requires-Dist: package-utils<1,>=0.6.1; extra == "full"
16
16
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "full"
17
17
  Requires-Dist: xattr<2,>=0.10.1; extra == "full"
@@ -20,7 +20,7 @@ Requires-Dist: hypothesis<7,>=6.97.1; extra == "dev"
20
20
  Requires-Dist: package-dev-tools<1,>=0.5.12; extra == "dev"
21
21
  Requires-Dist: types-PyYaml<7,>=6.0.12.12; extra == "dev"
22
22
  Requires-Dist: dirhash<1,>=0.2.1; extra == "dev"
23
- Requires-Dist: numpy<2,>=1.26.4; extra == "dev"
23
+ Requires-Dist: numpy<3,>=1.26.4; extra == "dev"
24
24
  Requires-Dist: package-utils<1,>=0.6.1; extra == "dev"
25
25
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "dev"
26
26
  Requires-Dist: xattr<2,>=0.10.1; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superpathlib"
3
- version = "2.0.3"
3
+ version = "2.0.5"
4
4
  description = "Extended Pathlib"
5
5
  authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
6
6
  license = {text = "MIT"}
@@ -13,7 +13,7 @@ dependencies = [
13
13
  [project.optional-dependencies]
14
14
  full = [
15
15
  "dirhash >=0.2.1, <1",
16
- "numpy >=1.26.4, <2",
16
+ "numpy >=1.26.4, <3",
17
17
  "package-utils >=0.6.1, <1",
18
18
  "PyYaml >=6.0.1, <7",
19
19
  "xattr >=0.10.1, <2",
@@ -27,7 +27,7 @@ dev = [
27
27
 
28
28
  # full
29
29
  "dirhash >=0.2.1, <1",
30
- "numpy >=1.26.4, <2",
30
+ "numpy >=1.26.4, <3",
31
31
  "package-utils >=0.6.1, <1",
32
32
  "PyYaml >=6.0.1, <7",
33
33
  "xattr >=0.10.1, <2",
@@ -41,7 +41,7 @@ requires = ["setuptools"]
41
41
  build-backend = "setuptools.build_meta"
42
42
 
43
43
  [tool.coverage.run]
44
- command_line = "-m pytest"
44
+ command_line = "-m pytest tests"
45
45
 
46
46
  [tool.coverage.report]
47
47
  precision = 4
@@ -67,12 +67,12 @@ pythonpath = [
67
67
  fix = true
68
68
 
69
69
  [tool.ruff.lint]
70
- select = [
71
- "E", # pycodestyle errors
72
- "W", # pycodestyle warnings
73
- "F", # pyflakes
74
- "I", # isort
75
- "UP", # pyupgrade
70
+ select = ["ALL"]
71
+ ignore = [
72
+ "ANN101", # annotate self
73
+ "ANN102", # annotate cls
74
+ "ANN401", # annotated with Any
75
+ "D", # docstrings
76
76
  ]
77
77
 
78
78
  [tool.ruff.lint.isort]
@@ -80,6 +80,7 @@ known-first-party = ["src"]
80
80
 
81
81
  [tool.ruff.lint.per-file-ignores]
82
82
  "__init__.py" = ["F401"]
83
+ "tests/*" = ["S101"] # assert used
83
84
 
84
85
  [tool.setuptools.package-data]
85
86
  superpathlib = ["py.typed"]
@@ -3,21 +3,21 @@ import pathlib
3
3
  import sys
4
4
 
5
5
 
6
- class Path(pathlib.Path):
6
+ class Path(pathlib.Path): # pragma: nocover
7
7
  """
8
8
  Extend pathlib functionality and enable further extensions by inheriting.
9
9
  """
10
10
 
11
11
  # _flavour attribute explicitly required to inherit from pathlib
12
- if sys.version_info.minor <= 11:
12
+ if sys.version_info < (3, 12):
13
13
  _flavour = (
14
- pathlib._windows_flavour # type: ignore[attr-defined] # noqa
14
+ pathlib._windows_flavour # type: ignore[attr-defined] # noqa: SLF001
15
15
  if os.name == "nt"
16
- else pathlib._posix_flavour # type: ignore[attr-defined] # noqa
16
+ else pathlib._posix_flavour # type: ignore[attr-defined] # noqa: SLF001
17
17
  )
18
- else: # pragma: nocover
18
+ else:
19
19
  _flavour = (
20
- pathlib.ntpath # type: ignore[attr-defined] # noqa
20
+ pathlib.ntpath # type: ignore[attr-defined]
21
21
  if os.name == "nt"
22
- else pathlib.posixpath # type: ignore[attr-defined] # noqa
22
+ else pathlib.posixpath # type: ignore[attr-defined]
23
23
  )
@@ -21,12 +21,12 @@ class Path(metadata_properties.Path):
21
21
  def cached_content(self) -> CachedFileContent[dict[str, str]]:
22
22
  from package_utils.storage import CachedFileContent
23
23
 
24
- return CachedFileContent(self, default={}) # type: ignore
24
+ return CachedFileContent(self, default={}) # type: ignore[arg-type]
25
25
 
26
26
  def create_cached_content(self, default: T) -> CachedFileContent[T]:
27
27
  from package_utils.storage import CachedFileContent
28
28
 
29
- return CachedFileContent(self, default=default) # type: ignore
29
+ return CachedFileContent(self, default=default) # type: ignore[arg-type]
30
30
 
31
31
  @cached_property
32
32
  def cached_text(self) -> CachedFileContent[str]:
@@ -39,7 +39,7 @@ class Path(metadata_properties.Path):
39
39
  self.text = content
40
40
 
41
41
  return CachedFileContent(
42
- self, # type: ignore
42
+ self, # type: ignore[arg-type]
43
43
  default="",
44
44
  load_function=load_function,
45
45
  save_function=save_function,
@@ -56,7 +56,7 @@ class Path(metadata_properties.Path):
56
56
  self.byte_content = content
57
57
 
58
58
  return CachedFileContent(
59
- self, # type: ignore
59
+ self, # type: ignore[arg-type]
60
60
  default=b"",
61
61
  load_function=load_function,
62
62
  save_function=save_function,
@@ -19,7 +19,7 @@ class Path(base.Path):
19
19
 
20
20
  @classmethod
21
21
  @classproperty
22
- def HOME(cls: type[T]) -> T:
22
+ def HOME(cls: type[T]) -> T: # noqa: N802
23
23
  return cls.home()
24
24
 
25
25
  @classmethod
@@ -2,12 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import typing
5
- from collections.abc import Iterable
6
5
  from typing import Any
7
6
 
8
7
  from . import base
9
8
 
10
9
  if typing.TYPE_CHECKING: # pragma: nocover
10
+ from collections.abc import Iterable
11
+
11
12
  from numpy.typing import NDArray
12
13
 
13
14
 
@@ -68,17 +69,17 @@ class Path(base.Path):
68
69
  import yaml
69
70
 
70
71
  # C implementation much faster but only supported on Linux
71
- Loader: type[yaml.CFullLoader | yaml.FullLoader] = (
72
+ Loader: type[yaml.CFullLoader | yaml.FullLoader] = ( # noqa: N806
72
73
  yaml.CFullLoader if hasattr(yaml, "CFullLoader") else yaml.FullLoader
73
74
  )
74
- return yaml.load(self.text, Loader=Loader) or {}
75
+ return yaml.load(self.text, Loader=Loader) or {} # noqa: S506
75
76
 
76
77
  @yaml.setter
77
78
  def yaml(self, value: dict[str, Any] | list[Any]) -> None:
78
79
  import yaml
79
80
 
80
81
  # C implementation much faster but only supported on Linux
81
- Dumper = yaml.CDumper if hasattr(yaml, "CDumper") else yaml.Dumper
82
+ Dumper = yaml.CDumper if hasattr(yaml, "CDumper") else yaml.Dumper # noqa: N806
82
83
  self.text = yaml.dump(value, Dumper=Dumper, width=1024)
83
84
 
84
85
  @property
@@ -86,11 +87,11 @@ class Path(base.Path):
86
87
  import numpy as np
87
88
 
88
89
  with self.open("rb") as fp:
89
- return np.load(fp) # type: ignore
90
+ return np.load(fp) # type: ignore[no-any-return]
90
91
 
91
92
  @numpy.setter
92
93
  def numpy(self, value: NDArray[Any]) -> None:
93
94
  import numpy as np
94
95
 
95
96
  with self.open("wb") as fp:
96
- np.save(fp, value) # noqa
97
+ np.save(fp, value)
@@ -22,10 +22,10 @@ class EncryptedPath(Path):
22
22
  @cached_property
23
23
  def password(self) -> str: # pragma: nocover
24
24
  if "GITHUB_ACTION" in os.environ:
25
- password = "github_action_password"
25
+ password = "github_action_password" # noqa: S105
26
26
  else:
27
27
  command = 'ksshaskpass -- "Enter passphrase for file encryption: "'
28
- password = subprocess.getoutput(command)
28
+ password = subprocess.getoutput(command) # noqa: S605
29
29
  return password
30
30
 
31
31
  @property
@@ -39,24 +39,32 @@ class EncryptedPath(Path):
39
39
  def read_bytes(self) -> bytes:
40
40
  encrypted_bytes = super().read_bytes()
41
41
  if encrypted_bytes:
42
- process = subprocess.Popen(
43
- self.decryption_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
42
+ process = subprocess.Popen( # noqa: S603
43
+ self.decryption_command,
44
+ stdin=subprocess.PIPE,
45
+ stdout=subprocess.PIPE,
44
46
  )
45
47
  decrypted_bytes, _ = process.communicate(input=encrypted_bytes)
46
48
  else:
47
49
  decrypted_bytes = encrypted_bytes
48
50
  return decrypted_bytes
49
51
 
50
- def write_bytes(self, data: bytes) -> int: # type: ignore
51
- process = subprocess.Popen(
52
- self.encryption_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
52
+ def write_bytes(self, data: bytes) -> int: # type: ignore[override]
53
+ process = subprocess.Popen( # noqa: S603
54
+ self.encryption_command,
55
+ stdin=subprocess.PIPE,
56
+ stdout=subprocess.PIPE,
53
57
  )
54
58
  encrypted_data = process.communicate(input=data)[0]
55
59
  return super().write_bytes(encrypted_data)
56
60
 
57
- def read_text(self, encoding: str | None = None, errors: str | None = None) -> str:
61
+ def read_text(
62
+ self,
63
+ encoding: str | None = None, # noqa: ARG002
64
+ errors: str | None = None, # noqa: ARG002
65
+ ) -> str:
58
66
  return self.read_bytes().decode()
59
67
 
60
- def write_text(self, data: str, **_: Any) -> int: # type: ignore
68
+ def write_text(self, data: str, **_: Any) -> int: # type: ignore[override]
61
69
  byte_data = data.encode()
62
70
  return self.write_bytes(byte_data)
@@ -1,23 +1,20 @@
1
+ import contextlib
1
2
  import os
2
3
  import shutil
3
4
  import tempfile
4
5
  import time
5
6
  import typing
6
7
  import urllib.parse
7
- from collections.abc import Callable, Generator
8
+ from collections.abc import Callable, Iterator
8
9
  from functools import cached_property
9
10
  from types import TracebackType
10
- from typing import Any, TypeVar
11
+ from typing import Any, TypeVar, cast
11
12
 
12
13
  from . import cached_content
13
14
  from .utils import find_first_match
14
15
 
15
16
  PathType = TypeVar("PathType", bound="Path")
16
17
 
17
- # Long import times relative to usage frequency: lazy imports
18
- # from datetime import datetime
19
- # import dirhash
20
-
21
18
 
22
19
  class Path(cached_content.Path):
23
20
  """
@@ -45,14 +42,16 @@ class Path(cached_content.Path):
45
42
  return path
46
43
 
47
44
  def with_timestamp(self: PathType) -> PathType:
48
- from datetime import datetime # noqa: E402, autoimport
45
+ from datetime import datetime, timezone
49
46
 
50
- timestamp = datetime.fromtimestamp(int(time.time())) # precision up to second
51
- return self.with_stem(f"{self.stem} {timestamp}")
47
+ timestamp = int(time.time()) # precision up to second
48
+ datetime_timestamp = datetime.fromtimestamp(timestamp, tz=timezone.utc)
49
+ return self.with_stem(f"{self.stem} {datetime_timestamp}")
52
50
 
53
51
  def copy_to(
54
52
  self,
55
53
  dest: PathType,
54
+ *,
56
55
  include_properties: bool = True,
57
56
  only_if_newer: bool = False,
58
57
  ) -> None:
@@ -70,18 +69,22 @@ class Path(cached_content.Path):
70
69
  def archive_format(self) -> str:
71
70
  # noinspection PyProtectedMember
72
71
  path_str = str(self)
73
- format_ = shutil._find_unpack_format(path_str) # type: ignore[attr-defined]
72
+ format_ = shutil._find_unpack_format(path_str) # type: ignore[attr-defined] # noqa: SLF001
74
73
  return typing.cast(str, format_)
75
74
 
76
75
  def unpack_if_archive(
77
- self, extract_dir: PathType | None = None, recursive: bool = True
76
+ self,
77
+ *,
78
+ extraction_directory: PathType | None = None,
79
+ recursive: bool = True,
78
80
  ) -> None:
79
81
  if self.archive_format is not None:
80
- self.unpack(extract_dir, recursive=recursive)
82
+ self.unpack(extraction_directory, recursive=recursive)
81
83
 
82
- def unpack(
84
+ def unpack( # noqa: PLR0913
83
85
  self: PathType,
84
- extract_dir: PathType | None = None,
86
+ extraction_directory: PathType | None = None,
87
+ *,
85
88
  remove_existing: bool = True,
86
89
  preserve_properties: bool = True,
87
90
  remove_original: bool = True,
@@ -94,43 +97,48 @@ class Path(cached_content.Path):
94
97
  if subfolder.exists() and cleanup_path.number_of_children == 1:
95
98
  subfolder.pop_parent() # pragma: nocover
96
99
 
97
- def cast_path(casted_path: PathType | None) -> PathType:
98
- return casted_path # type: ignore
99
-
100
100
  if archive_format is None:
101
101
  archive_format = self.archive_format
102
102
 
103
- if extract_dir is None:
104
- extract_name = self.name
105
- # noinspection PyProtectedMember
106
- unpack_formats = shutil._UNPACK_FORMATS # type: ignore[attr-defined]
107
- unpack_info = unpack_formats[archive_format]
108
- for archive_ext in unpack_info[0]:
109
- if extract_name.endswith(archive_ext):
110
- extract_name = extract_name.replace(archive_ext, "")
111
- extract_dir = self.with_name(extract_name)
112
-
113
- extract_dir = cast_path(extract_dir)
103
+ extraction_directory = (
104
+ self.create_extraction_directory(archive_format=archive_format)
105
+ if extraction_directory is None
106
+ else extraction_directory
107
+ )
114
108
 
115
109
  if remove_existing:
116
- if extract_dir.is_dir():
117
- extract_dir.rmtree(missing_ok=True)
110
+ if extraction_directory.is_dir():
111
+ extraction_directory.rmtree(missing_ok=True)
118
112
  else:
119
- extract_dir.unlink(missing_ok=True)
113
+ extraction_directory.unlink(missing_ok=True)
120
114
 
121
- shutil.unpack_archive(self, extract_dir=extract_dir, format=archive_format)
115
+ shutil.unpack_archive(
116
+ self,
117
+ extract_dir=extraction_directory,
118
+ format=archive_format,
119
+ )
122
120
 
123
- cleanup(extract_dir)
121
+ cleanup(extraction_directory)
124
122
  if preserve_properties:
125
- self.copy_properties_to(extract_dir)
123
+ self.copy_properties_to(extraction_directory)
126
124
 
127
125
  if remove_original:
128
126
  self.unlink()
129
127
 
130
128
  if recursive:
131
- for path in extract_dir.find():
129
+ for path in extraction_directory.find():
132
130
  path.unpack_if_archive()
133
131
 
132
+ def create_extraction_directory(self: PathType, archive_format: str) -> PathType:
133
+ extract_name = self.name
134
+ # noinspection PyProtectedMember
135
+ unpack_formats = shutil._UNPACK_FORMATS # type: ignore[attr-defined] # noqa: SLF001
136
+ unpack_info = unpack_formats[archive_format]
137
+ for archive_ext in unpack_info[0]:
138
+ if extract_name.endswith(archive_ext):
139
+ extract_name = extract_name.replace(archive_ext, "")
140
+ return self.with_name(extract_name)
141
+
134
142
  def pop_parent(self) -> None:
135
143
  """
136
144
  Remove first parent from path in filesystem.
@@ -162,87 +170,85 @@ class Path(cached_content.Path):
162
170
  This can be used to instantiate any object
163
171
  :return: Content in path that contains yaml format
164
172
  """
165
- import yaml # noqa: E402, autoimport
173
+ import yaml # , autoimport
166
174
 
167
- Loader: type[yaml.CFullLoader | yaml.FullLoader] = (
175
+ Loader: type[yaml.CFullLoader | yaml.FullLoader] = ( # noqa: N806
168
176
  yaml.CFullLoader if hasattr(yaml, "CFullLoader") else yaml.FullLoader
169
177
  )
170
- return yaml.load(self.text, Loader=Loader) or {}
178
+ return yaml.load(self.text, Loader=Loader) or {} # noqa: S506
171
179
 
172
180
  def update(self, value: dict[Any, Any]) -> dict[Any, Any]:
173
181
  # only read and write if value to add not empty
174
182
  if value:
175
- current_content = self.yaml
176
- assert isinstance(current_content, dict)
183
+ current_content = cast(dict[Any, Any], self.yaml)
177
184
  updated_content = current_content | value
178
185
  self.yaml = updated_content
179
186
  else:
180
187
  updated_content = value
181
188
  return updated_content
182
189
 
183
- def find(
190
+ def find( # noqa: PLR0913
184
191
  self: PathType,
185
192
  condition: Callable[[PathType], bool] | None = None,
186
- exclude: Callable[[PathType], bool] | None = None,
193
+ exclude: Callable[[PathType], bool] = lambda _: False,
194
+ *,
187
195
  recurse_on_match: bool = False,
188
196
  follow_symlinks: bool = False,
189
197
  only_folders: bool = False,
190
- ) -> Generator[PathType, None, None]:
198
+ ) -> Iterator[PathType]:
191
199
  """Find all subpaths under path that match condition.
192
200
 
193
201
  only_folders option can be used for efficiency reasons
194
202
  """
203
+
204
+ def extract_children_to_recurse_on(path: PathType) -> Iterator[PathType]:
205
+ # skip folders that do not allow listing
206
+ with contextlib.suppress(PermissionError):
207
+ for child in path.iterdir():
208
+ should_follow_symlink = follow_symlinks or not child.is_symlink()
209
+ should_follow_directories = not only_folders or child.is_dir()
210
+ if should_follow_symlink and should_follow_directories:
211
+ yield child
212
+
195
213
  if condition is None:
196
214
  recurse_on_match = True
197
215
 
198
216
  def condition(_: PathType) -> bool:
199
217
  return True
200
218
 
201
- if exclude is None:
202
-
203
- def exclude(_: PathType) -> bool:
204
- return False
205
-
206
219
  to_traverse = [self] if self.exists() else []
207
220
  while to_traverse:
208
221
  path = to_traverse.pop(0)
209
-
210
222
  if not exclude(path):
211
223
  match = condition(path)
212
224
  if match:
213
225
  yield path
214
-
215
- if not match or recurse_on_match:
216
- if only_folders or path.is_dir():
217
- try:
218
- for child in path.iterdir():
219
- if follow_symlinks or not child.is_symlink():
220
- if not only_folders or child.is_dir():
221
- to_traverse.append(child)
222
- except PermissionError: # pragma: nocover
223
- # skip folders that do not allow listing
224
- pass
226
+ should_recurse = recurse_on_match or not match
227
+ should_recurse_folder = only_folders or path.is_dir()
228
+ if should_recurse and should_recurse_folder:
229
+ to_traverse += list(extract_children_to_recurse_on(path))
225
230
 
226
231
  def rmtree(
227
232
  self,
233
+ *,
228
234
  missing_ok: bool = False,
229
235
  remove_root: bool = True,
230
236
  ignore_errors: bool = False,
231
237
  ) -> None:
232
- try:
233
- shutil.rmtree(self, ignore_errors=ignore_errors, onerror=self._on_error) # type: ignore
234
- except FileNotFoundError as exception:
235
- if missing_ok:
236
- pass
237
- else:
238
- raise exception
238
+ context = (
239
+ contextlib.suppress(FileNotFoundError)
240
+ if missing_ok
241
+ else contextlib.nullcontext()
242
+ )
243
+ with context:
244
+ shutil.rmtree(self, ignore_errors=ignore_errors, onerror=self._on_error) # type: ignore[arg-type]
239
245
  if not remove_root:
240
246
  self.mkdir()
241
247
 
242
248
  @classmethod
243
249
  def _on_error(
244
250
  cls,
245
- _: bool,
251
+ _: bool, # noqa: FBT001
246
252
  path_str: str,
247
253
  exc_info: tuple[type[Exception], Exception, TracebackType],
248
254
  ) -> None:
@@ -258,7 +264,7 @@ class Path(cached_content.Path):
258
264
  tokens_to_replace = self._flavour.sep, "."
259
265
  for part in parts:
260
266
  for token in tokens_to_replace:
261
- part = part.replace(token, "_")
267
+ part = part.replace(token, "_") # noqa: PLW2901
262
268
  path /= part
263
269
  return path
264
270
 
@@ -269,7 +275,11 @@ class Path(cached_content.Path):
269
275
 
270
276
  @classmethod
271
277
  def tempfile(
272
- cls: type[PathType], in_memory: bool = True, create: bool = True, **kwargs: Any
278
+ cls: type[PathType],
279
+ *,
280
+ in_memory: bool = True,
281
+ create: bool = True,
282
+ **kwargs: Any,
273
283
  ) -> PathType:
274
284
  """Usage:
275
285
 
@@ -288,7 +298,7 @@ class Path(cached_content.Path):
288
298
  return path
289
299
 
290
300
  @classmethod
291
- def tempdir(cls: type[PathType], in_memory: bool = True) -> PathType:
301
+ def tempdir(cls: type[PathType], *, in_memory: bool = True) -> PathType:
292
302
  path = cls.tempfile(in_memory=in_memory, create=False)
293
303
  path.mkdir()
294
304
  return path
@@ -296,7 +306,7 @@ class Path(cached_content.Path):
296
306
  def __enter__(self: PathType) -> PathType:
297
307
  return self
298
308
 
299
- def __exit__(self, *_: Any) -> None:
309
+ def __exit__(self, *_: object) -> None:
300
310
  if self.is_file():
301
311
  self.unlink(missing_ok=True)
302
312
  else:
@@ -1,12 +1,12 @@
1
+ import contextlib
1
2
  import hashlib
2
3
  import mimetypes
3
4
  import os
4
5
  import subprocess
5
- import typing
6
6
  import warnings
7
7
  from collections.abc import Callable
8
8
  from functools import wraps
9
- from typing import Any, TypeVar
9
+ from typing import Any, TypeVar, cast
10
10
 
11
11
  from . import content_properties
12
12
 
@@ -45,20 +45,20 @@ class Path(content_properties.Path):
45
45
  os.utime(self, (time, time)) # set create time as well
46
46
 
47
47
  command = "touch", "-d", f"@{time}", self
48
- try:
49
- subprocess.run(command)
50
- except subprocess.CalledProcessError: # pragma: nocover
51
- pass # Doesn't work on Windows
48
+ with contextlib.suppress(
49
+ subprocess.CalledProcessError,
50
+ ): # Doesn't work on Windows
51
+ subprocess.run(command, check=False) # noqa: S603
52
52
 
53
53
  @property
54
54
  def tags(self) -> list[str]:
55
- from .tags import XDGTags # noqa: E402, autoimport
55
+ from .tags import XDGTags # , autoimport
56
56
 
57
57
  return XDGTags(self).get()
58
58
 
59
59
  @tags.setter
60
60
  def tags(self, values: list[str | int | None]) -> None:
61
- from .tags import XDGTags # noqa: E402, autoimport
61
+ from .tags import XDGTags # , autoimport
62
62
 
63
63
  if len(values) == 0:
64
64
  XDGTags(self).clear()
@@ -71,7 +71,7 @@ class Path(content_properties.Path):
71
71
 
72
72
  @tag.setter
73
73
  def tag(self, value: str | int | None) -> None:
74
- from .tags import XDGTags # noqa: E402, autoimport
74
+ from .tags import XDGTags # , autoimport
75
75
 
76
76
  XDGTags(self).set(value)
77
77
 
@@ -110,15 +110,16 @@ class Path(content_properties.Path):
110
110
  def dir_content_hash(self) -> str | None:
111
111
  # dirhash package throws annoying warnings
112
112
  warnings.filterwarnings(
113
- action="ignore", module="pkg_resources|dirhash", category=DeprecationWarning
113
+ action="ignore",
114
+ module="pkg_resources|dirhash",
115
+ category=DeprecationWarning,
114
116
  )
115
117
 
116
- import dirhash # noqa: E402, F401, autoimport
118
+ import dirhash # , autoimport
117
119
 
118
120
  # use default algorithm used in cloud provider checksums
119
121
  # can be efficient because not used for cryptographic security
120
- content_hash = dirhash.dirhash(self, "md5") if self.exists() else None
121
- return typing.cast(str, content_hash)
122
+ return cast(str, dirhash.dirhash(self, "md5")) if self.has_children else None
122
123
 
123
124
  @property
124
125
  def file_content_hash(self) -> str:
@@ -33,7 +33,11 @@ class Path(encryption.Path):
33
33
 
34
34
  @create_parent_on_missing
35
35
  def touch(
36
- self, mode: int = 0o666, exist_ok: bool = True, mtime: float | None = None
36
+ self,
37
+ mode: int = 0o666,
38
+ *,
39
+ exist_ok: bool = True,
40
+ mtime: float | None = None,
37
41
  ) -> None:
38
42
  super().touch(mode=mode, exist_ok=exist_ok)
39
43
  if mtime is not None:
@@ -43,22 +47,22 @@ class Path(encryption.Path):
43
47
  def rmdir(self) -> None:
44
48
  return super().rmdir()
45
49
 
46
- def iterdir(self: T, missing_ok: bool = True) -> Generator[T, None, None]:
50
+ def iterdir(self: T, *, missing_ok: bool = True) -> Generator[T, None, None]:
47
51
  if self.exists() or not missing_ok:
48
52
  yield from super().iterdir()
49
53
 
50
54
  @create_parent_on_missing
51
- def rename(self: T, target: str | T, exist_ok: bool = False) -> T:
55
+ def rename(self: T, target: str | T, *, exist_ok: bool = False) -> T:
52
56
  target_path = self.__class__(target)
53
57
  rename = super().replace if exist_ok else super().rename
54
58
  try:
55
59
  target_path.create_parent()
56
60
  target_path = rename(target_path)
57
- except OSError as e:
58
- if exist_ok and "Directory not empty" in str(e):
61
+ except OSError as exception:
62
+ if exist_ok and "Directory not empty" in str(exception):
59
63
  target_path.rmtree()
60
64
  target_path = rename(target_path)
61
- elif "Invalid cross-device link" in str(e):
65
+ elif "Invalid cross-device link" in str(exception):
62
66
  # target is on different file system
63
67
  if target_path.exists():
64
68
  if exist_ok:
@@ -68,7 +72,7 @@ class Path(encryption.Path):
68
72
  target_path.unlink() # pragma: nocover
69
73
  else:
70
74
  message = f"Target already exists: {target_path }"
71
- raise Exception(message)
75
+ raise RuntimeError(message) from exception
72
76
  else:
73
77
  target_path.create_parent()
74
78
  target_path = shutil.move(self, target_path)
@@ -80,7 +84,7 @@ class Path(encryption.Path):
80
84
  path = self.rename(target, exist_ok=True)
81
85
  return typing.cast(T, path)
82
86
 
83
- def open(self, mode: str = "r", **kwargs: Any) -> IO[Any]: # type: ignore
87
+ def open(self, mode: str = "r", **kwargs: Any) -> IO[Any]: # type: ignore[override]
84
88
  try:
85
89
  res = super().open(mode, **kwargs)
86
90
  except FileNotFoundError:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  try:
4
- import xattr # noqa
4
+ import xattr
5
5
 
6
6
  except ModuleNotFoundError: # pragma: nocover
7
7
  # Don't fail if xattr not supported (Windows)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: superpathlib
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: Extended Pathlib
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
6
  License: MIT
@@ -11,7 +11,7 @@ License-File: LICENSE
11
11
  Requires-Dist: simple-classproperty<5,>=4.0.2
12
12
  Provides-Extra: full
13
13
  Requires-Dist: dirhash<1,>=0.2.1; extra == "full"
14
- Requires-Dist: numpy<2,>=1.26.4; extra == "full"
14
+ Requires-Dist: numpy<3,>=1.26.4; extra == "full"
15
15
  Requires-Dist: package-utils<1,>=0.6.1; extra == "full"
16
16
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "full"
17
17
  Requires-Dist: xattr<2,>=0.10.1; extra == "full"
@@ -20,7 +20,7 @@ Requires-Dist: hypothesis<7,>=6.97.1; extra == "dev"
20
20
  Requires-Dist: package-dev-tools<1,>=0.5.12; extra == "dev"
21
21
  Requires-Dist: types-PyYaml<7,>=6.0.12.12; extra == "dev"
22
22
  Requires-Dist: dirhash<1,>=0.2.1; extra == "dev"
23
- Requires-Dist: numpy<2,>=1.26.4; extra == "dev"
23
+ Requires-Dist: numpy<3,>=1.26.4; extra == "dev"
24
24
  Requires-Dist: package-utils<1,>=0.6.1; extra == "dev"
25
25
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "dev"
26
26
  Requires-Dist: xattr<2,>=0.10.1; extra == "dev"
@@ -5,14 +5,14 @@ hypothesis<7,>=6.97.1
5
5
  package-dev-tools<1,>=0.5.12
6
6
  types-PyYaml<7,>=6.0.12.12
7
7
  dirhash<1,>=0.2.1
8
- numpy<2,>=1.26.4
8
+ numpy<3,>=1.26.4
9
9
  package-utils<1,>=0.6.1
10
10
  PyYaml<7,>=6.0.1
11
11
  xattr<2,>=0.10.1
12
12
 
13
13
  [full]
14
14
  dirhash<1,>=0.2.1
15
- numpy<2,>=1.26.4
15
+ numpy<3,>=1.26.4
16
16
  package-utils<1,>=0.6.1
17
17
  PyYaml<7,>=6.0.1
18
18
  xattr<2,>=0.10.1
@@ -1,10 +1,11 @@
1
1
  import typing
2
2
  from typing import Any
3
3
 
4
- from content import byte_content, text_content
5
4
  from package_utils.storage import CachedFileContent
6
5
  from superpathlib import Path
7
- from utils import ignore_fixture_warning
6
+
7
+ from tests.content import byte_content, text_content
8
+ from tests.utils import ignore_fixture_warning
8
9
 
9
10
 
10
11
  @ignore_fixture_warning
@@ -31,7 +32,8 @@ def test_text(content: str) -> None:
31
32
  def test_content(path: Path, content: dict[str, dict[str, str]]) -> None:
32
33
  class Storage:
33
34
  content: CachedFileContent[dict[str, dict[str, str]]] = typing.cast(
34
- CachedFileContent[dict[str, dict[str, str]]], path.cached_content
35
+ CachedFileContent[dict[str, dict[str, str]]],
36
+ path.cached_content,
35
37
  )
36
38
 
37
39
  verify_storage(Storage, content)
@@ -42,14 +44,16 @@ def test_content(path: Path, content: dict[str, dict[str, str]]) -> None:
42
44
  def test_created_content(path: Path, content: dict[str, dict[str, str]]) -> None:
43
45
  class Storage:
44
46
  content: CachedFileContent[dict[str, dict[str, str]]] = typing.cast(
45
- CachedFileContent[dict[str, dict[str, str]]], path.create_cached_content({})
47
+ CachedFileContent[dict[str, dict[str, str]]],
48
+ path.create_cached_content({}),
46
49
  )
47
50
 
48
51
  verify_storage(Storage, content)
49
52
 
50
53
 
51
54
  def verify_storage(
52
- storage_class: type[Any], content: str | bytes | dict[str, dict[str, str]]
55
+ storage_class: type[Any],
56
+ content: str | bytes | dict[str, dict[str, str]],
53
57
  ) -> None:
54
58
  storage = storage_class()
55
59
  assert storage.content is not None
@@ -1,6 +1,7 @@
1
- from content import byte_content
2
1
  from superpathlib import Path
3
- from utils import ignore_fixture_warning
2
+
3
+ from tests.content import byte_content
4
+ from tests.utils import ignore_fixture_warning
4
5
 
5
6
 
6
7
  @ignore_fixture_warning
@@ -1,5 +1,7 @@
1
1
  import numpy as np
2
- from content import (
2
+ from superpathlib import Path
3
+
4
+ from tests.content import (
3
5
  byte_content,
4
6
  dictionary_content,
5
7
  floats_content,
@@ -7,8 +9,7 @@ from content import (
7
9
  text_content,
8
10
  text_lines_content,
9
11
  )
10
- from superpathlib import Path
11
- from utils import ignore_fixture_warning
12
+ from tests.utils import ignore_fixture_warning
12
13
 
13
14
 
14
15
  @ignore_fixture_warning
@@ -1,7 +1,8 @@
1
- from content import byte_content, text_content
2
1
  from hypothesis import HealthCheck, settings
3
2
  from superpathlib import Path
4
3
 
4
+ from tests.content import byte_content, text_content
5
+
5
6
  slow_test_settings = settings(
6
7
  max_examples=2,
7
8
  deadline=3000,
@@ -1,14 +1,15 @@
1
1
  from collections.abc import Callable
2
2
 
3
3
  import pytest
4
- from content import (
4
+ from superpathlib import Path
5
+
6
+ from tests.content import (
5
7
  byte_content,
6
8
  dictionary_content,
7
9
  slower_test_settings,
8
10
  text_lines_content,
9
11
  )
10
- from superpathlib import Path
11
- from utils import ignore_fixture_warning
12
+ from tests.utils import ignore_fixture_warning
12
13
 
13
14
 
14
15
  def test_tempfile() -> None:
@@ -47,7 +48,7 @@ def test_unpack_check(directory: Path) -> None:
47
48
  non_archive_assets = Path(__file__).parent / "assets" / "non_archives"
48
49
  assert not non_archive_assets.is_empty()
49
50
  for path in non_archive_assets.iterdir():
50
- path.unpack_if_archive(directory)
51
+ path.unpack_if_archive(extraction_directory=directory)
51
52
  assert directory.is_empty()
52
53
 
53
54
 
@@ -103,7 +104,9 @@ def test_move_existing(path: Path, path2: Path, content: bytes) -> None:
103
104
  @ignore_fixture_warning
104
105
  @byte_content
105
106
  def test_move_parent_not_existing(
106
- directory: Path, directory2: Path, content: bytes
107
+ directory: Path,
108
+ directory2: Path,
109
+ content: bytes,
107
110
  ) -> None:
108
111
  directory.rmtree()
109
112
  path = directory / directory.name
@@ -133,7 +136,9 @@ def test_move_directory(directory: Path, directory2: Path, content: bytes) -> No
133
136
  @ignore_fixture_warning
134
137
  @byte_content
135
138
  def test_move_directory_existing(
136
- directory: Path, directory2: Path, content: bytes
139
+ directory: Path,
140
+ directory2: Path,
141
+ content: bytes,
137
142
  ) -> None:
138
143
  def move_function() -> None:
139
144
  directory.rename(directory2, exist_ok=True)
@@ -144,7 +149,9 @@ def test_move_directory_existing(
144
149
  @ignore_fixture_warning
145
150
  @byte_content
146
151
  def test_replace_directory_existing(
147
- directory: Path, directory2: Path, content: bytes
152
+ directory: Path,
153
+ directory2: Path,
154
+ content: bytes,
148
155
  ) -> None:
149
156
  def move_function() -> None:
150
157
  directory.replace(directory2)
@@ -155,7 +162,9 @@ def test_replace_directory_existing(
155
162
  @ignore_fixture_warning
156
163
  @byte_content
157
164
  def test_move_directory_different_filesystem(
158
- directory: Path, in_memory_directory: Path, content: bytes
165
+ directory: Path,
166
+ in_memory_directory: Path,
167
+ content: bytes,
159
168
  ) -> None:
160
169
  filename = directory.name
161
170
  subpath = directory / filename
@@ -173,7 +182,9 @@ def test_move_directory_different_filesystem(
173
182
  @ignore_fixture_warning
174
183
  @byte_content
175
184
  def test_move_directory_existing_different_filesystem(
176
- directory: Path, in_memory_directory: Path, content: bytes
185
+ directory: Path,
186
+ in_memory_directory: Path,
187
+ content: bytes,
177
188
  ) -> None:
178
189
  def move_function() -> None:
179
190
  directory.rename(in_memory_directory, exist_ok=True)
@@ -2,11 +2,12 @@ import hashlib
2
2
  import math
3
3
  import time
4
4
 
5
- from content import byte_content, text_strategy
6
5
  from hypothesis import given, strategies
7
6
  from hypothesis.strategies import lists
8
7
  from superpathlib import Path
9
- from utils import ignore_fixture_warning
8
+
9
+ from tests.content import byte_content, text_strategy
10
+ from tests.utils import ignore_fixture_warning
10
11
 
11
12
 
12
13
  @ignore_fixture_warning
File without changes
File without changes
File without changes