bigraph-schema 0.0.71__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.

Potentially problematic release.


This version of bigraph-schema might be problematic. Click here for more details.

@@ -0,0 +1,1798 @@
1
+ """
2
+ ===========
3
+ Type System
4
+ ===========
5
+ """
6
+
7
+ import copy
8
+ import functools
9
+ import inspect
10
+ import random
11
+ import traceback
12
+ from pprint import pformat as pf
13
+
14
+ from bigraph_schema import (
15
+ deep_merge, is_schema_key, non_schema_keys, Registry,
16
+ type_parameter_key)
17
+ from bigraph_schema.parse import parse_expression
18
+ from bigraph_schema.utilities import union_keys, state_instance
19
+ from bigraph_schema.registry import (
20
+ remove_omitted, set_star_path, transform_path)
21
+
22
+ from bigraph_schema.type_functions import (
23
+ base_types, is_empty, is_method_key, register_base_reactions,
24
+ registry_types, resolve_path, set_apply, SYMBOL_TYPES, TYPE_FUNCTION_KEYS,
25
+ type_schema_keys, unit_types)
26
+ from bigraph_schema.type_system_adjunct import TypeSystemAdjunct
27
+
28
+
29
+ class TypeSystem(Registry):
30
+ """Handles type schemas and their operation"""
31
+
32
+ def __init__(self):
33
+ super().__init__()
34
+
35
+ self.inherits = {}
36
+
37
+ self.default_registry = Registry(function_keys=[
38
+ 'schema',
39
+ 'core'])
40
+
41
+ self.check_registry = Registry(function_keys=[
42
+ 'state',
43
+ 'schema',
44
+ 'core'])
45
+
46
+ self.apply_registry = Registry(function_keys=[
47
+ 'current',
48
+ 'update',
49
+ 'schema',
50
+ 'core'])
51
+
52
+ self.serialize_registry = Registry(function_keys=[
53
+ 'value',
54
+ 'schema',
55
+ 'core'])
56
+
57
+ self.deserialize_registry = Registry(function_keys=[
58
+ 'encoded',
59
+ 'schema',
60
+ 'core'])
61
+
62
+ self.fold_registry = Registry(function_keys=[
63
+ 'method',
64
+ 'state',
65
+ 'schema',
66
+ 'core'])
67
+
68
+ self.react_registry = Registry()
69
+ self.method_registry = Registry()
70
+
71
+ # register all the base methods and types
72
+ self.apply_registry.register(
73
+ 'set',
74
+ set_apply)
75
+
76
+ self._register_types(registry_types)
77
+ self._register_types(base_types)
78
+ self._register_types(unit_types)
79
+
80
+ # # TODO -- add a proper registration into registry
81
+ register_base_reactions(self)
82
+
83
+
84
+ def _register_types(self, type_library):
85
+ """
86
+ private method to implement initialization
87
+
88
+ basically the same as update_types, except it never updates
89
+ """
90
+ for type_key, type_data in type_library.items():
91
+ if not self.exists(type_key):
92
+ self.register(
93
+ type_key,
94
+ type_data)
95
+
96
+ return self
97
+
98
+
99
+ # is it useful just because it's simpler?
100
+ def update_types(self, type_updates):
101
+ """
102
+ initialize or update multiple types
103
+ """
104
+ for type_key, type_data in type_updates.items():
105
+ is_update = self.exists(type_key)
106
+
107
+ self.register(
108
+ type_key,
109
+ type_data,
110
+ update=is_update)
111
+
112
+ return self
113
+
114
+
115
+ def _lookup_registry(self, underscore_key):
116
+ """
117
+ private helper method
118
+
119
+ access the registry for the given key
120
+
121
+ >>> t = TypeSystem()
122
+ >>> t.foo_registry = "OK"
123
+ >>> t._lookup_registry('_foo')
124
+ 'OK'
125
+
126
+ """
127
+
128
+ if underscore_key == '_type':
129
+ return self
130
+ root = underscore_key.strip('_')
131
+ registry_key = f'{root}_registry'
132
+ if hasattr(self, registry_key):
133
+ return getattr(self, registry_key)
134
+
135
+
136
+ def _find_registry(self, underscore_key):
137
+ """
138
+ access the registry for the given key
139
+ and create if it doesn't exist
140
+
141
+ >>> t = TypeSystem()
142
+ >>> hasattr(t, 'foo_registry')
143
+ False
144
+ >>> t._find_registry('_foo').__class__ == Registry
145
+ True
146
+ >>> hasattr(t, 'foo_registry')
147
+ True
148
+ """
149
+
150
+ registry = self._lookup_registry(underscore_key)
151
+ if registry is None:
152
+ registry = Registry()
153
+ # setattr is needed here because the key is dynamically
154
+ # constructed
155
+ setattr(
156
+ self,
157
+ f'{underscore_key[1:]}_registry',
158
+ registry)
159
+
160
+ return registry
161
+
162
+
163
+ # TODO: explain this method
164
+ def register(self, key, schema, alternate_keys=tuple(), strict=True, update=False):
165
+ """
166
+ register the schema under the given key in the registry
167
+
168
+ if schema is a string, we expand and duplicate it under a new name
169
+ >>> t = TypeSystem()
170
+ >>> t.register('regetni', 'integer')
171
+ >>> t.find('regetni') == t.find('integer')
172
+ True
173
+ >>> id(t.find('regetni')) != id(t.find('integer'))
174
+ True
175
+
176
+ if '_type' is not provided, it will be filled in
177
+ >>> t = TypeSystem()
178
+ >>> i = copy.deepcopy(t.find('integer'))
179
+ >>> del(i['_type'])
180
+ >>> t.register('regetni', i)
181
+ >>> t.find('regetni')['_type'] == 'regetni'
182
+ True
183
+
184
+ inherated schemas are merged
185
+ >>> t = TypeSystem()
186
+ >>> t.register('foo', {'bar':{'baz':42}})
187
+ >>> t.register('bar', {'haz':{'quux':12}})
188
+ >>> t.register('quux', {'_inherit':['foo', 'bar']})
189
+ >>> t.find('quux')['bar']['baz'] == 42
190
+ True
191
+ >>> t.find('quux')['haz']['quux'] == 12
192
+ True
193
+
194
+ type parameters define subtypes specific to the schema, each subtype
195
+ is expanded
196
+ >>> t = TypeSystem()
197
+ >>> s = {'_0':'any', '_1':'any'}
198
+ >>> s['_type_parameters'] = ['0','1']
199
+ >>> s['_type'] = 'tuple'
200
+ >>> t.register('foo', s)
201
+ >>> t.find('foo')['_0']['_serialize'] == 'serialize_any'
202
+ True
203
+ """
204
+
205
+ if isinstance(schema, str):
206
+ schema = self.find(schema)
207
+ schema = copy.deepcopy(schema)
208
+ if self.exists(key) and update:
209
+ found = self.find(key)
210
+ schema = deep_merge(
211
+ found,
212
+ schema)
213
+ strict = False
214
+
215
+ if not(isinstance(schema, dict)):
216
+ raise Exception(
217
+ f'all type definitions must be dicts '
218
+ f'with the following keys: {type_schema_keys}\nnot: {schema}')
219
+ else:
220
+ if '_type' not in schema:
221
+ schema['_type'] = key
222
+
223
+ inherits = schema.get('_inherit', []) # list of immediate inherits
224
+ if isinstance(inherits, str):
225
+ inherits = [inherits]
226
+ schema['_inherit'] = inherits
227
+
228
+ self.inherits[key] = []
229
+ for inherit in inherits:
230
+ inherit_type = self.access(inherit)
231
+ new_schema = copy.deepcopy(inherit_type)
232
+
233
+ schema = self.merge_schemas(
234
+ new_schema,
235
+ schema)
236
+
237
+ self.inherits[key].append(
238
+ inherit_type)
239
+
240
+ parameters = schema.get('_type_parameters', [])
241
+ for subkey, subschema in schema.items():
242
+ if subkey == '_default' \
243
+ or subkey in TYPE_FUNCTION_KEYS \
244
+ or is_method_key(subkey, parameters):
245
+ if not(callable(subschema)):
246
+ schema[subkey] = subschema
247
+ else:
248
+ registry = self._find_registry(subkey)
249
+ function_name, module_key = registry.register_function(subschema)
250
+
251
+ schema[subkey] = function_name
252
+
253
+ elif subkey not in type_schema_keys:
254
+ if schema['_type'] in SYMBOL_TYPES:
255
+ schema[subkey] = subschema
256
+ else:
257
+ lookup = self.find(subschema)
258
+ if lookup is None:
259
+ raise Exception(
260
+ f'trying to register a new type ({key}), '
261
+ f'but it depends on a type ({subkey}) which is not in the registry')
262
+ else:
263
+ schema[subkey] = lookup
264
+
265
+ super().register(
266
+ key,
267
+ schema,
268
+ alternate_keys,
269
+ strict=strict)
270
+
271
+
272
+ def register_reaction(self, reaction_key, reaction):
273
+ self.react_registry.register(
274
+ reaction_key,
275
+ reaction)
276
+
277
+
278
+ def merge_schemas(self, current, update):
279
+ """
280
+ deep merge of two dicts (recursive merge of sub-dicts)
281
+ """
282
+
283
+ result = current.copy()
284
+
285
+ for key, value in update.items():
286
+ if key in result \
287
+ and isinstance(result[key], dict) \
288
+ and isinstance(value, dict):
289
+ result[key] = self.merge_schemas(result[key], value)
290
+ else:
291
+ result[key] = value
292
+
293
+ return result
294
+
295
+
296
+ def _sort(self, schema, state):
297
+ """
298
+ separates state and schema if they are mixed
299
+
300
+ helper used by type_functions
301
+ """
302
+ schema = self.access(schema)
303
+
304
+ sort_function = self.choose_method(
305
+ schema,
306
+ state,
307
+ 'sort')
308
+
309
+ return sort_function(
310
+ self,
311
+ schema,
312
+ state)
313
+
314
+
315
+ def exists(self, type_key):
316
+ return type_key in self.registry
317
+
318
+
319
+ def find(self, schema, strict=False):
320
+ """
321
+ expand the schema to its full type information from the type registry
322
+
323
+ strings are looked up in the registry if present as a key
324
+ >>> core = TypeSystem()
325
+ >>> for k,v in core.registry.items():
326
+ ... assert isinstance(k, str)
327
+ ... assert v == core.find(k)
328
+
329
+ """
330
+
331
+ found = None
332
+
333
+ if schema is None:
334
+ return self.access('any', strict=strict)
335
+
336
+ elif isinstance(schema, int):
337
+ return schema
338
+
339
+ elif isinstance(schema, tuple):
340
+ tuple_schema = {
341
+ '_type': 'tuple',
342
+ '_type_parameters': []}
343
+
344
+ for index, element in enumerate(schema):
345
+ tuple_schema['_type_parameters'].append(str(index))
346
+ tuple_schema[f'_{index}'] = element
347
+
348
+ return self.access(
349
+ tuple_schema,
350
+ strict=strict)
351
+
352
+ elif isinstance(schema, str):
353
+ found = self.registry.get(schema)
354
+
355
+ if found is None and schema not in ('', '{}'):
356
+ try:
357
+ parse = parse_expression(schema)
358
+ if parse != schema:
359
+ found = self.access(
360
+ parse,
361
+ strict=strict)
362
+ elif not strict:
363
+ found = {'_type': schema}
364
+
365
+ except Exception:
366
+ print(f'type did not parse: {schema}')
367
+ traceback.print_exc()
368
+
369
+ elif isinstance(schema, dict):
370
+ if '_description' in schema:
371
+ return schema
372
+
373
+ elif '_union' in schema:
374
+ union_schema = {
375
+ '_type': 'union',
376
+ '_type_parameters': []}
377
+
378
+ for index, element in enumerate(schema['_union']):
379
+ union_schema['_type_parameters'].append(str(index))
380
+ union_schema[f'_{index}'] = element
381
+
382
+ return self.access(
383
+ union_schema,
384
+ strict=strict)
385
+
386
+ elif '_type' in schema:
387
+ registry_type = self.retrieve(
388
+ schema['_type'])
389
+
390
+ found = self.merge_schemas(
391
+ registry_type,
392
+ schema)
393
+
394
+ else:
395
+ found = {
396
+ key: self.access(
397
+ branch,
398
+ strict=strict) if key != '_default' else branch
399
+ for key, branch in schema.items()}
400
+
401
+ elif isinstance(schema, list):
402
+ if isinstance(schema[0], int):
403
+ return schema
404
+
405
+ bindings = []
406
+ if len(schema) > 1:
407
+ schema, bindings = schema
408
+ else:
409
+ schema = schema[0]
410
+ found = self.access(
411
+ schema,
412
+ strict=strict)
413
+
414
+ if len(bindings) > 0:
415
+ found = found.copy()
416
+
417
+ if '_type_parameters' not in found:
418
+ found['_type_parameters'] = []
419
+ for index, binding in enumerate(bindings):
420
+ found['_type_parameters'].append(str(index))
421
+ found[f'_{index}'] = binding
422
+ else:
423
+ for parameter, binding in zip(found['_type_parameters'], bindings):
424
+ binding_type = self.access(
425
+ binding,
426
+ strict=strict) or binding
427
+
428
+ found[f'_{parameter}'] = binding_type
429
+
430
+ return found
431
+
432
+
433
+ def access(self, schema, strict=False):
434
+ if isinstance(schema, str):
435
+ return self.access_str(schema, strict=strict)
436
+ else:
437
+ return self.find(schema, strict=strict)
438
+
439
+
440
+ @functools.lru_cache(maxsize=None)
441
+ def access_str(self, schema, strict=False):
442
+ return self.find(
443
+ schema,
444
+ strict)
445
+
446
+
447
+ def retrieve(self, schema):
448
+ """
449
+ like access(schema) but raises an exception if nothing is found
450
+ """
451
+
452
+ found = self.find(
453
+ schema,
454
+ strict=True)
455
+
456
+ if found is None:
457
+ raise Exception(f'schema not found for type: {schema}')
458
+ return found
459
+
460
+
461
+ def _find_parameter(self, schema, parameter):
462
+ """
463
+ resolves the type parameter
464
+
465
+ helper for type_functions
466
+ """
467
+ schema_key = f'_{parameter}'
468
+ if schema_key not in schema:
469
+ schema = self.access(schema)
470
+ if schema_key not in schema:
471
+ return 'any'
472
+ # raise Exception(f'parameter {parameter} not found in schema:\n {schema}')
473
+
474
+ parameter_type = self.access(
475
+ schema[schema_key])
476
+
477
+ return parameter_type
478
+
479
+
480
+ def _parameters_for(self, initial_schema):
481
+ """
482
+ find any type parameters for this schema if they are present
483
+
484
+ helper for type_functions
485
+ """
486
+
487
+ if '_type_parameters' in initial_schema:
488
+ schema = initial_schema
489
+ else:
490
+ schema = self.access(initial_schema)
491
+
492
+ if '_type_parameters' not in schema:
493
+ return []
494
+ else:
495
+ result = []
496
+ for parameter in schema['_type_parameters']:
497
+ parameter_key = f'_{parameter}'
498
+ if parameter_key not in schema:
499
+ raise Exception(
500
+ f'looking for type parameter {parameter_key} but it is not in the schema:\n {pf(schema)}')
501
+ else:
502
+ parameter_type = schema[parameter_key]
503
+ result.append(
504
+ parameter_type)
505
+
506
+ return result
507
+
508
+
509
+ def default(self, schema):
510
+ """
511
+ produce the default value for the provided schema
512
+ """
513
+
514
+ default = None
515
+ found = self.retrieve(schema)
516
+
517
+ if '_default' in found:
518
+ default_value = found['_default']
519
+ if isinstance(default_value, str):
520
+ default_method = self.default_registry.access(default_value)
521
+ if default_method and callable(default_method):
522
+ default = default_method(found, self)
523
+ else:
524
+ default = self.deserialize(
525
+ found,
526
+ default_value)
527
+
528
+ elif not '_deserialize' in found:
529
+ raise Exception(
530
+ f'asking for default but no deserialize in {found}')
531
+
532
+ else:
533
+ default = self.deserialize(found, found['_default'])
534
+
535
+ else:
536
+ default = {}
537
+ for key, subschema in found.items():
538
+ if not is_schema_key(key):
539
+ default[key] = self.default(subschema)
540
+
541
+ return default
542
+
543
+
544
+ def dataclass(type_system, schema, path=None):
545
+ path = path or []
546
+ if not isinstance(path, list):
547
+ path = [path]
548
+
549
+ dataclass_function = type_system.choose_method(
550
+ schema,
551
+ {},
552
+ 'dataclass')
553
+
554
+ return dataclass_function(
555
+ schema,
556
+ path,
557
+ type_system)
558
+
559
+
560
+ def default_instance(core, schema, path=None):
561
+ dataclass = core.dataclass(schema, path)
562
+ default = core.default(schema)
563
+
564
+ return state_instance(dataclass, default)
565
+
566
+
567
+ def choose_method(self, schema, state, method_name):
568
+ """
569
+ find in the provided state, or schema if not there,
570
+ a method for the given method_name
571
+ """
572
+
573
+ method_key = f'_{method_name}'
574
+ found = None
575
+
576
+ if isinstance(state, dict) and method_key in state:
577
+ found = state[method_key]
578
+
579
+ elif isinstance(state, dict) and '_type' in state:
580
+ method_type = self.find(
581
+ state['_type'])
582
+
583
+ if method_type:
584
+ found = method_type.get(method_key)
585
+
586
+ if not found and isinstance(schema, dict) and method_key in schema:
587
+ found = schema[method_key]
588
+
589
+ if found is None:
590
+ any_type = self.access('any')
591
+ found = any_type[method_key]
592
+
593
+ registry = self._lookup_registry(method_key)
594
+ method_function = registry.access(
595
+ found)
596
+
597
+ if method_function is None:
598
+ raise Exception(f'no method "{method_name}" found for state {state} and schema {schema}')
599
+
600
+ return method_function
601
+
602
+
603
+ def slice(self, schema, state, path):
604
+ """
605
+ find the subschema and substate at a node in the place graph
606
+ given by the provided path
607
+ """
608
+
609
+ if not isinstance(path, (list, tuple)):
610
+ path = [path]
611
+
612
+ schema = self.access(schema)
613
+ if '..' in path:
614
+ path = resolve_path(path)
615
+
616
+ slice_function = self.choose_method(
617
+ schema,
618
+ state,
619
+ 'slice')
620
+
621
+ return slice_function(
622
+ schema,
623
+ state,
624
+ path,
625
+ self)
626
+
627
+
628
+ def match_node(self, schema, state, pattern):
629
+ if isinstance(pattern, dict):
630
+ if not isinstance(state, dict):
631
+ return False
632
+
633
+ if '_type' in pattern and not self.is_compatible(schema, pattern):
634
+ return False
635
+
636
+ for key, subpattern in pattern.items():
637
+ if key.startswith('_'):
638
+ continue
639
+
640
+ if key in schema:
641
+ subschema = schema[key]
642
+ else:
643
+ subschema = schema
644
+
645
+ if key in state:
646
+ matches = self.match_node(
647
+ subschema,
648
+ state[key],
649
+ pattern[key])
650
+ if not matches:
651
+ return False
652
+ else:
653
+ return False
654
+
655
+ return True
656
+
657
+ else:
658
+ return pattern == state
659
+
660
+
661
+ def _match_recur(self, schema, state, pattern, mode='first', path=()):
662
+ """
663
+ private helper method for match
664
+ """
665
+
666
+ matches = []
667
+
668
+ match = self.match_node(
669
+ schema,
670
+ state,
671
+ pattern)
672
+
673
+ if match:
674
+ if mode == 'first' or mode == 'immediate':
675
+ return [path]
676
+ else:
677
+ matches.append(path)
678
+ elif mode == 'immediate':
679
+ return []
680
+
681
+ if isinstance(state, dict):
682
+ for key, substate in state.items():
683
+ if key.startswith('_'):
684
+ continue
685
+
686
+ if key in schema:
687
+ subschema = schema[key]
688
+ else:
689
+ subschema = schema
690
+
691
+ submatches = self._match_recur(
692
+ subschema,
693
+ state[key],
694
+ pattern,
695
+ mode=mode,
696
+ path=path + (key,))
697
+
698
+ if mode == 'first' and len(submatches) > 0:
699
+ return submatches[0:1]
700
+ else:
701
+ matches.extend(submatches)
702
+
703
+ return matches
704
+
705
+
706
+ def match(self, original_schema, state, pattern, mode='first', path=()):
707
+ """
708
+ find the path or paths to any instances of a
709
+ given pattern in the tree.
710
+
711
+ "mode" can be a few things:
712
+ * immediate: only match top level
713
+ * first: only return the first match
714
+ * random: return a random match of all that matched
715
+ * all (or any other value): return every match in the tree
716
+ """
717
+
718
+ schema = self.access(original_schema)
719
+
720
+ matches = self._match_recur(
721
+ schema,
722
+ state,
723
+ pattern,
724
+ mode=mode,
725
+ path=path)
726
+
727
+ if mode == 'random':
728
+ matches_count = len(matches)
729
+ if matches_count > 0:
730
+ choice = random.randint(
731
+ 0,
732
+ matches_count-1)
733
+ matches = [matches[choice]]
734
+
735
+ return matches
736
+
737
+
738
+ def _react(self, schema, state, reaction, mode='random'):
739
+ """
740
+ helper for apply_update
741
+ """
742
+ # TODO: explain all this
743
+ # TODO: after the reaction, fill in the state with missing values
744
+ # from the schema
745
+
746
+ # TODO: add schema to redex and reactum
747
+
748
+ if 'redex' in reaction or 'reactum' in reaction or 'calls' in reaction:
749
+ redex = reaction.get('redex', {})
750
+ reactum = reaction.get('reactum', {})
751
+ calls = reaction.get('calls', {})
752
+ else:
753
+ # single key with reaction name
754
+ reaction_key = list(reaction.keys())[0]
755
+ make_reaction = self.react_registry.access(
756
+ reaction_key)
757
+ react = make_reaction(
758
+ schema,
759
+ state,
760
+ reaction.get(reaction_key, {}),
761
+ self)
762
+
763
+ redex = react.get('redex', {})
764
+ reactum = react.get('reactum', {})
765
+ calls = react.get('calls', {})
766
+
767
+ paths = self.match(
768
+ schema,
769
+ state,
770
+ redex,
771
+ mode=mode)
772
+
773
+ # for path in paths:
774
+ # path_schema, path_state = self.slice(
775
+ # schema,
776
+ # state,
777
+ # path)
778
+
779
+ def _merge_state(before):
780
+ """
781
+ helper used in call to transform_path
782
+ """
783
+ remaining = remove_omitted(
784
+ redex,
785
+ reactum,
786
+ before)
787
+
788
+ merged = deep_merge(
789
+ remaining,
790
+ reactum)
791
+
792
+ return merged
793
+
794
+ for path in paths:
795
+ state = transform_path(
796
+ state,
797
+ path,
798
+ _merge_state)
799
+
800
+ return state
801
+
802
+
803
+ def resolve(self, schema, update):
804
+ if update is None:
805
+ return schema
806
+ else:
807
+ resolve_function = self.choose_method(
808
+ schema,
809
+ update,
810
+ 'resolve')
811
+
812
+ return resolve_function(
813
+ schema,
814
+ update,
815
+ self)
816
+
817
+
818
+ def resolve_schemas(self, initial_current, initial_update):
819
+ """
820
+ """
821
+ if initial_current == initial_update:
822
+ return initial_current
823
+
824
+ current = self.access(initial_current)
825
+ update = self.access(initial_update)
826
+ # current = initial_current
827
+ # update = initial_update
828
+
829
+ if self.equivalent(current, update):
830
+ outcome = current
831
+
832
+ elif self.inherits_from(current, update):
833
+ outcome = current
834
+
835
+ elif self.inherits_from(update, current):
836
+ outcome = update
837
+
838
+ elif '_type' in current and '_type' in update and current['_type'] == update['_type']:
839
+ outcome = {}
840
+
841
+ for key in update:
842
+ if key == '_type_parameters' and '_type_parameters' in current:
843
+ for parameter in update['_type_parameters']:
844
+ parameter_key = f'_{parameter}'
845
+ if parameter in current['_type_parameters']:
846
+ if parameter_key in current:
847
+ if parameter_key in update:
848
+ outcome[parameter_key] = self.resolve_schemas(
849
+ current[parameter_key],
850
+ update[parameter_key])
851
+ else:
852
+ outcome[parameter_key] = current[parameter_key]
853
+ elif parameter_key in update:
854
+ outcome[parameter_key] = update[parameter_key]
855
+ else:
856
+ outcome[parameter_key] = update[parameter_key]
857
+ elif key not in current or type_parameter_key(current, key):
858
+ if update[key]:
859
+ outcome[key] = update[key]
860
+ else:
861
+ outcome[key] = current.get(key)
862
+ elif key in current and current[key]:
863
+ outcome[key] = self.resolve_schemas(
864
+ current[key],
865
+ update[key])
866
+ else:
867
+ outcome[key] = update[key]
868
+
869
+ elif '_type' in update and '_type' not in current:
870
+ outcome = self.resolve(update, current)
871
+
872
+ else:
873
+ # current = self.access(initial_current)
874
+ # update = self.access(initial_update)
875
+ outcome = self.resolve(current, update)
876
+
877
+ return outcome
878
+
879
+
880
+ def check_state(self, schema, state):
881
+ schema = self.access(schema)
882
+
883
+ check_function = self.choose_method(
884
+ schema,
885
+ state,
886
+ 'check')
887
+
888
+ return check_function(
889
+ schema,
890
+ state,
891
+ self)
892
+
893
+
894
+ def check(self, initial_schema, state):
895
+ schema = self.retrieve(initial_schema)
896
+ return self.check_state(schema, state)
897
+
898
+
899
+ def fold_state(self, schema, state, method, values):
900
+ schema = self.access(schema)
901
+
902
+ fold_function = self.choose_method(
903
+ schema,
904
+ state,
905
+ 'fold')
906
+
907
+ return fold_function(
908
+ schema,
909
+ state,
910
+ method,
911
+ values,
912
+ self)
913
+
914
+
915
+ def fold(self, initial_schema, state, method, values=None):
916
+ schema = self.retrieve(initial_schema)
917
+ return self.fold_state(
918
+ schema,
919
+ state,
920
+ method,
921
+ values)
922
+
923
+
924
+ def apply_update(self, schema, state, update,
925
+ top_schema=None, top_state=None, path=None):
926
+ schema = self.access(schema)
927
+
928
+ top_schema = top_schema or schema
929
+ top_state = top_state or state
930
+ path = path or []
931
+
932
+ if isinstance(update, dict) and '_react' in update:
933
+ new_state = self._react(
934
+ schema,
935
+ state,
936
+ update['_react'])
937
+
938
+ state = self.deserialize(schema, new_state)
939
+
940
+ elif isinstance(update, dict) and '_fold' in update:
941
+ fold = update['_fold']
942
+
943
+ if isinstance(fold, dict):
944
+ method = fold['method']
945
+ values = {
946
+ key: value
947
+ for key, value in fold.items()
948
+ if key != 'method'}
949
+
950
+ elif isinstance(fold, str):
951
+ method = fold
952
+ values = {}
953
+
954
+ else:
955
+ raise Exception(f'unknown fold: {pf(update)}')
956
+
957
+ state = self.fold(
958
+ schema,
959
+ state,
960
+ method,
961
+ values)
962
+
963
+ elif isinstance(schema, str) or isinstance(schema, list):
964
+ schema = self.access(schema)
965
+ state = self.apply_update(
966
+ schema,
967
+ state,
968
+ update,
969
+ top_schema=top_schema,
970
+ top_state=top_state,
971
+ path=path)
972
+
973
+ elif '_apply' in schema and schema['_apply'] != 'any':
974
+ apply_function = self.apply_registry.access(schema['_apply'])
975
+
976
+ state = apply_function(
977
+ schema,
978
+ state,
979
+ update,
980
+ top_schema,
981
+ top_state,
982
+ path,
983
+ self)
984
+
985
+ elif isinstance(update, dict):
986
+ for key, branch in update.items():
987
+ if key not in schema:
988
+ raise Exception(
989
+ f'trying to update a key that is not in the schema '
990
+ f'for state: {key}\n{state}\nwith schema:\n{schema}')
991
+ else:
992
+ subupdate = self.apply_update(
993
+ self.access(schema[key]),
994
+ state[key],
995
+ branch,
996
+ top_schema=top_schema,
997
+ top_state=top_state,
998
+ path=path + [key])
999
+
1000
+ state[key] = subupdate
1001
+ else:
1002
+ raise Exception(
1003
+ f'trying to apply update\n {update}\nto state\n {state}\n'
1004
+ f'with schema\n {schema}\nbut the update is not a dict')
1005
+
1006
+ return state
1007
+
1008
+
1009
+ def apply(self, original_schema, initial, update):
1010
+ schema = self.access(original_schema)
1011
+ state = copy.deepcopy(initial)
1012
+ return self.apply_update(
1013
+ schema,
1014
+ state,
1015
+ update)
1016
+
1017
+
1018
+ def merge_recur(self, schema, state, update):
1019
+ """
1020
+ not a helper to merge, as it would first appear
1021
+ """
1022
+ if is_empty(schema):
1023
+ merge_state = update
1024
+
1025
+ elif is_empty(update):
1026
+ merge_state = state
1027
+
1028
+ elif isinstance(update, dict):
1029
+ if isinstance(state, dict):
1030
+ for key, value in update.items():
1031
+ if is_schema_key(key):
1032
+ state[key] = value
1033
+ else:
1034
+ if isinstance(schema, str):
1035
+ schema = self.access(schema)
1036
+
1037
+ state[key] = self.merge_recur(
1038
+ schema.get(key),
1039
+ state.get(key),
1040
+ value)
1041
+ merge_state = state
1042
+ else:
1043
+ merge_state = update
1044
+ else:
1045
+ merge_state = update
1046
+
1047
+ return merge_state
1048
+
1049
+
1050
+ def merge(self, schema, state, path, update_schema, update_state, defer=False):
1051
+ top_schema, top_state = self.set_slice(
1052
+ schema,
1053
+ state,
1054
+ path,
1055
+ update_schema,
1056
+ update_state,
1057
+ defer)
1058
+
1059
+ return self.generate(top_schema, top_state)
1060
+
1061
+
1062
+ def bind(self, schema, state, key, target_schema, target_state):
1063
+ schema = self.retrieve(schema)
1064
+
1065
+ bind_function = self.choose_method(
1066
+ schema,
1067
+ state,
1068
+ 'bind')
1069
+
1070
+ return bind_function(
1071
+ schema,
1072
+ state,
1073
+ key,
1074
+ target_schema,
1075
+ target_state,
1076
+ self)
1077
+
1078
+
1079
+ def set_slice(self, schema, state, path, target_schema, target_state, defer=False):
1080
+ """
1081
+ Makes a local modification to the schema/state at the path, and
1082
+ returns the top_schema and top_state
1083
+ """
1084
+
1085
+ path = resolve_path(path)
1086
+
1087
+ if len(path) == 0:
1088
+ # deal with paths of length 0
1089
+ # this should never happen?
1090
+ merged_schema = self.resolve_schemas(
1091
+ schema,
1092
+ target_schema)
1093
+
1094
+ merged_state = deep_merge(
1095
+ state,
1096
+ target_state)
1097
+
1098
+ return merged_schema, merged_state
1099
+
1100
+ elif len(path) == 1:
1101
+ key = path[0]
1102
+ destination_schema, destination_state = self.slice(
1103
+ schema,
1104
+ state,
1105
+ key)
1106
+
1107
+ final_schema = self.resolve_schemas(
1108
+ destination_schema,
1109
+ target_schema)
1110
+
1111
+ if not defer:
1112
+ result_state = self.merge_recur(
1113
+ final_schema,
1114
+ destination_state,
1115
+ target_state)
1116
+
1117
+ else:
1118
+ result_state = self.merge_recur(
1119
+ final_schema,
1120
+ target_state,
1121
+ destination_state)
1122
+
1123
+ return self.bind(
1124
+ schema,
1125
+ state,
1126
+ key,
1127
+ final_schema,
1128
+ result_state)
1129
+
1130
+ else:
1131
+ head = path[0]
1132
+ tail = path[1:]
1133
+
1134
+ down_schema, down_state = self.slice(
1135
+ schema,
1136
+ state,
1137
+ head)
1138
+
1139
+ if head == '*':
1140
+ result_schema, result_state = down_schema, down_state
1141
+ for key in down_state:
1142
+ if key in target_state:
1143
+ subtarget_schema, subtarget_state = self.slice(
1144
+ target_schema,
1145
+ target_state,
1146
+ key)
1147
+
1148
+ try:
1149
+ result_schema, result_state = self.set_slice(
1150
+ result_schema,
1151
+ result_state,
1152
+ tail,
1153
+ subtarget_schema,
1154
+ subtarget_state,
1155
+ defer=defer)
1156
+
1157
+ except Exception as e:
1158
+ raise Exception(
1159
+ f'failed to set_slice at path {path}\n{str(e)}')
1160
+
1161
+ schema, state = self.bind(
1162
+ schema,
1163
+ state,
1164
+ key,
1165
+ result_schema,
1166
+ result_state)
1167
+
1168
+ return schema, state
1169
+
1170
+ else:
1171
+ try:
1172
+ result_schema, result_state = self.set_slice(
1173
+ down_schema,
1174
+ down_state,
1175
+ tail,
1176
+ target_schema,
1177
+ target_state,
1178
+ defer=defer)
1179
+ except Exception as e:
1180
+ raise Exception(f'failed to set_slice at path {path}\n{str(e)}')
1181
+
1182
+ return self.bind(
1183
+ schema,
1184
+ state,
1185
+ head,
1186
+ result_schema,
1187
+ result_state)
1188
+
1189
+
1190
+ def serialize(self, schema, state):
1191
+ schema = self.retrieve(schema)
1192
+
1193
+ serialize_function = self.choose_method(
1194
+ schema,
1195
+ state,
1196
+ 'serialize')
1197
+
1198
+ return serialize_function(
1199
+ schema,
1200
+ state,
1201
+ self)
1202
+
1203
+
1204
+ def deserialize(self, schema, state):
1205
+ schema = self.retrieve(schema)
1206
+
1207
+ deserialize_function = self.choose_method(
1208
+ schema,
1209
+ state,
1210
+ 'deserialize')
1211
+
1212
+ deserialized = deserialize_function(
1213
+ schema,
1214
+ state,
1215
+ self)
1216
+
1217
+ return deserialized
1218
+
1219
+
1220
+ def fill_state(self, schema, state=None, top_schema=None, top_state=None, path=None, type_key=None, context=None):
1221
+ # if a port is disconnected, build a store
1222
+ # for it under the '_open' key in the current
1223
+ # node (?)
1224
+
1225
+ # inform the user that they have disconnected
1226
+ # ports somehow
1227
+
1228
+ if schema is None:
1229
+ return None
1230
+ if state is None:
1231
+ state = self.default(schema)
1232
+ if top_schema is None:
1233
+ top_schema = schema
1234
+ if top_state is None:
1235
+ top_state = state
1236
+ if path is None:
1237
+ path = []
1238
+
1239
+ if '_inputs' in schema:
1240
+ inputs = state.get('inputs', {})
1241
+ state = self.fill_ports(
1242
+ schema['_inputs'],
1243
+ wires=inputs,
1244
+ state=state,
1245
+ top_schema=top_schema,
1246
+ top_state=top_state,
1247
+ path=path)
1248
+
1249
+ if '_outputs' in schema:
1250
+ outputs = state.get('outputs', {})
1251
+ state = self.fill_ports(
1252
+ schema['_outputs'],
1253
+ wires=outputs,
1254
+ state=state,
1255
+ top_schema=top_schema,
1256
+ top_state=top_state,
1257
+ path=path)
1258
+
1259
+ if isinstance(schema, str):
1260
+ schema = self.access(schema)
1261
+
1262
+ branches = non_schema_keys(schema)
1263
+
1264
+ if isinstance(state, dict):
1265
+ for branch in branches:
1266
+ subpath = path + [branch]
1267
+ state[branch] = self.fill_state(
1268
+ schema[branch],
1269
+ state=state.get(branch),
1270
+ top_schema=top_schema,
1271
+ top_state=top_state,
1272
+ path=subpath)
1273
+
1274
+ state_keys = non_schema_keys(state)
1275
+ for key in set(state_keys) - set(branches):
1276
+ subschema, substate = self.slice(
1277
+ schema,
1278
+ state,
1279
+ key)
1280
+
1281
+ subpath = path + [key]
1282
+ state[key] = self.fill_state(
1283
+ subschema,
1284
+ substate,
1285
+ top_schema=top_schema,
1286
+ top_state=top_state,
1287
+ path=subpath)
1288
+
1289
+ return state
1290
+
1291
+
1292
+ def fill(self, original_schema, state=None):
1293
+ schema = self.access(original_schema)
1294
+
1295
+ return self.fill_state(
1296
+ schema,
1297
+ state=state)
1298
+
1299
+
1300
+ def ports_schema(self, schema, instance, edge_path, ports_key='inputs'):
1301
+ found = self.access(schema)
1302
+
1303
+ edge_schema, edge_state = self.slice(
1304
+ schema,
1305
+ instance,
1306
+ edge_path)
1307
+
1308
+ ports_schema = edge_state.get(f'_{ports_key}',
1309
+ edge_schema.get(f'_{ports_key}'))
1310
+ ports = edge_state.get(ports_key)
1311
+
1312
+ return ports_schema, ports
1313
+
1314
+
1315
+ # start here for different methods of execution
1316
+ def view(self, schema, wires, path, top_schema=None, top_state=None):
1317
+ result = {}
1318
+
1319
+ if isinstance(wires, str):
1320
+ wires = [wires]
1321
+
1322
+ if isinstance(wires, (list, tuple)):
1323
+ _, result = self.slice(
1324
+ top_schema,
1325
+ top_state,
1326
+ list(path) + list(wires))
1327
+
1328
+ elif isinstance(wires, dict):
1329
+ result = {}
1330
+ for port_key, port_path in wires.items():
1331
+ subschema, _ = self.slice(
1332
+ schema,
1333
+ {},
1334
+ port_key)
1335
+
1336
+ inner_view = self.view(
1337
+ subschema,
1338
+ port_path,
1339
+ path,
1340
+ top_schema=top_schema,
1341
+ top_state=top_state)
1342
+
1343
+ if inner_view is not None:
1344
+ result[port_key] = inner_view
1345
+ else:
1346
+ raise Exception(f'trying to view state with these ports:\n{schema}\nbut not sure what these wires are:\n{wires}')
1347
+
1348
+ return result
1349
+
1350
+
1351
+ def view_edge(self, schema, state, edge_path=None, ports_key='inputs'):
1352
+ """
1353
+ project the current state into a form the edge expects, based on its ports.
1354
+ """
1355
+
1356
+ if schema is None:
1357
+ return None
1358
+ if state is None:
1359
+ state = self.default(schema)
1360
+ if edge_path is None:
1361
+ edge_path = []
1362
+
1363
+ ports_schema, ports = self.ports_schema(
1364
+ schema,
1365
+ state,
1366
+ edge_path=edge_path,
1367
+ ports_key=ports_key)
1368
+
1369
+ if not ports_schema:
1370
+ return None
1371
+ if ports is None:
1372
+ return None
1373
+
1374
+ return self.view(
1375
+ ports_schema,
1376
+ ports,
1377
+ edge_path[:-1],
1378
+ top_schema=schema,
1379
+ top_state=state)
1380
+
1381
+
1382
+ def project(self, ports, wires, path, states):
1383
+ result = {}
1384
+
1385
+ if isinstance(wires, str):
1386
+ wires = [wires]
1387
+
1388
+ if isinstance(wires, (list, tuple)):
1389
+ destination = resolve_path(list(path) + list(wires))
1390
+ result = set_star_path(
1391
+ result,
1392
+ destination,
1393
+ states)
1394
+
1395
+ elif isinstance(wires, dict):
1396
+ if isinstance(states, list):
1397
+ result = [
1398
+ self.project(ports, wires, path, state)
1399
+ for state in states]
1400
+ else:
1401
+ branches = []
1402
+ for key in wires.keys():
1403
+ subports, substates = self.slice(ports, states, key)
1404
+ projection = self.project(
1405
+ subports,
1406
+ wires[key],
1407
+ path,
1408
+ substates)
1409
+
1410
+ if projection is not None:
1411
+ branches.append(projection)
1412
+
1413
+ branches = [
1414
+ branch
1415
+ for branch in branches
1416
+ if branch is not None]
1417
+
1418
+ result = {}
1419
+ for branch in branches:
1420
+ deep_merge(result, branch)
1421
+ else:
1422
+ raise Exception(
1423
+ f'inverting state\n {states}\naccording to ports schema\n {ports}\nbut wires are not recognized\n {wires}')
1424
+
1425
+ return result
1426
+
1427
+
1428
+ def project_edge(self, schema, instance, edge_path, states, ports_key='outputs'):
1429
+ """
1430
+ Given states from the perspective of an edge (through its ports), produce states aligned to the tree
1431
+ the wires point to.
1432
+ (inverse of view)
1433
+ """
1434
+
1435
+ if schema is None:
1436
+ return None
1437
+ if instance is None:
1438
+ instance = self.default(schema)
1439
+
1440
+ ports_schema, ports = self.ports_schema(
1441
+ schema,
1442
+ instance,
1443
+ edge_path,
1444
+ ports_key)
1445
+
1446
+ if ports_schema is None:
1447
+ return None
1448
+ if ports is None:
1449
+ return None
1450
+
1451
+ return self.project(
1452
+ ports_schema,
1453
+ ports,
1454
+ edge_path[:-1],
1455
+ states)
1456
+
1457
+
1458
+ def equivalent(self, icurrent, iquestion):
1459
+ if icurrent == iquestion:
1460
+ return True
1461
+
1462
+ current = self.access(icurrent)
1463
+ question = self.access(iquestion)
1464
+
1465
+ if current is None:
1466
+ return question is None
1467
+
1468
+ if current == {}:
1469
+ return question == {}
1470
+
1471
+ elif '_type' in current:
1472
+ if '_type' in question:
1473
+ if current['_type'] == question['_type']:
1474
+ if '_type_parameters' in current:
1475
+ if '_type_parameters' in question:
1476
+ for parameter in current['_type_parameters']:
1477
+ parameter_key = f'_{parameter}'
1478
+ if parameter_key in question:
1479
+ if parameter_key not in current:
1480
+ return False
1481
+
1482
+ if not self.equivalent(current[parameter_key], question[parameter_key]):
1483
+ return False
1484
+ else:
1485
+ return False
1486
+ else:
1487
+ return False
1488
+ else:
1489
+ return False
1490
+ else:
1491
+ if '_type' in question:
1492
+ return False
1493
+
1494
+ for key, value in current.items():
1495
+ if not is_schema_key(key): # key not in type_schema_keys:
1496
+ if key not in question or not self.equivalent(current.get(key), question[key]):
1497
+ return False
1498
+
1499
+ for key in set(question.keys()) - set(current.keys()):
1500
+ if not is_schema_key(key): # key not in type_schema_keys:
1501
+ if key not in question or not self.equivalent(current.get(key), question[key]):
1502
+ return False
1503
+
1504
+ return True
1505
+
1506
+
1507
+ def inherits_from(self, descendant, ancestor):
1508
+ descendant = self.access(descendant)
1509
+ ancestor = self.access(ancestor)
1510
+
1511
+ if descendant == {}:
1512
+ return ancestor == {}
1513
+
1514
+ if descendant is None:
1515
+ return True
1516
+
1517
+ if ancestor is None:
1518
+ return False
1519
+
1520
+ if isinstance(ancestor, int):
1521
+ if isinstance(descendant, int):
1522
+ return ancestor == descendant
1523
+ else:
1524
+ return False
1525
+
1526
+ elif isinstance(ancestor, list):
1527
+ if isinstance(descendant, list):
1528
+ if len(ancestor) == len(descendant):
1529
+ for a, d in zip(ancestor, descendant):
1530
+ if not self.inherits_from(d, a):
1531
+ return False
1532
+ else:
1533
+ return False
1534
+
1535
+ elif '_type' in ancestor and ancestor['_type'] == 'any':
1536
+ return True
1537
+
1538
+ elif '_type' in descendant:
1539
+ if '_inherit' in descendant:
1540
+ for inherit in descendant['_inherit']:
1541
+
1542
+ if self.equivalent(inherit, ancestor) or self.inherits_from(inherit, ancestor):
1543
+ return True
1544
+
1545
+ return False
1546
+
1547
+ elif '_type_parameters' in descendant:
1548
+ for type_parameter in descendant['_type_parameters']:
1549
+ parameter_key = f'_{type_parameter}'
1550
+ if parameter_key in ancestor and parameter_key in descendant:
1551
+ if not self.inherits_from(descendant[parameter_key], ancestor[parameter_key]):
1552
+ return False
1553
+
1554
+ if '_type' not in ancestor or descendant['_type'] != ancestor['_type']:
1555
+ return False
1556
+
1557
+ else:
1558
+ for key, value in ancestor.items():
1559
+ if key not in type_schema_keys:
1560
+ # if not key.startswith('_'):
1561
+ if key in descendant:
1562
+ if not self.inherits_from(descendant[key], value):
1563
+ return False
1564
+ else:
1565
+ return False
1566
+
1567
+ return True
1568
+
1569
+
1570
+
1571
+ def wire_schema(self, schema, wires, path=None):
1572
+ outcome = {}
1573
+ path = path or []
1574
+
1575
+ if isinstance(wires, dict):
1576
+ for key, subwires in wires.items():
1577
+ outcome[key] = self.wire_schema(
1578
+ schema,
1579
+ wires[key],
1580
+ path + [key])
1581
+
1582
+ else:
1583
+ _, outcome = self.slice('schema', schema, wires)
1584
+
1585
+ return outcome
1586
+
1587
+
1588
+ def _generate_recur(self, schema, state, top_schema=None, top_state=None, path=None):
1589
+ """
1590
+ helper for type_functions
1591
+ """
1592
+ found = self.retrieve(
1593
+ schema)
1594
+
1595
+ generate_function = self.choose_method(
1596
+ found,
1597
+ state,
1598
+ 'generate')
1599
+
1600
+ return generate_function(
1601
+ self,
1602
+ found,
1603
+ state,
1604
+ top_schema=top_schema,
1605
+ top_state=top_state,
1606
+ path=path)
1607
+
1608
+
1609
+ def generate(self, schema, state):
1610
+ merged_schema, merged_state = self._sort(
1611
+ schema,
1612
+ state)
1613
+
1614
+ _, _, top_schema, top_state = self._generate_recur(
1615
+ merged_schema,
1616
+ merged_state)
1617
+
1618
+ return top_schema, top_state
1619
+
1620
+
1621
+ def find_method(self, schema, method_key):
1622
+ if not isinstance(schema, dict) or method_key not in schema:
1623
+ schema = self.access(schema)
1624
+
1625
+ if method_key in schema:
1626
+ registry = self._lookup_registry(
1627
+ method_key)
1628
+
1629
+ if registry is not None:
1630
+ method_name = schema[method_key]
1631
+ method = registry.access(method_name)
1632
+
1633
+ return method
1634
+
1635
+ def representation(self, schema, path=None, parents=None):
1636
+ """
1637
+ produce a string representation of the schema
1638
+
1639
+ * intended to be the inverse of parse_expression()
1640
+ """
1641
+
1642
+ path = path or []
1643
+ parents = parents or []
1644
+ schema_id = id(schema)
1645
+
1646
+ if schema_id in parents:
1647
+ index = parents.index(schema_id)
1648
+ reference = path[:index]
1649
+ output = '/'.join(reference)
1650
+
1651
+ return f'/{output}'
1652
+
1653
+ if isinstance(schema, str):
1654
+ return schema
1655
+
1656
+ elif isinstance(schema, tuple):
1657
+ inner = [
1658
+ self.representation(
1659
+ element,
1660
+ path + [index],
1661
+ parents + [schema_id])
1662
+ for index, element in enumerate(schema)]
1663
+
1664
+ pipes = '|'.join(inner)
1665
+ return f'({pipes})'
1666
+
1667
+ elif isinstance(schema, dict):
1668
+ if '_type' in schema:
1669
+ type = schema['_type']
1670
+
1671
+ inner = []
1672
+ block = ''
1673
+ if '_type_parameters' in schema:
1674
+ for parameter_key in schema['_type_parameters']:
1675
+ schema_key = f'_{parameter_key}'
1676
+ if schema_key in schema:
1677
+ parameter = self.representation(
1678
+ schema[schema_key],
1679
+ path + [schema_key],
1680
+ parents + [schema_id])
1681
+ inner.append(parameter)
1682
+ else:
1683
+ inner.append('()')
1684
+
1685
+ commas = ','.join(inner)
1686
+ block = f'[{commas}]'
1687
+
1688
+ if type == 'tuple':
1689
+ pipes = '|'.join(inner)
1690
+ return f'({pipes})'
1691
+ else:
1692
+ return f"{type}{block}"
1693
+
1694
+ else:
1695
+ inner = {}
1696
+ for key in non_schema_keys(schema):
1697
+ subschema = self.representation(
1698
+ schema[key],
1699
+ path + [key],
1700
+ parents + [schema_id])
1701
+
1702
+ inner[key] = subschema
1703
+
1704
+ colons = [
1705
+ f'{key}:{value}'
1706
+ for key, value in inner.items()]
1707
+
1708
+ pipes = '|'.join(colons)
1709
+ return f'({pipes})'
1710
+ else:
1711
+ return str(schema)
1712
+
1713
+ def validate_schema(self, schema, enforce_connections=False):
1714
+ """
1715
+ """
1716
+ # TODO: needs documentation and testing
1717
+
1718
+ # TODO:
1719
+ # check() always returns true or false,
1720
+ # validate() returns information about what doesn't match
1721
+
1722
+ # add ports and wires
1723
+ # validate ports are wired to a matching type,
1724
+ # either the same type or a subtype (more specific type)
1725
+ # declared ports are equivalent to the presence of a process
1726
+ # where the ports need to be looked up
1727
+
1728
+ report = {}
1729
+
1730
+ if schema is None:
1731
+ report = 'schema cannot be None'
1732
+
1733
+ elif isinstance(schema, str):
1734
+ typ = self.access(
1735
+ schema,
1736
+ strict=True)
1737
+
1738
+ if typ is None:
1739
+ report = f'type: {schema} is not in the registry'
1740
+
1741
+ elif isinstance(schema, dict):
1742
+ report = {}
1743
+
1744
+ schema_keys = set([])
1745
+ branches = set([])
1746
+
1747
+ for key, value in schema.items():
1748
+ if key == '_type':
1749
+ typ = self.access(
1750
+ value,
1751
+ strict=True)
1752
+ if typ is None:
1753
+ report[key] = f'type: {value} is not in the registry'
1754
+
1755
+ elif key in type_schema_keys:
1756
+ schema_keys.add(key)
1757
+ registry = self._lookup_registry(key)
1758
+ if registry is None or key == '_default':
1759
+ # deserialize and serialize back and check it is equal
1760
+ pass
1761
+ elif isinstance(value, str):
1762
+ element = registry.access(
1763
+ value,
1764
+ strict=True)
1765
+
1766
+ if element is None:
1767
+ report[key] = f'no entry in the {key} registry for: {value}'
1768
+ elif not inspect.isfunction(value):
1769
+ report[key] = f'unknown value for key {key}: {value}'
1770
+ else:
1771
+ branches.add(key)
1772
+ branch_report = self.validate_schema(value)
1773
+ if len(branch_report) > 0:
1774
+ report[key] = branch_report
1775
+
1776
+ return report
1777
+
1778
+
1779
+ apply_slice = TypeSystemAdjunct.apply_slice
1780
+ complete = TypeSystemAdjunct.complete
1781
+ compose = TypeSystemAdjunct.compose
1782
+ define = TypeSystemAdjunct.define
1783
+ fill_ports = TypeSystemAdjunct.fill_ports
1784
+ hydrate = TypeSystemAdjunct.hydrate
1785
+ import_types = TypeSystemAdjunct.import_types
1786
+ infer_wires = TypeSystemAdjunct.infer_wires
1787
+ infer_edge = TypeSystemAdjunct.infer_edge
1788
+ infer_schema = TypeSystemAdjunct.infer_schema
1789
+ link_place = TypeSystemAdjunct.link_place
1790
+ lookup = TypeSystemAdjunct.lookup
1791
+ merge_schemas = TypeSystemAdjunct.merge_schemas
1792
+ query = TypeSystemAdjunct.query
1793
+ resolve_parameters = TypeSystemAdjunct.resolve_parameters
1794
+ set = TypeSystemAdjunct.set
1795
+ set_update = TypeSystemAdjunct.set_update
1796
+ types = TypeSystemAdjunct.types
1797
+ validate = TypeSystemAdjunct.validate
1798
+ validate_state = TypeSystemAdjunct.validate_state