Determine override options for a particular configuration.

    The options are determined by searching following the order specified in selected_options.

    For the example config,

    .. code-block:: yaml

        config:
            value: 3
            override:
                2.76:
                    track:
                        value: 5

    value will be assigned the value 5 if we are at 2.76 TeV with a track bias, regardless of the event
    activity or leading hadron bias. The order of this configuration is specified by the order of the
    selected_options passed. The above example configuration is from the jet-hadron analysis.

    Since anchors aren't kept for scalar values, if you want to override an anchored value, you need to
    specify it as a single value in a list (or dict, but list is easier). After the anchor values propagate,
    single element lists can be converted into scalar values using ``simplify_data_representations()``.

    Args:
        config: The dict-like configuration from ruamel.yaml which should be overridden.
        selected_options: The selected analysis options. They will be checked in the order with which
            they are passed, so make certain that it matches the order in the configuration file!
        set_of_possible_options (tuple of enums): Possible options for the override value categories.
        config_containing_override: The dict-like config containing the override options in a map called
            "override". If it is not specified, it will look for it in the main config.
    Returns:
        dict-like object: The updated configuration

def override_options(config: DictLike, selected_options: Tuple[Any, ...], set_of_possible_options: Tuple[enum.Enum, ...], config_containing_override: DictLike = None) -> DictLike:
    """ Determine override options for a particular configuration.

    The options are determined by searching following the order specified in selected_options.

    For the example config,

    .. code-block:: yaml

        config:
            value: 3
            override:
                2.76:
                    track:
                        value: 5

    value will be assigned the value 5 if we are at 2.76 TeV with a track bias, regardless of the event
    activity or leading hadron bias. The order of this configuration is specified by the order of the
    selected_options passed. The above example configuration is from the jet-hadron analysis.

    Since anchors aren't kept for scalar values, if you want to override an anchored value, you need to
    specify it as a single value in a list (or dict, but list is easier). After the anchor values propagate,
    single element lists can be converted into scalar values using ``simplify_data_representations()``.

    Args:
        config: The dict-like configuration from ruamel.yaml which should be overridden.
        selected_options: The selected analysis options. They will be checked in the order with which
            they are passed, so make certain that it matches the order in the configuration file!
        set_of_possible_options (tuple of enums): Possible options for the override value categories.
        config_containing_override: The dict-like config containing the override options in a map called
            "override". If it is not specified, it will look for it in the main config.
    Returns:
        dict-like object: The updated configuration
    """
    if config_containing_override is None:
        config_containing_override = config
    override_opts = config_containing_override.pop("override")
    override_dict = determine_override_options(selected_options, override_opts, set_of_possible_options)
    logger.debug(f"override_dict: {override_dict}")

    # Set the configuration values to those specified in the override options
    # Cannot just use update() on config because we need to maintain the anchors.
    for k, v in override_dict.items():
        # Check if key is there and if it is not None! (The second part is important)
        if k in config:
            try:
                # If it has an anchor, we know that we want to preserve the type. So we check for the anchor
                # by trying to access it (Note that we don't actually care what the value is - just that it
                # exists). If it fails with an AttributeError, then we know we can just assign the value. If it
                # has an anchor, then we want to preserve the anchor information.
                config[k].anchor
                logger.debug(f"type: {type(config[k])}, k: {k}")
                if isinstance(config[k], list):
                    # Clear out the existing list entries
                    del config[k][:]
                    if isinstance(override_dict[k], (str, int, float, bool)):
                        # We have to treat str carefully because it is an iterable, but it will be expanded as
                        # individual characters if it's treated the same as a list, which is not the desired
                        # behavior! If we wrap it in [], then it will be treated as the only entry in the list
                        # NOTE: We also treat the basic types this way because they will be passed this way if
                        #       overriding indirectly with anchors (since the basic scalar types don't yet support
                        #       reassignment while maintaining their anchors).
                        config[k].append(override_dict[k])
                    else:
                        # Here we just assign all entries of the list to all entries of override_dict[k]
                        config[k].extend(override_dict[k])
                elif isinstance(config[k], dict):
                    # Clear out the existing entries because we are trying to replace everything
                    # Then we can simply update the dict with our new values
                    config[k].clear()
                    config[k].update(override_dict[k])
                elif isinstance(config[k], (int, float, bool)):
                    # This isn't really very good (since we lose information), but there's nothing that can be done
                    # about it at the moment (Dec 2018)
                    logger.debug("Overwriting YAML anchor object. It is currently unclear how to reassign this value.")
                    config[k] = v
                else:
                    # Raise a value error on all of the cases that we aren't already aware of.
                    raise ValueError(f"Object {k} (type {type(config[k])}) somehow has an anchor, but is something other than a list or dict. Attempting to assign directly to it.")
            except AttributeError:
                # If no anchor, just overwrite the value at this key
                config[k] = v
        else:
            raise KeyError(k, f"Trying to override key \"{k}\" that it is not in the config.")

    return config