1# code: utf-8 2from __future__ import absolute_import, print_function 3 4import inspect 5import sys 6from functools import partial 7 8import six 9 10from .helper import ReprHelper, PrettyReprHelper 11from .utilities import ReprInfo 12 13try: 14 from reprlib import recursive_repr 15except ImportError: 16 recursive_repr = None 17 18 19__all__ = ['ReprHelperMixin', 'autorepr'] 20 21 22_DEFAULT_INCLUDE_PRETTY = True 23 24 25def autorepr(*args, **kwargs): 26 """Class decorator to construct :code:`__repr__` **automatically** 27 based on the arguments to ``__init__``. 28 29 :code:`_repr_pretty_` for :py:mod:`IPython.lib.pretty` is also constructed, 30 unless `include_pretty=False`. 31 32 :param positional: Mark arguments as positional by number, or a list of 33 argument names. 34 :param include_pretty: Add a ``_repr_pretty_`` to the class (defaults to 35 True). 36 37 Example: 38 39 .. code-block:: python 40 41 >>> @autorepr 42 ... class A: 43 ... def __init__(self, a, b): 44 ... self.a = a 45 ... self.b = b 46 47 >>> print(A(1, 2)) 48 A(a=1, b=2) 49 50 .. code-block:: python 51 52 >>> @autorepr(positional=1) 53 ... class B: 54 ... def __init__(self, a, b): 55 ... self.a = a 56 ... self.b = b 57 58 >>> print(A(1, 2)) 59 A(1, b=2) 60 61 .. versionadded:: 1.5.0 62 """ 63 cls = positional = None 64 include_pretty = _DEFAULT_INCLUDE_PRETTY 65 66 # We allow using @autorepr or @autorepr(positional=..., ...), so check 67 # how we were called. 68 69 if args and not kwargs: 70 if len(args) != 1: 71 raise TypeError('Class must be only positional argument.') 72 73 cls, = args 74 75 if not isinstance(cls, type): 76 raise TypeError( 77 "The sole positional argument must be a class. To use the " 78 "'positional' argument, use a keyword.") 79 80 elif not args and kwargs: 81 valid_kwargs = {'positional', 'include_pretty'} 82 invalid_kwargs = set(kwargs) - valid_kwargs 83 84 if invalid_kwargs: 85 error = 'Unexpected keyword arguments: {}'.format(invalid_kwargs) 86 raise TypeError(error) 87 88 positional = kwargs.get('positional') 89 include_pretty = kwargs.get('include_pretty', include_pretty) 90 91 elif (args and kwargs) or (not args and not kwargs): 92 raise TypeError( 93 'Use bare @autorepr or @autorepr(...) with keyword args.') 94 95 # Define the methods we'll add to the decorated class. 96 97 def __repr__(self): 98 return self.__class__._represent.fstr.format(self=self) 99 100 if recursive_repr is not None: 101 __repr__ = recursive_repr()(__repr__) 102 103 _repr_pretty_ = None 104 if include_pretty: 105 _repr_pretty_ = _make_repr_pretty() 106 107 if cls is not None: 108 return _autorepr_decorate(cls, repr=__repr__, repr_pretty=_repr_pretty_) 109 else: 110 return partial( 111 _autorepr_decorate, repr=__repr__, repr_pretty=_repr_pretty_, 112 positional=positional, include_pretty=include_pretty) 113 114 115def _make_repr_pretty(): 116 def _repr_pretty_(self, p, cycle): 117 """Pretty printer for :class:`IPython.lib.pretty`""" 118 cls = self.__class__ 119 clsname = cls.__name__ 120 121 if cycle: 122 p.text('{}(...)'.format(clsname)) 123 else: 124 positional_args = cls._represent.args 125 keyword_args = cls._represent.kw 126 127 with p.group(len(clsname) + 1, clsname + '(', ')'): 128 for i, positional in enumerate(positional_args): 129 if i: 130 p.text(',') 131 p.breakable() 132 p.pretty(getattr(self, positional)) 133 134 for i, keyword in enumerate(keyword_args, 135 start=len(positional_args)): 136 if i: 137 p.text(',') 138 p.breakable() 139 with p.group(len(keyword) + 1, keyword + '='): 140 p.pretty(getattr(self, keyword)) 141 142 return _repr_pretty_ 143 144 145def _getparams(cls): 146 if sys.version_info >= (3, 3): 147 signature = inspect.signature(cls) 148 params = list(signature.parameters) 149 kwonly = {p.name for p in signature.parameters.values() 150 if p.kind == inspect.Parameter.KEYWORD_ONLY} 151 else: 152 argspec = inspect.getargspec(cls.__init__) 153 params = argspec.args[1:] 154 kwonly = set() 155 156 return params, kwonly 157 158 159def _autorepr_decorate(cls, repr, repr_pretty, positional=None, 160 include_pretty=_DEFAULT_INCLUDE_PRETTY): 161 params, kwonly = _getparams(cls) 162 163 # Args can be opted in as positional 164 if positional is None: 165 positional = [] 166 elif isinstance(positional, int): 167 positional = params[:positional] 168 elif isinstance(positional, six.string_types): 169 positional = [positional] 170 171 # Ensure positional args can't follow keyword args. 172 keyword_started = None 173 174 # _repr_pretty_ uses lists for the pretty printer calls 175 repr_args = [] 176 repr_kw = [] 177 178 # Construct format string for __repr__ 179 repr_fstr_parts = ['{self.__class__.__name__}', '('] 180 for i, arg in enumerate(params): 181 if i: 182 repr_fstr_parts.append(', ') 183 184 if arg in positional: 185 repr_fstr_parts.append('{{self.{0}!r}}'.format(arg)) 186 repr_args.append(arg) 187 188 if arg in kwonly: 189 raise ValueError("keyword only argument '{}' cannot" 190 " be positional".format(arg)) 191 if keyword_started: 192 raise ValueError( 193 "positional argument '{}' cannot follow keyword" 194 " argument '{}'".format(arg, keyword_started)) 195 else: 196 keyword_started = arg 197 repr_fstr_parts.append('{0}={{self.{0}!r}}'.format(arg)) 198 repr_kw.append(arg) 199 200 repr_fstr_parts.append(')') 201 202 # Store as class variable. 203 cls._represent = ReprInfo(''.join(repr_fstr_parts), repr_args, repr_kw) 204 205 cls.__repr__ = repr 206 if include_pretty: 207 cls._repr_pretty_ = repr_pretty 208 209 return cls 210 211 212class ReprHelperMixin(object): 213 """Mixin to provide :code:`__repr__` and :code:`_repr_pretty_` for 214 :py:mod:`IPython.lib.pretty` from user defined :code:`_repr_helper_` 215 function. 216 217 For full API, see :py:class:`represent.helper.BaseReprHelper`. 218 219 .. code-block:: python 220 221 def _repr_helper_(self, r): 222 r.positional_from_attr('attrname') 223 r.positional_with_value(value) 224 r.keyword_from_attr('attrname') 225 r.keyword_from_attr('keyword', 'attrname') 226 r.keyword_with_value('keyword', value) 227 228 .. versionadded:: 1.3 229 """ 230 231 __slots__ = () 232 233 def __repr__(self): 234 r = ReprHelper(self) 235 self._repr_helper_(r) 236 return str(r) 237 238 if recursive_repr is not None: 239 __repr__ = recursive_repr()(__repr__) 240 241 def _repr_pretty_(self, p, cycle): 242 with PrettyReprHelper(self, p, cycle) as r: 243 self._repr_helper_(r) 244