Source code for magic_repr

# coding: utf-8

import six
import sys

from six.moves import zip
from itertools import starmap, chain
from operator import attrgetter


__version__ = "0.2.0"

__all__ = ['make_repr']


ON_PYTHON2 = sys.version_info.major == 2


def force_unicode(value):
    """If input string is binary, then decode from utf-8."""
    if isinstance(value, six.binary_type):
        return value.decode('utf-8')
    return value


def is_multiline(text):
    return u'\n' in text


def padding_adder(padding):

    def adder(text, ignore_first_line=False):
        pad_text = u' ' * padding
        lines = text.split(u'\n')
        first_line, rest = cut_head(lines)

        # check if user wants to not add padding
        # for the first line
        if not ignore_first_line:
            first_line = pad_text + first_line

        rest = (pad_text + line
                 for line in rest)

        lines = chain((first_line, ), rest)
        return u'\n'.join(lines)

    return adder


def cut_head(lst):
    """Returns first element and an iterator over rest element.

    TODO: will be useful to make it work not only with lists
          but also with any iterable.
    """
    assert len(lst) > 0, 'Argument lst should have at least one item'
    return lst[0], lst[1:]


def serialize_text(out, text):
    """This method is used to append content of the `text`
    argument to the `out` argument.

    Depending on how many lines in the text, a
    padding can be added to all lines except the first
    one.

    Concatenation result is appended to the `out` argument.
    """
    padding = len(out)
    # we need to add padding to all lines
    # except the first one
    add_padding = padding_adder(padding)
    text = add_padding(text, ignore_first_line=True)

    return out + text


def serialize_list(out, lst, delimiter=u'', max_length=20):

    """This method is used to serialize list of text
    pieces like ["some=u'Another'", "blah=124"]

    Depending on how many lines are in these items,
    they are concatenated in row or as a column.

    Concatenation result is appended to the `out` argument.
    """

    have_multiline_items = any(map(is_multiline, lst))
    result_will_be_too_long = sum(map(len, lst)) > max_length

    if have_multiline_items or result_will_be_too_long:
        padding = len(out)
        add_padding = padding_adder(padding)

        # we need to add padding to all lines
        # except the first one
        head, rest = cut_head(lst)
        rest = map(add_padding, rest)

        # add padding to the head, but not for it's first line
        head = add_padding(head, ignore_first_line=True)

        # now join lines back
        lst = chain((head,), rest)
        delimiter += u'\n'
    else:
        delimiter += u' '

    return out + delimiter.join(lst)


def format_value(value):
    """This function should return unicode representation of the value
    """
    if isinstance(value, six.binary_type):
        # suppose, all byte strings are in unicode
        # don't know if everybody in the world uses anything else?
        return u"'{0}'".format(value.decode('utf-8'))

    elif isinstance(value, six.text_type):
        return u"u'{0}'".format(value)

    elif isinstance(value, (list, tuple)):
        # long lists or lists with multiline items
        # will be shown vertically
        values = list(map(format_value, value))
        result = serialize_list(u'[', values, delimiter=u',') + u']'
        return force_unicode(result)

    elif isinstance(value, dict):
        items = six.iteritems(value)

        # format each key/value pair as a text,
        # calling format_value recursively
        items = (tuple(map(format_value, item))
                 for item in items)

        items = list(items)
        # sort by keys for readability
        items.sort()

        # for each item value
        items = [
            serialize_text(
                u'{0}: '.format(key),
                item_value)
            for key, item_value in items]

        # and serialize these pieces as a list, enclosing
        # them into a curve brackets
        result = serialize_list(u'{', items, delimiter=u',') + u'}'
        return force_unicode(result)

    return force_unicode(repr(value))


[docs]def make_repr(*args, **kwargs): """Returns __repr__ method which returns ASCII representaion of the object with given fields. Without arguments, ``make_repr`` generates a method which outputs all object's non-protected (non-undercored) arguments which are not callables. Accepts ``*args``, which should be a names of object's attributes to be included in the output:: __repr__ = make_repr('foo', 'bar') If you want to generate attribute's content on the fly, then you should use keyword arguments and pass a callable of one argument:: __repr__ = make_repr(foo=lambda obj: obj.blah + 100500) """ def method(self): cls_name = self.__class__.__name__ if args: field_names = args else: undercored = lambda name: name.startswith('_') is_method = lambda name: callable(getattr(self, name)) good_name = lambda name: not undercored(name) \ and not is_method(name) field_names = filter(good_name, dir(self)) field_names = sorted(field_names) # on this stage, we make from field_names an # attribute getters field_getters = zip(field_names, map(attrgetter, field_names)) # now process keyword args, they must # contain callables of one argument # and callable should return a field's value field_getters = chain( field_getters, kwargs.items()) fields = ((name, format_value(getter(self))) for name, getter in field_getters) # prepare key strings fields = ((u'{0}='.format(name), value) for name, value in fields) # join values with they respective keys fields = list(starmap(serialize_text, fields)) beginning = u'<{cls_name} '.format( cls_name=cls_name, ) result = serialize_list( beginning, fields) # append closing braket result += u'>' if ON_PYTHON2: # on python 2.x repr returns bytes, but on python3 - unicode strings result = result.encode('utf-8') return result return method