Source code for xoutil.clipping

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

'''Complements for object string representation protocol.

There are contexts that using ``str`` or ``repr`` protocol would be inadequate
because shorter string representations are expected (e.g. formatting recursive
objects in `pprint`:mod: standard module that they have a new Boolean
parameter in Python 3 named ``compact``).

There is a protocol to complement operators used by standard string
representation functions (``__str__``, ``__repr__``) by defining a new one
with name ``__crop__``.  This operator will receive some extra parameters with
default values, see `crop`:func: function for details.

'''

from __future__ import (division as _py3_division,
                        print_function as _py3_print,
                        absolute_import as _py3_import)

from xoutil.eight import python_version    # noqa


#: Value for `max_width` parameter in functions that shorten strings, must not
#: be less than this value.
MIN_WIDTH = 8

#: Default value for `max_width` parameter in functions that shorten strings,
#: see `crop`:func:.
DEFAULT_MAX_WIDTH = 64

ELLIPSIS_ASCII = '...'
ELLIPSIS_UNICODE = '…'

#: Value used as a fill when a string representation overflows.
ELLIPSIS = ELLIPSIS_UNICODE if python_version == 3 else ELLIPSIS_ASCII

#: Operator name allowing objects to define theirs own method for string
#: shortening.
OPERATOR_NAME = '__crop__'

_LAMBDA_NAME = (lambda: 0).__name__


def _check_max_width(max_width, caller=None):
    '''Type constrain for "max_width" parameter.'''
    if max_width is None:
        max_width = DEFAULT_MAX_WIDTH
    elif max_width < MIN_WIDTH:
        msg = '{}() '.format(caller) if caller else ''
        msg += ('invalid value for `max_width`, must be between greated than '
                '{}; got {}').format(MIN_WIDTH, max_width)
        raise ValueError(msg)
    return max_width


[docs]def crop(obj, max_width=None, canonical=False): '''Return a reduced string representation of `obj`. Classes can now define a new special attribute ``__crop__``. It can be a `string <str>`:class: (or `unicode`:class: in Python 2). Or a method:: def __crop__(self, max_width=None, canonical=False): pass If the `obj` does not implement the ``__crop__`` protocol, a standard one is computed. :param max_width: Maximum length for the resulting string. If is not given, defaults to `DEFAULT_MAX_WIDTH`:obj:. :param canonical: If True `repr`:func: protocol must be used instead `str`:func: (the default). .. versionadded:: 1.8.0 ''' from functools import partial from xoutil.eight import callable, type_name, string_types max_width = _check_max_width(max_width, caller='crop') if isinstance(obj, string_types): res = obj # TODO: reduce else: oper = getattr(obj, OPERATOR_NAME, partial(_crop, obj)) if isinstance(oper, string_types): # XXX: Allowing to define expecting operator as a static resulting # string res = oper elif callable(oper): # XXX: I don't remember anymore why this check is needed if getattr(oper, '__self__', 'OK') is not None: try: res = oper(max_width=max_width, canonical=canonical) except TypeError: # Just preventing operator definition with no extra # parameters res = oper() else: res = NotImplemented else: msg = "crop() invalid '{}' type: {}" raise TypeError(msg.format(OPERATOR_NAME, type_name(oper))) return res
def _crop(obj, max_width=None, canonical=False): '''Internal crop tool.''' from collections import Set, Mapping from xoutil.eight import type_name res = repr(obj) if canonical else str(obj) if (res.startswith('<') and res.endswith('>')) or len(res) > max_width: try: res = obj.__name__ if res == _LAMBDA_NAME and not canonical and python_version == 3: # Just a gift res = res.replace(_LAMBDA_NAME, 'λ') except AttributeError: if isinstance(obj, (tuple, list, Set, Mapping)): res = crop_iterator(obj, max_width, canonical) else: res = '{}({})'.format(type_name(obj), ELLIPSIS) return res
[docs]def crop_iterator(obj, max_width=None, canonical=False): '''Return a reduced string representation of the iterator `obj`. See `crop`:func: function for a more general tool. If `max_width` is not given, defaults to ``DEFAULT_MAX_WIDTH``. .. versionadded:: 1.8.0 ''' from collections import Set, Mapping from xoutil.eight import type_name max_width = _check_max_width(max_width, caller='crop_iterator') classes = (tuple, list, Mapping, Set) cls = next((c for c in classes if isinstance(obj, c)), None) if cls: res = '' if cls is Set and not obj: borders = ('{}('.format(type_name(obj)), ')') else: borders = ('()', '[]', '{}', '{}')[classes.index(cls)] UNDEF = object() sep = ', ' if cls is Mapping: from xoutil.eight import iteritems def itemrepr(item): key, value = item return '{}: {}'.format(repr(key), repr(value)) else: iteritems = iter itemrepr = repr items = iteritems(obj) ok = True while ok: item = next(items, UNDEF) if item is not UNDEF: if res: res += sep aux = itemrepr(item) if len(res) + len(borders) + len(aux) <= max_width: res += aux else: res += ELLIPSIS ok = False else: ok = False return '{}{}{}'.format(borders[0], res, borders[1]) else: raise TypeError('crop_iterator() expects tuple, list, set, or ' 'mapping; got {}'.format(type_name(obj)))
# aliases short = small = crop # noqa