jetpytools 1.7.3__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, R1, 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__ = ()
214
316
 
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)()
317
+ class property(Generic[_T1_co, _P1, _R1_co], metaclass=_InjectSelfMeta):
318
+ """Property variant of `inject_self.cached` that auto-calls the wrapped method."""
217
319
 
218
- class init_kwargs(inject_self_base[T0_co, P0, R0_co]):
320
+ __slots__ = ("__func__",)
321
+
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]
466
+
467
+ return _wrapper
329
468
 
330
- if TYPE_CHECKING: # love you mypy...
331
469
 
332
- class _add_to_kwargs:
333
- def __call__(self, func: F1) -> F1: ...
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.
334
473
 
335
- class _inject_kwargs_params:
336
- def __call__(self, func: F0) -> F0: ...
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
+ """
337
478
 
338
- add_to_kwargs = _add_to_kwargs()
479
+ __slots__ = ()
339
480
 
340
- inject_kwargs_params = _inject_kwargs_params()
341
- else:
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.
342
485
 
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]): ...
486
+ This allows additional context or configuration values to be forwarded without requiring explicit parameters.
487
+ """
345
488
 
489
+ __slots__ = ()
346
490
 
347
- class complex_hash(Generic[T]):
348
- """
349
- Decorator for classes to add a ``__hash__`` method to them.
350
491
 
351
- Especially useful for NamedTuples.
352
- """
492
+ class _ComplexHash[**P, R]:
493
+ __slots__ = "func"
353
494
 
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__))
495
+ def __init__(self, func: Callable[P, R]) -> None:
496
+ self.func = func
358
497
 
359
- return inner_class_type # type: ignore
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,35 +558,33 @@ def get_subclasses(family: type[T], exclude: Sequence[type[T]] = []) -> list[typ
401
558
  return list(set(_subclasses(family)))
402
559
 
403
560
 
404
- class classproperty_base(Generic[T, R_co]):
561
+ class classproperty_base(Generic[_T, _R_co, _T_Any]):
405
562
  __isabstractmethod__: bool = False
406
563
 
564
+ fget: Callable[[type[_T]], _R_co]
565
+ fset: Callable[Concatenate[type[_T], _T_Any, ...], None] | None
566
+ fdel: Callable[[type[_T]], None] | None
567
+
407
568
  def __init__(
408
569
  self,
409
- fget: Callable[[type[T]], R_co] | classmethod[T, ..., R_co],
410
- fset: Callable[Concatenate[type[T], Any, ...], None]
411
- | classmethod[T, Concatenate[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]
412
573
  | None = None,
413
- fdel: Callable[[type[T]], None] | classmethod[T, ..., None] | None = None,
574
+ fdel: Callable[[type[_T]], None] | classmethod[_T, ..., None] | None = None,
414
575
  doc: str | None = None,
415
576
  ) -> None:
416
- self.fget = self._wrap(fget)
417
- self.fset = self._wrap(fset) if fset is not None else fset
418
- self.fdel = self._wrap(fdel) if fdel is not None else fdel
577
+ self.fget = fget.__func__ if isinstance(fget, classmethod) else fget
578
+ self.fset = fset.__func__ if isinstance(fset, classmethod) else fset
579
+ self.fdel = fdel.__func__ if isinstance(fdel, classmethod) else fdel
419
580
 
420
581
  self.__doc__ = doc
421
582
  self.__name__ = self.fget.__name__
422
583
 
423
- def _wrap(self, func: Callable[..., R1] | classmethod[T, P1, R1]) -> classmethod[T, P1, R1]:
424
- if not isinstance(func, classmethod):
425
- func = classmethod(func)
426
-
427
- return func
428
-
429
584
  def __set_name__(self, owner: object, name: str) -> None:
430
585
  self.__name__ = name
431
586
 
432
- def _get_cache(self, type_: type) -> dict[str, Any]:
587
+ def _get_cache(self, type_: type[_T]) -> dict[str, Any]:
433
588
  cache_key = getattr(self, "cache_key")
434
589
 
435
590
  if not hasattr(type_, cache_key):
@@ -437,21 +592,23 @@ class classproperty_base(Generic[T, R_co]):
437
592
 
438
593
  return getattr(type_, cache_key)
439
594
 
440
- def __get__(self, obj: T | None, type_: type | None = None) -> R_co:
441
- if type_ is None:
595
+ def __get__(self, obj: _T | None, type_: type[_T] | None = None) -> _R_co:
596
+ if type_ is None and obj is not None:
442
597
  type_ = type(obj)
598
+ elif type_ is None:
599
+ raise NotImplementedError("Both obj and type_ are None")
443
600
 
444
601
  if not isinstance(self, classproperty.cached):
445
- return self.fget.__get__(obj, type_)()
602
+ return self.fget(type_)
446
603
 
447
604
  if self.__name__ in (cache := self._get_cache(type_)):
448
605
  return cache[self.__name__]
449
606
 
450
- value = self.fget.__get__(obj, type_)()
607
+ value = self.fget(type_)
451
608
  cache[self.__name__] = value
452
609
  return value
453
610
 
454
- def __set__(self, obj: T, value: Any) -> None:
611
+ def __set__(self, obj: _T, value: _T_Any) -> None:
455
612
  if not self.fset:
456
613
  raise AttributeError(
457
614
  f'classproperty with getter "{self.__name__}" of "{obj.__class__.__name__}" object has no setter.'
@@ -459,12 +616,15 @@ class classproperty_base(Generic[T, R_co]):
459
616
 
460
617
  type_ = type(obj)
461
618
 
619
+ if not isinstance(self, classproperty.cached):
620
+ return self.fset(type_, value)
621
+
462
622
  if self.__name__ in (cache := self._get_cache(type_)):
463
623
  del cache[self.__name__]
464
624
 
465
- self.fset.__get__(None, type_)(value)
625
+ self.fset(type_, value)
466
626
 
467
- def __delete__(self, obj: T) -> None:
627
+ def __delete__(self, obj: _T) -> None:
468
628
  if not self.fdel:
469
629
  raise AttributeError(
470
630
  f'classproperty with getter "{self.__name__}" of "{obj.__class__.__name__}" object has no deleter.'
@@ -472,18 +632,21 @@ class classproperty_base(Generic[T, R_co]):
472
632
 
473
633
  type_ = type(obj)
474
634
 
635
+ if not isinstance(self, classproperty.cached):
636
+ return self.fdel(type_)
637
+
475
638
  if self.__name__ in (cache := self._get_cache(type_)):
476
639
  del cache[self.__name__]
477
640
 
478
- self.fdel.__get__(None, type_)()
641
+ self.fdel(type_)
479
642
 
480
643
 
481
- class classproperty(classproperty_base[T, R_co]):
644
+ class classproperty(classproperty_base[_T, _R_co, _T_Any]):
482
645
  """
483
646
  A combination of `classmethod` and `property`.
484
647
  """
485
648
 
486
- class cached(classproperty_base[T0, R0_co]):
649
+ class cached(classproperty_base[_T0, _R0_co, _T0_Any]):
487
650
  """
488
651
  A combination of `classmethod` and `property`.
489
652
 
@@ -516,10 +679,7 @@ class classproperty(classproperty_base[T, R_co]):
516
679
  del cache[name]
517
680
 
518
681
 
519
- _T_cc = TypeVar("_T_cc", default=Any)
520
-
521
-
522
- class cachedproperty(property, Generic[R_co, _T_cc]):
682
+ class cachedproperty(property, Generic[_R_co, _T_Any]):
523
683
  """
524
684
  Wrapper for a one-time get property, that will be cached.
525
685
 
@@ -540,17 +700,17 @@ class cachedproperty(property, Generic[R_co, _T_cc]):
540
700
 
541
701
  def __init__(
542
702
  self,
543
- fget: Callable[[Any], R_co],
544
- fset: Callable[[Any, _T_cc], None] | None = None,
703
+ fget: Callable[[Any], _R_co],
704
+ fset: Callable[[Any, _T_Any], None] | None = None,
545
705
  fdel: Callable[[Any], None] | None = None,
546
706
  doc: str | None = None,
547
707
  ) -> None: ...
548
708
 
549
- def getter(self, fget: Callable[..., R_co]) -> cachedproperty[R_co, _T_cc]: ...
709
+ def getter(self, fget: Callable[..., _R_co]) -> cachedproperty[_R_co, _T_Any]: ...
550
710
 
551
- def setter(self, fset: Callable[[Any, _T_cc], None]) -> cachedproperty[R_co, _T_cc]: ...
711
+ def setter(self, fset: Callable[[Any, _T_Any], None]) -> cachedproperty[_R_co, _T_Any]: ...
552
712
 
553
- def deleter(self, fdel: Callable[..., None]) -> cachedproperty[R_co, _T_cc]: ...
713
+ def deleter(self, fdel: Callable[..., None]) -> cachedproperty[_R_co, _T_Any]: ...
554
714
 
555
715
  if sys.version_info < (3, 13):
556
716
 
@@ -562,7 +722,7 @@ class cachedproperty(property, Generic[R_co, _T_cc]):
562
722
  def __get__(self, instance: None, owner: type | None = None) -> Self: ...
563
723
 
564
724
  @overload
565
- def __get__(self, instance: Any, owner: type | None = None) -> R_co: ...
725
+ def __get__(self, instance: Any, owner: type | None = None) -> _R_co: ...
566
726
 
567
727
  def __get__(self, instance: Any, owner: type | None = None) -> Any:
568
728
  if instance is None:
@@ -575,7 +735,7 @@ class cachedproperty(property, Generic[R_co, _T_cc]):
575
735
  cache[self.__name__] = value
576
736
  return value
577
737
 
578
- def __set__(self, instance: Any, value: _T_cc) -> None:
738
+ def __set__(self, instance: Any, value: _T_Any) -> None:
579
739
  if self.__name__ in (cache := instance.__dict__.setdefault(self.cache_key, {})):
580
740
  del cache[self.__name__]
581
741
 
@@ -622,18 +782,20 @@ class cachedproperty(property, Generic[R_co, _T_cc]):
622
782
  class KwargsNotNone(KwargsT):
623
783
  """Remove all None objects from this kwargs dict."""
624
784
 
625
- if not TYPE_CHECKING:
626
-
627
- def __new__(cls, *args: Any, **kwargs: Any) -> Self:
628
- 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})
629
788
 
630
789
 
631
790
  class SingletonMeta(type):
632
791
  _instances: ClassVar[dict[type[Any], Any]] = {}
633
792
  _singleton_init: bool
634
793
 
635
- def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any) -> SingletonMeta:
636
- 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)
637
799
 
638
800
  def __call__(cls, *args: Any, **kwargs: Any) -> SingletonMeta:
639
801
  if cls not in cls._instances:
@@ -647,34 +809,7 @@ class SingletonMeta(type):
647
809
  class Singleton(metaclass=SingletonMeta):
648
810
  """Handy class to inherit to have the SingletonMeta metaclass."""
649
811
 
650
-
651
- class to_singleton_impl:
652
- _ts_args = tuple[str, ...]()
653
- _ts_kwargs: Mapping[str, Any] = {}
654
- _add_classes = tuple[type, ...]()
655
-
656
- def __new__(_cls, cls: type[T]) -> T: # type: ignore
657
- if _cls._add_classes:
658
-
659
- class rcls(cls, *_cls._add_classes): # type: ignore
660
- ...
661
- else:
662
- rcls = cls # type: ignore
663
-
664
- return rcls(*_cls._ts_args, **_cls._ts_kwargs)
665
-
666
- @classmethod
667
- def with_args(cls, *args: Any, **kwargs: Any) -> type[to_singleton]:
668
- class _inner_singl(cls): # type: ignore
669
- _ts_args = args
670
- _ts_kwargs = kwargs
671
-
672
- return _inner_singl
673
-
674
-
675
- class to_singleton(to_singleton_impl):
676
- class as_property(to_singleton_impl):
677
- _add_classes = (property,)
812
+ __slots__ = ()
678
813
 
679
814
 
680
815
  class LinearRangeLut(Mapping[int, int]):
@@ -699,13 +834,14 @@ class LinearRangeLut(Mapping[int, int]):
699
834
  if self._misses_n > 2:
700
835
  self._ranges_idx_lut = self._ranges_idx_lut[missed_hit:] + self._ranges_idx_lut[:missed_hit]
701
836
 
702
- return idx
837
+ return idx # pyright: ignore[reportPossiblyUnboundVariable]
703
838
 
704
839
  def __len__(self) -> int:
705
840
  return len(self.ranges)
706
841
 
707
842
  def __iter__(self) -> Iterator[int]:
708
- return iter(range(len(self)))
843
+ for i in range(len(self)):
844
+ yield i
709
845
 
710
846
  def __setitem__(self, n: int, _range: range) -> NoReturn:
711
847
  raise NotImplementedError