hpcflow-new2 0.2.0a189__py3-none-any.whl → 0.2.0a190__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 (115) hide show
  1. hpcflow/__pyinstaller/hook-hpcflow.py +8 -6
  2. hpcflow/_version.py +1 -1
  3. hpcflow/app.py +1 -0
  4. hpcflow/data/scripts/main_script_test_hdf5_in_obj.py +1 -1
  5. hpcflow/data/scripts/main_script_test_hdf5_out_obj.py +1 -1
  6. hpcflow/sdk/__init__.py +21 -15
  7. hpcflow/sdk/app.py +2133 -770
  8. hpcflow/sdk/cli.py +281 -250
  9. hpcflow/sdk/cli_common.py +6 -2
  10. hpcflow/sdk/config/__init__.py +1 -1
  11. hpcflow/sdk/config/callbacks.py +77 -42
  12. hpcflow/sdk/config/cli.py +126 -103
  13. hpcflow/sdk/config/config.py +578 -311
  14. hpcflow/sdk/config/config_file.py +131 -95
  15. hpcflow/sdk/config/errors.py +112 -85
  16. hpcflow/sdk/config/types.py +145 -0
  17. hpcflow/sdk/core/actions.py +1054 -994
  18. hpcflow/sdk/core/app_aware.py +24 -0
  19. hpcflow/sdk/core/cache.py +81 -63
  20. hpcflow/sdk/core/command_files.py +275 -185
  21. hpcflow/sdk/core/commands.py +111 -107
  22. hpcflow/sdk/core/element.py +724 -503
  23. hpcflow/sdk/core/enums.py +192 -0
  24. hpcflow/sdk/core/environment.py +74 -93
  25. hpcflow/sdk/core/errors.py +398 -51
  26. hpcflow/sdk/core/json_like.py +540 -272
  27. hpcflow/sdk/core/loop.py +380 -334
  28. hpcflow/sdk/core/loop_cache.py +160 -43
  29. hpcflow/sdk/core/object_list.py +370 -207
  30. hpcflow/sdk/core/parameters.py +728 -600
  31. hpcflow/sdk/core/rule.py +59 -41
  32. hpcflow/sdk/core/run_dir_files.py +33 -22
  33. hpcflow/sdk/core/task.py +1546 -1325
  34. hpcflow/sdk/core/task_schema.py +240 -196
  35. hpcflow/sdk/core/test_utils.py +126 -88
  36. hpcflow/sdk/core/types.py +387 -0
  37. hpcflow/sdk/core/utils.py +410 -305
  38. hpcflow/sdk/core/validation.py +82 -9
  39. hpcflow/sdk/core/workflow.py +1192 -1028
  40. hpcflow/sdk/core/zarr_io.py +98 -137
  41. hpcflow/sdk/demo/cli.py +46 -33
  42. hpcflow/sdk/helper/cli.py +18 -16
  43. hpcflow/sdk/helper/helper.py +75 -63
  44. hpcflow/sdk/helper/watcher.py +61 -28
  45. hpcflow/sdk/log.py +83 -59
  46. hpcflow/sdk/persistence/__init__.py +8 -31
  47. hpcflow/sdk/persistence/base.py +988 -586
  48. hpcflow/sdk/persistence/defaults.py +6 -0
  49. hpcflow/sdk/persistence/discovery.py +38 -0
  50. hpcflow/sdk/persistence/json.py +408 -153
  51. hpcflow/sdk/persistence/pending.py +158 -123
  52. hpcflow/sdk/persistence/store_resource.py +37 -22
  53. hpcflow/sdk/persistence/types.py +307 -0
  54. hpcflow/sdk/persistence/utils.py +14 -11
  55. hpcflow/sdk/persistence/zarr.py +477 -420
  56. hpcflow/sdk/runtime.py +44 -41
  57. hpcflow/sdk/submission/{jobscript_info.py → enums.py} +39 -12
  58. hpcflow/sdk/submission/jobscript.py +444 -404
  59. hpcflow/sdk/submission/schedulers/__init__.py +133 -40
  60. hpcflow/sdk/submission/schedulers/direct.py +97 -71
  61. hpcflow/sdk/submission/schedulers/sge.py +132 -126
  62. hpcflow/sdk/submission/schedulers/slurm.py +263 -268
  63. hpcflow/sdk/submission/schedulers/utils.py +7 -2
  64. hpcflow/sdk/submission/shells/__init__.py +14 -15
  65. hpcflow/sdk/submission/shells/base.py +102 -29
  66. hpcflow/sdk/submission/shells/bash.py +72 -55
  67. hpcflow/sdk/submission/shells/os_version.py +31 -30
  68. hpcflow/sdk/submission/shells/powershell.py +37 -29
  69. hpcflow/sdk/submission/submission.py +203 -257
  70. hpcflow/sdk/submission/types.py +143 -0
  71. hpcflow/sdk/typing.py +163 -12
  72. hpcflow/tests/conftest.py +8 -6
  73. hpcflow/tests/schedulers/slurm/test_slurm_submission.py +5 -2
  74. hpcflow/tests/scripts/test_main_scripts.py +60 -30
  75. hpcflow/tests/shells/wsl/test_wsl_submission.py +6 -4
  76. hpcflow/tests/unit/test_action.py +86 -75
  77. hpcflow/tests/unit/test_action_rule.py +9 -4
  78. hpcflow/tests/unit/test_app.py +13 -6
  79. hpcflow/tests/unit/test_cli.py +1 -1
  80. hpcflow/tests/unit/test_command.py +71 -54
  81. hpcflow/tests/unit/test_config.py +20 -15
  82. hpcflow/tests/unit/test_config_file.py +21 -18
  83. hpcflow/tests/unit/test_element.py +58 -62
  84. hpcflow/tests/unit/test_element_iteration.py +3 -1
  85. hpcflow/tests/unit/test_element_set.py +29 -19
  86. hpcflow/tests/unit/test_group.py +4 -2
  87. hpcflow/tests/unit/test_input_source.py +116 -93
  88. hpcflow/tests/unit/test_input_value.py +29 -24
  89. hpcflow/tests/unit/test_json_like.py +44 -35
  90. hpcflow/tests/unit/test_loop.py +65 -58
  91. hpcflow/tests/unit/test_object_list.py +17 -12
  92. hpcflow/tests/unit/test_parameter.py +16 -7
  93. hpcflow/tests/unit/test_persistence.py +48 -35
  94. hpcflow/tests/unit/test_resources.py +20 -18
  95. hpcflow/tests/unit/test_run.py +8 -3
  96. hpcflow/tests/unit/test_runtime.py +2 -1
  97. hpcflow/tests/unit/test_schema_input.py +23 -15
  98. hpcflow/tests/unit/test_shell.py +3 -2
  99. hpcflow/tests/unit/test_slurm.py +8 -7
  100. hpcflow/tests/unit/test_submission.py +39 -19
  101. hpcflow/tests/unit/test_task.py +352 -247
  102. hpcflow/tests/unit/test_task_schema.py +33 -20
  103. hpcflow/tests/unit/test_utils.py +9 -11
  104. hpcflow/tests/unit/test_value_sequence.py +15 -12
  105. hpcflow/tests/unit/test_workflow.py +114 -83
  106. hpcflow/tests/unit/test_workflow_template.py +0 -1
  107. hpcflow/tests/workflows/test_jobscript.py +2 -1
  108. hpcflow/tests/workflows/test_workflows.py +18 -13
  109. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/METADATA +2 -1
  110. hpcflow_new2-0.2.0a190.dist-info/RECORD +165 -0
  111. hpcflow/sdk/core/parallel.py +0 -21
  112. hpcflow_new2-0.2.0a189.dist-info/RECORD +0 -158
  113. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/LICENSE +0 -0
  114. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/WHEEL +0 -0
  115. {hpcflow_new2-0.2.0a189.dist-info → hpcflow_new2-0.2.0a190.dist-info}/entry_points.txt +0 -0
@@ -4,15 +4,40 @@ graph of objects and either JSON or YAML.
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
+ from collections import defaultdict
8
+ from collections.abc import Container, Sequence, Mapping
7
9
  import copy
8
10
  from dataclasses import dataclass
9
11
  import enum
10
- from typing import Dict, List, Optional, Type, Union
12
+ from types import SimpleNamespace
13
+ from typing import overload, Protocol, cast, runtime_checkable, TYPE_CHECKING
14
+ from typing_extensions import final, override
11
15
 
16
+ from hpcflow.sdk.core.app_aware import AppAware
17
+ from hpcflow.sdk.typing import hydrate
12
18
  from hpcflow.sdk import app, get_SDK_logger
13
- from .utils import classproperty, get_md5_hash
14
- from .validation import get_schema
15
- from .errors import ToJSONLikeChildReferenceError
19
+ from hpcflow.sdk.core.utils import get_md5_hash
20
+ from hpcflow.sdk.core.validation import get_schema
21
+ from hpcflow.sdk.core.errors import ToJSONLikeChildReferenceError
22
+
23
+ if TYPE_CHECKING:
24
+ from typing import Any, ClassVar, Literal
25
+ from typing_extensions import Self, TypeAlias, TypeIs
26
+ from ..app import BaseApp
27
+ from .object_list import ObjectList
28
+
29
+ _BasicJsonTypes: TypeAlias = "int | float | str | None"
30
+ _WriteStructure: TypeAlias = (
31
+ "list[JSONable] | tuple[JSONable, ...] | set[JSONable] | dict[str, JSONable]"
32
+ )
33
+ JSONDocument: TypeAlias = "Sequence[JSONed] | Mapping[str, JSONed]"
34
+ JSONable: TypeAlias = "_WriteStructure | enum.Enum | BaseJSONLike | _BasicJsonTypes"
35
+ JSONed: TypeAlias = "JSONDocument | _BasicJsonTypes"
36
+
37
+ if TYPE_CHECKING:
38
+ _ChildType: TypeAlias = "type[enum.Enum | JSONLike]"
39
+ _JSONDeserState: TypeAlias = "dict[str, dict[str, JSONed]] | None"
40
+
16
41
 
17
42
  #: Primitive types supported by the serialization mechanism.
18
43
  PRIMITIVES = (
@@ -25,66 +50,186 @@ PRIMITIVES = (
25
50
  _SDK_logger = get_SDK_logger(__name__)
26
51
 
27
52
 
28
- def to_json_like(obj, shared_data=None, parent_refs=None, path=None):
53
+ @runtime_checkable
54
+ class _AltConstructFromJson(Protocol):
55
+ @classmethod
56
+ def _json_like_constructor(cls, json_like: Mapping[str, JSONed]) -> Self:
57
+ pass
58
+
59
+
60
+ def _is_base_json_like(value: JSONable) -> TypeIs[BaseJSONLike]:
61
+ return value is not None and hasattr(value, "to_json_like")
62
+
63
+
64
+ _MAX_DEPTH = 50
65
+
66
+
67
+ @overload
68
+ def to_json_like(
69
+ obj: int,
70
+ shared_data: _JSONDeserState = None,
71
+ parent_refs: dict | None = None,
72
+ path: list | None = None,
73
+ ) -> tuple[int, _JSONDeserState]:
74
+ ...
75
+
76
+
77
+ @overload
78
+ def to_json_like(
79
+ obj: float,
80
+ shared_data: _JSONDeserState = None,
81
+ parent_refs: dict | None = None,
82
+ path: list | None = None,
83
+ ) -> tuple[float, _JSONDeserState]:
84
+ ...
85
+
86
+
87
+ @overload
88
+ def to_json_like(
89
+ obj: str,
90
+ shared_data: _JSONDeserState = None,
91
+ parent_refs: dict | None = None,
92
+ path: list | None = None,
93
+ ) -> tuple[str, _JSONDeserState]:
94
+ ...
95
+
96
+
97
+ @overload
98
+ def to_json_like(
99
+ obj: None,
100
+ shared_data: _JSONDeserState = None,
101
+ parent_refs: dict | None = None,
102
+ path: list | None = None,
103
+ ) -> tuple[None, _JSONDeserState]:
104
+ ...
105
+
106
+
107
+ @overload
108
+ def to_json_like(
109
+ obj: enum.Enum,
110
+ shared_data: _JSONDeserState = None,
111
+ parent_refs: dict | None = None,
112
+ path: list | None = None,
113
+ ) -> tuple[str, _JSONDeserState]:
114
+ ...
115
+
116
+
117
+ @overload
118
+ def to_json_like(
119
+ obj: list[JSONable],
120
+ shared_data: _JSONDeserState = None,
121
+ parent_refs: dict | None = None,
122
+ path: list | None = None,
123
+ ) -> tuple[Sequence[JSONed], _JSONDeserState]:
124
+ ...
125
+
126
+
127
+ @overload
128
+ def to_json_like(
129
+ obj: tuple[JSONable, ...],
130
+ shared_data: _JSONDeserState = None,
131
+ parent_refs: dict | None = None,
132
+ path: list | None = None,
133
+ ) -> tuple[Sequence[JSONed], _JSONDeserState]:
134
+ ...
135
+
136
+
137
+ @overload
138
+ def to_json_like(
139
+ obj: set[JSONable],
140
+ shared_data: _JSONDeserState = None,
141
+ parent_refs: dict | None = None,
142
+ path: list | None = None,
143
+ ) -> tuple[Sequence[JSONed], _JSONDeserState]:
144
+ ...
145
+
146
+
147
+ @overload
148
+ def to_json_like(
149
+ obj: dict[str, JSONable],
150
+ shared_data: _JSONDeserState = None,
151
+ parent_refs: dict | None = None,
152
+ path: list | None = None,
153
+ ) -> tuple[Mapping[str, JSONed], _JSONDeserState]:
154
+ ...
155
+
156
+
157
+ @overload
158
+ def to_json_like(
159
+ obj: BaseJSONLike,
160
+ shared_data: _JSONDeserState = None,
161
+ parent_refs: dict | None = None,
162
+ path: list | None = None,
163
+ ) -> tuple[Mapping[str, JSONed], _JSONDeserState]:
164
+ ...
165
+
166
+
167
+ def to_json_like(
168
+ obj: JSONable,
169
+ shared_data: _JSONDeserState = None,
170
+ parent_refs: dict | None = None,
171
+ path: list | None = None,
172
+ ):
29
173
  """
30
174
  Convert the object to a JSON-like basic value tree.
31
175
  Such trees are trivial to serialize as JSON or YAML.
32
176
  """
33
177
  path = path or []
34
178
 
35
- if len(path) > 50:
179
+ if len(path) > _MAX_DEPTH:
36
180
  raise RuntimeError(f"I'm in too deep! Path is: {path}")
37
181
 
38
182
  if isinstance(obj, (list, tuple, set)):
39
- out = []
183
+ out_list: list[JSONed] = []
40
184
  for idx, item in enumerate(obj):
41
- if hasattr(item, "to_json_like"):
42
- item, shared_data = item.to_json_like(
185
+ if _is_base_json_like(item):
186
+ new_item, shared_data = item.to_json_like(
43
187
  shared_data=shared_data,
44
- exclude=list((parent_refs or {}).values()),
45
- path=path + [idx],
188
+ exclude=frozenset((parent_refs or {}).values()),
189
+ path=[*path, idx],
46
190
  )
191
+ out_list.append(new_item)
47
192
  else:
48
- item, shared_data = to_json_like(
49
- item, shared_data=shared_data, path=path + [idx]
193
+ new_std_item, shared_data = to_json_like(
194
+ item, shared_data=shared_data, path=[*path, idx]
50
195
  )
51
- out.append(item)
196
+ out_list.append(new_std_item)
52
197
  if isinstance(obj, tuple):
53
- out = tuple(out)
198
+ return tuple(out_list), shared_data
54
199
  elif isinstance(obj, set):
55
- out = set(out)
200
+ return set(out_list), shared_data
201
+ else:
202
+ return out_list, shared_data
56
203
 
57
204
  elif isinstance(obj, dict):
58
- out = {}
205
+ out_map: dict[str, JSONed] = {}
59
206
  for dct_key, dct_val in obj.items():
60
- if hasattr(dct_val, "to_json_like"):
207
+ if _is_base_json_like(dct_val):
61
208
  try:
62
- dct_val, shared_data = dct_val.to_json_like(
209
+ out_map[dct_key], shared_data = dct_val.to_json_like(
63
210
  shared_data=shared_data,
64
- exclude=[(parent_refs or {}).get(dct_key)],
65
- path=path + [dct_key],
211
+ exclude={(parent_refs or {}).get(dct_key)},
212
+ path=[*path, dct_key],
66
213
  )
67
214
  except ToJSONLikeChildReferenceError:
68
215
  continue
69
216
  else:
70
- dct_val, shared_data = to_json_like(
217
+ out_map[dct_key], shared_data = to_json_like(
71
218
  dct_val,
72
219
  shared_data=shared_data,
73
220
  parent_refs=parent_refs,
74
- path=path + [dct_key],
221
+ path=[*path, dct_key],
75
222
  )
76
- out.update({dct_key: dct_val})
223
+ return out_map, shared_data
77
224
 
78
225
  elif isinstance(obj, PRIMITIVES):
79
- out = obj
226
+ return obj, shared_data
80
227
 
81
228
  elif isinstance(obj, enum.Enum):
82
- out = obj.name
229
+ return obj.name, shared_data
83
230
 
84
231
  else:
85
- out, shared_data = obj.to_json_like(shared_data=shared_data, path=path)
86
-
87
- return out, shared_data
232
+ return obj.to_json_like(shared_data=shared_data, path=path)
88
233
 
89
234
 
90
235
  @dataclass
@@ -98,91 +243,89 @@ class ChildObjectSpec:
98
243
  name: str
99
244
  #: The name of the class (or class of members of a list) used to deserialize the
100
245
  #: attribute.
101
- class_name: Optional[str] = None
246
+ class_name: str | None = None
102
247
  #: The class (or class of members of a list) used to deserialize the
103
248
  #: attribute.
104
- class_obj: Optional[
105
- Type
106
- ] = None # TODO: no need for class_obj/class_name if shared data?
249
+ class_obj: type[enum.Enum | BaseJSONLike] | None = None
250
+ # TODO: no need for class_obj/class_name if shared data?
107
251
  #: The name of the key used in the JSON document, if different from the attribute
108
252
  #: name.
109
- json_like_name: Optional[str] = None
253
+ json_like_name: str | None = None
110
254
  #: If true, the attribute is really a list of instances,
111
255
  #: or a dictionary if :attr:`dict_key_attr` is set.
112
- is_multiple: Optional[bool] = False
256
+ is_multiple: bool = False
113
257
  #: If set, the name of an attribute of the object to use as a dictionary key.
114
258
  #: Requires that :attr:`is_multiple` be set as well.
115
- dict_key_attr: Optional[str] = None
259
+ dict_key_attr: str | None = None
116
260
  #: If set, the name of an attribute of the object to use as a dictionary value.
117
261
  #: If not set but :attr:`dict_key_attr` is set, the whole object is the value.
118
262
  #: Requires that :attr:`dict_key_attr` be set as well.
119
- dict_val_attr: Optional[str] = None
263
+ dict_val_attr: str | None = None
120
264
  #: If set, the attribute of the child object that contains a reference to its parent.
121
- parent_ref: Optional[
122
- str
123
- ] = None # TODO: do parent refs make sense when from shared? Prob not.
124
- #: If true, the object is not represented as a dict of attr name-values, but just a value.
125
- is_single_attribute: Optional[bool] = False
265
+ parent_ref: str | None = None
266
+ # TODO: do parent refs make sense when from shared? Prob not.
267
+ #: If true, enables special handling where there can be only one child descriptor
268
+ #: for a containing class.
269
+ is_single_attribute: bool = False
126
270
  #: If true, the object is an enum member and should use special serialization rules.
127
- is_enum: Optional[bool] = False
271
+ is_enum: bool = False
128
272
  #: If true, the child object is a dict, whose values are of the specified class.
129
273
  #: The dict structure will remain.
130
- is_dict_values: Optional[bool] = False
274
+ is_dict_values: bool = False
131
275
  #: If true, values that are not lists are cast to lists and multiple child objects
132
276
  #: are instantiated for each dict value.
133
- is_dict_values_ensure_list: Optional[bool] = False
277
+ is_dict_values_ensure_list: bool = False
134
278
  #: What key to look values up under in the shared data cache.
135
279
  #: If unspecified, the shared data cache is ignored.
136
- shared_data_name: Optional[str] = None
280
+ shared_data_name: str | None = None
137
281
  #: What attribute provides the value of the key into the shared data cache.
138
282
  #: If unspecified, a hash of the object dictionary is used.
139
283
  #: Ignored if :py:attr:`~.shared_data_name` is unspecified.
140
- shared_data_primary_key: Optional[str] = None
141
- # shared_data_secondary_keys: Optional[Tuple[str]] = None # TODO: what's the point?
284
+ shared_data_primary_key: str | None = None
285
+ # shared_data_secondary_keys: tuple[str, ...] | None = None # TODO: what's the point?
142
286
 
143
- def __post_init__(self):
144
- if self.class_name is not None and self.class_obj is not None:
145
- raise ValueError(f"Specify at most one of `class_name` and `class_obj`.")
287
+ def __post_init__(self) -> None:
288
+ if self.class_name and self.class_obj:
289
+ raise ValueError("Specify at most one of `class_name` and `class_obj`.")
146
290
 
147
- if self.dict_key_attr:
148
- if not isinstance(self.dict_key_attr, str):
149
- raise TypeError(
150
- f"`dict_key_attr` must be of type `str`, but has type "
151
- f"{type(self.dict_key_attr)} with value {self.dict_key_attr}."
152
- ) # TODO: test raise
291
+ if self.dict_key_attr and not isinstance(self.dict_key_attr, str):
292
+ raise TypeError(
293
+ "`dict_key_attr` must be of type `str`, but has type "
294
+ f"{type(self.dict_key_attr)} with value {self.dict_key_attr}."
295
+ ) # TODO: test raise
153
296
  if self.dict_val_attr:
154
297
  if not self.dict_key_attr:
155
298
  raise ValueError(
156
- f"If `dict_val_attr` is specified, `dict_key_attr` must be specified."
299
+ "If `dict_val_attr` is specified, `dict_key_attr` must be specified."
157
300
  ) # TODO: test raise
158
301
  if not isinstance(self.dict_val_attr, str):
159
302
  raise TypeError(
160
- f"`dict_val_attr` must be of type `str`, but has type "
303
+ "`dict_val_attr` must be of type `str`, but has type "
161
304
  f"{type(self.dict_val_attr)} with value {self.dict_val_attr}."
162
305
  ) # TODO: test raise
163
306
  if not self.is_multiple and self.dict_key_attr:
164
307
  raise ValueError(
165
- f"If `dict_key_attr` is specified, `is_multiple` must be set to True."
308
+ "If `dict_key_attr` is specified, `is_multiple` must be set to True."
166
309
  )
167
310
  if not self.is_multiple and self.is_dict_values:
168
311
  raise ValueError(
169
- f"If `is_dict_values` is specified, `is_multiple` must be set to True."
312
+ "If `is_dict_values` is specified, `is_multiple` must be set to True."
170
313
  )
171
314
  if self.is_dict_values_ensure_list and not self.is_dict_values:
172
315
  raise ValueError(
173
316
  "If `is_dict_values_ensure_list` is specified, `is_dict_values` must be "
174
317
  "set to True."
175
318
  )
176
- if self.parent_ref:
177
- if not isinstance(self.parent_ref, str):
178
- raise TypeError(
179
- f"`parent_ref` must be of type `str`, but has type "
180
- f"{type(self.parent_ref)} with value {self.parent_ref}."
181
- ) # TODO: test raise
319
+ if self.parent_ref and not isinstance(self.parent_ref, str):
320
+ raise TypeError(
321
+ "`parent_ref` must be of type `str`, but has type "
322
+ f"{type(self.parent_ref)} with value {self.parent_ref}."
323
+ ) # TODO: test raise
182
324
 
183
325
  self.json_like_name = self.json_like_name or self.name
184
326
 
185
327
 
328
+ @hydrate
186
329
  class BaseJSONLike:
187
330
  """
188
331
  An object that has a serialization as JSON or YAML.
@@ -197,41 +340,86 @@ class BaseJSONLike:
197
340
  in child objects.
198
341
  """
199
342
 
200
- _child_objects = None
201
- _validation_schema = None
343
+ _child_objects: ClassVar[Sequence[ChildObjectSpec]] = ()
344
+ _validation_schema: ClassVar[str | None] = None
345
+
346
+ __class_namespace: ClassVar[dict[str, Any] | SimpleNamespace | BaseApp | None] = None
347
+ _hash_value: str | None
202
348
 
203
- __class_namespace = None
204
- __class_namespace_is_dict = False
349
+ @overload
350
+ @classmethod
351
+ def _set_class_namespace(
352
+ cls, value: SimpleNamespace, is_dict: Literal[False] = False
353
+ ) -> None:
354
+ ...
355
+
356
+ @overload
357
+ @classmethod
358
+ def _set_class_namespace(cls, value: dict[str, Any], is_dict: Literal[True]) -> None:
359
+ ...
205
360
 
206
361
  @classmethod
207
- def _set_class_namespace(cls, value, is_dict=False):
362
+ def _set_class_namespace(
363
+ cls, value: dict[str, Any] | SimpleNamespace, is_dict=False
364
+ ) -> None:
208
365
  cls.__class_namespace = value
209
- cls.__class_namespace_is_dict = is_dict
210
366
 
211
- @classproperty
212
- def _class_namespace(cls):
213
- if not cls.__class_namespace:
367
+ @classmethod
368
+ def _class_namespace(cls) -> dict[str, Any] | SimpleNamespace | BaseApp:
369
+ if (ns := cls.__class_namespace) is None:
214
370
  raise ValueError(f"`{cls.__name__}` `class_namespace` must be set!")
215
- return cls.__class_namespace
371
+ return ns
216
372
 
217
373
  @classmethod
218
- def _get_child_class(cls, child_obj_spec):
219
- if child_obj_spec.class_obj:
220
- return child_obj_spec.class_obj
221
- elif child_obj_spec.class_name:
222
- if cls.__class_namespace_is_dict:
223
- return cls._class_namespace[child_obj_spec.class_name]
374
+ def __get_child_class(cls, child_spec: ChildObjectSpec) -> _ChildType | None:
375
+ if child_spec.class_obj:
376
+ return cast("_ChildType", child_spec.class_obj)
377
+ elif child_spec.class_name:
378
+ ns = cls._class_namespace()
379
+ if isinstance(ns, dict):
380
+ return ns[child_spec.class_name]
224
381
  else:
225
- return getattr(cls._class_namespace, child_obj_spec.class_name)
382
+ return getattr(ns, child_spec.class_name)
226
383
  else:
227
384
  return None
228
385
 
386
+ @classmethod
387
+ def _get_default_shared_data(cls) -> Mapping[str, ObjectList[JSONable]]:
388
+ return {}
389
+
390
+ @overload
391
+ @classmethod
392
+ def from_json_like(
393
+ cls,
394
+ json_like: str,
395
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
396
+ ) -> Self | None:
397
+ ...
398
+
399
+ @overload
400
+ @classmethod
401
+ def from_json_like(
402
+ cls,
403
+ json_like: Sequence[Mapping[str, JSONed]] | Mapping[str, JSONed],
404
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
405
+ ) -> Self:
406
+ ...
407
+
408
+ @overload
229
409
  @classmethod
230
410
  def from_json_like(
231
411
  cls,
232
- json_like: Union[Dict, List],
233
- shared_data: Optional[Dict[str, ObjectList]] = None,
234
- ):
412
+ json_like: None,
413
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
414
+ ) -> None:
415
+ ...
416
+
417
+ @classmethod
418
+ def from_json_like(
419
+ cls,
420
+ json_like: str | Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]] | None,
421
+ shared_data: Mapping[str, ObjectList[JSONable]] | None = None,
422
+ ) -> Self | None:
235
423
  """
236
424
  Make an instance of this class from JSON (or YAML) data.
237
425
 
@@ -246,128 +434,171 @@ class BaseJSONLike:
246
434
  -------
247
435
  The deserialised object.
248
436
  """
437
+ shared_data = shared_data or cls._get_default_shared_data()
438
+ if isinstance(json_like, str):
439
+ json_like = cls._parse_from_string(json_like)
440
+ if json_like is None:
441
+ # e.g. optional attributes # TODO: is this still needed?
442
+ return None
443
+ return cls._from_json_like(copy.deepcopy(json_like), shared_data)
249
444
 
250
- def _from_json_like_item(child_obj_spec, json_like_i):
251
- if not (
252
- child_obj_spec.class_name
253
- or child_obj_spec.class_obj
254
- or child_obj_spec.is_multiple
255
- or child_obj_spec.shared_data_name
256
- ):
257
- # Nothing to process:
258
- return json_like_i
445
+ @classmethod
446
+ def _parse_from_string(cls, string: str) -> dict[str, str] | None:
447
+ raise TypeError(f"unparseable {cls}: '{string}'")
448
+
449
+ @staticmethod
450
+ def __listify(v: JSONed, spec: ChildObjectSpec) -> list[JSONed]:
451
+ if spec.is_dict_values_ensure_list and isinstance(v, list):
452
+ return v
453
+ return [v]
259
454
 
260
- multi_chd_objs = []
455
+ @classmethod
456
+ def __remap_child_seq(
457
+ cls, spec: ChildObjectSpec, json_like: JSONed
458
+ ) -> tuple[list[JSONed], dict[str, list[int]]]:
459
+ if not spec.is_multiple:
460
+ return [json_like], {}
461
+ elif isinstance(json_like, list):
462
+ return json_like, {}
463
+ elif not isinstance(json_like, dict):
464
+ raise TypeError(
465
+ f"Child object {spec.name} of {cls.__name__!r} must be a list or "
466
+ f"dict, but is of type {type(json_like)} with value {json_like!r}."
467
+ )
261
468
 
469
+ multi_chd_objs: list[JSONed] = []
470
+
471
+ if spec.is_dict_values:
262
472
  # (if is_dict_values) indices into multi_chd_objs that enable reconstruction
263
473
  # of the source dict:
264
- is_dict_values_idx = {}
265
-
266
- if child_obj_spec.is_multiple:
267
- if type(json_like_i) == dict:
268
- if child_obj_spec.is_dict_values:
269
- # keep as a dict
270
- for k, v in json_like_i.items():
271
- if child_obj_spec.is_dict_values_ensure_list:
272
- if not isinstance(v, list):
273
- v = [v]
274
- else:
275
- v = [v]
276
-
277
- for i in v:
278
- new_multi_idx = len(multi_chd_objs)
279
- if k not in is_dict_values_idx:
280
- is_dict_values_idx[k] = []
281
- is_dict_values_idx[k].append(new_multi_idx)
282
- multi_chd_objs.append(i)
474
+ is_dict_values_idx: dict[str, list[int]] = defaultdict(list)
283
475
 
284
- else:
285
- # want to cast to a list
286
- if not child_obj_spec.dict_key_attr:
287
- raise ValueError(
288
- f"{cls.__name__!r}: must specify a `dict_key_attr` for child "
289
- f"object spec {child_obj_spec.name!r}."
290
- )
476
+ # keep as a dict
477
+ for k, v in json_like.items():
478
+ for item in cls.__listify(v, spec):
479
+ is_dict_values_idx[k].append(len(multi_chd_objs))
480
+ multi_chd_objs.append(item)
481
+ return multi_chd_objs, is_dict_values_idx
291
482
 
292
- for k, v in json_like_i.items():
293
- all_attrs = {child_obj_spec.dict_key_attr: k}
294
- if child_obj_spec.dict_val_attr:
295
- all_attrs[child_obj_spec.dict_val_attr] = v
296
- else:
297
- if not isinstance(v, dict):
298
- raise TypeError(
299
- f"Value for key {k!r} must be a dict representing "
300
- f"attributes of the {child_obj_spec.name!r} child "
301
- f"object (parent: {cls.__name__!r}). If it instead "
302
- f"represents a single attribute, set the "
303
- f"`dict_val_attr` of the child object spec."
304
- )
305
- all_attrs.update(v)
306
- multi_chd_objs.append(all_attrs)
307
-
308
- elif type(json_like_i) == list:
309
- multi_chd_objs = json_like_i
483
+ # want to cast to a list
484
+ if not spec.dict_key_attr:
485
+ raise ValueError(
486
+ f"{cls.__name__!r}: must specify a `dict_key_attr` for child "
487
+ f"object spec {spec.name!r}."
488
+ )
310
489
 
311
- else:
312
- raise TypeError(
313
- f"Child object {child_obj_spec.name} of {cls.__name__!r} must be "
314
- f"a list or dict, but is of type {type(json_like_i)} with value "
315
- f"{json_like_i!r}."
316
- )
490
+ for k, v in json_like.items():
491
+ all_attrs: dict[str, JSONed] = {spec.dict_key_attr: k}
492
+ if spec.dict_val_attr:
493
+ all_attrs[spec.dict_val_attr] = v
494
+ elif isinstance(v, dict):
495
+ all_attrs.update(v)
496
+ else:
497
+ raise TypeError(
498
+ f"Value for key {k!r} must be a dict representing "
499
+ f"attributes of the {spec.name!r} child object "
500
+ f"(parent: {cls.__name__!r}). If it instead "
501
+ f"represents a single attribute, set the "
502
+ f"`dict_val_attr` of the child object spec."
503
+ )
504
+ multi_chd_objs.append(all_attrs)
505
+
506
+ return multi_chd_objs, {}
507
+
508
+ @classmethod
509
+ def __inflate_enum(cls, chd_cls: type[enum.Enum], multi_chd_objs: list[JSONed]):
510
+ out: list[JSONable] = []
511
+ for item in multi_chd_objs:
512
+ if item is None:
513
+ out.append(None)
514
+ elif not isinstance(item, str):
515
+ raise ValueError(
516
+ f"Enumeration {chd_cls!r} has no name {item!r}. Available"
517
+ f" names are: {chd_cls._member_names_!r}."
518
+ )
317
519
  else:
318
- multi_chd_objs = [json_like_i]
520
+ try:
521
+ out.append(getattr(chd_cls, item.upper()))
522
+ except AttributeError:
523
+ raise ValueError(
524
+ f"Enumeration {chd_cls!r} has no name {item!r}. Available"
525
+ f" names are: {chd_cls._member_names_!r}."
526
+ )
527
+ return out
528
+
529
+ @classmethod
530
+ def _from_json_like(
531
+ cls,
532
+ json_like: Mapping[str, JSONed] | Sequence[Mapping[str, JSONed]],
533
+ shared_data: Mapping[str, ObjectList[JSONable]],
534
+ ) -> Self:
535
+ def from_json_like_item(
536
+ child_spec: ChildObjectSpec, json_like_i: JSONed
537
+ ) -> JSONable:
538
+ if not (
539
+ child_spec.class_name
540
+ or child_spec.class_obj
541
+ or child_spec.is_multiple
542
+ or child_spec.shared_data_name
543
+ ):
544
+ # Nothing to process:
545
+ return cast("JSONable", json_like_i)
546
+
547
+ # (if is_dict_values) indices into multi_chd_objs that enable reconstruction
548
+ # of the source dict:
549
+ multi_chd_objs, is_dict_values_idx = cls.__remap_child_seq(
550
+ child_spec, json_like_i
551
+ )
319
552
 
320
- out = []
321
- if chd.shared_data_name:
553
+ out: list[JSONable] = []
554
+ if child_spec.shared_data_name:
322
555
  for i in multi_chd_objs:
323
556
  if i is None:
324
557
  out.append(i)
325
558
  continue
326
559
 
560
+ sd_lookup_kwargs: dict[str, JSONable]
327
561
  if isinstance(i, str):
328
562
  if i.startswith("hash:"):
329
- sd_lookup_kwargs = {"_hash_value": i.split("hash:")[1]}
563
+ sd_lookup_kwargs = {"_hash_value": i.removeprefix("hash:")}
330
564
  else:
331
- sd_lookup_kwargs = {chd.shared_data_primary_key: i}
565
+ assert child_spec.shared_data_primary_key
566
+ sd_lookup_kwargs = {child_spec.shared_data_primary_key: i}
332
567
  elif isinstance(i, dict):
333
568
  sd_lookup_kwargs = i
334
569
  else:
335
570
  raise TypeError(
336
571
  "Shared data reference must be a str or a dict."
337
572
  ) # TODO: test raise
338
- chd_obj = shared_data[chd.shared_data_name].get(**sd_lookup_kwargs)
339
- out.append(chd_obj)
573
+ out.append(
574
+ shared_data[child_spec.shared_data_name].get(**sd_lookup_kwargs)
575
+ )
340
576
  else:
341
- chd_cls = cls._get_child_class(child_obj_spec)
342
- if child_obj_spec.is_enum:
343
- out = []
344
- for i in multi_chd_objs:
345
- if i is not None:
346
- try:
347
- i = getattr(chd_cls, i.upper())
348
- except AttributeError:
349
- raise ValueError(
350
- f"Enumeration {chd_cls!r} has no name {i!r}. Available"
351
- f" names are: {chd_cls._member_names_!r}."
352
- )
353
- out.append(i)
577
+ chd_cls = cls.__get_child_class(child_spec)
578
+ assert chd_cls is not None
579
+ if issubclass(chd_cls, enum.Enum):
580
+ out = cls.__inflate_enum(chd_cls, multi_chd_objs)
354
581
  else:
355
- out = []
356
- for i in multi_chd_objs:
357
- if i is not None:
358
- i = chd_cls.from_json_like(i, shared_data)
359
- out.append(i)
582
+ out.extend(
583
+ (
584
+ None
585
+ if item is None
586
+ else chd_cls.from_json_like(
587
+ cast("Any", item), # FIXME: This is "Trust me, bro!" hack
588
+ shared_data,
589
+ )
590
+ )
591
+ for item in multi_chd_objs
592
+ )
360
593
 
361
- if child_obj_spec.is_dict_values:
362
- out_dict = {}
363
- for k, v in is_dict_values_idx.items():
364
- out_dict[k] = [out[i] for i in v]
365
- if not child_obj_spec.is_dict_values_ensure_list:
366
- out_dict[k] = out_dict[k][0]
367
- out = out_dict
594
+ if child_spec.is_dict_values:
595
+ if child_spec.is_dict_values_ensure_list:
596
+ return {k: [out[i] for i in v] for k, v in is_dict_values_idx.items()}
597
+ else:
598
+ return {k: out[v[0]] for k, v in is_dict_values_idx.items()}
368
599
 
369
- elif not child_obj_spec.is_multiple:
370
- out = out[0]
600
+ elif not child_spec.is_multiple:
601
+ return out[0]
371
602
 
372
603
  return out
373
604
 
@@ -377,177 +608,214 @@ class BaseJSONLike:
377
608
  if not validated.is_valid:
378
609
  raise ValueError(validated.get_failures_string())
379
610
 
380
- if json_like is None:
381
- # e.g. optional attributes # TODO: is this still needed?
382
- return None
611
+ json_like_copy = copy.deepcopy(json_like)
383
612
 
384
- shared_data = shared_data or {}
385
- json_like = copy.deepcopy(json_like)
386
-
387
- for chd in cls._child_objects or []:
388
- if chd.is_single_attribute:
613
+ for child_spec in cls._child_objects:
614
+ if child_spec.is_single_attribute:
389
615
  if len(cls._child_objects) > 1:
390
616
  raise TypeError(
391
617
  f"If ChildObjectSpec has `is_single_attribute=True`, only one "
392
618
  f"ChildObjectSpec may be specified on the class. Specified child "
393
619
  f"objects specs are: {cls._child_objects!r}."
394
620
  )
395
- json_like = {chd.name: json_like}
621
+ json_like_copy = {child_spec.name: json_like_copy}
622
+
623
+ assert isinstance(json_like_copy, Mapping)
624
+ if child_spec.json_like_name and child_spec.json_like_name in json_like_copy:
625
+ json_like_copy = dict(json_like_copy)
626
+ json_like_copy[child_spec.name] = cast(
627
+ "JSONed",
628
+ from_json_like_item(
629
+ child_spec, json_like_copy.pop(child_spec.json_like_name)
630
+ ),
631
+ )
396
632
 
397
- if chd.json_like_name in json_like:
398
- json_like_i = json_like.pop(chd.json_like_name)
399
- json_like[chd.name] = _from_json_like_item(chd, json_like_i)
633
+ assert isinstance(json_like_copy, Mapping)
400
634
 
401
- need_hash = False
402
- if hasattr(cls, "_hash_value"):
403
- if "_hash_value" not in json_like:
404
- need_hash = True
635
+ need_hash = hasattr(cls, "_hash_value") and "_hash_value" not in json_like_copy
405
636
 
406
637
  try:
407
- if hasattr(cls, "_json_like_constructor"):
408
- obj = cls._json_like_constructor(json_like)
638
+ if issubclass(cls, _AltConstructFromJson):
639
+ obj = cls._json_like_constructor(json_like_copy)
409
640
  else:
410
- obj = cls(**json_like)
641
+ obj = cls(**json_like_copy)
411
642
  except TypeError as err:
412
643
  raise TypeError(
413
644
  f"Failed initialisation of class {cls.__name__!r}. Check the signature. "
414
645
  f"Caught TypeError: {err}"
415
- )
646
+ ) from err
416
647
 
417
648
  if need_hash:
418
649
  obj._set_hash()
419
-
420
650
  return obj
421
651
 
422
- def _set_parent_refs(self, child_name_attrs=None):
652
+ def __set_parent_ref(self, chd_obj: Any, child_spec: ChildObjectSpec):
653
+ if chd_obj is not None:
654
+ assert child_spec.parent_ref
655
+ setattr(chd_obj, child_spec.parent_ref, self)
656
+
657
+ def _set_parent_refs(self, child_name_attrs: Mapping[str, str] | None = None):
423
658
  """Assign references to self on child objects that declare a parent ref
424
659
  attribute."""
425
-
426
- for chd in self._child_objects:
427
- if chd.parent_ref:
428
- chd_name = (child_name_attrs or {}).get(chd.name, chd.name)
429
- if chd.is_multiple:
660
+ child_name_attrs = child_name_attrs or {}
661
+ for child_spec in self._child_objects:
662
+ if child_spec.parent_ref:
663
+ chd_name = child_name_attrs.get(child_spec.name, child_spec.name)
664
+ if child_spec.is_multiple:
430
665
  for chd_obj in getattr(self, chd_name):
431
- if chd_obj:
432
- setattr(chd_obj, chd.parent_ref, self)
666
+ self.__set_parent_ref(chd_obj, child_spec)
433
667
  else:
434
- chd_obj = getattr(self, chd_name)
435
- if chd_obj:
436
- setattr(chd_obj, chd.parent_ref, self)
668
+ self.__set_parent_ref(getattr(self, chd_name), child_spec)
437
669
 
438
- def _get_hash(self):
670
+ def _get_hash(self) -> str:
439
671
  json_like = self.to_json_like()[0]
440
672
  hash_val = self._get_hash_from_json_like(json_like)
441
673
  return hash_val
442
674
 
443
- def _set_hash(self):
675
+ def _set_hash(self) -> None:
444
676
  self._hash_value = self._get_hash()
445
677
 
446
678
  @staticmethod
447
- def _get_hash_from_json_like(json_like):
679
+ def _get_hash_from_json_like(json_like) -> str:
448
680
  json_like = copy.deepcopy(json_like)
449
681
  json_like.pop("_hash_value", None)
450
682
  return get_md5_hash(json_like)
451
683
 
452
- def to_dict(self):
684
+ @final
685
+ def to_dict(self) -> dict[str, Any]:
453
686
  """
454
687
  Serialize this object as a dictionary.
455
688
  """
456
689
  if hasattr(self, "__dict__"):
457
- return dict(self.__dict__)
690
+ return self._postprocess_to_dict(dict(self.__dict__))
458
691
  elif hasattr(self, "__slots__"):
459
- return {k: getattr(self, k) for k in self.__slots__}
692
+ return self._postprocess_to_dict(
693
+ {var_name: getattr(self, var_name) for var_name in self.__slots__}
694
+ )
695
+ else:
696
+ return self._postprocess_to_dict({})
460
697
 
461
- def to_json_like(self, dct=None, shared_data=None, exclude=None, path=None):
698
+ def _postprocess_to_dict(self, dct: dict[str, Any]) -> dict[str, Any]:
699
+ """
700
+ Apply any desired postprocessing to the results of :meth:`to_dict`.
701
+ """
702
+ return dct
703
+
704
+ def to_json_like(
705
+ self,
706
+ dct: dict[str, JSONable] | None = None,
707
+ shared_data: _JSONDeserState = None,
708
+ exclude: Container[str | None] = (),
709
+ path: list | None = None,
710
+ ) -> tuple[JSONDocument, _JSONDeserState]:
462
711
  """
463
712
  Serialize this object as an object structure that can be trivially converted
464
713
  to JSON. Note that YAML can also be produced from the result of this method;
465
714
  it just requires a different final serialization step.
466
715
  """
467
716
  if dct is None:
468
- dct = {k: v for k, v in self.to_dict().items() if k not in (exclude or [])}
717
+ dct_value = {k: v for k, v in self.to_dict().items() if k not in exclude}
718
+ else:
719
+ dct_value = dct
469
720
 
470
- parent_refs = {}
471
- for chd in self._child_objects or []:
472
- if chd.is_single_attribute:
473
- if len(self._child_objects) > 1:
474
- raise TypeError(
475
- f"If ChildObjectSpec has `is_single_attribute=True`, only one "
476
- f"ChildObjectSpec may be specified on the class."
477
- )
478
- dct = dct[chd.json_like_name]
721
+ parent_refs: dict[str, str] = {}
722
+ if self._child_objects:
723
+ for child_spec in self._child_objects:
724
+ if child_spec.is_single_attribute:
725
+ if len(self._child_objects) > 1:
726
+ raise TypeError(
727
+ "If ChildObjectSpec has `is_single_attribute=True`, only one "
728
+ "ChildObjectSpec may be specified on the class."
729
+ )
730
+ assert child_spec.json_like_name is not None
731
+ dct_value = dct_value[child_spec.json_like_name]
479
732
 
480
- if chd.parent_ref:
481
- parent_refs.update({chd.name: chd.parent_ref})
733
+ if child_spec.parent_ref:
734
+ parent_refs[child_spec.name] = child_spec.parent_ref
482
735
 
483
- json_like, shared_data = to_json_like(
484
- dct, shared_data=shared_data, parent_refs=parent_refs, path=path
736
+ json_like_, shared_data = to_json_like(
737
+ dct_value, shared_data=shared_data, parent_refs=parent_refs, path=path
485
738
  )
739
+ json_like: dict[str, JSONed] | list[JSONed] = cast("Any", json_like_)
486
740
  shared_data = shared_data or {}
487
741
 
488
- for chd in self._child_objects or []:
489
- if chd.name in json_like:
490
- json_like[chd.json_like_name] = json_like.pop(chd.name)
742
+ for child_spec in self._child_objects:
743
+ assert child_spec.json_like_name is not None
744
+ if child_spec.name in json_like:
745
+ assert isinstance(json_like, dict)
746
+ json_like[child_spec.json_like_name] = json_like.pop(child_spec.name)
491
747
 
492
- if chd.shared_data_name:
493
- if chd.shared_data_name not in shared_data:
494
- shared_data[chd.shared_data_name] = {}
748
+ if child_spec.shared_data_name:
749
+ assert isinstance(json_like, dict)
750
+ if child_spec.shared_data_name not in shared_data:
751
+ shared_data[child_spec.shared_data_name] = {}
495
752
 
496
- chd_obj_js = json_like.pop(chd.json_like_name)
753
+ chd_obj_js = json_like.pop(child_spec.json_like_name)
497
754
 
498
- if not chd.is_multiple:
755
+ if not child_spec.is_multiple:
499
756
  chd_obj_js = [chd_obj_js]
500
757
 
501
- shared_keys = []
758
+ shared_keys: list[JSONed] = []
759
+ assert isinstance(chd_obj_js, (list, tuple, set))
502
760
  for i in chd_obj_js:
503
761
  if i is None:
504
762
  continue
505
763
  i.pop("_hash_value", None)
506
764
  hash_i = self._get_hash_from_json_like(i)
507
765
  shared_keys.append(f"hash:{hash_i}")
766
+ shared_data[child_spec.shared_data_name].setdefault(hash_i, i)
508
767
 
509
- if hash_i not in shared_data[chd.shared_data_name]:
510
- shared_data[chd.shared_data_name].update({hash_i: i})
511
-
512
- if not chd.is_multiple:
768
+ if not child_spec.is_multiple:
513
769
  try:
514
- shared_keys = shared_keys[0]
770
+ json_like[child_spec.json_like_name] = shared_keys[0]
515
771
  except IndexError:
516
- shared_keys = None
772
+ json_like[child_spec.json_like_name] = None
773
+ else:
774
+ json_like[child_spec.json_like_name] = shared_keys
517
775
 
518
- json_like[chd.json_like_name] = shared_keys
776
+ return self._postprocess_to_json(json_like), shared_data
519
777
 
520
- return json_like, shared_data
778
+ def _postprocess_to_json(self, json_like: JSONDocument) -> JSONDocument:
779
+ return json_like
521
780
 
522
781
 
523
- class JSONLike(BaseJSONLike):
782
+ @hydrate
783
+ class JSONLike(BaseJSONLike, AppAware):
524
784
  """BaseJSONLike, where the class namespace is the App instance."""
525
785
 
526
- _app_attr = "app" # for some classes we change this to "_app"
786
+ __sdk_classes: ClassVar[list[type[BaseJSONLike]]] = []
527
787
 
528
- @classproperty
529
- def _class_namespace(cls):
788
+ @classmethod
789
+ def _class_namespace(cls) -> BaseApp:
530
790
  return getattr(cls, cls._app_attr)
531
791
 
532
- def to_dict(self):
792
+ @classmethod
793
+ def __get_classes(cls) -> list[type[BaseJSONLike]]:
533
794
  """
534
- Serialize this object as a dictionary.
795
+ Get the collection of actual SDK classes that conform to BaseJSONLike.
535
796
  """
536
- out = super().to_dict()
797
+ if not cls.__sdk_classes:
798
+ for cls_name in app.sdk_classes:
799
+ cls2 = getattr(app, cls_name)
800
+ if isinstance(cls2, type) and issubclass(cls2, BaseJSONLike):
801
+ cls.__sdk_classes.append(cls2)
802
+ return cls.__sdk_classes
803
+
804
+ @override
805
+ def _postprocess_to_dict(self, d: dict[str, Any]) -> dict[str, Any]:
806
+ out = super()._postprocess_to_dict(d)
537
807
 
538
808
  # remove parent references:
539
- for cls_name in app.sdk_classes:
540
- cls = getattr(app, cls_name)
541
- if hasattr(cls, "_child_objects"):
542
- for chd in cls._child_objects or []:
543
- if chd.parent_ref:
544
- # _SDK_logger.debug(
545
- # f"removing parent reference {chd.parent_ref!r} from child "
546
- # f"object {chd!r}."
547
- # )
548
- if (
549
- self.__class__.__name__ == chd.class_name
550
- or self.__class__ is chd.class_obj
551
- ):
552
- out.pop(chd.parent_ref, None)
809
+ for cls in self.__get_classes():
810
+ for child_spec in cls._child_objects:
811
+ if child_spec.parent_ref:
812
+ # _SDK_logger.debug(
813
+ # f"removing parent reference {chd.parent_ref!r} from child "
814
+ # f"object {chd!r}."
815
+ # )
816
+ if (
817
+ self.__class__.__name__ == child_spec.class_name
818
+ or self.__class__ is child_spec.class_obj
819
+ ):
820
+ out.pop(child_spec.parent_ref, None)
553
821
  return out