superpathlib 2.0.9__tar.gz → 2.0.11__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 (30) hide show
  1. {superpathlib-2.0.9/src/superpathlib.egg-info → superpathlib-2.0.11}/PKG-INFO +8 -6
  2. {superpathlib-2.0.9 → superpathlib-2.0.11}/pyproject.toml +25 -9
  3. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/cached_content.py +1 -1
  4. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/common_folders.py +17 -13
  5. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/content_properties.py +2 -2
  6. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/encryption.py +1 -0
  7. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/extra_functionality.py +42 -38
  8. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/metadata_properties.py +1 -9
  9. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/override.py +9 -7
  10. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/tags.py +2 -1
  11. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/utils.py +2 -3
  12. {superpathlib-2.0.9 → superpathlib-2.0.11/src/superpathlib.egg-info}/PKG-INFO +8 -6
  13. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/requires.txt +4 -3
  14. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_cached_content.py +2 -2
  15. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_functionality.py +4 -0
  16. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_metadata.py +2 -2
  17. {superpathlib-2.0.9 → superpathlib-2.0.11}/LICENSE +0 -0
  18. {superpathlib-2.0.9 → superpathlib-2.0.11}/README.md +0 -0
  19. {superpathlib-2.0.9 → superpathlib-2.0.11}/setup.cfg +0 -0
  20. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/__init__.py +0 -0
  21. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/base.py +0 -0
  22. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/path.py +0 -0
  23. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/py.typed +0 -0
  24. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/SOURCES.txt +0 -0
  25. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/dependency_links.txt +0 -0
  26. {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/top_level.txt +0 -0
  27. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_common_folders.py +0 -0
  28. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_content.py +0 -0
  29. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_encrypted_content.py +0 -0
  30. {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_inheritance.py +0 -0
@@ -1,29 +1,31 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: superpathlib
3
- Version: 2.0.9
3
+ Version: 2.0.11
4
4
  Summary: Extended Pathlib
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Source Code, https://github.com/quintenroets/superpathlib
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: simple-classproperty<5,>=4.0.2
12
+ Requires-Dist: typing_extensions<5,>=4.0
12
13
  Provides-Extra: full
13
14
  Requires-Dist: dirhash<1,>=0.2.1; extra == "full"
14
15
  Requires-Dist: numpy<3,>=1.26.4; extra == "full"
15
- Requires-Dist: package-utils<1,>=0.6.1; extra == "full"
16
+ Requires-Dist: package-utils<1,>=0.8.1; extra == "full"
16
17
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "full"
17
18
  Requires-Dist: xattr<2,>=0.10.1; extra == "full"
18
19
  Provides-Extra: dev
19
20
  Requires-Dist: hypothesis<7,>=6.97.1; extra == "dev"
20
- Requires-Dist: package-dev-tools<1,>=0.5.12; extra == "dev"
21
+ Requires-Dist: package-dev-tools<1,>=0.7.1; extra == "dev"
21
22
  Requires-Dist: types-PyYaml<7,>=6.0.12.12; extra == "dev"
22
23
  Requires-Dist: dirhash<1,>=0.2.1; extra == "dev"
23
24
  Requires-Dist: numpy<3,>=1.26.4; extra == "dev"
24
- Requires-Dist: package-utils<1,>=0.6.1; extra == "dev"
25
+ Requires-Dist: package-utils<1,>=0.8.0; extra == "dev"
25
26
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "dev"
26
27
  Requires-Dist: xattr<2,>=0.10.1; extra == "dev"
28
+ Dynamic: license-file
27
29
 
28
30
  # Superpathlib
29
31
  [![PyPI version](https://badge.fury.io/py/superpathlib.svg)](https://badge.fury.io/py/superpathlib)
@@ -1,26 +1,27 @@
1
1
  [project]
2
2
  name = "superpathlib"
3
- version = "2.0.9"
3
+ version = "2.0.11"
4
4
  description = "Extended Pathlib"
5
5
  authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
6
- license = {text = "MIT"}
6
+ license = "MIT"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
10
  "simple-classproperty >=4.0.2, <5",
11
+ "typing_extensions >=4.0, <5",
11
12
  ]
12
13
 
13
14
  [project.optional-dependencies]
14
15
  full = [
15
16
  "dirhash >=0.2.1, <1",
16
17
  "numpy >=1.26.4, <3",
17
- "package-utils >=0.6.1, <1",
18
+ "package-utils >=0.8.1, <1",
18
19
  "PyYaml >=6.0.1, <7",
19
20
  "xattr >=0.10.1, <2",
20
21
  ]
21
22
  dev = [
22
23
  "hypothesis >=6.97.1, <7",
23
- "package-dev-tools >=0.5.12, <1",
24
+ "package-dev-tools >=0.7.1, <1",
24
25
 
25
26
  # types
26
27
  "types-PyYaml >=6.0.12.12, <7",
@@ -28,7 +29,7 @@ dev = [
28
29
  # full
29
30
  "dirhash >=0.2.1, <1",
30
31
  "numpy >=1.26.4, <3",
31
- "package-utils >=0.6.1, <1",
32
+ "package-utils >=0.8.0, <1",
32
33
  "PyYaml >=6.0.1, <7",
33
34
  "xattr >=0.10.1, <2",
34
35
  ]
@@ -69,10 +70,13 @@ fix = true
69
70
  [tool.ruff.lint]
70
71
  select = ["ALL"]
71
72
  ignore = [
72
- "ANN101", # annotate self
73
- "ANN102", # annotate cls
74
73
  "ANN401", # annotated with Any
75
- "D", # docstrings
74
+ "D1", # missing docstrings
75
+ "D200", # one-line docstring
76
+ "D203", # conflicts with D211
77
+ "D205", # blank line between summary and description
78
+ "D212", # conflicts with D213
79
+ "D401", # imperative first line
76
80
  "G004", # logging f-string
77
81
  ]
78
82
 
@@ -81,7 +85,19 @@ ignore = [
81
85
  "F401" # unused import
82
86
  ]
83
87
  "tests/*" = [
84
- "S101" # assert used
88
+ "S101", # assert used
89
+ ]
90
+ "src/superpathlib/cached_content.py" = [
91
+ "PLC0415", # lazy imports for optional dependency
92
+ ]
93
+ "src/superpathlib/content_properties.py" = [
94
+ "PLC0415", # lazy imports for optional dependencies
95
+ ]
96
+ "src/superpathlib/extra_functionality.py" = [
97
+ "PLC0415", # lazy imports for optional dependencies
98
+ ]
99
+ "src/superpathlib/metadata_properties.py" = [
100
+ "PLC0415", # lazy imports for optional dependencies
85
101
  ]
86
102
 
87
103
  [tool.setuptools.package-data]
@@ -14,7 +14,7 @@ T = TypeVar("T")
14
14
 
15
15
  class Path(metadata_properties.Path):
16
16
  """
17
- Properties to for cached file content.
17
+ Properties for cached file content.
18
18
  """
19
19
 
20
20
  @cached_property
@@ -4,6 +4,7 @@ import typing
4
4
  from typing import Any, TypeVar
5
5
 
6
6
  from simple_classproperty import classproperty
7
+ from typing_extensions import Self
7
8
 
8
9
  from . import base
9
10
 
@@ -27,12 +28,15 @@ class PropertyMeta(abc.ABCMeta):
27
28
  ) -> "PropertyMeta":
28
29
  meta_class = super().__new__(cls, name, bases, attributes)
29
30
  if sys.version_info >= (3, 13):
30
- enable_classproperties(meta_class) # pragma: nocover
31
+ enable_classproperties(
32
+ meta_class, # type: ignore[arg-type]
33
+ ) # pragma: nocover
31
34
  return meta_class
32
35
 
33
36
 
34
37
  class Path(base.Path, metaclass=PropertyMeta):
35
- """Expose common folders as attributes.
38
+ """
39
+ Expose common folders as attributes.
36
40
 
37
41
  Use classmethod properties to ensure child classes return instance
38
42
  of child class. The classmethod and property decorators are combined
@@ -42,37 +46,37 @@ class Path(base.Path, metaclass=PropertyMeta):
42
46
 
43
47
  @classmethod
44
48
  @classproperty
45
- def HOME(cls: type[T]) -> T: # noqa: N802
49
+ def HOME(cls) -> Self: # noqa: N802
46
50
  return cls.home()
47
51
 
48
52
  @classmethod
49
53
  @classproperty
50
- def docs(cls: type[T]) -> T:
54
+ def docs(cls) -> Self:
51
55
  path = cls.HOME / "Documents"
52
- return typing.cast(T, path)
56
+ return typing.cast("Self", path)
53
57
 
54
58
  @classmethod
55
59
  @classproperty
56
- def scripts(cls: type[T]) -> T:
60
+ def scripts(cls) -> Self:
57
61
  path = cls.docs / "Scripts"
58
- return typing.cast(T, path)
62
+ return typing.cast("Self", path)
59
63
 
60
64
  @classmethod
61
65
  @classproperty
62
- def script_assets(cls: type[T]) -> T:
66
+ def script_assets(cls) -> Self:
63
67
  path = cls.scripts / "assets"
64
- return typing.cast(T, path)
68
+ return typing.cast("Self", path)
65
69
 
66
70
  @classmethod
67
71
  @classproperty
68
- def assets(cls: type[T]) -> T:
72
+ def assets(cls) -> Self:
69
73
  """
70
74
  Often overwritten by child classes for specific project.
71
75
  """
72
- return typing.cast(T, cls.script_assets)
76
+ return typing.cast("Self", cls.script_assets)
73
77
 
74
78
  @classmethod
75
79
  @classproperty
76
- def draft(cls: type[T]) -> T:
80
+ def draft(cls) -> Self:
77
81
  path = cls.docs / "draft.txt"
78
- return typing.cast(T, path)
82
+ return typing.cast("Self", path)
@@ -53,12 +53,12 @@ class Path(base.Path):
53
53
  @content_lines.setter
54
54
  def content_lines(self, lines: Iterable[Any]) -> None:
55
55
  lines = (line for line in lines if line)
56
- self.lines = typing.cast(list[str], lines)
56
+ self.lines = typing.cast("list[str]", lines)
57
57
 
58
58
  @property
59
59
  def json(self) -> dict[str, Any] | list[Any]:
60
60
  value = json.loads(self.text or "{}")
61
- return typing.cast(dict[str, Any] | list[Any], value)
61
+ return typing.cast("dict[str, Any] | list[Any]", value)
62
62
 
63
63
  @json.setter
64
64
  def json(self, content: dict[Any, Any] | list[Any]) -> None:
@@ -62,6 +62,7 @@ class EncryptedPath(Path):
62
62
  self,
63
63
  encoding: str | None = None, # noqa: ARG002
64
64
  errors: str | None = None, # noqa: ARG002
65
+ newline: str | None = None, # noqa: ARG002
65
66
  ) -> str:
66
67
  return self.read_bytes().decode()
67
68
 
@@ -5,43 +5,44 @@ import tempfile
5
5
  import time
6
6
  import typing
7
7
  import urllib.parse
8
+ from collections import deque
8
9
  from collections.abc import Callable, Iterator
9
10
  from functools import cached_property
10
11
  from types import TracebackType
11
- from typing import Any, TypeVar, cast
12
+ from typing import Any, cast
13
+
14
+ from typing_extensions import Self
12
15
 
13
16
  from . import cached_content
14
17
  from .utils import find_first_match
15
18
 
16
- PathType = TypeVar("PathType", bound="Path")
17
-
18
19
 
19
20
  class Path(cached_content.Path):
20
21
  """
21
22
  Additional functionality.
22
23
  """
23
24
 
24
- def create_parent(self: PathType) -> PathType:
25
+ def create_parent(self) -> Self:
25
26
  self.parent.mkdir(parents=True, exist_ok=True)
26
27
  return self.parent
27
28
 
28
- def with_nonexistent_name(self: PathType) -> PathType:
29
+ def with_nonexistent_name(self) -> Self:
29
30
  path = self
30
31
  if path.exists():
31
32
  stem = path.stem
32
33
 
33
- def with_number(i: int) -> PathType:
34
+ def with_number(i: int) -> "Path":
34
35
  return path.with_stem(f"{stem} ({i})")
35
36
 
36
37
  def nonexistent(i: int) -> bool:
37
38
  return not with_number(i).exists()
38
39
 
39
40
  first_free_number = find_first_match(nonexistent)
40
- path = with_number(first_free_number)
41
+ path = cast("Self", with_number(first_free_number))
41
42
 
42
43
  return path
43
44
 
44
- def with_timestamp(self: PathType) -> PathType:
45
+ def with_timestamp(self) -> Self:
45
46
  from datetime import datetime, timezone
46
47
 
47
48
  timestamp = int(time.time()) # precision up to second
@@ -50,7 +51,7 @@ class Path(cached_content.Path):
50
51
 
51
52
  def copy_to(
52
53
  self,
53
- dest: PathType,
54
+ dest: Self,
54
55
  *,
55
56
  include_properties: bool = True,
56
57
  only_if_newer: bool = False,
@@ -60,7 +61,7 @@ class Path(cached_content.Path):
60
61
  if include_properties:
61
62
  self.copy_properties_to(dest)
62
63
 
63
- def copy_properties_to(self, dest: PathType) -> None:
64
+ def copy_properties_to(self, dest: Self) -> None:
64
65
  for path in dest.find():
65
66
  path.tag = self.tag
66
67
  path.mtime = self.mtime
@@ -70,20 +71,20 @@ class Path(cached_content.Path):
70
71
  # noinspection PyProtectedMember
71
72
  path_str = str(self)
72
73
  format_ = shutil._find_unpack_format(path_str) # type: ignore[attr-defined] # noqa: SLF001
73
- return typing.cast(str, format_)
74
+ return typing.cast("str", format_)
74
75
 
75
76
  def unpack_if_archive(
76
77
  self,
77
78
  *,
78
- extraction_directory: PathType | None = None,
79
+ extraction_directory: Self | None = None,
79
80
  recursive: bool = True,
80
81
  ) -> None:
81
82
  if self.archive_format is not None:
82
83
  self.unpack(extraction_directory, recursive=recursive)
83
84
 
84
85
  def unpack( # noqa: PLR0913
85
- self: PathType,
86
- extraction_directory: PathType | None = None,
86
+ self,
87
+ extraction_directory: Self | None = None,
87
88
  *,
88
89
  remove_existing: bool = True,
89
90
  preserve_properties: bool = True,
@@ -91,7 +92,7 @@ class Path(cached_content.Path):
91
92
  archive_format: str | None = None,
92
93
  recursive: bool = True,
93
94
  ) -> None:
94
- def cleanup(cleanup_path: PathType) -> None:
95
+ def cleanup(cleanup_path: Self) -> None:
95
96
  (cleanup_path / "__MACOSX").rmtree(missing_ok=True)
96
97
  subfolder = cleanup_path / cleanup_path.name
97
98
  if subfolder.exists() and cleanup_path.number_of_children == 1:
@@ -129,7 +130,7 @@ class Path(cached_content.Path):
129
130
  for path in extraction_directory.find():
130
131
  path.unpack_if_archive()
131
132
 
132
- def create_extraction_directory(self: PathType, archive_format: str) -> PathType:
133
+ def create_extraction_directory(self, archive_format: str) -> Self:
133
134
  extract_name = self.name
134
135
  # noinspection PyProtectedMember
135
136
  unpack_formats = shutil._UNPACK_FORMATS # type: ignore[attr-defined] # noqa: SLF001
@@ -165,7 +166,8 @@ class Path(cached_content.Path):
165
166
  )
166
167
 
167
168
  def load_yaml(self) -> dict[Any, Any] | list[Any]:
168
- """Load yaml content of trusted path with an unsafe loader.
169
+ """
170
+ Load yaml content of trusted path with an unsafe loader.
169
171
 
170
172
  This can be used to instantiate any object
171
173
  :return: Content in path that contains yaml format
@@ -180,7 +182,7 @@ class Path(cached_content.Path):
180
182
  def update(self, value: dict[Any, Any]) -> dict[Any, Any]:
181
183
  # only read and write if value to add not empty
182
184
  if value:
183
- current_content = cast(dict[Any, Any], self.yaml)
185
+ current_content = cast("dict[Any, Any]", self.yaml)
184
186
  updated_content = current_content | value
185
187
  self.yaml = updated_content
186
188
  else:
@@ -188,20 +190,21 @@ class Path(cached_content.Path):
188
190
  return updated_content
189
191
 
190
192
  def find(
191
- self: PathType,
192
- condition: Callable[[PathType], bool] | None = None,
193
- exclude: Callable[[PathType], bool] = lambda _: False,
193
+ self,
194
+ condition: Callable[[Self], bool] | None = None,
195
+ exclude: Callable[[Self], bool] = lambda _: False,
194
196
  *,
195
197
  recurse_on_match: bool = False,
196
198
  follow_symlinks: bool = False,
197
199
  only_folders: bool = False,
198
- ) -> Iterator[PathType]:
199
- """Find all subpaths under path that match condition.
200
+ ) -> Iterator[Self]:
201
+ """
202
+ Find all subpaths under path that match condition.
200
203
 
201
204
  only_folders option can be used for efficiency reasons
202
205
  """
203
206
 
204
- def extract_children_to_recurse_on(path: PathType) -> Iterator[PathType]:
207
+ def extract_children_to_recurse_on(path: Self) -> Iterator[Self]:
205
208
  # skip folders that do not allow listing
206
209
  with contextlib.suppress(PermissionError):
207
210
  for child in path.iterdir():
@@ -213,12 +216,12 @@ class Path(cached_content.Path):
213
216
  if condition is None:
214
217
  recurse_on_match = True
215
218
 
216
- def condition(_: PathType) -> bool:
219
+ def condition(_: Self) -> bool:
217
220
  return True
218
221
 
219
- to_traverse = [self] if self.exists() else []
222
+ to_traverse = deque([self] if self.exists() else [])
220
223
  while to_traverse:
221
- path = to_traverse.pop(0)
224
+ path = to_traverse.popleft()
222
225
  if not exclude(path):
223
226
  match = condition(path)
224
227
  if match:
@@ -226,7 +229,7 @@ class Path(cached_content.Path):
226
229
  should_recurse = recurse_on_match or not match
227
230
  should_recurse_folder = only_folders or path.is_dir()
228
231
  if should_recurse and should_recurse_folder:
229
- to_traverse += list(extract_children_to_recurse_on(path))
232
+ to_traverse.extend(extract_children_to_recurse_on(path))
230
233
 
231
234
  def rmtree(
232
235
  self,
@@ -248,18 +251,18 @@ class Path(cached_content.Path):
248
251
  @classmethod
249
252
  def _on_error(
250
253
  cls,
251
- _: bool, # noqa: FBT001
254
+ func: Callable[[str], Any],
252
255
  path_str: str,
253
256
  exc_info: tuple[type[Exception], Exception, TracebackType],
254
257
  ) -> None:
255
258
  if exc_info[0] is PermissionError and os.name == "nt": # pragma: nocover
256
259
  path = Path(path_str)
257
260
  path.chmod(0o777)
258
- path.unlink()
261
+ func(path_str)
259
262
  else:
260
263
  raise exc_info[0]
261
264
 
262
- def subpath(self: PathType, *parts: str) -> PathType:
265
+ def subpath(self, *parts: str) -> Self:
263
266
  path = self
264
267
  tokens_to_replace = os.sep, "."
265
268
  for part in parts:
@@ -269,26 +272,27 @@ class Path(cached_content.Path):
269
272
  return path
270
273
 
271
274
  @classmethod
272
- def from_uri(cls: type[PathType], uri: str) -> PathType:
275
+ def from_uri(cls, uri: str) -> Self:
273
276
  path_str = urllib.parse.urlparse(uri).path
274
277
  return cls(path_str)
275
278
 
276
279
  @classmethod
277
280
  def tempfile(
278
- cls: type[PathType],
281
+ cls,
279
282
  *,
280
283
  in_memory: bool = True,
281
284
  create: bool = True,
282
285
  **kwargs: Any,
283
- ) -> PathType:
284
- """Usage:
286
+ ) -> Self:
287
+ """
288
+ Context manager for temporary file creation.
285
289
 
286
290
  with Path.tempfile() as tmp: run_command(log_file=tmp) logs = tmp.text
287
291
  process_logs(logs)
288
292
  """
289
293
  if in_memory:
290
294
  in_memory_folder = cls("/") / "dev" / "shm"
291
- if in_memory_folder.exists():
295
+ if in_memory_folder.exists(): # pragma: nocover
292
296
  kwargs["dir"] = in_memory_folder
293
297
  file_handle, path_str = tempfile.mkstemp(**kwargs)
294
298
  os.close(file_handle)
@@ -298,12 +302,12 @@ class Path(cached_content.Path):
298
302
  return path
299
303
 
300
304
  @classmethod
301
- def tempdir(cls: type[PathType], *, in_memory: bool = True) -> PathType:
305
+ def tempdir(cls, *, in_memory: bool = True) -> Self:
302
306
  path = cls.tempfile(in_memory=in_memory, create=False)
303
307
  path.mkdir()
304
308
  return path
305
309
 
306
- def __enter__(self: PathType) -> PathType:
310
+ def __enter__(self) -> Self:
307
311
  return self
308
312
 
309
313
  def __exit__(
@@ -1,8 +1,6 @@
1
- import contextlib
2
1
  import hashlib
3
2
  import mimetypes
4
3
  import os
5
- import subprocess
6
4
  import warnings
7
5
  from collections.abc import Callable
8
6
  from functools import wraps
@@ -44,12 +42,6 @@ class Path(content_properties.Path):
44
42
  def mtime(self, time: float) -> None:
45
43
  os.utime(self, (time, time)) # set create time as well
46
44
 
47
- command = "touch", "-d", f"@{time}", self
48
- with contextlib.suppress(
49
- subprocess.CalledProcessError,
50
- ): # Doesn't work on Windows
51
- subprocess.run(command, check=False) # noqa: S603
52
-
53
45
  @property
54
46
  def tags(self) -> list[str]:
55
47
  from .tags import XDGTags # , autoimport
@@ -119,7 +111,7 @@ class Path(content_properties.Path):
119
111
 
120
112
  # use default algorithm used in cloud provider checksums
121
113
  # can be efficient because not used for cryptographic security
122
- return cast(str, dirhash.dirhash(self, "md5")) if self.has_children else None
114
+ return cast("str", dirhash.dirhash(self, "md5")) if self.has_children else None
123
115
 
124
116
  @property
125
117
  def file_content_hash(self) -> str:
@@ -6,6 +6,8 @@ from functools import wraps
6
6
  from os import PathLike
7
7
  from typing import IO, Any, TypeVar
8
8
 
9
+ from typing_extensions import Self
10
+
9
11
  from . import encryption
10
12
  from .metadata_properties import catch_missing
11
13
 
@@ -47,12 +49,12 @@ class Path(encryption.Path):
47
49
  def rmdir(self) -> None:
48
50
  return super().rmdir()
49
51
 
50
- def iterdir(self: T, *, missing_ok: bool = True) -> Generator[T, None, None]:
52
+ def iterdir(self, *, missing_ok: bool = True) -> Generator[Self, None, None]:
51
53
  if self.exists() or not missing_ok:
52
54
  yield from super().iterdir()
53
55
 
54
56
  @create_parent_on_missing
55
- def rename(self: T, target: str | T, *, exist_ok: bool = False) -> T:
57
+ def rename(self, target: str | Self, *, exist_ok: bool = False) -> Self:
56
58
  target_path = self.__class__(target)
57
59
  rename = super().replace if exist_ok else super().rename
58
60
  try:
@@ -62,7 +64,7 @@ class Path(encryption.Path):
62
64
  if exist_ok and "Directory not empty" in str(exception):
63
65
  target_path.rmtree()
64
66
  target_path = rename(target_path)
65
- elif "Invalid cross-device link" in str(exception):
67
+ elif "Invalid cross-device link" in str(exception): # pragma: nocover
66
68
  # target is on different file system
67
69
  if target_path.exists():
68
70
  if exist_ok:
@@ -71,18 +73,18 @@ class Path(encryption.Path):
71
73
  else:
72
74
  target_path.unlink() # pragma: nocover
73
75
  else:
74
- message = f"Target already exists: {target_path }"
76
+ message = f"Target already exists: {target_path}"
75
77
  raise RuntimeError(message) from exception
76
78
  else:
77
79
  target_path.create_parent()
78
- target_path = shutil.move(self, target_path)
80
+ target_path = self.__class__(shutil.move(self, target_path))
79
81
  else:
80
82
  raise
81
83
  return target_path
82
84
 
83
- def replace(self: T, target: str | PathLike[str]) -> T:
85
+ def replace(self, target: str | PathLike[str]) -> Self:
84
86
  path = self.rename(target, exist_ok=True)
85
- return typing.cast(T, path)
87
+ return typing.cast("Self", path)
86
88
 
87
89
  def open(self, mode: str = "r", **kwargs: Any) -> IO[Any]: # type: ignore[override]
88
90
  try:
@@ -17,7 +17,8 @@ default_tag_name = "user.xdg.tags"
17
17
 
18
18
 
19
19
  class XDGTags:
20
- """More user-friendly way to interact with tags.
20
+ """
21
+ More user-friendly way to interact with tags.
21
22
 
22
23
  - Work with strings and integers instead of bytes
23
24
  - Use xdg tags by default ->
@@ -4,11 +4,10 @@ from collections.abc import Callable
4
4
  def find_first_match(condition: Callable[..., bool]) -> int:
5
5
  """
6
6
  :param condition: Condition that number needs to match.
7
- The condition is assumed to be valid for all integers staring
8
- from an initial value.
7
+ The condition is assumed to be valid for all integers starting
8
+ from an initial value.
9
9
  :return: First integer for which condition is valid.
10
10
  """
11
-
12
11
  # exponential increase for logarithmic search
13
12
  upper_bound = 1
14
13
  while not condition(upper_bound):
@@ -1,29 +1,31 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: superpathlib
3
- Version: 2.0.9
3
+ Version: 2.0.11
4
4
  Summary: Extended Pathlib
5
5
  Author-email: Quinten Roets <qdr2104@columbia.edu>
6
- License: MIT
6
+ License-Expression: MIT
7
7
  Project-URL: Source Code, https://github.com/quintenroets/superpathlib
8
8
  Requires-Python: >=3.10
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: simple-classproperty<5,>=4.0.2
12
+ Requires-Dist: typing_extensions<5,>=4.0
12
13
  Provides-Extra: full
13
14
  Requires-Dist: dirhash<1,>=0.2.1; extra == "full"
14
15
  Requires-Dist: numpy<3,>=1.26.4; extra == "full"
15
- Requires-Dist: package-utils<1,>=0.6.1; extra == "full"
16
+ Requires-Dist: package-utils<1,>=0.8.1; extra == "full"
16
17
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "full"
17
18
  Requires-Dist: xattr<2,>=0.10.1; extra == "full"
18
19
  Provides-Extra: dev
19
20
  Requires-Dist: hypothesis<7,>=6.97.1; extra == "dev"
20
- Requires-Dist: package-dev-tools<1,>=0.5.12; extra == "dev"
21
+ Requires-Dist: package-dev-tools<1,>=0.7.1; extra == "dev"
21
22
  Requires-Dist: types-PyYaml<7,>=6.0.12.12; extra == "dev"
22
23
  Requires-Dist: dirhash<1,>=0.2.1; extra == "dev"
23
24
  Requires-Dist: numpy<3,>=1.26.4; extra == "dev"
24
- Requires-Dist: package-utils<1,>=0.6.1; extra == "dev"
25
+ Requires-Dist: package-utils<1,>=0.8.0; extra == "dev"
25
26
  Requires-Dist: PyYaml<7,>=6.0.1; extra == "dev"
26
27
  Requires-Dist: xattr<2,>=0.10.1; extra == "dev"
28
+ Dynamic: license-file
27
29
 
28
30
  # Superpathlib
29
31
  [![PyPI version](https://badge.fury.io/py/superpathlib.svg)](https://badge.fury.io/py/superpathlib)
@@ -1,18 +1,19 @@
1
1
  simple-classproperty<5,>=4.0.2
2
+ typing_extensions<5,>=4.0
2
3
 
3
4
  [dev]
4
5
  hypothesis<7,>=6.97.1
5
- package-dev-tools<1,>=0.5.12
6
+ package-dev-tools<1,>=0.7.1
6
7
  types-PyYaml<7,>=6.0.12.12
7
8
  dirhash<1,>=0.2.1
8
9
  numpy<3,>=1.26.4
9
- package-utils<1,>=0.6.1
10
+ package-utils<1,>=0.8.0
10
11
  PyYaml<7,>=6.0.1
11
12
  xattr<2,>=0.10.1
12
13
 
13
14
  [full]
14
15
  dirhash<1,>=0.2.1
15
16
  numpy<3,>=1.26.4
16
- package-utils<1,>=0.6.1
17
+ package-utils<1,>=0.8.1
17
18
  PyYaml<7,>=6.0.1
18
19
  xattr<2,>=0.10.1
@@ -32,7 +32,7 @@ def test_text(content: str) -> None:
32
32
  def test_content(path: Path, content: dict[str, dict[str, str]]) -> None:
33
33
  class Storage:
34
34
  content: CachedFileContent[dict[str, dict[str, str]]] = typing.cast(
35
- CachedFileContent[dict[str, dict[str, str]]],
35
+ "CachedFileContent[dict[str, dict[str, str]]]",
36
36
  path.cached_content,
37
37
  )
38
38
 
@@ -44,7 +44,7 @@ def test_content(path: Path, content: dict[str, dict[str, str]]) -> None:
44
44
  def test_created_content(path: Path, content: dict[str, dict[str, str]]) -> None:
45
45
  class Storage:
46
46
  content: CachedFileContent[dict[str, dict[str, str]]] = typing.cast(
47
- CachedFileContent[dict[str, dict[str, str]]],
47
+ "CachedFileContent[dict[str, dict[str, str]]]",
48
48
  path.create_cached_content({}),
49
49
  )
50
50
 
@@ -11,6 +11,8 @@ from tests.content import (
11
11
  )
12
12
  from tests.utils import ignore_fixture_warning
13
13
 
14
+ MTIME_TOLERANCE = 0.01
15
+
14
16
 
15
17
  def test_tempfile() -> None:
16
18
  with Path.tempfile() as path:
@@ -297,3 +299,5 @@ def test_rmdir(directory: Path) -> None:
297
299
 
298
300
  def test_touch(path: Path) -> None:
299
301
  path.touch(mtime=1)
302
+ assert path.exists()
303
+ assert abs(path.mtime - 1) < MTIME_TOLERANCE
@@ -6,7 +6,7 @@ from hypothesis import given, strategies
6
6
  from hypothesis.strategies import lists
7
7
 
8
8
  from superpathlib import Path
9
- from tests.content import byte_content, text_strategy
9
+ from tests.content import byte_content, slower_test_settings, text_strategy
10
10
  from tests.utils import ignore_fixture_warning
11
11
 
12
12
 
@@ -44,7 +44,7 @@ def test_tag(path: Path, content: str) -> None:
44
44
  assert path.tag == content
45
45
 
46
46
 
47
- @ignore_fixture_warning
47
+ @slower_test_settings
48
48
  @byte_content
49
49
  def test_size(path: Path, content: bytes) -> None:
50
50
  assert isinstance(Path.size, property)
File without changes
File without changes
File without changes