# -*- coding: utf-8 -*- # pylint: disable=E1101, C0330, C0103 # E1101: Module X has no Y member # C0330: Wrong continued indentation # C0103: Invalid attribute/variable/method name """ utils.py ========= This is a collection of utilities used by the :mod:`wx.lib.plot` package. """ __docformat__ = "restructuredtext en" # Standard Library import functools import inspect import itertools from warnings import warn as _warn # Third Party import wx import numpy as np class PlotPendingDeprecation(wx.wxPyDeprecationWarning): pass class DisplaySide(object): """ Generic class for describing which sides of a box are displayed. Used for fine-tuning the axis, ticks, and values of a graph. This class somewhat mimics a collections.namedtuple factory function in that it is an iterable and can have indiviual elements accessible by name. It differs from a namedtuple in a few ways: - it's mutable - it's not a factory function but a full-fledged class - it contains type checking, only allowing boolean values - it contains name checking, only allowing valid_names as attributes :param bottom: Display the bottom side :type bottom: bool :param left: Display the left side :type left: bool :param top: Display the top side :type top: bool :param right: Display the right side :type right: bool """ # TODO: Do I want to replace with __slots__? # Not much memory gain because this class is only called a small # number of times, but it would remove the need for part of # __setattr__... valid_names = ("bottom", "left", "right", "top") def __init__(self, bottom, left, top, right): if not all([isinstance(x, bool) for x in [bottom, left, top, right]]): raise TypeError("All args must be bools") self.bottom = bottom self.left = left self.top = top self.right = right def __str__(self): s = "{}(bottom={}, left={}, top={}, right={})" s = s.format(self.__class__.__name__, self.bottom, self.left, self.top, self.right, ) return s def __repr__(self): # for now, just return the str representation return self.__str__() def __setattr__(self, name, value): """ Override __setattr__ to implement some type checking and prevent other attributes from being created. """ if name not in self.valid_names: err_str = "attribute must be one of {}" raise NameError(err_str.format(self.valid_names)) if not isinstance(value, bool): raise TypeError("'{}' must be a boolean".format(name)) self.__dict__[name] = value def __len__(self): return 4 def __hash__(self): return hash(tuple(self)) def __getitem__(self, key): return (self.bottom, self.left, self.top, self.right)[key] def __setitem__(self, key, value): if key == 0: self.bottom = value elif key == 1: self.left = value elif key == 2: self.top = value elif key == 3: self.right = value else: raise IndexError("list index out of range") def __iter__(self): return iter([self.bottom, self.left, self.top, self.right]) # TODO: replace with wx.DCPenChanger/wx.DCBrushChanger, etc. # Alternatively, replace those with this function... class TempStyle(object): """ Decorator / Context Manager to revert pen or brush changes. Will revert pen, brush, or both to their previous values after a method call or block finish. :param which: The item to save and revert after execution. Can be one of ``{'both', 'pen', 'brush'}``. :type which: str :param dc: The DC to get brush/pen info from. :type dc: :class:`wx.DC` :: # Using as a method decorator: @TempStyle() # same as @TempStyle('both') def func(self, dc, a, b, c): # dc must be 1st arg (beside self) # edit pen and brush here # Or as a context manager: with TempStyle('both', dc): # do stuff .. Note:: As of 2016-06-15, this can only be used as a decorator for **class methods**, not standard functions. There is a plan to try and remove this restriction, but I don't know when that will happen... .. epigraph:: *Combination Decorator and Context Manager! Also makes Julienne fries! Will not break! Will not... It broke!* -- The Genie """ _valid_types = {'both', 'pen', 'brush'} _err_str = ( "No DC provided and unable to determine DC from context for function " "`{func_name}`. When `{cls_name}` is used as a decorator, the " "decorated function must have a wx.DC as a keyword arg 'dc=' or " "as the first arg." ) def __init__(self, which='both', dc=None): if which not in self._valid_types: raise ValueError( "`which` must be one of {}".format(self._valid_types) ) self.which = which self.dc = dc self.prevPen = None self.prevBrush = None def __call__(self, func): @functools.wraps(func) def wrapper(instance, dc, *args, **kwargs): # fake the 'with' block. This solves: # 1. plots only being shown on 2nd menu selection in demo # 2. self.dc compalaining about not having a super called when # trying to get or set the pen/brush values in __enter__ and # __exit__: # RuntimeError: super-class __init__() of type # BufferedDC was never called self._save_items(dc) func(instance, dc, *args, **kwargs) self._revert_items(dc) #import copy # copy solves issue #1 above, but #self.dc = copy.copy(dc) # possibly causes issue #2. #with self: # print('in with') # func(instance, dc, *args, **kwargs) return wrapper def __enter__(self): self._save_items(self.dc) return self def __exit__(self, *exc): self._revert_items(self.dc) return False # True means exceptions *are* suppressed. def _save_items(self, dc): if self.which == 'both': self._save_pen(dc) self._save_brush(dc) elif self.which == 'pen': self._save_pen(dc) elif self.which == 'brush': self._save_brush(dc) else: err_str = ("How did you even get here?? This class forces " "correct values for `which` at instancing..." ) raise ValueError(err_str) def _revert_items(self, dc): if self.which == 'both': self._revert_pen(dc) self._revert_brush(dc) elif self.which == 'pen': self._revert_pen(dc) elif self.which == 'brush': self._revert_brush(dc) else: err_str = ("How did you even get here?? This class forces " "correct values for `which` at instancing...") raise ValueError(err_str) def _save_pen(self, dc): self.prevPen = dc.GetPen() def _save_brush(self, dc): self.prevBrush = dc.GetBrush() def _revert_pen(self, dc): dc.SetPen(self.prevPen) def _revert_brush(self, dc): dc.SetBrush(self.prevBrush) def pendingDeprecation(new_func): """ Raise `PendingDeprecationWarning` and display a message. Uses inspect.stack() to determine the name of the item that this is called from. :param new_func: The name of the function that should be used instead. :type new_func: string. """ warn_txt = "`{}` is pending deprecation. Please use `{}` instead." _warn(warn_txt.format(inspect.stack()[1][3], new_func), PlotPendingDeprecation) def scale_and_shift_point(x, y, scale=1, shift=0): """ Creates a scaled and shifted 2x1 numpy array of [x, y] values. The shift value must be in the scaled units. :param float `x`: The x value of the unscaled, unshifted point :param float `y`: The y valye of the unscaled, unshifted point :param np.array `scale`: The scale factor to use ``[x_sacle, y_scale]`` :param np.array `shift`: The offset to apply ``[x_shift, y_shift]``. Must be in scaled units :returns: a numpy array of 2 elements :rtype: np.array .. note:: :math:`new = (scale * old) + shift` """ point = scale * np.array([x, y]) + shift return point def set_displayside(value): """ Wrapper around :class:`~wx.lib.plot._DisplaySide` that allows for "overloaded" calls. If ``value`` is a boolean: all 4 sides are set to ``value`` If ``value`` is a 2-tuple: the bottom and left sides are set to ``value`` and the other sides are set to False. If ``value`` is a 4-tuple, then each item is set individually: ``(bottom, left, top, right)`` :param value: Which sides to display. :type value: bool, 2-tuple of bool, or 4-tuple of bool :raises: `TypeError` if setting an invalid value. :raises: `ValueError` if the tuple has incorrect length. :rtype: :class:`~wx.lib.plot._DisplaySide` """ err_txt = ("value must be a bool or a 2- or 4-tuple of bool") # TODO: for 2-tuple, do not change other sides? rather than set to False. if isinstance(value, bool): # turns on or off all axes _value = (value, value, value, value) elif isinstance(value, tuple): if len(value) == 2: _value = (value[0], value[1], False, False) elif len(value) == 4: _value = value else: raise ValueError(err_txt) else: raise TypeError(err_txt) return DisplaySide(*_value) def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = itertools.tee(iterable) next(b, None) return zip(a, b) if __name__ == "__main__": raise RuntimeError("This module is not intended to be run by itself.")