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,280 @@
1
+ from typing import Any
2
+ from uuid import uuid4
3
+
4
+ import numpy
5
+ import pytest
6
+ from py10x_core import BTraitableProcessor, XCache
7
+
8
+ from core_10x.code_samples.person import Person as BasePerson
9
+ from core_10x.package_refactoring import PackageRefactoring
10
+ from core_10x.rc import RC_TRUE
11
+ from core_10x.trait_definition import T
12
+ from core_10x.ts_store import TsDuplicateKeyError
13
+
14
+
15
+ class EnhancedPerson(BasePerson):
16
+ numpy_weight_lbs: Any = T()
17
+
18
+ def numpy_weight_lbs_get(self):
19
+ return numpy.float64(self.weight_lbs)
20
+
21
+
22
+ test_classes = {
23
+ cls_name: type(
24
+ cls_name,
25
+ (EnhancedPerson,),
26
+ {
27
+ '__module__': __name__,
28
+ #'s_custom_collection': True
29
+ },
30
+ )
31
+ for cls_name in (f'Person#{uuid4().hex}' for _ in range(2))
32
+ }
33
+
34
+ globals().update(test_classes)
35
+ Person, Person1 = test_classes.values()
36
+ TEST_COLLECTION = PackageRefactoring.find_class_id(Person)
37
+ TEST_COLLECTION1 = PackageRefactoring.find_class_id(Person1)
38
+
39
+ # TODO: split out s_custom_collection vs regular tests
40
+
41
+
42
+ @pytest.fixture(scope='module')
43
+ def ts_setup(ts_instance):
44
+ with ts_instance:
45
+ p = Person(first_name='John', last_name='Doe') # , _collection_name=TEST_COLLECTION)
46
+ p.set_values(age=30, weight_lbs=100)
47
+ assert p._rev == 0
48
+ assert p.save() == RC_TRUE
49
+ assert p._rev == 1
50
+ assert p.save() == RC_TRUE
51
+ assert p._rev == 1
52
+
53
+ p1 = Person1(first_name='Joe', last_name='Doe') # , _collection_name=TEST_COLLECTION1)
54
+ p1.set_values(age=32, weight_lbs=200)
55
+ assert p1.save()
56
+ assert p1.age == 32
57
+ assert p1._rev == 1
58
+
59
+ yield ts_instance, p, p1
60
+
61
+ # Cleanup
62
+ XCache.clear()
63
+ BTraitableProcessor.current().end_using()
64
+ for cn in [TEST_COLLECTION, TEST_COLLECTION1]:
65
+ ts_instance.delete_collection(cn)
66
+ assert not {TEST_COLLECTION, TEST_COLLECTION1}.intersection(ts_instance.collection_names('.*'))
67
+
68
+
69
+ class TestTSStore:
70
+ """Test class for TS Store functionality."""
71
+
72
+ def test_collection(self, ts_setup):
73
+ ts_store, _p, _p1 = ts_setup
74
+ collection = ts_store.collection(TEST_COLLECTION)
75
+ assert collection is not None
76
+
77
+ def test_save(self, ts_setup):
78
+ ts_store, p, _p1 = ts_setup
79
+ collection = ts_store.collection(TEST_COLLECTION)
80
+ serialized_entity = p.serialize_object()
81
+ _rev = collection.save(serialized_entity.copy())
82
+ assert p._rev == _rev
83
+
84
+ serialized_entity |= {'attr': {'nested': 'value'}}
85
+ _rev = collection.save(serialized_entity.copy())
86
+ serialized_entity |= dict(_rev=_rev)
87
+ assert p._rev + 1 == _rev
88
+ assert collection.load(p.id().value) == serialized_entity
89
+
90
+ # test that nested dictionary replaces rather than updates
91
+ serialized_entity |= {'attr': {'nested1': 'value1'}}
92
+ _rev = collection.save(serialized_entity.copy())
93
+ serialized_entity |= dict(_rev=_rev)
94
+ assert p._rev + 2 == _rev
95
+ # assert collection.load(_id) == serialized_entity|{'attr': {'nested': 'value', 'nested1': 'value1'}} #incorrect behavior
96
+ assert collection.load(p.id().value) == serialized_entity
97
+
98
+ # test that dots are not interpreted as nested fields at the top level
99
+ serialized_entity |= {'attr.nested2': 'value2'}
100
+ _rev = collection.save(serialized_entity.copy())
101
+ serialized_entity |= dict(_rev=_rev)
102
+ assert p._rev + 3 == _rev
103
+ assert collection.load(p.id().value) == serialized_entity
104
+
105
+ # check that we can have dictionary keys starting with $
106
+ serialized_entity |= {'foo': {'$foo': 1}}
107
+ # with pytest.raises(pyts_store.errors.WriteError, match="Unrecognized expression '\\$foo',"): #incorrect behavior
108
+ _rev = collection.save(serialized_entity.copy())
109
+ serialized_entity |= dict(_rev=_rev)
110
+ assert p._rev + 4 == _rev
111
+ assert collection.load(p.id().value) == serialized_entity
112
+
113
+ # check that we can *not* have top level keys starting with $ (current behavior, not ideal, but prob. ok)
114
+ serialized_entity |= {'$foo': 1}
115
+ with pytest.raises(Exception, match='Use of undefined variable: foo'):
116
+ _rev = collection.save(serialized_entity.copy())
117
+ serialized_entity |= dict(_rev=_rev)
118
+ assert p._rev + 4 == _rev
119
+ del serialized_entity['$foo']
120
+ assert collection.load(p.id().value) == serialized_entity
121
+
122
+ # test that dots are not interpreted as nested fields at the nested level
123
+ serialized_entity |= {'attr': {'nested.value': 1}}
124
+ _rev = collection.save(serialized_entity.copy())
125
+ serialized_entity |= dict(_rev=_rev)
126
+ assert p._rev + 5 == _rev
127
+ assert collection.load(p.id().value) == serialized_entity
128
+
129
+ # check that we can unset fields
130
+ del serialized_entity['attr']
131
+ _rev = collection.save(serialized_entity.copy())
132
+ serialized_entity |= dict(_rev=_rev)
133
+ assert p._rev + 6 == _rev
134
+ assert collection.load(p.id().value) == serialized_entity
135
+
136
+ def test_delete_restore(self, ts_setup):
137
+ ts_store, p, _p1 = ts_setup
138
+ collection = ts_store.collection(TEST_COLLECTION)
139
+ serialized_entity = p.serialize_object()
140
+ id_value = serialized_entity['_id']
141
+ assert collection.delete(id_value)
142
+ assert collection.load(id_value) is None
143
+ # TODO: restore is not implemented so use save_new meanwhile
144
+ # assert collection.restore(id_value)
145
+ collection.save_new(serialized_entity)
146
+ assert collection.load(id_value) == serialized_entity
147
+
148
+ def test_find(self, ts_setup):
149
+ ts_store, p, _p1 = ts_setup
150
+ collection = ts_store.collection(TEST_COLLECTION)
151
+ result = collection.find()
152
+ assert next(iter(result)) == p.serialize_object()
153
+ assert list(result) == []
154
+
155
+ def test_load(self, ts_setup):
156
+ ts_store, p, _p1 = ts_setup
157
+ collection = ts_store.collection(TEST_COLLECTION)
158
+ id_value = p.id().value
159
+ result = collection.load(id_value)
160
+ assert result == p.serialize_object()
161
+
162
+ def test_delete(self, ts_setup):
163
+ ts_store, _p, p1 = ts_setup
164
+ collection = ts_store.collection(TEST_COLLECTION1)
165
+ id_value = p1.id().value
166
+ result = collection.delete(id_value)
167
+ assert result
168
+ result = collection.load(id_value)
169
+ assert result is None
170
+
171
+ def test_save_new_with_overwrite(self, ts_setup):
172
+ """Test save_new with overwrite=True flag."""
173
+ ts_store, p, _p1 = ts_setup
174
+ collection = ts_store.collection(TEST_COLLECTION)
175
+ serialized_entity = p.serialize_object()
176
+ id_value = serialized_entity['_id']
177
+
178
+ # Try to save_new with overwrite=True on existing document
179
+ result = collection.save_new(serialized_entity.copy(), overwrite=True)
180
+ assert result == 1
181
+ assert collection.load(id_value) == serialized_entity
182
+
183
+ def test_save_new_with_set_operation(self, ts_setup):
184
+ """Test save_new with $set MongoDB-style operation."""
185
+ ts_store, _p, _p1 = ts_setup
186
+ collection = ts_store.collection(TEST_COLLECTION)
187
+
188
+ # Create a new document with $set
189
+ doc_id = 'test_doc_123'
190
+ serialized_entity = {'$set': {'_id': doc_id, 'name': 'Test Document', 'value': 42}}
191
+
192
+ result = collection.save_new(serialized_entity)
193
+ assert result == 1
194
+
195
+ loaded = collection.load(doc_id)
196
+ assert loaded == serialized_entity['$set'] | {'_rev': 1}
197
+
198
+ def test_save_new_duplicate_key_error(self, ts_setup):
199
+ """Test that save_new raises TsDuplicateKeyError when inserting duplicate without overwrite."""
200
+ ts_store, _p, _p1 = ts_setup
201
+ collection = ts_store.collection(TEST_COLLECTION)
202
+
203
+ # Test 1: Duplicate without $set
204
+ doc_id = 'duplicate_test_123'
205
+ serialized_entity = {'_id': doc_id, 'name': 'First Document'}
206
+
207
+ result = collection.save_new(serialized_entity)
208
+ assert result == 1
209
+
210
+ # Try to insert the same document again without overwrite (no $set)
211
+ with pytest.raises(TsDuplicateKeyError, match=f'Duplicate key error collection.*dup key.*{doc_id}'):
212
+ collection.save_new(serialized_entity, overwrite=False)
213
+
214
+ # Test 2: Duplicate with $set
215
+ doc_id2 = 'duplicate_test_456'
216
+ serialized_entity2 = {'$set': {'_id': doc_id2, 'name': 'First Document with $set'}}
217
+
218
+ result = collection.save_new(serialized_entity2)
219
+ assert result == 1
220
+
221
+ # Try to insert the same document again without overwrite (with $set)
222
+ with pytest.raises(TsDuplicateKeyError, match=f'Duplicate key error collection.*dup key.*{doc_id2}'):
223
+ collection.save_new(serialized_entity2, overwrite=False)
224
+
225
+ def test_save_new_with_set_and_overwrite(self, ts_setup):
226
+ """Test save_new with $set and overwrite=True."""
227
+ ts_store, _p, _p1 = ts_setup
228
+ collection = ts_store.collection(TEST_COLLECTION)
229
+
230
+ doc_id = 'set_overwrite_test_123'
231
+ # First insert
232
+ serialized_entity1 = {'_id': doc_id, 'name': 'Original'}
233
+ result = collection.save_new(serialized_entity1)
234
+ assert result == 1
235
+
236
+ # Update with $set and overwrite=True
237
+ serialized_entity2 = {'$set': {'_id': doc_id, 'name': 'Updated', 'new_field': 'new_value'}}
238
+ result = collection.save_new(serialized_entity2, overwrite=True)
239
+ assert result == 1
240
+
241
+ loaded = collection.load(doc_id)
242
+ assert loaded['name'] == 'Updated'
243
+ assert loaded['new_field'] == 'new_value'
244
+
245
+ def test_ts_class_association_ts_uri_resolution(self, ts_setup):
246
+ """Test that TsClassAssociation.ts_uri correctly resolves store URIs for classes and their subclasses."""
247
+ from core_10x.py_class import PyClass
248
+ from core_10x.traitable import NamedTsStore, T, Traitable, TsClassAssociation
249
+
250
+ ts_store, _p, _p1 = ts_setup
251
+
252
+ class Dummy1(Traitable):
253
+ text: str = T(T.ID)
254
+
255
+ class Dummy2(Traitable):
256
+ text: str = T(T.ID)
257
+
258
+ class Dummy3(Dummy2): ...
259
+
260
+ dummy_uri1 = 'mongodb://localhost/dummy1'
261
+ dummy_uri2 = 'mongodb://localhost/dummy2'
262
+
263
+ with ts_store:
264
+ # Create and save NamedTsStore objects
265
+ ns1 = NamedTsStore(logical_name='dummy1', uri=dummy_uri1, _replace=True)
266
+ ns1.save()
267
+ ns2 = NamedTsStore(logical_name='dummy2', uri=dummy_uri2, _replace=True)
268
+ ns2.save()
269
+
270
+ # Create and save TsClassAssociation objects
271
+ name1 = PyClass.name(Dummy1)
272
+ name2 = PyClass.name(Dummy2)
273
+ TsClassAssociation(py_canonical_name=name1, ts_logical_name='dummy1', _replace=True).save()
274
+ TsClassAssociation(py_canonical_name=name2, ts_logical_name='dummy2', _replace=True).save()
275
+
276
+ # Class-specific association
277
+ assert TsClassAssociation.ts_uri(Dummy1) == dummy_uri1
278
+
279
+ # Subclass should inherit parent's association if it has none of its own
280
+ assert TsClassAssociation.ts_uri(Dummy3) == dummy_uri2
core_10x/trait.py ADDED
@@ -0,0 +1,377 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import copy
5
+ import functools
6
+ import inspect
7
+ import locale
8
+ import platform
9
+ import sys
10
+ from inspect import Parameter
11
+ from types import GenericAlias
12
+ from typing import get_origin, get_type_hints
13
+
14
+ from py10x_core import BTrait
15
+
16
+ from core_10x.named_constant import NamedConstant
17
+ from core_10x.rc import RC
18
+ from core_10x.trait_definition import T, TraitDefinition, Ui
19
+ from core_10x.xnone import XNone
20
+
21
+
22
+ class Trait(BTrait):
23
+ # TODO: re-enable __slots__ when the dust settles..
24
+ # __slots__ = ('t_def','getter_params')
25
+ s_datatype_traitclass_map = {}
26
+
27
+ @staticmethod
28
+ def register_by_datatype(trait_class, data_type):
29
+ assert inspect.isclass(trait_class) and issubclass(trait_class, Trait), 'trait class must be a subclass of Trait'
30
+ found = Trait.s_datatype_traitclass_map.get(data_type)
31
+ assert not found, f'data_type {data_type} for {trait_class} is already registered for {found}'
32
+ Trait.s_datatype_traitclass_map[data_type] = trait_class
33
+
34
+ s_baseclass_traitclass_map = {}
35
+
36
+ @staticmethod
37
+ def register_by_baseclass(trait_class, base_class):
38
+ assert inspect.isclass(trait_class) and issubclass(trait_class, Trait), 'trait class must be a subclass of Trait'
39
+ assert inspect.isclass(base_class), 'base class must be a class'
40
+ found = Trait.s_baseclass_traitclass_map.get(base_class)
41
+ assert not found, f'base_class {base_class} for {trait_class} is already registered for {found}'
42
+ Trait.s_baseclass_traitclass_map[base_class] = trait_class
43
+
44
+ @staticmethod
45
+ def real_trait_class(data_type):
46
+ real_trait_class = Trait.s_datatype_traitclass_map.get(data_type)
47
+ if real_trait_class:
48
+ return real_trait_class
49
+
50
+ tmap = Trait.s_baseclass_traitclass_map
51
+ base_class: type
52
+ for base_class in reversed(tmap):
53
+ if issubclass(data_type, base_class):
54
+ return tmap[base_class]
55
+
56
+ return generic_trait
57
+
58
+ s_ui_hint = None
59
+
60
+ def __init_subclass__(cls, data_type: type = None, register: bool = True, base_class: type = False):
61
+ cls.s_baseclass = base_class
62
+ if register:
63
+ assert data_type and inspect.isclass(data_type), f'{cls} - data_type is not valid'
64
+ if base_class:
65
+ Trait.register_by_baseclass(cls, data_type)
66
+ else:
67
+ Trait.register_by_datatype(cls, data_type)
68
+
69
+ assert cls.s_ui_hint, f'{cls} must define s_ui_hint'
70
+
71
+ def __init__(self, t_def: TraitDefinition, btrait: BTrait = None):
72
+ if btrait is None:
73
+ super().__init__()
74
+ else:
75
+ super().__init__(btrait)
76
+
77
+ self.t_def = t_def
78
+ self.getter_params = ()
79
+
80
+ def __get__(self, instance, owner):
81
+ if not self.getter_params:
82
+ return instance.get_value(self)
83
+
84
+ return functools.partial(instance.get_value, self)
85
+
86
+ def __set__(self, instance, value):
87
+ if not self.getter_params:
88
+ instance.set_value(self, value).throw()
89
+
90
+ else:
91
+ if not isinstance(value, trait_value):
92
+ raise TypeError(f'May not set a value to {instance.__class__.__name__}.{self.name} as it requires params')
93
+
94
+ instance.set_value(self, value.value, *value.args).throw()
95
+
96
+ # def __deepcopy__(self, memodict={}):
97
+ # return Trait(self.t_def.copy(), btrait = self)
98
+
99
+ @staticmethod
100
+ def create(trait_name: str, t_def: TraitDefinition) -> Trait:
101
+ dt = t_def.data_type
102
+ if isinstance(dt, GenericAlias):
103
+ dt = get_origin(dt) # get original type, e.g. `list` from `list[int]`
104
+ # TODO: could be useful to also keep get_args(dt) for extra checking?
105
+ trait_class = Trait.real_trait_class(dt)
106
+ trait = trait_class(t_def)
107
+ trait.set_name(trait_name)
108
+ trait.data_type = dt
109
+ trait.flags = t_def.flags.value()
110
+ trait.default = t_def.default
111
+ if t_def.fmt:
112
+ trait.fmt = t_def.fmt
113
+
114
+ trait.create_proc()
115
+
116
+ trait.post_ctor()
117
+ ui_hint: Ui = copy.deepcopy(t_def.ui_hint)
118
+ ui_hint.adjust(trait)
119
+ trait.ui_hint = ui_hint
120
+ return trait
121
+
122
+ @staticmethod
123
+ def method_defs(trait_name: str) -> dict:
124
+ return {
125
+ f'{trait_name}_{(method_suffix := method_key.lower())}': (method_suffix, method_def)
126
+ for method_key, method_def in TRAIT_METHOD.s_dir.items()
127
+ }
128
+
129
+ def set_trait_funcs(self, traitable_cls, rc):
130
+ for method_name, (method_suffix, method_def) in Trait.method_defs(self.name).items():
131
+ method = getattr(traitable_cls, method_name, None)
132
+ if method and method_suffix == 'get' and self.t_def.default is not XNone: # -- getter and default are defined - figure out which to use
133
+ for cls in traitable_cls.__mro__:
134
+ cls_vars = vars(cls)
135
+ if method_name in cls_vars: # -- found method on cls - use method, unless
136
+ if isinstance(cls_vars.get(self.name), TraitDefinition): # -- default is on same cls then - error
137
+ rc.add_error(
138
+ f'Ambiguous definition for {method_name} on {cls} - both {self.name}.default and {traitable_cls}.{method_name} are defined.'
139
+ )
140
+ elif isinstance(cls_vars.get(self.name), TraitDefinition): # -- default found on cls - use default
141
+ method = None # use default
142
+ else:
143
+ continue
144
+ break
145
+
146
+ f = method_def.value(self, method, method_suffix, rc)
147
+ if f:
148
+ set_f = getattr(self, f'set_f_{method_suffix}')
149
+ set_f(f, bool(method))
150
+
151
+ def create_f_get(self, f, attr_name: str, rc: RC):
152
+ if not f: # -- no custom getter, just the default value
153
+ f = lambda traitable: self.default_value()
154
+ f.__name__ = 'default_value'
155
+ params = ()
156
+
157
+ else:
158
+ # TODO: if default is defined in a subclass relative to where the getter is defined, override the getter?
159
+
160
+ sig = inspect.signature(f)
161
+ params = []
162
+ param: Parameter
163
+ for pname, param in sig.parameters.items():
164
+ if pname != 'self':
165
+ pkind = param.kind
166
+ if pkind != Parameter.POSITIONAL_OR_KEYWORD:
167
+ rc.add_error(f'{f.__name__} - {pname} is not a positional parameter')
168
+ else:
169
+ params.append(param)
170
+ params = tuple(params)
171
+
172
+ self.getter_params = params
173
+ return f
174
+
175
+ def create_f_set(self, f, attr_name: str, rc: RC):
176
+ if not f:
177
+ return None
178
+
179
+ # -- custom setter
180
+ resolved_hints = get_type_hints(f, sys.modules[f.__module__].__dict__ if f.__module__ in sys.modules else {}, f.__class__.__dict__)
181
+ assert resolved_hints.get('return') is RC, f'{f.__name__} - setter must return RC'
182
+
183
+ sig = inspect.signature(f)
184
+ params = tuple(sig.parameters.values())
185
+ n = len(params)
186
+ if n < 3:
187
+ rc.add_error(f'{f.__name__} - setter must have at least 3 parameters: self, trait, value')
188
+
189
+ getter_params = self.getter_params
190
+ if getter_params:
191
+ if getter_params != tuple(params[3:]):
192
+ rc.add_error(f'{f.__name__} - setter must have same params as the getter: {getter_params}')
193
+
194
+ return f
195
+
196
+ def create_f_common_trait_with_value(self, f, attr_name: str, rc: RC):
197
+ cls = self.__class__
198
+ # TODO: check f's signature
199
+ if not f:
200
+ common_f = getattr(cls, attr_name, None)
201
+ if common_f:
202
+ f = lambda obj_or_cls, trait, value: common_f(trait, value)
203
+ f.__name__ = f'{cls.__name__}.{common_f.__name__}'
204
+
205
+ return f
206
+
207
+ def create_f_common_trait_with_value_static(self, f, attr_name: str, rc: RC):
208
+ cls = self.__class__
209
+ if f:
210
+ assert isinstance(f.__self__, type), f'{f.__name__} must be declared as @classmethod'
211
+ else:
212
+ f = getattr(cls, attr_name, None)
213
+ return self.create_f_common_trait_with_value(f, attr_name, rc)
214
+
215
+ def create_f_choices(self, f, attr_name: str, rc: RC):
216
+ cls = self.__class__
217
+ if not f:
218
+ choices_f = getattr(cls, attr_name, None)
219
+ if choices_f:
220
+ f = lambda obj, trait: choices_f(trait)
221
+ f.__name__ = f'{cls.__name__}.{choices_f.__name__}'
222
+
223
+ return f
224
+
225
+ def create_f_plain(self, f, attr_name: str, rc: RC):
226
+ return f
227
+
228
+ # =======================================================================================================================
229
+ # Formatting
230
+ # =======================================================================================================================
231
+ # fmt: off
232
+ s_locales = {
233
+ 'Windows': 'USA',
234
+ 'Linux': 'en_US',
235
+ }
236
+ # fmt: on
237
+
238
+ def locale_change(self, old_value, value):
239
+ if value:
240
+ return value
241
+
242
+ try:
243
+ return locale.setlocale(locale.LC_NUMERIC, self.__class__.s_locales.get(platform.system(), 'en_US'))
244
+ except Exception:
245
+ return None
246
+
247
+ def _format(self, fmt: str) -> str:
248
+ if not fmt:
249
+ fmt = ':'
250
+ else:
251
+ c = fmt[0]
252
+ if c != '!' and c != ':':
253
+ fmt = ':' + fmt
254
+
255
+ return f'{{{fmt}}}'
256
+
257
+ def use_format_str(self, fmt: str, value) -> str:
258
+ if isinstance(value, str) and not fmt:
259
+ return value
260
+ return self._format(fmt).format(value)
261
+
262
+ # ===================================================================================================================
263
+ # Trait Interface
264
+ # ===================================================================================================================
265
+
266
+ def post_ctor(self): ...
267
+
268
+ def check_integrity(self, cls, rc: RC):
269
+ pass
270
+
271
+ def default_value(self):
272
+ return self.default
273
+
274
+ def same_values(self, value1, value2) -> bool:
275
+ raise NotImplementedError
276
+
277
+ def from_any(self, value):
278
+ if isinstance(value, self.data_type):
279
+ return value
280
+
281
+ if isinstance(value, str):
282
+ return self.from_str(value)
283
+
284
+ return self.from_any_xstr(value)
285
+
286
+ def from_str(self, s: str):
287
+ lit = ast.literal_eval(s)
288
+ return self.from_any_xstr(lit)
289
+
290
+ def from_any_xstr(self, value):
291
+ raise NotImplementedError
292
+
293
+ def to_str(self, v) -> str:
294
+ return self.use_format_str(self.fmt, v)
295
+
296
+ def is_acceptable_type(self, data_type: type) -> bool:
297
+ return data_type is self.data_type
298
+
299
+ def serialize(self, value):
300
+ raise NotImplementedError
301
+
302
+ def deserialize(self, value) -> RC:
303
+ raise NotImplementedError
304
+
305
+ def to_id(self, value) -> str:
306
+ raise NotImplementedError
307
+
308
+ def choices(self):
309
+ return XNone
310
+
311
+ # TODO: unify XNone/None conversions with object serialization/deserializatoin in c++
312
+ # TODO: call these from c++ directly in place of f_serialize/f_deserialize?
313
+ def serialize_value(self, value, replace_xnone=False):
314
+ return None if replace_xnone and value is XNone else self.f_serialize(self, value)
315
+
316
+ def deserialize_value(self, value, replace_none=False):
317
+ return XNone if replace_none and value is None else self.f_deserialize(self, value)
318
+
319
+ # ===================================================================================================================
320
+
321
+
322
+ # ---- Methods Associated with a trait
323
+ # fmt: off
324
+ class TRAIT_METHOD(NamedConstant):
325
+ GET = Trait.create_f_get
326
+ SET = Trait.create_f_set
327
+ VERIFY = Trait.create_f_plain
328
+ FROM_STR = Trait.create_f_common_trait_with_value
329
+ FROM_ANY_XSTR = Trait.create_f_common_trait_with_value
330
+ IS_ACCEPTABLE_TYPE = Trait.create_f_common_trait_with_value
331
+ TO_STR = Trait.create_f_common_trait_with_value
332
+ SERIALIZE = Trait.create_f_common_trait_with_value_static
333
+ DESERIALIZE = Trait.create_f_common_trait_with_value_static
334
+ TO_ID = Trait.create_f_common_trait_with_value
335
+ CHOICES = Trait.create_f_choices
336
+ STYLE_SHEET = Trait.create_f_plain
337
+ # fmt: on
338
+
339
+
340
+ class generic_trait(Trait, register=False):
341
+ s_ui_hint = Ui.NONE
342
+
343
+ def post_ctor(self):
344
+ assert not self.flags_on(T.ID), f'generic trait {self.name} may not be an ID trait'
345
+ assert self.flags_on(T.RUNTIME), f'generic trait {self.name} must be a RUNTIME trait'
346
+
347
+ def is_acceptable_type(self, data_type: type) -> bool:
348
+ return issubclass(data_type, self.data_type)
349
+
350
+ def same_values(self, value1, value2) -> bool:
351
+ return value1 is value2
352
+
353
+
354
+ class trait_value:
355
+ def __init__(self, value, *args):
356
+ self.value = value
357
+ self.args = args
358
+
359
+ def __call__(self, *args, **kwargs): ...
360
+
361
+
362
+ class BoundTrait:
363
+ def __init__(self, obj, trait: Trait):
364
+ self.obj = obj
365
+ self.trait = trait
366
+ # self.args = ()
367
+
368
+ def __getattr__(self, attr_name):
369
+ trait_attr = getattr(self.trait, attr_name, None)
370
+ if trait_attr:
371
+ # if callable(trait_attr):
372
+ return trait_attr
373
+
374
+ return lambda: getattr(self.obj, attr_name)(self.trait)
375
+
376
+ def __call__(self):
377
+ return self.trait