Skip to content

Commit 4313545

Browse files
committed
Update unit handling
This refactors unit handling so that it keeps supporting old format but also supports format in COVESA/vehicle_signal_specification#669. It only focus on keeping existing functionality. No functionality for parsing and verifying domains added. Signed-off-by: Erik Jaegervall <[email protected]>
1 parent 6bc5a12 commit 4313545

File tree

8 files changed

+132
-114
lines changed

8 files changed

+132
-114
lines changed

tests/binary/test_binary.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@ def test_binary(change_test_dir):
6363
check_expected('A.String', 'Node type=SENSOR')
6464
check_expected('A.Int', 'Node type=ACTUATOR')
6565

66-
os.system("rm -f test.binary ctestparser out.txt ../../binary/go_parser/gotestparser")
66+
os.system("rm -f test.binary ctestparser out.txt")
67+
os.system("rm -f ../../binary/go_parser/gotestparser ../../binary/go_parser/out.txt")

tests/model/explicit_units.yaml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
units:
2-
puncheon:
3-
label: Puncheon
4-
description: Volume measure in puncheons (1 puncheon = 318 liters)
5-
domain: volume
6-
hogshead:
7-
label: Hogshead
8-
description: Volume measure in hogsheads (1 hogshead = 238 liters)
9-
domain: volume
1+
puncheon:
2+
definition: Volume measure in puncheons (1 puncheon = 318 liters)
3+
unit: Puncheon
4+
quantity: volume
5+
hogshead:
6+
definition: Volume measure in hogsheads (1 hogshead = 238 liters)
7+
unit: Hogshead
8+
quantity: volume
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
units:
2+
puncheon:
3+
label: Puncheon
4+
description: Volume measure in puncheons (1 puncheon = 318 liters)
5+
domain: volume
6+
hogshead:
7+
label: Hogshead
8+
description: Volume measure in hogsheads (1 hogshead = 238 liters)
9+
domain: volume

tests/model/test_contants.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
# Copyright (c) 2020 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+
19
import pytest
210
import os
311

4-
from vspec.model.constants import VSSType, VSSDataType, Unit, StringStyle, VSSTreeType, VSSConstant
12+
from vspec.model.constants import VSSType, VSSDataType, VSSUnitCollection, StringStyle, VSSTreeType, VSSUnit
513

614

715
@pytest.mark.parametrize("style_enum, style_str",
@@ -30,19 +38,26 @@ def test_invalid_string_styles():
3038
StringStyle.from_str("not_a_valid_case")
3139

3240

33-
def test_manually_loaded_units():
41+
@pytest.mark.parametrize("unit_file",
42+
['explicit_units.yaml',
43+
'explicit_units_old_syntax.yaml'])
44+
def test_manually_loaded_units(unit_file):
3445
"""
3546
Test correct parsing of units
3647
"""
37-
unit_file = os.path.join(os.path.dirname(__file__), 'explicit_units.yaml')
38-
Unit.load_config_file(unit_file)
39-
assert Unit.PUNCHEON == Unit.from_str("puncheon")
40-
assert Unit.HOGSHEAD == Unit.from_str("hogshead")
48+
unit_file = os.path.join(os.path.dirname(__file__), unit_file)
49+
VSSUnitCollection.load_config_file(unit_file)
50+
assert VSSUnitCollection.get_unit("puncheon") == "puncheon"
51+
assert VSSUnitCollection.get_unit("puncheon").definition == \
52+
"Volume measure in puncheons (1 puncheon = 318 liters)"
53+
assert VSSUnitCollection.get_unit("puncheon").unit == \
54+
"Puncheon"
55+
assert VSSUnitCollection.get_unit("puncheon").quantity == \
56+
"volume"
4157

4258

4359
def test_invalid_unit():
44-
with pytest.raises(Exception):
45-
Unit.from_str("not_a_valid_case")
60+
assert VSSUnitCollection.get_unit("unknown") is None
4661

4762

4863
@pytest.mark.parametrize("type_enum,type_str",
@@ -108,12 +123,12 @@ def test_invalid_vss_tree_types():
108123
VSSDataType.from_str("not_a_valid_case")
109124

110125

111-
def test_vss_constants():
112-
""" Test VSSConstant class """
113-
item = VSSConstant("mylabel", "myvalue", "mydescription", "mydomain")
114-
assert item.value == "myvalue"
115-
assert item.label == "mylabel"
116-
assert item.description == "mydescription"
117-
assert item.domain == "mydomain"
118-
# String subclass so just comparing shall get "value"
119-
assert item == "myvalue"
126+
def test_unit():
127+
""" Test Unit class """
128+
item = VSSUnit("myid", "myunit", "mydefinition", "myquantity")
129+
assert item.value == "myid"
130+
assert item.unit == "myunit"
131+
assert item.definition == "mydefinition"
132+
assert item.quantity == "myquantity"
133+
# String subclass so just comparing shall get "myid"
134+
assert item == "myid"

tests/model/test_vsstree.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
# Copyright (c) 2020 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+
19
import unittest
210
import os
311

4-
from vspec.model.constants import VSSType, VSSDataType, Unit, VSSTreeType
12+
from vspec.model.constants import VSSType, VSSDataType, VSSUnitCollection, VSSTreeType
513
from vspec.model.vsstree import VSSNode
614

715

@@ -35,7 +43,7 @@ def test_complex_construction(self):
3543
"aggregate": False,
3644
"default": "test-default", "$file_name$": "testfile"}
3745
unit_file = os.path.join(os.path.dirname(__file__), 'explicit_units.yaml')
38-
Unit.load_config_file(unit_file)
46+
VSSUnitCollection.load_config_file(unit_file)
3947
node = VSSNode(
4048
"test",
4149
source,
@@ -45,7 +53,7 @@ def test_complex_construction(self):
4553
self.assertEqual(VSSType.SENSOR, node.type)
4654
self.assertEqual("26d6e362-a422-11ea-bb37-0242ac130002", node.uuid)
4755
self.assertEqual(VSSDataType.UINT8, node.datatype)
48-
self.assertEqual(Unit.HOGSHEAD, node.unit)
56+
self.assertEqual(VSSUnitCollection.get_unit("hogshead"), node.unit)
4957
self.assertEqual(0, node.min)
5058
self.assertEqual(100, node.max)
5159
self.assertEqual(["one", "two"], node.allowed)
@@ -71,7 +79,7 @@ def test_merge_nodes(self):
7179
"datatype": "uint8", "unit": "hogshead", "min": 0, "max": 100, "$file_name$": "testfile"}
7280

7381
unit_file = os.path.join(os.path.dirname(__file__), 'explicit_units.yaml')
74-
Unit.load_config_file(unit_file)
82+
VSSUnitCollection.load_config_file(unit_file)
7583

7684
node_target = VSSNode(
7785
"MyNode",
@@ -95,7 +103,7 @@ def test_merge_nodes(self):
95103
node_target.uuid)
96104
self.assertTrue(node_target.has_datatype())
97105
self.assertEqual(VSSDataType.UINT8, node_target.datatype)
98-
self.assertEqual(Unit.HOGSHEAD, node_target.unit)
106+
self.assertEqual(VSSUnitCollection.get_unit("hogshead"), node_target.unit)
99107
self.assertEqual(0, node_target.min)
100108
self.assertEqual(100, node_target.max)
101109

vspec/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from .model.vsstree import VSSNode
2525
from .model.exceptions import ImpossibleMergeException, IncompleteElementException
26-
from .model.constants import VSSTreeType, Unit
26+
from .model.constants import VSSTreeType, VSSUnitCollection
2727

2828
nestable_types = set(["branch", "struct"])
2929

@@ -871,11 +871,11 @@ def load_units(vspec_file: str, unit_files: List[str]):
871871
vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
872872
default_vss_unit_file = vspec_dir + os.path.sep + 'units.yaml'
873873
if os.path.exists(default_vss_unit_file):
874-
total_nbr_units = Unit.load_config_file(default_vss_unit_file)
874+
total_nbr_units = VSSUnitCollection.load_config_file(default_vss_unit_file)
875875
logging.info(f"Added {total_nbr_units} units from {default_vss_unit_file}")
876876
else:
877877
for unit_file in unit_files:
878-
nbr_units = Unit.load_config_file(unit_file)
878+
nbr_units = VSSUnitCollection.load_config_file(unit_file)
879879
if (nbr_units == 0):
880880
logging.warning(f"Warning: No units found in {unit_file}")
881881
else:

vspec/model/constants.py

Lines changed: 61 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
#
1414
# noinspection PyPackageRequirements
1515
import re
16+
import logging
17+
import sys
1618
from enum import Enum, EnumMeta
1719
from typing import (
18-
Sequence, Type, TypeVar, Optional, Dict, Tuple, Iterator, TextIO
20+
Sequence, Type, TypeVar, Optional, Dict, TextIO
1921
)
2022

2123
import yaml
@@ -26,80 +28,28 @@
2628
T = TypeVar("T")
2729

2830

29-
class VSSConstant(str):
30-
"""String subclass that can tag it with description and domain.
31+
class VSSUnit(str):
32+
"""String subclass for storing unit information.
3133
"""
32-
label: str
33-
description: Optional[str] = None
34-
domain: Optional[str] = None
35-
36-
def __new__(cls, label: str, value: str, description: str = "", domain: str = "") -> 'VSSConstant':
37-
self = super().__new__(cls, value)
38-
self.label = label
39-
self.description = description
40-
self.domain = domain
34+
id: str # Typically abbreviation like "V"
35+
unit: Optional[str] = None # Typically full name like "Volt"
36+
definition: Optional[str] = None
37+
quantity: Optional[str] = None # Typically quantity, like "Voltage"
38+
39+
def __new__(cls, id: str, unit: Optional[str] = None, definition: Optional[str] = None,
40+
quantity: Optional[str] = None) -> 'VSSUnit':
41+
self = super().__new__(cls, id)
42+
self.id = id
43+
self.unit = unit
44+
self.definition = definition
45+
self.quantity = quantity
4146
return self
4247

4348
@property
4449
def value(self):
4550
return self
4651

4752

48-
def dict_to_constant_config(name: str, info: Dict[str, str]) -> Tuple[str, VSSConstant]:
49-
label = info['label']
50-
label = NON_ALPHANUMERIC_WORD.sub('', label).upper()
51-
description = info.get('description', '')
52-
domain = info.get('domain', '')
53-
return label, VSSConstant(info['label'], name, description, domain)
54-
55-
56-
def iterate_config_members(config: Dict[str, Dict[str, str]]) -> Iterator[Tuple[str, VSSConstant]]:
57-
for u, v in config.items():
58-
yield dict_to_constant_config(u, v)
59-
60-
61-
class VSSRepositoryMeta(type):
62-
"""This class defines the enumeration behavior for vss:
63-
- Access through Class.ATTRIBUTE
64-
- Class.add_config(Dict[str, Dict[str, str]]): Adds values from file
65-
- from_str(str): reverse lookup
66-
- values(): sequence of values
67-
"""
68-
69-
def __new__(mcs, cls, bases, classdict):
70-
cls = super().__new__(mcs, cls, bases, classdict)
71-
72-
if not hasattr(cls, '__reverse_lookup__'):
73-
cls.__reverse_lookup__ = {
74-
v.value: v for v in cls.__members__.values()
75-
}
76-
if not hasattr(cls, '__values__'):
77-
cls.__values__ = list(cls.__reverse_lookup__.keys())
78-
79-
return cls
80-
81-
def __getattr__(cls, key: str) -> str:
82-
try:
83-
return cls.__members__[key] # type: ignore[index]
84-
except KeyError as e:
85-
raise AttributeError(
86-
f"type object '{cls.__name__}' has no attribute '{key}'"
87-
) from e
88-
89-
def add_config(cls, config: Dict[str, Dict[str, str]]):
90-
for k, v in iterate_config_members(config):
91-
if v.value not in cls.__reverse_lookup__ and k not in cls.__members__:
92-
cls.__members__[k] = v # type: ignore[index]
93-
cls.__reverse_lookup__[v.value] = v # type: ignore[index]
94-
cls.__values__.append(v.value) # type: ignore[attr-defined]
95-
96-
def from_str(cls: Type[T], value: str) -> T:
97-
return cls.__reverse_lookup__[value] # type: ignore[attr-defined]
98-
99-
def values(cls: Type[T]) -> Sequence[str]:
100-
return cls.__values__ # type: ignore[attr-defined]
101-
102-
10353
class EnumMetaWithReverseLookup(EnumMeta):
10454
"""This class extends EnumMeta and adds:
10555
- from_str(str): reverse lookup
@@ -175,24 +125,61 @@ class VSSDataType(Enum, metaclass=EnumMetaWithReverseLookup):
175125
STRING_ARRAY = "string[]"
176126

177127

178-
class Unit(metaclass=VSSRepositoryMeta):
179-
__members__: Dict[str, str] = dict()
128+
class VSSUnitCollection():
129+
units: Dict[str, VSSUnit] = dict()
180130

181131
@staticmethod
182132
def get_config_dict(yaml_file: TextIO, key: str) -> Dict[str, Dict[str, str]]:
183133
yaml_config = yaml.safe_load(yaml_file)
184-
configs = yaml_config.get(key, {})
134+
if (len(yaml_config) == 1) and (key in yaml_config):
135+
# Old style unit file
136+
configs = yaml_config.get(key, {})
137+
else:
138+
# New style unit file
139+
configs = yaml_config
185140
return configs
186141

187-
@staticmethod
188-
def load_config_file(config_file: str) -> int:
142+
@classmethod
143+
def load_config_file(cls, config_file: str) -> int:
189144
added_configs = 0
190145
with open(config_file) as my_yaml_file:
191-
my_units = Unit.get_config_dict(my_yaml_file, 'units')
146+
my_units = cls.get_config_dict(my_yaml_file, 'units')
192147
added_configs = len(my_units)
193-
Unit.add_config(my_units)
148+
for k, v in my_units.items():
149+
unit = k
150+
if "unit" in v:
151+
unit = v["unit"]
152+
elif "label" in v:
153+
# Old syntax
154+
unit = v["label"]
155+
definition = None
156+
if "definition" in v:
157+
definition = v["definition"]
158+
elif "description" in v:
159+
# Old syntax
160+
definition = v["description"]
161+
162+
quantity = None
163+
if "quantity" in v:
164+
quantity = v["quantity"]
165+
elif "domain" in v:
166+
# Old syntax
167+
quantity = v["domain"]
168+
else:
169+
logging.error("No quantity (domain) found for unit %s", k)
170+
sys.exit(-1)
171+
172+
unit_node = VSSUnit(k, unit, definition, quantity)
173+
cls.units[k] = unit_node
194174
return added_configs
195175

176+
@classmethod
177+
def get_unit(cls, id: str) -> Optional[VSSUnit]:
178+
if id in cls.units:
179+
return cls.units[id]
180+
else:
181+
return None
182+
196183

197184
class VSSTreeType(Enum, metaclass=EnumMetaWithReverseLookup):
198185
SIGNAL_TREE = "signal_tree"

vspec/model/vsstree.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# SPDX-License-Identifier: MPL-2.0
1010

1111
from anytree import Node, Resolver, ChildResolverError, RenderTree # type: ignore[import]
12-
from .constants import VSSType, VSSDataType, Unit, VSSConstant
12+
from .constants import VSSType, VSSDataType, VSSUnitCollection, VSSUnit
1313
from .exceptions import NameStyleValidationException, \
1414
ImpossibleMergeException, IncompleteElementException
1515
from typing import Any, Optional, Set, List
@@ -46,7 +46,7 @@ class VSSNode(Node):
4646
# neither in core or extended,
4747
whitelisted_extended_attributes: List[str] = []
4848

49-
unit: Optional[VSSConstant]
49+
unit: Optional[VSSUnit]
5050

5151
min = ""
5252
max = ""
@@ -149,9 +149,8 @@ def extractCoreAttribute(name: str):
149149
sys.exit(-1)
150150

151151
unit = self.source_dict["unit"]
152-
try:
153-
self.unit = Unit.from_str(unit)
154-
except KeyError:
152+
self.unit = VSSUnitCollection.get_unit(unit)
153+
if self.unit is None:
155154
logging.error(f"Unknown unit {unit} for signal {self.qualified_name()}. Terminating.")
156155
sys.exit(-1)
157156

0 commit comments

Comments
 (0)