python-library-reactive-model 0.1.2__tar.gz → 0.1.3__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.
Files changed (15) hide show
  1. {python_library_reactive_model-0.1.2 → python_library_reactive_model-0.1.3}/.gitignore +3 -1
  2. {python_library_reactive_model-0.1.2 → python_library_reactive_model-0.1.3}/PKG-INFO +1 -1
  3. {python_library_reactive_model-0.1.2 → python_library_reactive_model-0.1.3}/pyproject.toml +1 -1
  4. python_library_reactive_model-0.1.3/reactive_model/__init__.py +13 -0
  5. python_library_reactive_model-0.1.2/reactive_model/computed.py → python_library_reactive_model-0.1.3/reactive_model/computed_model.py +1 -1
  6. python_library_reactive_model-0.1.3/reactive_model/computed_property.py +24 -0
  7. python_library_reactive_model-0.1.2/reactive_model/dict_ref.py → python_library_reactive_model-0.1.3/reactive_model/dict_ref_model.py +1 -1
  8. python_library_reactive_model-0.1.2/reactive_model/list_ref.py → python_library_reactive_model-0.1.3/reactive_model/list_ref_model.py +27 -9
  9. python_library_reactive_model-0.1.2/reactive_model/ref.py → python_library_reactive_model-0.1.3/reactive_model/ref_model.py +1 -1
  10. python_library_reactive_model-0.1.3/tests/test_computed_property.py +110 -0
  11. python_library_reactive_model-0.1.2/reactive_model/__init__.py +0 -11
  12. /python_library_reactive_model-0.1.2/reactive_model/reactive.py → /python_library_reactive_model-0.1.3/reactive_model/reactive_model.py +0 -0
  13. {python_library_reactive_model-0.1.2 → python_library_reactive_model-0.1.3}/reactive_model/track.py +0 -0
  14. {python_library_reactive_model-0.1.2 → python_library_reactive_model-0.1.3}/test.bat +0 -0
  15. {python_library_reactive_model-0.1.2 → python_library_reactive_model-0.1.3}/tests/test_reactive_model.py +0 -0
@@ -8,4 +8,6 @@ build/
8
8
  .env
9
9
  .pytest_cache/
10
10
  config.yaml
11
- logs/
11
+ logs/
12
+ .cursor/
13
+ uv.lock
@@ -1,4 +1,4 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-library-reactive-model
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-library-reactive-model"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = []
10
10
 
@@ -0,0 +1,13 @@
1
+ from .computed_model import ComputedModel
2
+ from .ref_model import RefModel
3
+ from .dict_ref_model import DictRefModel
4
+ from .list_ref_model import ListRefModel
5
+ from .computed_property import computed_property
6
+
7
+ __all__ = [
8
+ "RefModel",
9
+ "ComputedModel",
10
+ "DictRefModel",
11
+ "ListRefModel",
12
+ "computed_property",
13
+ ]
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Callable, TypeVar, cast
4
4
 
5
- from .reactive import ReactiveModel
5
+ from .reactive_model import ReactiveModel
6
6
  from .track import Collector, Trackable, compute_context, track
7
7
 
8
8
  T = TypeVar("T")
@@ -0,0 +1,24 @@
1
+ from typing import Callable, Generic, TypeVar
2
+
3
+ from reactive_model.computed_model import ComputedModel
4
+
5
+ T = TypeVar("T")
6
+
7
+ class computed_property(Generic[T]):
8
+ def __init__(self, func: Callable[..., T]) -> None:
9
+ self.func = func
10
+ self.storage_name = ""
11
+
12
+ def __set_name__(self, owner, name):
13
+ self.storage_name = f"__computed_{name}"
14
+
15
+ def __get__(self, obj, owner=None) -> T:
16
+ if obj is None:
17
+ return self
18
+
19
+ model = obj.__dict__.get(self.storage_name)
20
+ if model is None:
21
+ model = ComputedModel(lambda: self.func(obj))
22
+ obj.__dict__[self.storage_name] = model
23
+
24
+ return model.value
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Iterable, Iterator, MutableMapping
4
4
  from typing import Generic, TypeVar
5
5
 
6
- from .ref import RefModel
6
+ from .ref_model import RefModel
7
7
  from .track import track
8
8
 
9
9
  K = TypeVar("K")
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Iterable, MutableSequence
4
4
  from typing import Generic, TypeVar, overload
5
5
 
6
- from .ref import RefModel
6
+ from .ref_model import RefModel
7
7
  from .track import track
8
8
 
9
9
  T = TypeVar("T")
@@ -18,6 +18,7 @@ class ListProxy(MutableSequence[T], Generic[T]):
18
18
 
19
19
  @overload
20
20
  def __getitem__(self, index: int) -> T: ...
21
+
21
22
  @overload
22
23
  def __getitem__(self, index: slice) -> list[T]: ...
23
24
 
@@ -26,6 +27,7 @@ class ListProxy(MutableSequence[T], Generic[T]):
26
27
 
27
28
  @overload
28
29
  def __setitem__(self, index: int, value: T) -> None: ...
30
+
29
31
  @overload
30
32
  def __setitem__(self, index: slice, value: Iterable[T]) -> None: ...
31
33
 
@@ -41,25 +43,41 @@ class ListProxy(MutableSequence[T], Generic[T]):
41
43
  self._owner._value.insert(index, value)
42
44
  self._owner.touch()
43
45
 
46
+ def append(self, value: T) -> None:
47
+ self._owner._value.append(value)
48
+ self._owner.touch()
49
+
50
+ def extend(self, values: Iterable[T]) -> None:
51
+ self._owner._value.extend(values)
52
+ self._owner.touch()
53
+
44
54
  def clear(self) -> None:
45
55
  if self._owner._value:
46
56
  self._owner._value.clear()
47
57
  self._owner.touch()
48
58
 
49
- def extend(self, values: Iterable[T]) -> None:
50
- data = list(values)
51
- if data:
52
- self._owner._value.extend(data)
53
- self._owner.touch()
59
+ def pop(self, index: int = -1) -> T:
60
+ item = self._owner._value.pop(index)
61
+ self._owner.touch()
62
+ return item
54
63
 
55
- def sort(self, *, key=None, reverse: bool = False) -> None:
56
- self._owner._value.sort(key=key, reverse=reverse)
64
+ def remove(self, value: T) -> None:
65
+ self._owner._value.remove(value)
57
66
  self._owner.touch()
58
67
 
59
68
  def reverse(self) -> None:
60
69
  self._owner._value.reverse()
61
70
  self._owner.touch()
62
71
 
72
+ def sort(self, *args: object, **kwargs: object) -> None:
73
+ self._owner._value.sort(*args, **kwargs) # type: ignore[arg-type]
74
+ self._owner.touch()
75
+
76
+ def __iadd__(self, other: Iterable[T]) -> ListProxy[T]:
77
+ self._owner._value += list(other)
78
+ self._owner.touch()
79
+ return self
80
+
63
81
  def __repr__(self) -> str:
64
82
  return repr(self._owner._value)
65
83
 
@@ -79,4 +97,4 @@ class ListRefModel(RefModel[list[T]]):
79
97
  @value.setter
80
98
  def value(self, value: list[T]) -> None:
81
99
  self._value = value
82
- self.touch()
100
+ self.touch()
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  from typing import TypeVar
3
3
 
4
- from .reactive import ReactiveModel
4
+ from .reactive_model import ReactiveModel
5
5
  from .track import track
6
6
 
7
7
  T = TypeVar("T")
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import unittest
4
+
5
+ from reactive_model import RefModel, computed_property
6
+
7
+
8
+ class TestComputedProperty(unittest.TestCase):
9
+ def test_access_on_class_returns_descriptor(self) -> None:
10
+ class Host:
11
+ def __init__(self) -> None:
12
+ self.x = RefModel(1)
13
+
14
+ @computed_property
15
+ def doubled(self) -> int:
16
+ return self.x.value * 2
17
+
18
+ desc = Host.doubled
19
+ self.assertIsInstance(desc, computed_property)
20
+ self.assertEqual(desc.storage_name, "__computed_doubled")
21
+
22
+ def test_lazy_compute_and_cache(self) -> None:
23
+ calls: list[int] = []
24
+
25
+ class Host:
26
+ def __init__(self) -> None:
27
+ self.x = RefModel(1)
28
+
29
+ @computed_property
30
+ def doubled(self) -> int:
31
+ calls.append(1)
32
+ return self.x.value * 2
33
+
34
+ h = Host()
35
+ self.assertEqual(calls, [])
36
+ self.assertEqual(h.doubled, 2)
37
+ self.assertEqual(calls, [1])
38
+ self.assertEqual(h.doubled, 2)
39
+ self.assertEqual(calls, [1])
40
+
41
+ def test_invalidates_when_dependency_changes(self) -> None:
42
+ class Host:
43
+ def __init__(self) -> None:
44
+ self.x = RefModel(1)
45
+
46
+ @computed_property
47
+ def doubled(self) -> int:
48
+ return self.x.value * 2
49
+
50
+ h = Host()
51
+ self.assertEqual(h.doubled, 2)
52
+ h.x.value = 3
53
+ self.assertEqual(h.doubled, 6)
54
+
55
+ def test_model_stored_on_instance_dict(self) -> None:
56
+ class Host:
57
+ def __init__(self) -> None:
58
+ self.x = RefModel(10)
59
+
60
+ @computed_property
61
+ def label(self) -> str:
62
+ return f"x={self.x.value}"
63
+
64
+ h = Host()
65
+ self.assertNotIn("__computed_label", h.__dict__)
66
+ _ = h.label
67
+ self.assertIn("__computed_label", h.__dict__)
68
+ model = h.__dict__["__computed_label"]
69
+ self.assertEqual(model.value, "x=10")
70
+
71
+ def test_per_instance_independence(self) -> None:
72
+ class Host:
73
+ def __init__(self, n: int) -> None:
74
+ self.x = RefModel(n)
75
+
76
+ @computed_property
77
+ def doubled(self) -> int:
78
+ return self.x.value * 2
79
+
80
+ a = Host(2)
81
+ b = Host(5)
82
+ self.assertEqual(a.doubled, 4)
83
+ self.assertEqual(b.doubled, 10)
84
+ a.x.value = 3
85
+ self.assertEqual(a.doubled, 6)
86
+ self.assertEqual(b.doubled, 10)
87
+
88
+ def test_two_computed_properties_same_instance(self) -> None:
89
+ class Host:
90
+ def __init__(self) -> None:
91
+ self.x = RefModel(2)
92
+
93
+ @computed_property
94
+ def doubled(self) -> int:
95
+ return self.x.value * 2
96
+
97
+ @computed_property
98
+ def tripled(self) -> int:
99
+ return self.x.value * 3
100
+
101
+ h = Host()
102
+ self.assertEqual(h.doubled, 4)
103
+ self.assertEqual(h.tripled, 6)
104
+ h.x.value = 4
105
+ self.assertEqual(h.doubled, 8)
106
+ self.assertEqual(h.tripled, 12)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ unittest.main()
@@ -1,11 +0,0 @@
1
- from .computed import ComputedModel
2
- from .ref import RefModel
3
- from .dict_ref import DictRefModel
4
- from .list_ref import ListRefModel
5
-
6
- __all__ = [
7
- "RefModel",
8
- "ComputedModel",
9
- "DictRefModel",
10
- "ListRefModel",
11
- ]