jetpytools 1.7.4__py3-none-any.whl → 2.0.1__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 jetpytools might be problematic. Click here for more details.

jetpytools/types/utils.py CHANGED
@@ -4,28 +4,28 @@ import sys
4
4
  from contextlib import suppress
5
5
  from functools import wraps
6
6
  from inspect import Signature
7
- from inspect import _empty as empty_param
8
7
  from typing import (
9
8
  TYPE_CHECKING,
10
9
  Any,
11
10
  Callable,
12
11
  ClassVar,
13
12
  Concatenate,
14
- Generator,
15
13
  Generic,
16
14
  Iterable,
17
15
  Iterator,
18
16
  Mapping,
19
17
  NoReturn,
18
+ ParamSpec,
20
19
  Protocol,
20
+ Self,
21
21
  Sequence,
22
22
  cast,
23
23
  overload,
24
24
  )
25
25
 
26
- from typing_extensions import Self, TypeVar, deprecated
26
+ from typing_extensions import TypeVar, deprecated
27
27
 
28
- from .builtins import F0, F1, P0, P1, R0, T0, KwargsT, P, R, R0_co, R1_co, R_co, T, T0_co, T1_co, T_co
28
+ from .builtins import KwargsT
29
29
 
30
30
  __all__ = [
31
31
  "KwargsNotNone",
@@ -38,12 +38,11 @@ __all__ = [
38
38
  "get_subclasses",
39
39
  "inject_kwargs_params",
40
40
  "inject_self",
41
- "to_singleton",
42
41
  ]
43
42
  # ruff: noqa: N801
44
43
 
45
44
 
46
- def copy_signature(target: F0, /) -> Callable[[Callable[..., Any]], F0]:
45
+ def copy_signature[F: Callable[..., Any]](target: F, /) -> Callable[[Callable[..., Any]], F]:
47
46
  """
48
47
  Utility function to copy the signature of one function to another one.
49
48
 
@@ -71,292 +70,441 @@ def copy_signature(target: F0, /) -> Callable[[Callable[..., Any]], F0]:
71
70
  # another thing
72
71
  """
73
72
 
74
- def decorator(wrapped: Callable[..., Any]) -> F0:
75
- return cast(F0, wrapped)
73
+ def decorator(wrapped: Callable[..., Any]) -> F:
74
+ return cast(F, wrapped)
76
75
 
77
76
  return decorator
78
77
 
79
78
 
80
- class injected_self_func(Protocol[T_co, P, R_co]):
81
- @overload
82
- @staticmethod
83
- def __call__(*args: P.args, **kwargs: P.kwargs) -> R_co: ...
79
+ type _InnerInjectSelfType = dict[_InjectSelfMeta, dict[_InjectSelfMeta, _InnerInjectSelfType]]
80
+ _inject_self_cls: _InnerInjectSelfType = {}
84
81
 
85
- @overload
86
- @staticmethod
87
- def __call__(self: T_co, *args: P.args, **kwargs: P.kwargs) -> R_co: # type: ignore[misc]
88
- ...
89
82
 
90
- @overload
91
- @staticmethod
92
- def __call__(cls: type[T_co], *args: P.args, **kwargs: P.kwargs) -> R_co: ...
83
+ class _InjectSelfMeta(type):
84
+ """
85
+ Metaclass used to manage subclass relationships and type flattening for the `inject_self` hierarchy.
86
+ """
87
+
88
+ _subclasses: frozenset[_InjectSelfMeta]
89
+ """All descendant metaclasses of a given `inject_self` subclass."""
93
90
 
91
+ def __new__[MetaSelf: _InjectSelfMeta](
92
+ mcls: type[MetaSelf], name: str, bases: tuple[type, ...], namespace: dict[str, Any], /, **kwargs: Any
93
+ ) -> MetaSelf:
94
+ cls = super().__new__(mcls, name, bases, namespace, **kwargs)
94
95
 
95
- self_objects_cache = dict[Any, Any]()
96
+ clsd = _inject_self_cls.setdefault(cls, {})
96
97
 
98
+ for k, v in _inject_self_cls.items():
99
+ if k in namespace.values():
100
+ clsd[k] = v
97
101
 
98
- class inject_self_base(Generic[T_co, P, R_co]):
99
- cache: bool | None
100
- signature: Signature | None
101
- init_kwargs: list[str] | None
102
- first_key: str | None
102
+ cls._subclasses = frozenset(cls._flatten_cls())
103
103
 
104
- def __init__(self, function: Callable[Concatenate[T_co, P], R_co], /, *, cache: bool = False) -> None:
104
+ return cls
105
+
106
+ def _flatten_cls(cls, d: _InnerInjectSelfType | None = None) -> Iterator[_InjectSelfMeta]:
105
107
  """
106
- Wrap ``function`` to always have a self provided to it.
108
+ Recursively flatten and yield all nested inject_self metaclass relationships.
107
109
 
108
- :param function: Method to wrap.
109
- :param cache: Whether to cache the self object.
110
+ :param d: Optional inner dictionary representing the hierarchy level.
111
+ :yield: Each `_InjectSelfMeta` subclass found in the hierarchy.
110
112
  """
113
+ if d is None:
114
+ d = _inject_self_cls[cls]
111
115
 
112
- self.cache = self.init_kwargs = None
116
+ for k, v in d.items():
117
+ yield k
118
+ yield from cls._flatten_cls(v)
113
119
 
114
- if isinstance(self, inject_self.cached):
115
- self.cache = True
120
+ def __instancecheck__(cls, instance: Any) -> bool:
121
+ """Allow isinstance() checks to succeed for any flattened subclass."""
122
+ return any(type.__instancecheck__(t, instance) for t in cls._subclasses)
116
123
 
117
- self.function = function
124
+ def __subclasscheck__(cls, subclass: type) -> bool:
125
+ """Allow issubclass() checks to succeed for any flattened subclass."""
126
+ return any(type.__subclasscheck__(t, subclass) for t in cls._subclasses)
118
127
 
119
- self.signature = self.first_key = self.init_kwargs = None
120
128
 
121
- self.args = tuple[Any]()
122
- self.kwargs = dict[str, Any]()
129
+ _T = TypeVar("_T")
130
+ _T0 = TypeVar("_T0")
123
131
 
124
- self.clean_kwargs = False
132
+ _T_co = TypeVar("_T_co", covariant=True)
133
+ _T0_co = TypeVar("_T0_co", covariant=True)
134
+ _T1_co = TypeVar("_T1_co", covariant=True)
125
135
 
126
- def __get__(
127
- self,
128
- class_obj: type[T] | T | None,
129
- class_type: type[T | type[T]] | Any, # type: ignore[valid-type]
130
- ) -> injected_self_func[T_co, P, R_co]:
131
- if not self.signature or not self.first_key:
132
- self.signature = Signature.from_callable(self.function, eval_str=True)
133
- self.first_key = next(iter(list(self.signature.parameters.keys())), None)
134
-
135
- if isinstance(self, inject_self.init_kwargs):
136
- from ..exceptions import CustomValueError
136
+ _R_co = TypeVar("_R_co", covariant=True)
137
+ _R0_co = TypeVar("_R0_co", covariant=True)
138
+ _R1_co = TypeVar("_R1_co", covariant=True)
137
139
 
138
- if 4 not in {x.kind for x in self.signature.parameters.values()}:
139
- raise CustomValueError(
140
- "This function hasn't got any kwargs!", "inject_self.init_kwargs", self.function
141
- )
142
-
143
- self.init_kwargs = list[str](k for k, x in self.signature.parameters.items() if x.kind != 4)
144
-
145
- @wraps(self.function)
146
- def _wrapper(*args: Any, **kwargs: Any) -> Any:
147
- first_arg = (args[0] if args else None) or (kwargs.get(self.first_key, None) if self.first_key else None)
148
-
149
- if first_arg and (
150
- (is_obj := isinstance(first_arg, class_type))
151
- or isinstance(first_arg, type(class_type))
152
- or first_arg is class_type
153
- ):
154
- obj = first_arg if is_obj else first_arg()
155
- if args:
156
- args = args[1:]
157
- elif kwargs and self.first_key:
158
- kwargs.pop(self.first_key)
159
- elif class_obj is None:
160
- if self.cache:
161
- if class_type not in self_objects_cache:
162
- obj = self_objects_cache[class_type] = class_type(*self.args, **self.kwargs)
163
- else:
164
- obj = self_objects_cache[class_type]
165
- elif self.init_kwargs:
166
- obj = class_type(
167
- *self.args, **(self.kwargs | {k: v for k, v in kwargs.items() if k not in self.init_kwargs})
168
- )
169
- if self.clean_kwargs:
170
- kwargs = {k: v for k, v in kwargs.items() if k in self.init_kwargs}
171
- else:
172
- obj = class_type(*self.args, **self.kwargs)
140
+ _T_Any = TypeVar("_T_Any", default=Any)
141
+ _T0_Any = TypeVar("_T0_Any", default=Any)
142
+
143
+ _P = ParamSpec("_P")
144
+ _P0 = ParamSpec("_P0")
145
+ _P1 = ParamSpec("_P1")
146
+
147
+
148
+ class _InjectedSelfFunc(Protocol[_T_co, _P, _R_co]):
149
+ """
150
+ Protocol defining the callable interface for wrapped functions under `inject_self`.
151
+
152
+ This allows the injected function to be called in any of the following forms:
153
+ - As a normal function: `f(*args, **kwargs)`
154
+ - As a bound method: `f(self, *args, **kwargs)`
155
+ - As a class method: `f(cls, *args, **kwargs)`
156
+ """
157
+
158
+ @overload
159
+ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ...
160
+ @overload
161
+ def __call__(_self, self: _T_co, /, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... # type: ignore[misc] # noqa: N805
162
+ @overload
163
+ def __call__(self, cls: type[_T_co], /, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: ... # pyright: ignore[reportGeneralTypeIssues]
164
+
165
+
166
+ _self_objects_cache = dict[type[Any], Any]()
167
+
168
+
169
+ class _InjectSelfBase(Generic[_T_co, _P, _R_co]):
170
+ """
171
+ Base descriptor implementation for `inject_self`.
172
+ """
173
+
174
+ __isabstractmethod__ = False
175
+ __slots__ = ("_function", "_init_signature", "_signature", "args", "kwargs")
176
+
177
+ _signature: Signature | None
178
+ _init_signature: Signature | None
179
+
180
+ def __init__(self, function: Callable[Concatenate[_T_co, _P], _R_co], /, *args: Any, **kwargs: Any) -> None:
181
+ """
182
+ Initialize the inject_self descriptor.
183
+
184
+ :param function: The function or method to wrap.
185
+ :param *args: Positional arguments to pass when instantiating the target class.
186
+ :param **kwargs: Keyword arguments to pass when instantiating the target class.
187
+ """
188
+ self._function = function
189
+
190
+ self._signature = self._init_signature = None
191
+
192
+ self.args = args
193
+ self.kwargs = kwargs
194
+
195
+ def __get__(self, instance: Any | None, owner: type | None = None) -> _InjectedSelfFunc[_T_co, _P, _R_co]: # pyright: ignore[reportGeneralTypeIssues]
196
+ """
197
+ Return a wrapped callable that automatically injects an instance as the first argument when called.
198
+ """
199
+ if owner is None:
200
+ owner = type(instance)
201
+
202
+ @wraps(self._function)
203
+ def _wrapper(*args: Any, **kwargs: Any) -> _R_co:
204
+ """
205
+ Call wrapper that performs the actual injection of `self`.
206
+ """
207
+ if args and isinstance((first_arg := args[0]), (owner, type(owner))):
208
+ # Instance or class explicitly provided as first argument
209
+ obj = first_arg if isinstance(first_arg, owner) else first_arg() # type: ignore[operator]
210
+ args = args[1:]
211
+ elif instance is None:
212
+ # Accessed via class
213
+ obj, kwargs = self._handle_class_access(owner, kwargs)
173
214
  else:
174
- obj = class_obj
215
+ # Accessed via instance
216
+ obj = instance
175
217
 
176
- return self.function(obj, *args, **kwargs) # type: ignore
218
+ return self._function(obj, *args, **kwargs) # type: ignore[arg-type]
177
219
 
178
220
  return _wrapper
179
221
 
180
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co:
181
- return self.__get__(None, self)(*args, **kwargs)
222
+ def _handle_class_access[T](self, owner: type[T], kwargs: dict[str, Any]) -> tuple[T, dict[str, Any]]:
223
+ """
224
+ Handle logic when the descriptor is accessed from the class level.
225
+
226
+ :param owner: The class object owning the descriptor.
227
+ :param kwargs: Keyword arguments passed to the wrapped function.
228
+ :return: A tuple of `(self_object, updated_kwargs)`.
229
+ """
230
+ if isinstance(self, inject_self.cached):
231
+ # Cached instance creation
232
+ try:
233
+ return _self_objects_cache[owner], kwargs
234
+ except KeyError:
235
+ return _self_objects_cache.setdefault(owner, owner()), kwargs
236
+
237
+ if isinstance(self, (inject_self.init_kwargs, inject_self.init_kwargs.clean)):
238
+ # Constructor accepts forwarded kwargs
239
+ has_kwargs = any(
240
+ param.kind in (param.VAR_KEYWORD, param.KEYWORD_ONLY)
241
+ for param in self.__signature__.parameters.values()
242
+ )
243
+
244
+ if not has_kwargs:
245
+ from ..exceptions import CustomValueError
246
+
247
+ raise CustomValueError(
248
+ f"Function {self._function.__name__} doesn't accept keyword arguments.",
249
+ "inject_self.init_kwargs",
250
+ self._function,
251
+ )
252
+
253
+ if not self._init_signature:
254
+ self._init_signature = Signature.from_callable(owner)
255
+
256
+ init_kwargs = self.kwargs | {k: kwargs[k] for k in kwargs.keys() & self._init_signature.parameters.keys()}
257
+
258
+ obj = owner(*self.args, **init_kwargs)
259
+
260
+ if isinstance(self, inject_self.init_kwargs.clean):
261
+ # Clean up forwarded kwargs
262
+ kwargs = {k: v for k, v in kwargs.items() if k not in self._init_signature.parameters}
263
+
264
+ return obj, kwargs
265
+
266
+ return owner(*self.args, **self.kwargs), kwargs
267
+
268
+ def __call__(self_, self: _T_co, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: # type: ignore[misc] # noqa: N805
269
+ return self_.__get__(self, type(self))(*args, **kwargs)
270
+
271
+ @property
272
+ def __func__(self) -> Callable[Concatenate[_T_co, _P], _R_co]:
273
+ """Return the original wrapped function."""
274
+ return self._function
182
275
 
183
276
  @property
184
277
  def __signature__(self) -> Signature:
185
- return Signature.from_callable(self.function)
278
+ """Return (and cache) the signature of the wrapped function."""
279
+ if not self._signature:
280
+ self._signature = Signature.from_callable(self._function)
281
+ return self._signature
186
282
 
187
283
  @classmethod
188
- def with_args(
284
+ def with_args[T0, **P0, R0](
189
285
  cls, *args: Any, **kwargs: Any
190
- ) -> Callable[[Callable[Concatenate[T0_co, P0], R0_co]], inject_self[T0_co, P0, R0_co]]:
191
- """Provide custom args to instantiate the ``self`` object with."""
286
+ ) -> Callable[[Callable[Concatenate[T0, P0], R0]], inject_self[T0, P0, R0]]:
287
+ """
288
+ Decorator factory to construct an `inject_self` or subclass (`cached`, `init_kwargs`, etc.)
289
+ with specific instantiation arguments.
290
+ """
192
291
 
292
+ # TODO: The precise subclass type cannot be expressed yet when the class is itself generic.
193
293
  def _wrapper(function: Callable[Concatenate[T0, P0], R0]) -> inject_self[T0, P0, R0]:
194
- inj = cls(function) # type: ignore
195
- inj.args = args
196
- inj.kwargs = kwargs
197
- return inj # type: ignore
294
+ return cls(function, *args, **kwargs) # type: ignore[return-value, arg-type]
198
295
 
199
296
  return _wrapper
200
297
 
201
298
 
202
- class inject_self(inject_self_base[T_co, P, R_co]):
203
- """Wrap a method so it always has a constructed ``self`` provided to it."""
299
+ class inject_self(_InjectSelfBase[_T_co, _P, _R_co], metaclass=_InjectSelfMeta):
300
+ """
301
+ Descriptor that ensures the wrapped function always has a constructed `self`.
302
+
303
+ When accessed via a class, it will automatically instantiate an object before calling the function.
304
+ When accessed via an instance, it simply binds.
305
+
306
+ Subclasses such as `cached`, `init_kwargs`, and `init_kwargs.clean`
307
+ define variations in how the injected object is created or reused.
308
+ """
309
+
310
+ __slots__ = ()
204
311
 
205
- class cached(inject_self_base[T0_co, P0, R0_co]):
312
+ class cached(_InjectSelfBase[_T0_co, _P0, _R0_co], metaclass=_InjectSelfMeta):
206
313
  """
207
- Wrap a method so it always has a constructed ``self`` provided to it.
208
- Once ``self`` is constructed, it will be reused.
314
+ Variant of `inject_self` that caches the constructed instance.
315
+
316
+ The first time the method is accessed via the class, a `self` object is created and stored.
317
+ Subsequent calls reuse it.
209
318
  """
210
319
 
211
- class property(Generic[T1_co, R1_co]):
212
- def __init__(self, function: Callable[[T1_co], R1_co]) -> None:
213
- self.function = inject_self(function)
320
+ __slots__ = ()
321
+
322
+ class property(Generic[_T1_co, _P1, _R1_co], metaclass=_InjectSelfMeta):
323
+ """Property variant of `inject_self.cached` that auto-calls the wrapped method."""
214
324
 
215
- def __get__(self, class_obj: type[T1_co] | T1_co | None, class_type: type[T1_co] | T1_co) -> R1_co:
216
- return self.function.__get__(class_obj, class_type)()
325
+ __slots__ = ("__func__",)
217
326
 
218
- class init_kwargs(inject_self_base[T0_co, P0, R0_co]):
327
+ def __init__(self, function: Callable[[_T1_co], _R1_co], /) -> None:
328
+ self.__func__ = inject_self.cached(function)
329
+
330
+ def __get__(self, instance: _T1_co | None, owner: type[_T1_co]) -> _R1_co: # pyright: ignore[reportGeneralTypeIssues]
331
+ """Return the result of calling the cached method without arguments."""
332
+ return self.__func__.__get__(instance, owner)()
333
+
334
+ class init_kwargs(_InjectSelfBase[_T0_co, _P0, _R0_co], metaclass=_InjectSelfMeta):
219
335
  """
220
- Wrap a method so it always has a constructed ``self`` provided to it.
221
- When constructed, kwargs to the function will be passed to the constructor.
336
+ Variant of `inject_self` that forwards function keyword arguments to the class constructor
337
+ when instantiating `self`.
222
338
  """
223
339
 
224
- @classmethod
225
- def clean(cls, function: Callable[Concatenate[T1_co, P1], R1_co]) -> inject_self[T1_co, P1, R1_co]:
226
- """Wrap a method, pass kwargs to the constructor and remove them from actual **kwargs."""
227
- inj = cls(function) # type: ignore
228
- inj.clean_kwargs = True
229
- return inj # type: ignore
340
+ __slots__ = ()
230
341
 
231
- class property(Generic[T0_co, R0_co]):
232
- def __init__(self, function: Callable[[T0_co], R0_co]) -> None:
233
- self.function = inject_self(function)
342
+ class clean(_InjectSelfBase[_T1_co, _P1, _R1_co], metaclass=_InjectSelfMeta):
343
+ """
344
+ Variant of `inject_self.init_kwargs` that removes any forwarded kwargs from the final function call
345
+ after using them for construction.
346
+ """
234
347
 
235
- def __get__(self, class_obj: type[T0_co] | T0_co | None, class_type: type[T0_co] | T0_co) -> R0_co:
236
- return self.function.__get__(class_obj, class_type)()
348
+ __slots__ = ()
237
349
 
350
+ class property(Generic[_T0_co, _R0_co], metaclass=_InjectSelfMeta):
351
+ """Property variant of `inject_self` that auto-calls the wrapped method."""
238
352
 
239
- class inject_kwargs_params_base_func(Generic[T_co, P, R_co]):
240
- def __call__(self: T_co, *args: P.args, **kwargs: P.kwargs) -> R_co:
241
- raise NotImplementedError
353
+ __slots__ = ("__func__",)
242
354
 
355
+ def __init__(self, function: Callable[[_T0_co], _R0_co], /) -> None:
356
+ self.__func__ = inject_self(function)
243
357
 
244
- class inject_kwargs_params_base(Generic[T_co, P, R_co]):
245
- signature: Signature | None
358
+ def __get__(self, instance: _T0_co | None, owner: type[_T0_co]) -> _R0_co: # pyright: ignore[reportGeneralTypeIssues]
359
+ """Return the result of calling the injected method without arguments."""
360
+ return self.__func__.__get__(instance, owner)()
246
361
 
247
- _kwargs_name = "kwargs"
248
362
 
249
- def __init__(self, function: Callable[Concatenate[T_co, P], R_co]) -> None:
250
- self.function = function
363
+ class _InjectKwargsParamsBase(Generic[_T_co, _P, _R_co]):
364
+ """
365
+ Base descriptor implementation for `inject_kwargs_params`.
366
+ """
251
367
 
252
- self.signature = None
368
+ __isabstractmethod__ = False
369
+ __slots__ = ("_function", "_signature")
253
370
 
254
- def __get__(self, class_obj: T, class_type: type[T]) -> inject_kwargs_params_base_func[T_co, P, R_co]:
255
- if not self.signature:
256
- self.signature = Signature.from_callable(self.function, eval_str=True)
371
+ _kwargs_name = "kwargs"
372
+ _signature: Signature | None
257
373
 
258
- if (
259
- isinstance(self, inject_kwargs_params.add_to_kwargs) # type: ignore[arg-type]
260
- and (4 not in {x.kind for x in self.signature.parameters.values()})
261
- ):
262
- from ..exceptions import CustomValueError
374
+ def __init__(self, func: Callable[Concatenate[_T_co, _P], _R_co], /) -> None:
375
+ """
376
+ Initialize the inject_kwargs_params descriptor.
263
377
 
264
- raise CustomValueError(
265
- "This function hasn't got any kwargs!", "inject_kwargs_params.add_to_kwargs", self.function
266
- )
378
+ :param function: The target function or method whose parameters will be injected
379
+ from the instance's `self.kwargs` mapping.
380
+ """
381
+ self._function = func
382
+ self._signature = None
267
383
 
268
- this = self
384
+ @overload
385
+ def __get__(self, instance: None, owner: type) -> Self: ...
386
+ @overload
387
+ def __get__(self, instance: Any, owner: type | None = None) -> Callable[_P, _R_co]: ...
388
+ def __get__(self, instance: Any | None, owner: type | None = None) -> Any:
389
+ """
390
+ Descriptor binding logic.
269
391
 
270
- @wraps(self.function)
271
- def _wrapper(self: Any, *_args: Any, **kwargs: Any) -> R_co:
272
- assert this.signature
392
+ When accessed via an instance, returns a wrapped function that injects values from `instance.<_kwargs_name>`
393
+ into matching function parameters.
273
394
 
274
- if class_obj and not isinstance(self, class_type):
275
- _args = (self, *_args)
276
- self = class_obj
395
+ When accessed via the class, returns the descriptor itself.
396
+ """
397
+ if instance is None:
398
+ return self
277
399
 
278
- if not hasattr(self, this._kwargs_name):
279
- from ..exceptions import CustomRuntimeError
400
+ if not hasattr(instance, self._kwargs_name):
401
+ from ..exceptions import CustomRuntimeError
280
402
 
281
- raise CustomRuntimeError(
282
- f'This class doesn\'t have any "{this._kwargs_name}" attribute!', reason=self.__class__
283
- )
403
+ raise CustomRuntimeError(
404
+ f'Missing attribute "{self._kwargs_name}" on {type(instance).__name__}.', owner, self.__class__
405
+ )
284
406
 
285
- this_kwargs = self.kwargs.copy()
286
- args, n_args = list(_args), len(_args)
407
+ @wraps(self._function)
408
+ def wrapper(*args: Any, **kwargs: Any) -> _R_co:
409
+ """
410
+ Wrapper that performs parameter injection before calling the wrapped function.
411
+ """
412
+ injectable_kwargs = dict(getattr(instance, self._kwargs_name))
287
413
 
288
- for i, (key, value) in enumerate(this.signature.parameters.items()):
289
- if key not in this_kwargs:
290
- continue
414
+ if not injectable_kwargs:
415
+ return self._function(instance, *args, **kwargs)
291
416
 
292
- kw_value = this_kwargs.pop(key)
417
+ args_list = [instance, *args]
418
+ kwargs = kwargs.copy()
293
419
 
294
- if value.default is empty_param:
420
+ for i, (name, param) in enumerate(self.__signature__.parameters.items()):
421
+ if name not in injectable_kwargs:
295
422
  continue
296
423
 
297
- if i < n_args:
298
- if args[i] != value.default:
299
- continue
424
+ value_from_kwargs = injectable_kwargs.pop(name)
300
425
 
301
- args[i] = kw_value
426
+ if i < len(args_list):
427
+ # Positional arg case
428
+ if args_list[i] == param.default:
429
+ args_list[i] = value_from_kwargs
302
430
  else:
303
- if key in kwargs and kwargs[key] != value.default:
304
- continue
431
+ # Keyword arg case
432
+ if kwargs.get(name, param.default) == param.default:
433
+ kwargs[name] = value_from_kwargs
305
434
 
306
- kwargs[key] = kw_value
435
+ # Merge leftover kwargs if subclass allows
436
+ if isinstance(self, inject_kwargs_params.add_to_kwargs):
437
+ kwargs |= injectable_kwargs
307
438
 
308
- if isinstance(this, inject_kwargs_params.add_to_kwargs): # type: ignore[arg-type]
309
- kwargs |= this_kwargs
439
+ return self._function(*tuple(args_list), **kwargs)
310
440
 
311
- return this.function(self, *args, **kwargs)
441
+ return wrapper
312
442
 
313
- return _wrapper # type: ignore
443
+ def __call__(self_, self: _T_co, *args: _P.args, **kwargs: _P.kwargs) -> _R_co: # type: ignore[misc] # noqa: N805
444
+ return self_.__get__(self, type(self))(*args, **kwargs)
314
445
 
315
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co:
316
- return self.__get__(None, self)(*args, **kwargs) # type: ignore
446
+ @property
447
+ def __func__(self) -> Callable[Concatenate[_T_co, _P], _R_co]:
448
+ """Return the original wrapped function."""
449
+ return self._function
317
450
 
318
451
  @property
319
452
  def __signature__(self) -> Signature:
320
- return Signature.from_callable(self.function)
453
+ """Return (and cache) the signature of the wrapped function."""
454
+ if not self._signature:
455
+ self._signature = Signature.from_callable(self._function)
456
+ return self._signature
321
457
 
322
458
  @classmethod
323
- def with_name(cls, kwargs_name: str) -> type[inject_kwargs_params]: # type: ignore
324
- class _inner(inject_kwargs_params): # type: ignore
325
- _kwargs_name = kwargs_name
459
+ def with_name[T0, **P0, R0](
460
+ cls, kwargs_name: str = "kwargs"
461
+ ) -> Callable[[Callable[Concatenate[T0, P0], R0]], inject_kwargs_params[T0, P0, R0]]:
462
+ """
463
+ Decorator factory that creates a subclass of `inject_kwargs_params` with a custom name
464
+ for the keyword argument store.
465
+ """
466
+ ns = cls.__dict__.copy()
467
+ ns["_kwargs_name"] = kwargs_name
468
+
469
+ custom_cls = type(cls.__name__, cls.__bases__, ns)
326
470
 
327
- return _inner
471
+ # TODO: The precise subclass type cannot be expressed yet when the class is itself generic.
472
+ def _wrapper(function: Callable[Concatenate[T0, P0], R0]) -> inject_kwargs_params[T0, P0, R0]:
473
+ return custom_cls(function) # pyright: ignore[reportReturnType, reportArgumentType]
328
474
 
475
+ return _wrapper
329
476
 
330
- if TYPE_CHECKING: # love you mypy...
331
477
 
332
- class _add_to_kwargs:
333
- def __call__(self, func: F1) -> F1: ...
478
+ class inject_kwargs_params(_InjectKwargsParamsBase[_T_co, _P, _R_co]):
479
+ """
480
+ Descriptor that injects parameters into functions based on an instance's keyword mapping.
334
481
 
335
- class _inject_kwargs_params:
336
- def __call__(self, func: F0) -> F0: ...
482
+ When a method wrapped with `@inject_kwargs_params` is called, the descriptor inspects the function's signature
483
+ and replaces any arguments matching keys in `self.kwargs` (or another mapping defined by `_kwargs_name`)
484
+ if their values equal the parameter's default.
485
+ """
337
486
 
338
- add_to_kwargs = _add_to_kwargs()
487
+ __slots__ = ()
339
488
 
340
- inject_kwargs_params = _inject_kwargs_params()
341
- else:
489
+ class add_to_kwargs(_InjectKwargsParamsBase[_T0_co, _P0, _R0_co]):
490
+ """
491
+ Variant of `inject_kwargs_params` that merges unused entries from `self.kwargs` into the keyword arguments
492
+ passed to the target function.
342
493
 
343
- class inject_kwargs_params(Generic[T, P, R], inject_kwargs_params_base[T, P, R]):
344
- class add_to_kwargs(Generic[T0, P0, R0], inject_kwargs_params_base[T0, P0, R0]): ...
494
+ This allows additional context or configuration values to be forwarded without requiring explicit parameters.
495
+ """
345
496
 
497
+ __slots__ = ()
346
498
 
347
- class complex_hash(Generic[T]):
348
- """
349
- Decorator for classes to add a ``__hash__`` method to them.
350
499
 
351
- Especially useful for NamedTuples.
352
- """
500
+ class _ComplexHash[**P, R]:
501
+ __slots__ = "func"
353
502
 
354
- def __new__(cls, class_type: T) -> T: # type: ignore
355
- class inner_class_type(class_type): # type: ignore
356
- def __hash__(self) -> int:
357
- return complex_hash.hash(self.__class__.__name__, *(getattr(self, key) for key in self.__annotations__))
503
+ def __init__(self, func: Callable[P, R]) -> None:
504
+ self.func = func
358
505
 
359
- return inner_class_type # type: ignore
506
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
507
+ return self.func(*args, **kwargs)
360
508
 
361
509
  @staticmethod
362
510
  def hash(*args: Any) -> int:
@@ -380,7 +528,24 @@ class complex_hash(Generic[T]):
380
528
  return hash("_".join(values))
381
529
 
382
530
 
383
- def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[type[T]]:
531
+ @_ComplexHash
532
+ def complex_hash[T](cls: type[T]) -> type[T]:
533
+ """
534
+ Decorator for classes to add a ``__hash__`` method to them.
535
+
536
+ Especially useful for NamedTuples.
537
+ """
538
+
539
+ def __hash__(self: T) -> int: # noqa: N807
540
+ return complex_hash.hash(self.__class__.__name__, *(getattr(self, key) for key in self.__annotations__))
541
+
542
+ ns = cls.__dict__.copy()
543
+ ns["__hash__"] = __hash__
544
+
545
+ return type(cls.__name__, (cls,), ns) # pyright: ignore[reportReturnType]
546
+
547
+
548
+ def get_subclasses[T](family: type[T], exclude: Sequence[type[T]] = []) -> list[type[T]]:
384
549
  """
385
550
  Get all subclasses of a given type.
386
551
 
@@ -391,7 +556,7 @@ def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[typ
391
556
  :return: List of all subclasses of "family".
392
557
  """
393
558
 
394
- def _subclasses(cls: type[T]) -> Generator[type[T], None, None]:
559
+ def _subclasses(cls: type[T]) -> Iterator[type[T]]:
395
560
  for subclass in cls.__subclasses__():
396
561
  yield from _subclasses(subclass)
397
562
  if subclass in exclude:
@@ -401,24 +566,20 @@ def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[typ
401
566
  return list(set(_subclasses(family)))
402
567
 
403
568
 
404
- _T_Any = TypeVar("_T_Any", default=Any)
405
- _T0_Any = TypeVar("_T0_Any", default=Any)
406
-
407
-
408
- class classproperty_base(Generic[T, R_co, _T_Any]):
569
+ class classproperty_base(Generic[_T, _R_co, _T_Any]):
409
570
  __isabstractmethod__: bool = False
410
571
 
411
- fget: Callable[[type[T]], R_co]
412
- fset: Callable[Concatenate[type[T], _T_Any, ...], None] | None
413
- fdel: Callable[[type[T]], None] | None
572
+ fget: Callable[[type[_T]], _R_co]
573
+ fset: Callable[Concatenate[type[_T], _T_Any, ...], None] | None
574
+ fdel: Callable[[type[_T]], None] | None
414
575
 
415
576
  def __init__(
416
577
  self,
417
- fget: Callable[[type[T]], R_co] | classmethod[T, ..., R_co],
418
- fset: Callable[Concatenate[type[T], _T_Any, ...], None]
419
- | classmethod[T, Concatenate[_T_Any, ...], None]
578
+ fget: Callable[[type[_T]], _R_co] | classmethod[_T, ..., _R_co],
579
+ fset: Callable[Concatenate[type[_T], _T_Any, ...], None]
580
+ | classmethod[_T, Concatenate[_T_Any, ...], None]
420
581
  | None = None,
421
- fdel: Callable[[type[T]], None] | classmethod[T, ..., None] | None = None,
582
+ fdel: Callable[[type[_T]], None] | classmethod[_T, ..., None] | None = None,
422
583
  doc: str | None = None,
423
584
  ) -> None:
424
585
  self.fget = fget.__func__ if isinstance(fget, classmethod) else fget
@@ -431,69 +592,67 @@ class classproperty_base(Generic[T, R_co, _T_Any]):
431
592
  def __set_name__(self, owner: object, name: str) -> None:
432
593
  self.__name__ = name
433
594
 
434
- def _get_cache(self, type_: type[T]) -> dict[str, Any]:
595
+ def _get_cache(self, owner: type[_T]) -> dict[str, Any]:
435
596
  cache_key = getattr(self, "cache_key")
436
597
 
437
- if not hasattr(type_, cache_key):
438
- setattr(type_, cache_key, {})
598
+ if not hasattr(owner, cache_key):
599
+ setattr(owner, cache_key, {})
439
600
 
440
- return getattr(type_, cache_key)
601
+ return getattr(owner, cache_key)
441
602
 
442
- def __get__(self, obj: T | None, type_: type[T] | None = None) -> R_co:
443
- if type_ is None and obj is not None:
444
- type_ = type(obj)
445
- elif type_ is None:
446
- raise NotImplementedError("Both obj and type_ are None")
603
+ def __get__(self, instance: Any | None, owner: type | None = None) -> _R_co:
604
+ if owner is None:
605
+ owner = type(instance)
447
606
 
448
607
  if not isinstance(self, classproperty.cached):
449
- return self.fget(type_)
608
+ return self.fget(owner)
450
609
 
451
- if self.__name__ in (cache := self._get_cache(type_)):
610
+ if self.__name__ in (cache := self._get_cache(owner)):
452
611
  return cache[self.__name__]
453
612
 
454
- value = self.fget(type_)
613
+ value = self.fget(owner)
455
614
  cache[self.__name__] = value
456
615
  return value
457
616
 
458
- def __set__(self, obj: T, value: _T_Any) -> None:
617
+ def __set__(self, instance: Any, value: _T_Any) -> None:
459
618
  if not self.fset:
460
619
  raise AttributeError(
461
- f'classproperty with getter "{self.__name__}" of "{obj.__class__.__name__}" object has no setter.'
620
+ f'classproperty with getter "{self.__name__}" of "{instance.__class__.__name__}" object has no setter.'
462
621
  )
463
622
 
464
- type_ = type(obj)
623
+ owner = type(instance)
465
624
 
466
625
  if not isinstance(self, classproperty.cached):
467
- return self.fset(type_, value)
626
+ return self.fset(owner, value)
468
627
 
469
- if self.__name__ in (cache := self._get_cache(type_)):
628
+ if self.__name__ in (cache := self._get_cache(owner)):
470
629
  del cache[self.__name__]
471
630
 
472
- self.fset(type_, value)
631
+ self.fset(owner, value)
473
632
 
474
- def __delete__(self, obj: T) -> None:
633
+ def __delete__(self, instance: Any) -> None:
475
634
  if not self.fdel:
476
635
  raise AttributeError(
477
- f'classproperty with getter "{self.__name__}" of "{obj.__class__.__name__}" object has no deleter.'
636
+ f'classproperty with getter "{self.__name__}" of "{instance.__class__.__name__}" object has no deleter.'
478
637
  )
479
638
 
480
- type_ = type(obj)
639
+ owner = type(instance)
481
640
 
482
641
  if not isinstance(self, classproperty.cached):
483
- return self.fdel(type_)
642
+ return self.fdel(owner)
484
643
 
485
- if self.__name__ in (cache := self._get_cache(type_)):
644
+ if self.__name__ in (cache := self._get_cache(owner)):
486
645
  del cache[self.__name__]
487
646
 
488
- self.fdel(type_)
647
+ self.fdel(owner)
489
648
 
490
649
 
491
- class classproperty(classproperty_base[T, R_co, _T_Any]):
650
+ class classproperty(classproperty_base[_T, _R_co, _T_Any]):
492
651
  """
493
652
  A combination of `classmethod` and `property`.
494
653
  """
495
654
 
496
- class cached(classproperty_base[T0, R0_co, _T0_Any]):
655
+ class cached(classproperty_base[_T0, _R0_co, _T0_Any]):
497
656
  """
498
657
  A combination of `classmethod` and `property`.
499
658
 
@@ -526,7 +685,7 @@ class classproperty(classproperty_base[T, R_co, _T_Any]):
526
685
  del cache[name]
527
686
 
528
687
 
529
- class cachedproperty(property, Generic[R_co, _T_Any]):
688
+ class cachedproperty(property, Generic[_R_co, _T_Any]):
530
689
  """
531
690
  Wrapper for a one-time get property, that will be cached.
532
691
 
@@ -547,17 +706,17 @@ class cachedproperty(property, Generic[R_co, _T_Any]):
547
706
 
548
707
  def __init__(
549
708
  self,
550
- fget: Callable[[Any], R_co],
709
+ fget: Callable[[Any], _R_co],
551
710
  fset: Callable[[Any, _T_Any], None] | None = None,
552
711
  fdel: Callable[[Any], None] | None = None,
553
712
  doc: str | None = None,
554
713
  ) -> None: ...
555
714
 
556
- def getter(self, fget: Callable[..., R_co]) -> cachedproperty[R_co, _T_Any]: ...
715
+ def getter(self, fget: Callable[..., _R_co]) -> cachedproperty[_R_co, _T_Any]: ...
557
716
 
558
- def setter(self, fset: Callable[[Any, _T_Any], None]) -> cachedproperty[R_co, _T_Any]: ...
717
+ def setter(self, fset: Callable[[Any, _T_Any], None]) -> cachedproperty[_R_co, _T_Any]: ...
559
718
 
560
- def deleter(self, fdel: Callable[..., None]) -> cachedproperty[R_co, _T_Any]: ...
719
+ def deleter(self, fdel: Callable[..., None]) -> cachedproperty[_R_co, _T_Any]: ...
561
720
 
562
721
  if sys.version_info < (3, 13):
563
722
 
@@ -569,7 +728,7 @@ class cachedproperty(property, Generic[R_co, _T_Any]):
569
728
  def __get__(self, instance: None, owner: type | None = None) -> Self: ...
570
729
 
571
730
  @overload
572
- def __get__(self, instance: Any, owner: type | None = None) -> R_co: ...
731
+ def __get__(self, instance: Any, owner: type | None = None) -> _R_co: ...
573
732
 
574
733
  def __get__(self, instance: Any, owner: type | None = None) -> Any:
575
734
  if instance is None:
@@ -629,18 +788,20 @@ class cachedproperty(property, Generic[R_co, _T_Any]):
629
788
  class KwargsNotNone(KwargsT):
630
789
  """Remove all None objects from this kwargs dict."""
631
790
 
632
- if not TYPE_CHECKING:
633
-
634
- def __new__(cls, *args: Any, **kwargs: Any) -> Self:
635
- return KwargsT(**{key: value for key, value in KwargsT(*args, **kwargs).items() if value is not None})
791
+ @copy_signature(KwargsT.__init__)
792
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
793
+ super().__init__({key: value for key, value in dict(*args, **kwargs).items() if value is not None})
636
794
 
637
795
 
638
796
  class SingletonMeta(type):
639
797
  _instances: ClassVar[dict[type[Any], Any]] = {}
640
798
  _singleton_init: bool
641
799
 
642
- def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any) -> SingletonMeta:
643
- return type.__new__(cls, name, bases, namespace | {"_singleton_init": kwargs.pop("init", False)})
800
+ def __new__[MetaSelf: SingletonMeta](
801
+ mcls: type[MetaSelf], name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any
802
+ ) -> MetaSelf:
803
+ namespace["_singleton_init"] = kwargs.pop("init", False)
804
+ return super().__new__(mcls, name, bases, namespace, **kwargs)
644
805
 
645
806
  def __call__(cls, *args: Any, **kwargs: Any) -> SingletonMeta:
646
807
  if cls not in cls._instances:
@@ -654,34 +815,7 @@ class SingletonMeta(type):
654
815
  class Singleton(metaclass=SingletonMeta):
655
816
  """Handy class to inherit to have the SingletonMeta metaclass."""
656
817
 
657
-
658
- class to_singleton_impl:
659
- _ts_args = tuple[str, ...]()
660
- _ts_kwargs: Mapping[str, Any] = {}
661
- _add_classes = tuple[type, ...]()
662
-
663
- def __new__(_cls, cls: type[T]) -> T: # type: ignore
664
- if _cls._add_classes:
665
-
666
- class rcls(cls, *_cls._add_classes): # type: ignore
667
- ...
668
- else:
669
- rcls = cls # type: ignore
670
-
671
- return rcls(*_cls._ts_args, **_cls._ts_kwargs)
672
-
673
- @classmethod
674
- def with_args(cls, *args: Any, **kwargs: Any) -> type[to_singleton]:
675
- class _inner_singl(cls): # type: ignore
676
- _ts_args = args
677
- _ts_kwargs = kwargs
678
-
679
- return _inner_singl
680
-
681
-
682
- class to_singleton(to_singleton_impl):
683
- class as_property(to_singleton_impl):
684
- _add_classes = (property,)
818
+ __slots__ = ()
685
819
 
686
820
 
687
821
  class LinearRangeLut(Mapping[int, int]):
@@ -706,13 +840,14 @@ class LinearRangeLut(Mapping[int, int]):
706
840
  if self._misses_n > 2:
707
841
  self._ranges_idx_lut = self._ranges_idx_lut[missed_hit:] + self._ranges_idx_lut[:missed_hit]
708
842
 
709
- return idx
843
+ return idx # pyright: ignore[reportPossiblyUnboundVariable]
710
844
 
711
845
  def __len__(self) -> int:
712
846
  return len(self.ranges)
713
847
 
714
848
  def __iter__(self) -> Iterator[int]:
715
- return iter(range(len(self)))
849
+ for i in range(len(self)):
850
+ yield i
716
851
 
717
852
  def __setitem__(self, n: int, _range: range) -> NoReturn:
718
853
  raise NotImplementedError