<reponame>pradyunsg/pySATsolver<filename>tests/lib/yaml/expand.py
"""Expansion logic for shorthands in YAML.
"""

from .exceptions import AggregatedYAMLErrors, MalformedYAML, YAMLException


def expand_shorthands(test_id, test_dict):
    """Expands all the shorthands to the "expanded" form.

    This simplifies the rest of the code by eliminating the need for them to handle
    shorthands and basic sanity checks.
    """

    _ensure_keys(
        test_id, "top-level", test_dict, {"name", "actions", "index", "results"}
    )

    # Expand everything.
    index = _expand_index(test_id, test_dict["index"])
    actions = _expand_actions(test_id, test_dict["actions"])
    results = _expand_results(test_id, test_dict["results"])

    return {"index": index, "actions": actions, "results": results}


def _ensure_keys(test_id, level, di, expected_keys):
    actual_keys = set(di.keys())
    if actual_keys != expected_keys:
        missing = sorted(expected_keys - actual_keys)
        extra = sorted(actual_keys - expected_keys)

        parts = []
        if missing:
            parts.append("(have missing: " + str(missing)[1:-1] + ")")
        if extra:
            parts.append("(got extra: " + str(extra)[1:-1] + ")")

        raise MalformedYAML(
            test_id, "{} has incorrect structure: {}", level, " ".join(parts)
        )


def _meta_expand(test_id, argument, name, function):
    if not isinstance(argument, list):
        raise MalformedYAML(test_id, "has a non-list " + name)

    errors = AggregatedYAMLErrors()
    for i, item in enumerate(argument):
        try:
            item = function(test_id, i, item)
        except YAMLException as e:
            errors.add(e)
        else:
            # Substitute with the changed item.
            argument[i] = item

    if errors:
        raise errors

    return argument


def _expand_index(test_id, index):
    return _meta_expand(test_id, index, "index", _expand_index_item)


def _expand_index_item(test_id, i, item):
    if isinstance(item, str):
        item = _expand_index_item_string(test_id, i, item)
    elif isinstance(item, dict):
        pass
    else:
        raise MalformedYAML(test_id, "index[{}] is incorrect: {!r}", i, item)

    _ensure_keys(
        test_id, "index[{}]".format(i), item, {"name", "version", "depends", "extras"}
    )
    if not (
        isinstance(item["name"], str)
        and isinstance(item["version"], str)
        and isinstance(item["depends"], list)
        and all(isinstance(obj, str) for obj in item["depends"])
        and isinstance(item["extras"], dict)
        and all(isinstance(obj, str) for obj in item["extras"].keys())
        and all(isinstance(obj, list) for obj in item["extras"].values())
        and all(
            isinstance(elem, str) for obj in item["extras"].values() for elem in obj
        )
    ):
        raise MalformedYAML(test_id, "index[{}] has incorrect structure.", i)

    return item


def _expand_index_item_string(test_id, i, item):
    """Expand an item in the index, which is a string.
    """

    def _split_and_strip(my_str, splitwith, count=None):
        """Split a string and strip each component
        """
        if count is None:
            return [x.strip() for x in my_str.strip().split(splitwith)]
        else:
            return [x.strip() for x in my_str.strip().split(splitwith, count)]

    # Super Cautious Checking of depends.
    if "depends" in item:  # this could be wrong but eh. Will deal with that later.
        errored = False
        try:
            name_version, depends_str = _split_and_strip(item, " depends ", 1)
            depends_str = _split_and_strip(depends_str, "&")
        except ValueError:
            errored = True

        if errored:
            raise MalformedYAML(
                test_id, "index[{}] does not specify 'depends' correctly", i
            )
    else:
        name_version = item
        depends_str = []

    try:
        name, version = _split_and_strip(name_version, " ", 1)
    except ValueError:
        raise MalformedYAML(
            test_id, "index[{}] does not specify name and version correctly", i
        )

    return {"name": name, "version": version, "depends": depends_str, "extras": {}}


def _expand_actions(test_id, actions):
    return _meta_expand(test_id, actions, "actions", _expand_action_item)


def _expand_action_item(test_id, i, item):
    if isinstance(item, dict):
        return _expand_action_item_dict(test_id, i, item)
    else:
        raise MalformedYAML(test_id, "actions[{}] is not a dictionary: {!r}", i, item)


def _expand_action_item_dict(test_id, i, item):
    if not item:
        raise MalformedYAML(test_id, "actions[{}] is an empty dictionary.", i)
    elif "action" in item:
        if "items" not in item:
            raise MalformedYAML(
                test_id, "actions[{}] has an 'action' but no 'items'.", i
            )
        return item  # We'll validate later.
    elif len(item) == 1:
        # convert {"install": ...} -> {"action": "install", "items": ...}
        key, value = next(iter(item.items()))
        return {"action": key, "items": value}
    else:
        raise MalformedYAML(test_id, "actions[{}] is an incorrect dictionary.", i)


def _expand_results(test_id, results):
    return _meta_expand(test_id, results, "results", _expand_result_item)


def _expand_result_item(test_id, i, item):
    if isinstance(item, dict):
        return _expand_result_item_dict(test_id, i, item)
    else:
        raise MalformedYAML(test_id, "results[{}] is not a dictionary.", i)


def _expand_result_item_dict(test_id, i, item):
    if not item:
        raise MalformedYAML(test_id, "results[{}] is an empty dictionary.", i)

    if "set" in item and "graph" not in item:
        raise MalformedYAML(test_id, "results[{}] has a 'set' but no 'graph'.", i)
    elif "graph" in item and "set" not in item:
        raise MalformedYAML(test_id, "results[{}] has a 'graph' but no 'set'.", i)

    return item

"""Expansion logic for shorthands in YAML.
"""

from .exceptions import AggregatedYAMLErrors, MalformedYAML, YAMLException


def expand_shorthands(test_id, test_dict):
    """Expands all the shorthands to the "expanded" form.

    This simplifies the rest of the code by eliminating the need for them to handle
    shorthands and basic sanity checks.
    """

    _ensure_keys(
        test_id, "top-level", test_dict, {"name", "actions", "index", "results"}
    )

    # Expand everything.
    index = _expand_index(test_id, test_dict["index"])
    actions = _expand_actions(test_id, test_dict["actions"])
    results = _expand_results(test_id, test_dict["results"])

    return {"index": index, "actions": actions, "results": results}


def _ensure_keys(test_id, level, di, expected_keys):
    actual_keys = set(di.keys())
    if actual_keys != expected_keys:
        missing = sorted(expected_keys - actual_keys)
        extra = sorted(actual_keys - expected_keys)

        parts = []
        if missing:
            parts.append("(have missing: " + str(missing)[1:-1] + ")")
        if extra:
            parts.append("(got extra: " + str(extra)[1:-1] + ")")

        raise MalformedYAML(
            test_id, "{} has incorrect structure: {}", level, " ".join(parts)
        )


def _meta_expand(test_id, argument, name, function):
    if not isinstance(argument, list):
        raise MalformedYAML(test_id, "has a non-list " + name)

    errors = AggregatedYAMLErrors()
    for i, item in enumerate(argument):
        try:
            item = function(test_id, i, item)
        except YAMLException as e:
            errors.add(e)
        else:
            # Substitute with the changed item.
            argument[i] = item

    if errors:
        raise errors

    return argument


def _expand_index(test_id, index):
    return _meta_expand(test_id, index, "index", _expand_index_item)


def _expand_index_item(test_id, i, item):
    if isinstance(item, str):
        item = _expand_index_item_string(test_id, i, item)
    elif isinstance(item, dict):
        pass
    else:
        raise MalformedYAML(test_id, "index[{}] is incorrect: {!r}", i, item)

    _ensure_keys(
        test_id, "index[{}]".format(i), item, {"name", "version", "depends", "extras"}
    )
    if not (
        isinstance(item["name"], str)
        and isinstance(item["version"], str)
        and isinstance(item["depends"], list)
        and all(isinstance(obj, str) for obj in item["depends"])
        and isinstance(item["extras"], dict)
        and all(isinstance(obj, str) for obj in item["extras"].keys())
        and all(isinstance(obj, list) for obj in item["extras"].values())
        and all(
            isinstance(elem, str) for obj in item["extras"].values() for elem in obj
        )
    ):
        raise MalformedYAML(test_id, "index[{}] has incorrect structure.", i)

    return item


def _expand_index_item_string(test_id, i, item):
    """Expand an item in the index, which is a string.
    """

    def _split_and_strip(my_str, splitwith, count=None):
        """Split a string and strip each component
        """
        if count is None:
            return [x.strip() for x in my_str.strip().split(splitwith)]
        else:
            return [x.strip() for x in my_str.strip().split(splitwith, count)]

    # Super Cautious Checking of depends.
    if "depends" in item:  # this could be wrong but eh. Will deal with that later.
        errored = False
        try:
            name_version, depends_str = _split_and_strip(item, " depends ", 1)
            depends_str = _split_and_strip(depends_str, "&")
        except ValueError:
            errored = True

        if errored:
            raise MalformedYAML(
                test_id, "index[{}] does not specify 'depends' correctly", i
            )
    else:
        name_version = item
        depends_str = []

    try:
        name, version = _split_and_strip(name_version, " ", 1)
    except ValueError:
        raise MalformedYAML(
            test_id, "index[{}] does not specify name and version correctly", i
        )

    return {"name": name, "version": version, "depends": depends_str, "extras": {}}


def _expand_actions(test_id, actions):
    return _meta_expand(test_id, actions, "actions", _expand_action_item)


def _expand_action_item(test_id, i, item):
    if isinstance(item, dict):
        return _expand_action_item_dict(test_id, i, item)
    else:
        raise MalformedYAML(test_id, "actions[{}] is not a dictionary: {!r}", i, item)


def _expand_action_item_dict(test_id, i, item):
    if not item:
        raise MalformedYAML(test_id, "actions[{}] is an empty dictionary.", i)
    elif "action" in item:
        if "items" not in item:
            raise MalformedYAML(
                test_id, "actions[{}] has an 'action' but no 'items'.", i
            )
        return item  # We'll validate later.
    elif len(item) == 1:
        # convert {"install": ...} -> {"action": "install", "items": ...}
        key, value = next(iter(item.items()))
        return {"action": key, "items": value}
    else:
        raise MalformedYAML(test_id, "actions[{}] is an incorrect dictionary.", i)


def _expand_results(test_id, results):
    return _meta_expand(test_id, results, "results", _expand_result_item)


def _expand_result_item(test_id, i, item):
    if isinstance(item, dict):
        return _expand_result_item_dict(test_id, i, item)
    else:
        raise MalformedYAML(test_id, "results[{}] is not a dictionary.", i)


def _expand_result_item_dict(test_id, i, item):
    if not item:
        raise MalformedYAML(test_id, "results[{}] is an empty dictionary.", i)

    if "set" in item and "graph" not in item:
        raise MalformedYAML(test_id, "results[{}] has a 'set' but no 'graph'.", i)
    elif "graph" in item and "set" not in item:
        raise MalformedYAML(test_id, "results[{}] has a 'graph' but no 'set'.", i)

    return item
Expansion logic for shorthands in YAML. Expands all the shorthands to the "expanded" form.      This simplifies the rest of the code by eliminating the need for them to handle     shorthands and basic sanity checks. # Expand everything. # Substitute with the changed item. Expand an item in the index, which is a string. Split a string and strip each component # Super Cautious Checking of depends. # this could be wrong but eh. Will deal with that later. # We'll validate later. # convert {"install": ...} -> {"action": "install", "items": ...}