absfuyu 5.0.0__py3-none-any.whl → 6.1.2__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 (103) hide show
  1. absfuyu/__init__.py +5 -3
  2. absfuyu/__main__.py +3 -3
  3. absfuyu/cli/__init__.py +13 -2
  4. absfuyu/cli/audio_group.py +98 -0
  5. absfuyu/cli/color.py +30 -14
  6. absfuyu/cli/config_group.py +9 -2
  7. absfuyu/cli/do_group.py +23 -6
  8. absfuyu/cli/game_group.py +27 -2
  9. absfuyu/cli/tool_group.py +81 -11
  10. absfuyu/config/__init__.py +3 -3
  11. absfuyu/core/__init__.py +12 -8
  12. absfuyu/core/baseclass.py +929 -96
  13. absfuyu/core/baseclass2.py +44 -3
  14. absfuyu/core/decorator.py +70 -4
  15. absfuyu/core/docstring.py +64 -41
  16. absfuyu/core/dummy_cli.py +3 -3
  17. absfuyu/core/dummy_func.py +19 -6
  18. absfuyu/dxt/__init__.py +2 -2
  19. absfuyu/dxt/base_type.py +93 -0
  20. absfuyu/dxt/dictext.py +204 -16
  21. absfuyu/dxt/dxt_support.py +2 -2
  22. absfuyu/dxt/intext.py +151 -34
  23. absfuyu/dxt/listext.py +969 -127
  24. absfuyu/dxt/strext.py +77 -17
  25. absfuyu/extra/__init__.py +2 -2
  26. absfuyu/extra/audio/__init__.py +8 -0
  27. absfuyu/extra/audio/_util.py +57 -0
  28. absfuyu/extra/audio/convert.py +192 -0
  29. absfuyu/extra/audio/lossless.py +281 -0
  30. absfuyu/extra/beautiful.py +3 -2
  31. absfuyu/extra/da/__init__.py +72 -0
  32. absfuyu/extra/da/dadf.py +1600 -0
  33. absfuyu/extra/da/dadf_base.py +186 -0
  34. absfuyu/extra/da/df_func.py +181 -0
  35. absfuyu/extra/da/mplt.py +219 -0
  36. absfuyu/extra/ggapi/__init__.py +8 -0
  37. absfuyu/extra/ggapi/gdrive.py +223 -0
  38. absfuyu/extra/ggapi/glicense.py +148 -0
  39. absfuyu/extra/ggapi/glicense_df.py +186 -0
  40. absfuyu/extra/ggapi/gsheet.py +88 -0
  41. absfuyu/extra/img/__init__.py +30 -0
  42. absfuyu/extra/img/converter.py +402 -0
  43. absfuyu/extra/img/dup_check.py +291 -0
  44. absfuyu/extra/pdf.py +87 -0
  45. absfuyu/extra/rclone.py +253 -0
  46. absfuyu/extra/xml.py +90 -0
  47. absfuyu/fun/__init__.py +7 -20
  48. absfuyu/fun/rubik.py +442 -0
  49. absfuyu/fun/tarot.py +2 -2
  50. absfuyu/game/__init__.py +2 -2
  51. absfuyu/game/game_stat.py +2 -2
  52. absfuyu/game/schulte.py +78 -0
  53. absfuyu/game/sudoku.py +2 -2
  54. absfuyu/game/tictactoe.py +2 -3
  55. absfuyu/game/wordle.py +6 -4
  56. absfuyu/general/__init__.py +4 -4
  57. absfuyu/general/content.py +4 -4
  58. absfuyu/general/human.py +2 -2
  59. absfuyu/general/resrel.py +213 -0
  60. absfuyu/general/shape.py +3 -8
  61. absfuyu/general/tax.py +344 -0
  62. absfuyu/logger.py +806 -59
  63. absfuyu/numbers/__init__.py +13 -0
  64. absfuyu/numbers/number_to_word.py +321 -0
  65. absfuyu/numbers/shorten_number.py +303 -0
  66. absfuyu/numbers/time_duration.py +217 -0
  67. absfuyu/pkg_data/__init__.py +2 -2
  68. absfuyu/pkg_data/deprecated.py +2 -2
  69. absfuyu/pkg_data/logo.py +1462 -0
  70. absfuyu/sort.py +4 -4
  71. absfuyu/tools/__init__.py +28 -2
  72. absfuyu/tools/checksum.py +144 -9
  73. absfuyu/tools/converter.py +120 -34
  74. absfuyu/tools/generator.py +461 -0
  75. absfuyu/tools/inspector.py +752 -0
  76. absfuyu/tools/keygen.py +2 -2
  77. absfuyu/tools/obfuscator.py +47 -9
  78. absfuyu/tools/passwordlib.py +89 -25
  79. absfuyu/tools/shutdownizer.py +3 -8
  80. absfuyu/tools/sw.py +718 -0
  81. absfuyu/tools/web.py +10 -13
  82. absfuyu/typings.py +138 -0
  83. absfuyu/util/__init__.py +114 -6
  84. absfuyu/util/api.py +41 -18
  85. absfuyu/util/cli.py +119 -0
  86. absfuyu/util/gui.py +91 -0
  87. absfuyu/util/json_method.py +43 -14
  88. absfuyu/util/lunar.py +2 -2
  89. absfuyu/util/package.py +124 -0
  90. absfuyu/util/path.py +702 -82
  91. absfuyu/util/performance.py +122 -7
  92. absfuyu/util/shorten_number.py +244 -21
  93. absfuyu/util/text_table.py +481 -0
  94. absfuyu/util/zipped.py +8 -7
  95. absfuyu/version.py +79 -59
  96. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/METADATA +52 -11
  97. absfuyu-6.1.2.dist-info/RECORD +105 -0
  98. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/WHEEL +1 -1
  99. absfuyu/extra/data_analysis.py +0 -1078
  100. absfuyu/general/generator.py +0 -303
  101. absfuyu-5.0.0.dist-info/RECORD +0 -68
  102. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/entry_points.txt +0 -0
  103. {absfuyu-5.0.0.dist-info → absfuyu-6.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Bases for other features (with library)
5
5
 
6
- Version: 5.0.0
7
- Date updated: 12/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module Package
@@ -62,6 +62,47 @@ class ShowAllMethodsMixinInspectVer:
62
62
  return result
63
63
 
64
64
 
65
+ class UniversalConfigMixin:
66
+ """
67
+ Universal config Mixin
68
+
69
+ This use these attributes:
70
+ - _instance_config
71
+ - config
72
+
73
+ Example:
74
+ --------
75
+ >>> class Test(UniversalConfigMixin):
76
+ >>> DEFAULT_CONFIG = {"test": True}
77
+ >>> test = Test(config={"new_key": True})
78
+ >>> print(test.config)
79
+ {'test': True, 'new_key': True}
80
+ """
81
+
82
+ DEFAULT_CONFIG = {}
83
+
84
+ def __init__(self, **kwargs) -> None:
85
+ try:
86
+ super().__init__(**kwargs)
87
+ except TypeError:
88
+ pass
89
+ # instance override
90
+ self._instance_config = kwargs.get("config", {})
91
+
92
+ @property
93
+ def config(self):
94
+ # Priority: instance > class > default
95
+ merged = dict(self.DEFAULT_CONFIG)
96
+
97
+ try:
98
+ # merged.update(getattr(self, "CLASS_CONFIG", {}))
99
+ merged.update(self._instance_config)
100
+ except Exception:
101
+ pass
102
+
103
+ return merged
104
+
105
+
65
106
  # Metaclass
66
107
  # ---------------------------------------------------------------------------
67
108
  class PerformanceTrackingMeta(type):
@@ -71,7 +112,7 @@ class PerformanceTrackingMeta(type):
71
112
  Usage:
72
113
  ------
73
114
  >>> class Demo(metaclass=PerformanceTrackingMeta):
74
- ... def __init__(self):...
115
+ ... def __init__(self): ...
75
116
  >>> Demo()
76
117
  --------------------------------------
77
118
  Class: Demo
absfuyu/core/decorator.py CHANGED
@@ -3,20 +3,24 @@ Absfuyu: Core
3
3
  -------------
4
4
  Decorator
5
5
 
6
- Version: 5.0.0
7
- Date updated: 22/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module Package
11
11
  # ---------------------------------------------------------------------------
12
- __all__ = ["dummy_decorator", "dummy_decorator_with_args"]
12
+ __all__ = [
13
+ "dummy_decorator",
14
+ "dummy_decorator_with_args",
15
+ "add_subclass_methods_decorator",
16
+ ]
13
17
 
14
18
 
15
19
  # Library
16
20
  # ---------------------------------------------------------------------------
17
21
  from collections.abc import Callable
18
22
  from functools import wraps
19
- from typing import ParamSpec, TypeVar, overload
23
+ from typing import ParamSpec, TypeVar, cast, overload
20
24
 
21
25
  # Type
22
26
  # ---------------------------------------------------------------------------
@@ -52,8 +56,10 @@ def dummy_decorator_with_args(*args, **kwargs):
52
56
 
53
57
  @overload
54
58
  def decorator(obj: T) -> T: ...
59
+
55
60
  @overload
56
61
  def decorator(obj: Callable[P, R]) -> Callable[P, R]: ...
62
+
57
63
  def decorator(obj: Callable[P, R] | T) -> Callable[P, R] | T:
58
64
  if isinstance(obj, type):
59
65
  return obj
@@ -65,3 +71,63 @@ def dummy_decorator_with_args(*args, **kwargs):
65
71
  return wrapper
66
72
 
67
73
  return decorator
74
+
75
+
76
+ def add_subclass_methods_decorator(cls: T) -> T:
77
+ """
78
+ Class decorator replace the ``__init_subclass__`` method.
79
+
80
+ This method populates a dictionary with subclass names as keys
81
+ and their available methods as values.
82
+
83
+ - Create 2 class attributes: ``_METHOD_INCLUDE`` (bool) and ``SUBCLASS_METHODS`` (dict[str, list[str]])
84
+ - Automatically add subclass methods to class variable: ``SUBCLASS_METHODS``
85
+ - Set class attribute ``_METHOD_INCLUDE`` to ``False`` to exclude from ``SUBCLASS_METHODS``
86
+
87
+ Example:
88
+ --------
89
+ >>> # Normal behavior
90
+ >>> @add_subclass_methods_decorator
91
+ >>> class TestParent: ...
92
+ >>> class TestChild(TestParent):
93
+ ... def method1(self): ...
94
+ >>> TestChild.SUBCLASS_METHODS
95
+ {'__main__.TestChild': ['method1']}
96
+
97
+ >>> # Hidden from ``SUBCLASS_METHODS``
98
+ >>> @add_subclass_methods_decorator
99
+ >>> class TestParent: ...
100
+ >>> class TestChildHidden(TestParent):
101
+ ... _METHOD_INCLUDE = False
102
+ ... def method1(self): ...
103
+ >>> TestChildHidden.SUBCLASS_METHODS
104
+ {}
105
+ """
106
+
107
+ # Check for class
108
+ if not isinstance(cls, type):
109
+ raise ValueError("Object is not a class")
110
+
111
+ class AutoSubclassMixin:
112
+ _METHOD_INCLUDE: bool = True # Include in SUBCLASS_METHODS
113
+ SUBCLASS_METHODS: dict[str, list[str]] = {}
114
+
115
+ def __init_subclass__(cls, *args, **kwargs) -> None:
116
+ """
117
+ This create a dictionary with:
118
+ - key (str) : Subclass
119
+ - value (list[str]): List of available methods
120
+ """
121
+ super().__init_subclass__(*args, **kwargs)
122
+
123
+ if cls._METHOD_INCLUDE and not any(
124
+ [x.endswith(cls.__name__) for x in cls.SUBCLASS_METHODS.keys()]
125
+ ):
126
+ methods_list: list[str] = [
127
+ k for k, v in cls.__dict__.items() if callable(v)
128
+ ]
129
+ if len(methods_list) > 0:
130
+ name = f"{cls.__module__}.{cls.__name__}"
131
+ cls.SUBCLASS_METHODS.update({name: sorted(methods_list)})
132
+
133
+ return cast(T, type("AutoSubclass", (AutoSubclassMixin, cls), {}))
absfuyu/core/docstring.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Sphinx docstring decorator
5
5
 
6
- Version: 5.0.0
7
- Date updated: 13/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module Package
@@ -28,9 +28,9 @@ from typing import ClassVar, ParamSpec, TypeVar, overload
28
28
 
29
29
  # Type
30
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`
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
34
 
35
35
  _SPHINX_DOCS_TEMPLATE = Template("$line_break*$mode in version $version$reason*")
36
36
 
@@ -46,6 +46,7 @@ class SphinxDocstringMode(Enum):
46
46
  ADDED = "Added"
47
47
  CHANGED = "Changed"
48
48
  DEPRECATED = "Deprecated"
49
+ PENDING_REMOVE = "Pending to remove"
49
50
 
50
51
 
51
52
  class SphinxDocstring:
@@ -53,6 +54,24 @@ class SphinxDocstring:
53
54
  A class-based decorator to add a 'Version added',
54
55
  'Version changed', or 'Deprecated' note to a function's docstring,
55
56
  formatted for Sphinx documentation.
57
+
58
+ Parameters
59
+ ----------
60
+ version : str
61
+ The version in which the function was added, changed, or deprecated.
62
+
63
+ reason : str | None, optional
64
+ An optional reason or description for the change
65
+ or deprecation, by default ``None``
66
+
67
+ mode : SphinxDocstringMode, optional
68
+ Specifies whether the function was 'added', 'changed', or 'deprecated',
69
+ by default SphinxDocstringMode.ADDED
70
+
71
+
72
+ Usage
73
+ -----
74
+ Use this as a decorator (``@SphinxDocstring(<parameters>)``)
56
75
  """
57
76
 
58
77
  _LINEBREAK: ClassVar[str] = "\n\n" # Use ClassVar for constant
@@ -75,7 +94,7 @@ class SphinxDocstring:
75
94
  An optional reason or description for the change
76
95
  or deprecation, by default ``None``
77
96
 
78
- mode : SphinxDocstringMode | int, optional
97
+ mode : SphinxDocstringMode, optional
79
98
  Specifies whether the function was 'added', 'changed', or 'deprecated',
80
99
  by default SphinxDocstringMode.ADDED
81
100
 
@@ -87,35 +106,27 @@ class SphinxDocstring:
87
106
  self.reason = reason
88
107
  self.mode = mode
89
108
 
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]:
109
+ @overload # Class overload
110
+ def __call__(self, obj: _T) -> _T: ...
111
+
112
+ @overload # Function overload
113
+ def __call__(self, obj: Callable[_P, _R]) -> Callable[_P, _R]: ...
114
+
115
+ def __call__(self, obj: _T | Callable[_P, _R]) -> _T | Callable[_P, _R]:
116
+ """
117
+ Decorator for class and callable
118
+ """
119
+
95
120
  # Class wrapper
96
121
  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
122
+ return self._update_doc(obj)
101
123
 
102
124
  # Function wrapper
103
125
  @wraps(obj)
104
- def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
126
+ def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
105
127
  return obj(*args, **kwargs)
106
128
 
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
129
+ return self._update_doc(wrapper)
119
130
 
120
131
  def _calculate_white_space(self, docs: str | None) -> int:
121
132
  """
@@ -127,19 +138,19 @@ class SphinxDocstring:
127
138
  if docs is None:
128
139
  return res
129
140
 
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
141
+ try:
142
+ # Replace tabs with space and split line
143
+ lines = docs.expandtabs(4).splitlines()
144
+ except UnicodeError:
145
+ return res
146
+ else:
147
+ # Get indentation of each line and unique it
148
+ indent_set = {len(line) - len(line.lstrip()) for line in lines[1:]}
149
+ # Drop 0
150
+ res_list = sorted([x for x in indent_set if x > 0])
151
+
152
+ if res_list:
153
+ return res_list[0]
143
154
  return res
144
155
 
145
156
  def _generate_version_note(self, num_of_white_spaces: int) -> str:
@@ -155,9 +166,21 @@ class SphinxDocstring:
155
166
  reason=reason_str,
156
167
  )
157
168
 
169
+ @overload
170
+ def _update_doc(self, obj: _T) -> _T: ...
171
+ @overload
172
+ def _update_doc(self, obj: Callable[_P, _R]) -> Callable[_P, _R]: ...
173
+ def _update_doc(self, obj: _T | Callable[_P, _R]) -> _T | Callable[_P, _R]:
174
+ """Update docstring for an object"""
175
+ obj.__doc__ = (obj.__doc__ or "") + self._generate_version_note(
176
+ num_of_white_spaces=self._calculate_white_space(obj.__doc__)
177
+ )
178
+ return obj
179
+
158
180
 
159
181
  # Partial
160
182
  # ---------------------------------------------------------------------------
161
183
  versionadded = partial(SphinxDocstring, mode=SphinxDocstringMode.ADDED)
162
184
  versionchanged = partial(SphinxDocstring, mode=SphinxDocstringMode.CHANGED)
163
185
  deprecated = partial(SphinxDocstring, mode=SphinxDocstringMode.DEPRECATED)
186
+ pendingremove = partial(SphinxDocstring, mode=SphinxDocstringMode.PENDING_REMOVE)
absfuyu/core/dummy_cli.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Dummy cli
5
5
 
6
- Version: 5.0.0
7
- Date updated: 25/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module Package
@@ -57,7 +57,7 @@ def get_parser(
57
57
 
58
58
 
59
59
  def cli() -> None:
60
- desc = "This is a dummy cli, please install <click> and <colorama> package to use this feature"
60
+ desc = "This is a dummy cli, install <click> and <colorama> package to use this feature"
61
61
  arg_parser = get_parser(
62
62
  name=__title__,
63
63
  description=desc,
@@ -3,8 +3,8 @@ Absfuyu: Core
3
3
  -------------
4
4
  Dummy functions when other libraries are unvailable
5
5
 
6
- Version: 5.0.0
7
- Date updated: 14/02/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
  """
9
9
 
10
10
  # Module Package
@@ -12,6 +12,7 @@ Date updated: 14/02/2025 (dd/mm/yyyy)
12
12
  __all__ = [
13
13
  "tqdm",
14
14
  "unidecode",
15
+ "dummy_function",
15
16
  ]
16
17
 
17
18
 
@@ -23,8 +24,9 @@ from importlib import import_module
23
24
  # ---------------------------------------------------------------------------
24
25
  # tqdm wrapper
25
26
  try:
26
- tqdm = import_module("tqdm").tqdm
27
- except ModuleNotFoundError:
27
+ _tqdm = import_module("tqdm")
28
+ tqdm = getattr(_tqdm, "tqdm") # noqa
29
+ except (ImportError, AttributeError):
28
30
 
29
31
  def tqdm(iterable, *args, **kwargs):
30
32
  """
@@ -36,8 +38,9 @@ except ModuleNotFoundError:
36
38
 
37
39
  # unidecode wrapper
38
40
  try:
39
- unidecode = import_module("unidecode").unidecode
40
- except ModuleNotFoundError:
41
+ _unidecode = import_module("unidecode")
42
+ unidecode = getattr(_unidecode, "unidecode") # noqa
43
+ except (ImportError, AttributeError):
41
44
 
42
45
  def unidecode(*args, **kwargs):
43
46
  """
@@ -45,3 +48,13 @@ except ModuleNotFoundError:
45
48
  install package ``unidecode`` to fully use this feature
46
49
  """
47
50
  return args[0]
51
+
52
+
53
+ # dummy
54
+ def dummy_function(*args, **kwargs):
55
+ """This is a dummy function"""
56
+ if args:
57
+ return args[0]
58
+ if kwargs:
59
+ return kwargs[list(kwargs)[0]]
60
+ return None
absfuyu/dxt/__init__.py CHANGED
@@ -3,8 +3,8 @@ Absfuyu: Data Extension
3
3
  -----------------------
4
4
  Extension for data type such as ``list``, ``str``, ``dict``, ...
5
5
 
6
- Version: 5.0.0
7
- Date updated: 11/01/2025 (dd/mm/yyyy)
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
8
 
9
9
  Features:
10
10
  ---------
@@ -0,0 +1,93 @@
1
+ """
2
+ Absfuyu: Data Extension
3
+ -----------------------
4
+ Base type expansion
5
+
6
+ Version: 6.1.1
7
+ Date updated: 30/12/2025 (dd/mm/yyyy)
8
+ """
9
+
10
+ # from __future__ import annotations
11
+
12
+ # Module Package
13
+ # ---------------------------------------------------------------------------
14
+ __all__ = []
15
+
16
+
17
+ # Library
18
+ # ---------------------------------------------------------------------------
19
+ from abc import ABC
20
+ from typing import Generic, Self, TypeVar
21
+
22
+ # Class
23
+ # ---------------------------------------------------------------------------
24
+ T = TypeVar("T")
25
+
26
+
27
+ class BaseType(Generic[T], ABC):
28
+ """
29
+ A universal base class for creating custom immutable type wrappers.
30
+
31
+ Features:
32
+ ----------
33
+ - Works with any immutable type (int, str, float, etc.)
34
+ - Provides validation hook via `_validate`
35
+ - Automatically infers the base type from generic argument
36
+
37
+
38
+ Example:
39
+ --------
40
+ >>> class NonNegativeInt(BaseType[int], int):
41
+ >>> @classmethod
42
+ >>> def _validate(cls, value: int) -> None:
43
+ >>> if value < 0:
44
+ >>> raise ValueError("Value must be non-negative")
45
+ """
46
+
47
+ # Automatically infer `_base_type` when subclassed
48
+ def __init_subclass__(cls, *args, **kwargs) -> None:
49
+ super().__init_subclass__(*args, **kwargs)
50
+
51
+ # print(getattr(cls, "__orig_bases__", []))
52
+ for base in getattr(cls, "__orig_bases__", []):
53
+ if hasattr(base, "__args__") and base.__args__:
54
+ cls._base_type: type[T] = base.__args__[0]
55
+ break
56
+ else:
57
+ raise TypeError(f"{cls.__name__} must specify a generic base type, e.g. BaseType[int]")
58
+
59
+ def __new__(cls, value: T) -> Self:
60
+ base_type = cls._base_type
61
+ if not isinstance(value, base_type):
62
+ raise TypeError(f"{cls.__name__} must be initialized with {base_type.__name__}, got {type(value).__name__}")
63
+ cls._validate(value)
64
+ return base_type.__new__(cls, value) # type: ignore
65
+
66
+ @classmethod
67
+ def _validate(cls, value: T) -> None:
68
+ """Override this in subclasses to add custom validation."""
69
+ pass
70
+
71
+ def __repr__(self) -> str:
72
+ # base_name = self._base_type.__name__
73
+ # return f"{self.__class__.__name__}({base_name}={self._base_type(self)!r})" # type: ignore
74
+ return f"{self.__class__.__name__}({self._base_type(self)!r})" # type: ignore
75
+
76
+
77
+ class IntBase(BaseType[int], int):
78
+ """
79
+ A base class for creating custom integer-like types.
80
+ Provides a hook for validation and extension.
81
+
82
+ Parameters
83
+ ----------
84
+ value : int
85
+ Int value
86
+
87
+
88
+ Usage:
89
+ ------
90
+ Use ``_validate(cls, value: int) -> None:`` classmethod to create custom validator
91
+ """
92
+
93
+ pass