import dis
import warnings
from .compat import PY3
from .formatting import bulleted_list
from .functional import keysorted, sliding_window
[docs]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()