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.
- core_10x/__init__.py +42 -0
- core_10x/backbone/__init__.py +0 -0
- core_10x/backbone/backbone_store.py +59 -0
- core_10x/backbone/backbone_traitable.py +30 -0
- core_10x/backbone/backbone_user.py +66 -0
- core_10x/backbone/bound_data_domain.py +49 -0
- core_10x/backbone/namespace.py +101 -0
- core_10x/backbone/vault.py +38 -0
- core_10x/code_samples/__init__.py +0 -0
- core_10x/code_samples/_package_manifest.py +3 -0
- core_10x/code_samples/directories.py +181 -0
- core_10x/code_samples/person.py +76 -0
- core_10x/concrete_traits.py +356 -0
- core_10x/conftest.py +12 -0
- core_10x/curve.py +321 -0
- core_10x/data_domain.py +48 -0
- core_10x/data_domain_binder.py +45 -0
- core_10x/directory.py +250 -0
- core_10x/entity.py +8 -0
- core_10x/entity_filter.py +5 -0
- core_10x/environment_variables.py +147 -0
- core_10x/exec_control.py +84 -0
- core_10x/experimental/__init__.py +0 -0
- core_10x/experimental/data_protocol_ex.py +34 -0
- core_10x/global_cache.py +121 -0
- core_10x/manual_tests/__init__.py +0 -0
- core_10x/manual_tests/calendar_test.py +35 -0
- core_10x/manual_tests/ctor_update_bug.py +58 -0
- core_10x/manual_tests/debug_graph_on.py +17 -0
- core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
- core_10x/manual_tests/enum_bits_test.py +17 -0
- core_10x/manual_tests/env_vars_trivial_test.py +12 -0
- core_10x/manual_tests/existing_traitable.py +33 -0
- core_10x/manual_tests/k10x_test1.py +13 -0
- core_10x/manual_tests/named_constant_test.py +121 -0
- core_10x/manual_tests/nucleus_trivial_test.py +42 -0
- core_10x/manual_tests/polars_test.py +14 -0
- core_10x/manual_tests/py_class_test.py +4 -0
- core_10x/manual_tests/rc_test.py +42 -0
- core_10x/manual_tests/rdate_test.py +12 -0
- core_10x/manual_tests/reference_serialization_bug.py +19 -0
- core_10x/manual_tests/resource_trivial_test.py +10 -0
- core_10x/manual_tests/store_uri_test.py +6 -0
- core_10x/manual_tests/trait_definition_test.py +19 -0
- core_10x/manual_tests/trait_filter_test.py +15 -0
- core_10x/manual_tests/trait_flag_modification_test.py +42 -0
- core_10x/manual_tests/trait_modification_bug.py +26 -0
- core_10x/manual_tests/traitable_as_of_test.py +82 -0
- core_10x/manual_tests/traitable_heir_test.py +39 -0
- core_10x/manual_tests/traitable_history_test.py +41 -0
- core_10x/manual_tests/traitable_serialization_test.py +54 -0
- core_10x/manual_tests/traitable_trivial_test.py +71 -0
- core_10x/manual_tests/trivial_graph_test.py +16 -0
- core_10x/manual_tests/ts_class_association_test.py +64 -0
- core_10x/manual_tests/ts_trivial_test.py +35 -0
- core_10x/named_constant.py +425 -0
- core_10x/nucleus.py +81 -0
- core_10x/package_manifest.py +85 -0
- core_10x/package_refactoring.py +153 -0
- core_10x/py_class.py +431 -0
- core_10x/rc.py +155 -0
- core_10x/rdate.py +339 -0
- core_10x/resource.py +189 -0
- core_10x/roman_number.py +67 -0
- core_10x/testlib/__init__.py +0 -0
- core_10x/testlib/test_store.py +240 -0
- core_10x/testlib/traitable_history_tests.py +787 -0
- core_10x/testlib/ts_tests.py +280 -0
- core_10x/trait.py +377 -0
- core_10x/trait_definition.py +176 -0
- core_10x/trait_filter.py +205 -0
- core_10x/trait_method_error.py +36 -0
- core_10x/traitable.py +1082 -0
- core_10x/traitable_cli.py +153 -0
- core_10x/traitable_heir.py +33 -0
- core_10x/traitable_id.py +31 -0
- core_10x/ts_store.py +172 -0
- core_10x/ts_store_type.py +26 -0
- core_10x/ts_union.py +147 -0
- core_10x/ui_hint.py +153 -0
- core_10x/unit_tests/test_concrete_traits.py +156 -0
- core_10x/unit_tests/test_converters.py +51 -0
- core_10x/unit_tests/test_curve.py +157 -0
- core_10x/unit_tests/test_directory.py +54 -0
- core_10x/unit_tests/test_documentation.py +172 -0
- core_10x/unit_tests/test_environment_variables.py +15 -0
- core_10x/unit_tests/test_filters.py +239 -0
- core_10x/unit_tests/test_graph.py +348 -0
- core_10x/unit_tests/test_named_constant.py +98 -0
- core_10x/unit_tests/test_rc.py +11 -0
- core_10x/unit_tests/test_rdate.py +484 -0
- core_10x/unit_tests/test_trait_method_error.py +80 -0
- core_10x/unit_tests/test_trait_modification.py +19 -0
- core_10x/unit_tests/test_traitable.py +959 -0
- core_10x/unit_tests/test_traitable_history.py +1 -0
- core_10x/unit_tests/test_ts_store.py +1 -0
- core_10x/unit_tests/test_ts_union.py +369 -0
- core_10x/unit_tests/test_ui_nodes.py +81 -0
- core_10x/unit_tests/test_xxcalendar.py +471 -0
- core_10x/vault/__init__.py +0 -0
- core_10x/vault/sec_keys.py +133 -0
- core_10x/vault/security_keys_old.py +168 -0
- core_10x/vault/vault.py +56 -0
- core_10x/vault/vault_traitable.py +56 -0
- core_10x/vault/vault_user.py +70 -0
- core_10x/xdate_time.py +136 -0
- core_10x/xnone.py +71 -0
- core_10x/xxcalendar.py +228 -0
- infra_10x/__init__.py +0 -0
- infra_10x/manual_tests/__init__.py +0 -0
- infra_10x/manual_tests/test_misc.py +16 -0
- infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
- infra_10x/mongodb_admin.py +111 -0
- infra_10x/mongodb_store.py +346 -0
- infra_10x/mongodb_utils.py +129 -0
- infra_10x/unit_tests/conftest.py +13 -0
- infra_10x/unit_tests/test_mongo_db.py +36 -0
- infra_10x/unit_tests/test_mongo_history.py +1 -0
- py10x_universe-0.1.3.dist-info/METADATA +406 -0
- py10x_universe-0.1.3.dist-info/RECORD +214 -0
- py10x_universe-0.1.3.dist-info/WHEEL +4 -0
- py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
- ui_10x/__init__.py +0 -0
- ui_10x/apps/__init__.py +0 -0
- ui_10x/apps/collection_editor_app.py +100 -0
- ui_10x/choice.py +212 -0
- ui_10x/collection_editor.py +135 -0
- ui_10x/concrete_trait_widgets.py +220 -0
- ui_10x/conftest.py +8 -0
- ui_10x/entity_stocker.py +173 -0
- ui_10x/examples/__init__.py +0 -0
- ui_10x/examples/_guess_word_data.py +14076 -0
- ui_10x/examples/collection_editor.py +17 -0
- ui_10x/examples/date_selector.py +14 -0
- ui_10x/examples/entity_stocker.py +18 -0
- ui_10x/examples/guess_word.py +392 -0
- ui_10x/examples/message_box.py +20 -0
- ui_10x/examples/multi_choice.py +17 -0
- ui_10x/examples/py_data_browser.py +66 -0
- ui_10x/examples/radiobox.py +29 -0
- ui_10x/examples/single_choice.py +31 -0
- ui_10x/examples/style_sheet.py +47 -0
- ui_10x/examples/trivial_entity_editor.py +18 -0
- ui_10x/platform.py +20 -0
- ui_10x/platform_interface.py +517 -0
- ui_10x/py_data_browser.py +249 -0
- ui_10x/qt6/__init__.py +0 -0
- ui_10x/qt6/conftest.py +8 -0
- ui_10x/qt6/manual_tests/__init__.py +0 -0
- ui_10x/qt6/manual_tests/basic_test.py +35 -0
- ui_10x/qt6/platform_implementation.py +275 -0
- ui_10x/qt6/utils.py +665 -0
- ui_10x/rio/__init__.py +0 -0
- ui_10x/rio/apps/examples/examples/__init__.py +22 -0
- ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
- ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
- ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
- ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
- ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
- ui_10x/rio/apps/examples/rio.toml +14 -0
- ui_10x/rio/component_builder.py +497 -0
- ui_10x/rio/components/__init__.py +9 -0
- ui_10x/rio/components/group_box.py +31 -0
- ui_10x/rio/components/labeled_checkbox.py +18 -0
- ui_10x/rio/components/line_edit.py +37 -0
- ui_10x/rio/components/radio_button.py +32 -0
- ui_10x/rio/components/separator.py +24 -0
- ui_10x/rio/components/splitter.py +121 -0
- ui_10x/rio/components/tree_view.py +75 -0
- ui_10x/rio/conftest.py +35 -0
- ui_10x/rio/internals/__init__.py +0 -0
- ui_10x/rio/internals/app.py +192 -0
- ui_10x/rio/manual_tests/__init__.py +0 -0
- ui_10x/rio/manual_tests/basic_test.py +24 -0
- ui_10x/rio/manual_tests/splitter.py +27 -0
- ui_10x/rio/platform_implementation.py +91 -0
- ui_10x/rio/style_sheet.py +53 -0
- ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
- ui_10x/rio/unit_tests/test_internals.py +630 -0
- ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
- ui_10x/rio/widgets/__init__.py +46 -0
- ui_10x/rio/widgets/application.py +109 -0
- ui_10x/rio/widgets/button.py +48 -0
- ui_10x/rio/widgets/button_group.py +60 -0
- ui_10x/rio/widgets/calendar.py +23 -0
- ui_10x/rio/widgets/checkbox.py +24 -0
- ui_10x/rio/widgets/dialog.py +137 -0
- ui_10x/rio/widgets/group_box.py +27 -0
- ui_10x/rio/widgets/layout.py +34 -0
- ui_10x/rio/widgets/line_edit.py +37 -0
- ui_10x/rio/widgets/list.py +105 -0
- ui_10x/rio/widgets/message_box.py +70 -0
- ui_10x/rio/widgets/scroll_area.py +31 -0
- ui_10x/rio/widgets/spacer.py +6 -0
- ui_10x/rio/widgets/splitter.py +45 -0
- ui_10x/rio/widgets/text_edit.py +28 -0
- ui_10x/rio/widgets/tree.py +89 -0
- ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
- ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
- ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
- ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
- ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
- ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
- ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
- ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
- ui_10x/table_header_view.py +305 -0
- ui_10x/table_view.py +174 -0
- ui_10x/trait_editor.py +189 -0
- ui_10x/trait_widget.py +131 -0
- ui_10x/traitable_editor.py +200 -0
- ui_10x/traitable_view.py +131 -0
- ui_10x/unit_tests/conftest.py +8 -0
- ui_10x/unit_tests/test_platform.py +9 -0
- 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
|