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,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)
|