Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
InstrumentSettings.py 5.83 KiB
from __future__ import (absolute_import, division, print_function)

import warnings


# Have to patch warnings at runtime to not print the source code. This is even advertised as a 'feature' of
# the warnings library in the documentation: https://docs.python.org/3/library/warnings.html#warnings.showwarning
def warning_no_source(msg, *ignored):
    return str(msg) + '\n'

warnings.formatwarning = warning_no_source
warnings.simplefilter('always', UserWarning)


class InstrumentSettings(object):
    # Holds instance variables updated at runtime
    def __init__(self, attr_mapping, adv_conf_dict=None, basic_conf_dict=None, kwargs=None):
        self._attr_mapping = attr_mapping
        self._adv_config_dict = adv_conf_dict
        self._basic_conf_dict = basic_conf_dict
        self._kwargs = kwargs

        self._unknown_keys_found = False
        self._parse_attributes(dict_to_parse=adv_conf_dict)
        self._parse_attributes(dict_to_parse=basic_conf_dict)
        self._parse_attributes(dict_to_parse=kwargs)
        if self._unknown_keys_found:
            _print_known_keys(attr_mapping)

    def __getattr__(self, item):
        map_entry = next((attr_tuple for attr_tuple in self._attr_mapping if item == attr_tuple[-1]), None)
        if map_entry:
            # User forgot to enter the param:
            raise AttributeError("The parameter with name: '" + str(map_entry[0]) + "' is required but was not set or "
                                 "passed.\nPlease set this configuration option and try again")
        else:
            # If you have got here from a grep or something similar this error message means the line caller
            # has asked for a class attribute which does not exist. These attributes are set in a mapping file which
            # is passed in whilst InstrumentSettings is being constructed. Check that the 'script name' (i.e. not user
            # friendly name) is typed correctly in both the script(s) and mapping file.
            raise AttributeError("The attribute in the script with name " + str(item) + " is unknown to the mapping."
                                 "\nPlease contact the development team.")

    def check_expected_attributes_are_set(self, expected_attr_names):
        for expected_attr in expected_attr_names:
            if not [attr_entry for attr_entry in self._attr_mapping if expected_attr == attr_entry[-1]]:
                raise ValueError("Expected attribute '" + str(expected_attr) + "' is unknown to attribute mapping")

        # Filter down the full mapping list
        found_tuple_list = [tuple_entry for tuple_entry in self._attr_mapping if tuple_entry[-1] in expected_attr_names]
        expected_params_dict = dict(found_tuple_list)
        self._check_attribute_is_set(expected_params_dict)

    def update_attributes(self, advanced_config=None, basic_config=None, kwargs=None, suppress_warnings=False):
        self._adv_config_dict = advanced_config if advanced_config else self._adv_config_dict
        self._basic_conf_dict = basic_config if basic_config else self._basic_conf_dict
        self._kwargs = kwargs if kwargs else self._kwargs

        has_known_keys_already_been_printed = self._unknown_keys_found
        # Only update if one in hierarchy below it has been updated
        if advanced_config:
            self._parse_attributes(self._adv_config_dict, suppress_warnings=suppress_warnings)
        if advanced_config or basic_config:
            self._parse_attributes(self._basic_conf_dict,
                                   suppress_warnings=(not bool(basic_config or suppress_warnings)))
        if advanced_config or basic_config or kwargs:
            self._parse_attributes(self._kwargs, suppress_warnings=(not bool(kwargs or suppress_warnings)))

        if not has_known_keys_already_been_printed and self._unknown_keys_found:
            _print_known_keys(self._attr_mapping)

    def _parse_attributes(self, dict_to_parse, suppress_warnings=False):
        if not dict_to_parse:
            return

        for config_key in dict_to_parse:
            # Recurse down all dictionaries
            if isinstance(dict_to_parse[config_key], dict):
                self._parse_attributes(dict_to_parse[config_key])
                continue  # Skip so we don't accidentally re-add this dictionary

            # Update attributes from said dictionary
            found_attribute = next((attr_tuple for attr_tuple in self._attr_mapping
                                    if config_key == attr_tuple[0]), None)
            if found_attribute:
                # The first element of the attribute is the config name and the last element is the friendly name
                self._update_attribute(attr_name=found_attribute[-1], attr_val=dict_to_parse[found_attribute[0]],
                                       friendly_name=found_attribute[0], suppress_warnings=suppress_warnings)
            elif not suppress_warnings:
                warnings.warn("Ignoring unknown configuration key: " + str(config_key))
                self._unknown_keys_found = True
                continue

    def _update_attribute(self, attr_name, attr_val, friendly_name, suppress_warnings):
        # Does the attribute exist - has it changed and are we suppressing warnings
        if hasattr(self, attr_name) and getattr(self, attr_name) != attr_val and not suppress_warnings:
            warnings.warn("Replacing parameter: '" + str(friendly_name) + "' which was previously set to: '" +
                          str(getattr(self, attr_name)) + "' with new value: '" + str(attr_val) + "'")
        setattr(self, attr_name, attr_val)


def _print_known_keys(master_mapping):
    print ("\nKnown keys are:")
    print("----------------------------------")
    sorted_attributes = sorted(master_mapping, key=lambda tup: tup[0])
    for tuple_entry in sorted_attributes:
        print (tuple_entry[0] + ', ', end="")
    print("\n----------------------------------")