absfuyu 5.0.1__py3-none-any.whl → 5.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of absfuyu might be problematic. Click here for more details.
- absfuyu/__init__.py +1 -1
- absfuyu/__main__.py +3 -3
- absfuyu/cli/__init__.py +2 -2
- absfuyu/cli/color.py +30 -14
- absfuyu/cli/config_group.py +9 -2
- absfuyu/cli/do_group.py +13 -6
- absfuyu/cli/game_group.py +9 -2
- absfuyu/cli/tool_group.py +15 -9
- absfuyu/config/__init__.py +2 -2
- absfuyu/core/__init__.py +2 -2
- absfuyu/core/baseclass.py +448 -79
- absfuyu/core/baseclass2.py +2 -2
- absfuyu/core/decorator.py +70 -4
- absfuyu/core/docstring.py +43 -25
- absfuyu/core/dummy_cli.py +2 -2
- absfuyu/core/dummy_func.py +15 -4
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/dictext.py +5 -2
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +34 -3
- absfuyu/dxt/listext.py +300 -113
- absfuyu/dxt/strext.py +75 -15
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/beautiful.py +2 -2
- absfuyu/extra/da/__init__.py +36 -0
- absfuyu/extra/da/dadf.py +1177 -0
- absfuyu/extra/da/dadf_base.py +186 -0
- absfuyu/extra/da/df_func.py +97 -0
- absfuyu/extra/da/mplt.py +219 -0
- absfuyu/extra/data_analysis.py +10 -1067
- absfuyu/fun/__init__.py +2 -2
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -3
- absfuyu/game/wordle.py +2 -2
- absfuyu/general/__init__.py +2 -2
- absfuyu/general/content.py +2 -2
- absfuyu/general/human.py +2 -2
- absfuyu/general/shape.py +2 -2
- absfuyu/logger.py +2 -2
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/sort.py +2 -2
- absfuyu/tools/__init__.py +28 -2
- absfuyu/tools/checksum.py +27 -7
- absfuyu/tools/converter.py +120 -34
- absfuyu/tools/generator.py +251 -110
- absfuyu/tools/inspector.py +463 -0
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +45 -7
- absfuyu/tools/passwordlib.py +88 -24
- absfuyu/tools/shutdownizer.py +2 -2
- absfuyu/tools/web.py +2 -2
- absfuyu/typings.py +136 -0
- absfuyu/util/__init__.py +18 -4
- absfuyu/util/api.py +36 -16
- absfuyu/util/json_method.py +43 -14
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/path.py +190 -82
- absfuyu/util/performance.py +122 -7
- absfuyu/util/shorten_number.py +40 -10
- absfuyu/util/text_table.py +306 -0
- absfuyu/util/zipped.py +8 -7
- absfuyu/version.py +2 -2
- {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/METADATA +9 -2
- absfuyu-5.2.0.dist-info/RECORD +76 -0
- absfuyu-5.0.1.dist-info/RECORD +0 -68
- {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/WHEEL +0 -0
- {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/licenses/LICENSE +0 -0
absfuyu/util/path.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Path
|
|
|
3
3
|
-------------
|
|
4
4
|
Path related
|
|
5
5
|
|
|
6
|
-
Version: 5.
|
|
7
|
-
Date updated:
|
|
6
|
+
Version: 5.2.0
|
|
7
|
+
Date updated: 14/03/2025 (dd/mm/yyyy)
|
|
8
8
|
|
|
9
9
|
Feature:
|
|
10
10
|
--------
|
|
@@ -16,8 +16,15 @@ Feature:
|
|
|
16
16
|
# ---------------------------------------------------------------------------
|
|
17
17
|
__all__ = [
|
|
18
18
|
# Main
|
|
19
|
+
"DirectoryBase",
|
|
19
20
|
"Directory",
|
|
20
21
|
"SaveFileAs",
|
|
22
|
+
# Mixin
|
|
23
|
+
"DirectoryInfoMixin",
|
|
24
|
+
"DirectoryBasicOperationMixin",
|
|
25
|
+
"DirectoryArchiverMixin",
|
|
26
|
+
"DirectoryOrganizerMixin",
|
|
27
|
+
"DirectoryTreeMixin",
|
|
21
28
|
# Support
|
|
22
29
|
"FileOrFolderWithModificationTime",
|
|
23
30
|
"DirectoryInfo",
|
|
@@ -32,9 +39,11 @@ import shutil
|
|
|
32
39
|
from datetime import datetime
|
|
33
40
|
from functools import partial
|
|
34
41
|
from pathlib import Path
|
|
35
|
-
from typing import Any, Literal, NamedTuple
|
|
42
|
+
from typing import Any, ClassVar, Literal, NamedTuple
|
|
36
43
|
|
|
37
|
-
from absfuyu.core import
|
|
44
|
+
from absfuyu.core.baseclass import BaseClass
|
|
45
|
+
from absfuyu.core.decorator import add_subclass_methods_decorator
|
|
46
|
+
from absfuyu.core.docstring import deprecated, versionadded, versionchanged
|
|
38
47
|
from absfuyu.logger import logger
|
|
39
48
|
|
|
40
49
|
|
|
@@ -53,21 +62,40 @@ class FileOrFolderWithModificationTime(NamedTuple):
|
|
|
53
62
|
modification_time: datetime
|
|
54
63
|
|
|
55
64
|
|
|
65
|
+
@deprecated(
|
|
66
|
+
"5.1.0", reason="Support for ``DirectoryInfoMixin`` which is also deprecated"
|
|
67
|
+
)
|
|
56
68
|
@versionadded("3.3.0")
|
|
57
69
|
class DirectoryInfo(NamedTuple):
|
|
58
70
|
"""
|
|
59
71
|
Information of a directory
|
|
60
72
|
"""
|
|
61
73
|
|
|
62
|
-
files: int
|
|
63
|
-
folders: int
|
|
64
74
|
creation_time: datetime
|
|
65
75
|
modification_time: datetime
|
|
66
76
|
|
|
67
77
|
|
|
68
|
-
# Class - Directory
|
|
78
|
+
# Class - Directory
|
|
69
79
|
# ---------------------------------------------------------------------------
|
|
70
|
-
|
|
80
|
+
@add_subclass_methods_decorator
|
|
81
|
+
class DirectoryBase(BaseClass):
|
|
82
|
+
"""
|
|
83
|
+
Directory - Base
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
source_path : str | Path
|
|
88
|
+
Source folder
|
|
89
|
+
|
|
90
|
+
create_if_not_exist : bool
|
|
91
|
+
Create directory when not exist,
|
|
92
|
+
by default ``False``
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Custom attribute
|
|
96
|
+
_METHOD_INCLUDE: ClassVar[bool] = True # Include in DIR_METHODS
|
|
97
|
+
SUBCLASS_METHODS: ClassVar[dict[str, list[str]]] = {}
|
|
98
|
+
|
|
71
99
|
def __init__(
|
|
72
100
|
self,
|
|
73
101
|
source_path: str | Path,
|
|
@@ -80,80 +108,53 @@ class DirectoryBase:
|
|
|
80
108
|
Source folder
|
|
81
109
|
|
|
82
110
|
create_if_not_exist : bool
|
|
83
|
-
Create directory when not exist
|
|
84
|
-
|
|
111
|
+
Create directory when not exist,
|
|
112
|
+
by default ``False``
|
|
85
113
|
"""
|
|
86
114
|
self.source_path = Path(source_path)
|
|
87
|
-
if
|
|
88
|
-
if
|
|
115
|
+
if not self.source_path.exists():
|
|
116
|
+
if create_if_not_exist:
|
|
89
117
|
self.source_path.mkdir(exist_ok=True, parents=True)
|
|
118
|
+
else:
|
|
119
|
+
raise FileNotFoundError("Directory not existed")
|
|
90
120
|
|
|
91
|
-
def __str__(self) -> str:
|
|
92
|
-
return self.source_path.__str__()
|
|
93
|
-
|
|
94
|
-
def __repr__(self) -> str:
|
|
95
|
-
return f"{self.__class__.__name__}({self.source_path})"
|
|
96
|
-
|
|
97
|
-
def __format__(self, __format_spec: str) -> str:
|
|
98
|
-
"""
|
|
99
|
-
Change format of an object.
|
|
100
|
-
Avaiable option: ``info``
|
|
101
|
-
|
|
102
|
-
Usage
|
|
103
|
-
-----
|
|
104
|
-
>>> print(f"{<object>:<format_spec>}")
|
|
105
|
-
>>> print(<object>.__format__(<format_spec>))
|
|
106
|
-
>>> print(format(<object>, <format_spec>))
|
|
107
|
-
"""
|
|
108
|
-
# Show quick info
|
|
109
|
-
if __format_spec.lower().startswith("info"):
|
|
110
|
-
return self.quick_info().__repr__()
|
|
111
|
-
|
|
112
|
-
# No format spec
|
|
113
|
-
return self.__repr__()
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def everything(self) -> list[Path]:
|
|
119
|
-
"""
|
|
120
|
-
Every folders and files in this Directory
|
|
121
|
-
"""
|
|
122
|
-
return list(x for x in self.source_path.glob("**/*"))
|
|
123
|
-
|
|
124
|
-
@versionadded("3.3.0")
|
|
125
|
-
def _every_folder(self) -> list[Path]:
|
|
126
|
-
"""
|
|
127
|
-
Every folders in this Directory
|
|
128
|
-
"""
|
|
129
|
-
return list(x for x in self.source_path.glob("**/*") if x.is_dir())
|
|
122
|
+
class DirectoryInfoMixin(DirectoryBase):
|
|
123
|
+
"""
|
|
124
|
+
Directory - Info
|
|
130
125
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"""
|
|
134
|
-
Every folders in this Directory
|
|
135
|
-
"""
|
|
136
|
-
return list(x for x in self.source_path.glob("**/*") if x.is_file())
|
|
126
|
+
- Quick info
|
|
127
|
+
"""
|
|
137
128
|
|
|
138
|
-
|
|
129
|
+
@deprecated("5.1.0", reason="Not efficient")
|
|
139
130
|
@versionadded("3.3.0")
|
|
140
131
|
def quick_info(self) -> DirectoryInfo:
|
|
141
132
|
"""
|
|
142
133
|
Quick information about this Directory
|
|
143
134
|
|
|
144
|
-
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
DirectoryInfo
|
|
138
|
+
DirectoryInfo
|
|
145
139
|
"""
|
|
146
140
|
source_stat: os.stat_result = self.source_path.stat()
|
|
147
141
|
out = DirectoryInfo(
|
|
148
|
-
files=len(self._every_file()),
|
|
149
|
-
folders=len(self._every_folder()),
|
|
150
142
|
creation_time=datetime.fromtimestamp(source_stat.st_ctime),
|
|
151
143
|
modification_time=datetime.fromtimestamp(source_stat.st_mtime),
|
|
152
144
|
)
|
|
153
145
|
return out
|
|
154
146
|
|
|
155
147
|
|
|
156
|
-
class
|
|
148
|
+
class DirectoryBasicOperationMixin(DirectoryBase):
|
|
149
|
+
"""
|
|
150
|
+
Directory - Basic operation
|
|
151
|
+
|
|
152
|
+
- Rename
|
|
153
|
+
- Copy
|
|
154
|
+
- Move
|
|
155
|
+
- Delete
|
|
156
|
+
"""
|
|
157
|
+
|
|
157
158
|
# Rename
|
|
158
159
|
def rename(self, new_name: str) -> None:
|
|
159
160
|
"""
|
|
@@ -214,7 +215,7 @@ class DirectoryBasicOperation(DirectoryBase):
|
|
|
214
215
|
shutil.move(self.source_path, Path(dst))
|
|
215
216
|
logger.debug(f"Moving to {dst}...DONE")
|
|
216
217
|
|
|
217
|
-
except
|
|
218
|
+
except OSError as e: # File already exists
|
|
218
219
|
logger.error(e)
|
|
219
220
|
logger.debug("Overwriting file...")
|
|
220
221
|
if content_only:
|
|
@@ -316,31 +317,54 @@ class DirectoryBasicOperation(DirectoryBase):
|
|
|
316
317
|
except Exception as e:
|
|
317
318
|
logger.error(f"Removing {self.source_path}...FAILED\n{e}")
|
|
318
319
|
|
|
319
|
-
|
|
320
|
+
|
|
321
|
+
class DirectoryArchiverMixin(DirectoryBase):
|
|
322
|
+
"""
|
|
323
|
+
Directory - Archiver/Compress
|
|
324
|
+
|
|
325
|
+
- Compress
|
|
326
|
+
- Decompress
|
|
327
|
+
- Register extra zip format <staticmethod>
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
@versionchanged("5.1.0", reason="Update funcionality (new parameter)")
|
|
320
331
|
def compress(
|
|
321
|
-
self,
|
|
332
|
+
self,
|
|
333
|
+
format: Literal["zip", "tar", "gztar", "bztar", "xztar"] = "zip",
|
|
334
|
+
delete_after_compress: bool = False,
|
|
335
|
+
move_inside: bool = True,
|
|
322
336
|
) -> Path | None:
|
|
323
337
|
"""
|
|
324
338
|
Compress the directory (Default: Create ``.zip`` file)
|
|
325
339
|
|
|
326
340
|
Parameters
|
|
327
341
|
----------
|
|
328
|
-
format : Literal["zip", "tar", "gztar", "bztar", "xztar"]
|
|
342
|
+
format : Literal["zip", "tar", "gztar", "bztar", "xztar"], optional
|
|
343
|
+
By default ``"zip"``
|
|
329
344
|
- ``zip``: ZIP file (if the ``zlib`` module is available).
|
|
330
345
|
- ``tar``: Uncompressed tar file. Uses POSIX.1-2001 pax format for new archives.
|
|
331
346
|
- ``gztar``: gzip'ed tar-file (if the ``zlib`` module is available).
|
|
332
347
|
- ``bztar``: bzip2'ed tar-file (if the ``bz2`` module is available).
|
|
333
348
|
- ``xztar``: xz'ed tar-file (if the ``lzma`` module is available).
|
|
334
349
|
|
|
350
|
+
delete_after_compress : bool, optional
|
|
351
|
+
Delete directory after compress, by default ``False``
|
|
352
|
+
|
|
353
|
+
move_inside : bool, optional
|
|
354
|
+
Move the commpressed file inside the directory,
|
|
355
|
+
by default ``True``
|
|
356
|
+
|
|
335
357
|
Returns
|
|
336
358
|
-------
|
|
337
359
|
Path
|
|
338
360
|
Compressed path
|
|
361
|
+
|
|
339
362
|
None
|
|
340
363
|
When fail to compress
|
|
341
364
|
"""
|
|
342
365
|
logger.debug(f"Zipping {self.source_path}...")
|
|
343
366
|
try:
|
|
367
|
+
# Zip
|
|
344
368
|
# zip_name = self.source_path.parent.joinpath(self.source_path.name).__str__()
|
|
345
369
|
# shutil.make_archive(zip_name, format=format, root_dir=self.source_path)
|
|
346
370
|
zip_path = shutil.make_archive(
|
|
@@ -348,26 +372,85 @@ class DirectoryBasicOperation(DirectoryBase):
|
|
|
348
372
|
)
|
|
349
373
|
logger.debug(f"Zipping {self.source_path}...DONE")
|
|
350
374
|
logger.debug(f"Path: {zip_path}")
|
|
375
|
+
|
|
376
|
+
# Del
|
|
377
|
+
if delete_after_compress:
|
|
378
|
+
move_inside = False
|
|
379
|
+
shutil.rmtree(self.source_path)
|
|
380
|
+
|
|
381
|
+
# Move
|
|
382
|
+
if move_inside:
|
|
383
|
+
zf = Path(zip_path)
|
|
384
|
+
_move_path = self.source_path.joinpath(zf.name)
|
|
385
|
+
if _move_path.exists():
|
|
386
|
+
_move_path.unlink(missing_ok=True)
|
|
387
|
+
_move = zf.rename(_move_path)
|
|
388
|
+
return _move
|
|
389
|
+
|
|
351
390
|
return Path(zip_path)
|
|
352
|
-
except
|
|
391
|
+
except (FileExistsError, OSError) as e:
|
|
353
392
|
logger.error(f"Zipping {self.source_path}...FAILED\n{e}")
|
|
354
393
|
return None
|
|
355
394
|
|
|
395
|
+
@staticmethod
|
|
396
|
+
@versionadded("5.1.0")
|
|
397
|
+
def register_extra_zip_format() -> None:
|
|
398
|
+
"""This register extra extension for zipfile"""
|
|
399
|
+
extra_extension = [".zip", ".cbz"]
|
|
400
|
+
shutil.unregister_unpack_format("zip")
|
|
401
|
+
shutil.register_unpack_format(
|
|
402
|
+
"zip",
|
|
403
|
+
extra_extension,
|
|
404
|
+
shutil._unpack_zipfile, # type: ignore
|
|
405
|
+
description="ZIP file",
|
|
406
|
+
)
|
|
356
407
|
|
|
357
|
-
|
|
358
|
-
|
|
408
|
+
@versionadded("5.1.0")
|
|
409
|
+
def decompress(
|
|
410
|
+
self,
|
|
411
|
+
format: Literal["zip", "tar", "gztar", "bztar", "xztar"] | None = None,
|
|
412
|
+
delete_after_done: bool = False,
|
|
413
|
+
) -> None:
|
|
414
|
+
"""
|
|
415
|
+
Decompress compressed file in directory (first level only)
|
|
416
|
+
|
|
417
|
+
Parameters
|
|
418
|
+
----------
|
|
419
|
+
format : Literal["zip", "tar", "gztar", "bztar", "xztar"] | None, optional
|
|
420
|
+
By default ``None``
|
|
421
|
+
- ``zip``: ZIP file (if the ``zlib`` module is available).
|
|
422
|
+
- ``tar``: Uncompressed tar file. Uses POSIX.1-2001 pax format for new archives.
|
|
423
|
+
- ``gztar``: gzip'ed tar-file (if the ``zlib`` module is available).
|
|
424
|
+
- ``bztar``: bzip2'ed tar-file (if the ``bz2`` module is available).
|
|
425
|
+
- ``xztar``: xz'ed tar-file (if the ``lzma`` module is available).
|
|
359
426
|
|
|
427
|
+
delete_after_done : bool, optional
|
|
428
|
+
Delete compressed file when extracted, by default ``False``
|
|
429
|
+
"""
|
|
430
|
+
# Register extra extension
|
|
431
|
+
self.register_extra_zip_format()
|
|
432
|
+
|
|
433
|
+
# Decompress first level only
|
|
434
|
+
for path in self.source_path.glob("*"):
|
|
435
|
+
try:
|
|
436
|
+
shutil.unpack_archive(
|
|
437
|
+
path, path.parent.joinpath(path.stem), format=format
|
|
438
|
+
)
|
|
439
|
+
if delete_after_done and path.is_file():
|
|
440
|
+
path.unlink(missing_ok=True)
|
|
441
|
+
except OSError:
|
|
442
|
+
continue
|
|
360
443
|
|
|
361
|
-
class Directory(DirectoryBasicOperation, DirectoryTree):
|
|
362
|
-
"""
|
|
363
|
-
Some shortcuts for directory
|
|
364
444
|
|
|
365
|
-
|
|
366
|
-
- delete, rename, copy, move
|
|
367
|
-
- zip
|
|
368
|
-
- quick_info
|
|
445
|
+
class DirectoryOrganizerMixin(DirectoryBase):
|
|
369
446
|
"""
|
|
447
|
+
Directory - File organizer - SOON
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
pass
|
|
370
451
|
|
|
452
|
+
|
|
453
|
+
class DirectoryTreeMixin(DirectoryBase):
|
|
371
454
|
# Directory structure
|
|
372
455
|
def _list_dir(self, *ignore: str) -> list[Path]:
|
|
373
456
|
"""
|
|
@@ -504,10 +587,41 @@ class Directory(DirectoryBasicOperation, DirectoryTree):
|
|
|
504
587
|
return self.list_structure("__pycache__", ".pyc")
|
|
505
588
|
|
|
506
589
|
|
|
590
|
+
class Directory(
|
|
591
|
+
DirectoryTreeMixin,
|
|
592
|
+
DirectoryOrganizerMixin,
|
|
593
|
+
DirectoryArchiverMixin,
|
|
594
|
+
DirectoryBasicOperationMixin,
|
|
595
|
+
DirectoryInfoMixin,
|
|
596
|
+
):
|
|
597
|
+
"""
|
|
598
|
+
Some shortcuts for directory
|
|
599
|
+
|
|
600
|
+
Parameters
|
|
601
|
+
----------
|
|
602
|
+
source_path : str | Path
|
|
603
|
+
Source folder
|
|
604
|
+
|
|
605
|
+
create_if_not_exist : bool
|
|
606
|
+
Create directory when not exist,
|
|
607
|
+
by default ``False``
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
Example:
|
|
611
|
+
--------
|
|
612
|
+
>>> # For a list of method
|
|
613
|
+
>>> Directory.SUBCLASS_METHODS
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
|
|
507
619
|
# Class - SaveFileAs
|
|
508
620
|
# ---------------------------------------------------------------------------
|
|
509
621
|
class SaveFileAs:
|
|
510
|
-
"""
|
|
622
|
+
"""
|
|
623
|
+
File as multiple file type
|
|
624
|
+
"""
|
|
511
625
|
|
|
512
626
|
def __init__(self, data: Any, *, encoding: str | None = "utf-8") -> None:
|
|
513
627
|
"""
|
|
@@ -552,9 +666,3 @@ class SaveFileAs:
|
|
|
552
666
|
# from absfuyu.util.json_method import JsonFile
|
|
553
667
|
# temp = JsonFile(path, sort_keys=False)
|
|
554
668
|
# temp.save_json()
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
# Dev and Test new feature before get added to the main class
|
|
558
|
-
# ---------------------------------------------------------------------------
|
|
559
|
-
class _NewDirFeature(Directory):
|
|
560
|
-
pass
|
absfuyu/util/performance.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Performance
|
|
|
3
3
|
--------------------
|
|
4
4
|
Performance Check
|
|
5
5
|
|
|
6
|
-
Version: 5.
|
|
7
|
-
Date updated:
|
|
6
|
+
Version: 5.2.0
|
|
7
|
+
Date updated: 15/03/2025 (dd/mm/yyyy)
|
|
8
8
|
|
|
9
9
|
Feature:
|
|
10
10
|
--------
|
|
@@ -20,6 +20,7 @@ Feature:
|
|
|
20
20
|
__all__ = [
|
|
21
21
|
# Wrapper
|
|
22
22
|
"function_debug",
|
|
23
|
+
"function_benchmark",
|
|
23
24
|
"measure_performance",
|
|
24
25
|
"retry",
|
|
25
26
|
# Class
|
|
@@ -32,12 +33,12 @@ __all__ = [
|
|
|
32
33
|
import time
|
|
33
34
|
import tracemalloc
|
|
34
35
|
from collections.abc import Callable
|
|
36
|
+
from dataclasses import dataclass
|
|
35
37
|
from functools import wraps
|
|
36
38
|
from inspect import getsource
|
|
37
|
-
from typing import Any, ParamSpec, TypeVar
|
|
39
|
+
from typing import Any, Literal, ParamSpec, TypeVar, overload
|
|
38
40
|
|
|
39
|
-
from absfuyu.core import versionadded, versionchanged
|
|
40
|
-
from absfuyu.dxt import ListNoDunder
|
|
41
|
+
from absfuyu.core import deprecated, versionadded, versionchanged
|
|
41
42
|
|
|
42
43
|
# Type
|
|
43
44
|
# ---------------------------------------------------------------------------
|
|
@@ -45,6 +46,30 @@ P = ParamSpec("P") # Parameter type
|
|
|
45
46
|
R = TypeVar("R") # Return type
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
# Support
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
@dataclass
|
|
52
|
+
class BenchmarkResult:
|
|
53
|
+
"""
|
|
54
|
+
Use ``format(BenchmarkResult(...), "seconds")`` to view result in seconds.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
min_: float
|
|
58
|
+
max_: float
|
|
59
|
+
avg: float
|
|
60
|
+
|
|
61
|
+
def __format__(self, format_spec: str) -> str:
|
|
62
|
+
clsname = self.__class__.__name__
|
|
63
|
+
if format_spec.lower().strip().startswith("seconds"):
|
|
64
|
+
fields = [f"{x}={getattr(self, x):,.6f}s" for x in self._get_fields()]
|
|
65
|
+
return f"{clsname}({', '.join(fields)})"
|
|
66
|
+
return repr(self)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _get_fields(cls) -> tuple[str, ...]:
|
|
70
|
+
return tuple(cls.__dataclass_fields__)
|
|
71
|
+
|
|
72
|
+
|
|
48
73
|
# Function
|
|
49
74
|
# ---------------------------------------------------------------------------
|
|
50
75
|
@versionchanged("3.2.0", reason="Updated functionality")
|
|
@@ -108,6 +133,96 @@ def measure_performance(f: Callable[P, R]) -> Callable[P, R]:
|
|
|
108
133
|
return wrapper
|
|
109
134
|
|
|
110
135
|
|
|
136
|
+
@overload
|
|
137
|
+
def function_benchmark(func: Callable[P, R], /) -> Callable[P, R]: ...
|
|
138
|
+
@overload
|
|
139
|
+
def function_benchmark(*, n: int = 1) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
140
|
+
@overload
|
|
141
|
+
def function_benchmark(
|
|
142
|
+
*, n: int = 1, result_only: Literal[False] = False
|
|
143
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
144
|
+
@overload
|
|
145
|
+
def function_benchmark(
|
|
146
|
+
*, n: int = 1, result_only: Literal[True] = ...
|
|
147
|
+
) -> Callable[[Callable[P, R]], Callable[P, BenchmarkResult]]: ...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@versionadded("5.2.0")
|
|
151
|
+
def function_benchmark(
|
|
152
|
+
func: Callable[P, R] | None = None, /, *, n: int = 1, result_only: bool = False
|
|
153
|
+
):
|
|
154
|
+
"""
|
|
155
|
+
This run function for ``n`` times and calculate min, max, average runtime.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
func : Callable[P, R] | None, optional
|
|
160
|
+
Callable with parameter **P and returns R, by default ``None``
|
|
161
|
+
|
|
162
|
+
n : int, optional
|
|
163
|
+
Run how many times, by default ``1``
|
|
164
|
+
|
|
165
|
+
result_only : bool, optional
|
|
166
|
+
Returns BenchmarkResult instead of ``func`` result, by default ``False``
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
Usage
|
|
170
|
+
-----
|
|
171
|
+
Use this as a decorator (``@function_benchmark``)
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
--------
|
|
175
|
+
>>> @function_benchmark
|
|
176
|
+
>>> def test():
|
|
177
|
+
... return 1 + 1
|
|
178
|
+
>>> test()
|
|
179
|
+
BenchmarkResult(min_=0.000000s, max_=0.000000s, avg=0.000000s)
|
|
180
|
+
2
|
|
181
|
+
|
|
182
|
+
>>> @function_benchmark(n=1)
|
|
183
|
+
>>> def test():
|
|
184
|
+
... return 1 + 1
|
|
185
|
+
>>> test()
|
|
186
|
+
BenchmarkResult(min_=0.000000s, max_=0.000000s, avg=0.000000s)
|
|
187
|
+
2
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
times = max(n, 1)
|
|
191
|
+
|
|
192
|
+
def decorator(f: Callable[P, R]) -> Callable[P, R | BenchmarkResult]:
|
|
193
|
+
@wraps(f)
|
|
194
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | BenchmarkResult:
|
|
195
|
+
output = f(*args, **kwargs) # Run function and save result into a variable
|
|
196
|
+
|
|
197
|
+
def _run() -> float:
|
|
198
|
+
# Performance check
|
|
199
|
+
start_time = time.perf_counter() # Start time measure
|
|
200
|
+
f(*args, **kwargs)
|
|
201
|
+
finish_time = time.perf_counter() # Get finished time
|
|
202
|
+
return finish_time - start_time
|
|
203
|
+
|
|
204
|
+
# run = (_run() for _ in range(times))
|
|
205
|
+
run = [_run() for _ in range(times)]
|
|
206
|
+
try:
|
|
207
|
+
avg_runtime = sum(run) / len(run)
|
|
208
|
+
except ZeroDivisionError:
|
|
209
|
+
avg_runtime = min(run)
|
|
210
|
+
result = BenchmarkResult(min(run), max(run), avg_runtime)
|
|
211
|
+
|
|
212
|
+
if result_only:
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
print(format(result, "seconds"))
|
|
216
|
+
|
|
217
|
+
return output
|
|
218
|
+
|
|
219
|
+
return wrapper
|
|
220
|
+
|
|
221
|
+
if func is None:
|
|
222
|
+
return decorator
|
|
223
|
+
return decorator(func)
|
|
224
|
+
|
|
225
|
+
|
|
111
226
|
@versionadded("3.2.0")
|
|
112
227
|
def function_debug(f: Callable[P, R]) -> Callable[P, R]:
|
|
113
228
|
"""
|
|
@@ -225,7 +340,7 @@ def retry(retries: int, delay: float = 1):
|
|
|
225
340
|
|
|
226
341
|
# Class
|
|
227
342
|
# ---------------------------------------------------------------------------
|
|
228
|
-
|
|
343
|
+
@deprecated("5.1.0", reason="Use `absfuyu.tools.inspector` instead")
|
|
229
344
|
class Checker:
|
|
230
345
|
"""
|
|
231
346
|
Check a variable
|
|
@@ -284,7 +399,7 @@ class Checker:
|
|
|
284
399
|
def dir_(self) -> list[str]:
|
|
285
400
|
"""``dir()`` of variable"""
|
|
286
401
|
# return self.item_to_check.__dir__()
|
|
287
|
-
return
|
|
402
|
+
return [x for x in dir(self.item_to_check) if not x.startswith("__")]
|
|
288
403
|
|
|
289
404
|
@property
|
|
290
405
|
def source(self) -> str | None:
|
absfuyu/util/shorten_number.py
CHANGED
|
@@ -3,8 +3,8 @@ Absfuyu: Shorten number
|
|
|
3
3
|
-----------------------
|
|
4
4
|
Short number base on suffixes
|
|
5
5
|
|
|
6
|
-
Version: 5.
|
|
7
|
-
Date updated:
|
|
6
|
+
Version: 5.2.0
|
|
7
|
+
Date updated: 10/03/2025 (dd/mm/yyyy)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Module level
|
|
@@ -156,11 +156,28 @@ class Decimal:
|
|
|
156
156
|
"""
|
|
157
157
|
Shorten large number
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
original_value : int | float
|
|
162
|
+
Value to shorten
|
|
163
|
+
|
|
164
|
+
base : int
|
|
165
|
+
Short by base (must be > 0)
|
|
166
|
+
|
|
167
|
+
suffixes : list[str]
|
|
168
|
+
List of suffixes to use (ascending order)
|
|
169
|
+
|
|
170
|
+
factory : UnitSuffixFactory | None
|
|
171
|
+
``UnitSuffixFactory`` to use
|
|
172
|
+
(will overwrite ``base`` and ``suffixes``)
|
|
173
|
+
|
|
174
|
+
suffix_full_name : bool
|
|
175
|
+
Use suffix full name (available with ``UnitSuffixFactory``), by default ``False``
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
Decimal
|
|
180
|
+
Decimal instance
|
|
164
181
|
"""
|
|
165
182
|
|
|
166
183
|
original_value: int | float = field(repr=False)
|
|
@@ -173,6 +190,7 @@ class Decimal:
|
|
|
173
190
|
suffix: str = field(init=False)
|
|
174
191
|
|
|
175
192
|
def __post_init__(self) -> None:
|
|
193
|
+
self.base = max(1, self.base) # Make sure that base >= 1
|
|
176
194
|
self._get_factory()
|
|
177
195
|
self.value, self.suffix = self._convert_decimal()
|
|
178
196
|
|
|
@@ -228,9 +246,21 @@ class Decimal:
|
|
|
228
246
|
"""
|
|
229
247
|
Convert to string
|
|
230
248
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
decimal : int, optional
|
|
252
|
+
Round up to which decimal, by default ``2``
|
|
253
|
+
|
|
254
|
+
separator : str, optional
|
|
255
|
+
Character between value and suffix, by default ``" "``
|
|
256
|
+
|
|
257
|
+
float_only : bool, optional
|
|
258
|
+
Returns value as <float> instead of <int> when ``decimal = 0``, by default ``True``
|
|
259
|
+
|
|
260
|
+
Returns
|
|
261
|
+
-------
|
|
262
|
+
str
|
|
263
|
+
Decimal string
|
|
234
264
|
"""
|
|
235
265
|
val = self.value.__round__(decimal)
|
|
236
266
|
formatted_value = f"{val:,}"
|