pathlibutil 0.1.8__tar.gz → 0.2.0__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pathlibutil
3
- Version: 0.1.8
3
+ Version: 0.2.0
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
@@ -32,6 +32,7 @@ Description-Content-Type: text/markdown
32
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
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
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)
35
36
 
36
37
  ---
37
38
 
@@ -52,6 +53,15 @@ Description-Content-Type: text/markdown
52
53
  - `TimeInt` objects for `atime`, `ctime`, `mtime` and `birthtime`
53
54
  - `ByteInt` object for `size`
54
55
  - `Path.relative_to()` to get relative path from a file or directory, `walk_up` to walk up the directory tree.
56
+ - `Path.with_suffix()` to change the multiple suffixes of a file
57
+ - `Path.cwd()` to get the current working directory or executable path when script is bundled, e.g. with `pyinstaller`
58
+ - `Path.resolve()` to resolve a unc path to a mapped windows drive.
59
+ - `Path.walk()` to walk over a directory tree like `os.walk()`
60
+ - `Path.iterdir()` with `recursive` all files from the directory tree will be yielded and `exclude_dirs` via callable.
61
+
62
+ JSON serialization of `Path` objects is supported in `pathlibutil.json`.
63
+
64
+ - `pathlibutil.json.dumps()` and `pathlibutil.json.dump()` to serialize `Path` objects as posix paths.
55
65
 
56
66
  ## Installation
57
67
 
@@ -173,7 +183,7 @@ deleted directories and the amount of memory freed in MB.
173
183
  > `Path.delete()`, `Path.size()` and `ByteInt`
174
184
 
175
185
  ```python
176
- from pathlibutil import Path, ByteInt
186
+ from pathlibutil import ByteInt, Path
177
187
 
178
188
  mem = ByteInt(0)
179
189
  i = 0
@@ -200,9 +210,10 @@ to register new archive formats.
200
210
  > Path.make_archive(), Path.archive_formats and Path.move()
201
211
 
202
212
  ```python
203
- import pathlibutil
204
213
  import shutil
205
214
 
215
+ import pathlibutil
216
+
206
217
 
207
218
  class RegisterFooBarFormat(pathlibutil.Path, archive="foobar"):
208
219
  @classmethod
@@ -261,3 +272,40 @@ backup = archive.move("./backup/")
261
272
  print(f"archive created: {archive.name} and moved to: {backup.parent}")
262
273
  ```
263
274
 
275
+ ## Example 6
276
+
277
+ Access the current working directory with optional parameter `frozen` to determine
278
+ different directories when script is bundled to an executable,
279
+ e.g. with `pyinstaller`.
280
+ > `Path.cwd()`
281
+
282
+ ```cmd
283
+ >>> poetry run examples/example6.py -b
284
+ Building frozen: K:/pathlibutil/examples/example6.exe
285
+ Build succeeded: 0
286
+
287
+ >>> poetry run examples/example6.py
288
+ we are not frozen
289
+
290
+ bundle dir is K:/pathlibutil/examples
291
+ sys.argv[0] is K:/pathlibutil/examples/example6.py
292
+ sys.executable is K:/pathlibutil/.venv/Scripts/python.exe
293
+ os.getcwd is K:/pathlibutil
294
+
295
+ Path.cwd(frozen=True) is K:/pathlibutil
296
+ Path.cwd(frozen=False) is K:/pathlibutil
297
+ Path.cwd(frozen=_MEIPASS) is K:/pathlibutil
298
+
299
+ >>> examples/example6.exe
300
+ we are ever so frozen
301
+
302
+ bundle dir is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
303
+ sys.argv[0] is examples/example6.exe
304
+ sys.executable is K:/pathlibutil/examples/example6.exe
305
+ os.getcwd is K:/pathlibutil
306
+
307
+ Path.cwd(frozen=True) is K:/pathlibutil/examples
308
+ Path.cwd(frozen=False) is K:/pathlibutil
309
+ Path.cwd(frozen=_MEIPASS) is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
310
+ ```
311
+
@@ -8,6 +8,7 @@
8
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
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
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
12
 
12
13
  ---
13
14
 
@@ -28,6 +29,15 @@
28
29
  - `TimeInt` objects for `atime`, `ctime`, `mtime` and `birthtime`
29
30
  - `ByteInt` object for `size`
30
31
  - `Path.relative_to()` to get relative path from a file or directory, `walk_up` to walk up the directory tree.
32
+ - `Path.with_suffix()` to change the multiple suffixes of a file
33
+ - `Path.cwd()` to get the current working directory or executable path when script is bundled, e.g. with `pyinstaller`
34
+ - `Path.resolve()` to resolve a unc path to a mapped windows drive.
35
+ - `Path.walk()` to walk over a directory tree like `os.walk()`
36
+ - `Path.iterdir()` with `recursive` all files from the directory tree will be yielded and `exclude_dirs` via callable.
37
+
38
+ JSON serialization of `Path` objects is supported in `pathlibutil.json`.
39
+
40
+ - `pathlibutil.json.dumps()` and `pathlibutil.json.dump()` to serialize `Path` objects as posix paths.
31
41
 
32
42
  ## Installation
33
43
 
@@ -149,7 +159,7 @@ deleted directories and the amount of memory freed in MB.
149
159
  > `Path.delete()`, `Path.size()` and `ByteInt`
150
160
 
151
161
  ```python
152
- from pathlibutil import Path, ByteInt
162
+ from pathlibutil import ByteInt, Path
153
163
 
154
164
  mem = ByteInt(0)
155
165
  i = 0
@@ -176,9 +186,10 @@ to register new archive formats.
176
186
  > Path.make_archive(), Path.archive_formats and Path.move()
177
187
 
178
188
  ```python
179
- import pathlibutil
180
189
  import shutil
181
190
 
191
+ import pathlibutil
192
+
182
193
 
183
194
  class RegisterFooBarFormat(pathlibutil.Path, archive="foobar"):
184
195
  @classmethod
@@ -236,3 +247,40 @@ backup = archive.move("./backup/")
236
247
 
237
248
  print(f"archive created: {archive.name} and moved to: {backup.parent}")
238
249
  ```
250
+
251
+ ## Example 6
252
+
253
+ Access the current working directory with optional parameter `frozen` to determine
254
+ different directories when script is bundled to an executable,
255
+ e.g. with `pyinstaller`.
256
+ > `Path.cwd()`
257
+
258
+ ```cmd
259
+ >>> poetry run examples/example6.py -b
260
+ Building frozen: K:/pathlibutil/examples/example6.exe
261
+ Build succeeded: 0
262
+
263
+ >>> poetry run examples/example6.py
264
+ we are not frozen
265
+
266
+ bundle dir is K:/pathlibutil/examples
267
+ sys.argv[0] is K:/pathlibutil/examples/example6.py
268
+ sys.executable is K:/pathlibutil/.venv/Scripts/python.exe
269
+ os.getcwd is K:/pathlibutil
270
+
271
+ Path.cwd(frozen=True) is K:/pathlibutil
272
+ Path.cwd(frozen=False) is K:/pathlibutil
273
+ Path.cwd(frozen=_MEIPASS) is K:/pathlibutil
274
+
275
+ >>> examples/example6.exe
276
+ we are ever so frozen
277
+
278
+ bundle dir is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
279
+ sys.argv[0] is examples/example6.exe
280
+ sys.executable is K:/pathlibutil/examples/example6.exe
281
+ os.getcwd is K:/pathlibutil
282
+
283
+ Path.cwd(frozen=True) is K:/pathlibutil/examples
284
+ Path.cwd(frozen=False) is K:/pathlibutil
285
+ Path.cwd(frozen=_MEIPASS) is C:/Users/CHRIST~1.DOE/AppData/Local/Temp/_MEI106042
286
+ ```
@@ -3,6 +3,6 @@
3
3
  """
4
4
 
5
5
  from pathlibutil.path import Path, Register7zFormat
6
- from pathlibutil.types import ByteInt, TimeInt, byteint, StatResult
6
+ from pathlibutil.types import ByteInt, StatResult, TimeInt, byteint
7
7
 
8
8
  __all__ = ["Path", "Register7zFormat", "ByteInt", "byteint", "TimeInt", "StatResult"]
@@ -0,0 +1,25 @@
1
+ import functools
2
+ import importlib
3
+ import pathlib
4
+
5
+ json = importlib.import_module("json")
6
+
7
+ from json import load, loads
8
+
9
+
10
+ class PathEncoder(json.JSONEncoder):
11
+ def default(self, obj):
12
+ if isinstance(obj, pathlib.Path):
13
+ return obj.as_posix()
14
+
15
+ return super().default(obj)
16
+
17
+
18
+ @functools.wraps(json.dump)
19
+ def dump(obj, fp, *, cls=PathEncoder, **kwargs):
20
+ return json.dump(obj, fp, cls=cls, **kwargs)
21
+
22
+
23
+ @functools.wraps(json.dumps)
24
+ def dumps(obj, *, cls=PathEncoder, **kwargs):
25
+ return json.dumps(obj, cls=cls, **kwargs)
@@ -2,8 +2,11 @@ import errno
2
2
  import hashlib
3
3
  import itertools
4
4
  import os
5
+ import re
5
6
  import shutil
6
- from typing import Callable, Dict, Generator, List, Set, Union
7
+ import subprocess
8
+ import sys
9
+ from typing import Callable, Dict, Generator, List, Literal, Set, Tuple, Union
7
10
 
8
11
  from pathlibutil.base import BasePath, _Path
9
12
  from pathlibutil.types import ByteInt, StatResult, _stat_result, byteint
@@ -19,8 +22,8 @@ class Path(BasePath):
19
22
 
20
23
  - Contextmanger lets you change the current working directory.
21
24
  ```python
22
- with Path('path/to/directory') as cwd:
23
- print(f'current working directory: {cwd}')
25
+ with Path("path/to/directory") as cwd:
26
+ print(f"current working directory: {cwd}")
24
27
  ```
25
28
  """
26
29
 
@@ -81,15 +84,20 @@ class Path(BasePath):
81
84
  if not self.is_file():
82
85
  raise FileNotFoundError(f"'{self}' is not an existing file")
83
86
 
84
- try:
85
- args = (kwargs.pop("length"),)
86
- except KeyError:
87
- args = ()
88
-
89
- return hashlib.new(
87
+ hash = hashlib.new(
90
88
  name=algorithm or self.default_hash,
91
89
  data=self.read_bytes(),
92
- ).hexdigest(*args)
90
+ )
91
+
92
+ try:
93
+ return hash.hexdigest()
94
+ except TypeError as e:
95
+ try:
96
+ length = kwargs["length"]
97
+ except KeyError:
98
+ raise e
99
+
100
+ return hash.hexdigest(length)
93
101
 
94
102
  def verify(
95
103
  self, digest: str, algorithm: str = None, *, strict: bool = True, **kwargs
@@ -475,6 +483,157 @@ class Path(BasePath):
475
483
 
476
484
  return relative
477
485
 
486
+ @classmethod
487
+ def cwd(cls, *, frozen: Literal[True, False, "_MEIPASS"] = False) -> _Path:
488
+ """
489
+ Return a `Path` object representing the current working directory.
490
+
491
+ The `frozen` parameter takes only effect when the script is bundled to a
492
+ executable, e.g. with `pyinstaller`.
493
+
494
+ - `False`: Returns the current working directory, this is the default.
495
+ - `True`: Returns the directory of the executable.
496
+ - `"_MEIPASS"`: Returns the directory of the bundled resources.
497
+ """
498
+ if getattr(sys, "frozen", False):
499
+ if frozen is True:
500
+ return cls(sys.executable).parent
501
+ elif isinstance(frozen, str):
502
+ return cls(getattr(sys, frozen))
503
+
504
+ return super().cwd()
505
+
506
+ @classmethod
507
+ def _net_use(cls) -> Dict[str, str]:
508
+ """
509
+ Return a dictionary of mapped network drives. Keys are UNC paths and values
510
+ are drive letters.
511
+ """
512
+
513
+ def run(cmd: str) -> str:
514
+ """execute `command` and return stdout with cp850 encoding."""
515
+ result = subprocess.run(
516
+ cmd,
517
+ capture_output=True,
518
+ shell=True,
519
+ encoding="cp850",
520
+ check=True,
521
+ )
522
+
523
+ return result.stdout
524
+
525
+ try:
526
+ mapped_drives = re.finditer(
527
+ r"^OK\s+(?P<drive>[A-Z]):\s+(?P<unc>\S+)",
528
+ run("net use"),
529
+ re.IGNORECASE | re.MULTILINE,
530
+ )
531
+ return {
532
+ match.group("unc") + "\\": cls(match.group("drive") + ":\\")
533
+ for match in mapped_drives
534
+ }
535
+ except Exception:
536
+ return {}
537
+
538
+ def _resolve_unc(self) -> _Path:
539
+ """
540
+ Resolve UNC paths to mapped network drives.
541
+ """
542
+ if not hasattr(self.__class__, "_netuse"):
543
+ self.__class__._netuse = self._net_use()
544
+
545
+ try:
546
+ drive = self._netuse[self.anchor]
547
+ return drive.joinpath(self.relative_to(self.anchor))
548
+ except KeyError:
549
+ return self
550
+
551
+ def resolve(self, strict: bool = False, unc: bool = True) -> _Path:
552
+ """
553
+ Make the path absolute, resolving all symlinks on the way and also normalizing
554
+ it.
555
+
556
+ If `strict` is `True`, a `FileNotFoundError` will be raised if the path does
557
+ not exist.
558
+
559
+ On Windows if `unc` is `False`, UNC paths will be resolved to mapped network
560
+ drives.
561
+
562
+ >>> Path("T:/file.txt").resolve()
563
+ Path("\\\\server\\temp\\file.txt")
564
+
565
+ >>> Path("//server/temp/file.txt").resolve(unc=False)
566
+ Path("T:\\file.txt")
567
+ """
568
+
569
+ p = super().resolve(strict)
570
+
571
+ if unc is True or os.name != "nt":
572
+ return p
573
+
574
+ return p._resolve_unc()
575
+
576
+ def walk(
577
+ self,
578
+ top_down: bool = True,
579
+ on_error: Callable[[OSError], object] = None,
580
+ follow_symlinks: bool = False,
581
+ ) -> Generator[Tuple[_Path, List[str], List[str]], None, None]:
582
+ """
583
+ Walks the directory tree and yields a 3-tuple of (dirpath, dirnames, filenames).
584
+ """
585
+ try:
586
+ yield from super().walk(
587
+ top_down,
588
+ on_error,
589
+ follow_symlinks,
590
+ )
591
+ except AttributeError:
592
+ for dirpath, dirnames, filenames in os.walk(
593
+ self,
594
+ top_down,
595
+ on_error,
596
+ follow_symlinks,
597
+ ):
598
+ yield self.__class__(dirpath), dirnames, filenames
599
+
600
+ def iterdir(
601
+ self,
602
+ *,
603
+ recursive: Union[bool, int] = False,
604
+ exclude_dirs: Callable[[_Path], bool] = None,
605
+ **kwargs,
606
+ ) -> Generator[_Path, None, None]:
607
+ """
608
+ Iterates over the files in the directory.
609
+
610
+ If `recursive` is `True` all files from the directory tree will
611
+ be yielded if it is an `integer` files are yielded to this max. directory depth
612
+ optional` **kwargs` are passed to `Path.walk()`.
613
+
614
+ When recursing, folders can be excluded by passing a callable for `exclude_dirs`, e.g.
615
+
616
+ ```python
617
+ def exclude_version_control(dirpath: _Path) -> bool:
618
+ return dirpath.name in (".git", ".svn", ".hg", ".bzr", "CVS")
619
+ ```
620
+ """
621
+ if recursive is not False:
622
+ if exclude_dirs and not callable(exclude_dirs):
623
+ raise TypeError("exclude_dirs must be a callable")
624
+
625
+ depth = recursive if type(recursive) == int else None
626
+
627
+ for root, dirs, files in self.walk(**kwargs):
628
+ if depth is not None and len(root.relative_to(self).parts) >= depth:
629
+ dirs[:] = []
630
+ elif exclude_dirs:
631
+ dirs[:] = [d for d in dirs if not exclude_dirs(root.joinpath(d))]
632
+
633
+ yield from (root.joinpath(file) for file in files)
634
+ else:
635
+ yield from super().iterdir()
636
+
478
637
 
479
638
  class Register7zFormat(Path, archive="7z"):
480
639
  """
@@ -491,20 +650,18 @@ class Register7zFormat(Path, archive="7z"):
491
650
 
492
651
  Example:
493
652
  ```python
494
- class Register7zArchive(pathlibutil.Path, archive='7z'):
653
+ class Register7zArchive(pathlibutil.Path, archive="7z"):
495
654
  @classmethod
496
655
  def _register_archive_format(cls):
497
656
  try:
498
657
  from py7zr import pack_7zarchive, unpack_7zarchive
499
658
  except ModuleNotFoundError:
500
- raise ModuleNotFoundError('pip install pathlibutil[7z]')
659
+ raise ModuleNotFoundError("pip install pathlibutil[7z]")
501
660
  else:
502
661
  shutil.register_archive_format(
503
- '7z', pack_7zarchive, description='7zip archive'
504
- )
505
- shutil.register_unpack_format(
506
- '7z', ['.7z'], unpack_7zarchive
662
+ "7z", pack_7zarchive, description="7zip archive"
507
663
  )
664
+ shutil.register_unpack_format("7z", [".7z"], unpack_7zarchive)
508
665
  ```
509
666
  """
510
667
 
@@ -2,7 +2,7 @@ import functools
2
2
  import os
3
3
  import re
4
4
  from datetime import datetime, tzinfo
5
- from typing import Set, Tuple, TypeVar, Iterable
5
+ from typing import Iterable, Set, Tuple, TypeVar
6
6
 
7
7
  _ByteInt = TypeVar("_ByteInt", bound="ByteInt")
8
8
  _stat_result = TypeVar("_stat_result", bound="os.stat_result")
@@ -206,6 +206,7 @@ def byteint(func):
206
206
  ```python
207
207
  randbyte = byteint(random.randint)
208
208
 
209
+
209
210
  @byteint
210
211
  def randhexbyte():
211
212
  return hex(random.randint(0, 2**32))
@@ -1,31 +1,38 @@
1
+ [build-system]
2
+ build-backend = "poetry.core.masonry.api"
3
+
4
+ requires = [
5
+ "poetry-core",
6
+ ]
7
+
1
8
  [tool.poetry]
2
9
  name = "pathlibutil"
3
- version = "v0.1.8"
10
+ version = "v0.2.0"
4
11
  description = "inherits from pathlib.Path with methods for hashing, copying, deleting and more"
5
- authors = ["Christoph Dörrer <d-chris@web.de>"]
12
+ authors = [ "Christoph Dörrer <d-chris@web.de>" ]
6
13
  readme = "README.md"
7
14
  license = "MIT"
8
15
  classifiers = [
9
- "Programming Language :: Python :: 3.8",
10
- "Programming Language :: Python :: 3.9",
11
- "Programming Language :: Python :: 3.10",
12
- "Programming Language :: Python :: 3.11",
13
- "Programming Language :: Python :: 3.12",
14
- "License :: OSI Approved :: MIT License",
15
- "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3.8",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
16
23
  ]
17
- keywords = ["pathlib", "hashlib", "shutil"]
24
+ keywords = [ "pathlib", "hashlib", "shutil" ]
18
25
  homepage = "https://d-chris.github.io"
19
26
  repository = "https://github.com/d-chris/pathlibutil"
20
27
  documentation = "https://d-chris.github.io/pathlibutil"
21
- include = ["LICENSE"]
28
+ include = [ "LICENSE" ]
22
29
 
23
30
  [tool.poetry.dependencies]
24
31
  python = ">=3.8.1,<3.13"
25
32
  py7zr = { version = "^0.20.2", optional = true }
26
33
 
27
34
  [tool.poetry.extras]
28
- 7z = ["py7zr"]
35
+ 7z = [ "py7zr" ]
29
36
 
30
37
  [tool.poetry.group.dev.dependencies]
31
38
  pytest = "^7.4.3"
@@ -44,6 +51,7 @@ docformatter = "^1.7.5"
44
51
  jinja2-pdoc = "^0.2.0"
45
52
  pdoc = "^14.3.0"
46
53
  click = "^8.1.7"
54
+ pyinstaller = "^6.5.0"
47
55
 
48
56
  [[tool.poetry.source]]
49
57
  name = "PyPI"
@@ -54,19 +62,15 @@ name = "testpypi"
54
62
  url = "https://test.pypi.org/legacy/"
55
63
  priority = "explicit"
56
64
 
57
- [build-system]
58
- requires = ["poetry-core"]
59
- build-backend = "poetry.core.masonry.api"
60
-
61
65
  [tool.pytest.ini_options]
62
66
  minversion = "6.0"
63
67
  testpaths = "tests"
64
68
  addopts = [
65
- "--random-order",
66
- "--color=yes",
67
- "-s",
68
- # "--cov=pathlibutil",
69
- # "--cov-report=term-missing:skip-covered",
70
- # "--cov-append",
71
- # "--cov-report=html",
69
+ "--random-order",
70
+ "--color=yes",
71
+ "-s",
72
+ # "--cov=pathlibutil",
73
+ # "--cov-report=term-missing:skip-covered",
74
+ # "--cov-append",
75
+ # "--cov-report=html",
72
76
  ]
File without changes