datahold 4.0.0.dev5__tar.gz → 4.0.0.dev7__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 (35) hide show
  1. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/PKG-INFO +1 -1
  2. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/pyproject.toml +1 -1
  3. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold/__init__.py +41 -169
  4. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold.egg-info/PKG-INFO +1 -1
  5. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold.egg-info/SOURCES.txt +0 -4
  6. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_FrozenHoldDict.py +56 -2
  7. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_FrozenHoldList.py +55 -2
  8. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_FrozenHoldSet.py +60 -3
  9. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_HoldDict.py +49 -3
  10. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_HoldList.py +48 -2
  11. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_HoldSet.py +48 -1
  12. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/tests/test_testdata.py +54 -0
  13. datahold-4.0.0.dev7/tests/testdata.toml +136 -0
  14. datahold-4.0.0.dev5/tests/test_datahold_TestCopy.py +0 -81
  15. datahold-4.0.0.dev5/tests/test_datahold_TestDataAttribute.py +0 -46
  16. datahold-4.0.0.dev5/tests/test_datahold_TestFrozenMutability.py +0 -36
  17. datahold-4.0.0.dev5/tests/test_datahold_TestMutableBehavior.py +0 -33
  18. datahold-4.0.0.dev5/tests/testdata.toml +0 -209
  19. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/LICENSE.txt +0 -0
  20. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/MANIFEST.in +0 -0
  21. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/README.rst +0 -0
  22. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v1.1.rst +0 -0
  23. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v1.2.rst +0 -0
  24. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v2.0.rst +0 -0
  25. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v2.1.rst +0 -0
  26. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v2.2.rst +0 -0
  27. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v2.3.rst +0 -0
  28. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v3.0.rst +0 -0
  29. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/docs/v4.0.rst +0 -0
  30. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/run_tests.py +0 -0
  31. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/setup.cfg +0 -0
  32. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold/py.typed +0 -0
  33. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold.egg-info/dependency_links.txt +0 -0
  34. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold.egg-info/requires.txt +0 -0
  35. {datahold-4.0.0.dev5 → datahold-4.0.0.dev7}/src/datahold.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datahold
3
- Version: 4.0.0.dev5
3
+ Version: 4.0.0.dev7
4
4
  Summary: This project wraps common mutable datastructures for inheritance with modification.
5
5
  Author-email: Johannes <johannes.programming@gmail.com>
6
6
  License-Expression: MIT
@@ -34,7 +34,7 @@ license-files = [
34
34
  name = "datahold"
35
35
  readme = "README.rst"
36
36
  requires-python = ">=3.12"
37
- version = "4.0.0.dev5"
37
+ version = "4.0.0.dev7"
38
38
 
39
39
  [project.urls]
40
40
  Download = "https://pypi.org/project/datahold/#files"
@@ -10,19 +10,13 @@ __all__: list[str] = [
10
10
  "BaseDataMapping",
11
11
  "BaseDataSequence",
12
12
  "BaseDataSet",
13
- "BaseHoldCollection",
13
+ "BaseHoldObject",
14
14
  "DataDict",
15
15
  "DataList",
16
16
  "DataSet",
17
17
  "FrozenDataDict",
18
18
  "FrozenDataList",
19
19
  "FrozenDataSet",
20
- "FrozenHoldDict",
21
- "FrozenHoldList",
22
- "FrozenHoldSet",
23
- "HoldDict",
24
- "HoldList",
25
- "HoldSet",
26
20
  ]
27
21
 
28
22
  import enum
@@ -45,6 +39,11 @@ from frozendict import frozendict
45
39
  ### UTILS ###
46
40
 
47
41
 
42
+ class Copyable(Protocol):
43
+ @setdoc.basic
44
+ def copy(self: Self) -> Self: ...
45
+
46
+
48
47
  class Missing(enum.Enum):
49
48
  missing = None
50
49
 
@@ -61,6 +60,23 @@ class SupportsKeysAndGetitem[Key, Value](Protocol):
61
60
  def keys(self: Self) -> abc.Iterable[Key]: ...
62
61
 
63
62
 
63
+ ### OBJECT ###
64
+
65
+
66
+ class BaseHoldObject[HoldData: Copyable]:
67
+ """Provide abc for usable classes with slots."""
68
+
69
+ __slots__ = ("_data",)
70
+
71
+ @setdoc.basic
72
+ def __fget__(self: Self) -> HoldData:
73
+ return self._data.copy()
74
+
75
+ @setdoc.basic
76
+ def __fset__(self: Self, data: HoldData, /) -> None:
77
+ self._data: HoldData = data
78
+
79
+
64
80
  ### COLLECTION ###
65
81
 
66
82
 
@@ -103,12 +119,6 @@ class BaseDataCollection[Item](
103
119
  def __fget__(self: Self) -> Data[Item]: ...
104
120
 
105
121
 
106
- class BaseHoldCollection[Item](BaseDataCollection[Item]):
107
- """Provide abc for usable classes with slots."""
108
-
109
- __slots__ = ("_data",)
110
-
111
-
112
122
  ### ABSTRACT SET ###
113
123
 
114
124
 
@@ -134,7 +144,7 @@ class BaseDataSet[Item: abc.Hashable](BaseDataAbstractSet[Item]):
134
144
 
135
145
  @abstractmethod
136
146
  @setdoc.basic
137
- def __fget__(self: Self) -> set[Item]: ...
147
+ def __fget__(self: Self) -> Data[Item]: ...
138
148
 
139
149
  @abstractmethod
140
150
  @setdoc.basic
@@ -189,23 +199,6 @@ class FrozenDataSet[Item: abc.Hashable](BaseDataSet[Item], abc.Hashable):
189
199
  return hash(frozenset(self.__fget__()))
190
200
 
191
201
 
192
- class FrozenHoldSet[Item: abc.Hashable](
193
- FrozenDataSet[Item],
194
- BaseHoldCollection[Item],
195
- ):
196
- """Provide usable frozen set-like with slots."""
197
-
198
- __slots__ = ()
199
-
200
- @setdoc.basic
201
- def __fget__(self: Self) -> FrozenHoldSet.Data[Item]:
202
- return set(self._data)
203
-
204
- @setdoc.basic
205
- def __fset__(self: Self, data: FrozenHoldSet.Data[Item], /) -> None:
206
- self._data: FrozenHoldSet.Data[Item] = data
207
-
208
-
209
202
  class DataSet[Item: abc.Hashable](
210
203
  BaseDataSet[Item],
211
204
  abc.MutableSet[Item],
@@ -214,18 +207,6 @@ class DataSet[Item: abc.Hashable](
214
207
 
215
208
  __slots__ = ()
216
209
 
217
- @abstractmethod
218
- @setdoc.basic
219
- def __fget__(self: Self) -> DataSet.Data[Item]: ...
220
-
221
- @abstractmethod
222
- @setdoc.basic
223
- def __fset__(
224
- self: Self,
225
- value: DataSet.Data[Item],
226
- /,
227
- ) -> None: ...
228
-
229
210
  @setdoc.basic
230
211
  def add(self: Self, item: Item, /) -> None:
231
212
  self.__fset__(self.__fget__() | {item})
@@ -241,57 +222,40 @@ class DataSet[Item: abc.Hashable](
241
222
  *others: abc.Iterable[abc.Hashable],
242
223
  ) -> None:
243
224
  data: set[Item]
244
- data = set(self.__fget__())
225
+ data = self.__fget__()
245
226
  data.difference_update(*others)
246
- self.__fset__(set(data))
227
+ self.__fset__(data)
247
228
 
248
229
  @setdoc.basic
249
230
  def discard(self: Self, item: abc.Hashable, /) -> None:
250
231
  data: set[Item]
251
- data = set(self.__fget__())
232
+ data = self.__fget__()
252
233
  data.discard(item)
253
- self.__fset__(set(data))
234
+ self.__fset__(data)
254
235
 
255
236
  @setdoc.basic
256
237
  def intersection_update(
257
238
  self: Self, /, *others: abc.Iterable[abc.Hashable]
258
239
  ) -> None:
259
240
  data: set[Item]
260
- data = set(self.__fget__())
241
+ data = self.__fget__()
261
242
  data.intersection_update(*others)
262
- self.__fset__(set(data))
243
+ self.__fset__(data)
263
244
 
264
245
  @setdoc.basic
265
246
  def symmetric_difference_update(
266
247
  self: Self, other: abc.Iterable[Item], /
267
248
  ) -> None:
268
249
  data: set[Item]
269
- data = set(self.__fget__())
250
+ data = self.__fget__()
270
251
  data.symmetric_difference_update(other)
271
- self.__fset__(set(data))
252
+ self.__fset__(data)
272
253
 
273
254
  @setdoc.basic
274
255
  def update(self: Self, /, *others: abc.Iterable[Item]) -> None:
275
256
  self.__fset__(self.__fget__().union(*others))
276
257
 
277
258
 
278
- class HoldSet[Item: abc.Hashable](
279
- DataSet[Item],
280
- BaseHoldCollection[Item],
281
- ):
282
- """Provide usable mutable set-like with slots."""
283
-
284
- __slots__ = ()
285
-
286
- @setdoc.basic
287
- def __fget__(self: Self) -> HoldSet.Data[Item]:
288
- return set(self._data)
289
-
290
- @setdoc.basic
291
- def __fset__(self: Self, value: HoldSet.Data[Item], /) -> None:
292
- self._data: HoldSet.Data[Item] = value
293
-
294
-
295
259
  ### MAPPING ###
296
260
  class BaseDataMapping[Key, Value](
297
261
  BaseDataCollection[Key], abc.Mapping[Key, Value]
@@ -337,6 +301,10 @@ class BaseDataDict[Key: abc.Hashable, Value](
337
301
  | abc.Iterable[tuple[DataKey | str, Optional[DataValue]]]
338
302
  )
339
303
 
304
+ @abstractmethod
305
+ @setdoc.basic
306
+ def __fget__(self: Self) -> Data[Key, Value]: ...
307
+
340
308
  @abstractmethod
341
309
  @setdoc.basic
342
310
  def __fset__(self: Self, data: Data[Key, Value], /) -> None: ...
@@ -369,10 +337,6 @@ class BaseDataDict[Key: abc.Hashable, Value](
369
337
 
370
338
  # __ror__ is unnecessary because of how __or__ is defined
371
339
 
372
- @abstractmethod
373
- @setdoc.basic
374
- def __fget__(self: Self) -> Data[Key, Value]: ...
375
-
376
340
  @classmethod
377
341
  @setdoc.basic
378
342
  def fromkeys(
@@ -397,23 +361,6 @@ class FrozenDataDict[Key: abc.Hashable, Value](
397
361
  return hash(frozendict(self.__fget__()))
398
362
 
399
363
 
400
- class FrozenHoldDict[Key: abc.Hashable, Value](
401
- FrozenDataDict[Key, Value],
402
- BaseHoldCollection[Key | str],
403
- ):
404
- """Provide usable frozen dict-like with slots."""
405
-
406
- __slots__ = ()
407
-
408
- @setdoc.basic
409
- def __fget__(self: Self) -> FrozenHoldDict.Data[Key, Value]:
410
- return dict(self._data)
411
-
412
- @setdoc.basic
413
- def __fset__(self: Self, data: FrozenHoldDict.Data[Key, Value], /) -> None:
414
- self._data: FrozenHoldDict.Data[Key, Value] = data
415
-
416
-
417
364
  class DataDict[Key: abc.Hashable, Value](
418
365
  BaseDataDict[Key, Value],
419
366
  abc.MutableMapping[Key | str, Optional[Value]],
@@ -429,14 +376,6 @@ class DataDict[Key: abc.Hashable, Value](
429
376
  del data[key]
430
377
  self.__fset__(data)
431
378
 
432
- @abstractmethod
433
- @setdoc.basic
434
- def __fget__(self: Self) -> DataDict.Data[Key, Value]: ...
435
-
436
- @abstractmethod
437
- @setdoc.basic
438
- def __fset__(self: Self, data: DataDict.Data[Key, Value], /) -> None: ...
439
-
440
379
  @setdoc.basic
441
380
  def __ior__(
442
381
  self: Self,
@@ -464,27 +403,6 @@ class DataDict[Key: abc.Hashable, Value](
464
403
  return type(self)(self)
465
404
 
466
405
 
467
- class HoldDict[Key: abc.Hashable, Value](
468
- DataDict[Key, Value],
469
- BaseHoldCollection[Key | str],
470
- ):
471
- """Provide usable mutable dict-like with slots."""
472
-
473
- __slots__ = ()
474
-
475
- @setdoc.basic
476
- def __fget__(self: Self) -> HoldDict.Data[Key, Value]:
477
- return dict(self._data)
478
-
479
- @setdoc.basic
480
- def __fset__(
481
- self: Self,
482
- value: HoldDict.Data[Key, Value],
483
- /,
484
- ) -> None:
485
- self._data: HoldDict.Data[Key, Value] = value
486
-
487
-
488
406
  ### SEQUENCE ###
489
407
 
490
408
 
@@ -568,7 +486,11 @@ class BaseDataList[Item](BaseDataSequence[Item]):
568
486
 
569
487
  @abstractmethod
570
488
  @setdoc.basic
571
- def __fset__(self: Self, value: list[Item], /) -> None: ...
489
+ def __fget__(self: Self) -> Data[Item]: ...
490
+
491
+ @abstractmethod
492
+ @setdoc.basic
493
+ def __fset__(self: Self, data: Data[Item], /) -> None: ...
572
494
 
573
495
  @setdoc.basic
574
496
  def __ge__(self: Self, other: object, /) -> NotImplementedType | bool:
@@ -629,10 +551,6 @@ class BaseDataList[Item](BaseDataSequence[Item]):
629
551
 
630
552
  __rmul__ = __mul__
631
553
 
632
- @abstractmethod
633
- @setdoc.basic
634
- def __fget__(self: Self) -> Data[Item]: ...
635
-
636
554
 
637
555
  class FrozenDataList[Item](
638
556
  BaseDataList[Item],
@@ -647,23 +565,6 @@ class FrozenDataList[Item](
647
565
  return hash(tuple(self.__fget__()))
648
566
 
649
567
 
650
- class FrozenHoldList[Item](
651
- FrozenDataList[Item],
652
- BaseHoldCollection[Item],
653
- ):
654
- """Provide usable frozen list-like with slots."""
655
-
656
- __slots__ = ()
657
-
658
- @setdoc.basic
659
- def __fget__(self: Self) -> FrozenHoldList.Data[Item]:
660
- return list(self._data)
661
-
662
- @setdoc.basic
663
- def __fset__(self: Self, data: FrozenHoldList.Data[Item], /) -> None:
664
- self._data: FrozenHoldList.Data[Item] = data
665
-
666
-
667
568
  class DataList[Item](
668
569
  BaseDataList[Item],
669
570
  abc.MutableSequence[Item],
@@ -681,18 +582,6 @@ class DataList[Item](
681
582
  del data[other]
682
583
  self.__fset__(data)
683
584
 
684
- @abstractmethod
685
- @setdoc.basic
686
- def __fget__(self: Self) -> DataList.Data[Item]: ...
687
-
688
- @abstractmethod
689
- @setdoc.basic
690
- def __fset__(
691
- self: Self,
692
- value: DataList.Data[Item],
693
- /,
694
- ) -> None: ...
695
-
696
585
  @setdoc.basic
697
586
  def __imul__(self: Self, other: SupportsIndex, /) -> Self:
698
587
  self.__fset__(self.__fget__() * other)
@@ -742,20 +631,3 @@ class DataList[Item](
742
631
  data = self.__fget__()
743
632
  data.sort(key=key, reverse=reverse)
744
633
  self.__fset__(data)
745
-
746
-
747
- class HoldList[Item](
748
- DataList[Item],
749
- BaseHoldCollection[Item],
750
- ):
751
- """Provide usable mutable list-like with slots."""
752
-
753
- __slots__ = ()
754
-
755
- @setdoc.basic
756
- def __fget__(self: Self) -> HoldList.Data[Item]:
757
- return list(self._data)
758
-
759
- @setdoc.basic
760
- def __fset__(self: Self, value: HoldList.Data[Item], /) -> None:
761
- self._data: HoldList.Data[Item] = value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datahold
3
- Version: 4.0.0.dev5
3
+ Version: 4.0.0.dev7
4
4
  Summary: This project wraps common mutable datastructures for inheritance with modification.
5
5
  Author-email: Johannes <johannes.programming@gmail.com>
6
6
  License-Expression: MIT
@@ -25,9 +25,5 @@ tests/test_FrozenHoldSet.py
25
25
  tests/test_HoldDict.py
26
26
  tests/test_HoldList.py
27
27
  tests/test_HoldSet.py
28
- tests/test_datahold_TestCopy.py
29
- tests/test_datahold_TestDataAttribute.py
30
- tests/test_datahold_TestFrozenMutability.py
31
- tests/test_datahold_TestMutableBehavior.py
32
28
  tests/test_testdata.py
33
29
  tests/testdata.toml
@@ -1,11 +1,55 @@
1
- __all__: list[str] = ["TestFrozenHoldDict"]
1
+ __all__: list[str] = [
2
+ "TestCopy",
3
+ "TestDataAttribute",
4
+ "TestFrozenHoldDict",
5
+ "TestFrozenMutability",
6
+ ]
2
7
 
3
8
  import unittest
9
+ from collections import abc
4
10
  from typing import Any, Self
5
11
 
6
12
  from frozendict import frozendict
7
13
 
8
- from datahold import FrozenHoldDict
14
+ from datahold import BaseHoldObject, FrozenDataDict
15
+
16
+
17
+ class FrozenHoldDict[Key: abc.Hashable, Value](
18
+ BaseHoldObject[FrozenDataDict.Data[Key, Value]],
19
+ FrozenDataDict[Key, Value],
20
+ ):
21
+ """Provide usable frozen dict-like with slots."""
22
+
23
+ __slots__ = ()
24
+
25
+
26
+ class TestCopy(unittest.TestCase):
27
+
28
+ def test_frozen_have_no_copy_2(self: Self) -> None:
29
+ """
30
+ Frozen classes should not define their own copy method.
31
+ (If a parent class or wrapped object exposes one, we ignore that.)
32
+ """
33
+ args: Any
34
+ cls: Any
35
+ copy_obj: Any
36
+ obj: Any
37
+ cls, args = (FrozenHoldDict, ({"a": 1},))
38
+ # They must not *define* copy themselves
39
+ self.assertNotIn("copy", cls.__dict__)
40
+ # Optional: if they *do* expose copy on the instance, it should not
41
+ # create a mutable variant; you can drop this if you prefer.
42
+ obj = cls(*args)
43
+ if hasattr(obj, "copy"):
44
+ copy_obj = obj.copy()
45
+ self.assertIsInstance(copy_obj, cls)
46
+
47
+
48
+ class TestDataAttribute(unittest.TestCase):
49
+ def test_dict_data_is_immutable_mapping(self: Self) -> None:
50
+ f: FrozenHoldDict[Any, Any]
51
+ f = FrozenHoldDict({"a": 1})
52
+ self.assertIsInstance(f.__fget__(), dict)
9
53
 
10
54
 
11
55
  class TestFrozenHoldDict(unittest.TestCase):
@@ -127,5 +171,15 @@ class TestFrozenHoldDict(unittest.TestCase):
127
171
  )
128
172
 
129
173
 
174
+ class TestFrozenMutability(unittest.TestCase):
175
+ def test_frozen_dict_cannot_mutate(self: Self) -> None:
176
+ f: Any
177
+ f = FrozenHoldDict({"a": 1})
178
+ with self.assertRaises((TypeError, AttributeError)):
179
+ f["b"] = 2
180
+ with self.assertRaises((TypeError, AttributeError)):
181
+ f.pop("a", None)
182
+
183
+
130
184
  if __name__ == "__main__":
131
185
  unittest.main()
@@ -1,8 +1,51 @@
1
- __all__: list[str] = ["TestFrozenHoldLists"]
1
+ __all__: list[str] = [
2
+ "TestCopy",
3
+ "TestDataAttribute",
4
+ "TestFrozenHoldLists",
5
+ "TestFrozenMutability",
6
+ ]
2
7
  import unittest
3
8
  from typing import Any, Self
4
9
 
5
- from datahold import FrozenHoldList
10
+ from datahold import BaseHoldObject, FrozenDataList
11
+
12
+
13
+ class FrozenHoldList[Item](
14
+ BaseHoldObject[FrozenDataList.Data[Item]],
15
+ FrozenDataList[Item],
16
+ ):
17
+ """Provide usable frozen list-like with slots."""
18
+
19
+ __slots__ = ()
20
+
21
+
22
+ class TestCopy(unittest.TestCase):
23
+
24
+ def test_frozen_have_no_copy_2(self: Self) -> None:
25
+ """
26
+ Frozen classes should not define their own copy method.
27
+ (If a parent class or wrapped object exposes one, we ignore that.)
28
+ """
29
+ args: Any
30
+ cls: Any
31
+ copy_obj: Any
32
+ obj: Any
33
+ cls, args = (FrozenHoldList, ([1, 2],))
34
+ # They must not *define* copy themselves
35
+ self.assertNotIn("copy", cls.__dict__)
36
+ # Optional: if they *do* expose copy on the instance, it should not
37
+ # create a mutable variant; you can drop this if you prefer.
38
+ obj = cls(*args)
39
+ if hasattr(obj, "copy"):
40
+ copy_obj = obj.copy()
41
+ self.assertIsInstance(copy_obj, cls)
42
+
43
+
44
+ class TestDataAttribute(unittest.TestCase):
45
+ def test_list_data_is_tuple(self: Self) -> None:
46
+ f: FrozenHoldList[Any]
47
+ f = FrozenHoldList([1, 2, 3])
48
+ self.assertIsInstance(f.__fget__(), list)
6
49
 
7
50
 
8
51
  class TestFrozenHoldList(unittest.TestCase):
@@ -144,5 +187,15 @@ class TestFrozenHoldList(unittest.TestCase):
144
187
  self.obj.index(99)
145
188
 
146
189
 
190
+ class TestFrozenMutability(unittest.TestCase):
191
+ def test_frozen_list_cannot_mutate(self: Self) -> None:
192
+ f: Any
193
+ f = FrozenHoldList([1, 2, 3])
194
+ with self.assertRaises((TypeError, AttributeError)):
195
+ f.append(4)
196
+ with self.assertRaises((TypeError, AttributeError)):
197
+ f.pop()
198
+
199
+
147
200
  if __name__ == "__main__":
148
201
  unittest.main()
@@ -1,8 +1,54 @@
1
- __all__: list[str] = ["TestFrozenHoldSet"]
1
+ __all__: list[str] = [
2
+ "TestCopy",
3
+ "TestDataAttribute",
4
+ "TestFrozenHoldSet",
5
+ "TestFrozenMutability",
6
+ ]
2
7
  import unittest
3
- from typing import Self
8
+ from collections import abc
9
+ from typing import Any, Self
4
10
 
5
- from datahold import FrozenHoldSet
11
+ from datahold import BaseHoldObject, FrozenDataSet
12
+
13
+
14
+ class FrozenHoldSet[Item: abc.Hashable](
15
+ BaseHoldObject[FrozenDataSet.Data[Item]],
16
+ FrozenDataSet[Item],
17
+ ):
18
+ """Provide usable frozen set-like with slots."""
19
+
20
+ __slots__ = ()
21
+
22
+
23
+ class TestCopy(unittest.TestCase):
24
+
25
+ def test_frozen_have_no_copy_2(self: Self) -> None:
26
+ """
27
+ Frozen classes should not define their own copy method.
28
+ (If a parent class or wrapped object exposes one, we ignore that.)
29
+ """
30
+ args: Any
31
+ cls: Any
32
+ copy_obj: Any
33
+ obj: Any
34
+ cls = FrozenHoldSet
35
+ args = ({1, 2},)
36
+ # They must not *define* copy themselves
37
+ self.assertNotIn("copy", cls.__dict__)
38
+
39
+ # Optional: if they *do* expose copy on the instance, it should not
40
+ # create a mutable variant; you can drop this if you prefer.
41
+ obj = cls(*args)
42
+ if hasattr(obj, "copy"):
43
+ copy_obj = obj.copy()
44
+ self.assertIsInstance(copy_obj, cls)
45
+
46
+
47
+ class TestDataAttribute(unittest.TestCase):
48
+ def test_set_data_is_frozenset(self: Self) -> None:
49
+ f: FrozenHoldSet[Any]
50
+ f = FrozenHoldSet({1, 2, 3})
51
+ self.assertIsInstance(f.__fget__(), set)
6
52
 
7
53
 
8
54
  class TestFrozenHoldSet(unittest.TestCase):
@@ -185,5 +231,16 @@ class TestFrozenHoldSet(unittest.TestCase):
185
231
  )
186
232
 
187
233
 
234
+ class TestFrozenMutability(unittest.TestCase):
235
+
236
+ def test_frozen_set_cannot_mutate(self: Self) -> None:
237
+ f: Any
238
+ f = FrozenHoldSet({1, 2, 3})
239
+ with self.assertRaises((TypeError, AttributeError)):
240
+ f.add(4)
241
+ with self.assertRaises((TypeError, AttributeError)):
242
+ f.remove(1)
243
+
244
+
188
245
  if __name__ == "__main__":
189
246
  unittest.main()
@@ -1,8 +1,45 @@
1
- __all__: list[str] = ["TestHoldDict"]
1
+ __all__: list[str] = [
2
+ "TestDataAttribute",
3
+ "TestHoldDict",
4
+ "TestMutableBehavior",
5
+ "TestCopy",
6
+ ]
2
7
  import unittest
3
- from typing import Any, Self
8
+ from collections import abc
9
+ from typing import Any, Self, cast
4
10
 
5
- from datahold import HoldDict
11
+ from datahold import BaseHoldObject, DataDict
12
+
13
+
14
+ class HoldDict[Key: abc.Hashable, Value](
15
+ BaseHoldObject[DataDict.Data[Key, Value]],
16
+ DataDict[Key, Value],
17
+ ):
18
+ """Provide usable mutable dict-like with slots."""
19
+
20
+ __slots__ = ()
21
+
22
+
23
+ class TestCopy(unittest.TestCase):
24
+ def test_mutable_copy_returns_same_type_and_is_shallow(self: Self) -> None:
25
+ d: HoldDict[str, dict[str, int]]
26
+ d_copy: HoldDict[str, dict[str, int]]
27
+ d = HoldDict({"a": {"x": 1}})
28
+ d_copy = d.copy()
29
+ self.assertIsInstance(d_copy, type(d))
30
+ self.assertIsNot(d_copy, d)
31
+ self.assertEqual(dict(d_copy), dict(d))
32
+
33
+ # shallow: inner object is shared
34
+ cast(dict[str, int], d["a"])["x"] = 2
35
+ self.assertEqual(cast(dict[str, int], d_copy["a"])["x"], 2)
36
+
37
+
38
+ class TestDataAttribute(unittest.TestCase):
39
+ def test_dict_data_is_immutable_mapping(self: Self) -> None:
40
+ m: HoldDict[Any, Any]
41
+ m = HoldDict({"a": 1})
42
+ self.assertIsInstance(m.__fget__(), dict)
6
43
 
7
44
 
8
45
  class TestHoldDict(unittest.TestCase):
@@ -164,5 +201,14 @@ class TestHoldDict(unittest.TestCase):
164
201
  self.assertIsNot(result, self.obj)
165
202
 
166
203
 
204
+ class TestMutableBehavior(unittest.TestCase):
205
+ def test_hold_dict_mutates_and_syncs_data(self: Self) -> None:
206
+ x: HoldDict[Any, Any]
207
+ x = HoldDict({"a": 1})
208
+ x["b"] = 2
209
+ self.assertEqual(x["b"], 2)
210
+ self.assertEqual(x.__fget__()["b"], 2)
211
+
212
+
167
213
  if __name__ == "__main__":
168
214
  unittest.main()