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