py10x-universe 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. core_10x/__init__.py +42 -0
  2. core_10x/backbone/__init__.py +0 -0
  3. core_10x/backbone/backbone_store.py +59 -0
  4. core_10x/backbone/backbone_traitable.py +30 -0
  5. core_10x/backbone/backbone_user.py +66 -0
  6. core_10x/backbone/bound_data_domain.py +49 -0
  7. core_10x/backbone/namespace.py +101 -0
  8. core_10x/backbone/vault.py +38 -0
  9. core_10x/code_samples/__init__.py +0 -0
  10. core_10x/code_samples/_package_manifest.py +3 -0
  11. core_10x/code_samples/directories.py +181 -0
  12. core_10x/code_samples/person.py +76 -0
  13. core_10x/concrete_traits.py +356 -0
  14. core_10x/conftest.py +12 -0
  15. core_10x/curve.py +321 -0
  16. core_10x/data_domain.py +48 -0
  17. core_10x/data_domain_binder.py +45 -0
  18. core_10x/directory.py +250 -0
  19. core_10x/entity.py +8 -0
  20. core_10x/entity_filter.py +5 -0
  21. core_10x/environment_variables.py +147 -0
  22. core_10x/exec_control.py +84 -0
  23. core_10x/experimental/__init__.py +0 -0
  24. core_10x/experimental/data_protocol_ex.py +34 -0
  25. core_10x/global_cache.py +121 -0
  26. core_10x/manual_tests/__init__.py +0 -0
  27. core_10x/manual_tests/calendar_test.py +35 -0
  28. core_10x/manual_tests/ctor_update_bug.py +58 -0
  29. core_10x/manual_tests/debug_graph_on.py +17 -0
  30. core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
  31. core_10x/manual_tests/enum_bits_test.py +17 -0
  32. core_10x/manual_tests/env_vars_trivial_test.py +12 -0
  33. core_10x/manual_tests/existing_traitable.py +33 -0
  34. core_10x/manual_tests/k10x_test1.py +13 -0
  35. core_10x/manual_tests/named_constant_test.py +121 -0
  36. core_10x/manual_tests/nucleus_trivial_test.py +42 -0
  37. core_10x/manual_tests/polars_test.py +14 -0
  38. core_10x/manual_tests/py_class_test.py +4 -0
  39. core_10x/manual_tests/rc_test.py +42 -0
  40. core_10x/manual_tests/rdate_test.py +12 -0
  41. core_10x/manual_tests/reference_serialization_bug.py +19 -0
  42. core_10x/manual_tests/resource_trivial_test.py +10 -0
  43. core_10x/manual_tests/store_uri_test.py +6 -0
  44. core_10x/manual_tests/trait_definition_test.py +19 -0
  45. core_10x/manual_tests/trait_filter_test.py +15 -0
  46. core_10x/manual_tests/trait_flag_modification_test.py +42 -0
  47. core_10x/manual_tests/trait_modification_bug.py +26 -0
  48. core_10x/manual_tests/traitable_as_of_test.py +82 -0
  49. core_10x/manual_tests/traitable_heir_test.py +39 -0
  50. core_10x/manual_tests/traitable_history_test.py +41 -0
  51. core_10x/manual_tests/traitable_serialization_test.py +54 -0
  52. core_10x/manual_tests/traitable_trivial_test.py +71 -0
  53. core_10x/manual_tests/trivial_graph_test.py +16 -0
  54. core_10x/manual_tests/ts_class_association_test.py +64 -0
  55. core_10x/manual_tests/ts_trivial_test.py +35 -0
  56. core_10x/named_constant.py +425 -0
  57. core_10x/nucleus.py +81 -0
  58. core_10x/package_manifest.py +85 -0
  59. core_10x/package_refactoring.py +153 -0
  60. core_10x/py_class.py +431 -0
  61. core_10x/rc.py +155 -0
  62. core_10x/rdate.py +339 -0
  63. core_10x/resource.py +189 -0
  64. core_10x/roman_number.py +67 -0
  65. core_10x/testlib/__init__.py +0 -0
  66. core_10x/testlib/test_store.py +240 -0
  67. core_10x/testlib/traitable_history_tests.py +787 -0
  68. core_10x/testlib/ts_tests.py +280 -0
  69. core_10x/trait.py +377 -0
  70. core_10x/trait_definition.py +176 -0
  71. core_10x/trait_filter.py +205 -0
  72. core_10x/trait_method_error.py +36 -0
  73. core_10x/traitable.py +1082 -0
  74. core_10x/traitable_cli.py +153 -0
  75. core_10x/traitable_heir.py +33 -0
  76. core_10x/traitable_id.py +31 -0
  77. core_10x/ts_store.py +172 -0
  78. core_10x/ts_store_type.py +26 -0
  79. core_10x/ts_union.py +147 -0
  80. core_10x/ui_hint.py +153 -0
  81. core_10x/unit_tests/test_concrete_traits.py +156 -0
  82. core_10x/unit_tests/test_converters.py +51 -0
  83. core_10x/unit_tests/test_curve.py +157 -0
  84. core_10x/unit_tests/test_directory.py +54 -0
  85. core_10x/unit_tests/test_documentation.py +172 -0
  86. core_10x/unit_tests/test_environment_variables.py +15 -0
  87. core_10x/unit_tests/test_filters.py +239 -0
  88. core_10x/unit_tests/test_graph.py +348 -0
  89. core_10x/unit_tests/test_named_constant.py +98 -0
  90. core_10x/unit_tests/test_rc.py +11 -0
  91. core_10x/unit_tests/test_rdate.py +484 -0
  92. core_10x/unit_tests/test_trait_method_error.py +80 -0
  93. core_10x/unit_tests/test_trait_modification.py +19 -0
  94. core_10x/unit_tests/test_traitable.py +959 -0
  95. core_10x/unit_tests/test_traitable_history.py +1 -0
  96. core_10x/unit_tests/test_ts_store.py +1 -0
  97. core_10x/unit_tests/test_ts_union.py +369 -0
  98. core_10x/unit_tests/test_ui_nodes.py +81 -0
  99. core_10x/unit_tests/test_xxcalendar.py +471 -0
  100. core_10x/vault/__init__.py +0 -0
  101. core_10x/vault/sec_keys.py +133 -0
  102. core_10x/vault/security_keys_old.py +168 -0
  103. core_10x/vault/vault.py +56 -0
  104. core_10x/vault/vault_traitable.py +56 -0
  105. core_10x/vault/vault_user.py +70 -0
  106. core_10x/xdate_time.py +136 -0
  107. core_10x/xnone.py +71 -0
  108. core_10x/xxcalendar.py +228 -0
  109. infra_10x/__init__.py +0 -0
  110. infra_10x/manual_tests/__init__.py +0 -0
  111. infra_10x/manual_tests/test_misc.py +16 -0
  112. infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
  113. infra_10x/mongodb_admin.py +111 -0
  114. infra_10x/mongodb_store.py +346 -0
  115. infra_10x/mongodb_utils.py +129 -0
  116. infra_10x/unit_tests/conftest.py +13 -0
  117. infra_10x/unit_tests/test_mongo_db.py +36 -0
  118. infra_10x/unit_tests/test_mongo_history.py +1 -0
  119. py10x_universe-0.1.3.dist-info/METADATA +406 -0
  120. py10x_universe-0.1.3.dist-info/RECORD +214 -0
  121. py10x_universe-0.1.3.dist-info/WHEEL +4 -0
  122. py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
  123. ui_10x/__init__.py +0 -0
  124. ui_10x/apps/__init__.py +0 -0
  125. ui_10x/apps/collection_editor_app.py +100 -0
  126. ui_10x/choice.py +212 -0
  127. ui_10x/collection_editor.py +135 -0
  128. ui_10x/concrete_trait_widgets.py +220 -0
  129. ui_10x/conftest.py +8 -0
  130. ui_10x/entity_stocker.py +173 -0
  131. ui_10x/examples/__init__.py +0 -0
  132. ui_10x/examples/_guess_word_data.py +14076 -0
  133. ui_10x/examples/collection_editor.py +17 -0
  134. ui_10x/examples/date_selector.py +14 -0
  135. ui_10x/examples/entity_stocker.py +18 -0
  136. ui_10x/examples/guess_word.py +392 -0
  137. ui_10x/examples/message_box.py +20 -0
  138. ui_10x/examples/multi_choice.py +17 -0
  139. ui_10x/examples/py_data_browser.py +66 -0
  140. ui_10x/examples/radiobox.py +29 -0
  141. ui_10x/examples/single_choice.py +31 -0
  142. ui_10x/examples/style_sheet.py +47 -0
  143. ui_10x/examples/trivial_entity_editor.py +18 -0
  144. ui_10x/platform.py +20 -0
  145. ui_10x/platform_interface.py +517 -0
  146. ui_10x/py_data_browser.py +249 -0
  147. ui_10x/qt6/__init__.py +0 -0
  148. ui_10x/qt6/conftest.py +8 -0
  149. ui_10x/qt6/manual_tests/__init__.py +0 -0
  150. ui_10x/qt6/manual_tests/basic_test.py +35 -0
  151. ui_10x/qt6/platform_implementation.py +275 -0
  152. ui_10x/qt6/utils.py +665 -0
  153. ui_10x/rio/__init__.py +0 -0
  154. ui_10x/rio/apps/examples/examples/__init__.py +22 -0
  155. ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
  156. ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
  157. ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
  158. ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
  159. ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
  160. ui_10x/rio/apps/examples/rio.toml +14 -0
  161. ui_10x/rio/component_builder.py +497 -0
  162. ui_10x/rio/components/__init__.py +9 -0
  163. ui_10x/rio/components/group_box.py +31 -0
  164. ui_10x/rio/components/labeled_checkbox.py +18 -0
  165. ui_10x/rio/components/line_edit.py +37 -0
  166. ui_10x/rio/components/radio_button.py +32 -0
  167. ui_10x/rio/components/separator.py +24 -0
  168. ui_10x/rio/components/splitter.py +121 -0
  169. ui_10x/rio/components/tree_view.py +75 -0
  170. ui_10x/rio/conftest.py +35 -0
  171. ui_10x/rio/internals/__init__.py +0 -0
  172. ui_10x/rio/internals/app.py +192 -0
  173. ui_10x/rio/manual_tests/__init__.py +0 -0
  174. ui_10x/rio/manual_tests/basic_test.py +24 -0
  175. ui_10x/rio/manual_tests/splitter.py +27 -0
  176. ui_10x/rio/platform_implementation.py +91 -0
  177. ui_10x/rio/style_sheet.py +53 -0
  178. ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
  179. ui_10x/rio/unit_tests/test_internals.py +630 -0
  180. ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
  181. ui_10x/rio/widgets/__init__.py +46 -0
  182. ui_10x/rio/widgets/application.py +109 -0
  183. ui_10x/rio/widgets/button.py +48 -0
  184. ui_10x/rio/widgets/button_group.py +60 -0
  185. ui_10x/rio/widgets/calendar.py +23 -0
  186. ui_10x/rio/widgets/checkbox.py +24 -0
  187. ui_10x/rio/widgets/dialog.py +137 -0
  188. ui_10x/rio/widgets/group_box.py +27 -0
  189. ui_10x/rio/widgets/layout.py +34 -0
  190. ui_10x/rio/widgets/line_edit.py +37 -0
  191. ui_10x/rio/widgets/list.py +105 -0
  192. ui_10x/rio/widgets/message_box.py +70 -0
  193. ui_10x/rio/widgets/scroll_area.py +31 -0
  194. ui_10x/rio/widgets/spacer.py +6 -0
  195. ui_10x/rio/widgets/splitter.py +45 -0
  196. ui_10x/rio/widgets/text_edit.py +28 -0
  197. ui_10x/rio/widgets/tree.py +89 -0
  198. ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
  199. ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
  200. ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
  201. ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
  202. ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
  203. ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
  204. ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
  205. ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
  206. ui_10x/table_header_view.py +305 -0
  207. ui_10x/table_view.py +174 -0
  208. ui_10x/trait_editor.py +189 -0
  209. ui_10x/trait_widget.py +131 -0
  210. ui_10x/traitable_editor.py +200 -0
  211. ui_10x/traitable_view.py +131 -0
  212. ui_10x/unit_tests/conftest.py +8 -0
  213. ui_10x/unit_tests/test_platform.py +9 -0
  214. ui_10x/utils.py +661 -0
@@ -0,0 +1,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)