pathlibutil 0.2.1__tar.gz → 0.3.2__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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pathlibutil
3
- Version: 0.2.1
3
+ Version: 0.3.2
4
4
  Summary: inherits from pathlib.Path with methods for hashing, copying, deleting and more
5
5
  Home-page: https://d-chris.github.io
6
6
  License: MIT
7
- Keywords: pathlib,hashlib,shutil
7
+ Keywords: pathlib,hashlib,shutil,urllib.parse
8
8
  Author: Christoph Dörrer
9
9
  Author-email: d-chris@web.de
10
- Requires-Python: >=3.8.1,<3.13
10
+ Requires-Python: >=3.8.1,<4.0.0
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.9
15
15
  Classifier: Programming Language :: Python :: 3.10
16
16
  Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
18
19
  Classifier: Programming Language :: Python :: 3.8
19
20
  Provides-Extra: 7z
20
21
  Requires-Dist: py7zr (>=0.20.2,<0.21.0) ; extra == "7z"
@@ -22,17 +23,21 @@ Project-URL: Documentation, https://d-chris.github.io/pathlibutil
22
23
  Project-URL: Repository, https://github.com/d-chris/pathlibutil
23
24
  Description-Content-Type: text/markdown
24
25
 
26
+ <!--
27
+ filename: ./README.md
28
+ -->
29
+
25
30
  # pathlibutil
26
31
 
27
32
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/)
28
- [![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
33
+ [![PyPI - Version](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
29
34
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
30
35
  [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
31
- [![GitHub Workflow Test)](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
32
- [![Website](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/pathlibutil)
33
- [![GitHub tag (with filter)](https://img.shields.io/github/v/tag/d-chris/pathlibutil?logo=github&label=github)](https://github.com/d-chris/pathlibutil)
34
- [![Coverage](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil%2Fhtmlcov&up_message=available&down_message=missing&logo=codecov&label=coverage)](https://d-chris.github.io/pathlibutil/htmlcov)
35
- [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
36
+ [![GitHub - Pytest](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
37
+ [![GitHub - Page](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/pathlibutil)
38
+ [![GitHub - Release](https://img.shields.io/github/v/tag/d-chris/pathlibutil?logo=github&label=github)](https://github.com/d-chris/pathlibutil)
39
+ [![codecov](https://codecov.io/gh/d-chris/pathlibutil/graph/badge.svg?token=U7I9FYMRSR)](https://codecov.io/gh/d-chris/pathlibutil)
40
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/.pre-commit-config.yaml)
36
41
 
37
42
  ---
38
43
 
@@ -58,11 +63,20 @@ Description-Content-Type: text/markdown
58
63
  - `Path.resolve()` to resolve a unc path to a mapped windows drive.
59
64
  - `Path.walk()` to walk over a directory tree like `os.walk()`
60
65
  - `Path.iterdir()` with `recursive` all files from the directory tree will be yielded and `exclude_dirs` via callable.
66
+ - `Path.is_expired()` to check if a file is expired by a given `datetime.timedelta`
67
+ - `Path.expand()` yields file paths for multiple file patterns if they exsits.
61
68
 
62
69
  JSON serialization of `Path` objects is supported in `pathlibutil.json`.
63
70
 
64
71
  - `pathlibutil.json.dumps()` and `pathlibutil.json.dump()` to serialize `Path` objects as posix paths.
65
72
 
73
+ Parse and modify URLs with `pathlibutil.urlpath`.
74
+
75
+ - `pathlibutil.urlpath.UrlPath()` modify URL and easy access the `path` of the url like a `pathlib.PurePosixPath` object.
76
+ - `pathlibutil.urlpath.UrlNetloc()` to parse and modify the `netloc` part of a URL.
77
+ - `pathlibutil.urlpath.normalize_url()` to normalize a URL string.
78
+
79
+
66
80
  ## Installation
67
81
 
68
82
  ```bash
@@ -123,7 +137,7 @@ file = Path("pathlibutil.md5")
123
137
 
124
138
  with file.open("w") as f:
125
139
  f.write(
126
- f"# MD5 checksums generated with pathlibutil "
140
+ "# MD5 checksums generated with pathlibutil "
127
141
  "(https://pypi.org/project/pathlibutil/)\n\n"
128
142
  )
129
143
 
@@ -308,4 +322,3 @@ Path.cwd(frozen=True) is K:/pathlibutil/examples
308
322
  Path.cwd(frozen=False) is K:/pathlibutil
309
323
  Path.cwd(frozen=_MEIPASS) is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
310
324
  ```
311
-
@@ -1,14 +1,18 @@
1
+ <!--
2
+ filename: ./README.md
3
+ -->
4
+
1
5
  # pathlibutil
2
6
 
3
7
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pathlibutil)](https://pypi.org/project/pathlibutil/)
4
- [![PyPI](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
8
+ [![PyPI - Version](https://img.shields.io/pypi/v/pathlibutil)](https://pypi.org/project/pathlibutil/)
5
9
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pathlibutil)](https://pypi.org/project/pathlibutil/)
6
10
  [![PyPI - License](https://img.shields.io/pypi/l/pathlibutil)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/LICENSE)
7
- [![GitHub Workflow Test)](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
8
- [![Website](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/pathlibutil)
9
- [![GitHub tag (with filter)](https://img.shields.io/github/v/tag/d-chris/pathlibutil?logo=github&label=github)](https://github.com/d-chris/pathlibutil)
10
- [![Coverage](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil%2Fhtmlcov&up_message=available&down_message=missing&logo=codecov&label=coverage)](https://d-chris.github.io/pathlibutil/htmlcov)
11
- [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
11
+ [![GitHub - Pytest](https://img.shields.io/github/actions/workflow/status/d-chris/pathlibutil/pytest.yml?logo=github&label=pytest)](https://github.com/d-chris/pathlibutil/actions/workflows/pytest.yml)
12
+ [![GitHub - Page](https://img.shields.io/website?url=https%3A%2F%2Fd-chris.github.io%2Fpathlibutil&up_message=pdoc&logo=github&label=documentation)](https://d-chris.github.io/pathlibutil)
13
+ [![GitHub - Release](https://img.shields.io/github/v/tag/d-chris/pathlibutil?logo=github&label=github)](https://github.com/d-chris/pathlibutil)
14
+ [![codecov](https://codecov.io/gh/d-chris/pathlibutil/graph/badge.svg?token=U7I9FYMRSR)](https://codecov.io/gh/d-chris/pathlibutil)
15
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://raw.githubusercontent.com/d-chris/pathlibutil/main/.pre-commit-config.yaml)
12
16
 
13
17
  ---
14
18
 
@@ -34,11 +38,20 @@
34
38
  - `Path.resolve()` to resolve a unc path to a mapped windows drive.
35
39
  - `Path.walk()` to walk over a directory tree like `os.walk()`
36
40
  - `Path.iterdir()` with `recursive` all files from the directory tree will be yielded and `exclude_dirs` via callable.
41
+ - `Path.is_expired()` to check if a file is expired by a given `datetime.timedelta`
42
+ - `Path.expand()` yields file paths for multiple file patterns if they exsits.
37
43
 
38
44
  JSON serialization of `Path` objects is supported in `pathlibutil.json`.
39
45
 
40
46
  - `pathlibutil.json.dumps()` and `pathlibutil.json.dump()` to serialize `Path` objects as posix paths.
41
47
 
48
+ Parse and modify URLs with `pathlibutil.urlpath`.
49
+
50
+ - `pathlibutil.urlpath.UrlPath()` modify URL and easy access the `path` of the url like a `pathlib.PurePosixPath` object.
51
+ - `pathlibutil.urlpath.UrlNetloc()` to parse and modify the `netloc` part of a URL.
52
+ - `pathlibutil.urlpath.normalize_url()` to normalize a URL string.
53
+
54
+
42
55
  ## Installation
43
56
 
44
57
  ```bash
@@ -99,7 +112,7 @@ file = Path("pathlibutil.md5")
99
112
 
100
113
  with file.open("w") as f:
101
114
  f.write(
102
- f"# MD5 checksums generated with pathlibutil "
115
+ "# MD5 checksums generated with pathlibutil "
103
116
  "(https://pypi.org/project/pathlibutil/)\n\n"
104
117
  )
105
118
 
@@ -283,4 +296,4 @@ os.getcwd is K:/pathlibutil
283
296
  Path.cwd(frozen=True) is K:/pathlibutil/examples
284
297
  Path.cwd(frozen=False) is K:/pathlibutil
285
298
  Path.cwd(frozen=_MEIPASS) is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
286
- ```
299
+ ```
@@ -0,0 +1,44 @@
1
+ import os
2
+ import pathlib
3
+ import sys
4
+ from typing import Generator, TypeVar
5
+
6
+ _Path = TypeVar("_Path", bound=pathlib.Path)
7
+
8
+
9
+ class BasePath(pathlib.Path):
10
+ """
11
+ Baseclass to inherit from `pathlib.Path`.
12
+
13
+ This class is only needed for python versions < 3.12.
14
+ """
15
+
16
+ if sys.version_info < (3, 12):
17
+ _flavour = (
18
+ pathlib._windows_flavour if os.name == "nt" else pathlib._posix_flavour
19
+ )
20
+
21
+ @classmethod
22
+ def expand(cls, file: str) -> Generator[_Path, None, None]:
23
+ """
24
+ yields only Path object of file names that exists. Supports glob patterns in
25
+ filename as wildcards.
26
+
27
+ >>> list(Path.expand(__file__))
28
+ [BasePath('pathlibutil/base.py')]
29
+
30
+ >>> list(Path.expand("pathlibutil/*.py")
31
+ [BasePath('pathlibutil/base.py'), BasePath('pathlibutil/json.py'),
32
+ BasePath('pathlibutil/path.py'), BasePath('pathlibutil/types.py'),
33
+ BasePath('pathlibutil/__init__.py')]
34
+ """
35
+
36
+ file = cls(file)
37
+ try:
38
+ file.resolve(True)
39
+ except (OSError, FileNotFoundError):
40
+ parent, pattern = file.parent, file.name
41
+
42
+ yield from parent.glob(pattern)
43
+ else:
44
+ yield file
@@ -6,10 +6,11 @@ import re
6
6
  import shutil
7
7
  import subprocess
8
8
  import sys
9
+ from datetime import datetime, timedelta
9
10
  from typing import Callable, Dict, Generator, List, Literal, Set, Tuple, Union
10
11
 
11
12
  from pathlibutil.base import BasePath, _Path
12
- from pathlibutil.types import ByteInt, StatResult, _stat_result, byteint
13
+ from pathlibutil.types import ByteInt, StatResult, TimeInt, _stat_result, byteint
13
14
 
14
15
 
15
16
  class Path(BasePath):
@@ -635,6 +636,52 @@ class Path(BasePath):
635
636
  else:
636
637
  yield from super().iterdir()
637
638
 
639
+ def is_expired(self, *, stat="st_mtime", **kwargs) -> bool:
640
+ """
641
+ Returns `True` if the time of the file is greater than a given threshold.
642
+
643
+ For `**kwargs` see `datetime.timedelta`.
644
+
645
+ >>> Path("README.md").is_expired(weeks=9999)
646
+ False
647
+ """
648
+ try:
649
+ attr: TimeInt = getattr(self.stat(), stat)
650
+ diff = datetime.now() - attr.datetime
651
+ except AttributeError as e:
652
+ stats = [attr for attr in dir(os.stat_result) if attr.endswith("time")]
653
+
654
+ raise ValueError(f"{stat=} is not from {stats}") from e
655
+
656
+ return diff > timedelta(**kwargs)
657
+
658
+ @classmethod
659
+ def expand(
660
+ cls,
661
+ *files: str,
662
+ duplicates: bool = True,
663
+ ) -> Generator[_Path, None, None]:
664
+ """
665
+ Yields only Path object of file names that exists. Supports glob patterns in
666
+ filename as wildcards.
667
+
668
+ If `duplicates` is `False` only one instance of each file is yielded.
669
+
670
+ >>> list(Path.expand("README.md", "*.md", duplicates=False))
671
+ [Path('README.md')]
672
+ """
673
+ if duplicates:
674
+ for file in files:
675
+ yield from super().expand(file)
676
+ else:
677
+ seen = set()
678
+
679
+ for file in files:
680
+ for item in super().expand(file):
681
+ if item not in seen:
682
+ seen.add(item)
683
+ yield item
684
+
638
685
 
639
686
  class Register7zFormat(Path, archive="7z"):
640
687
  """
@@ -0,0 +1,404 @@
1
+ import itertools
2
+ import pathlib
3
+ import re
4
+ import urllib.parse as up
5
+ import urllib.request
6
+ from dataclasses import asdict, dataclass, field
7
+ from functools import cached_property, wraps
8
+ from typing import Any, Dict, Optional, Tuple, TypeVar, Union
9
+
10
+
11
+ @dataclass
12
+ class UrlNetloc:
13
+ """
14
+ A dataclass to represent the netloc part of a URL.
15
+
16
+ >>> url = UrlNetloc.from_netloc("www.example.com:443")
17
+ >>> url.port = None
18
+ >>> str(url)
19
+ 'www.example.com'
20
+ """
21
+
22
+ hostname: str
23
+ port: Optional[int] = field(default=None)
24
+ username: Optional[str] = field(default=None)
25
+ password: Optional[str] = field(default=None)
26
+
27
+ def __str__(self) -> str:
28
+ return self.netloc
29
+
30
+ @property
31
+ def netloc(self) -> str:
32
+ """netloc string representation of the `dataclass`"""
33
+
34
+ netloc = ""
35
+
36
+ if self.username:
37
+ netloc += self.username
38
+
39
+ if self.password:
40
+ netloc += f":{self.password}"
41
+
42
+ netloc += "@"
43
+
44
+ if ":" in self.hostname:
45
+ netloc += f"[{self.hostname}]"
46
+ else:
47
+ netloc += self.hostname
48
+
49
+ if self.port:
50
+ netloc += f":{self.port:d}"
51
+
52
+ return netloc
53
+
54
+ @classmethod
55
+ def from_netloc(cls, netloc: str, normalize: bool = False) -> "UrlNetloc":
56
+ """Parse a netloc string into a `UrlNetloc` object"""
57
+
58
+ if not netloc.startswith("//"):
59
+ netloc = f"//{netloc}"
60
+
61
+ url = up.urlparse(netloc)
62
+
63
+ hostname = url.hostname
64
+
65
+ if normalize is False:
66
+ try:
67
+ pattern = re.escape(url.hostname)
68
+ hostname = re.search(pattern, netloc, re.IGNORECASE).group()
69
+ except AttributeError:
70
+ pass
71
+
72
+ return cls(
73
+ hostname=hostname,
74
+ port=url.port,
75
+ username=url.username,
76
+ password=url.password,
77
+ )
78
+
79
+ def to_dict(self, prune: bool = False) -> Dict[str, Any]:
80
+ """
81
+ Convert the `UrlNetloc` object to a dictionary
82
+
83
+ If `prune` is `True`, remove all key-value pairs from the dict where the value
84
+ is `None`.
85
+ """
86
+
87
+ data = asdict(self)
88
+
89
+ if not prune:
90
+ return data
91
+
92
+ return {k: v for k, v in data.items() if v is not None}
93
+
94
+
95
+ _UrlPath = TypeVar("_UrlPath", bound="UrlPath")
96
+
97
+
98
+ def normalize_url(
99
+ url: str,
100
+ port: bool = False,
101
+ sort: bool = True,
102
+ ) -> str:
103
+ """
104
+ Function to normalize a URL by converting the scheme and host to lowercase, removing
105
+ port if present, and sorting the query parameters.
106
+
107
+ >>> normalize_url("https://www.ExamplE.com:443/Path?b=2&a=1")
108
+ 'https://www.example.com/Path?a=1&b=2'
109
+ """
110
+
111
+ url = UrlPath(url)
112
+
113
+ if port is False:
114
+ ports = {url.scheme.lower(): url.port}
115
+ else:
116
+ ports = {}
117
+
118
+ return url.normalize(sort=sort, ports=ports)
119
+
120
+
121
+ def urlpath(func):
122
+ """
123
+ decorator to return a `UrlPath` object from a `urllib.parse.ParseResult` object.
124
+ """
125
+
126
+ @wraps(func)
127
+ def wrapper(self, *args, **kwargs) -> _UrlPath:
128
+ result = func(self, *args, **kwargs)
129
+
130
+ return self.__class__(result.geturl(), **self._kwargs)
131
+
132
+ return wrapper
133
+
134
+
135
+ class UrlPath(up.ParseResult):
136
+ """
137
+ Class to manipulate URLs to change the scheme, netloc, path, query, and fragment.
138
+
139
+ Wrap the `pathlib.PurePosixPath` methods to return a new `UrlPath` object
140
+
141
+ >>> url = UrlPath("https://www.example.com/path/to/file").with_suffix(".txt")
142
+ >>> str(url)
143
+ 'https://www.example.com/path/to/file.txt'
144
+
145
+ """
146
+
147
+ _default_ports = {
148
+ "http": 80,
149
+ "https": 443,
150
+ }
151
+
152
+ def __new__(cls, url, **kwargs) -> _UrlPath:
153
+ parsed_url = up.urlparse(url, **kwargs)
154
+ return super().__new__(cls, *parsed_url)
155
+
156
+ def __init__(
157
+ self,
158
+ url: str,
159
+ scheme: str = "",
160
+ allow_fragments: bool = True,
161
+ ) -> None:
162
+ """
163
+ Initialize the `UrlPath` object with a URL string.
164
+
165
+ A `ValueError` is raised if the URL is not valid.
166
+ """
167
+ self._url = url
168
+ self._kwargs = {
169
+ "scheme": scheme,
170
+ "allow_fragments": allow_fragments,
171
+ }
172
+ self._path = pathlib.PurePosixPath(up.unquote(self.path))
173
+
174
+ def __str__(self) -> str:
175
+ return self.normalize()
176
+
177
+ def geturl(self, normalize: bool = False) -> str:
178
+ """
179
+ Return a re-combined version of the URL.
180
+
181
+ If `normalize` is `True` scheme and netloc is converted to lowercase,
182
+ default ports are removed and query parameters are sorted.
183
+ """
184
+ if normalize:
185
+ return self.normalize()
186
+
187
+ return super().geturl()
188
+
189
+ def normalize(self, sort: bool = True, **kwargs) -> str:
190
+ """
191
+ Normalize the URL by converting the scheme and host to lowercase, removing the
192
+ default port if present, and sorting the query parameters.
193
+ """
194
+
195
+ ports = kwargs.get("ports", self._default_ports)
196
+
197
+ scheme = self.scheme.lower()
198
+ netloc = UrlNetloc.from_netloc(self.netloc, normalize=True)
199
+
200
+ try:
201
+ if ports[scheme] == netloc.port:
202
+ netloc.port = None
203
+ except KeyError:
204
+ pass
205
+
206
+ path = up.quote(up.unquote(self.path))
207
+ query = up.urlencode(sorted(up.parse_qsl(self.query))) if sort else self.query
208
+
209
+ return up.urlunparse(
210
+ (
211
+ scheme,
212
+ str(netloc),
213
+ path,
214
+ self.params,
215
+ query,
216
+ self.fragment,
217
+ )
218
+ )
219
+
220
+ def __getattr__(self, attr: str) -> Any:
221
+
222
+ try:
223
+ attr = getattr(self._path, attr)
224
+ except AttributeError as e:
225
+ raise AttributeError(
226
+ f"'{self.__class__.__name__}' object has no attribute '{attr}'"
227
+ ) from e
228
+
229
+ if not callable(attr):
230
+ return attr
231
+
232
+ @wraps(attr)
233
+ def wrapper(*args, **kwargs) -> _UrlPath:
234
+ result = attr(*args, **kwargs)
235
+
236
+ return self.with_path(result)
237
+
238
+ return wrapper
239
+
240
+ @urlpath
241
+ def with_scheme(self, scheme: str) -> _UrlPath:
242
+ """
243
+ Change the scheme of the URL.
244
+ """
245
+ return self._replace(scheme=scheme)
246
+
247
+ @urlpath
248
+ def with_netloc(self, netloc: Union[str, UrlNetloc]) -> _UrlPath:
249
+ """
250
+ Change the netloc of the URL.
251
+ """
252
+ return self._replace(netloc=str(netloc))
253
+
254
+ @urlpath
255
+ def with_path(self, path: Union[str, pathlib.PurePosixPath]) -> _UrlPath:
256
+ """
257
+ Change the path of the URL.
258
+ """
259
+
260
+ try:
261
+ path = path.as_posix()
262
+ except AttributeError as e:
263
+ if not isinstance(path, str):
264
+ raise TypeError(
265
+ f"Expected str or PurePosixPath, got {type(path)}"
266
+ ) from e
267
+
268
+ return self._replace(path=path)
269
+
270
+ @urlpath
271
+ def with_params(self, params: str) -> _UrlPath:
272
+ """
273
+ Change the parameters of the URL.
274
+ """
275
+ return self._replace(params=params)
276
+
277
+ @urlpath
278
+ def with_query(self, query: str) -> _UrlPath:
279
+ """
280
+ Change the query of the URL.
281
+ """
282
+ return self._replace(query=query)
283
+
284
+ @urlpath
285
+ def with_fragment(self, fragment: str) -> _UrlPath:
286
+ """
287
+ Change the fragment of the URL.
288
+ """
289
+ return self._replace(fragment=fragment)
290
+
291
+ def with_port(self, port: int) -> _UrlPath:
292
+ """
293
+ change the port in the netloc of the URL.
294
+
295
+ If `port` is `None`, the port is removed.
296
+ """
297
+
298
+ netloc = UrlNetloc.from_netloc(self.netloc)
299
+ netloc.port = port
300
+
301
+ return self.with_netloc(netloc)
302
+
303
+ def with_hostname(self, hostname: str) -> _UrlPath:
304
+ """
305
+ change the hostname in the netloc of the URL
306
+ """
307
+
308
+ netloc = UrlNetloc.from_netloc(self.netloc)
309
+ netloc.hostname = hostname
310
+
311
+ return self.with_netloc(netloc)
312
+
313
+ def with_credentials(self, username: str, password: str = None) -> _UrlPath:
314
+ """
315
+ change the username and password in the netloc of the URL
316
+
317
+ to change only `username` the `password` must also be provided.
318
+
319
+ If `username` is `None`, the credentials are removed.
320
+ """
321
+
322
+ netloc = UrlNetloc.from_netloc(self.netloc)
323
+ netloc.username = username
324
+ netloc.password = password
325
+
326
+ return self.with_netloc(netloc)
327
+
328
+ @cached_property
329
+ def parts(self) -> Tuple[str, ...]:
330
+ """
331
+ return the parts of the path without any '/'.
332
+ """
333
+ return tuple(part for part in self._path.parts if not part.startswith("/"))
334
+
335
+ @property
336
+ def anchor(self) -> str:
337
+ """
338
+ The concatenation of the netloc and root of the path.
339
+
340
+ >>> UrlPath("//server/root/path/file.txt").anchor
341
+ '//server/root'
342
+ """
343
+ try:
344
+ root = self.parts[0]
345
+ except IndexError:
346
+ root = ""
347
+
348
+ return f"//{self.netloc}/{root}"
349
+
350
+ def with_anchor(self, anchor: str, root: bool = False, **kwargs) -> _UrlPath:
351
+ """
352
+ Change the anchor of the URL.
353
+
354
+ If `root` is `True`, the root of the path will not be removed.
355
+
356
+ >>> url = UrlPath("//server/root/path/file.txt")
357
+ >>> url.with_anchor("https://www.server.com").geturl()
358
+ 'https://www.server.com/path/file.txt'
359
+ """
360
+ anchor = self.__class__(anchor, **kwargs)
361
+
362
+ url = self.with_netloc(anchor.netloc)
363
+
364
+ if anchor.scheme != url.scheme:
365
+ url = url.with_scheme(anchor.scheme)
366
+
367
+ if root is False:
368
+ parts = url.parts[1:]
369
+ else:
370
+ parts = url.parts
371
+
372
+ # if anchor has a path, anchor and url path are concatenated
373
+ if any(anchor.parts):
374
+ return url.with_path("/".join(itertools.chain(anchor.parts, parts)))
375
+
376
+ # if root is False, the root of the path is removed
377
+ if root is False:
378
+ return url.with_path("/".join(parts))
379
+
380
+ return url
381
+
382
+ def exists(self, errors: bool = False, **kwargs) -> bool:
383
+ """
384
+ Check if the URL returns a 200 status code.
385
+
386
+ If `errors` is `False`, exceptions are suppressed and `False` is returned.
387
+
388
+ For `kwargs` see `urllib.request.urlopen`.
389
+ """
390
+ try:
391
+ with urllib.request.urlopen(self.normalize(False), **kwargs) as response:
392
+ return response.status == 200
393
+ except Exception as e:
394
+ if errors is not False:
395
+ raise e
396
+
397
+ return False
398
+
399
+
400
+ __all__ = [
401
+ "UrlNetloc",
402
+ "UrlPath",
403
+ "normalize_url",
404
+ ]
@@ -1,13 +1,11 @@
1
1
  [build-system]
2
2
  build-backend = "poetry.core.masonry.api"
3
3
 
4
- requires = [
5
- "poetry-core",
6
- ]
4
+ requires = [ "poetry-core" ]
7
5
 
8
6
  [tool.poetry]
9
7
  name = "pathlibutil"
10
- version = "v0.2.1"
8
+ version = "v0.3.2"
11
9
  description = "inherits from pathlib.Path with methods for hashing, copying, deleting and more"
12
10
  authors = [ "Christoph Dörrer <d-chris@web.de>" ]
13
11
  readme = "README.md"
@@ -18,40 +16,36 @@ classifiers = [
18
16
  "Programming Language :: Python :: 3.10",
19
17
  "Programming Language :: Python :: 3.11",
20
18
  "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
21
20
  "License :: OSI Approved :: MIT License",
22
21
  "Operating System :: OS Independent",
23
22
  ]
24
- keywords = [ "pathlib", "hashlib", "shutil" ]
23
+ keywords = [ "pathlib", "hashlib", "shutil", "urllib.parse" ]
25
24
  homepage = "https://d-chris.github.io"
26
25
  repository = "https://github.com/d-chris/pathlibutil"
27
26
  documentation = "https://d-chris.github.io/pathlibutil"
28
- include = [ "LICENSE" ]
29
27
 
30
28
  [tool.poetry.dependencies]
31
- python = ">=3.8.1,<3.13"
29
+ python = "^3.8.1"
32
30
  py7zr = { version = "^0.20.2", optional = true }
33
31
 
34
32
  [tool.poetry.extras]
35
33
  7z = [ "py7zr" ]
36
34
 
37
35
  [tool.poetry.group.dev.dependencies]
38
- pytest = "^7.4.3"
39
36
  tox = "^4.11.4"
37
+ pyinstaller = { version = "^6.10.0", python = "<3.14" }
38
+
39
+ [tool.poetry.group.test.dependencies]
40
+ pytest = "^8.3.3"
40
41
  pytest-random-order = "^1.1.0"
41
42
  pytest-cov = "^4.1.0"
42
43
  pytest-mock = "^3.12.0"
43
- exrex = "^0.11.0"
44
+ exrex = { git = "https://github.com/asciimoo/exrex", rev = "1c22c70" }
44
45
 
45
- [tool.poetry.group.code.dependencies]
46
- flake8 = "^7.0.0"
47
- black = "^23.12.1"
48
- docformatter = "^1.7.5"
49
-
50
- [tool.poetry.group.doc.dependencies]
51
- jinja2-pdoc = "^0.2.0"
46
+ [tool.poetry.group.docs.dependencies]
52
47
  pdoc = "^14.3.0"
53
- click = "^8.1.7"
54
- pyinstaller = "^6.5.0"
48
+ jinja2-pdoc = "^1.1.0"
55
49
 
56
50
  [[tool.poetry.source]]
57
51
  name = "PyPI"
@@ -62,6 +56,9 @@ name = "testpypi"
62
56
  url = "https://test.pypi.org/legacy/"
63
57
  priority = "explicit"
64
58
 
59
+ [tool.isort]
60
+ profile = "black"
61
+
65
62
  [tool.pytest.ini_options]
66
63
  minversion = "6.0"
67
64
  testpaths = "tests"
@@ -69,8 +66,7 @@ addopts = [
69
66
  "--random-order",
70
67
  "--color=yes",
71
68
  "-s",
72
- # "--cov=pathlibutil",
73
- # "--cov-report=term-missing:skip-covered",
74
- # "--cov-append",
75
- # "--cov-report=html",
69
+ "--cov=pathlibutil",
70
+ "--cov-report=term-missing:skip-covered",
71
+ "--cov-report=xml",
76
72
  ]
@@ -1,19 +0,0 @@
1
- import os
2
- import pathlib
3
- import sys
4
- from typing import TypeVar
5
-
6
- _Path = TypeVar("_Path", bound=pathlib.Path)
7
-
8
-
9
- class BasePath(pathlib.Path):
10
- """
11
- Baseclass to inherit from `pathlib.Path`.
12
-
13
- This class is only needed for python versions < 3.12.
14
- """
15
-
16
- if sys.version_info < (3, 12):
17
- _flavour = (
18
- pathlib._windows_flavour if os.name == "nt" else pathlib._posix_flavour
19
- )
File without changes