jetpytools 1.7.4__py3-none-any.whl → 2.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 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,433 @@ 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__[T](self, instance: T | None, owner: type[T]) -> _InjectedSelfFunc[T, _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
+
200
+ @wraps(self._function)
201
+ def _wrapper(*args: Any, **kwargs: Any) -> _R_co:
202
+ """
203
+ Call wrapper that performs the actual injection of `self`.
204
+ """
205
+ if args and isinstance((first_arg := args[0]), (owner, type(owner))):
206
+ # Instance or class explicitly provided as first argument
207
+ obj = first_arg if isinstance(first_arg, owner) else first_arg() # type: ignore[operator]
208
+ args = args[1:]
209
+ elif instance is None:
210
+ # Accessed via class
211
+ obj, kwargs = self._handle_class_access(owner, kwargs)
173
212
  else:
174
- obj = class_obj
213
+ # Accessed via instance
214
+ obj = instance
175
215
 
176
- return self.function(obj, *args, **kwargs) # type: ignore
216
+ return self._function(obj, *args, **kwargs) # type: ignore[arg-type]
177
217
 
178
218
  return _wrapper
179
219
 
180
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co:
181
- return self.__get__(None, self)(*args, **kwargs)
220
+ def _handle_class_access[T](self, owner: type[T], kwargs: dict[str, Any]) -> tuple[T, dict[str, Any]]:
221
+ """
222
+ Handle logic when the descriptor is accessed from the class level.
223
+
224
+ :param owner: The class object owning the descriptor.
225
+ :param kwargs: Keyword arguments passed to the wrapped function.
226
+ :return: A tuple of `(self_object, updated_kwargs)`.
227
+ """
228
+ if isinstance(self, inject_self.cached):
229
+ # Cached instance creation
230
+ try:
231
+ return _self_objects_cache[owner], kwargs
232
+ except KeyError:
233
+ return _self_objects_cache.setdefault(owner, owner()), kwargs
234
+
235
+ if isinstance(self, (inject_self.init_kwargs, inject_self.init_kwargs.clean)):
236
+ # Constructor accepts forwarded kwargs
237
+ has_kwargs = any(
238
+ param.kind in (param.VAR_KEYWORD, param.KEYWORD_ONLY)
239
+ for param in self.__signature__.parameters.values()
240
+ )
241
+
242
+ if not has_kwargs:
243
+ from ..exceptions import CustomValueError
244
+
245
+ raise CustomValueError(
246
+ f"Function {self._function.__name__} doesn't accept keyword arguments.",
247
+ "inject_self.init_kwargs",
248
+ self._function,
249
+ )
250
+
251
+ if not self._init_signature:
252
+ self._init_signature = Signature.from_callable(owner)
253
+
254
+ init_kwargs = self.kwargs | {k: kwargs[k] for k in kwargs.keys() & self._init_signature.parameters.keys()}
255
+
256
+ obj = owner(*self.args, **init_kwargs)
257
+
258
+ if isinstance(self, inject_self.init_kwargs.clean):
259
+ # Clean up forwarded kwargs
260
+ kwargs = {k: v for k, v in kwargs.items() if k not in self._init_signature.parameters}
261
+
262
+ return obj, kwargs
263
+
264
+ return owner(*self.args, **self.kwargs), kwargs
265
+
266
+ @property
267
+ def __func__(self) -> Callable[Concatenate[_T_co, _P], _R_co]:
268
+ """Return the original wrapped function."""
269
+ return self._function
182
270
 
183
271
  @property
184
272
  def __signature__(self) -> Signature:
185
- return Signature.from_callable(self.function)
273
+ """Return (and cache) the signature of the wrapped function."""
274
+ if not self._signature:
275
+ self._signature = Signature.from_callable(self._function)
276
+ return self._signature
186
277
 
187
278
  @classmethod
188
- def with_args(
279
+ def with_args[T0, **P0, R0](
189
280
  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."""
281
+ ) -> Callable[[Callable[Concatenate[T0, P0], R0]], inject_self[T0, P0, R0]]:
282
+ """
283
+ Decorator factory to construct an `inject_self` or subclass (`cached`, `init_kwargs`, etc.)
284
+ with specific instantiation arguments.
285
+ """
192
286
 
287
+ # TODO: The precise subclass type cannot be expressed yet when the class is itself generic.
193
288
  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
289
+ return cls(function, *args, **kwargs) # type: ignore[return-value, arg-type]
198
290
 
199
291
  return _wrapper
200
292
 
201
293
 
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."""
294
+ class inject_self(_InjectSelfBase[_T_co, _P, _R_co], metaclass=_InjectSelfMeta):
295
+ """
296
+ Descriptor that ensures the wrapped function always has a constructed `self`.
297
+
298
+ When accessed via a class, it will automatically instantiate an object before calling the function.
299
+ When accessed via an instance, it simply binds.
300
+
301
+ Subclasses such as `cached`, `init_kwargs`, and `init_kwargs.clean`
302
+ define variations in how the injected object is created or reused.
303
+ """
304
+
305
+ __slots__ = ()
204
306
 
205
- class cached(inject_self_base[T0_co, P0, R0_co]):
307
+ class cached(_InjectSelfBase[_T0_co, _P0, _R0_co], metaclass=_InjectSelfMeta):
206
308
  """
207
- Wrap a method so it always has a constructed ``self`` provided to it.
208
- Once ``self`` is constructed, it will be reused.
309
+ Variant of `inject_self` that caches the constructed instance.
310
+
311
+ The first time the method is accessed via the class, a `self` object is created and stored.
312
+ Subsequent calls reuse it.
209
313
  """
210
314
 
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)
315
+ __slots__ = ()
316
+
317
+ class property(Generic[_T1_co, _P1, _R1_co], metaclass=_InjectSelfMeta):
318
+ """Property variant of `inject_self.cached` that auto-calls the wrapped method."""
214
319
 
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)()
320
+ __slots__ = ("__func__",)
217
321
 
218
- class init_kwargs(inject_self_base[T0_co, P0, R0_co]):
322
+ def __init__(self, function: Callable[[_T1_co], _R1_co], /) -> None:
323
+ self.__func__ = inject_self.cached(function)
324
+
325
+ def __get__(self, instance: _T1_co | None, owner: type[_T1_co]) -> _R1_co: # pyright: ignore[reportGeneralTypeIssues]
326
+ """Return the result of calling the cached method without arguments."""
327
+ return self.__func__.__get__(instance, owner)()
328
+
329
+ class init_kwargs(_InjectSelfBase[_T0_co, _P0, _R0_co], metaclass=_InjectSelfMeta):
219
330
  """
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.
331
+ Variant of `inject_self` that forwards function keyword arguments to the class constructor
332
+ when instantiating `self`.
222
333
  """
223
334
 
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
335
+ __slots__ = ()
230
336
 
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)
337
+ class clean(_InjectSelfBase[_T1_co, _P1, _R1_co], metaclass=_InjectSelfMeta):
338
+ """
339
+ Variant of `inject_self.init_kwargs` that removes any forwarded kwargs from the final function call
340
+ after using them for construction.
341
+ """
234
342
 
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)()
343
+ __slots__ = ()
237
344
 
345
+ class property(Generic[_T0_co, _R0_co], metaclass=_InjectSelfMeta):
346
+ """Property variant of `inject_self` that auto-calls the wrapped method."""
238
347
 
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
348
+ __slots__ = ("__func__",)
242
349
 
350
+ def __init__(self, function: Callable[[_T0_co], _R0_co], /) -> None:
351
+ self.__func__ = inject_self(function)
243
352
 
244
- class inject_kwargs_params_base(Generic[T_co, P, R_co]):
245
- signature: Signature | None
353
+ def __get__(self, instance: _T0_co | None, owner: type[_T0_co]) -> _R0_co: # pyright: ignore[reportGeneralTypeIssues]
354
+ """Return the result of calling the injected method without arguments."""
355
+ return self.__func__.__get__(instance, owner)()
246
356
 
247
- _kwargs_name = "kwargs"
248
357
 
249
- def __init__(self, function: Callable[Concatenate[T_co, P], R_co]) -> None:
250
- self.function = function
358
+ class _InjectKwargsParamsBase(Generic[_T_co, _P, _R_co]):
359
+ """
360
+ Base descriptor implementation for `inject_kwargs_params`.
361
+ """
251
362
 
252
- self.signature = None
363
+ __isabstractmethod__ = False
364
+ __slots__ = ("_function", "_signature")
253
365
 
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)
366
+ _kwargs_name = "kwargs"
367
+ _signature: Signature | None
257
368
 
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
369
+ def __init__(self, func: Callable[Concatenate[_T_co, _P], _R_co], /) -> None:
370
+ """
371
+ Initialize the inject_kwargs_params descriptor.
263
372
 
264
- raise CustomValueError(
265
- "This function hasn't got any kwargs!", "inject_kwargs_params.add_to_kwargs", self.function
266
- )
373
+ :param function: The target function or method whose parameters will be injected
374
+ from the instance's `self.kwargs` mapping.
375
+ """
376
+ self._function = func
377
+ self._signature = None
267
378
 
268
- this = self
379
+ @overload
380
+ def __get__(self, instance: None, owner: type[Any]) -> Self: ...
381
+ @overload
382
+ def __get__(self, instance: Any, owner: type[Any]) -> Callable[_P, _R_co]: ...
383
+ def __get__(self, instance: Any | None, owner: type[Any]) -> Self | Callable[_P, _R_co]:
384
+ """
385
+ Descriptor binding logic.
269
386
 
270
- @wraps(self.function)
271
- def _wrapper(self: Any, *_args: Any, **kwargs: Any) -> R_co:
272
- assert this.signature
387
+ When accessed via an instance, returns a wrapped function that injects values from `instance.<_kwargs_name>`
388
+ into matching function parameters.
273
389
 
274
- if class_obj and not isinstance(self, class_type):
275
- _args = (self, *_args)
276
- self = class_obj
390
+ When accessed via the class, returns the descriptor itself.
391
+ """
392
+ if instance is None:
393
+ return self
277
394
 
278
- if not hasattr(self, this._kwargs_name):
279
- from ..exceptions import CustomRuntimeError
395
+ if not hasattr(instance, self._kwargs_name):
396
+ from ..exceptions import CustomRuntimeError
280
397
 
281
- raise CustomRuntimeError(
282
- f'This class doesn\'t have any "{this._kwargs_name}" attribute!', reason=self.__class__
283
- )
398
+ raise CustomRuntimeError(
399
+ f'Missing attribute "{self._kwargs_name}" on {type(instance).__name__}.', owner, self.__class__
400
+ )
284
401
 
285
- this_kwargs = self.kwargs.copy()
286
- args, n_args = list(_args), len(_args)
402
+ @wraps(self._function)
403
+ def wrapper(*args: Any, **kwargs: Any) -> _R_co:
404
+ """
405
+ Wrapper that performs parameter injection before calling the wrapped function.
406
+ """
407
+ injectable_kwargs = dict(getattr(instance, self._kwargs_name))
287
408
 
288
- for i, (key, value) in enumerate(this.signature.parameters.items()):
289
- if key not in this_kwargs:
290
- continue
409
+ if not injectable_kwargs:
410
+ return self._function(instance, *args, **kwargs)
291
411
 
292
- kw_value = this_kwargs.pop(key)
412
+ args_list = [instance, *args]
413
+ kwargs = kwargs.copy()
293
414
 
294
- if value.default is empty_param:
415
+ for i, (name, param) in enumerate(self.__signature__.parameters.items()):
416
+ if name not in injectable_kwargs:
295
417
  continue
296
418
 
297
- if i < n_args:
298
- if args[i] != value.default:
299
- continue
419
+ value_from_kwargs = injectable_kwargs.pop(name)
300
420
 
301
- args[i] = kw_value
421
+ if i < len(args_list):
422
+ # Positional arg case
423
+ if args_list[i] == param.default:
424
+ args_list[i] = value_from_kwargs
302
425
  else:
303
- if key in kwargs and kwargs[key] != value.default:
304
- continue
426
+ # Keyword arg case
427
+ if kwargs.get(name, param.default) == param.default:
428
+ kwargs[name] = value_from_kwargs
305
429
 
306
- kwargs[key] = kw_value
430
+ # Merge leftover kwargs if subclass allows
431
+ if isinstance(self, inject_kwargs_params.add_to_kwargs):
432
+ kwargs |= injectable_kwargs
307
433
 
308
- if isinstance(this, inject_kwargs_params.add_to_kwargs): # type: ignore[arg-type]
309
- kwargs |= this_kwargs
434
+ return self._function(*tuple(args_list), **kwargs)
310
435
 
311
- return this.function(self, *args, **kwargs)
436
+ return wrapper
312
437
 
313
- return _wrapper # type: ignore
314
-
315
- def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co:
316
- return self.__get__(None, self)(*args, **kwargs) # type: ignore
438
+ @property
439
+ def __func__(self) -> Callable[Concatenate[_T_co, _P], _R_co]:
440
+ """Return the original wrapped function."""
441
+ return self._function
317
442
 
318
443
  @property
319
444
  def __signature__(self) -> Signature:
320
- return Signature.from_callable(self.function)
445
+ """Return (and cache) the signature of the wrapped function."""
446
+ if not self._signature:
447
+ self._signature = Signature.from_callable(self._function)
448
+ return self._signature
321
449
 
322
450
  @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
451
+ def with_name[T0, **P0, R0](
452
+ cls, kwargs_name: str = "kwargs"
453
+ ) -> Callable[[Callable[Concatenate[T0, P0], R0]], inject_kwargs_params[T0, P0, R0]]:
454
+ """
455
+ Decorator factory that creates a subclass of `inject_kwargs_params` with a custom name
456
+ for the keyword argument store.
457
+ """
458
+ ns = cls.__dict__.copy()
459
+ ns["_kwargs_name"] = kwargs_name
326
460
 
327
- return _inner
461
+ custom_cls = type(cls.__name__, cls.__bases__, ns)
328
462
 
463
+ # TODO: The precise subclass type cannot be expressed yet when the class is itself generic.
464
+ def _wrapper(function: Callable[Concatenate[T0, P0], R0]) -> inject_kwargs_params[T0, P0, R0]:
465
+ return custom_cls(function) # pyright: ignore[reportReturnType, reportArgumentType]
329
466
 
330
- if TYPE_CHECKING: # love you mypy...
467
+ return _wrapper
331
468
 
332
- class _add_to_kwargs:
333
- def __call__(self, func: F1) -> F1: ...
334
469
 
335
- class _inject_kwargs_params:
336
- def __call__(self, func: F0) -> F0: ...
470
+ class inject_kwargs_params[T, **P, R](_InjectKwargsParamsBase[T, P, R]):
471
+ """
472
+ Descriptor that injects parameters into functions based on an instance's keyword mapping.
337
473
 
338
- add_to_kwargs = _add_to_kwargs()
474
+ When a method wrapped with `@inject_kwargs_params` is called, the descriptor inspects the function's signature
475
+ and replaces any arguments matching keys in `self.kwargs` (or another mapping defined by `_kwargs_name`)
476
+ if their values equal the parameter's default.
477
+ """
339
478
 
340
- inject_kwargs_params = _inject_kwargs_params()
341
- else:
479
+ __slots__ = ()
342
480
 
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]): ...
481
+ class add_to_kwargs[T0, **P0, R0](_InjectKwargsParamsBase[T0, P0, R0]):
482
+ """
483
+ Variant of `inject_kwargs_params` that merges unused entries from `self.kwargs` into the keyword arguments
484
+ passed to the target function.
345
485
 
486
+ This allows additional context or configuration values to be forwarded without requiring explicit parameters.
487
+ """
346
488
 
347
- class complex_hash(Generic[T]):
348
- """
349
- Decorator for classes to add a ``__hash__`` method to them.
489
+ __slots__ = ()
350
490
 
351
- Especially useful for NamedTuples.
352
- """
353
491
 
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__))
492
+ class _ComplexHash[**P, R]:
493
+ __slots__ = "func"
358
494
 
359
- return inner_class_type # type: ignore
495
+ def __init__(self, func: Callable[P, R]) -> None:
496
+ self.func = func
497
+
498
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
499
+ return self.func(*args, **kwargs)
360
500
 
361
501
  @staticmethod
362
502
  def hash(*args: Any) -> int:
@@ -380,7 +520,24 @@ class complex_hash(Generic[T]):
380
520
  return hash("_".join(values))
381
521
 
382
522
 
383
- def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[type[T]]:
523
+ @_ComplexHash
524
+ def complex_hash[T](cls: type[T]) -> type[T]:
525
+ """
526
+ Decorator for classes to add a ``__hash__`` method to them.
527
+
528
+ Especially useful for NamedTuples.
529
+ """
530
+
531
+ def __hash__(self: T) -> int: # noqa: N807
532
+ return complex_hash.hash(self.__class__.__name__, *(getattr(self, key) for key in self.__annotations__))
533
+
534
+ ns = cls.__dict__.copy()
535
+ ns["__hash__"] = __hash__
536
+
537
+ return type(cls.__name__, (cls,), ns) # pyright: ignore[reportReturnType]
538
+
539
+
540
+ def get_subclasses[T](family: type[T], exclude: Sequence[type[T]] = []) -> list[type[T]]:
384
541
  """
385
542
  Get all subclasses of a given type.
386
543
 
@@ -391,7 +548,7 @@ def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[typ
391
548
  :return: List of all subclasses of "family".
392
549
  """
393
550
 
394
- def _subclasses(cls: type[T]) -> Generator[type[T], None, None]:
551
+ def _subclasses(cls: type[T]) -> Iterator[type[T]]:
395
552
  for subclass in cls.__subclasses__():
396
553
  yield from _subclasses(subclass)
397
554
  if subclass in exclude:
@@ -401,24 +558,20 @@ def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[typ
401
558
  return list(set(_subclasses(family)))
402
559
 
403
560
 
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]):
561
+ class classproperty_base(Generic[_T, _R_co, _T_Any]):
409
562
  __isabstractmethod__: bool = False
410
563
 
411
- fget: Callable[[type[T]], R_co]
412
- fset: Callable[Concatenate[type[T], _T_Any, ...], None] | None
413
- fdel: Callable[[type[T]], None] | None
564
+ fget: Callable[[type[_T]], _R_co]
565
+ fset: Callable[Concatenate[type[_T], _T_Any, ...], None] | None
566
+ fdel: Callable[[type[_T]], None] | None
414
567
 
415
568
  def __init__(
416
569
  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]
570
+ fget: Callable[[type[_T]], _R_co] | classmethod[_T, ..., _R_co],
571
+ fset: Callable[Concatenate[type[_T], _T_Any, ...], None]
572
+ | classmethod[_T, Concatenate[_T_Any, ...], None]
420
573
  | None = None,
421
- fdel: Callable[[type[T]], None] | classmethod[T, ..., None] | None = None,
574
+ fdel: Callable[[type[_T]], None] | classmethod[_T, ..., None] | None = None,
422
575
  doc: str | None = None,
423
576
  ) -> None:
424
577
  self.fget = fget.__func__ if isinstance(fget, classmethod) else fget
@@ -431,7 +584,7 @@ class classproperty_base(Generic[T, R_co, _T_Any]):
431
584
  def __set_name__(self, owner: object, name: str) -> None:
432
585
  self.__name__ = name
433
586
 
434
- def _get_cache(self, type_: type[T]) -> dict[str, Any]:
587
+ def _get_cache(self, type_: type[_T]) -> dict[str, Any]:
435
588
  cache_key = getattr(self, "cache_key")
436
589
 
437
590
  if not hasattr(type_, cache_key):
@@ -439,7 +592,7 @@ class classproperty_base(Generic[T, R_co, _T_Any]):
439
592
 
440
593
  return getattr(type_, cache_key)
441
594
 
442
- def __get__(self, obj: T | None, type_: type[T] | None = None) -> R_co:
595
+ def __get__(self, obj: _T | None, type_: type[_T] | None = None) -> _R_co:
443
596
  if type_ is None and obj is not None:
444
597
  type_ = type(obj)
445
598
  elif type_ is None:
@@ -455,7 +608,7 @@ class classproperty_base(Generic[T, R_co, _T_Any]):
455
608
  cache[self.__name__] = value
456
609
  return value
457
610
 
458
- def __set__(self, obj: T, value: _T_Any) -> None:
611
+ def __set__(self, obj: _T, value: _T_Any) -> None:
459
612
  if not self.fset:
460
613
  raise AttributeError(
461
614
  f'classproperty with getter "{self.__name__}" of "{obj.__class__.__name__}" object has no setter.'
@@ -471,7 +624,7 @@ class classproperty_base(Generic[T, R_co, _T_Any]):
471
624
 
472
625
  self.fset(type_, value)
473
626
 
474
- def __delete__(self, obj: T) -> None:
627
+ def __delete__(self, obj: _T) -> None:
475
628
  if not self.fdel:
476
629
  raise AttributeError(
477
630
  f'classproperty with getter "{self.__name__}" of "{obj.__class__.__name__}" object has no deleter.'
@@ -488,12 +641,12 @@ class classproperty_base(Generic[T, R_co, _T_Any]):
488
641
  self.fdel(type_)
489
642
 
490
643
 
491
- class classproperty(classproperty_base[T, R_co, _T_Any]):
644
+ class classproperty(classproperty_base[_T, _R_co, _T_Any]):
492
645
  """
493
646
  A combination of `classmethod` and `property`.
494
647
  """
495
648
 
496
- class cached(classproperty_base[T0, R0_co, _T0_Any]):
649
+ class cached(classproperty_base[_T0, _R0_co, _T0_Any]):
497
650
  """
498
651
  A combination of `classmethod` and `property`.
499
652
 
@@ -526,7 +679,7 @@ class classproperty(classproperty_base[T, R_co, _T_Any]):
526
679
  del cache[name]
527
680
 
528
681
 
529
- class cachedproperty(property, Generic[R_co, _T_Any]):
682
+ class cachedproperty(property, Generic[_R_co, _T_Any]):
530
683
  """
531
684
  Wrapper for a one-time get property, that will be cached.
532
685
 
@@ -547,17 +700,17 @@ class cachedproperty(property, Generic[R_co, _T_Any]):
547
700
 
548
701
  def __init__(
549
702
  self,
550
- fget: Callable[[Any], R_co],
703
+ fget: Callable[[Any], _R_co],
551
704
  fset: Callable[[Any, _T_Any], None] | None = None,
552
705
  fdel: Callable[[Any], None] | None = None,
553
706
  doc: str | None = None,
554
707
  ) -> None: ...
555
708
 
556
- def getter(self, fget: Callable[..., R_co]) -> cachedproperty[R_co, _T_Any]: ...
709
+ def getter(self, fget: Callable[..., _R_co]) -> cachedproperty[_R_co, _T_Any]: ...
557
710
 
558
- def setter(self, fset: Callable[[Any, _T_Any], None]) -> cachedproperty[R_co, _T_Any]: ...
711
+ def setter(self, fset: Callable[[Any, _T_Any], None]) -> cachedproperty[_R_co, _T_Any]: ...
559
712
 
560
- def deleter(self, fdel: Callable[..., None]) -> cachedproperty[R_co, _T_Any]: ...
713
+ def deleter(self, fdel: Callable[..., None]) -> cachedproperty[_R_co, _T_Any]: ...
561
714
 
562
715
  if sys.version_info < (3, 13):
563
716
 
@@ -569,7 +722,7 @@ class cachedproperty(property, Generic[R_co, _T_Any]):
569
722
  def __get__(self, instance: None, owner: type | None = None) -> Self: ...
570
723
 
571
724
  @overload
572
- def __get__(self, instance: Any, owner: type | None = None) -> R_co: ...
725
+ def __get__(self, instance: Any, owner: type | None = None) -> _R_co: ...
573
726
 
574
727
  def __get__(self, instance: Any, owner: type | None = None) -> Any:
575
728
  if instance is None:
@@ -629,18 +782,20 @@ class cachedproperty(property, Generic[R_co, _T_Any]):
629
782
  class KwargsNotNone(KwargsT):
630
783
  """Remove all None objects from this kwargs dict."""
631
784
 
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})
785
+ @copy_signature(KwargsT.__init__)
786
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
787
+ super().__init__({key: value for key, value in dict(*args, **kwargs).items() if value is not None})
636
788
 
637
789
 
638
790
  class SingletonMeta(type):
639
791
  _instances: ClassVar[dict[type[Any], Any]] = {}
640
792
  _singleton_init: bool
641
793
 
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)})
794
+ def __new__[MetaSelf: SingletonMeta](
795
+ mcls: type[MetaSelf], name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any
796
+ ) -> MetaSelf:
797
+ namespace["_singleton_init"] = kwargs.pop("init", False)
798
+ return super().__new__(mcls, name, bases, namespace, **kwargs)
644
799
 
645
800
  def __call__(cls, *args: Any, **kwargs: Any) -> SingletonMeta:
646
801
  if cls not in cls._instances:
@@ -654,34 +809,7 @@ class SingletonMeta(type):
654
809
  class Singleton(metaclass=SingletonMeta):
655
810
  """Handy class to inherit to have the SingletonMeta metaclass."""
656
811
 
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,)
812
+ __slots__ = ()
685
813
 
686
814
 
687
815
  class LinearRangeLut(Mapping[int, int]):
@@ -706,13 +834,14 @@ class LinearRangeLut(Mapping[int, int]):
706
834
  if self._misses_n > 2:
707
835
  self._ranges_idx_lut = self._ranges_idx_lut[missed_hit:] + self._ranges_idx_lut[:missed_hit]
708
836
 
709
- return idx
837
+ return idx # pyright: ignore[reportPossiblyUnboundVariable]
710
838
 
711
839
  def __len__(self) -> int:
712
840
  return len(self.ranges)
713
841
 
714
842
  def __iter__(self) -> Iterator[int]:
715
- return iter(range(len(self)))
843
+ for i in range(len(self)):
844
+ yield i
716
845
 
717
846
  def __setitem__(self, n: int, _range: range) -> NoReturn:
718
847
  raise NotImplementedError