"""Helpers to deal with "from" vs "from_" issue in ErratumPushItem.

The advisory model has a "from" field, which unfortunately clashes with
a Python keyword of the same name. This prevents directly declaring the
field as "from".

Despite the clash, it is desirable to have the attrs field named "from"
so that usage of this model does not have to be frequently accompanied
by some "from_" => "from" renaming (e.g. before uploading to Pulp).

It is possible to make this work by:

- in the ErratumPushItem class, declare the attribute as "from_"
- then, after the class is created, patch it to effectively rename it
  to "from"

This makes use of the __attrs_attrs__ attribute which is associated
with each attrs class. Though the name implies it is private, this is
a supported & documented method of extending the behavior of attrs,
see: https://www.attrs.org/en/stable/extending.html

A simpler trick may come to mind: just setattr(ErratumPushItem, 'from', attr.ib(...))
to work around the inability to write "from = attr.ib(...)".
Unfortunately, that does not work because the attrs library internally
tries to eval some code of the form "<attr_name> = ...", which will never
work if the name is a keyword.
"""


class AttrsRenamer(object):
    def __init__(self, delegate, attrs_old_to_new):
        """Helper to selectively rename certain attributes for __attrs_attrs__
        within a class.

        Arguments:
            delegate (tuple)
                Original instance of __attrs_attrs__, generated by attrs library.
            attrs_old_to_new (dict)
                A mapping from old to new attribute names.
        """
        self._delegate = delegate
        self._attrs_old_to_new = attrs_old_to_new

        self._attrs_by_name = {}
        for old_name, new_name in self._attrs_old_to_new.items():
            old_attr = getattr(delegate, old_name)

            # Make a new attribute, identical to old in all respects
            # except for the name.
            # (we do this dynamically to cope with differences between ancient and newer
            # versions of attrs library)
            attr_kwargs = {"name": new_name}
            for argname in [
                "default",
                "validator",
                "repr",
                "cmp",
                "hash",
                "init",
                "inherited",
            ]:
                if hasattr(old_attr, argname):
                    attr_kwargs[argname] = getattr(old_attr, argname)
            new_attr = old_attr.__class__(**attr_kwargs)

            self._attrs_by_name[new_name] = new_attr

    def __iter__(self):
        for elem in self._delegate:
            new_name = self._attrs_old_to_new.get(elem.name)
            if new_name:
                # Something we've renamed - yield ours
                yield self._attrs_by_name[new_name]
            else:
                # Something else - yield original
                yield elem

    def __getattr__(self, name):
        # If it's one of the new attribute names, just return it directly
        if name in self._attrs_by_name:
            return self._attrs_by_name[name]

        # If it's one of the old attribute names, make it not exist
        if name in self._attrs_old_to_new:
            raise AttributeError()

        # If it's anything else then just let the delegate handle it,
        # giving exactly normal behavior
        return getattr(self._delegate, name)


def fixup_attrs(cls):
    # Fix up the set of attributes on the class.
    #
    # This impacts dynamic use of the class e.g. via attr.fields, attr.asdict,
    # attr.evolve, ...
    cls.__attrs_attrs__ = AttrsRenamer(cls.__attrs_attrs__, {"from_": "from"})


def fixup_init(cls):
    # Fix up the constructor of the class to support both names
    # of the "from" attribute.
    #
    # As the underlying attribute is actually stored as "from_", we need to patch
    # the constructor to accept the "from" argument too and rename it for storage.
    #
    # If the caller provides both "from" and "from_", the former is preferred.
    cls_init = cls.__init__

    def new_init(*args, **kwargs):
        if "from" in kwargs:
            kwargs["from_"] = kwargs.pop("from") or kwargs.get("from_")
        return cls_init(*args, **kwargs)

    cls.__init__ = new_init


def fixup_props(cls):
    # Fix up properties of the class so that there is a "from" property which
    # aliases the underlying "from_".
    from_attr = property(lambda self: self.from_)
    setattr(cls, "from", from_attr)


def fixup_erratum_class(cls):
    # Do all the fixups to make "from" and "from_" aliases.
    fixup_attrs(cls)
    fixup_init(cls)
    fixup_props(cls)
