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.
- {superpathlib-2.0.9/src/superpathlib.egg-info → superpathlib-2.0.11}/PKG-INFO +8 -6
- {superpathlib-2.0.9 → superpathlib-2.0.11}/pyproject.toml +25 -9
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/cached_content.py +1 -1
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/common_folders.py +17 -13
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/content_properties.py +2 -2
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/encryption.py +1 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/extra_functionality.py +42 -38
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/metadata_properties.py +1 -9
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/override.py +9 -7
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/tags.py +2 -1
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/utils.py +2 -3
- {superpathlib-2.0.9 → superpathlib-2.0.11/src/superpathlib.egg-info}/PKG-INFO +8 -6
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/requires.txt +4 -3
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_cached_content.py +2 -2
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_functionality.py +4 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_metadata.py +2 -2
- {superpathlib-2.0.9 → superpathlib-2.0.11}/LICENSE +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/README.md +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/setup.cfg +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/__init__.py +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/base.py +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/path.py +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib/py.typed +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/SOURCES.txt +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/dependency_links.txt +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/src/superpathlib.egg-info/top_level.txt +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_common_folders.py +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_content.py +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_encrypted_content.py +0 -0
- {superpathlib-2.0.9 → superpathlib-2.0.11}/tests/test_inheritance.py +0 -0
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: superpathlib
|
|
3
|
-
Version: 2.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
[](https://badge.fury.io/py/superpathlib)
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "superpathlib"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.11"
|
|
4
4
|
description = "Extended Pathlib"
|
|
5
5
|
authors = [{name = "Quinten Roets", email = "qdr2104@columbia.edu"}]
|
|
6
|
-
license =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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]
|
|
@@ -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(
|
|
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
|
-
"""
|
|
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
|
|
49
|
+
def HOME(cls) -> Self: # noqa: N802
|
|
46
50
|
return cls.home()
|
|
47
51
|
|
|
48
52
|
@classmethod
|
|
49
53
|
@classproperty
|
|
50
|
-
def docs(cls
|
|
54
|
+
def docs(cls) -> Self:
|
|
51
55
|
path = cls.HOME / "Documents"
|
|
52
|
-
return typing.cast(
|
|
56
|
+
return typing.cast("Self", path)
|
|
53
57
|
|
|
54
58
|
@classmethod
|
|
55
59
|
@classproperty
|
|
56
|
-
def scripts(cls
|
|
60
|
+
def scripts(cls) -> Self:
|
|
57
61
|
path = cls.docs / "Scripts"
|
|
58
|
-
return typing.cast(
|
|
62
|
+
return typing.cast("Self", path)
|
|
59
63
|
|
|
60
64
|
@classmethod
|
|
61
65
|
@classproperty
|
|
62
|
-
def script_assets(cls
|
|
66
|
+
def script_assets(cls) -> Self:
|
|
63
67
|
path = cls.scripts / "assets"
|
|
64
|
-
return typing.cast(
|
|
68
|
+
return typing.cast("Self", path)
|
|
65
69
|
|
|
66
70
|
@classmethod
|
|
67
71
|
@classproperty
|
|
68
|
-
def assets(cls
|
|
72
|
+
def assets(cls) -> Self:
|
|
69
73
|
"""
|
|
70
74
|
Often overwritten by child classes for specific project.
|
|
71
75
|
"""
|
|
72
|
-
return typing.cast(
|
|
76
|
+
return typing.cast("Self", cls.script_assets)
|
|
73
77
|
|
|
74
78
|
@classmethod
|
|
75
79
|
@classproperty
|
|
76
|
-
def draft(cls
|
|
80
|
+
def draft(cls) -> Self:
|
|
77
81
|
path = cls.docs / "draft.txt"
|
|
78
|
-
return typing.cast(
|
|
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:
|
|
@@ -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,
|
|
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
|
|
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
|
|
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) ->
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
86
|
-
extraction_directory:
|
|
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:
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
192
|
-
condition: Callable[[
|
|
193
|
-
exclude: Callable[[
|
|
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[
|
|
199
|
-
"""
|
|
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:
|
|
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(_:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
261
|
+
func(path_str)
|
|
259
262
|
else:
|
|
260
263
|
raise exc_info[0]
|
|
261
264
|
|
|
262
|
-
def subpath(self
|
|
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
|
|
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
|
|
281
|
+
cls,
|
|
279
282
|
*,
|
|
280
283
|
in_memory: bool = True,
|
|
281
284
|
create: bool = True,
|
|
282
285
|
**kwargs: Any,
|
|
283
|
-
) ->
|
|
284
|
-
"""
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
85
|
+
def replace(self, target: str | PathLike[str]) -> Self:
|
|
84
86
|
path = self.rename(target, exist_ok=True)
|
|
85
|
-
return typing.cast(
|
|
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:
|
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: superpathlib
|
|
3
|
-
Version: 2.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
[](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.
|
|
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.
|
|
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.
|
|
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
|
-
@
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|