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.

Files changed (72) hide show
  1. absfuyu/__init__.py +1 -1
  2. absfuyu/__main__.py +3 -3
  3. absfuyu/cli/__init__.py +2 -2
  4. absfuyu/cli/color.py +30 -14
  5. absfuyu/cli/config_group.py +9 -2
  6. absfuyu/cli/do_group.py +13 -6
  7. absfuyu/cli/game_group.py +9 -2
  8. absfuyu/cli/tool_group.py +15 -9
  9. absfuyu/config/__init__.py +2 -2
  10. absfuyu/core/__init__.py +2 -2
  11. absfuyu/core/baseclass.py +448 -79
  12. absfuyu/core/baseclass2.py +2 -2
  13. absfuyu/core/decorator.py +70 -4
  14. absfuyu/core/docstring.py +43 -25
  15. absfuyu/core/dummy_cli.py +2 -2
  16. absfuyu/core/dummy_func.py +15 -4
  17. absfuyu/dxt/__init__.py +2 -2
  18. absfuyu/dxt/dictext.py +5 -2
  19. absfuyu/dxt/dxt_support.py +2 -2
  20. absfuyu/dxt/intext.py +34 -3
  21. absfuyu/dxt/listext.py +300 -113
  22. absfuyu/dxt/strext.py +75 -15
  23. absfuyu/extra/__init__.py +2 -2
  24. absfuyu/extra/beautiful.py +2 -2
  25. absfuyu/extra/da/__init__.py +36 -0
  26. absfuyu/extra/da/dadf.py +1177 -0
  27. absfuyu/extra/da/dadf_base.py +186 -0
  28. absfuyu/extra/da/df_func.py +97 -0
  29. absfuyu/extra/da/mplt.py +219 -0
  30. absfuyu/extra/data_analysis.py +10 -1067
  31. absfuyu/fun/__init__.py +2 -2
  32. absfuyu/fun/tarot.py +2 -2
  33. absfuyu/game/__init__.py +2 -2
  34. absfuyu/game/game_stat.py +2 -2
  35. absfuyu/game/sudoku.py +2 -2
  36. absfuyu/game/tictactoe.py +2 -3
  37. absfuyu/game/wordle.py +2 -2
  38. absfuyu/general/__init__.py +2 -2
  39. absfuyu/general/content.py +2 -2
  40. absfuyu/general/human.py +2 -2
  41. absfuyu/general/shape.py +2 -2
  42. absfuyu/logger.py +2 -2
  43. absfuyu/pkg_data/__init__.py +2 -2
  44. absfuyu/pkg_data/deprecated.py +2 -2
  45. absfuyu/sort.py +2 -2
  46. absfuyu/tools/__init__.py +28 -2
  47. absfuyu/tools/checksum.py +27 -7
  48. absfuyu/tools/converter.py +120 -34
  49. absfuyu/tools/generator.py +251 -110
  50. absfuyu/tools/inspector.py +463 -0
  51. absfuyu/tools/keygen.py +2 -2
  52. absfuyu/tools/obfuscator.py +45 -7
  53. absfuyu/tools/passwordlib.py +88 -24
  54. absfuyu/tools/shutdownizer.py +2 -2
  55. absfuyu/tools/web.py +2 -2
  56. absfuyu/typings.py +136 -0
  57. absfuyu/util/__init__.py +18 -4
  58. absfuyu/util/api.py +36 -16
  59. absfuyu/util/json_method.py +43 -14
  60. absfuyu/util/lunar.py +2 -2
  61. absfuyu/util/path.py +190 -82
  62. absfuyu/util/performance.py +122 -7
  63. absfuyu/util/shorten_number.py +40 -10
  64. absfuyu/util/text_table.py +306 -0
  65. absfuyu/util/zipped.py +8 -7
  66. absfuyu/version.py +2 -2
  67. {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/METADATA +9 -2
  68. absfuyu-5.2.0.dist-info/RECORD +76 -0
  69. absfuyu-5.0.1.dist-info/RECORD +0 -68
  70. {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/WHEEL +0 -0
  71. {absfuyu-5.0.1.dist-info → absfuyu-5.2.0.dist-info}/entry_points.txt +0 -0
  72. {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.0.0
7
- Date updated: 22/02/2025 (dd/mm/yyyy)
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 versionadded
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 | version 3.4.0: Remake Directory into modular class
78
+ # Class - Directory
69
79
  # ---------------------------------------------------------------------------
70
- class DirectoryBase:
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
- (Default: ``False``)
111
+ Create directory when not exist,
112
+ by default ``False``
85
113
  """
86
114
  self.source_path = Path(source_path)
87
- if create_if_not_exist:
88
- if not self.source_path.exists():
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
- # Everything
116
- @property
117
- @versionadded("3.3.0")
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
- @versionadded("3.3.0")
132
- def _every_file(self) -> list[Path]:
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
- # Quick information
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
- :rtype: DirectoryInfo
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 DirectoryBasicOperation(DirectoryBase):
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 shutil.Error as e: # File already exists
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
- # Zip
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, *, format: Literal["zip", "tar", "gztar", "bztar", "xztar"] = "zip"
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 Exception as e:
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
- class DirectoryTree(DirectoryBase):
358
- pass
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
- - list_structure
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
- """File as multiple file type"""
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
@@ -3,8 +3,8 @@ Absfuyu: Performance
3
3
  --------------------
4
4
  Performance Check
5
5
 
6
- Version: 5.0.0
7
- Date updated: 22/02/2025 (dd/mm/yyyy)
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
- # TODO: Rewrite this with inspect
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 ListNoDunder(self.item_to_check.__dir__())
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:
@@ -3,8 +3,8 @@ Absfuyu: Shorten number
3
3
  -----------------------
4
4
  Short number base on suffixes
5
5
 
6
- Version: 5.0.0
7
- Date updated: 24/02/2025 (dd/mm/yyyy)
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
- :param original_value: Value to shorten
160
- :param base: Short by base (must be > 0)
161
- :param suffixes: List of suffixes to use (ascending order)
162
- :param factory: ``UnitSuffixFactory`` to use (will overwrite ``base`` and ``suffixes``)
163
- :param suffix_full_name: Use suffix full name (default: False)
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
- :param decimal: Round up to which decimal
232
- :param separator: Character between value and suffix, default: ``" "``
233
- :param float_only: Returns value as <float> instead of <int> when ``decimal = 0``
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:,}"