forked from release-engineering/pushsource
/
erratum_fixup.py
140 lines (113 loc) · 4.94 KB
/
erratum_fixup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""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.
"""
import inspect
import six
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}
if hasattr(old_attr, "alias"):
attr_kwargs["alias"] = new_name
for argname in [
"default",
"validator",
"repr",
"cmp",
"hash",
"init",
"inherited",
]:
if hasattr(old_attr, argname):
attr_kwargs[argname] = getattr(old_attr, argname)
elif (
six.PY3
and argname
in inspect.signature(old_attr.__class__.__init__).parameters.keys()
):
attr_kwargs[argname] = None
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)