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,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
6
|
+
from core_10x.global_cache import cache
|
|
7
|
+
from core_10x.py_class import PyClass
|
|
8
|
+
|
|
9
|
+
CLASS_ID_DELIMITER = '/'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PackageRefactoring:
|
|
13
|
+
# fmt: off
|
|
14
|
+
s_module_name = '_refactoring'
|
|
15
|
+
s_records_name = '_records'
|
|
16
|
+
# fmt: on
|
|
17
|
+
s_instances = {}
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def register_top_level_packages(cls, *top_level_packages):
|
|
21
|
+
instances = cls.s_instances
|
|
22
|
+
for top_level_package in top_level_packages:
|
|
23
|
+
pkg_refact = cls(top_level_package)
|
|
24
|
+
instances[top_level_package] = pkg_refact
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def default_class_id(cls: type = None, canonical_class_name: str = None) -> str:
|
|
28
|
+
if canonical_class_name is None:
|
|
29
|
+
assert cls and inspect.isclass(cls), f'{cls} is not a valid class'
|
|
30
|
+
canonical_class_name = PyClass.name(cls)
|
|
31
|
+
assert '__main__' not in canonical_class_name, f'{cls} is defined in __main__ and does not have a stable id'
|
|
32
|
+
assert '<locals>' not in canonical_class_name, f'{cls} is defined locally in a function and cannot be reconstructed'
|
|
33
|
+
return canonical_class_name.replace('.', CLASS_ID_DELIMITER)
|
|
34
|
+
|
|
35
|
+
def __init__(self, top_package_name: str):
|
|
36
|
+
try:
|
|
37
|
+
pkg_init = importlib.import_module(top_package_name)
|
|
38
|
+
self.file_name = pkg_init.__file__.replace('__init__', self.__class__.s_module_name)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
raise OSError(f'Unknown top-level package {top_package_name}') from e
|
|
41
|
+
|
|
42
|
+
self.package_name = top_package_name
|
|
43
|
+
try:
|
|
44
|
+
module = importlib.import_module(self.s_module_name, top_package_name)
|
|
45
|
+
except Exception:
|
|
46
|
+
self.cid_to_path = {}
|
|
47
|
+
self.path_to_cid = {}
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
self.cid_to_path = records = getattr(module, self.s_records_name, None)
|
|
51
|
+
if records is None:
|
|
52
|
+
raise OSError(f'{top_package_name}.{self.s_module_name} is missing refactoring records {self.s_records_name}')
|
|
53
|
+
|
|
54
|
+
self.path_to_cid = path_to_cid = {}
|
|
55
|
+
for class_id, canonical_class_name in records.items():
|
|
56
|
+
assert isinstance(class_id, str), f'{class_id} must be a str'
|
|
57
|
+
assert isinstance(canonical_class_name, str), f'{canonical_class_name} must be a canonical class name'
|
|
58
|
+
|
|
59
|
+
path_to_cid[canonical_class_name] = class_id
|
|
60
|
+
|
|
61
|
+
def _refactor(self, cur_path: str, new_path: str):
|
|
62
|
+
cid = self.path_to_cid.get(cur_path)
|
|
63
|
+
if cid is None:
|
|
64
|
+
raise OSError(f'Unknown canonical class name {cur_path}')
|
|
65
|
+
|
|
66
|
+
self.cid_to_path[cid] = new_path
|
|
67
|
+
del self.path_to_cid[cur_path]
|
|
68
|
+
self.path_to_cid[new_path] = cid
|
|
69
|
+
|
|
70
|
+
def _save(self):
|
|
71
|
+
text = [f'{self.__class__.s_records_name} = {{']
|
|
72
|
+
for cid, path in self.cid_to_path.items():
|
|
73
|
+
text.append(f'\t{cid}:\t\t"{path}"')
|
|
74
|
+
text.append('}')
|
|
75
|
+
s = '\n'.join(text)
|
|
76
|
+
with open(self.file_name, mode='w') as f:
|
|
77
|
+
f.write(s)
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
@cache
|
|
81
|
+
def find_class_id(cls) -> str:
|
|
82
|
+
canonical_class_name = PyClass.name(cls)
|
|
83
|
+
pkg_refact: PackageRefactoring
|
|
84
|
+
for pkg_refact in PackageRefactoring.s_instances.values():
|
|
85
|
+
cid = pkg_refact.path_to_cid.get(canonical_class_name)
|
|
86
|
+
if cid is not None:
|
|
87
|
+
return cid
|
|
88
|
+
|
|
89
|
+
return PackageRefactoring.default_class_id(cls)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
@cache
|
|
93
|
+
def find_class(class_id: str):
|
|
94
|
+
pkg_refact: PackageRefactoring
|
|
95
|
+
for pkg_refact in PackageRefactoring.s_instances.values():
|
|
96
|
+
path = pkg_refact.cid_to_path.get(class_id)
|
|
97
|
+
if path is not None:
|
|
98
|
+
canonical_class_name = path
|
|
99
|
+
break
|
|
100
|
+
else:
|
|
101
|
+
canonical_class_name = class_id.replace(CLASS_ID_DELIMITER, '.')
|
|
102
|
+
|
|
103
|
+
cls = PyClass.find(canonical_class_name)
|
|
104
|
+
if cls is None or not inspect.isclass(cls):
|
|
105
|
+
raise OSError(f'Unknown class {canonical_class_name}; class_id = {class_id}')
|
|
106
|
+
|
|
107
|
+
return cls
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _find_or_create_instance(canonical_class_name) -> PackageRefactoring:
|
|
111
|
+
parts = canonical_class_name.split('.', maxsplit=1)
|
|
112
|
+
if len(parts) < 2:
|
|
113
|
+
raise ValueError(f'Invalid canonical class name {canonical_class_name}')
|
|
114
|
+
|
|
115
|
+
package_name = parts[0]
|
|
116
|
+
pkg_refactor: PackageRefactoring = PackageRefactoring.s_instances.get(package_name)
|
|
117
|
+
if not pkg_refactor:
|
|
118
|
+
pkg_refactor = PackageRefactoring(package_name)
|
|
119
|
+
PackageRefactoring.s_instances[package_name] = pkg_refactor
|
|
120
|
+
|
|
121
|
+
return pkg_refactor
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def remove_class(canonical_class_name: str, save: bool = True):
|
|
125
|
+
pkg_refactor = PackageRefactoring._find_or_create_instance(canonical_class_name)
|
|
126
|
+
cid = pkg_refactor.path_to_cid.get(canonical_class_name)
|
|
127
|
+
if cid is None:
|
|
128
|
+
raise ValueError(f'Unknown canonical class name {canonical_class_name}')
|
|
129
|
+
|
|
130
|
+
del pkg_refactor.cid_to_path[canonical_class_name]
|
|
131
|
+
del pkg_refactor.path_to_cid[cid]
|
|
132
|
+
|
|
133
|
+
if save:
|
|
134
|
+
pkg_refactor._save()
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def move_class(cur_path: str, new_path: str):
|
|
138
|
+
refactor1 = PackageRefactoring._find_or_create_instance(cur_path)
|
|
139
|
+
refactor2 = PackageRefactoring._find_or_create_instance(new_path)
|
|
140
|
+
the_same = refactor1 is refactor2
|
|
141
|
+
cid = refactor1.path_to_cid.get(cur_path)
|
|
142
|
+
if cid is not None:
|
|
143
|
+
del refactor1.path_to_cid[cur_path]
|
|
144
|
+
del refactor1.cid_to_path[cid]
|
|
145
|
+
if not the_same:
|
|
146
|
+
refactor1._save()
|
|
147
|
+
|
|
148
|
+
else:
|
|
149
|
+
cid = PackageRefactoring.default_class_id(canonical_class_name=cur_path)
|
|
150
|
+
|
|
151
|
+
refactor2.cid_to_path[cid] = new_path
|
|
152
|
+
refactor2.path_to_cid[new_path] = cid
|
|
153
|
+
refactor2._save()
|
core_10x/py_class.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import io
|
|
5
|
+
import pickle
|
|
6
|
+
import shlex
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from importlib_resources import files
|
|
10
|
+
|
|
11
|
+
from core_10x.global_cache import cache
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PyClass:
|
|
18
|
+
"""
|
|
19
|
+
Module top-level classes ONLY
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# fmt: off
|
|
23
|
+
NO_NAME = lambda cls: ''
|
|
24
|
+
QUAL_NAME = lambda cls: cls.__qualname__
|
|
25
|
+
CANONICAL_NAME = lambda cls: f'{cls.__module__}.{cls.__qualname__}'
|
|
26
|
+
# fmt: on
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def name(cls, name_type: Callable[[type], str] = CANONICAL_NAME) -> str:
|
|
30
|
+
try:
|
|
31
|
+
return name_type(cls)
|
|
32
|
+
except Exception as ex:
|
|
33
|
+
raise ValueError('cls must be a valid class') from ex
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def top_level_package(cls) -> str:
|
|
37
|
+
return cls.__module__.split('.', maxsplit=1)[0]
|
|
38
|
+
|
|
39
|
+
# ===================================================================================================================
|
|
40
|
+
# Finding classes/symbols in the code base
|
|
41
|
+
# ===================================================================================================================
|
|
42
|
+
@staticmethod
|
|
43
|
+
@cache
|
|
44
|
+
def dummy_unpickler() -> pickle.Unpickler:
|
|
45
|
+
file = io.BytesIO()
|
|
46
|
+
return pickle.Unpickler(file)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
@cache
|
|
50
|
+
def find_symbol(canonical_symbol_name: str):
|
|
51
|
+
try:
|
|
52
|
+
module_name, symbol_name = canonical_symbol_name.rsplit('.', maxsplit=1)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
raise ValueError(f"Invalid canonical_symbol_name = '{canonical_symbol_name}'") from e
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
return PyClass.dummy_unpickler().find_class(module_name, symbol_name)
|
|
58
|
+
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def find(canonical_class_name: str, *parents):
|
|
64
|
+
cls = PyClass.find_symbol(canonical_class_name)
|
|
65
|
+
if not cls or not inspect.isclass(cls):
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
subclass = all(issubclass(cls, parent) for parent in parents)
|
|
69
|
+
return cls if subclass else None
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def find_by_topic_and_suffix(topic: str, suffix: str, package_name: str, module_name: str, class_name: str):
|
|
73
|
+
module_name_with_topic = f'{module_name}_{topic}'
|
|
74
|
+
class_name_with_suffix = f'{class_name}{suffix}'
|
|
75
|
+
found = PyClass.find(f'{package_name}.{module_name_with_topic}.{class_name_with_suffix}')
|
|
76
|
+
if not found:
|
|
77
|
+
found = PyClass.find(f'{package_name}.{topic}.{module_name_with_topic}.{class_name_with_suffix}')
|
|
78
|
+
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
@cache
|
|
83
|
+
def find_related_class(cls: type, topic: str, class_name_suffix: str, *alternative_packages, alternative_parent_class: type = None) -> type:
|
|
84
|
+
"""
|
|
85
|
+
Tries to find a class whose name is cls.__name__ + class_name_suffix "related" to cls by a topic (e.g., 'ui').
|
|
86
|
+
By convention the module name must be the cls' module short name + _ + topic.
|
|
87
|
+
First, tries the alternative_packages, if any, then cls-module-package, then the cls-module-package.topic.
|
|
88
|
+
If fails, to find, tries the same logic for alternative_parent_class (cls must be a subclass of it), if any.
|
|
89
|
+
|
|
90
|
+
For example:
|
|
91
|
+
|
|
92
|
+
Consider class TextMessage in module abc.infra.messenger. Its custom editor class, will be looked up as follows:
|
|
93
|
+
PyClass.find_related_class(TextMessage, 'ui', 'Editor', *extra_packages)
|
|
94
|
+
- editor class name: TextMessageEditor
|
|
95
|
+
- editor short module name: messenger_ui
|
|
96
|
+
- search for TextMessageEditor in the following order:
|
|
97
|
+
-- [ ex_package.messenger_ui for ex_package in extra_packages ]
|
|
98
|
+
-- the module abc.infra.messenger_ui
|
|
99
|
+
-- the module abc.infra.ui.messenger_ui
|
|
100
|
+
"""
|
|
101
|
+
parts = cls.__module__.rsplit('.', maxsplit=1)
|
|
102
|
+
assert len(parts) >= 2, f'{cls} - package is missing'
|
|
103
|
+
|
|
104
|
+
module_name = f'{parts[-1]}'
|
|
105
|
+
class_name = cls.__name__
|
|
106
|
+
|
|
107
|
+
# -- 1) look it up in alternative_packages
|
|
108
|
+
for alt_package in alternative_packages:
|
|
109
|
+
found = PyClass.find_by_topic_and_suffix(topic, class_name_suffix, alt_package, module_name, class_name)
|
|
110
|
+
if found:
|
|
111
|
+
return found
|
|
112
|
+
|
|
113
|
+
# -- 2) look it up in the cls' package
|
|
114
|
+
package_name = parts[0]
|
|
115
|
+
found = PyClass.find_by_topic_and_suffix(topic, class_name_suffix, package_name, module_name, class_name)
|
|
116
|
+
if found:
|
|
117
|
+
return found
|
|
118
|
+
|
|
119
|
+
# -- 3) look it up for alternative_parent_class, if any
|
|
120
|
+
if alternative_parent_class:
|
|
121
|
+
assert issubclass(cls, alternative_parent_class), f'{cls} is not a subclass of {alternative_parent_class}'
|
|
122
|
+
found = PyClass.find_related_class(alternative_parent_class, topic, class_name_suffix)
|
|
123
|
+
|
|
124
|
+
return found
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def derived_from(cls, *parents, exclude_parents: tuple = ()) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
:param cls: a class
|
|
130
|
+
:param parents: classes the cls must be derived from
|
|
131
|
+
:param exclude_parents: classes the cls must NOT be derived from
|
|
132
|
+
"""
|
|
133
|
+
if any(issubclass(cls, parent) for parent in exclude_parents):
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
return all(issubclass(cls, parent) for parent in parents)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def parents(cls) -> tuple:
|
|
140
|
+
tree = inspect.getclasstree([cls])
|
|
141
|
+
try:
|
|
142
|
+
return tree[-1][0][1]
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise AssertionError(f'Something went wrong with inheritance tree of class {PyClass.name(cls)}') from e
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def class_tree(root_class: type, *classes) -> dict:
|
|
148
|
+
all_nodes = {}
|
|
149
|
+
for cls in classes:
|
|
150
|
+
PyClass._collect_class_nodes(cls, root_class, all_nodes)
|
|
151
|
+
|
|
152
|
+
return all_nodes.get(root_class)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _collect_class_nodes(cls: type, root_class: type, all_nodes: dict):
|
|
156
|
+
if not issubclass(cls, root_class):
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
node = all_nodes.get(cls)
|
|
160
|
+
if node is not None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
node = {}
|
|
164
|
+
all_nodes[cls] = node
|
|
165
|
+
parents = PyClass.parents(cls)
|
|
166
|
+
for p in parents:
|
|
167
|
+
PyClass._collect_class_nodes(p, root_class, all_nodes)
|
|
168
|
+
p_node = all_nodes.get(p)
|
|
169
|
+
if p_node is not None:
|
|
170
|
+
p_node[cls] = node
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def inheritance_paths(root_class: type, child_class: type) -> list:
|
|
174
|
+
tree = PyClass.class_tree(root_class, child_class)
|
|
175
|
+
return PyClass._inheritance_paths(tree)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _inheritance_paths(tree: dict) -> list:
|
|
179
|
+
res = []
|
|
180
|
+
for cls, class_entry in tree.items():
|
|
181
|
+
if not class_entry:
|
|
182
|
+
res.append([cls])
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
cls_paths = PyClass._inheritance_paths(class_entry)
|
|
186
|
+
for path in cls_paths:
|
|
187
|
+
x_path = [cls]
|
|
188
|
+
x_path.extend(path)
|
|
189
|
+
res.append(x_path)
|
|
190
|
+
|
|
191
|
+
return res
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def full_name_space(cls) -> dict:
|
|
195
|
+
full_ns = {}
|
|
196
|
+
classes = *PyClass.parents(cls), cls
|
|
197
|
+
for c in classes:
|
|
198
|
+
ns = dict(vars(inspect.getmodule(c)))
|
|
199
|
+
full_ns.update(ns)
|
|
200
|
+
|
|
201
|
+
return full_ns
|
|
202
|
+
|
|
203
|
+
# ---- We may need it to create local vars with particular names and values
|
|
204
|
+
# locals().update( { var_name: var_value } )
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def module_class_names(package_name: str, py_file_name: str) -> list:
|
|
208
|
+
try:
|
|
209
|
+
res = []
|
|
210
|
+
src = files(package_name).joinpath(py_file_name).read_text()
|
|
211
|
+
for line in src.split('\n'):
|
|
212
|
+
if line.startswith('class'):
|
|
213
|
+
class_name = shlex.shlex(line[5:]).get_token()
|
|
214
|
+
if class_name.isidentifier():
|
|
215
|
+
res.append(class_name)
|
|
216
|
+
|
|
217
|
+
return res
|
|
218
|
+
|
|
219
|
+
except Exception:
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def class_names_by_module(*package_names, exclude_packages=()) -> dict:
|
|
224
|
+
res = {}
|
|
225
|
+
for pname in package_names:
|
|
226
|
+
PyClass._collect_class_names(res, pname, exclude_packages)
|
|
227
|
+
|
|
228
|
+
return res
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def canonical_class_names(*package_names, exclude_packages=()) -> list:
|
|
232
|
+
res = []
|
|
233
|
+
for pname in package_names:
|
|
234
|
+
PyClass._collect_class_names(res, pname, exclude_packages)
|
|
235
|
+
|
|
236
|
+
return res
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _collect_class_names(res, package_name: str, exclude_packages: tuple):
|
|
240
|
+
if package_name in exclude_packages:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
dir = files(package_name)
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
raise AssertionError(f"'{package_name}' is neither a package, nor a module") from e
|
|
248
|
+
|
|
249
|
+
for item in dir.iterdir():
|
|
250
|
+
name: str = item.name
|
|
251
|
+
if name.startswith('__'):
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
if item.is_dir():
|
|
255
|
+
if isinstance(res, dict):
|
|
256
|
+
subres = {}
|
|
257
|
+
PyClass._collect_class_names(subres, package_name + '.' + name, exclude_packages)
|
|
258
|
+
if subres:
|
|
259
|
+
res[name] = subres
|
|
260
|
+
|
|
261
|
+
else:
|
|
262
|
+
PyClass._collect_class_names(res, package_name + '.' + name, exclude_packages)
|
|
263
|
+
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
if name.endswith('.py'):
|
|
267
|
+
PyClass._collect_module_class_names(res, package_name, name)
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _collect_module_class_names(res, package_name: str, py_file_name: str):
|
|
272
|
+
class_names = PyClass.module_class_names(package_name, py_file_name)
|
|
273
|
+
if class_names:
|
|
274
|
+
module_name = py_file_name[:-3]
|
|
275
|
+
if isinstance(res, dict):
|
|
276
|
+
res[module_name] = class_names
|
|
277
|
+
else:
|
|
278
|
+
full_module_name = f'{package_name}.{module_name}'
|
|
279
|
+
res.extend([f'{full_module_name}.{cname}' for cname in class_names])
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def all_classes(*package_names, exclude_packages=(), parent_classes=()) -> tuple:
|
|
283
|
+
"""
|
|
284
|
+
Warning! This function will import every class in the packages specified.
|
|
285
|
+
"""
|
|
286
|
+
all_class_names = PyClass.canonical_class_names(*package_names, exclude_packages=exclude_packages)
|
|
287
|
+
return tuple(cls for class_name in all_class_names if (cls := PyClass.find(class_name, *parent_classes)))
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def class_name_tree(classes: tuple) -> dict:
|
|
291
|
+
dir = {}
|
|
292
|
+
for cls in classes:
|
|
293
|
+
module_name = cls.__module__
|
|
294
|
+
path = module_name.split('.')
|
|
295
|
+
d = dir
|
|
296
|
+
for package in path[:-1]:
|
|
297
|
+
d = d.setdefault(package, {})
|
|
298
|
+
module_classes = d.setdefault(path[-1], [])
|
|
299
|
+
module_classes.append(cls.__qualname__)
|
|
300
|
+
|
|
301
|
+
return dir
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def own_attribute(cls, attr_name: str) -> tuple: # -- (exists, value)
|
|
305
|
+
d = cls.__dict__
|
|
306
|
+
value = d.get(attr_name, d)
|
|
307
|
+
rc = value is not d
|
|
308
|
+
return (rc, value if rc else None)
|
|
309
|
+
|
|
310
|
+
# ===================================================================================================================
|
|
311
|
+
# Class mapping to deal with migrations / refactoring class canonical names
|
|
312
|
+
# ===================================================================================================================
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# s_migration_map = {}
|
|
316
|
+
# s_rev_migration_map = {}
|
|
317
|
+
# @staticmethod
|
|
318
|
+
# def register_migration(canonical_class_name: str, new_canonical_class_name: str):
|
|
319
|
+
# existing_mapping = PyClass.s_migration_map.get(canonical_class_name)
|
|
320
|
+
# reverse_mapping = PyClass.s_rev_migration_map.get(new_canonical_class_name)
|
|
321
|
+
#
|
|
322
|
+
# if not existing_mapping:
|
|
323
|
+
# PyClass.s_migration_map[canonical_class_name] = new_canonical_class_name
|
|
324
|
+
# PyClass.s_rev_migration_map[new_canonical_class_name] = canonical_class_name
|
|
325
|
+
#
|
|
326
|
+
# else:
|
|
327
|
+
# assert existing_mapping == new_canonical_class_name, f'{canonical_class_name} is already migrated to {existing_mapping}'
|
|
328
|
+
# assert reverse_mapping == canonical_class_name, f'{new_canonical_class_name} is mapped to {reverse_mapping}'
|
|
329
|
+
#
|
|
330
|
+
# module_name, class_name = canonical_class_name.rsplit('.', 1)
|
|
331
|
+
# new_module_name, new_class_name = new_canonical_class_name.rsplit('.', 1)
|
|
332
|
+
#
|
|
333
|
+
# deferred_module = sys.modules.get(module_name)
|
|
334
|
+
# if not isinstance(deferred_module, DeferredModule):
|
|
335
|
+
# assert not hasattr(deferred_module, class_name), f'{class_name} is already defined in {module_name}'
|
|
336
|
+
# deferred_module = DeferredModule(module_name)
|
|
337
|
+
#
|
|
338
|
+
# if class_name not in deferred_module.__dict__:
|
|
339
|
+
# setattr(deferred_module, class_name, DeferredClass(module_name = new_module_name, class_name = new_class_name))
|
|
340
|
+
#
|
|
341
|
+
# @staticmethod
|
|
342
|
+
# def register_multiple_migrations(migration_map: dict):
|
|
343
|
+
# for canonical_class_name, new_canonical_class_name in migration_map.items():
|
|
344
|
+
# PyClass.register_migration(canonical_class_name, new_canonical_class_name)
|
|
345
|
+
#
|
|
346
|
+
# @staticmethod
|
|
347
|
+
# def all_migrations(canonical_class_name: str) -> list:
|
|
348
|
+
# rev_map = PyClass.s_rev_migration_map
|
|
349
|
+
# all_names = []
|
|
350
|
+
# while canonical_class_name:
|
|
351
|
+
# all_names.append(canonical_class_name)
|
|
352
|
+
# canonical_class_name = rev_map.get(canonical_class_name)
|
|
353
|
+
#
|
|
354
|
+
# return all_names
|
|
355
|
+
#
|
|
356
|
+
# @staticmethod
|
|
357
|
+
# @cache
|
|
358
|
+
# def effective_canonical_name(cls) -> str:
|
|
359
|
+
# return PyClass.all_migrations(PyClass.name(cls))[-1]
|
|
360
|
+
#
|
|
361
|
+
# class DeferredModule(type(sys)):
|
|
362
|
+
# def __init__(self, module_name: str):
|
|
363
|
+
# super().__init__(module_name)
|
|
364
|
+
# self.deferred_count = None
|
|
365
|
+
# self.module = sys.modules.get(module_name)
|
|
366
|
+
#
|
|
367
|
+
# try:
|
|
368
|
+
# self.__spec__ = importlib.util.find_spec(module_name)
|
|
369
|
+
# except ModuleNotFoundError:
|
|
370
|
+
# pass
|
|
371
|
+
#
|
|
372
|
+
# sys.modules[module_name] = self
|
|
373
|
+
# parent, _, name = module_name.rpartition('.')
|
|
374
|
+
# assert parent, f'{module_name} - top level deferred modules are not allowed'
|
|
375
|
+
#
|
|
376
|
+
# parent_module = sys.modules.get(parent)
|
|
377
|
+
# if not parent_module:
|
|
378
|
+
# top_level = parent.rpartition('.')[0]
|
|
379
|
+
# parent_module = DeferredModule(parent) if top_level else importlib.import_module(parent)
|
|
380
|
+
#
|
|
381
|
+
# self.parent = parent
|
|
382
|
+
# self.name = name
|
|
383
|
+
#
|
|
384
|
+
# setattr(parent_module, name, self)
|
|
385
|
+
#
|
|
386
|
+
# def __getattribute__(self, item):
|
|
387
|
+
# if item == '__path__':
|
|
388
|
+
# parent_path = sys.modules[self.parent].__path__
|
|
389
|
+
# name = self.name
|
|
390
|
+
# return [ f'{p_path}/{name}' for p_path in parent_path ]
|
|
391
|
+
#
|
|
392
|
+
# if item[0] in ( 'm', '_' ) and item[1] == '_':
|
|
393
|
+
# return object.__getattribute__(self, item)
|
|
394
|
+
#
|
|
395
|
+
# module_name = self.__name__
|
|
396
|
+
#
|
|
397
|
+
# d_count = self.deferred_count
|
|
398
|
+
# if d_count is None:
|
|
399
|
+
# assert self is sys.modules[module_name], f'Module {module_name} is different from sys.modules[{module_name}]'
|
|
400
|
+
# module = self.module
|
|
401
|
+
# if module:
|
|
402
|
+
# getattr(module, item) #-- import it as it's deferred
|
|
403
|
+
# self.__dict__.update(module.__dict__)
|
|
404
|
+
# else:
|
|
405
|
+
# del sys.modules[module_name]
|
|
406
|
+
# spec = importlib.util.find_spec(module_name)
|
|
407
|
+
# if spec:
|
|
408
|
+
# module = importlib.import_module(module_name)
|
|
409
|
+
# self.__dict__.update(module.__dict__)
|
|
410
|
+
#
|
|
411
|
+
# sys.modules[module_name] = self
|
|
412
|
+
# setattr(sys.modules[self.parent], self.name, self)
|
|
413
|
+
#
|
|
414
|
+
# d_count = sum( isinstance(value, DeferredClass) for value in self.__dict__.values() )
|
|
415
|
+
#
|
|
416
|
+
# df_class = self.__dict__.get(item)
|
|
417
|
+
# if isinstance(df_class, DeferredClass):
|
|
418
|
+
# self.__dict__[item] = df_class.finalize()
|
|
419
|
+
# d_count -= 1
|
|
420
|
+
#
|
|
421
|
+
# self.deferred_count = d_count
|
|
422
|
+
# return object.__getattribute__(self, item)
|
|
423
|
+
#
|
|
424
|
+
# class DeferredClass:
|
|
425
|
+
# def __init__(self, module_name: str = None, class_name: str = None):
|
|
426
|
+
# self.module_name = module_name
|
|
427
|
+
# self.class_name = class_name
|
|
428
|
+
#
|
|
429
|
+
# def finalize(self) -> type:
|
|
430
|
+
# module = importlib.import_module(self.module_name)
|
|
431
|
+
# return getattr(module, self.class_name)
|