Source code for interface.default

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()