rjdbcm/Aspidites

View on GitHub
Aspidites/api/api.py

Summary

Maintainability
A
35 mins
Test Coverage
# cython: language_level=3, annotation_typing=True, c_string_encoding=utf-8, boundscheck=False, wraparound=False, initializedcheck=False
# Aspidites
# Copyright (C) 2021 Ross J. Duff

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import itertools
from collections import OrderedDict
from typing import Union, ItemsView
from textwrap import wrap
from inspect import isfunction, signature
from Aspidites._vendor.fn.underscore import ArityError, _Callable
from .templates import warning_template
import cython


def _wrap_lines(text, padchar, width, wrapped_lines, pad):
    for l in text.splitlines():
        line = wrap(l, width, replace_whitespace=False)
        for s in line:
            s += padchar * width * pad
        wrapped_lines.extend(line)
    return wrapped_lines


def _wrap(text, width, pad, padchar):
    """
    Do not remove whitespaces in string but still wrap text to max width.
    Instead of passing the entire text to textwrap.wrap, split and pass each
    line instead. This way list formatting is not mangled by textwrap.wrap.
    """
    pad = pad or True
    padchar = padchar or " ".encode("UTF-8")
    width = width or 160

    wrapped_lines = []
    try:
        _wrap_lines(text, padchar, width, wrapped_lines, pad)
    except ArityError:
        wrapped_lines.extend([str(text)])

    return wrapped_lines


def bordered(text, width=160) -> str:
    """Create a fancy bordered textbox as used in :class:`Aspidites.api.Warn`."""
    i: str
    lines: list = [t for t in _wrap(text, width=width, pad=True, padchar=" ")]
    lens: list = [len(i) for i in lines]
    width = max(lens, default=width) or width
    res = ["╭" + "┉" * width + "╮"]
    for s in lines:
        while len(s) < width:
            s += " "
        res.append("┊" + s + "┊")
    res.append("╰" + "┉" * width + "╯")
    return "\n".join(res)


def _format_locals(lokals: dict):
    d = {k: v for k, v in lokals.items() if not str(k).startswith("@")}
    str_locals = _format_items(d)
    return str_locals


def _format_items(x):
    str_locals = "".encode("UTF-8")
    for k, v in x.items():
        if isfunction(v):
            s = str(signature(v)).replace("'", "")
            str_locals += f"{k}: {s}\n".encode("UTF-8")
        else:
            str_locals += f"{k}: {str(v)}\n".encode("UTF-8")
    return str_locals


class Warn:
    """Creates a nice human-readable warning with a fancy border."""

    sig: cython.p_char
    name: Union[_Callable, cython.p_char]
    local_items: ItemsView
    str_locals: bytes
    at_fault: bytes
    func_name: cython.p_char
    fname: cython.p_char
    # noinspection PyUnresolvedReferences
    lineno: cython.int
    fkwargs: cython.p_char

    def __init__(self, stack, func, *args, **kwargs):
        self.stack = stack
        self.func = func
        self.args = args
        self.kwargs = kwargs

    def create(self, exc=Exception()) -> str:
        local_items = self.stack[1][0].f_locals.items()
        str_locals = self.format_locals(local_items, exc)
        func_name = self.stack[1][0].f_code.co_name
        fname = self.stack[1][0].f_code.co_filename
        lineno = self.stack[1][0].f_code.co_firstlineno
        fkwargs = str(self.format_kwargs()).lstrip("b'").rstrip("'")
        name = self.func.__name__ if hasattr(self.func, "__name__") else str(self.func)
        sig = f"{name}({str(self.args).strip('()')}{fkwargs})"
        at_fault = (
            str(name).encode("UTF-8")
            if isinstance(exc, TypeError)
            else sig.encode("UTF-8")
        )
        return warning_template.safe_substitute(
            file=fname,
            lineno=lineno,
            func=bordered(func_name),
            atfault=bordered(at_fault.decode("UTF-8")),
            bound=bordered(str_locals.decode("UTF-8")),
            tb=bordered(str(exc)),
        )

    def format_kwargs(self, sep: str = ", ") -> bytes:
        return f'{sep}{str(self.kwargs).strip("{} ").replace(":", "=") if len(self.kwargs) else ""}'.encode(
            "UTF-8"
        )

        # noinspection PyMethodMayBeStatic

    def format_locals(self, local_vars, exc: Exception) -> bytes:
        OrderedDict(itertools.islice(local_vars, 10))
        lokals: dict = dict(filter(lambda x: x[1] != str(exc), local_vars))
        str_locals: bytes = _format_locals(lokals)
        return str_locals.rstrip("\n".encode("UTF-8"))