blob: db72c2356923e914f052a397f098b89dbccb68e8 [file] [log] [blame]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
'''Code to help make typed objects out of parsed YAML / json / hjson'''
from typing import Callable, Dict, List, Optional, Sequence, TypeVar
import yaml
try:
from yaml import CSafeLoader as YamlLoader
except ImportError:
from yaml import SafeLoader as YamlLoader # type: ignore
T = TypeVar('T')
def check_keys(obj: object,
what: str,
required_keys: List[str],
optional_keys: List[str]) -> Dict[str, object]:
'''Check that obj is a dict object with the expected keys
If not, raise a ValueError; the what argument names the object.
'''
if not isinstance(obj, dict):
raise ValueError("{} is expected to be a dict, but was actually a {}."
.format(what, type(obj).__name__))
allowed = set()
missing = []
for key in required_keys:
assert key not in allowed
allowed.add(key)
if key not in obj:
missing.append(key)
for key in optional_keys:
assert key not in allowed
allowed.add(key)
unexpected = []
for key in obj:
if key not in allowed:
unexpected.append(key)
if missing or unexpected:
mstr = ('The following required fields were missing: {}.'
.format(', '.join(missing)) if missing else '')
ustr = ('The following unexpected fields were found: {}.'
.format(', '.join(unexpected)) if unexpected else '')
raise ValueError("{} doesn't have the right keys. {}{}{}"
.format(what,
mstr,
' ' if mstr and ustr else '',
ustr))
return obj
def check_str(obj: object, what: str) -> str:
'''Check that the given object is a string
If not, raise a ValueError; the what argument names the object.
'''
if not isinstance(obj, str):
raise ValueError('{} is of type {}, not a string.'
.format(what, type(obj).__name__))
return obj
def check_optional_str(obj: object, what: str) -> Optional[str]:
'''Check that the given object is a string or None
If not, raise a ValueError; the what argument names the object.
'''
if obj is not None and not isinstance(obj, str):
raise ValueError('{} is of type {}, not a string.'
.format(what, type(obj).__name__))
return obj
def check_bool(obj: object, what: str) -> bool:
'''Check that the given object is a bool
If not, raise a ValueError; the what argument names the object.
'''
if obj is not True and obj is not False:
raise ValueError('{} is of type {}, not a bool.'
.format(what, type(obj).__name__))
return obj
def check_int(obj: object, what: str) -> int:
'''Check that the given object is an integer
If not, raise a ValueError; the what argument names the object.
'''
if not isinstance(obj, int):
raise ValueError('{} is of type {}, not an integer.'
.format(what, type(obj).__name__))
return obj
def check_list(obj: object, what: str) -> List[object]:
'''Check that the given object is a list
If not, raise a ValueError; the what argument names the object.
'''
if not isinstance(obj, list):
raise ValueError('{} is of type {}, not a list.'
.format(what, type(obj).__name__))
return obj
def index_list(what: str,
objs: Sequence[T],
get_key: Callable[[T], str]) -> Dict[str, T]:
ret = {}
for obj in objs:
key = get_key(obj)
if key in ret:
raise ValueError('Duplicate object with key {} in {}.'
.format(key, what))
ret[key] = obj
return ret
def get_optional_str(data: Dict[str, object],
key: str, what: str) -> Optional[str]:
return check_optional_str(data.get(key), '{} field for {}'.format(key, what))
def load_yaml(path: str, what: Optional[str]) -> object:
'''Load a YAML file at path.
If there is no such file, or the file is not well-formed YAML, this raises
a RuntimeError. If what is not None, it will be used in the error message.
'''
for_msg = ' for ' + what if what is not None else ''
try:
with open(path, 'r') as handle:
return yaml.load(handle, Loader=YamlLoader)
except FileNotFoundError:
raise RuntimeError('Cannot find YAML file{} at {!r}.'
.format(for_msg, path)) from None
except yaml.YAMLError as err:
raise RuntimeError('Failed to parse YAML file{} at {!r}: {}'
.format(for_msg, path, err)) from None