import functools
import weakref
from functools import singledispatch, update_wrapper
# Descriptor version
[docs]class singledispatchmethod:
"""Single-dispatch generic method descriptor.
Supports wrapping existing descriptors and handles non-descriptor
callables as instance methods.
"""
def __init__(self, func):
if not callable(func) and not hasattr(func, "__get__"):
raise TypeError(f"{func!r} is not callable or a descriptor")
self.dispatcher = singledispatch(func)
self.func = func
[docs] def register(self, cls, method=None):
"""generic_method.register(cls, func) -> func
Registers a new implementation for the given *cls* on a *generic_method*.
"""
return self.dispatcher.register(cls, func=method)
def __get__(self, obj, cls=None):
def _method(*args, **kwargs):
method = self.dispatcher.dispatch(args[0].__class__)
return method.__get__(obj, cls)(*args, **kwargs) # type: ignore
_method.__isabstractmethod__ = self.__isabstractmethod__ # type: ignore
_method.register = self.register # type: ignore
update_wrapper(_method, self.func)
return _method
@property
def __isabstractmethod__(self):
return getattr(self.func, "__isabstractmethod__", False)
def _splitlines_no_ff(source):
"""Split a string into lines ignoring form feed and other chars.
This mimics how the Python parser splits source code.
"""
idx = 0
lines = []
next_line = ""
while idx < len(source):
c = source[idx]
next_line += c
idx += 1
# Keep \r\n together
if c == "\r" and idx < len(source) and source[idx] == "\n":
next_line += "\n"
idx += 1
if c in "\r\n":
lines.append(next_line)
next_line = ""
if next_line:
lines.append(next_line)
return lines
def _pad_whitespace(source):
r"""Replace all chars except '\f\t' in a line with spaces."""
result = ""
for c in source:
if c in "\f\t":
result += c
else:
result += " "
return result
[docs]def get_source_segment(source, node, padded=False):
"""Get source code segment of the *source* that generated *node*.
.. note::
This is a polyfill for the ast.get_source_segment function
that was introduced in python 3.8.
If some location information (`lineno`, `end_lineno`, `col_offset`,
or `end_col_offset`) is missing, return None.
If *padded* is `True`, the first line of a multi-line statement will
be padded with spaces to match its original position.
"""
try:
if node.end_lineno is None or node.end_col_offset is None:
return None
lineno = node.lineno - 1
end_lineno = node.end_lineno - 1
col_offset = node.col_offset
end_col_offset = node.end_col_offset
except AttributeError:
return None
lines = _splitlines_no_ff(source)
if end_lineno == lineno:
return lines[lineno].encode()[col_offset:end_col_offset].decode()
if padded:
padding = _pad_whitespace(lines[lineno].encode()[:col_offset].decode())
else:
padding = ""
first = padding + lines[lineno].encode()[col_offset:].decode()
last = lines[end_lineno].encode()[:end_col_offset].decode()
lines = lines[lineno + 1 : end_lineno]
lines.insert(0, first)
lines.append(last)
return "".join(lines)
# references
# really we should just use cached_property but not supported by Python 3.7
# https://stackoverflow.com/questions/33672412/python-functools-lru-cache-with-instance-methods-release-object/33672499#33672499
def lru_cache(*lru_args, **lru_kwargs):
def decorator(func):
@functools.wraps(func)
def wrapped_func(self, *args, **kwargs):
# We're storing the wrapped method inside the instance. If we had
# a strong reference to self the instance would never die.
self_weak = weakref.ref(self)
@functools.wraps(func)
@functools.lru_cache(*lru_args, **lru_kwargs)
def cached_method(*args, **kwargs):
return func(self_weak(), *args, **kwargs)
setattr(self, func.__name__, cached_method)
return cached_method(*args, **kwargs)
return wrapped_func
return decorator