py10x-universe 0.1.3__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.
Files changed (214) hide show
  1. core_10x/__init__.py +42 -0
  2. core_10x/backbone/__init__.py +0 -0
  3. core_10x/backbone/backbone_store.py +59 -0
  4. core_10x/backbone/backbone_traitable.py +30 -0
  5. core_10x/backbone/backbone_user.py +66 -0
  6. core_10x/backbone/bound_data_domain.py +49 -0
  7. core_10x/backbone/namespace.py +101 -0
  8. core_10x/backbone/vault.py +38 -0
  9. core_10x/code_samples/__init__.py +0 -0
  10. core_10x/code_samples/_package_manifest.py +3 -0
  11. core_10x/code_samples/directories.py +181 -0
  12. core_10x/code_samples/person.py +76 -0
  13. core_10x/concrete_traits.py +356 -0
  14. core_10x/conftest.py +12 -0
  15. core_10x/curve.py +321 -0
  16. core_10x/data_domain.py +48 -0
  17. core_10x/data_domain_binder.py +45 -0
  18. core_10x/directory.py +250 -0
  19. core_10x/entity.py +8 -0
  20. core_10x/entity_filter.py +5 -0
  21. core_10x/environment_variables.py +147 -0
  22. core_10x/exec_control.py +84 -0
  23. core_10x/experimental/__init__.py +0 -0
  24. core_10x/experimental/data_protocol_ex.py +34 -0
  25. core_10x/global_cache.py +121 -0
  26. core_10x/manual_tests/__init__.py +0 -0
  27. core_10x/manual_tests/calendar_test.py +35 -0
  28. core_10x/manual_tests/ctor_update_bug.py +58 -0
  29. core_10x/manual_tests/debug_graph_on.py +17 -0
  30. core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
  31. core_10x/manual_tests/enum_bits_test.py +17 -0
  32. core_10x/manual_tests/env_vars_trivial_test.py +12 -0
  33. core_10x/manual_tests/existing_traitable.py +33 -0
  34. core_10x/manual_tests/k10x_test1.py +13 -0
  35. core_10x/manual_tests/named_constant_test.py +121 -0
  36. core_10x/manual_tests/nucleus_trivial_test.py +42 -0
  37. core_10x/manual_tests/polars_test.py +14 -0
  38. core_10x/manual_tests/py_class_test.py +4 -0
  39. core_10x/manual_tests/rc_test.py +42 -0
  40. core_10x/manual_tests/rdate_test.py +12 -0
  41. core_10x/manual_tests/reference_serialization_bug.py +19 -0
  42. core_10x/manual_tests/resource_trivial_test.py +10 -0
  43. core_10x/manual_tests/store_uri_test.py +6 -0
  44. core_10x/manual_tests/trait_definition_test.py +19 -0
  45. core_10x/manual_tests/trait_filter_test.py +15 -0
  46. core_10x/manual_tests/trait_flag_modification_test.py +42 -0
  47. core_10x/manual_tests/trait_modification_bug.py +26 -0
  48. core_10x/manual_tests/traitable_as_of_test.py +82 -0
  49. core_10x/manual_tests/traitable_heir_test.py +39 -0
  50. core_10x/manual_tests/traitable_history_test.py +41 -0
  51. core_10x/manual_tests/traitable_serialization_test.py +54 -0
  52. core_10x/manual_tests/traitable_trivial_test.py +71 -0
  53. core_10x/manual_tests/trivial_graph_test.py +16 -0
  54. core_10x/manual_tests/ts_class_association_test.py +64 -0
  55. core_10x/manual_tests/ts_trivial_test.py +35 -0
  56. core_10x/named_constant.py +425 -0
  57. core_10x/nucleus.py +81 -0
  58. core_10x/package_manifest.py +85 -0
  59. core_10x/package_refactoring.py +153 -0
  60. core_10x/py_class.py +431 -0
  61. core_10x/rc.py +155 -0
  62. core_10x/rdate.py +339 -0
  63. core_10x/resource.py +189 -0
  64. core_10x/roman_number.py +67 -0
  65. core_10x/testlib/__init__.py +0 -0
  66. core_10x/testlib/test_store.py +240 -0
  67. core_10x/testlib/traitable_history_tests.py +787 -0
  68. core_10x/testlib/ts_tests.py +280 -0
  69. core_10x/trait.py +377 -0
  70. core_10x/trait_definition.py +176 -0
  71. core_10x/trait_filter.py +205 -0
  72. core_10x/trait_method_error.py +36 -0
  73. core_10x/traitable.py +1082 -0
  74. core_10x/traitable_cli.py +153 -0
  75. core_10x/traitable_heir.py +33 -0
  76. core_10x/traitable_id.py +31 -0
  77. core_10x/ts_store.py +172 -0
  78. core_10x/ts_store_type.py +26 -0
  79. core_10x/ts_union.py +147 -0
  80. core_10x/ui_hint.py +153 -0
  81. core_10x/unit_tests/test_concrete_traits.py +156 -0
  82. core_10x/unit_tests/test_converters.py +51 -0
  83. core_10x/unit_tests/test_curve.py +157 -0
  84. core_10x/unit_tests/test_directory.py +54 -0
  85. core_10x/unit_tests/test_documentation.py +172 -0
  86. core_10x/unit_tests/test_environment_variables.py +15 -0
  87. core_10x/unit_tests/test_filters.py +239 -0
  88. core_10x/unit_tests/test_graph.py +348 -0
  89. core_10x/unit_tests/test_named_constant.py +98 -0
  90. core_10x/unit_tests/test_rc.py +11 -0
  91. core_10x/unit_tests/test_rdate.py +484 -0
  92. core_10x/unit_tests/test_trait_method_error.py +80 -0
  93. core_10x/unit_tests/test_trait_modification.py +19 -0
  94. core_10x/unit_tests/test_traitable.py +959 -0
  95. core_10x/unit_tests/test_traitable_history.py +1 -0
  96. core_10x/unit_tests/test_ts_store.py +1 -0
  97. core_10x/unit_tests/test_ts_union.py +369 -0
  98. core_10x/unit_tests/test_ui_nodes.py +81 -0
  99. core_10x/unit_tests/test_xxcalendar.py +471 -0
  100. core_10x/vault/__init__.py +0 -0
  101. core_10x/vault/sec_keys.py +133 -0
  102. core_10x/vault/security_keys_old.py +168 -0
  103. core_10x/vault/vault.py +56 -0
  104. core_10x/vault/vault_traitable.py +56 -0
  105. core_10x/vault/vault_user.py +70 -0
  106. core_10x/xdate_time.py +136 -0
  107. core_10x/xnone.py +71 -0
  108. core_10x/xxcalendar.py +228 -0
  109. infra_10x/__init__.py +0 -0
  110. infra_10x/manual_tests/__init__.py +0 -0
  111. infra_10x/manual_tests/test_misc.py +16 -0
  112. infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
  113. infra_10x/mongodb_admin.py +111 -0
  114. infra_10x/mongodb_store.py +346 -0
  115. infra_10x/mongodb_utils.py +129 -0
  116. infra_10x/unit_tests/conftest.py +13 -0
  117. infra_10x/unit_tests/test_mongo_db.py +36 -0
  118. infra_10x/unit_tests/test_mongo_history.py +1 -0
  119. py10x_universe-0.1.3.dist-info/METADATA +406 -0
  120. py10x_universe-0.1.3.dist-info/RECORD +214 -0
  121. py10x_universe-0.1.3.dist-info/WHEEL +4 -0
  122. py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
  123. ui_10x/__init__.py +0 -0
  124. ui_10x/apps/__init__.py +0 -0
  125. ui_10x/apps/collection_editor_app.py +100 -0
  126. ui_10x/choice.py +212 -0
  127. ui_10x/collection_editor.py +135 -0
  128. ui_10x/concrete_trait_widgets.py +220 -0
  129. ui_10x/conftest.py +8 -0
  130. ui_10x/entity_stocker.py +173 -0
  131. ui_10x/examples/__init__.py +0 -0
  132. ui_10x/examples/_guess_word_data.py +14076 -0
  133. ui_10x/examples/collection_editor.py +17 -0
  134. ui_10x/examples/date_selector.py +14 -0
  135. ui_10x/examples/entity_stocker.py +18 -0
  136. ui_10x/examples/guess_word.py +392 -0
  137. ui_10x/examples/message_box.py +20 -0
  138. ui_10x/examples/multi_choice.py +17 -0
  139. ui_10x/examples/py_data_browser.py +66 -0
  140. ui_10x/examples/radiobox.py +29 -0
  141. ui_10x/examples/single_choice.py +31 -0
  142. ui_10x/examples/style_sheet.py +47 -0
  143. ui_10x/examples/trivial_entity_editor.py +18 -0
  144. ui_10x/platform.py +20 -0
  145. ui_10x/platform_interface.py +517 -0
  146. ui_10x/py_data_browser.py +249 -0
  147. ui_10x/qt6/__init__.py +0 -0
  148. ui_10x/qt6/conftest.py +8 -0
  149. ui_10x/qt6/manual_tests/__init__.py +0 -0
  150. ui_10x/qt6/manual_tests/basic_test.py +35 -0
  151. ui_10x/qt6/platform_implementation.py +275 -0
  152. ui_10x/qt6/utils.py +665 -0
  153. ui_10x/rio/__init__.py +0 -0
  154. ui_10x/rio/apps/examples/examples/__init__.py +22 -0
  155. ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
  156. ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
  157. ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
  158. ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
  159. ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
  160. ui_10x/rio/apps/examples/rio.toml +14 -0
  161. ui_10x/rio/component_builder.py +497 -0
  162. ui_10x/rio/components/__init__.py +9 -0
  163. ui_10x/rio/components/group_box.py +31 -0
  164. ui_10x/rio/components/labeled_checkbox.py +18 -0
  165. ui_10x/rio/components/line_edit.py +37 -0
  166. ui_10x/rio/components/radio_button.py +32 -0
  167. ui_10x/rio/components/separator.py +24 -0
  168. ui_10x/rio/components/splitter.py +121 -0
  169. ui_10x/rio/components/tree_view.py +75 -0
  170. ui_10x/rio/conftest.py +35 -0
  171. ui_10x/rio/internals/__init__.py +0 -0
  172. ui_10x/rio/internals/app.py +192 -0
  173. ui_10x/rio/manual_tests/__init__.py +0 -0
  174. ui_10x/rio/manual_tests/basic_test.py +24 -0
  175. ui_10x/rio/manual_tests/splitter.py +27 -0
  176. ui_10x/rio/platform_implementation.py +91 -0
  177. ui_10x/rio/style_sheet.py +53 -0
  178. ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
  179. ui_10x/rio/unit_tests/test_internals.py +630 -0
  180. ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
  181. ui_10x/rio/widgets/__init__.py +46 -0
  182. ui_10x/rio/widgets/application.py +109 -0
  183. ui_10x/rio/widgets/button.py +48 -0
  184. ui_10x/rio/widgets/button_group.py +60 -0
  185. ui_10x/rio/widgets/calendar.py +23 -0
  186. ui_10x/rio/widgets/checkbox.py +24 -0
  187. ui_10x/rio/widgets/dialog.py +137 -0
  188. ui_10x/rio/widgets/group_box.py +27 -0
  189. ui_10x/rio/widgets/layout.py +34 -0
  190. ui_10x/rio/widgets/line_edit.py +37 -0
  191. ui_10x/rio/widgets/list.py +105 -0
  192. ui_10x/rio/widgets/message_box.py +70 -0
  193. ui_10x/rio/widgets/scroll_area.py +31 -0
  194. ui_10x/rio/widgets/spacer.py +6 -0
  195. ui_10x/rio/widgets/splitter.py +45 -0
  196. ui_10x/rio/widgets/text_edit.py +28 -0
  197. ui_10x/rio/widgets/tree.py +89 -0
  198. ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
  199. ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
  200. ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
  201. ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
  202. ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
  203. ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
  204. ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
  205. ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
  206. ui_10x/table_header_view.py +305 -0
  207. ui_10x/table_view.py +174 -0
  208. ui_10x/trait_editor.py +189 -0
  209. ui_10x/trait_widget.py +131 -0
  210. ui_10x/traitable_editor.py +200 -0
  211. ui_10x/traitable_view.py +131 -0
  212. ui_10x/unit_tests/conftest.py +8 -0
  213. ui_10x/unit_tests/test_platform.py +9 -0
  214. ui_10x/utils.py +661 -0
@@ -0,0 +1,959 @@
1
+ from __future__ import annotations
2
+
3
+ import collections
4
+ import re
5
+ import uuid
6
+ from collections import Counter
7
+ from contextlib import nullcontext
8
+ from datetime import date
9
+ from typing import TYPE_CHECKING, Any, Self
10
+
11
+ import numpy as np
12
+ import pytest
13
+ from core_10x import trait_definition
14
+ from core_10x.code_samples.person import Person
15
+ from core_10x.exec_control import BTP, CACHE_ONLY, GRAPH_ON, INTERACTIVE
16
+ from core_10x.py_class import PyClass
17
+ from core_10x.rc import RC, RC_TRUE
18
+ from core_10x.trait import Trait
19
+ from core_10x.trait_definition import RT, M, T, TraitDefinition, TraitModification
20
+ from core_10x.trait_method_error import TraitMethodError
21
+ from core_10x.traitable import THIS_CLASS, AnonymousTraitable, Traitable, TraitAccessor
22
+ from core_10x.traitable_id import ID
23
+ from core_10x.xnone import XNone
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Generator
27
+
28
+
29
+ class SubTraitable(Traitable):
30
+ trait1: int = RT(T.ID)
31
+ trait2: str = RT()
32
+
33
+ x = '123' # -- not in slots
34
+
35
+
36
+ class SubTraitable2(SubTraitable):
37
+ trait1 = M(flags=(None, T.ID)) # removes ID flag
38
+ trait2: int = M()
39
+ trait3: float = T() // 'trait definition comment'
40
+ trait4: int = RT(0)
41
+
42
+
43
+ class SubTraitable3(SubTraitable):
44
+ trait2: list[str] = M() // 'trait modification comment'
45
+
46
+
47
+ def test_subclass_traits():
48
+ expected_traits = {'trait1', 'trait2'}
49
+ assert {t.name for t in SubTraitable.traits(flags_off=T.RESERVED)} == expected_traits
50
+
51
+
52
+ def test_subclass2_traits():
53
+ expected_traits = ['trait1', 'trait2', 'trait3', 'trait4']
54
+ assert [t.name for t in SubTraitable2.traits(flags_off=T.RESERVED)] == expected_traits
55
+ assert SubTraitable.trait('trait2').data_type is str
56
+ assert SubTraitable2.trait('trait2').data_type is int
57
+ assert SubTraitable3.trait('trait2').data_type is list
58
+ assert SubTraitable2.trait('trait4').default_value() == 0
59
+ assert SubTraitable2.trait('trait2').ui_hint.tip == 'Trait2'
60
+ assert SubTraitable2.trait('trait3').ui_hint.tip == 'trait definition comment'
61
+ assert SubTraitable3.trait('trait2').ui_hint.tip == 'trait modification comment'
62
+
63
+
64
+ def test_is_storable():
65
+ assert not SubTraitable.is_storable()
66
+ assert SubTraitable2.is_storable()
67
+
68
+ with pytest.raises(OSError, match='No Traitable Store is specified: neither explicitly, nor via environment variable XX_MAIN_TS_STORE_URI'):
69
+ SubTraitable2().save()
70
+
71
+ assert 'is not storable' in SubTraitable(trait1=uuid.uuid1().int).save().error()
72
+
73
+
74
+ def test_trait_update():
75
+ with CACHE_ONLY():
76
+ instance = SubTraitable(trait1=10, trait2='hello', _replace=True)
77
+ assert instance.trait2 == 'hello'
78
+
79
+ assert instance == SubTraitable.update(trait1=10, trait2='world')
80
+ assert instance.trait2 == 'world'
81
+
82
+ assert instance == SubTraitable.update(trait1=10, trait2=None) # setting to None
83
+ assert instance.trait2 is None
84
+
85
+
86
+ def test_instance_slots():
87
+ assert Traitable.__slots__ == ()
88
+ assert SubTraitable.__slots__ == ()
89
+ instance = SubTraitable(trait1=10)
90
+ assert not hasattr(instance, '__dict__')
91
+ with pytest.raises(AttributeError):
92
+ instance.non_existent_attr = 'value'
93
+
94
+
95
+ def test_post_init_called():
96
+ calls = []
97
+
98
+ class X(Traitable):
99
+ x: int = RT()
100
+
101
+ def __post_init__(self):
102
+ calls.append(self)
103
+
104
+ x = X(x=1)
105
+ assert calls == [x]
106
+
107
+
108
+ def test_overriding_init_disallowed():
109
+ with pytest.raises(
110
+ TypeError,
111
+ match=r'Overriding __init__ is not allowed in DisallowedInit\. Use __post_init__ instead\.',
112
+ ):
113
+
114
+ class DisallowedInit(Traitable):
115
+ def __init__(self, *args, **kwargs):
116
+ super().__init__(*args, **kwargs)
117
+
118
+
119
+ def test_init_with_id():
120
+ with GRAPH_ON(): # isolate lazy reference so it does not affect other tests
121
+ pid = ID('John|Smith')
122
+ p = Person(pid)
123
+ assert p.id() == pid
124
+
125
+
126
+ def test_init_with_trait_values():
127
+ with CACHE_ONLY():
128
+ p = Person(first_name='John', last_name='Smith')
129
+ assert p.first_name == 'John'
130
+ assert p.last_name == 'Smith'
131
+
132
+
133
+ def test_set_values():
134
+ with CACHE_ONLY():
135
+ p = Person(first_name='John', last_name='Smith')
136
+ rc = p.set_values(age=19, weight_lb=200)
137
+ assert rc
138
+
139
+
140
+ def test_dynamic_traits():
141
+ class X(Traitable):
142
+ s_own_trait_definitions = dict(x=RT(data_type=int, default=10))
143
+
144
+ x = X()
145
+ assert x.T.x.data_type is int
146
+ assert x.x == 10
147
+
148
+ class Y(X):
149
+ y: int = RT(20)
150
+
151
+ y = Y()
152
+ assert y.T.x.data_type is int
153
+ assert y.x == 10
154
+
155
+ assert y.T.y.data_type is int
156
+ assert y.y == 20
157
+
158
+
159
+ def test_collection_name_trait():
160
+ class X(Traitable):
161
+ x: int
162
+
163
+ assert not X.is_storable()
164
+ assert not X.trait('_collection_name')
165
+
166
+ class Y(Traitable):
167
+ s_default_trait_factory = T
168
+ s_custom_collection = True
169
+ y: int
170
+
171
+ @classmethod
172
+ def load_data(cls, id: ID) -> dict | None:
173
+ return None
174
+
175
+ assert Y.is_storable()
176
+ assert Y.trait('_collection_name')
177
+
178
+ y = Y(_collection_name='test')
179
+
180
+ assert y.id().collection_name == 'test'
181
+ assert y._collection_name == 'test'
182
+
183
+
184
+ @pytest.mark.parametrize('on_graph', [0, 1])
185
+ @pytest.mark.parametrize('debug', [0, 1])
186
+ @pytest.mark.parametrize('convert_values', [0, 1])
187
+ @pytest.mark.parametrize('use_parent_cache', [True, False])
188
+ @pytest.mark.parametrize('use_default_cache', [True, False])
189
+ @pytest.mark.parametrize('use_existing_instance_by_id', [True, False])
190
+ @pytest.mark.parametrize('self_ref', [True, False])
191
+ @pytest.mark.parametrize('nested', [True, False])
192
+ def test_traitable_ref_load(on_graph, debug, convert_values, use_parent_cache, use_default_cache, use_existing_instance_by_id, self_ref, nested):
193
+ load_calls = collections.Counter()
194
+
195
+ class X(Traitable):
196
+ i: int = T(T.ID)
197
+ x: THIS_CLASS = T()
198
+ y: int = T()
199
+
200
+ @classmethod
201
+ def exists_in_store(cls, id: ID) -> bool:
202
+ return id.value == '1'
203
+
204
+ @classmethod
205
+ def load_data(cls, id: ID) -> dict | None:
206
+ v = id.value
207
+ load_calls[v] += 1
208
+ i = int(v)
209
+ return {'_id': v, 'i': i, '_rev': 1, 'y': 1} | ({'x': {'_id': str(i + int(not self_ref))}} if i < 3 else {})
210
+
211
+ with BTP.create(on_graph, convert_values, debug, use_parent_cache, use_default_cache):
212
+ x = X.existing_instance_by_id(ID('1')) if use_existing_instance_by_id else X.existing_instance(i=1)
213
+ assert x
214
+ x1 = X(i=3, x=x, y=2, _replace=True)
215
+ assert x1.x is x
216
+ assert x1.y == 2 # still 2 as lazy-load occurs before setting parameters passed
217
+ assert load_calls == {'3': 1} # lazy-load since no db access in constructor
218
+ load_calls.clear()
219
+
220
+ assert x.i == 1
221
+ expected = lambda n: {str(i): 1 for i in range(1, n + 1)}
222
+ if debug and not self_ref:
223
+ assert load_calls == expected(3)
224
+ assert x.x.x == x1 # found existing instance
225
+ assert x1.x is XNone # reload in debug mode
226
+ else:
227
+ with BTP.create(-1, -1, -1, use_parent_cache=False, use_default_cache=False) if nested else nullcontext():
228
+ assert load_calls == expected(1)
229
+ assert x.x
230
+ assert not self_ref or x == x.x
231
+ assert load_calls == expected(1)
232
+ assert x.x.i == 1 + int(not self_ref)
233
+ assert load_calls == expected(1 + int(not self_ref))
234
+ assert x.x.x == (x if self_ref else x1) # found existing instance
235
+ assert x1.x is x # no reload
236
+
237
+ # TODO: change_flags; as_of context
238
+ # TODO: nodes with args...
239
+
240
+
241
+ def test_trait_methods():
242
+ class A(Traitable):
243
+ s_default_trait_factory = T
244
+ t: int
245
+
246
+ @classmethod
247
+ def exists_in_store(cls, id):
248
+ return False
249
+
250
+ @classmethod
251
+ def load_data(cls, id):
252
+ return None
253
+
254
+ class B(A):
255
+ def t_get(self):
256
+ return 1
257
+
258
+ @classmethod
259
+ def t_serialize(cls, trait, value):
260
+ return value + 1
261
+
262
+ class C(B):
263
+ t: str = M()
264
+
265
+ def t_get(self):
266
+ return 2
267
+
268
+ @classmethod
269
+ def t_serialize(cls, trait, value):
270
+ return value + 2
271
+
272
+ class D(C):
273
+ t: date = M()
274
+
275
+ for t, dt in zip([A, B, C, D], [int, int, str, date], strict=True):
276
+ assert t.trait('t').data_type is dt
277
+
278
+ for t, v in zip([A, B, C, D], [XNone, 1, 2, 2], strict=True):
279
+ assert t().t is v
280
+ assert t().serialize_object()['t'] == (v * 2 or None)
281
+
282
+
283
+ def test_serialize_rt_object():
284
+ class X(Traitable):
285
+ x: int = RT(T.ID)
286
+ y: int = RT(T.ID)
287
+
288
+ assert not X.existing_instance_by_id(ID('1|2'), _throw=False) # not found, can't find by id
289
+ x = X.existing_instance(x=1, y=2) # creates new instance by id traits
290
+ assert x == X.existing_instance_by_id(ID('1|2'))
291
+ assert x == X(x=1, y=2)
292
+
293
+ s = x.serialize(False)
294
+ assert s == {'_id': [1, 2]}
295
+
296
+ assert X.deserialize(s) == x
297
+
298
+
299
+ def test_trait_modification_inheritance_with_flags():
300
+ """Test complex trait modification inheritance with M() overriding flags and getters."""
301
+
302
+ class X(Traitable):
303
+ x: int = RT(T.ID)
304
+ v: int = RT(T.ID)
305
+
306
+ def v_get(self):
307
+ return self.x + 1
308
+
309
+ class Y(X):
310
+ # M() removes ID flag; getter from X is inherited
311
+ v: int = M(flags=(None, T.ID), default=3)
312
+
313
+ class Z(Y):
314
+ # M() changes data type; value is provided by v_get defined here
315
+ v: float = M(default=XNone)
316
+
317
+ def v_get(self):
318
+ return float(self.x + 3)
319
+
320
+ x = X(x=1)
321
+ y = Y(x=1)
322
+ z = Z(x=1)
323
+
324
+ # X: v is computed via v_get
325
+ assert x.v == 2 # x + 1
326
+
327
+ # Y: v uses default
328
+ assert y.v == 3
329
+
330
+ # Z: v uses its own v_get and returns float
331
+ assert z.v == 4.0 # x + 3, as float
332
+
333
+ # Verify trait definitions
334
+ assert X.trait('v').flags_on(T.ID)
335
+ assert not Y.trait('v').flags_on(T.ID) # ID flag removed
336
+ assert Z.trait('v').data_type is float
337
+
338
+ assert X.trait('v').default is XNone
339
+ assert Y.trait('v').default == 3
340
+ assert Z.trait('v').default is XNone
341
+
342
+
343
+ def test_anonymous_traitable(monkeypatch):
344
+ monkeypatch.setattr('core_10x.package_refactoring.PackageRefactoring.default_class_id', lambda cls, *args, **kwargs: PyClass.name(cls))
345
+
346
+ class X(AnonymousTraitable):
347
+ a: int = T()
348
+
349
+ class Y(Traitable):
350
+ y: int = T(T.ID)
351
+ x: Traitable = T()
352
+
353
+ @classmethod
354
+ def exists_in_store(cls, id):
355
+ return False
356
+
357
+ @classmethod
358
+ def load_data(cls, id):
359
+ return None
360
+
361
+ class Z(Y):
362
+ x: AnonymousTraitable = M(T.EMBEDDED)
363
+
364
+ x = X(a=1)
365
+ assert x.serialize(True) == {'a': 1}
366
+
367
+ y = Y(y=0, x=x, _replace=True)
368
+ with pytest.raises(
369
+ TraitMethodError, match=r"test_anonymous_traitable.<locals>.X - anonymous' instance may not be serialized as external reference"
370
+ ):
371
+ y.serialize_object()
372
+
373
+ z = Z(y=1, x=x, _replace=True)
374
+ s = z.serialize_object()
375
+ assert s['x'] == {'_obj': {'a': 1}, '_type': '_nx', '_cls': 'test_traitable.test_anonymous_traitable.<locals>.X'}
376
+
377
+ z = Z(y=2, x=Y(y=3), _replace=True)
378
+ with pytest.raises(TraitMethodError, match=r'test_anonymous_traitable.<locals>.Y/3 - embedded instance must be anonymous'):
379
+ z.serialize_object()
380
+
381
+
382
+ def test_own_trait_defs():
383
+ cnt = Counter()
384
+
385
+ def assert_and_cont(what, obj, arg, trait=None):
386
+ assert isinstance(obj, Traitable)
387
+ if trait:
388
+ assert isinstance(trait, Trait)
389
+ assert trait.data_type is int
390
+ assert trait.flags_on(T.RUNTIME | T.EXPENSIVE)
391
+ cnt[what] += arg
392
+
393
+ class X(Traitable):
394
+ def x_get(self, arg) -> int:
395
+ assert_and_cont('get', self, arg)
396
+ return 1
397
+
398
+ def x_set(self, trait, value, arg) -> RC:
399
+ assert_and_cont('set', self, arg, trait)
400
+ self.raw_set_value(trait, value + 1, arg)
401
+ return RC_TRUE
402
+
403
+ @classmethod
404
+ def own_trait_definitions(cls) -> Generator[tuple[str, trait_definition.TraitDefinition]]:
405
+ yield 'x', TraitDefinition(T.RUNTIME | T.EXPENSIVE, data_type=int)
406
+
407
+ t = X.trait('x')
408
+ assert cnt == {}
409
+ x = X()
410
+ assert x.x(1) == 1
411
+ assert cnt == {'get': 1}
412
+
413
+ x.set_value(t, 2, 1)
414
+ assert x.x(1) == 3
415
+ assert cnt == {'get': 1, 'set': 1}
416
+
417
+
418
+ def test_trait_get_default_override():
419
+ class X(Traitable):
420
+ x: int = RT(default=1)
421
+
422
+ assert X().x == 1
423
+
424
+ with pytest.raises(
425
+ RuntimeError,
426
+ match=r'Ambiguous definition for x_get on <class \'test_traitable.test_trait_get_default_override.<locals>.Y\'> - both x.default and <class \'test_traitable.test_trait_get_default_override.<locals>.Y\'>.x_get are defined.',
427
+ ):
428
+
429
+ class Y(X):
430
+ x: int = RT(default=2)
431
+
432
+ def x_get(self):
433
+ return 2
434
+
435
+ class Z(X):
436
+ def x_get(self):
437
+ return 3
438
+
439
+ class T(Z):
440
+ def x_get(self):
441
+ return 4
442
+
443
+ class S(T):
444
+ x: int = RT(default=5)
445
+
446
+ class R(S):
447
+ x: int = RT(default=XNone)
448
+
449
+ with pytest.raises(
450
+ RuntimeError,
451
+ match=r'Ambiguous definition for x_get on <class \'test_traitable.test_trait_get_default_override.<locals>.P\'> - both x.default and <class \'test_traitable.test_trait_get_default_override.<locals>.P\'>.x_get are defined.',
452
+ ):
453
+
454
+ class P(S):
455
+ x: int = M()
456
+
457
+ def x_get(self):
458
+ return 6
459
+
460
+ assert Z().x == 3
461
+ assert T().x == 4
462
+ assert S().x == 5
463
+ assert R().x == 4
464
+
465
+
466
+ def test_create_and_share():
467
+ class X(Traitable):
468
+ x: int = RT(T.ID)
469
+ y: int = RT(T.ID)
470
+ z: int = RT()
471
+ t: int = RT(T.ID_LIKE)
472
+
473
+ def y_get(self):
474
+ return self.t
475
+
476
+ # with pytest.raises(TypeError, match=re.escape('test_create_and_share.<locals>.X expects at least one ID trait value')):
477
+ with pytest.raises(TypeError, match=re.escape("test_create_and_share.<locals>.X.x (<class 'int'>) - invalid value ''")):
478
+ X()
479
+
480
+ with pytest.raises(TypeError, match=re.escape("test_create_and_share.<locals>.X.y (<class 'int'>) - invalid value ''")):
481
+ X(x=1)
482
+
483
+ X(x=1, y=1, z=1, _replace=True)
484
+
485
+ with pytest.raises(ValueError, match=re.escape('test_create_and_share.<locals>.X.z - non-ID trait value cannot be set during initialization')):
486
+ X(x=1, y=1, z=2)
487
+
488
+ with pytest.raises(ValueError, match=re.escape('test_create_and_share.<locals>.X.z - non-ID trait value cannot be set during initialization')):
489
+ X(x=1, y=1, z=1)
490
+
491
+ with pytest.raises(ValueError, match=re.escape('test_create_and_share.<locals>.X.z - non-ID trait value cannot be set during initialization')):
492
+ X(x=1, t=1, z=1)
493
+
494
+ assert X(x=1, t=1).z == 1
495
+ assert X(x=1, y=1).z == 1
496
+
497
+ with INTERACTIVE():
498
+ x = X() # empty object allowed - OK!
499
+ z = X(x=1, y=1)
500
+ assert z.z == 1 # found from parent
501
+
502
+ x.x = 1
503
+ x.y = 1
504
+ assert not x.share(False)
505
+ assert x.z is XNone
506
+
507
+
508
+ def test_serialize(monkeypatch):
509
+ monkeypatch.setattr('core_10x.package_refactoring.PackageRefactoring.default_class_id', lambda cls, *args, **kwargs: PyClass.name(cls))
510
+ save_calls = Counter()
511
+ history_save_calls = Counter()
512
+ load_calls = Counter()
513
+ serialized = {}
514
+
515
+ class X(Traitable):
516
+ x: int = T(T.ID)
517
+ y: Self = T()
518
+ z: int = T()
519
+
520
+ @classmethod
521
+ def exists_in_store(cls, id: ID) -> bool:
522
+ return id.value in serialized or int(id.value) > 3
523
+
524
+ @classmethod
525
+ def load_data(cls, id: ID) -> dict | None:
526
+ load_calls[id.value] += 1
527
+ return serialized.get(id.value)
528
+
529
+ @classmethod
530
+ def store(cls):
531
+ class Store:
532
+ def auth_user(self):
533
+ return 'test_user'
534
+
535
+ def collection(self, collection_name):
536
+ class Collection:
537
+ def create_index(self, name, trait_name):
538
+ return name
539
+
540
+ def save(self, serialized_data):
541
+ if not collection_name.endswith('#history'):
542
+ id_value = serialized_data['_id']
543
+ save_calls[id_value] += 1
544
+ else:
545
+ id_value = serialized_data['$set']['_id']
546
+ history_save_calls[id_value] += 1
547
+
548
+ serialized[id_value] = serialized_data
549
+ return 1
550
+
551
+ save_new = save
552
+
553
+ return Collection()
554
+
555
+ return Store()
556
+
557
+ def z_get(self) -> int:
558
+ return self.y._rev if self.y and self.y._rev else 0
559
+
560
+ class Y(X): ...
561
+
562
+ x = X(x=0)
563
+ assert not serialized
564
+ x.save().throw()
565
+ assert not load_calls
566
+ assert dict(save_calls) == {'0': 1}
567
+ assert dict(history_save_calls) == {next(iter(history_save_calls)): 1}
568
+ assert serialized['0']['z'] == 0
569
+ save_calls.clear()
570
+ load_calls.clear()
571
+
572
+ x = X(x=1, y=X(x=2, y=X(x=1), _replace=True), _replace=True)
573
+ x.save()
574
+ assert save_calls == {'1': 1}
575
+ assert load_calls == {str(i): 1 for i in range(1, 3)}
576
+ save_calls.clear()
577
+ load_calls.clear()
578
+
579
+ x.save(save_references=True)
580
+ assert save_calls == {'1': 1, '2': 1}
581
+ assert load_calls == {}
582
+ save_calls.clear()
583
+
584
+ X(x=3, y=Y(_id=ID('4')), z=0, _replace=True).save(save_references=True)
585
+ assert load_calls == {'3': 1}
586
+ assert save_calls == {'3': 1} # save of a lazy load is noop
587
+
588
+ assert X(x=3).y.__class__ is Y
589
+ with pytest.raises(
590
+ TraitMethodError,
591
+ match=r"Failed in <class 'test_traitable.test_serialize.<locals>.X'>.z.z_get\n object = 5;\noriginal exception = RuntimeError: test_serialize.<locals>.X/6: object reference not found in store",
592
+ ):
593
+ X(x=5, y=X(_id=ID('6')), _replace=True).save(save_references=True)
594
+
595
+
596
+ def test_reference_serialization_roundtrip(monkeypatch):
597
+ """Test that references are correctly serialized and can be loaded back in a round-trip."""
598
+ monkeypatch.setattr('core_10x.package_refactoring.PackageRefactoring.default_class_id', lambda cls, *args, **kwargs: PyClass.name(cls))
599
+ serialized = {}
600
+
601
+ class Person(Traitable):
602
+ first_name: str = T(T.ID)
603
+ last_name: str = T(T.ID)
604
+ spouse: Self = T()
605
+
606
+ @classmethod
607
+ def exists_in_store(cls, id: ID) -> bool:
608
+ return id.value in serialized
609
+
610
+ @classmethod
611
+ def load_data(cls, id: ID) -> dict | None:
612
+ return serialized.get(id.value)
613
+
614
+ @classmethod
615
+ def store(cls):
616
+ class Store:
617
+ def auth_user(self):
618
+ return 'test_user'
619
+
620
+ def collection(self, collection_name):
621
+ class Collection:
622
+ def create_index(self, name, trait_name):
623
+ return name
624
+
625
+ def save(self, serialized_data):
626
+ id_value = serialized_data['_id']
627
+ serialized[id_value] = serialized_data
628
+ return 1
629
+
630
+ save_new = save
631
+
632
+ return Collection()
633
+
634
+ return Store()
635
+
636
+ # Create both people
637
+ p2 = Person(first_name='Tatiana', last_name='Pevzner', _replace=True)
638
+ p1 = Person(first_name='Ilya', last_name='Pevzner', spouse=p2, _replace=True)
639
+
640
+ # Manually serialize both with references (simulating save_references=True behavior)
641
+ serialized['Tatiana|Pevzner'] = p2.serialize_object()
642
+ serialized['Ilya|Pevzner'] = p1.serialize_object()
643
+ assert serialized['Ilya|Pevzner']['spouse'] == {'_id': 'Tatiana|Pevzner'}
644
+
645
+ # Reload and verify references are preserved
646
+ loaded_p1 = Person.load(ID('Ilya|Pevzner'))
647
+ assert loaded_p1.first_name == 'Ilya'
648
+ assert loaded_p1.spouse is not None
649
+ assert loaded_p1.spouse.first_name == 'Tatiana'
650
+ assert loaded_p1.spouse.last_name == 'Pevzner'
651
+ assert type(loaded_p1.spouse) is Person
652
+
653
+
654
+ def test_id_trait_set():
655
+ class X(Traitable):
656
+ x: int = RT(T.ID)
657
+ y: int = RT(T.ID_LIKE)
658
+
659
+ with INTERACTIVE():
660
+ x = X()
661
+ x.x = 1
662
+ x.y = 1
663
+ assert x.x == 1
664
+ x.x = 2
665
+ assert x.x == 2
666
+
667
+ x.share(False)
668
+ with pytest.raises(ValueError, match=r"test_id_trait_set.<locals>.X.x \(<class 'int'>\) - cannot change ID trait value from '2' to '3'"):
669
+ x.x = 3
670
+
671
+ with pytest.raises(ValueError, match=r"test_id_trait_set.<locals>.X.x \(<class 'int'>\) - cannot change ID trait value from '2' to '2'"):
672
+ x.x = 2
673
+ assert x.x == 2
674
+
675
+ with pytest.raises(ValueError, match=r"test_id_trait_set.<locals>.X.y \(<class 'int'>\) - cannot change ID_LIKE trait value from '1' to '4'"):
676
+ x.y = 4
677
+
678
+ with pytest.raises(ValueError, match=r"test_id_trait_set.<locals>.X.y \(<class 'int'>\) - cannot change ID_LIKE trait value from '1' to '1'"):
679
+ x.y = 1
680
+
681
+ assert x.y == 1
682
+
683
+ x = X(x=1)
684
+ with pytest.raises(ValueError, match=r"test_id_trait_set.<locals>.X.x \(<class 'int'>\) - cannot change ID trait value from '1' to '2'"):
685
+ x.x = 2
686
+
687
+ with pytest.raises(ValueError, match=r"test_id_trait_set.<locals>.X.x \(<class 'int'>\) - cannot change ID trait value from '1' to '1'"):
688
+ x.x = 1
689
+ assert x.x == 1
690
+
691
+
692
+ def test_reload():
693
+ rev = 0
694
+
695
+ class X(Traitable):
696
+ x: int = T(T.ID)
697
+
698
+ @classmethod
699
+ def load_data(cls, id: ID) -> dict | None:
700
+ nonlocal rev
701
+ rev += 1
702
+ data = {'_id': id.value, 'x': int(id.value), '_rev': rev}
703
+ return data
704
+
705
+ # reload of lazy ref
706
+ x = X(ID('1'))
707
+ x.reload()
708
+ assert x._rev == 1
709
+
710
+ x.reload()
711
+ assert x._rev == 2
712
+
713
+ x = X(ID('2'))
714
+ with GRAPH_ON():
715
+ x.reload()
716
+ assert x._rev == 3
717
+ assert rev == 3
718
+ assert x._rev == 3
719
+ assert rev == 3
720
+
721
+ x = X(ID('3'))
722
+ with GRAPH_ON():
723
+ y = X(ID('3'))
724
+ assert y._rev == 4
725
+ assert x._rev == 4
726
+
727
+
728
+ def test_separation():
729
+ class X(Traitable):
730
+ x: int = RT(T.ID)
731
+ y: int = RT()
732
+
733
+ with GRAPH_ON() as g1:
734
+ x1 = X(x=1, y=1, _replace=True)
735
+
736
+ with GRAPH_ON() as g2:
737
+ x2 = X(x=1, y=2, _replace=True)
738
+
739
+ assert X(x=1).y is XNone
740
+
741
+ with g1:
742
+ assert X(x=1).y == 1
743
+ with pytest.raises(RuntimeError, match=r'X/1: object not usable - origin cache is not reachable'):
744
+ x2.get_value('y')
745
+
746
+ with g2:
747
+ assert X(x=1).y == 2
748
+ with pytest.raises(RuntimeError, match=r'X/1: object not usable - origin cache is not reachable'):
749
+ x1.get_value('y')
750
+
751
+ with pytest.raises(RuntimeError, match=r'X/1: object not usable - origin cache is not reachable'):
752
+ x1.get_value('y')
753
+
754
+ with pytest.raises(RuntimeError, match=r'X/1: object not usable - origin cache is not reachable'):
755
+ x2.get_value('y')
756
+
757
+
758
+ def test_id_like_with_replace_and_non_id_update():
759
+ class Cross(Traitable):
760
+ cross: str = RT(T.ID) # e.g., GBP/USD or CHF/JPY
761
+ base_ccy: str = RT(T.ID_LIKE) # Base (left) currency
762
+ quote_ccy: str = RT(T.ID_LIKE) # Quote (right) currency
763
+ value: str = RT()
764
+ value2: str = RT()
765
+
766
+ def cross_get(self) -> str:
767
+ return f'{self.base_ccy}/{self.quote_ccy}'
768
+
769
+ c1 = Cross(base_ccy='A', quote_ccy='B', value='123', value2='456', _replace=True)
770
+ assert c1.cross == 'A/B'
771
+ assert c1.id().value == 'A/B'
772
+ assert c1.value == '123'
773
+ assert c1.value2 == '456'
774
+
775
+ c2 = Cross(base_ccy='A', quote_ccy='B')
776
+ assert c2.cross == 'A/B'
777
+ assert c2.id().value == 'A/B'
778
+ assert c2.value == '123'
779
+ assert c2.value2 == '456'
780
+
781
+ c3 = Cross(base_ccy='A', quote_ccy='B', value='234', _replace=True)
782
+ assert c2.cross == 'A/B'
783
+ assert c2.id().value == 'A/B'
784
+ assert c2.value == '234'
785
+ assert c2.value2 is XNone
786
+
787
+ assert c1 == c2 == c3 # all objects with the same ID are equal
788
+
789
+ with pytest.raises(
790
+ ValueError,
791
+ match=re.escape('test_id_like_with_replace_and_non_id_update.<locals>.Cross.value - non-ID trait value cannot be set during initialization'),
792
+ ):
793
+ Cross(base_ccy='A', quote_ccy='B', value='234')
794
+
795
+
796
+ def test_existing_instance_api_variants():
797
+ class RuntimePerson(Person):
798
+ @classmethod
799
+ def own_trait_definitions(cls) -> Generator[tuple[str, TraitDefinition]]:
800
+ for trait_name, trait in Person.s_dir.items():
801
+ if not trait.flags_on(T.RUNTIME | T.RESERVED):
802
+ yield trait_name, TraitModification(flags=T.RUNTIME).apply(trait.t_def)
803
+
804
+ assert not RuntimePerson.is_storable()
805
+
806
+ with CACHE_ONLY():
807
+ ppl_stored = [
808
+ Person(last_name='Davidovich', first_name='Sasha', weight_lbs=170, _replace=True),
809
+ Person(last_name='Pevzner', first_name='Ilya', weight_lbs=200, _replace=True),
810
+ RuntimePerson(last_name='Lesin', first_name='Alex', weight_lbs=190, _replace=True),
811
+ RuntimePerson(last_name='Smith', first_name='John', weight_lbs=180, _replace=True),
812
+ ]
813
+
814
+ existing_id_traits = [{trait.name: p.get_value(trait) for trait in p.traits(flags_on=T.ID.value())} for p in ppl_stored]
815
+
816
+ ppl_found = [obj.__class__.existing_instance(**id_values) for obj, id_values in zip(ppl_stored, existing_id_traits, strict=True)]
817
+ assert ppl_found == ppl_stored
818
+
819
+ # Positive / negative lookups with _throw flag
820
+ p1 = RuntimePerson.existing_instance(last_name='Smith', first_name='John')
821
+ assert p1
822
+
823
+ p2 = Person.existing_instance(last_name='Pevzner', first_name='Arthur', _throw=False)
824
+ assert p2 is None
825
+
826
+ for i, cls in enumerate(ppl_stored):
827
+ # existing_instance_by_id with ID and raw id value
828
+ id1 = ppl_found[i].id()
829
+ p3 = cls.existing_instance_by_id(_id=id1)
830
+
831
+ id1_val = id1.value
832
+ p4 = cls.existing_instance_by_id(_id_value=id1_val)
833
+ assert p3 == p4
834
+
835
+ id2_val = 'Smith|Josh'
836
+ p5 = cls.existing_instance_by_id(_id_value=id2_val, _throw=False)
837
+ assert p5 is None
838
+
839
+
840
+ @pytest.mark.parametrize('value', [{'x': 1}, {}, [], [1], np.float64(1.1)])
841
+ def test_any_trait(value):
842
+ class X(Traitable):
843
+ x: int = T(T.ID)
844
+ y: Any = T()
845
+ z: list = T()
846
+
847
+ @classmethod
848
+ def load_data(cls, id: ID) -> dict | None:
849
+ return None
850
+
851
+ with GRAPH_ON():
852
+ x = X(x=1, y=value, z=[value], _replace=True)
853
+ assert x.y == value
854
+ assert x.z[0] == value
855
+ assert type(x.y) is type(value)
856
+ assert type(x.z[0]) is type(value)
857
+ s = x.serialize_object()
858
+
859
+ with GRAPH_ON():
860
+ x = X.deserialize_object(x.s_bclass, None, s)
861
+ assert x.y == value
862
+ assert x.z[0] == value
863
+ assert type(x.y) is type(value)
864
+ assert type(x.z[0]) is type(value)
865
+ assert s == x.serialize_object()
866
+
867
+
868
+ @pytest.mark.parametrize('t', [T, RT])
869
+ def test_exceptions(t):
870
+ class X(Traitable):
871
+ x: int = t(T.ID)
872
+ y: int = t(default=1)
873
+
874
+ with CACHE_ONLY():
875
+ x = X(_id=ID('1'))
876
+ match = (
877
+ r'test_exceptions.<locals>.X/1: object is an invalid lazy reference to non-storable that does not exist in memory'
878
+ if t is RT
879
+ else r'test_exceptions.<locals>.X/1: object reference not found in store'
880
+ )
881
+ with pytest.raises(RuntimeError, match=match):
882
+ assert x.y == 1
883
+
884
+
885
+ def test_multiple_inheritance():
886
+ """Test universal multiple inheritance with __slots__=() and MRO resolution"""
887
+
888
+ # Test 1: Basic multiple inheritance works
889
+ class EntityA(Traitable):
890
+ name: str = RT(T.ID)
891
+ value: int = RT()
892
+
893
+ def value_get(self) -> int:
894
+ return 42
895
+
896
+ class EntityB(Traitable):
897
+ rev: int = RT(T.ID)
898
+ value: str = RT()
899
+
900
+ def value_get(self) -> str:
901
+ return 'forty two'
902
+
903
+ class Combined(EntityA, EntityB):
904
+ extra: str = RT()
905
+
906
+ assert EntityA.__slots__ == ()
907
+ assert EntityB.__slots__ == ()
908
+ assert Combined.__slots__ == ()
909
+
910
+ # Verify MRO
911
+ mro_names = [cls.__name__ for cls in Combined.__mro__]
912
+ assert mro_names[:4] == ['Combined', 'EntityA', 'EntityB', 'Traitable']
913
+
914
+ obj = Combined(name='test', rev=1)
915
+
916
+ assert obj.name == 'test'
917
+ assert obj.value == 42
918
+ assert obj.rev == 1
919
+
920
+ assert isinstance(obj.T, TraitAccessor)
921
+ assert obj.T.name.data_type is str
922
+ assert obj.T.value.data_type is int
923
+ assert obj.T.rev.data_type is int
924
+
925
+
926
+ def test_mutable_default():
927
+ class X(Traitable):
928
+ x: dict = RT(default={})
929
+
930
+ x = X()
931
+ y = X()
932
+ with GRAPH_ON():
933
+ x.x.update(a=1)
934
+
935
+ assert x.x == {'a': 1}
936
+ assert y.x == {}
937
+
938
+
939
+ def test_runtime_id_error():
940
+ with pytest.raises(
941
+ RuntimeError,
942
+ match=r"<class 'test_traitable.test_runtime_id_error.<locals>.X'>.x is a RUNTIME ID trait - traitable must not be storable \(all traits must be RUNTIME\)",
943
+ ):
944
+
945
+ class X(Traitable):
946
+ x: int = RT(T.ID)
947
+ y: int = T()
948
+
949
+
950
+ def test_serialize_runtime_exogenous_reference():
951
+ class X(Traitable):
952
+ x: int = RT()
953
+
954
+ x = X(x=1)
955
+ with pytest.raises(
956
+ TypeError,
957
+ match=rf'test_serialize_runtime_exogenous_reference.<locals>.X/{x.id_value()} - cannot serialize a non-storable exogenous reference',
958
+ ):
959
+ x.serialize(False)