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