vss-tools 0.5.0.dev0__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 (39) hide show
  1. vspec/__init__.py +996 -0
  2. vspec/loggingconfig.py +22 -0
  3. vspec/model/__init__.py +7 -0
  4. vspec/model/constants.py +307 -0
  5. vspec/model/exceptions.py +29 -0
  6. vspec/model/vsstree.py +602 -0
  7. vspec/py.typed +0 -0
  8. vspec/utils/__init__.py +23 -0
  9. vspec/utils/idgen_utils.py +97 -0
  10. vspec/utils/stringstyle.py +25 -0
  11. vspec/utils/vss2id_val.py +165 -0
  12. vspec/vspec2vss_config.py +53 -0
  13. vspec/vspec2x.py +191 -0
  14. vspec/vss2x.py +38 -0
  15. vspec/vssexporters/__init__.py +7 -0
  16. vspec/vssexporters/vss2binary.py +147 -0
  17. vspec/vssexporters/vss2csv.py +82 -0
  18. vspec/vssexporters/vss2ddsidl.py +295 -0
  19. vspec/vssexporters/vss2franca.py +98 -0
  20. vspec/vssexporters/vss2graphql.py +143 -0
  21. vspec/vssexporters/vss2id.py +172 -0
  22. vspec/vssexporters/vss2json.py +116 -0
  23. vspec/vssexporters/vss2jsonschema.py +159 -0
  24. vspec/vssexporters/vss2protobuf.py +171 -0
  25. vspec/vssexporters/vss2yaml.py +122 -0
  26. vss_tools-0.5.0.dev0.data/scripts/vspec2csv.py +29 -0
  27. vss_tools-0.5.0.dev0.data/scripts/vspec2ddsidl.py +29 -0
  28. vss_tools-0.5.0.dev0.data/scripts/vspec2franca.py +29 -0
  29. vss_tools-0.5.0.dev0.data/scripts/vspec2graphql.py +29 -0
  30. vss_tools-0.5.0.dev0.data/scripts/vspec2id.py +28 -0
  31. vss_tools-0.5.0.dev0.data/scripts/vspec2json.py +29 -0
  32. vss_tools-0.5.0.dev0.data/scripts/vspec2jsonschema.py +29 -0
  33. vss_tools-0.5.0.dev0.data/scripts/vspec2protobuf.py +29 -0
  34. vss_tools-0.5.0.dev0.data/scripts/vspec2yaml.py +29 -0
  35. vss_tools-0.5.0.dev0.dist-info/LICENSE +373 -0
  36. vss_tools-0.5.0.dev0.dist-info/METADATA +65 -0
  37. vss_tools-0.5.0.dev0.dist-info/RECORD +39 -0
  38. vss_tools-0.5.0.dev0.dist-info/WHEEL +5 -0
  39. vss_tools-0.5.0.dev0.dist-info/top_level.txt +1 -0
vspec/__init__.py ADDED
@@ -0,0 +1,996 @@
1
+ # Copyright (c) 2016 Contributors to COVESA
2
+ #
3
+ # This program and the accompanying materials are made available under the
4
+ # terms of the Mozilla Public License 2.0 which is available at
5
+ # https://www.mozilla.org/en-US/MPL/2.0/
6
+ #
7
+ # SPDX-License-Identifier: MPL-2.0
8
+
9
+ #
10
+ # VSpec file parser.
11
+ #
12
+
13
+ import yaml
14
+ import os
15
+ import uuid
16
+ import sys
17
+ import collections
18
+ import logging
19
+ from copy import deepcopy
20
+ from typing import List, Optional
21
+
22
+ from anytree import (Resolver, LevelOrderIter, PreOrderIter, RenderTree) # type: ignore[import]
23
+
24
+ from .model.vsstree import VSSNode
25
+ from .model.exceptions import ImpossibleMergeException, IncompleteElementException
26
+ from .model.constants import VSSTreeType, VSSUnitCollection, VSSQuantityCollection
27
+
28
+ nestable_types = set(["branch", "struct"])
29
+
30
+
31
+ class VSpecError(Exception):
32
+ def __init__(self, *args, **kwargs):
33
+ self.file_name = args[0]
34
+ self.line_nr = args[1]
35
+ self.message = args[2]
36
+ Exception.__init__(self, *args, **kwargs)
37
+
38
+ def __str__(self):
39
+ return "{}: {}: {}".format(self.file_name, self.line_nr, self.message)
40
+
41
+
42
+ # Try to open a file name that can reside
43
+ # in any directory listed in incude_paths.
44
+ # If successful, read context and return file
45
+ #
46
+ def search_and_read(file_name, include_paths):
47
+ # If absolute path, then ignore include paths
48
+ if file_name[0] == '/':
49
+ with open(file_name, "r") as fp:
50
+ text = fp.read()
51
+ fp.close()
52
+ return os.path.dirname(file_name), text
53
+
54
+ for directory in include_paths:
55
+ try:
56
+ path = "{}/{}".format(directory, file_name)
57
+ with open(path, "r") as fp:
58
+ text = fp.read()
59
+ fp.close()
60
+ return os.path.dirname(path), text
61
+ except IOError:
62
+ pass
63
+
64
+ # We failed, raise last exception we ran into.
65
+ raise VSpecError(file_name, 0, "File error")
66
+
67
+
68
+ def convert_yaml_to_list(raw_yaml):
69
+ if isinstance(raw_yaml, list):
70
+ return raw_yaml
71
+
72
+ # Sort the dictionary according to line number.
73
+ # The reason is that when the YAML file is loaded
74
+ # the object order is not preserved in the created
75
+ # dictionary
76
+ raw_yaml = collections.OrderedDict(
77
+ sorted(raw_yaml.items(), key=lambda x: x[1]['$line$']))
78
+ lst = []
79
+ for elem in raw_yaml:
80
+ if isinstance(raw_yaml[elem], dict):
81
+ raw_yaml[elem]['$name$'] = elem
82
+ lst.append(raw_yaml[elem])
83
+
84
+ return lst
85
+
86
+
87
+ def load_tree(
88
+ file_name,
89
+ include_paths,
90
+ tree_type: VSSTreeType,
91
+ break_on_name_style_violation=False,
92
+ expand_inst=True,
93
+ data_type_tree: Optional[VSSNode] = None):
94
+ if expand_inst and tree_type == VSSTreeType.DATA_TYPE_TREE:
95
+ logging.error("Instance expansion is not supported for VSS type tree.")
96
+ sys.exit(-1)
97
+
98
+ flat_model = load_flat_model(file_name, "", include_paths, tree_type)
99
+ absolute_path_flat_model = create_absolute_paths(flat_model)
100
+ deep_model = create_nested_model(absolute_path_flat_model, file_name)
101
+ cleanup_deep_model(deep_model)
102
+ dict_tree = deep_model["children"]
103
+ tree = render_tree(
104
+ dict_tree,
105
+ tree_type,
106
+ break_on_name_style_violation=break_on_name_style_violation)
107
+ if expand_inst:
108
+ expand_tree_instances(tree)
109
+ return tree
110
+
111
+
112
+ def check_type_usage(tree: VSSNode, tree_type: VSSTreeType, type_tree: Optional[VSSNode] = None):
113
+ """
114
+ Check usages of types within the tree.
115
+ This methods shall be called after overlays (or additional type files) have been merged.
116
+ """
117
+ if tree_type == VSSTreeType.DATA_TYPE_TREE:
118
+ check_data_type_references(tree)
119
+ elif tree_type == VSSTreeType.SIGNAL_TREE:
120
+ check_data_type_references_across_trees(tree, type_tree)
121
+
122
+
123
+ def load_flat_model(file_name, prefix, include_paths, tree_type: VSSTreeType):
124
+ # Hooks into YAML parser to add line numbers
125
+ # and file name into each element
126
+ def yaml_compose_node(parent, index):
127
+ # the line number where the previous token has ended (plus empty lines)
128
+ line = loader.line
129
+ try:
130
+ node = yaml.composer.Composer.compose_node(loader, parent, index)
131
+ except yaml.scanner.ScannerError as e:
132
+ raise VSpecError(file_name, line + 1, e)
133
+ except yaml.parser.ParserError as e:
134
+ raise VSpecError(file_name, line + 1, e)
135
+
136
+ if node.value == '$include$':
137
+ node.value = f'$include${load_flat_model.include_index}'
138
+ load_flat_model.include_index = load_flat_model.include_index + 1
139
+
140
+ # Avoid having root-level line numbers as non-dictionary entries
141
+ if parent:
142
+ node.__line__ = line + 1
143
+ node.__file_name__ = file_name
144
+ else:
145
+ node.__line__ = None
146
+ node.__file_name = None
147
+ return node
148
+
149
+ def yaml_construct_mapping(node, deep=True):
150
+ mapping = yaml.constructor.Constructor.construct_mapping(
151
+ loader, node, deep=deep)
152
+
153
+ # Replace
154
+ # { 'Vehicle.Speed': { 'datatype': 'boolean', 'type': 'sensor' }}
155
+ # with
156
+ # { '$name$': 'Vehicle.Speed', 'datatype': 'boolean', 'type': 'sensor' }
157
+
158
+ for key, val in list(mapping.items()):
159
+ if key[0] == '$':
160
+ continue
161
+
162
+ if val is None:
163
+ mapping['$name$'] = key
164
+ del mapping[key]
165
+ break
166
+
167
+ # Add line number and file name to element.
168
+ if node.__line__ is not None:
169
+ mapping['$line$'] = node.__line__
170
+ mapping['$file_name$'] = node.__file_name__
171
+
172
+ return mapping
173
+
174
+ directory, text = search_and_read(file_name, include_paths)
175
+
176
+ # Do a trial pasing of the file to find out if it is list- or
177
+ # object-formatted.
178
+ loader = yaml.Loader(text)
179
+ loader.compose_node = yaml_compose_node # type: ignore[assignment]
180
+
181
+ loader.construct_mapping = yaml_construct_mapping # type: ignore[assignment]
182
+ test_yaml = loader.get_data()
183
+
184
+ # Depending on if this is a list or an object, expand
185
+ # the #include diretives differently
186
+ #
187
+ if isinstance(test_yaml, list):
188
+ text = yamilify_includes(text, True)
189
+ else:
190
+ text = yamilify_includes(text, False)
191
+
192
+ # Re-initialize loader with the new text hosting the
193
+ # yamilified includes.
194
+ loader = yaml.Loader(text)
195
+ loader.compose_node = yaml_compose_node # type: ignore[assignment]
196
+
197
+ loader.construct_mapping = yaml_construct_mapping # type: ignore[assignment]
198
+ raw_yaml = loader.get_data()
199
+
200
+ # Check for file with no objects.
201
+ if not raw_yaml:
202
+ return []
203
+
204
+ raw_yaml = convert_yaml_to_list(raw_yaml)
205
+
206
+ # Sanity check of loaded code
207
+ check_yaml_usage(raw_yaml, file_name)
208
+
209
+ # Recursively expand all include files.
210
+ if directory not in include_paths:
211
+ include_paths = [directory] + include_paths
212
+ expanded_includes = expand_includes(
213
+ raw_yaml, prefix, include_paths, tree_type)
214
+
215
+ flat_model = cleanup_flat_entries(expanded_includes, tree_type)
216
+
217
+ return flat_model
218
+
219
+
220
+ def cleanup_flat_entries(flat_model, tree_type: VSSTreeType):
221
+ """
222
+ # 1. Check that the declared type is part of the available types for the tree.
223
+ # 2. Check that allowed values are provided as arrays.
224
+ """
225
+ available_types = tree_type.available_types()
226
+
227
+ # Traverse the flat list of the parsed specification
228
+ for elem in flat_model:
229
+ # Is this an include element?
230
+ if "type" not in elem:
231
+ raise VSpecError(elem["$file_name$"], elem["$line$"], "No type specified!")
232
+
233
+ # Check, with case sensitivity that we do have
234
+ # a validated type.
235
+ if not elem["type"] in available_types:
236
+ raise VSpecError(elem["$file_name$"], elem["$line$"],
237
+ "Unknown type: {}".format(elem["type"]))
238
+
239
+ if "allowed" in elem and not isinstance(elem["allowed"], list):
240
+ raise VSpecError(elem["$file_name$"], elem["$line$"],
241
+ "Allowed values are not represented as array.")
242
+
243
+ return flat_model
244
+
245
+
246
+ #
247
+ # Delete parser-specific elements
248
+ #
249
+ # Parser metadata is cleaned in two steps. An initial step just after vspec files are parsed.
250
+ # Then some data is removed but not all as it is used for error messages.
251
+ # That needs to be removed in a second step, just before exporting.
252
+ #
253
+ def cleanup_deep_model(deep_model):
254
+
255
+ if "$line$" in deep_model:
256
+ del deep_model["$line$"]
257
+
258
+ if "$prefix$" in deep_model:
259
+ del deep_model["$prefix$"]
260
+
261
+ if "$name$" in deep_model:
262
+ del deep_model['$name$']
263
+
264
+ # children as of today exists only for branches and structs
265
+ if "children" in deep_model:
266
+ children = deep_model["children"]
267
+ for child in deep_model["children"]:
268
+ cleanup_deep_model(children[child])
269
+
270
+ return None
271
+
272
+ #
273
+ # Meta data on extended attributes needs to be cleaned as part of the second cleaning step.
274
+ # as it is not included in first step.
275
+ #
276
+
277
+
278
+ def clean_metadata(node):
279
+
280
+ if isinstance(node, VSSNode):
281
+ clean_metadata(node.extended_attributes)
282
+ for child in node.children:
283
+ clean_metadata(child)
284
+ elif isinstance(node, dict):
285
+ for k in list(node.keys()):
286
+ clean_metadata(node[k])
287
+ if k in ["$file_name$", "$line$"]:
288
+ del node[k]
289
+ elif isinstance(node, list):
290
+ for elem in node:
291
+ clean_metadata(elem)
292
+
293
+
294
+ def verify_mandatory_attributes(node, abort_on_unknown_attribute: bool):
295
+ """
296
+ Verify that mandatory attributes are present.
297
+ Need to be checked first after overlays (if any) have been applied, as attributes are not
298
+ mandatory in individual files but only in the final tree
299
+ """
300
+ if isinstance(node, VSSNode):
301
+ if node.delete:
302
+ logging.info(
303
+ f"Node {node.qualified_name()} will be deleted. Please note, that if {node.qualified_name()} "
304
+ f"is a branch all subsequent nodes will also be deleted irrespective of their 'delete' value."
305
+ )
306
+ node.parent = None
307
+ node.children = []
308
+ node.verify_attributes(abort_on_unknown_attribute)
309
+ for child in node.children:
310
+ verify_mandatory_attributes(child, abort_on_unknown_attribute)
311
+
312
+
313
+ #
314
+ # Verify that we are using correct YAML in the model
315
+ #
316
+ def check_yaml_usage(flat_model, file_name):
317
+ for elem in flat_model:
318
+ if isinstance(elem, list):
319
+ raise VSpecError(
320
+ file_name,
321
+ 0,
322
+ "Element {} is not a list entry. (Did you forget a ':'?)".format(elem))
323
+
324
+ # FIXME:
325
+ # Add more usage checks, such as absence of nested models.
326
+ # and mutually exclusive elements.
327
+
328
+
329
+ # Expand yaml include elements (inserted by yamilify_include())
330
+ #
331
+ def expand_includes(flat_model, prefix, include_paths, tree_type: VSSTreeType):
332
+ # Build up a new spec model based on the old one, but
333
+ # with expanded include directives.
334
+
335
+ new_flat_model: List[VSSNode] = []
336
+
337
+ # Traverse the flat list of the parsed specification
338
+ for elem in flat_model:
339
+ # Is this an include element?
340
+ if elem['$name$'][0:9] == "$include$":
341
+ include_prefix = elem.get("prefix", "")
342
+
343
+ if include_prefix != "":
344
+ prefix_found = False
345
+ for dict_elem in new_flat_model:
346
+ if dict_elem["$name$"] == include_prefix:
347
+ prefix_found = True
348
+ break
349
+ if not prefix_found:
350
+ # Printing line number does not make sense, as we work on a
351
+ # modified tree
352
+ logging.warning(
353
+ f"No branch matching prefix {include_prefix} for #include in {elem['$file_name$']}")
354
+
355
+ # Append include prefix to our current prefix.
356
+ # Make sure we do not start new prefix with a "."
357
+ if prefix != "":
358
+ if include_prefix != "":
359
+ include_prefix = "{}.{}".format(prefix, include_prefix)
360
+ else:
361
+ include_prefix = prefix
362
+
363
+ # Recursively load included file
364
+ inc_elem = load_flat_model(
365
+ elem["file"], include_prefix, include_paths, tree_type)
366
+
367
+ # Add the loaded elements at the end of the new spec model
368
+ new_flat_model.extend(inc_elem)
369
+ else:
370
+ # Add a prefix to the element
371
+ elem["$prefix$"] = prefix
372
+ # Add the existing elements at the end of the new spec model
373
+ new_flat_model.append(elem)
374
+
375
+ return new_flat_model
376
+
377
+
378
+ def expand_tree_instances(tree: VSSNode):
379
+ tree_node: VSSNode
380
+
381
+ def rollout_list(instance_entry):
382
+ '''
383
+ Converts "Prefix[1,n] to [Prefix1, Prefix2, ..., Prefixn]"
384
+ '''
385
+ prefix = ""
386
+ if "[" in instance_entry: # if so unroll
387
+ unrolled_items = []
388
+ prefix = instance_entry[:instance_entry.find("[")]
389
+ start = instance_entry[instance_entry.find(
390
+ "[") + 1:instance_entry.find(",")]
391
+ end = instance_entry[instance_entry.find(
392
+ ",") + 1:instance_entry.find("]")]
393
+ for i in range(int(start), int(end) + 1):
394
+ unrolled_items.append(f"{prefix}{i}")
395
+ else: # if not, add
396
+ unrolled_items = instance_entry
397
+ return unrolled_items, prefix
398
+
399
+ def is_instance_branch(node, unrolled_instances, prefix):
400
+ '''
401
+ Check if node is a branch that has the same name as an instance for parent node
402
+ '''
403
+ for instance in unrolled_instances:
404
+ if isinstance(instance, list):
405
+ # This means the element of the instances is a list, e.g. it was
406
+ # something like Row[1,4]
407
+ for element in instance:
408
+ if element == node.name:
409
+ return True
410
+ else:
411
+ # in this case our instances have been a simple list of elements
412
+ # (as opposed to a list of lists), e.g. ['Left', 'Right'], so we just compare by name
413
+ if instance == node.name:
414
+ return True
415
+
416
+ # Now we try to be smarter - check if it seems to be an instance by prefix, e.g. Pos3 and prefix is Pos
417
+ # This is only for specifying instances outside specified range, e.g.
418
+ # specifying Row5 while instance is specified as Row[1,4]
419
+ if prefix != "" and prefix in node.name:
420
+ number = node.name.split(prefix, 1)[1]
421
+ return number.isnumeric()
422
+ return False
423
+
424
+ def create_instantiated_branch(branch_name, parent, nodes_to_expand):
425
+ # Check if the branch we want to create as part of expansion (e.g.
426
+ # Row1, Pos2, Left, ...) already exist
427
+ old_node = None
428
+ for child in parent.children:
429
+ if child.name == branch_name:
430
+ old_node = child
431
+ instantiated_branch = VSSNode(
432
+ branch_name,
433
+ # autopep8: off
434
+ {"type": "branch",
435
+ "description": parent.description,
436
+ "comment": parent.comment,
437
+ "$file_name$": "Generated"
438
+ },
439
+ # autopep8: on
440
+ VSSTreeType.SIGNAL_TREE.available_types(),
441
+ parent)
442
+ if old_node is not None:
443
+ # If it exist we take the new one as default (to give e.g. default descriptions and comments)
444
+ # Then merge anything from the old (expanded) instance above, to get e.g. updated comment
445
+ # Finally remove the old node by removing parent
446
+ instantiated_branch.merge(old_node)
447
+ for child in old_node.children:
448
+ child.parent = instantiated_branch
449
+ old_node.parent = None
450
+
451
+ # Deep copy needed so that we can change attributes/dict/children
452
+ # independently
453
+ for expand_node in deepcopy(nodes_to_expand):
454
+ # Check if this branch/signal already exists in the instantiated
455
+ # branch
456
+ for existing_item in instantiated_branch.children:
457
+ if expand_node.name == existing_item.name:
458
+ # A child with the same name already exists
459
+ # Typical use-case is that a single instance of this signal has been re-defined in an overlay
460
+ # Then data from the overlay (for example A.B.Row2.Column2.Sig)
461
+ # shall have precedence over the expanded instance
462
+ # This is handled by removing the old node from tree and
463
+ # instead merging it to the new node
464
+ existing_item.parent = None
465
+ expand_node.merge(existing_item)
466
+ break
467
+ expand_node.parent = instantiated_branch
468
+ return instantiated_branch
469
+
470
+ # Checking each node for instances and expand them
471
+ # The walking order makes sure, we do not need to recurse
472
+ for tree_node in PreOrderIter(tree):
473
+ if tree_node.has_instances():
474
+ # print(f"This node has instances: {tree_node.qualified_name()}, they are *{tree_node.instances}*")
475
+
476
+ # Instances can be many things: A string Row[1,4] that is shorthand for a list,
477
+ # a simple list of of strings ['Left', 'Right'],
478
+ # or a list of lists ['Row[1,4]', ['Left', 'Right']], which expresses
479
+ # multidimensional instances, where the n'th entry will be expanded at the
480
+ # n'th level under the current node, i.e. in the example above you expect
481
+ # children branches Row1, Row2, Row3, Row4, where each of them has a "left"
482
+ # and a "right" child.
483
+ # See
484
+ # https://covesa.github.io/vehicle_signal_specification/rule_set/instances/
485
+ # for an explanation.
486
+ # This is a bit painful
487
+
488
+ unrolled_instances = []
489
+ array_prefix = ""
490
+
491
+ # Instances are a list in .vspec e.g. ["left", "right"]
492
+ if isinstance(tree_node.instances, list):
493
+ for instance_entry in tree_node.instances:
494
+ # check every entry whether it is shorthand for a list
495
+ # e.g. Prefix[1,3]
496
+ unrolled_items, tmpprefix = rollout_list(instance_entry)
497
+ if array_prefix == "":
498
+ # For "smart" comparison only use prefix from first
499
+ # level
500
+ array_prefix = tmpprefix
501
+ unrolled_instances.append(unrolled_items)
502
+
503
+ # it is not a list, e.g. instance in vspec is just Sensor[1,10]
504
+ else:
505
+ unrolled_items, array_prefix = rollout_list(
506
+ tree_node.instances)
507
+ unrolled_instances.append(unrolled_items)
508
+
509
+ # When a node has instances we need to decide what to do with the children
510
+ # The default behavior is to duplicate each child under each created instance,
511
+ # but there are exceptions.
512
+ #
513
+ # The first exception is nodes marked in vspec to be excluded from instantiation
514
+ # (instantiate: False)
515
+ #
516
+ # The other exception is nodes/branches that actually already are expanded.
517
+ # This shall typically only occur when analyzing overlays, where you may find
518
+ # a signal specified as e.g. Vehicle.Cabin.Door.Row2.Right.Window.Tint
519
+ # which then shall not be copied for each instance of Door as it is valid only
520
+ # for one of the instances
521
+
522
+ nodes_to_stay = [] # < nodes excluded from instantiation
523
+ nodes_to_expand = [] # < nodes shall use the instances as parent
524
+
525
+ for child in tree_node.children:
526
+ if is_instance_branch(child, unrolled_instances, array_prefix):
527
+ # Child is already an instance, for example Row2.Right.X
528
+ nodes_to_stay.append(child)
529
+ elif child.is_instantiated():
530
+ # Child has an explicit or implicit "instantiate:true",
531
+ # this is the default
532
+ nodes_to_expand.append(child)
533
+ else:
534
+ nodes_to_stay.append(child)
535
+
536
+ tree_node.children = deepcopy(nodes_to_stay)
537
+
538
+ # now iterate over instances
539
+ for instance in unrolled_instances:
540
+ if isinstance(instance, list):
541
+ # This means the element of the instances is a list, e.g. it was
542
+ # something like Row[1,4]
543
+ # In that case the expectation is, that further elements in the
544
+ # unrolled instances list will be expanded under it.
545
+ # We will only expand the first layer, and come back later, e.g.
546
+ # instances are ['Row[1,2]', 'Pos[1,3]']. In that case we will
547
+ # add Row1 and Row2 branches under the current node and duplicate
548
+ # the subtree, and we set instances property of the new "RowX"
549
+ # branches to 'Pos[1,3]'
550
+ # The PreOrderIter will pass the newly created "RowX" instances
551
+ # next so they will be expanded
552
+
553
+ for element in instance:
554
+ instantiated_branch = create_instantiated_branch(
555
+ element, tree_node, nodes_to_expand)
556
+
557
+ if len(unrolled_instances) > 1:
558
+ # We need to expand more (see above). PreOrderIter
559
+ # allows us to do this without recursing
560
+ instantiated_branch.instances = unrolled_instances[1:]
561
+ # break outer for loop,
562
+ break
563
+
564
+ else:
565
+ # in this case our instances have been a simple list of elements
566
+ # (as opposed to a list of lists), e.g. ['Left', 'Right'], so we
567
+ # just add them all in parallel as childs of the current
568
+ # loop
569
+ create_instantiated_branch(
570
+ instance, tree_node, nodes_to_expand)
571
+
572
+ # As instance expansions moves signals in the tree, we need to recreate
573
+ # UUIDs
574
+ create_tree_uuids(tree)
575
+
576
+
577
+ #
578
+ # Take the flat model created by _load() and merge all $prefix$ with its name
579
+ # I.e: $prefix$ = "Cabin.Doors.1"
580
+ # name = "Window.Pos"
581
+ # -> name = "Cabin.Doors.1.Window.Pos"
582
+ #
583
+ # $prefix$ is deleted
584
+ #
585
+ #
586
+ def create_absolute_paths(flat_model):
587
+ for elem in flat_model:
588
+ # Create a list of path components to the given element
589
+ #
590
+ # $prefix$='body.door.front.left' name='lock' ->
591
+ # [ 'body', 'door', 'front', 'left', 'lock' ]
592
+ name = elem['$name$']
593
+
594
+ if elem["$prefix$"] == "":
595
+ new_name = name
596
+ else:
597
+ new_name = "{}.{}".format(elem["$prefix$"], name)
598
+
599
+ elem['$name$'] = new_name
600
+ del elem["$prefix$"]
601
+
602
+ return flat_model
603
+
604
+
605
+ #
606
+ # Take the flat model with absolute signal names parsed from the vspec
607
+ # file and create a nested variant where each component of a prefix
608
+ # becomes a branch.
609
+ #
610
+
611
+ def create_nested_model(flat_model, file_name):
612
+ deep_model = {
613
+ "children": {},
614
+ "type": "branch",
615
+ "$file_name$": file_name,
616
+ "$line$": 0
617
+ }
618
+
619
+ # Traverse the flat list of the parsed specification
620
+ for elem in flat_model:
621
+ # Create children for branch type objects
622
+ if elem["type"] in nestable_types:
623
+ elem["children"] = {}
624
+
625
+ # Create a list of path components to the given element
626
+ # name='body.door.front.left.lock' ->
627
+ # [ 'body', 'door', 'front', 'left', 'lock' ]
628
+ name_list = elem['$name$'].split(".")
629
+
630
+ # Extract name
631
+ name = name_list[-1]
632
+
633
+ # Locate the correct branch in the tree
634
+ parent_branch = find_branch_or_struct(deep_model, name_list[:-1], 0)
635
+
636
+ # If an element with name is already in the parent branch
637
+ # we update its fields with the fields from the new element
638
+ if name in parent_branch["children"]:
639
+ old_elem = parent_branch["children"][name]
640
+ # never update the type
641
+ elem.pop("type", None)
642
+ # concatenate file names
643
+ fname = "{}:{}".format(
644
+ old_elem["$file_name$"], elem["$file_name$"])
645
+ old_elem.update(elem)
646
+ old_elem["$file_name$"] = fname
647
+ else:
648
+ parent_branch["children"][name] = elem
649
+
650
+ return deep_model
651
+
652
+
653
+ # Find the given prefix somewhere under the tree rooted in elem.
654
+ # If the branch referred to by prefix is not found, create it implicitly
655
+ # if autocreate is True. Note that structs are not auto-created.
656
+ def find_branch_or_struct(elem, name_list, index, autocreate=True):
657
+ # Have we reached the end of the name list
658
+ if len(name_list) == index:
659
+ if (elem['type'] not in nestable_types):
660
+ raise VSpecError(
661
+ elem.get(
662
+ "$file_name$", "??"), elem.get(
663
+ "$line$", "??"), "Not a branch or struct: {}.".format(
664
+ elem['$name$']))
665
+
666
+ return elem
667
+
668
+ if (elem['type'] not in nestable_types):
669
+ raise VSpecError(elem.get("$file_name$", "??"),
670
+ elem.get("$line$", "??"),
671
+ "{} is not a branch or struct.".format(list_to_path(name_list[:index])))
672
+
673
+ children = elem["children"]
674
+
675
+ if name_list[index] not in children:
676
+ if autocreate and elem['type'] != "struct":
677
+ logging.info(f"Autocreating implicit branch {name_list[index]}")
678
+
679
+ # If we are above Vehicle (e.g. vehicle not defined), we are
680
+ # missing a name
681
+ if "$name$" not in elem:
682
+ elem['$name$'] = ""
683
+ # autopep8: off
684
+ newelem = {'type': elem['type'],
685
+ 'children': {},
686
+ '$line$': '0',
687
+ '$file_name$':
688
+ '<generated>',
689
+ '$name$': f"{elem['$name$']}.{name_list[index]}"}
690
+ # autopep8: on
691
+ children[name_list[index]] = newelem
692
+ # Search again
693
+ find_branch_or_struct(elem, name_list, index, autocreate)
694
+ else:
695
+ raise VSpecError(
696
+ elem.get(
697
+ "$file_name$", "??"), elem.get(
698
+ "$line$", "??"), "Missing branch: {} in {}.".format(
699
+ name_list[index], list_to_path(name_list)))
700
+
701
+ # Traverse all children, looking for the
702
+ # Move on to next element in prefix.
703
+ return find_branch_or_struct(
704
+ children[name_list[index]], name_list, index + 1, autocreate)
705
+
706
+
707
+ def list_to_path(name_list):
708
+ path = ""
709
+ for name in name_list:
710
+ if path == "":
711
+ path = name
712
+ else:
713
+ path = "{}.{}".format(path, name)
714
+
715
+ return path
716
+
717
+
718
+ #
719
+ # Convert
720
+ # #include door.vspec, body.door.front.left
721
+ # to
722
+ # - $include$:
723
+ # file: door.vspec
724
+ # prefix: body.door.front.left
725
+ #
726
+ # This yaml version of the include directive will
727
+ # then be further processed to actually include
728
+ # the given file.
729
+ #
730
+ def yamilify_includes(text, is_list):
731
+
732
+ # Logic below expects a new line after "#include", to support vspec files where there
733
+ # is no newline at end we add a newline here
734
+ text += '\n'
735
+
736
+ while True:
737
+ st_index = text.find("\n#include")
738
+ if st_index == -1:
739
+ return text
740
+
741
+ end_index = text.find("\n", st_index + 1)
742
+ if end_index == -1:
743
+ return text
744
+
745
+ include_arg = text[st_index + 10:end_index].split()
746
+ if len(include_arg) == 2:
747
+ [include_file, include_prefix] = include_arg
748
+ else:
749
+ include_prefix = '""'
750
+ [include_file] = include_arg
751
+
752
+ if is_list:
753
+ fmt_str = """{}
754
+
755
+ - $name$: $include$
756
+ file: {}
757
+ prefix: {}
758
+ {}"""
759
+ else:
760
+ fmt_str = """{}
761
+
762
+ $include$:
763
+ file: {}
764
+ prefix: {}
765
+ {}"""
766
+
767
+ text = fmt_str.format(
768
+ text[:st_index], include_file, include_prefix, text[end_index:])
769
+
770
+ return text
771
+
772
+
773
+ def render_tree(
774
+ tree_dict,
775
+ tree_type: VSSTreeType,
776
+ break_on_name_style_violation=False) -> VSSNode:
777
+ if len(tree_dict) != 1:
778
+ for item in tree_dict.keys():
779
+ logging.info(f"Found root node {item}")
780
+ raise Exception(
781
+ f"Invalid VSS model, must have single root node, found {len(tree_dict)}")
782
+
783
+ root_element_name = next(iter(tree_dict.keys()))
784
+ root_element = tree_dict[root_element_name]
785
+ if root_element['type'] != 'branch':
786
+ raise Exception(
787
+ f"Invalid VSS model, root must be branch, found {root_element_name} of type {root_element['type']}")
788
+
789
+ tree_root = VSSNode(
790
+ root_element_name,
791
+ root_element,
792
+ tree_type.available_types(),
793
+ break_on_name_style_violation=break_on_name_style_violation)
794
+
795
+ if "children" in root_element.keys():
796
+ child_nodes = root_element["children"]
797
+ render_subtree(
798
+ child_nodes,
799
+ tree_type,
800
+ tree_root,
801
+ break_on_name_style_violation=break_on_name_style_violation)
802
+
803
+ create_tree_uuids(tree_root)
804
+ return tree_root
805
+
806
+
807
+ def render_subtree(
808
+ subtree,
809
+ tree_type: VSSTreeType,
810
+ parent,
811
+ break_on_name_style_violation=False):
812
+ for element_name in subtree:
813
+ current_element = subtree[element_name]
814
+
815
+ try:
816
+ new_element = VSSNode(
817
+ element_name,
818
+ current_element,
819
+ tree_type.available_types(),
820
+ parent=parent,
821
+ break_on_name_style_violation=break_on_name_style_violation)
822
+ except IncompleteElementException as e:
823
+ logging.error(f"Invalid VSS: {e}")
824
+ logging.error("Terminating.")
825
+ sys.exit(-1)
826
+ if "children" in current_element.keys():
827
+ child_nodes = current_element["children"]
828
+ render_subtree(
829
+ child_nodes,
830
+ tree_type,
831
+ new_element,
832
+ break_on_name_style_violation=break_on_name_style_violation)
833
+
834
+
835
+ def merge_elem(base, overlay_element):
836
+ r = Resolver()
837
+ element_name = "/" + overlay_element.qualified_name("/")
838
+
839
+ if not VSSNode.node_exists(base, element_name):
840
+ # The node in the overlay does not exist, so we connect it
841
+ # print(f"Not exists {overlay_element.qualified_name()} does not exist, creating.")
842
+ new_parent_name = "/" + overlay_element.parent.qualified_name("/")
843
+ new_parent = r.get(base, new_parent_name)
844
+ overlay_element.parent = new_parent
845
+
846
+ else:
847
+ # else we merge the node. The merge function of VSSNode is not recursive
848
+ # so children in base will not be overwritten
849
+ # print(f"Merging {overlay_element.qualified_name()}")
850
+ other_node: VSSNode = r.get(base, element_name)
851
+ try:
852
+ other_node.merge(overlay_element)
853
+ except ImpossibleMergeException as e:
854
+ logging.error(f"Merging impossible: {e}")
855
+ sys.exit(-1)
856
+
857
+
858
+ def merge_tree(base: VSSNode, overlay: VSSNode):
859
+ overlay_element: VSSNode
860
+ for overlay_element in LevelOrderIter(overlay):
861
+ merge_elem(base, overlay_element)
862
+
863
+
864
+ def create_tree_uuids(root: VSSNode):
865
+ VSS_NAMESPACE = "vehicle_signal_specification"
866
+ namespace_uuid = uuid.uuid5(uuid.NAMESPACE_OID, VSS_NAMESPACE)
867
+ vss_element: VSSNode
868
+ for vss_element in PreOrderIter(root):
869
+ vss_element.uuid = uuid.uuid5(
870
+ namespace_uuid, vss_element.qualified_name()).hex
871
+
872
+
873
+ def load_quantities(vspec_file: str, quantity_files: List[str]):
874
+ """
875
+ Reset the list of quantities defined (if any)
876
+ and add new quantities
877
+ """
878
+ VSSQuantityCollection.reset_quantities()
879
+
880
+ total_nbr_quantities = 0
881
+ if not quantity_files:
882
+ # Search for a file quantities.yaml in same directory as vspec file
883
+ vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
884
+ default_vss_quantity_file = vspec_dir + os.path.sep + 'quantities.yaml'
885
+ if os.path.exists(default_vss_quantity_file):
886
+ total_nbr_quantities = VSSQuantityCollection.load_config_file(default_vss_quantity_file)
887
+ logging.info(f"Added {total_nbr_quantities} quantities from {default_vss_quantity_file}")
888
+ else:
889
+ for quantity_file in quantity_files:
890
+ nbr_quantities = VSSQuantityCollection.load_config_file(quantity_file)
891
+ if (nbr_quantities == 0):
892
+ logging.warning(f"Warning: No quantities found in {quantity_file}")
893
+ else:
894
+ logging.info(f"Added {nbr_quantities} quantities from {quantity_file}")
895
+ total_nbr_quantities += nbr_quantities
896
+
897
+ if (total_nbr_quantities == 0):
898
+ logging.info("No quantities defined!")
899
+
900
+
901
+ def load_units(vspec_file: str, unit_files: List[str]):
902
+ """
903
+ Reset the list of units defined (if any)
904
+ and add new units
905
+ """
906
+ VSSUnitCollection.reset_units()
907
+
908
+ total_nbr_units = 0
909
+ if not unit_files:
910
+ # Search for a file units.yaml in same directory as vspec file
911
+ vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
912
+ default_vss_unit_file = vspec_dir + os.path.sep + 'units.yaml'
913
+ if os.path.exists(default_vss_unit_file):
914
+ total_nbr_units = VSSUnitCollection.load_config_file(default_vss_unit_file)
915
+ logging.info(f"Added {total_nbr_units} units from {default_vss_unit_file}")
916
+ else:
917
+ for unit_file in unit_files:
918
+ nbr_units = VSSUnitCollection.load_config_file(unit_file)
919
+ if (nbr_units == 0):
920
+ logging.warning(f"Warning: No units found in {unit_file}")
921
+ else:
922
+ logging.info(f"Added {nbr_units} units from {unit_file}")
923
+ total_nbr_units += nbr_units
924
+
925
+ if (total_nbr_units == 0):
926
+ logging.error("No units defined! Terminating!")
927
+ sys.exit(-1)
928
+
929
+
930
+ def check_data_type_references(tree: VSSNode):
931
+ """
932
+ Check that the data type names referenced by tree nodes exist in the tree.
933
+ """
934
+ errors: List[str] = []
935
+ check_data_type_references_recursive(
936
+ tree, [
937
+ str(attr) for attr in VSSNode.get_tree_attrs(
938
+ tree, lambda n: n.qualified_name(), lambda n: n.is_struct())], errors)
939
+ if len(errors) > 0:
940
+ error_str = ",".join(errors)
941
+ logging.error(
942
+ f"{len(errors)} data type reference errors detected. Errors: {error_str}")
943
+ sys.exit(-1)
944
+
945
+
946
+ def check_data_type_references_recursive(
947
+ node: VSSNode, data_type_qualified_names: List[str], errors: List[str]):
948
+ """
949
+ Check that the data type names referenced by tree nodes exist in the tree.
950
+ """
951
+ if node.is_property() or node.is_signal():
952
+ if node.datatype is None:
953
+ undecorated_data_type = node.data_type_str.replace('[]', '')
954
+ if undecorated_data_type not in data_type_qualified_names:
955
+ errors.append(
956
+ f"Data Type reference invalid. Node Name: {node.name}. "
957
+ f"Node Qualified Name: {node.qualified_name()}. "
958
+ f"Data Type: {undecorated_data_type}")
959
+
960
+ separator = "."
961
+ # is the property circularly refereing to the struct in which it is defined?
962
+ member_of = separator.join(node.qualified_name(separator).split(separator)[:-1])
963
+ if member_of == undecorated_data_type:
964
+ errors.append(
965
+ "Circular reference detected in data type. "
966
+ f"Data Type: {member_of}. Property: {node.name}")
967
+
968
+ for n in node.children:
969
+ check_data_type_references_recursive(
970
+ n, data_type_qualified_names, errors)
971
+
972
+
973
+ def check_data_type_references_across_trees(
974
+ signal_root: VSSNode, data_type_root: Optional[VSSNode]):
975
+ """
976
+ Check that the data type names referenced by nodes in the signal tree are present in the data types tree.
977
+ """
978
+ nonexistant_types = []
979
+ for _, _, node in RenderTree(signal_root):
980
+ # signals with user-defined data types
981
+ if node.is_signal() and node.datatype is None:
982
+ if data_type_root is None or (not node.does_attribute_exist(
983
+ data_type_root,
984
+ lambda n: n.base_data_type_str(),
985
+ lambda n: n.qualified_name(),
986
+ lambda n: n.is_struct())):
987
+ nonexistant_types.append(f'{node.data_type_str}')
988
+
989
+ if len(nonexistant_types) > 0:
990
+ error_str = ", ".join(nonexistant_types)
991
+ logging.error(
992
+ f"Following types were referenced in signals but have not been defined: {error_str}")
993
+ sys.exit(-1)
994
+
995
+
996
+ load_flat_model.include_index = 1 # type: ignore[attr-defined]