| # 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 |