absfuyu 4.1.1__py3-none-any.whl → 5.0.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 (76) hide show
  1. absfuyu/__init__.py +4 -4
  2. absfuyu/__main__.py +13 -1
  3. absfuyu/cli/__init__.py +4 -2
  4. absfuyu/cli/color.py +7 -0
  5. absfuyu/cli/do_group.py +9 -91
  6. absfuyu/cli/tool_group.py +136 -0
  7. absfuyu/config/__init__.py +17 -34
  8. absfuyu/core/__init__.py +49 -0
  9. absfuyu/core/baseclass.py +299 -0
  10. absfuyu/core/baseclass2.py +165 -0
  11. absfuyu/core/decorator.py +67 -0
  12. absfuyu/core/docstring.py +163 -0
  13. absfuyu/core/dummy_cli.py +67 -0
  14. absfuyu/core/dummy_func.py +47 -0
  15. absfuyu/dxt/__init__.py +42 -0
  16. absfuyu/dxt/dictext.py +201 -0
  17. absfuyu/dxt/dxt_support.py +79 -0
  18. absfuyu/dxt/intext.py +586 -0
  19. absfuyu/dxt/listext.py +508 -0
  20. absfuyu/dxt/strext.py +530 -0
  21. absfuyu/{extensions → extra}/__init__.py +3 -2
  22. absfuyu/extra/beautiful.py +251 -0
  23. absfuyu/{extensions/extra → extra}/data_analysis.py +51 -82
  24. absfuyu/fun/__init__.py +110 -135
  25. absfuyu/fun/tarot.py +9 -17
  26. absfuyu/game/__init__.py +6 -0
  27. absfuyu/game/game_stat.py +6 -0
  28. absfuyu/game/sudoku.py +7 -1
  29. absfuyu/game/tictactoe.py +12 -5
  30. absfuyu/game/wordle.py +14 -8
  31. absfuyu/general/__init__.py +6 -79
  32. absfuyu/general/content.py +22 -36
  33. absfuyu/general/generator.py +17 -42
  34. absfuyu/general/human.py +108 -228
  35. absfuyu/general/shape.py +1334 -0
  36. absfuyu/logger.py +8 -13
  37. absfuyu/pkg_data/__init__.py +137 -99
  38. absfuyu/pkg_data/deprecated.py +133 -0
  39. absfuyu/pkg_data/passwordlib_lzma.pkl +0 -0
  40. absfuyu/sort.py +6 -130
  41. absfuyu/tools/__init__.py +2 -2
  42. absfuyu/tools/checksum.py +44 -22
  43. absfuyu/tools/converter.py +82 -50
  44. absfuyu/tools/keygen.py +25 -30
  45. absfuyu/tools/obfuscator.py +246 -112
  46. absfuyu/tools/passwordlib.py +330 -0
  47. absfuyu/tools/shutdownizer.py +287 -0
  48. absfuyu/tools/web.py +2 -9
  49. absfuyu/util/__init__.py +15 -15
  50. absfuyu/util/api.py +10 -15
  51. absfuyu/util/json_method.py +7 -24
  52. absfuyu/util/lunar.py +3 -9
  53. absfuyu/util/path.py +22 -27
  54. absfuyu/util/performance.py +43 -67
  55. absfuyu/util/shorten_number.py +65 -14
  56. absfuyu/util/zipped.py +9 -15
  57. absfuyu-5.0.0.dist-info/METADATA +143 -0
  58. absfuyu-5.0.0.dist-info/RECORD +68 -0
  59. absfuyu/core.py +0 -57
  60. absfuyu/everything.py +0 -32
  61. absfuyu/extensions/beautiful.py +0 -188
  62. absfuyu/extensions/dev/__init__.py +0 -244
  63. absfuyu/extensions/dev/password_hash.py +0 -80
  64. absfuyu/extensions/dev/passwordlib.py +0 -258
  65. absfuyu/extensions/dev/project_starter.py +0 -60
  66. absfuyu/extensions/dev/shutdownizer.py +0 -156
  67. absfuyu/extensions/extra/__init__.py +0 -24
  68. absfuyu/fun/WGS.py +0 -134
  69. absfuyu/general/data_extension.py +0 -1796
  70. absfuyu/tools/stats.py +0 -226
  71. absfuyu/util/pkl.py +0 -67
  72. absfuyu-4.1.1.dist-info/METADATA +0 -121
  73. absfuyu-4.1.1.dist-info/RECORD +0 -61
  74. {absfuyu-4.1.1.dist-info → absfuyu-5.0.0.dist-info}/WHEEL +0 -0
  75. {absfuyu-4.1.1.dist-info → absfuyu-5.0.0.dist-info}/entry_points.txt +0 -0
  76. {absfuyu-4.1.1.dist-info → absfuyu-5.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,299 @@
1
+ """
2
+ Absfuyu: Core
3
+ -------------
4
+ Bases for other features
5
+
6
+ Version: 5.0.0
7
+ Date updated: 12/02/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module Package
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = [
13
+ # Color
14
+ "CLITextColor",
15
+ # Mixins
16
+ "ShowAllMethodsMixin",
17
+ "AutoREPRMixin",
18
+ # Class
19
+ "BaseClass",
20
+ # Metaclass
21
+ "PositiveInitArgsMeta",
22
+ ]
23
+
24
+
25
+ # Color
26
+ # ---------------------------------------------------------------------------
27
+ class CLITextColor:
28
+ """Color code for text in terminal"""
29
+
30
+ WHITE = "\x1b[37m"
31
+ BLACK = "\x1b[30m"
32
+ BLUE = "\x1b[34m"
33
+ GRAY = "\x1b[90m"
34
+ GREEN = "\x1b[32m"
35
+ RED = "\x1b[91m"
36
+ DARK_RED = "\x1b[31m"
37
+ MAGENTA = "\x1b[35m"
38
+ YELLOW = "\x1b[33m"
39
+ RESET = "\x1b[39m"
40
+
41
+
42
+ # Mixins
43
+ # ---------------------------------------------------------------------------
44
+ class ShowAllMethodsMixin:
45
+ """
46
+ Show all methods of the class and its parent class minus ``object`` class
47
+
48
+ *This class is meant to be used with other class*
49
+ """
50
+
51
+ @classmethod
52
+ def show_all_methods(
53
+ cls,
54
+ include_classmethod: bool = True,
55
+ classmethod_indicator: str = "<classmethod>",
56
+ include_staticmethod: bool = True,
57
+ staticmethod_indicator: str = "<staticmethod>",
58
+ include_private_method: bool = False,
59
+ print_result: bool = False,
60
+ ) -> dict[str, list[str]]:
61
+ """
62
+ Class method to display all methods of the class and its parent classes,
63
+ including the class in which they are defined in alphabetical order.
64
+
65
+ Parameters
66
+ ----------
67
+ include_classmethod : bool, optional
68
+ Whether to include classmethod in the output, by default ``True``
69
+
70
+ classmethod_indicator : str, optional
71
+ A string used to mark classmethod in the output. This string is appended
72
+ to the name of each classmethod to visually differentiate it from regular
73
+ instance methods, by default ``"<classmethod>"``
74
+
75
+ include_staticmethod : bool, optional
76
+ Whether to include staticmethod in the output, by default ``True``
77
+
78
+ staticmethod_indicator : str, optional
79
+ A string used to mark staticmethod in the output. This string is appended
80
+ to the name of each staticmethod to visually differentiate it from regular
81
+ instance methods, by default ``"<staticmethod>"``
82
+
83
+ include_private_method : bool, optional
84
+ Whether to include private method in the output, by default ``False``
85
+
86
+ print_result : bool, optional
87
+ Beautifully print the output, by default ``False``
88
+
89
+ Returns
90
+ -------
91
+ dict[str, list[str]]
92
+ A dictionary where keys are class names and values are lists of method names.
93
+ """
94
+ classes = cls.__mro__[::-1][1:] # MRO in reverse order
95
+ result = {}
96
+ for base in classes:
97
+ methods = []
98
+ for name, attr in base.__dict__.items():
99
+ # Skip private attribute
100
+ if name.startswith("__"):
101
+ continue
102
+
103
+ # Skip private Callable
104
+ if base.__name__ in name and not include_private_method:
105
+ continue
106
+
107
+ # Function
108
+ if callable(attr):
109
+ if isinstance(attr, staticmethod):
110
+ if include_staticmethod:
111
+ methods.append(f"{name} {staticmethod_indicator}")
112
+ else:
113
+ methods.append(name)
114
+ if isinstance(attr, classmethod) and include_classmethod:
115
+ methods.append(f"{name} {classmethod_indicator}")
116
+
117
+ if methods:
118
+ result[base.__name__] = sorted(methods)
119
+
120
+ if print_result:
121
+ cls.__print_show_all_result(result)
122
+
123
+ return result
124
+
125
+ @classmethod
126
+ def show_all_properties(cls, print_result: bool = False) -> dict[str, list[str]]:
127
+ """
128
+ Class method to display all properties of the class and its parent classes,
129
+ including the class in which they are defined in alphabetical order.
130
+
131
+ Parameters
132
+ ----------
133
+ print_result : bool, optional
134
+ Beautifully print the output, by default ``False``
135
+
136
+ Returns
137
+ -------
138
+ dict[str, list[str]]
139
+ A dictionary where keys are class names and values are lists of property names.
140
+ """
141
+ classes = cls.__mro__[::-1][1:] # MRO in reverse order
142
+ result = {}
143
+ for base in classes:
144
+ properties = []
145
+ for name, attr in base.__dict__.items():
146
+ # Skip private attribute
147
+ if name.startswith("__"):
148
+ continue
149
+
150
+ if isinstance(attr, property):
151
+ properties.append(name)
152
+
153
+ if properties:
154
+ result[base.__name__] = sorted(properties)
155
+
156
+ if print_result:
157
+ cls.__print_show_all_result(result)
158
+
159
+ return result
160
+
161
+ @staticmethod
162
+ def __print_show_all_result(result: dict[str, list[str]]) -> None:
163
+ """
164
+ Pretty print the result of ``ShowAllMethodsMixin.show_all_methods()``
165
+
166
+ Parameters
167
+ ----------
168
+ result : dict[str, list[str]]
169
+ Result of ``ShowAllMethodsMixin.show_all_methods()``
170
+ """
171
+ print_func = print # Can be extended with function parameter
172
+
173
+ # Loop through each class base
174
+ for order, (class_base, methods) in enumerate(result.items(), start=1):
175
+ mlen = len(methods) # How many methods in that class
176
+ print_func(f"{order:02}. <{class_base}> | len: {mlen:02}")
177
+
178
+ # Modify methods list
179
+ max_method_name_len = max([len(x) for x in methods])
180
+ if mlen % 2 == 0:
181
+ p1, p2 = methods[: int(mlen / 2)], methods[int(mlen / 2) :]
182
+ else:
183
+ p1, p2 = methods[: int(mlen / 2) + 1], methods[int(mlen / 2) + 1 :]
184
+ p2.append("")
185
+ new_methods = list(zip(p1, p2))
186
+
187
+ # This print 2 methods in 1 line
188
+ for x1, x2 in new_methods:
189
+ if x2 == "":
190
+ print_func(f" - {x1.ljust(max_method_name_len)}")
191
+ else:
192
+ print_func(
193
+ f" - {x1.ljust(max_method_name_len)} - {x2.ljust(max_method_name_len)}"
194
+ )
195
+
196
+ # This print 1 method in one line
197
+ # for name in methods:
198
+ # print(f" - {name.ljust(max_method_name_len)}")
199
+
200
+ print_func("".ljust(88, "-"))
201
+
202
+
203
+ class AutoREPRMixin:
204
+ """
205
+ Generate ``repr()`` output as ``<class(param1=any, param2=any, ...)>``
206
+
207
+ *This class is meant to be used with other class*
208
+
209
+
210
+ Example:
211
+ --------
212
+ >>> class Test(AutoREPRMixin):
213
+ ... def __init__(self, param):
214
+ ... self.param = param
215
+ >>> print(repr(Test(1)))
216
+ Test(param=1)
217
+ """
218
+
219
+ def __repr__(self) -> str:
220
+ """
221
+ Generate a string representation of the instance's attributes.
222
+
223
+ This function retrieves attributes from either the ``__dict__`` or
224
+ ``__slots__`` of the instance, excluding private attributes (those
225
+ starting with an underscore). The attributes are returned as a
226
+ formatted string, with each attribute represented as ``"key=value"``.
227
+
228
+ Convert ``self.__dict__`` from ``{"a": "b"}`` to ``a=repr(b)``
229
+ or ``self.__slots__`` from ``("a",)`` to ``a=repr(self.a)``
230
+ (excluding private attributes)
231
+ """
232
+ # Default output
233
+ out = []
234
+ sep = ", " # Separator
235
+
236
+ # Get attributes
237
+ cls_dict = getattr(self, "__dict__", None)
238
+ cls_slots = getattr(self, "__slots__", None)
239
+
240
+ # Check if __dict__ exist and len(__dict__) > 0
241
+ if cls_dict is not None and len(cls_dict) > 0:
242
+ out = [
243
+ f"{k}={repr(v)}"
244
+ for k, v in self.__dict__.items()
245
+ if not k.startswith("_")
246
+ ]
247
+
248
+ # Check if __slots__ exist and len(__slots__) > 0
249
+ elif cls_slots is not None and len(cls_slots) > 0:
250
+ out = [
251
+ f"{x}={repr(getattr(self, x))}"
252
+ for x in self.__slots__ # type: ignore
253
+ if not x.startswith("_")
254
+ ]
255
+
256
+ # Return out
257
+ return f"{self.__class__.__name__}({sep.join(out)})"
258
+
259
+
260
+ # Class
261
+ # ---------------------------------------------------------------------------
262
+ class BaseClass(ShowAllMethodsMixin, AutoREPRMixin):
263
+ """Base class"""
264
+
265
+ def __str__(self) -> str:
266
+ return repr(self)
267
+
268
+ def __format__(self, format_spec: str) -> str:
269
+ """
270
+ Formats the object according to the specified format.
271
+ If no format_spec is provided, returns the object's string representation.
272
+ (Currently a dummy function)
273
+
274
+ Usage
275
+ -----
276
+ >>> print(f"{<object>:<format_spec>}")
277
+ >>> print(<object>.__format__(<format_spec>))
278
+ >>> print(format(<object>, <format_spec>))
279
+ """
280
+
281
+ return self.__str__()
282
+
283
+
284
+ # Metaclass
285
+ # ---------------------------------------------------------------------------
286
+ class PositiveInitArgsMeta(type):
287
+ """Make sure that every args in a class __init__ is positive"""
288
+
289
+ def __call__(cls, *args, **kwargs):
290
+ # Check if all positional and keyword arguments are positive
291
+ for arg in args:
292
+ if isinstance(arg, (int, float)) and arg < 0:
293
+ raise ValueError(f"Argument {arg} must be positive")
294
+ for key, value in kwargs.items():
295
+ if isinstance(value, (int, float)) and value < 0:
296
+ raise ValueError(f"Argument {key}={value} must be positive")
297
+
298
+ # Call the original __init__ method
299
+ return super().__call__(*args, **kwargs)
@@ -0,0 +1,165 @@
1
+ """
2
+ Absfuyu: Core
3
+ -------------
4
+ Bases for other features (with library)
5
+
6
+ Version: 5.0.0
7
+ Date updated: 12/02/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module Package
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = [
13
+ # Class
14
+ "ShowAllMethodsMixinInspectVer",
15
+ # Metaclass
16
+ "PerformanceTrackingMeta",
17
+ # Class decorator
18
+ "positive_class_init_args",
19
+ ]
20
+
21
+
22
+ # Library
23
+ # ---------------------------------------------------------------------------
24
+ import time
25
+ import tracemalloc
26
+ from collections.abc import Callable
27
+ from functools import wraps
28
+ from inspect import getmro, isfunction
29
+ from types import MethodType
30
+ from typing import Any, ParamSpec, TypeVar
31
+
32
+ # Type
33
+ # ---------------------------------------------------------------------------
34
+ P = ParamSpec("P") # Parameter type
35
+ T = TypeVar("T", bound=type) # Type type - Can be any subtype of `type`
36
+ # R = TypeVar("R") # Return type
37
+
38
+
39
+ # Class
40
+ # ---------------------------------------------------------------------------
41
+ class ShowAllMethodsMixinInspectVer:
42
+ @classmethod
43
+ def show_all_methods(
44
+ cls,
45
+ include_classmethod: bool = True,
46
+ classmethod_indicator: str = "<classmethod>",
47
+ ) -> dict[str, list[str]]:
48
+ result = {}
49
+ for base in getmro(cls)[::-1]:
50
+ methods = []
51
+ # for name, attr in inspect.getmembers(base, predicate=callable):
52
+ for name, attr in base.__dict__.items():
53
+ if name.startswith("__"):
54
+ continue
55
+ if isfunction(attr):
56
+ methods.append(name)
57
+ # if inspect.ismethod(attr):
58
+ if isinstance(attr, (classmethod, MethodType)) and include_classmethod:
59
+ methods.append(f"{name} {classmethod_indicator}")
60
+ if methods:
61
+ result[base.__name__] = sorted(methods)
62
+ return result
63
+
64
+
65
+ # Metaclass
66
+ # ---------------------------------------------------------------------------
67
+ class PerformanceTrackingMeta(type):
68
+ """
69
+ A metaclass that tracks the instantiation time of classes.
70
+
71
+ Usage:
72
+ ------
73
+ >>> class Demo(metaclass=PerformanceTrackingMeta):
74
+ ... def __init__(self):...
75
+ >>> Demo()
76
+ --------------------------------------
77
+ Class: Demo
78
+ Memory usage: 0.000000 MB
79
+ Peak memory usage: 0.000008 MB
80
+ Time elapsed: 0.000001 s
81
+ --------------------------------------
82
+ """
83
+
84
+ def __new__(mcs, name: str, bases: tuple, attrs: dict[str, Any]):
85
+ """
86
+ Intercepts class creation to wrap the ``__init__`` method.
87
+
88
+ Parameters
89
+ ----------
90
+ mcs
91
+ The metaclass
92
+
93
+ name : str
94
+ The class name
95
+
96
+ bases : tuple
97
+ Tuple of base classes
98
+
99
+ attrs : dict[str, Any]
100
+ Dictionary of attributes
101
+ """
102
+ if "__init__" in attrs:
103
+ original_init = attrs["__init__"]
104
+
105
+ @wraps(original_init)
106
+ def wrapped_init(self, *args, **kwargs):
107
+ # Performance check
108
+ tracemalloc.start()
109
+ start_time = time.perf_counter()
110
+
111
+ # Run __init__()
112
+ original_init(self, *args, **kwargs)
113
+
114
+ # Performance stop
115
+ current, peak = tracemalloc.get_traced_memory()
116
+ end_time = time.perf_counter()
117
+ tracemalloc.stop()
118
+ creation_time = end_time - start_time
119
+
120
+ # Print output
121
+ print(
122
+ f"{''.ljust(38, '-')}\n"
123
+ f"Class: {name}\n"
124
+ f"Memory usage:\t\t {current / 10**6:,.6f} MB\n"
125
+ f"Peak memory usage:\t {peak / 10**6:,.6f} MB\n"
126
+ f"Time elapsed:\t\t {creation_time:,.6f} s\n"
127
+ f"{''.ljust(38, '-')}"
128
+ )
129
+
130
+ attrs["__init__"] = wrapped_init
131
+ return super().__new__(mcs, name, bases, attrs)
132
+
133
+
134
+ # Decorator
135
+ # ---------------------------------------------------------------------------
136
+ def positive_class_init_args(cls: T):
137
+ """
138
+ A class decorator that ensures all arguments in the ``__init__()`` method are positive.
139
+ """
140
+ # original_init: Callable[P, None] | None = getattr(cls, "__init__", None)
141
+ # if original_init is None:
142
+ # return cls
143
+ try:
144
+ original_init: Callable[P, None] = cls.__init__ # type: ignore
145
+ except AttributeError:
146
+ return cls
147
+
148
+ @wraps(original_init)
149
+ def new_init(self, *args: P.args, **kwargs: P.kwargs):
150
+ # Check if all positional arguments are positive
151
+ for arg in args:
152
+ if isinstance(arg, (int, float)) and arg < 0:
153
+ raise ValueError(f"Argument {arg} must be positive")
154
+
155
+ # Check if all keyword arguments are positive
156
+ for key, value in kwargs.items():
157
+ if isinstance(value, (int, float)) and value < 0:
158
+ raise ValueError(f"Argument {key}={value} must be positive")
159
+
160
+ # Call the original __init__ method
161
+ original_init(self, *args, **kwargs)
162
+
163
+ # setattr(cls, "__init__", new_init)
164
+ cls.__init__ = new_init # type: ignore
165
+ return cls
@@ -0,0 +1,67 @@
1
+ """
2
+ Absfuyu: Core
3
+ -------------
4
+ Decorator
5
+
6
+ Version: 5.0.0
7
+ Date updated: 22/02/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module Package
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = ["dummy_decorator", "dummy_decorator_with_args"]
13
+
14
+
15
+ # Library
16
+ # ---------------------------------------------------------------------------
17
+ from collections.abc import Callable
18
+ from functools import wraps
19
+ from typing import ParamSpec, TypeVar, overload
20
+
21
+ # Type
22
+ # ---------------------------------------------------------------------------
23
+ P = ParamSpec("P") # Parameter type
24
+ R = TypeVar("R") # Return type - Can be anything
25
+ T = TypeVar("T", bound=type) # Type type - Can be any subtype of `type`
26
+
27
+
28
+ # Decorator
29
+ # ---------------------------------------------------------------------------
30
+ @overload
31
+ def dummy_decorator(obj: T) -> T: ...
32
+ @overload
33
+ def dummy_decorator(obj: Callable[P, R]) -> Callable[P, R]: ...
34
+ def dummy_decorator(obj: Callable[P, R] | T) -> Callable[P, R] | T:
35
+ """
36
+ This is a decorator that does nothing. Normally used as a placeholder
37
+ """
38
+ if isinstance(obj, type):
39
+ return obj
40
+
41
+ @wraps(obj)
42
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
43
+ return obj(*args, **kwargs)
44
+
45
+ return wrapper
46
+
47
+
48
+ def dummy_decorator_with_args(*args, **kwargs):
49
+ """
50
+ This is a decorator with args and kwargs that does nothing. Normally used as a placeholder
51
+ """
52
+
53
+ @overload
54
+ def decorator(obj: T) -> T: ...
55
+ @overload
56
+ def decorator(obj: Callable[P, R]) -> Callable[P, R]: ...
57
+ def decorator(obj: Callable[P, R] | T) -> Callable[P, R] | T:
58
+ if isinstance(obj, type):
59
+ return obj
60
+
61
+ @wraps(obj)
62
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
63
+ return obj(*args, **kwargs)
64
+
65
+ return wrapper
66
+
67
+ return decorator
@@ -0,0 +1,163 @@
1
+ """
2
+ Absfuyu: Core
3
+ -------------
4
+ Sphinx docstring decorator
5
+
6
+ Version: 5.0.0
7
+ Date updated: 13/02/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # Module Package
11
+ # ---------------------------------------------------------------------------
12
+ __all__ = [
13
+ "SphinxDocstring",
14
+ "SphinxDocstringMode",
15
+ "versionadded",
16
+ "versionchanged",
17
+ "deprecated",
18
+ ]
19
+
20
+
21
+ # Library
22
+ # ---------------------------------------------------------------------------
23
+ from collections.abc import Callable
24
+ from enum import Enum
25
+ from functools import partial, wraps
26
+ from string import Template
27
+ from typing import ClassVar, ParamSpec, TypeVar, overload
28
+
29
+ # Type
30
+ # ---------------------------------------------------------------------------
31
+ P = ParamSpec("P") # Parameter type
32
+ R = TypeVar("R") # Return type - Can be anything
33
+ T = TypeVar("T", bound=type) # Type type - Can be any subtype of `type`
34
+
35
+ _SPHINX_DOCS_TEMPLATE = Template("$line_break*$mode in version $version$reason*")
36
+
37
+
38
+ # Class
39
+ # ---------------------------------------------------------------------------
40
+ class SphinxDocstringMode(Enum):
41
+ """
42
+ Enum representing the mode of the version change
43
+ (added, changed, or deprecated)
44
+ """
45
+
46
+ ADDED = "Added"
47
+ CHANGED = "Changed"
48
+ DEPRECATED = "Deprecated"
49
+
50
+
51
+ class SphinxDocstring:
52
+ """
53
+ A class-based decorator to add a 'Version added',
54
+ 'Version changed', or 'Deprecated' note to a function's docstring,
55
+ formatted for Sphinx documentation.
56
+ """
57
+
58
+ _LINEBREAK: ClassVar[str] = "\n\n" # Use ClassVar for constant
59
+
60
+ def __init__(
61
+ self,
62
+ version: str,
63
+ reason: str | None = None,
64
+ mode: SphinxDocstringMode = SphinxDocstringMode.ADDED,
65
+ ) -> None:
66
+ """
67
+ Initializes the SphinxDocstring decorator.
68
+
69
+ Parameters
70
+ ----------
71
+ version : str
72
+ The version in which the function was added, changed, or deprecated.
73
+
74
+ reason : str | None, optional
75
+ An optional reason or description for the change
76
+ or deprecation, by default ``None``
77
+
78
+ mode : SphinxDocstringMode | int, optional
79
+ Specifies whether the function was 'added', 'changed', or 'deprecated',
80
+ by default SphinxDocstringMode.ADDED
81
+
82
+ Usage
83
+ -----
84
+ Use this as a decorator (``@SphinxDocstring(<parameters>)``)
85
+ """
86
+ self.version = version
87
+ self.reason = reason
88
+ self.mode = mode
89
+
90
+ @overload
91
+ def __call__(self, obj: T) -> T: ... # Class overload
92
+ @overload
93
+ def __call__(self, obj: Callable[P, R]) -> Callable[P, R]: ... # Function overload
94
+ def __call__(self, obj: T | Callable[P, R]) -> T | Callable[P, R]:
95
+ # Class wrapper
96
+ if isinstance(obj, type): # if inspect.isclass(obj):
97
+ obj.__doc__ = (obj.__doc__ or "") + self._generate_version_note(
98
+ num_of_white_spaces=self._calculate_white_space(obj.__doc__)
99
+ )
100
+ return obj
101
+
102
+ # Function wrapper
103
+ @wraps(obj)
104
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
105
+ return obj(*args, **kwargs)
106
+
107
+ # Add docstring
108
+ # version_note = self._generate_version_note()
109
+ # if wrapper.__doc__ is None:
110
+ # wrapper.__doc__ = version_note
111
+ # else:
112
+ # wrapper.__doc__ += version_note
113
+
114
+ wrapper.__doc__ = (wrapper.__doc__ or "") + self._generate_version_note(
115
+ num_of_white_spaces=self._calculate_white_space(wrapper.__doc__)
116
+ )
117
+
118
+ return wrapper
119
+
120
+ def _calculate_white_space(self, docs: str | None) -> int:
121
+ """
122
+ Calculates the number of leading white spaces
123
+ in __doc__ of original function
124
+ """
125
+
126
+ res = 0
127
+ if docs is None:
128
+ return res
129
+
130
+ # # Index of the last whitespaces
131
+ # # https://stackoverflow.com/a/13649118
132
+ # res = next((i for i, c in enumerate(docs) if not c.isspace()), 0)
133
+ # return res if res % 2 == 0 else res - 1
134
+
135
+ # Alternative solution
136
+ for char in docs:
137
+ if char == " ":
138
+ res += 1
139
+ if char == "\t":
140
+ res += 4
141
+ if not char.isspace():
142
+ break
143
+ return res
144
+
145
+ def _generate_version_note(self, num_of_white_spaces: int) -> str:
146
+ """
147
+ Generates the version note string based on the mode
148
+ """
149
+ reason_str = f": {self.reason}" if self.reason else ""
150
+ # return f"{self._LINEBREAK}*{self.mode.value} in version {self.version}{reason_str}*"
151
+ return _SPHINX_DOCS_TEMPLATE.substitute(
152
+ line_break=self._LINEBREAK + " " * num_of_white_spaces,
153
+ mode=self.mode.value,
154
+ version=self.version,
155
+ reason=reason_str,
156
+ )
157
+
158
+
159
+ # Partial
160
+ # ---------------------------------------------------------------------------
161
+ versionadded = partial(SphinxDocstring, mode=SphinxDocstringMode.ADDED)
162
+ versionchanged = partial(SphinxDocstring, mode=SphinxDocstringMode.CHANGED)
163
+ deprecated = partial(SphinxDocstring, mode=SphinxDocstringMode.DEPRECATED)