1# -*- coding: utf-8 -*-
2# pylint: disable=E1101, C0330, C0103
3#   E1101: Module X has no Y member
4#   C0330: Wrong continued indentation
5#   C0103: Invalid attribute/variable/method name
6"""
7utils.py
8=========
9
10This is a collection of utilities used by the :mod:`wx.lib.plot` package.
11
12"""
13__docformat__ = "restructuredtext en"
14
15# Standard Library
16import functools
17import inspect
18import itertools
19from warnings import warn as _warn
20
21# Third Party
22import wx
23import numpy as np
24
25class PlotPendingDeprecation(wx.wxPyDeprecationWarning):
26    pass
27
28class DisplaySide(object):
29    """
30    Generic class for describing which sides of a box are displayed.
31
32    Used for fine-tuning the axis, ticks, and values of a graph.
33
34    This class somewhat mimics a collections.namedtuple factory function in
35    that it is an iterable and can have indiviual elements accessible by name.
36    It differs from a namedtuple in a few ways:
37
38    - it's mutable
39    - it's not a factory function but a full-fledged class
40    - it contains type checking, only allowing boolean values
41    - it contains name checking, only allowing valid_names as attributes
42
43    :param bottom: Display the bottom side
44    :type bottom: bool
45    :param left: Display the left side
46    :type left: bool
47    :param top: Display the top side
48    :type top: bool
49    :param right: Display the right side
50    :type right: bool
51    """
52    # TODO: Do I want to replace with __slots__?
53    #       Not much memory gain because this class is only called a small
54    #       number of times, but it would remove the need for part of
55    #       __setattr__...
56    valid_names = ("bottom", "left", "right", "top")
57
58    def __init__(self, bottom, left, top, right):
59        if not all([isinstance(x, bool) for x in [bottom, left, top, right]]):
60            raise TypeError("All args must be bools")
61        self.bottom = bottom
62        self.left = left
63        self.top = top
64        self.right = right
65
66    def __str__(self):
67        s = "{}(bottom={}, left={}, top={}, right={})"
68        s = s.format(self.__class__.__name__,
69                     self.bottom,
70                     self.left,
71                     self.top,
72                     self.right,
73                     )
74        return s
75
76    def __repr__(self):
77        # for now, just return the str representation
78        return self.__str__()
79
80    def __setattr__(self, name, value):
81        """
82        Override __setattr__ to implement some type checking and prevent
83        other attributes from being created.
84        """
85        if name not in self.valid_names:
86            err_str = "attribute must be one of {}"
87            raise NameError(err_str.format(self.valid_names))
88        if not isinstance(value, bool):
89            raise TypeError("'{}' must be a boolean".format(name))
90        self.__dict__[name] = value
91
92    def __len__(self):
93        return 4
94
95    def __hash__(self):
96        return hash(tuple(self))
97
98    def __getitem__(self, key):
99        return (self.bottom, self.left, self.top, self.right)[key]
100
101    def __setitem__(self, key, value):
102        if key == 0:
103            self.bottom = value
104        elif key == 1:
105            self.left = value
106        elif key == 2:
107            self.top = value
108        elif key == 3:
109            self.right = value
110        else:
111            raise IndexError("list index out of range")
112
113    def __iter__(self):
114        return iter([self.bottom, self.left, self.top, self.right])
115
116
117# TODO: replace with wx.DCPenChanger/wx.DCBrushChanger, etc.
118#       Alternatively, replace those with this function...
119class TempStyle(object):
120    """
121    Decorator / Context Manager to revert pen or brush changes.
122
123    Will revert pen, brush, or both to their previous values after a method
124    call or block finish.
125
126    :param which: The item to save and revert after execution. Can be
127                  one of ``{'both', 'pen', 'brush'}``.
128    :type which: str
129    :param dc: The DC to get brush/pen info from.
130    :type dc: :class:`wx.DC`
131
132    ::
133
134        # Using as a method decorator:
135        @TempStyle()                        # same as @TempStyle('both')
136        def func(self, dc, a, b, c):        # dc must be 1st arg (beside self)
137            # edit pen and brush here
138
139        # Or as a context manager:
140        with TempStyle('both', dc):
141            # do stuff
142
143    .. Note::
144
145       As of 2016-06-15, this can only be used as a decorator for **class
146       methods**, not standard functions. There is a plan to try and remove
147       this restriction, but I don't know when that will happen...
148
149    .. epigraph::
150
151       *Combination Decorator and Context Manager! Also makes Julienne fries!
152       Will not break! Will not... It broke!*
153
154       -- The Genie
155    """
156    _valid_types = {'both', 'pen', 'brush'}
157    _err_str = (
158        "No DC provided and unable to determine DC from context for function "
159        "`{func_name}`. When `{cls_name}` is used as a decorator, the "
160        "decorated function must have a wx.DC as a keyword arg 'dc=' or "
161        "as the first arg."
162    )
163
164    def __init__(self, which='both', dc=None):
165        if which not in self._valid_types:
166            raise ValueError(
167                "`which` must be one of {}".format(self._valid_types)
168            )
169        self.which = which
170        self.dc = dc
171        self.prevPen = None
172        self.prevBrush = None
173
174    def __call__(self, func):
175
176        @functools.wraps(func)
177        def wrapper(instance, dc, *args, **kwargs):
178            # fake the 'with' block. This solves:
179            # 1.  plots only being shown on 2nd menu selection in demo
180            # 2.  self.dc compalaining about not having a super called when
181            #     trying to get or set the pen/brush values in __enter__ and
182            #     __exit__:
183            #         RuntimeError: super-class __init__() of type
184            #         BufferedDC was never called
185            self._save_items(dc)
186            func(instance, dc, *args, **kwargs)
187            self._revert_items(dc)
188
189            #import copy                    # copy solves issue #1 above, but
190            #self.dc = copy.copy(dc)        # possibly causes issue #2.
191
192            #with self:
193            #    print('in with')
194            #    func(instance, dc, *args, **kwargs)
195
196        return wrapper
197
198    def __enter__(self):
199        self._save_items(self.dc)
200        return self
201
202    def __exit__(self, *exc):
203        self._revert_items(self.dc)
204        return False    # True means exceptions *are* suppressed.
205
206    def _save_items(self, dc):
207        if self.which == 'both':
208            self._save_pen(dc)
209            self._save_brush(dc)
210        elif self.which == 'pen':
211            self._save_pen(dc)
212        elif self.which == 'brush':
213            self._save_brush(dc)
214        else:
215            err_str = ("How did you even get here?? This class forces "
216                       "correct values for `which` at instancing..."
217                       )
218            raise ValueError(err_str)
219
220    def _revert_items(self, dc):
221        if self.which == 'both':
222            self._revert_pen(dc)
223            self._revert_brush(dc)
224        elif self.which == 'pen':
225            self._revert_pen(dc)
226        elif self.which == 'brush':
227            self._revert_brush(dc)
228        else:
229            err_str = ("How did you even get here?? This class forces "
230                       "correct values for `which` at instancing...")
231            raise ValueError(err_str)
232
233    def _save_pen(self, dc):
234        self.prevPen = dc.GetPen()
235
236    def _save_brush(self, dc):
237        self.prevBrush = dc.GetBrush()
238
239    def _revert_pen(self, dc):
240        dc.SetPen(self.prevPen)
241
242    def _revert_brush(self, dc):
243        dc.SetBrush(self.prevBrush)
244
245
246def pendingDeprecation(new_func):
247    """
248    Raise `PendingDeprecationWarning` and display a message.
249
250    Uses inspect.stack() to determine the name of the item that this
251    is called from.
252
253    :param new_func: The name of the function that should be used instead.
254    :type new_func: string.
255    """
256    warn_txt = "`{}` is pending deprecation. Please use `{}` instead."
257    _warn(warn_txt.format(inspect.stack()[1][3], new_func),
258          PlotPendingDeprecation)
259
260
261def scale_and_shift_point(x, y, scale=1, shift=0):
262    """
263    Creates a scaled and shifted 2x1 numpy array of [x, y] values.
264
265    The shift value must be in the scaled units.
266
267    :param float `x`:        The x value of the unscaled, unshifted point
268    :param float `y`:        The y valye of the unscaled, unshifted point
269    :param np.array `scale`: The scale factor to use ``[x_sacle, y_scale]``
270    :param np.array `shift`: The offset to apply ``[x_shift, y_shift]``.
271                             Must be in scaled units
272
273    :returns: a numpy array of 2 elements
274    :rtype: np.array
275
276    .. note::
277
278       :math:`new = (scale * old) + shift`
279    """
280    point = scale * np.array([x, y]) + shift
281    return point
282
283
284def set_displayside(value):
285    """
286    Wrapper around :class:`~wx.lib.plot._DisplaySide` that allows for "overloaded" calls.
287
288    If ``value`` is a boolean: all 4 sides are set to ``value``
289
290    If ``value`` is a 2-tuple: the bottom and left sides are set to ``value``
291    and the other sides are set to False.
292
293    If ``value`` is a 4-tuple, then each item is set individually: ``(bottom,
294    left, top, right)``
295
296    :param value: Which sides to display.
297    :type value:   bool, 2-tuple of bool, or 4-tuple of bool
298    :raises: `TypeError` if setting an invalid value.
299    :raises: `ValueError` if the tuple has incorrect length.
300    :rtype: :class:`~wx.lib.plot._DisplaySide`
301    """
302    err_txt = ("value must be a bool or a 2- or 4-tuple of bool")
303
304    # TODO: for 2-tuple, do not change other sides? rather than set to False.
305    if isinstance(value, bool):
306        # turns on or off all axes
307        _value = (value, value, value, value)
308    elif isinstance(value, tuple):
309        if len(value) == 2:
310            _value = (value[0], value[1], False, False)
311        elif len(value) == 4:
312            _value = value
313        else:
314            raise ValueError(err_txt)
315    else:
316        raise TypeError(err_txt)
317    return DisplaySide(*_value)
318
319
320def pairwise(iterable):
321    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
322    a, b = itertools.tee(iterable)
323    next(b, None)
324    return zip(a, b)
325
326if __name__ == "__main__":
327    raise RuntimeError("This module is not intended to be run by itself.")
328