1# coding: utf-8
2from __future__ import absolute_import, division, print_function
3
4import inspect
5
6import six
7
8from .compat.contextlib import suppress
9
10__all__ = ['ReprMixinBase', 'ReprMixin']
11
12
13class ReprMixinBase(object):
14    """Mixin to construct :code:`__repr__` for named arguments **automatically**.
15
16    :code:`_repr_pretty_` for :py:mod:`IPython.lib.pretty` is also constructed.
17
18    :param positional: Mark arguments as positional by number, or a list of
19        argument names.
20
21    .. deprecated:: 1.5.0
22
23        Use the :func:`~represent.core.autorepr` class decorator instead.
24    """
25
26    def __init__(self, positional=None, *args, **kwargs):
27        cls = self.__class__
28        # On first init, class variables for repr won't exist.
29        #
30        # Subclasses created after an initialisation of the superclass
31        # will require the repr class variables to be created for the new
32        # class.
33        if (not hasattr(cls, '_repr_clsname')
34                or cls._repr_clsname != cls.__name__):
35            cls._repr_clsname = cls.__name__
36            cls._repr_positional = positional
37
38            # Support Python 3 and Python 2 argspecs,
39            # including keyword only arguments
40            try:
41                argspec = inspect.getfullargspec(self.__init__)
42            except AttributeError:
43                argspec = inspect.getargspec(self.__init__)
44
45            fun_args = argspec.args[1:]
46            kwonly = set()
47            with suppress(AttributeError):
48                fun_args.extend(argspec.kwonlyargs)
49                kwonly.update(argspec.kwonlyargs)
50
51            # Args can be opted in as positional
52            if positional is None:
53                positional = []
54            elif isinstance(positional, int):
55                positional = fun_args[:positional]
56            elif isinstance(positional, six.string_types):
57                positional = [positional]
58
59            # Ensure positional args can't follow keyword args.
60            keyword_started = None
61
62            # _repr_pretty_ uses lists for the pretty printer calls
63            cls._repr_pretty_positional_args = list()
64            cls._repr_pretty_keyword_args = list()
65
66            # Construct format string for __repr__
67            repr_parts = [cls.__name__, '(']
68            for i, arg in enumerate(fun_args):
69                if i:
70                    repr_parts.append(', ')
71
72                if arg in positional:
73                    repr_parts.append('{{self.{0}!r}}'.format(arg))
74                    cls._repr_pretty_positional_args.append(arg)
75
76                    if arg in kwonly:
77                        raise ValueError("keyword only argument '{}' cannot be"
78                                         " positional".format(arg))
79                    if keyword_started:
80                        raise ValueError(
81                            "positional argument '{}' cannot follow keyword"
82                            " argument '{}'".format(arg, keyword_started))
83                else:
84                    keyword_started = arg
85                    repr_parts.append('{0}={{self.{0}!r}}'.format(arg))
86                    cls._repr_pretty_keyword_args.append(arg)
87
88            repr_parts.append(')')
89
90            # Store as class variable.
91            cls._repr_formatstr = ''.join(repr_parts)
92
93        # Pass on args for cooperative multiple inheritance.
94        super(ReprMixinBase, self).__init__(*args, **kwargs)
95
96    def __repr__(self):
97        return self.__class__._repr_formatstr.format(self=self)
98
99    def _repr_pretty_(self, p, cycle):
100        """Pretty printer for IPython.lib.pretty"""
101        cls = self.__class__
102        clsname = cls.__name__
103
104        if cycle:
105            p.text('{}(...)'.format(clsname))
106        else:
107            positional_args = cls._repr_pretty_positional_args
108            keyword_args = cls._repr_pretty_keyword_args
109
110            with p.group(len(clsname) + 1, clsname + '(', ')'):
111                for i, positional in enumerate(positional_args):
112                    if i:
113                        p.text(',')
114                        p.breakable()
115                    p.pretty(getattr(self, positional))
116
117                for i, keyword in enumerate(keyword_args,
118                                            start=len(positional_args)):
119                    if i:
120                        p.text(',')
121                        p.breakable()
122                    with p.group(len(keyword) + 1, keyword + '='):
123                        p.pretty(getattr(self, keyword))
124
125
126class ReprMixin(ReprMixinBase):
127    """Mixin to construct :code:`__repr__` for named arguments **automatically**.
128
129    :code:`_repr_pretty_` for :py:mod:`IPython.lib.pretty` is also constructed.
130
131    This class differs from :py:class:`~represent.core.ReprMixinBase` in that it
132    supports unpickling by providing ``__getstate__`` and ``__setstate__``,
133    ensuring :py:class:`~represent.core.ReprMixinBase` is initialised.
134
135    :param positional: Mark arguments as positional by number, or a list of
136        argument names.
137
138    .. versionchanged:: 1.2
139       ``RepresentationMixin`` renamed to ``ReprMixin``
140
141    .. deprecated:: 1.5.0
142
143        Use the :func:`~represent.core.autorepr` class decorator instead.
144    """
145
146    # To enable pickle support, we must ensure __init__ gets called. __new__
147    # could be used instead, but we can only make pickle call __new__ when
148    # using protocol 2 and above.
149    #
150    # Provide default __getstate__ and __setstate__ which calls __init__
151    # Subclasses that implement these must call ReprMixin.__init__
152    # in __setstate__.
153    def __getstate__(self):
154        return (self.__class__._repr_positional, self.__dict__)
155
156    def __setstate__(self, d):
157        positional, real_dict = d
158        ReprMixin.__init__(self, positional)
159        self.__dict__.update(real_dict)
160