storebind 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benjamin Chau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.3
2
+ Name: storebind
3
+ Version: 0.1.0
4
+ Summary: Typed application state, dependency injection, and reactive subscriptions for Python.
5
+ Keywords: state-management,dependency-injection,reactive,store,typed-state
6
+ Author: Benjamin Chau
7
+ Author-email: Benjamin Chau <68836494+swarfte@users.noreply.github.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Benjamin Chau
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Programming Language :: Python :: 3.11
33
+ Classifier: Programming Language :: Python :: 3.12
34
+ Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Programming Language :: Python :: 3.14
36
+ Classifier: Typing :: Typed
37
+ Requires-Python: >=3.11
38
+ Description-Content-Type: text/markdown
39
+
File without changes
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "storebind"
3
+ version = "0.1.0"
4
+ description = "Typed application state, dependency injection, and reactive subscriptions for Python."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Benjamin Chau", email = "68836494+swarfte@users.noreply.github.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = []
11
+ license = { file = "LICENSE" }
12
+ keywords = [
13
+ "state-management",
14
+ "dependency-injection",
15
+ "reactive",
16
+ "store",
17
+ "typed-state",
18
+ ]
19
+
20
+ classifiers = [
21
+ "Development Status :: 3 - Alpha",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Typing :: Typed",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.9.25,<0.10.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "mypy>=2.3.1",
38
+ "pytest>=9.1.1",
39
+ "pytest-asyncio>=1.4.0",
40
+ "ruff>=0.16.8",
41
+ ]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ asyncio_mode = "auto"
46
+
47
+ [tool.ruff]
48
+ line-length = 88
49
+ target-version = "py311"
50
+
51
+ [tool.ruff.lint]
52
+ select = [
53
+ "E",
54
+ "F",
55
+ "I",
56
+ "UP",
57
+ "B",
58
+ "SIM",
59
+ ]
60
+
61
+ [tool.mypy]
62
+ python_version = "3.11"
63
+ strict = true
64
+ files = [
65
+ "src",
66
+ "tests",
67
+ ]
@@ -0,0 +1,25 @@
1
+ from .base import BaseStore
2
+ from .exceptions import (
3
+ InvalidStateTypeError,
4
+ MissingStateValueError,
5
+ StateNotDeclaredError,
6
+ StoreAlreadyRegisteredError,
7
+ StoreBindError,
8
+ StoreNotRegisteredError,
9
+ )
10
+ from .registry import StoreRegistry
11
+
12
+ store = StoreRegistry()
13
+
14
+
15
+ __all__ = [
16
+ "BaseStore",
17
+ "InvalidStateTypeError",
18
+ "MissingStateValueError",
19
+ "StateNotDeclaredError",
20
+ "StoreAlreadyRegisteredError",
21
+ "StoreBindError",
22
+ "StoreNotRegisteredError",
23
+ "StoreRegistry",
24
+ "store",
25
+ ]
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from typing import Any, ClassVar, get_type_hints
5
+
6
+ from .exceptions import MissingStateValueError
7
+
8
+ _MISSING = object()
9
+
10
+
11
+ class BaseStore:
12
+ """
13
+ Base class for all StoreBind stores.
14
+
15
+ Store fields are declared using type annotations:
16
+
17
+ class ConfigStore(BaseStore):
18
+ theme: str = "dark"
19
+ style: str
20
+ """
21
+
22
+ __storebind_internal_fields__: ClassVar[frozenset[str]] = frozenset(
23
+ {
24
+ "__storebind_internal_fields__",
25
+ }
26
+ )
27
+
28
+ def __init__(self, **initial_values: Any) -> None:
29
+ field_types = self.field_types()
30
+
31
+ unknown_fields = set(initial_values) - set(field_types)
32
+
33
+ if unknown_fields:
34
+ fields = ", ".join(sorted(unknown_fields))
35
+ raise TypeError(
36
+ f"{type(self).__name__} received unknown state field(s): {fields}."
37
+ )
38
+
39
+ missing_fields: list[str] = []
40
+
41
+ for field_name in field_types:
42
+ if field_name in initial_values:
43
+ value = initial_values[field_name]
44
+ else:
45
+ value = self._get_default(field_name)
46
+
47
+ if value is _MISSING:
48
+ missing_fields.append(field_name)
49
+ continue
50
+
51
+ object.__setattr__(
52
+ self,
53
+ field_name,
54
+ deepcopy(value),
55
+ )
56
+
57
+ if missing_fields:
58
+ formatted_fields = ", ".join(sorted(missing_fields))
59
+
60
+ raise MissingStateValueError(
61
+ f"{type(self).__name__} requires initial values "
62
+ f"for: {formatted_fields}."
63
+ )
64
+
65
+ @classmethod
66
+ def field_types(cls) -> dict[str, Any]:
67
+ """
68
+ Return declared store fields and their resolved types.
69
+ """
70
+
71
+ annotations = get_type_hints(cls)
72
+
73
+ return {
74
+ name: annotation
75
+ for name, annotation in annotations.items()
76
+ if not name.startswith("_")
77
+ }
78
+
79
+ @classmethod
80
+ def _get_default(cls, field_name: str) -> Any:
81
+ for base in cls.__mro__:
82
+ if field_name in base.__dict__:
83
+ return base.__dict__[field_name]
84
+
85
+ return _MISSING
86
+
87
+ def to_dict(self) -> dict[str, Any]:
88
+ return {
89
+ field_name: getattr(self, field_name) for field_name in self.field_types()
90
+ }
91
+
92
+ def __repr__(self) -> str:
93
+ values = ", ".join(
94
+ f"{name}={value!r}" for name, value in self.to_dict().items()
95
+ )
96
+
97
+ return f"{type(self).__name__}({values})"
@@ -0,0 +1,22 @@
1
+ class StoreBindError(Exception):
2
+ """Base exception for StoreBind."""
3
+
4
+
5
+ class StoreAlreadyRegisteredError(StoreBindError):
6
+ """Raised when a store class is registered more than once."""
7
+
8
+
9
+ class StoreNotRegisteredError(StoreBindError):
10
+ """Raised when accessing an unregistered store."""
11
+
12
+
13
+ class StateNotDeclaredError(StoreBindError):
14
+ """Raised when accessing a state field not declared by the store."""
15
+
16
+
17
+ class MissingStateValueError(StoreBindError):
18
+ """Raised when a required state field has no initial value."""
19
+
20
+
21
+ class InvalidStateTypeError(StoreBindError, TypeError):
22
+ """Raised when a state value does not match its declared type."""
File without changes
@@ -0,0 +1,549 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import types
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from threading import RLock
8
+ from typing import (
9
+ Any,
10
+ TypeVar,
11
+ Union,
12
+ get_args,
13
+ get_origin,
14
+ )
15
+
16
+ from .base import BaseStore
17
+ from .exceptions import (
18
+ InvalidStateTypeError,
19
+ StateNotDeclaredError,
20
+ StoreAlreadyRegisteredError,
21
+ StoreNotRegisteredError,
22
+ )
23
+
24
+ StoreT = TypeVar("StoreT", bound=BaseStore)
25
+ ReturnT = TypeVar("ReturnT")
26
+
27
+ Subscriber = Callable[[Any, Any], None]
28
+
29
+
30
+ class StoreRegistry:
31
+ """
32
+ Registry containing application-wide store instances.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ validate_types: bool = True,
39
+ ) -> None:
40
+ self._stores: dict[type[BaseStore], BaseStore] = {}
41
+
42
+ self._subscribers: dict[
43
+ tuple[type[BaseStore], str],
44
+ set[Subscriber],
45
+ ] = {}
46
+
47
+ self._validate_types = validate_types
48
+ self._lock = RLock()
49
+
50
+ def register(
51
+ self,
52
+ store_type: type[StoreT],
53
+ /,
54
+ *,
55
+ replace: bool = False,
56
+ **initial_values: Any,
57
+ ) -> StoreT:
58
+ """
59
+ Register and instantiate a store class.
60
+ """
61
+
62
+ if not isinstance(store_type, type):
63
+ raise TypeError("register() expects a BaseStore class, not an instance.")
64
+
65
+ if not issubclass(store_type, BaseStore):
66
+ raise TypeError(f"{store_type!r} must inherit from BaseStore.")
67
+
68
+ with self._lock:
69
+ if store_type in self._stores and not replace:
70
+ raise StoreAlreadyRegisteredError(
71
+ f"{store_type.__name__} is already registered."
72
+ )
73
+
74
+ instance = store_type(**initial_values)
75
+
76
+ if self._validate_types:
77
+ self._validate_store(instance)
78
+
79
+ self._stores[store_type] = instance
80
+
81
+ if replace:
82
+ self._remove_store_subscribers(store_type)
83
+
84
+ return instance
85
+
86
+ def unregister(
87
+ self,
88
+ store_type: type[BaseStore],
89
+ ) -> None:
90
+ with self._lock:
91
+ self._require_store(store_type)
92
+
93
+ del self._stores[store_type]
94
+ self._remove_store_subscribers(store_type)
95
+
96
+ def is_registered(
97
+ self,
98
+ store_type: type[BaseStore],
99
+ ) -> bool:
100
+ with self._lock:
101
+ return store_type in self._stores
102
+
103
+ def use(
104
+ self,
105
+ store_type: type[StoreT],
106
+ ) -> StoreT:
107
+ """
108
+ Return the registered store instance.
109
+ """
110
+
111
+ with self._lock:
112
+ instance = self._require_store(store_type)
113
+
114
+ return instance # type: ignore[return-value]
115
+
116
+ def get(
117
+ self,
118
+ store_type: type[BaseStore],
119
+ attribute_name: str,
120
+ ) -> Any:
121
+ with self._lock:
122
+ instance = self._require_store(store_type)
123
+
124
+ self._require_attribute(
125
+ store_type,
126
+ attribute_name,
127
+ )
128
+
129
+ return getattr(instance, attribute_name)
130
+
131
+ def set(
132
+ self,
133
+ store_type: type[BaseStore],
134
+ attribute_name: str,
135
+ value: Any,
136
+ ) -> Any:
137
+ self._require_attribute(
138
+ store_type,
139
+ attribute_name,
140
+ )
141
+
142
+ if self._validate_types:
143
+ self._validate_field_value(
144
+ store_type,
145
+ attribute_name,
146
+ value,
147
+ )
148
+
149
+ subscriber_key = (
150
+ store_type,
151
+ attribute_name,
152
+ )
153
+
154
+ with self._lock:
155
+ instance = self._require_store(store_type)
156
+ old_value = getattr(instance, attribute_name)
157
+
158
+ if self._values_equal(old_value, value):
159
+ return value
160
+
161
+ setattr(instance, attribute_name, value)
162
+
163
+ subscribers = tuple(
164
+ self._subscribers.get(
165
+ subscriber_key,
166
+ (),
167
+ )
168
+ )
169
+
170
+ for callback in subscribers:
171
+ callback(value, old_value)
172
+
173
+ return value
174
+
175
+ def inject(
176
+ self,
177
+ store_type: type[BaseStore],
178
+ attribute_name: str,
179
+ *,
180
+ as_: str,
181
+ ) -> Callable[
182
+ [Callable[..., ReturnT]],
183
+ Callable[..., ReturnT],
184
+ ]:
185
+ """
186
+ Inject a store value into a function parameter.
187
+
188
+ Explicit caller arguments take priority over injection.
189
+ """
190
+
191
+ if not as_:
192
+ raise ValueError("inject() requires a non-empty 'as_' parameter.")
193
+
194
+ self._require_attribute(
195
+ store_type,
196
+ attribute_name,
197
+ )
198
+
199
+ def decorator(
200
+ function: Callable[..., ReturnT],
201
+ ) -> Callable[..., ReturnT]:
202
+ signature = inspect.signature(function)
203
+
204
+ if as_ not in signature.parameters:
205
+ raise TypeError(
206
+ f"{function.__qualname__}() has no parameter named {as_!r}."
207
+ )
208
+
209
+ if inspect.iscoroutinefunction(function):
210
+
211
+ @wraps(function)
212
+ async def async_wrapper(
213
+ *args: Any,
214
+ **kwargs: Any,
215
+ ) -> Any:
216
+ call_kwargs = self._build_injected_kwargs(
217
+ function,
218
+ signature,
219
+ args,
220
+ kwargs,
221
+ store_type,
222
+ attribute_name,
223
+ as_,
224
+ )
225
+
226
+ return await function(
227
+ *args,
228
+ **call_kwargs,
229
+ )
230
+
231
+ return async_wrapper # type: ignore[return-value]
232
+
233
+ @wraps(function)
234
+ def sync_wrapper(
235
+ *args: Any,
236
+ **kwargs: Any,
237
+ ) -> ReturnT:
238
+ call_kwargs = self._build_injected_kwargs(
239
+ function,
240
+ signature,
241
+ args,
242
+ kwargs,
243
+ store_type,
244
+ attribute_name,
245
+ as_,
246
+ )
247
+
248
+ return function(
249
+ *args,
250
+ **call_kwargs,
251
+ )
252
+
253
+ return sync_wrapper
254
+
255
+ return decorator
256
+
257
+ def capture(
258
+ self,
259
+ store_type: type[BaseStore],
260
+ attribute_name: str,
261
+ ) -> Callable[
262
+ [Callable[..., ReturnT]],
263
+ Callable[..., ReturnT],
264
+ ]:
265
+ """
266
+ Write a function's return value into a store field.
267
+ """
268
+
269
+ self._require_attribute(
270
+ store_type,
271
+ attribute_name,
272
+ )
273
+
274
+ def decorator(
275
+ function: Callable[..., ReturnT],
276
+ ) -> Callable[..., ReturnT]:
277
+ if inspect.iscoroutinefunction(function):
278
+
279
+ @wraps(function)
280
+ async def async_wrapper(
281
+ *args: Any,
282
+ **kwargs: Any,
283
+ ) -> Any:
284
+ result = await function(
285
+ *args,
286
+ **kwargs,
287
+ )
288
+
289
+ self.set(
290
+ store_type,
291
+ attribute_name,
292
+ result,
293
+ )
294
+
295
+ return result
296
+
297
+ return async_wrapper # type: ignore[return-value]
298
+
299
+ @wraps(function)
300
+ def sync_wrapper(
301
+ *args: Any,
302
+ **kwargs: Any,
303
+ ) -> ReturnT:
304
+ result = function(
305
+ *args,
306
+ **kwargs,
307
+ )
308
+
309
+ self.set(
310
+ store_type,
311
+ attribute_name,
312
+ result,
313
+ )
314
+
315
+ return result
316
+
317
+ return sync_wrapper
318
+
319
+ return decorator
320
+
321
+ def subscribe(
322
+ self,
323
+ store_type: type[BaseStore],
324
+ attribute_name: str,
325
+ callback: Subscriber,
326
+ *,
327
+ immediate: bool = False,
328
+ ) -> Callable[[], None]:
329
+ self._require_store(store_type)
330
+ self._require_attribute(
331
+ store_type,
332
+ attribute_name,
333
+ )
334
+
335
+ key = (
336
+ store_type,
337
+ attribute_name,
338
+ )
339
+
340
+ with self._lock:
341
+ callbacks = self._subscribers.setdefault(
342
+ key,
343
+ set(),
344
+ )
345
+
346
+ callbacks.add(callback)
347
+
348
+ if immediate:
349
+ current_value = self.get(
350
+ store_type,
351
+ attribute_name,
352
+ )
353
+
354
+ callback(
355
+ current_value,
356
+ current_value,
357
+ )
358
+
359
+ def unsubscribe() -> None:
360
+ with self._lock:
361
+ callbacks = self._subscribers.get(key)
362
+
363
+ if callbacks is None:
364
+ return
365
+
366
+ callbacks.discard(callback)
367
+
368
+ if not callbacks:
369
+ self._subscribers.pop(
370
+ key,
371
+ None,
372
+ )
373
+
374
+ return unsubscribe
375
+
376
+ def reset(self) -> None:
377
+ """
378
+ Remove every registered store and subscriber.
379
+ """
380
+
381
+ with self._lock:
382
+ self._stores.clear()
383
+ self._subscribers.clear()
384
+
385
+ def _require_store(
386
+ self,
387
+ store_type: type[StoreT],
388
+ ) -> StoreT:
389
+ try:
390
+ instance = self._stores[store_type]
391
+ except KeyError:
392
+ raise StoreNotRegisteredError(
393
+ f"{store_type.__name__} is not registered."
394
+ ) from None
395
+
396
+ return instance # type: ignore[return-value]
397
+
398
+ def _require_attribute(
399
+ self,
400
+ store_type: type[BaseStore],
401
+ attribute_name: str,
402
+ ) -> None:
403
+ field_types = store_type.field_types()
404
+
405
+ if attribute_name not in field_types:
406
+ raise StateNotDeclaredError(
407
+ f"{store_type.__name__} has no declared state field {attribute_name!r}."
408
+ )
409
+
410
+ def _validate_store(
411
+ self,
412
+ instance: BaseStore,
413
+ ) -> None:
414
+ store_type = type(instance)
415
+
416
+ for field_name in store_type.field_types():
417
+ self._validate_field_value(
418
+ store_type,
419
+ field_name,
420
+ getattr(instance, field_name),
421
+ )
422
+
423
+ def _validate_field_value(
424
+ self,
425
+ store_type: type[BaseStore],
426
+ attribute_name: str,
427
+ value: Any,
428
+ ) -> None:
429
+ expected_type = store_type.field_types()[attribute_name]
430
+
431
+ if not self._matches_type(
432
+ value,
433
+ expected_type,
434
+ ):
435
+ raise InvalidStateTypeError(
436
+ f"{store_type.__name__}.{attribute_name} "
437
+ f"expects {expected_type!r}, "
438
+ f"but received {type(value).__name__}."
439
+ )
440
+
441
+ def _matches_type(
442
+ self,
443
+ value: Any,
444
+ expected_type: Any,
445
+ ) -> bool:
446
+ if expected_type is Any:
447
+ return True
448
+
449
+ origin = get_origin(expected_type)
450
+ arguments = get_args(expected_type)
451
+
452
+ if origin in {
453
+ Union,
454
+ types.UnionType,
455
+ }:
456
+ return any(self._matches_type(value, argument) for argument in arguments)
457
+
458
+ if origin is list:
459
+ if not isinstance(value, list):
460
+ return False
461
+
462
+ if not arguments:
463
+ return True
464
+
465
+ item_type = arguments[0]
466
+
467
+ return all(self._matches_type(item, item_type) for item in value)
468
+
469
+ if origin is dict:
470
+ if not isinstance(value, dict):
471
+ return False
472
+
473
+ if len(arguments) != 2:
474
+ return True
475
+
476
+ key_type, value_type = arguments
477
+
478
+ return all(
479
+ self._matches_type(key, key_type)
480
+ and self._matches_type(item, value_type)
481
+ for key, item in value.items()
482
+ )
483
+
484
+ if origin is tuple:
485
+ return isinstance(value, tuple)
486
+
487
+ if origin is set:
488
+ return isinstance(value, set)
489
+
490
+ if origin is not None:
491
+ try:
492
+ return isinstance(value, origin)
493
+ except TypeError:
494
+ return True
495
+
496
+ try:
497
+ return isinstance(value, expected_type)
498
+ except TypeError:
499
+ # Some typing constructs cannot be checked safely
500
+ # through isinstance().
501
+ return True
502
+
503
+ def _build_injected_kwargs(
504
+ self,
505
+ function: Callable[..., Any],
506
+ signature: inspect.Signature,
507
+ args: tuple[Any, ...],
508
+ kwargs: dict[str, Any],
509
+ store_type: type[BaseStore],
510
+ attribute_name: str,
511
+ parameter_name: str,
512
+ ) -> dict[str, Any]:
513
+ call_kwargs = dict(kwargs)
514
+
515
+ try:
516
+ bound_arguments = signature.bind_partial(
517
+ *args,
518
+ **call_kwargs,
519
+ )
520
+ except TypeError:
521
+ # Preserve the original function call error.
522
+ return call_kwargs
523
+
524
+ if parameter_name not in bound_arguments.arguments:
525
+ call_kwargs[parameter_name] = self.get(
526
+ store_type,
527
+ attribute_name,
528
+ )
529
+
530
+ return call_kwargs
531
+
532
+ def _remove_store_subscribers(
533
+ self,
534
+ store_type: type[BaseStore],
535
+ ) -> None:
536
+ keys = [key for key in self._subscribers if key[0] is store_type]
537
+
538
+ for key in keys:
539
+ del self._subscribers[key]
540
+
541
+ @staticmethod
542
+ def _values_equal(
543
+ old_value: Any,
544
+ new_value: Any,
545
+ ) -> bool:
546
+ try:
547
+ return bool(old_value == new_value)
548
+ except Exception:
549
+ return old_value is new_value