-
Notifications
You must be signed in to change notification settings - Fork 16
/
default.py
106 lines (84 loc) · 3.74 KB
/
default.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
import dis
import warnings
from .compat import PY3
from .formatting import bulleted_list
from .functional import keysorted, sliding_window
class default(object):
"""Default implementation of a function in terms of interface methods.
"""
def __init__(self, implementation):
self.implementation = implementation
def __repr__(self):
return "{}({})".format(type(self).__name__, self.implementation)
class UnsafeDefault(UserWarning):
pass
if PY3: # pragma: nocover-py2
_DEFAULT_USES_NON_INTERFACE_MEMBER_TEMPLATE = (
"Default for {iface}.{method} uses non-interface attributes.\n\n"
"The following attributes are used but are not part of "
"the interface:\n"
"{non_members}\n\n"
"Consider changing {iface}.{method} or making these attributes"
" part of {iface}."
)
def warn_if_defaults_use_non_interface_members(interface_name,
defaults,
members):
"""Warn if an interface default uses non-interface members of self.
"""
for method_name, attrs in non_member_attributes(defaults, members):
warnings.warn(_DEFAULT_USES_NON_INTERFACE_MEMBER_TEMPLATE.format(
iface=interface_name,
method=method_name,
non_members=bulleted_list(attrs),
), category=UnsafeDefault, stacklevel=3)
def non_member_attributes(defaults, members):
from .typed_signature import TypedSignature
for default_name, default in keysorted(defaults):
impl = default.implementation
if isinstance(impl, staticmethod):
# staticmethods can't use attributes of the interface.
continue
elif isinstance(impl, property):
impl = impl.fget
self_name = TypedSignature(impl).first_argument_name
if self_name is None:
# No parameters.
# TODO: This is probably a bug in the interface, since a method
# with no parameters that's not a staticmethod probably can't
# be called in any natural way.
continue
used = accessed_attributes_of_local(impl, self_name)
non_interface_usages = used - members
if non_interface_usages:
yield default_name, sorted(non_interface_usages)
def accessed_attributes_of_local(f, local_name):
"""
Get a list of attributes of ``local_name`` accessed by ``f``.
The analysis performed by this function is conservative, meaning that
it's not guaranteed to find **all** attributes used.
"""
try:
instrs = dis.get_instructions(f)
except TypeError:
# Got a default wrapping an object that's not a python function. Be
# conservative and assume this is safe.
return set()
used = set()
# Find sequences of the form: LOAD_FAST(local_name), LOAD_ATTR(<name>).
# This will find all usages of the form ``local_name.<name>``.
#
# It will **NOT** find usages in which ``local_name`` is aliased to
# another name.
for first, second in sliding_window(instrs, 2):
if first.opname == 'LOAD_FAST' and first.argval == local_name:
if second.opname in ('LOAD_ATTR', 'LOAD_METHOD', 'STORE_ATTR'):
used.add(second.argval)
return used
else: # pragma: nocover-py3
def warn_if_defaults_use_non_interface_members(*args, **kwargs):
pass
def non_member_warnings(*args, **kwargs):
return iter(())
def accessed_attributes_of_local(*args, **kwargs):
return set()