diff --git a/chaco/base_candle_plot.py b/chaco/base_candle_plot.py index e8cca679b..25f7a80d0 100644 --- a/chaco/base_candle_plot.py +++ b/chaco/base_candle_plot.py @@ -17,6 +17,7 @@ # Chaco imports from .base_xy_plot import BaseXYPlot +from .chaco_traits import Optional # TODO: allow to set the width of the bar @@ -54,11 +55,11 @@ class BaseCandlePlot(BaseXYPlot): #: The color of the stems reaching from the bar ends to the min and max #: values. Also the color of the endcap line segments at min and max. If #: None, this defaults to **bar_line_color**. - stem_color = Union(None, ColorTrait("black")) + stem_color = Optional(ColorTrait("black")) #: The color of the line drawn across the bar at the center values. #: If None, this defaults to **bar_line_color**. - center_color = Union(None, ColorTrait("black")) + center_color = Optional(ColorTrait("black")) #: The color of the outline to draw around the bar. outline_color = ColorTrait("black") @@ -69,11 +70,11 @@ class BaseCandlePlot(BaseXYPlot): #: The thickness, in pixels, of the stem lines. If None, this defaults #: to **line_width**. - stem_width = Union(None, Int(1)) + stem_width = Optional(Int(1)) #: The thickeness, in pixels, of the line drawn across the bar at the #: center values. If None, this defaults to **line_width**. - center_width = Union(None, Int(1)) + center_width = Optional(Int(1)) #: Whether or not to draw bars at the min and max extents of the error bar end_cap = Bool(True) diff --git a/chaco/chaco_traits.py b/chaco/chaco_traits.py index f3cdc1a2a..2408aab08 100644 --- a/chaco/chaco_traits.py +++ b/chaco/chaco_traits.py @@ -12,7 +12,7 @@ """ # Enthought library imports -from traits.api import Enum +from traits.api import Enum, Union, TraitError # ---------------------------------------------------------------------------- # Box positioning traits: used to specify positions of boxes relative to @@ -24,3 +24,59 @@ #: Values correspond to: top, bottom, left, right, top left, top right, bottom #: left, bottom right box_position_enum = Enum("T", "B", "L", "R", "TL", "TR", "BL", "BR") + + +class MappedUnion(Union): + """Version of the Union trait that handles mapped traits correctly.""" + + #: This is not mapped by default. + is_mapped = False + + def __init__(self, *traits, **metadata): + super().__init__(*traits, **metadata) + + # look for post_setattr and is_mapped on traits + post_setattrs = [] + mapped_traits = [] + for trait in traits: + if trait is None: + continue + post_setattr = getattr(trait, "post_setattr", None) + if post_setattr is not None: + post_setattrs.append(post_setattr) + if trait.is_mapped: + self.is_mapped = True + mapped_traits.append(trait) + + if post_setattrs: + self.post_setattrs = post_setattrs + self.post_setattr = self._post_setattr + if self.is_mapped: + self.mapped_traits = mapped_traits + + def mapped_value(self, value): + for trait in self.mapped_traits: + try: + return trait.mapped_value(value) + except Exception: + pass + + return value + + def _post_setattr(self, object, name, value): + for post_setattr in self.post_setattrs: + try: + post_setattr(object, name, value) + return + except Exception: + pass + + if self.is_mapped: + setattr(object, name + "_", value) + + +class Optional(MappedUnion): + """Convenience class""" + + def __init__(self, trait, **metadata): + super().__init__(None, trait, **metadata) diff --git a/chaco/data_view.py b/chaco/data_view.py index 20825b0fa..6e68af389 100644 --- a/chaco/data_view.py +++ b/chaco/data_view.py @@ -20,6 +20,7 @@ from .axis import PlotAxis from .base_1d_mapper import Base1DMapper from .base_2d_plot import Base2DPlot +from .chaco_traits import Optional from .data_range_2d import DataRange2D from .grid import PlotGrid from .linear_mapper import LinearMapper @@ -236,10 +237,10 @@ class DataView(OverlayPlotContainer): observe='y_axis.[title,orientation], x_axis.[title,orientation]' ) - _padding_top = Union(None, Int()) - _padding_bottom = Union(None, Int()) - _padding_left = Union(None, Int()) - _padding_right = Union(None, Int()) + _padding_top = Optional(Int()) + _padding_bottom = Optional(Int()) + _padding_left = Optional(Int()) + _padding_right = Optional(Int()) def _find_padding(self, side): SIDE_TO_TRAIT_MAP = { diff --git a/chaco/grid.py b/chaco/grid.py index b8a23463b..341e7ee27 100644 --- a/chaco/grid.py +++ b/chaco/grid.py @@ -46,6 +46,7 @@ # Local, relative imports from .abstract_overlay import AbstractOverlay from .abstract_mapper import AbstractMapper +from .chaco_traits import Optional from .log_mapper import LogMapper from .ticks import AbstractTickGenerator, DefaultTickGenerator @@ -110,11 +111,11 @@ class PlotGrid(AbstractOverlay): #: The dataspace value at which to start this grid. If None, then #: uses the mapper.range.low. - data_min = Union(None, Float) + data_min = Optional(Float) #: The dataspace value at which to end this grid. If None, then uses #: the mapper.range.high. - data_max = Union(None, Float) + data_max = Optional(Float) #: A callable that implements the AbstractTickGenerator Interface. tick_generator = Instance(AbstractTickGenerator) diff --git a/chaco/overlays/scatter_inspector_overlay.py b/chaco/overlays/scatter_inspector_overlay.py index 8ef88ab9b..d1ee9fccd 100644 --- a/chaco/overlays/scatter_inspector_overlay.py +++ b/chaco/overlays/scatter_inspector_overlay.py @@ -13,11 +13,12 @@ # Enthought library imports from enable.api import ColorTrait, MarkerTrait -from traits.api import Float, Int, Str, Union +from traits.api import Float, Int, Str from traits.observation.events import TraitChangeEvent # Local, relative imports from chaco.abstract_overlay import AbstractOverlay +from chaco.chaco_traits import Optional from chaco.plots.scatterplot import render_markers @@ -32,19 +33,19 @@ class ScatterInspectorOverlay(AbstractOverlay): #: The style to use when a point is hovered over hover_metadata_name = Str("hover") - hover_marker = Union(None, MarkerTrait) - hover_marker_size = Union(None, Int) - hover_line_width = Union(None, Float) - hover_color = Union(None, ColorTrait) - hover_outline_color = Union(None, ColorTrait) + hover_marker = Optional(MarkerTrait) + hover_marker_size = Optional(Int) + hover_line_width = Optional(Float) + hover_color = Optional(ColorTrait) + hover_outline_color = Optional(ColorTrait) #: The style to use when a point has been selected by a click selection_metadata_name = Str("selections") - selection_marker = Union(None, MarkerTrait) - selection_marker_size = Union(None, Int) - selection_line_width = Union(None, Float) - selection_color = Union(None, ColorTrait) - selection_outline_color = Union(None, ColorTrait) + selection_marker = Optional(MarkerTrait) + selection_marker_size = Optional(Int) + selection_line_width = Optional(Float) + selection_color = Optional(ColorTrait) + selection_outline_color = Optional(ColorTrait) # For now, implement the equivalent of this Traits 3 feature manually # using a series of trait change handlers (defined at the end of the diff --git a/chaco/plots/multi_line_plot.py b/chaco/plots/multi_line_plot.py index 0a98557b4..ef716d932 100644 --- a/chaco/plots/multi_line_plot.py +++ b/chaco/plots/multi_line_plot.py @@ -39,6 +39,7 @@ from chaco.array_data_source import ArrayDataSource from chaco.base import arg_find_runs, bin_search from chaco.base_xy_plot import BaseXYPlot +from chaco.chaco_traits import Optional class MultiLinePlot(BaseXYPlot): @@ -118,7 +119,7 @@ class MultiLinePlot(BaseXYPlot): color = black_color_trait(requires_redraw=True) #: A function that returns the color of lines. Overrides `color` if not None. - color_func = Union(None, Callable) + color_func = Optional(Callable) #: The color to use to highlight the line when selected. selected_color = ColorTrait("lightyellow") diff --git a/chaco/plotscrollbar.py b/chaco/plotscrollbar.py index 063cd8c79..c25f2986f 100644 --- a/chaco/plotscrollbar.py +++ b/chaco/plotscrollbar.py @@ -12,6 +12,8 @@ from enable.api import NativeScrollBar +from .chaco_traits import Optional + class PlotScrollBar(NativeScrollBar): """ @@ -43,7 +45,7 @@ class PlotScrollBar(NativeScrollBar): _mapper = Any() # Stores the index (0 or 1) corresponding to self.axis - _axis_index = Union(None, Int) + _axis_index = Optional(Int) # ---------------------------------------------------------------------- # Public methods diff --git a/chaco/tests/test_chaco_traits.py b/chaco/tests/test_chaco_traits.py new file mode 100644 index 000000000..f0337510e --- /dev/null +++ b/chaco/tests/test_chaco_traits.py @@ -0,0 +1,117 @@ +# (C) Copyright 2005-2021 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +import unittest + +from traits.api import Float, HasTraits, Int, Map, Str +from enable.api import ColorTrait + +from ..chaco_traits import MappedUnion, Optional + + +class UsesMappedUnion(HasTraits): + + no_mapped = MappedUnion(Int, Float) + + mapped = MappedUnion( + None, Str, Map({'yes': True, 'no': False}), ColorTrait + ) + + optional = Optional(ColorTrait) + + optional_default = Optional(ColorTrait, default_value='red') + + +class TestMappedUnion(unittest.TestCase): + + def test_no_mapped(self): + no_mapped = MappedUnion(Int, Float) + + self.assertFalse(no_mapped.is_mapped) + self.assertIsNone(no_mapped.post_setattr) + + def test_mapped(self): + mapped = MappedUnion(None, Str, Map({'yes': True, 'no': False})) + + self.assertTrue(mapped.is_mapped) + self.assertIsNotNone(mapped.post_setattr) + + def test_optional(self): + mapped = Optional(ColorTrait) + + self.assertTrue(mapped.is_mapped) + self.assertIsNotNone(mapped.post_setattr) + + def test_no_mapped_class(self): + mapped_union = UsesMappedUnion() + + no_mapped = mapped_union.trait('no_mapped') + + self.assertFalse(no_mapped.is_mapped) + self.assertIsNone(no_mapped.post_setattr) + + self.assertFalse(hasattr(mapped_union, 'no_mapped_')) + + mapped_union.no_mapped = 1 + + self.assertFalse(hasattr(mapped_union, 'no_mapped_')) + + def test_mapped_class(self): + mapped_union = UsesMappedUnion() + + mapped = mapped_union.trait('mapped') + + self.assertTrue(mapped.is_mapped) + self.assertIsNotNone(mapped.post_setattr) + + # test default + self.assertIsNone(mapped_union.mapped_) + + # test mapper works + mapped_union.mapped = 'yes' + + self.assertTrue(mapped_union.mapped_) + + # test second mapper works + mapped_union.mapped = 'red' + + self.assertEqual(mapped_union.mapped_, (1.0, 0.0, 0.0, 1.0)) + + # test non-mapped value works + mapped_union.mapped = 'notacolor' + + self.assertEqual(mapped_union.mapped_, 'notacolor') + + def test_optional_class(self): + mapped_union = UsesMappedUnion() + + optional = mapped_union.trait('optional') + + self.assertTrue(optional.is_mapped) + self.assertIsNotNone(optional.post_setattr) + + # test default + self.assertIsNone(mapped_union.optional_) + + # test mapper works + mapped_union.optional = 'red' + + self.assertEqual(mapped_union.optional_, (1.0, 0.0, 0.0, 1.0)) + + # test non-mapped value works + mapped_union.optional = None + + self.assertIsNone(mapped_union.optional_) + + def test_optional_default_class(self): + mapped_union = UsesMappedUnion() + + # test default + self.assertEqual(mapped_union.optional_default_, (1.0, 0.0, 0.0, 1.0)) diff --git a/chaco/tools/better_selecting_zoom.py b/chaco/tools/better_selecting_zoom.py index 9c4f81fe6..8e2489d03 100644 --- a/chaco/tools/better_selecting_zoom.py +++ b/chaco/tools/better_selecting_zoom.py @@ -10,7 +10,6 @@ import numpy -from chaco.abstract_overlay import AbstractOverlay from enable.api import ColorTrait, KeySpec from traits.api import ( Bool, @@ -22,6 +21,8 @@ Union ) +from chaco.abstract_overlay import AbstractOverlay +from chaco.chaco_traits import Optional from .better_zoom import BetterZoom from .tool_states import SelectedZoomState @@ -91,10 +92,10 @@ class BetterSelectingZoom(AbstractOverlay, BetterZoom): event_state = Enum("normal", "selecting", "pre_selecting") # The (x,y) screen point where the mouse went down. - _screen_start = Union(None, Tuple) + _screen_start = Optional(Tuple) # The (x,,y) screen point of the last seen mouse move event. - _screen_end = Union(None, Tuple) + _screen_end = Optional(Tuple) # If **always_on** is False, this attribute indicates whether the tool # is currently enabled. diff --git a/chaco/tools/image_inspector_tool.py b/chaco/tools/image_inspector_tool.py index b4fc2213b..c98afdb09 100644 --- a/chaco/tools/image_inspector_tool.py +++ b/chaco/tools/image_inspector_tool.py @@ -17,6 +17,7 @@ # Chaco imports from chaco.abstract_overlay import AbstractOverlay +from chaco.chaco_traits import Optional from chaco.overlays.text_box_overlay import TextBoxOverlay from chaco.plots.image_plot import ImagePlot @@ -42,7 +43,7 @@ class ImageInspectorTool(BaseTool): # Stores the value of self.visible when the mouse leaves the tool, # so that it can be restored when the mouse enters again. - _old_visible = Union(None, Bool) + _old_visible = Optional(Bool) def normal_key_pressed(self, event): if self.inspector_key.match(event): diff --git a/chaco/tools/line_segment_tool.py b/chaco/tools/line_segment_tool.py index 5cf2f7479..7073885bd 100644 --- a/chaco/tools/line_segment_tool.py +++ b/chaco/tools/line_segment_tool.py @@ -21,6 +21,7 @@ # Chaco imports from chaco.abstract_overlay import AbstractOverlay +from chaco.chaco_traits import Optional class LineSegmentTool(AbstractOverlay): @@ -55,10 +56,10 @@ class LineSegmentTool(AbstractOverlay): #: The data (index, value) position of the mouse cursor; this is used by various #: draw() routines. - mouse_position = Union(None, Tuple) + mouse_position = Optional(Tuple) # The index of the vertex being dragged, if any. - _dragged = Union(None, Int) + _dragged = Optional(Int) # Is the point being dragged is a newly placed point? This informs the # "dragging" state about what to do if the user presses Escape while diff --git a/chaco/tools/range_selection.py b/chaco/tools/range_selection.py index 88f2c0c57..bb132bee8 100644 --- a/chaco/tools/range_selection.py +++ b/chaco/tools/range_selection.py @@ -34,6 +34,7 @@ # Chaco imports from chaco.abstract_controller import AbstractController +from chaco.chaco_traits import Optional class RangeSelection(AbstractController): @@ -153,7 +154,7 @@ class RangeSelection(AbstractController): _mapper = Any() # Shadow trait for the **axis_index** property. - _axis_index = Union(None, Int) + _axis_index = Optional(Int) # The data space start and end coordinates of the selected region, # expressed as an array. diff --git a/chaco/transform_color_mapper.py b/chaco/transform_color_mapper.py index c5b4aebc1..61b8e2c56 100644 --- a/chaco/transform_color_mapper.py +++ b/chaco/transform_color_mapper.py @@ -11,8 +11,9 @@ from numpy import clip, isinf, ones_like, empty from chaco.color_mapper import ColorMapper -from traits.api import Callable, Tuple, Float, observe, Union +from traits.api import Callable, Tuple, Float, observe +from .chaco_traits import Optional from .speedups import map_colors, map_colors_uint8 @@ -35,12 +36,12 @@ class TransformColorMapper(ColorMapper): unit interval [0,1] to itself (e.g. x^2 or sin(pi*x/2)). """ - data_func = Union(None, Callable) + data_func = Optional(Callable) - unit_func = Union(None, Callable) + unit_func = Optional(Callable) transformed_bounds = Tuple( - Union(None, Float), Union(None, Float) + Optional(Float), Optional(Float) ) # ------------------------------------------------------------------- diff --git a/examples/demo/canvas/axis_tool.py b/examples/demo/canvas/axis_tool.py index 7e4912584..067c679aa 100644 --- a/examples/demo/canvas/axis_tool.py +++ b/examples/demo/canvas/axis_tool.py @@ -1,4 +1,5 @@ from enable.api import BaseTool, ColorTrait +from chaco.chaco_traits import Optional from traits.api import ( Any, Bool, @@ -64,7 +65,7 @@ class AxisTool(BaseTool): down_tick_label_color = ColorTrait("red") down_bgcolor = ColorTrait("lightgray") down_border_visible = Bool(True) - down_border_color = Union(None, ColorTrait) + down_border_color = Optional(ColorTrait) _cached_tick_color = ColorTrait _cached_axis_line_color = ColorTrait diff --git a/examples/demo/canvas/mptools.py b/examples/demo/canvas/mptools.py index 07d4c8361..77e13a22a 100644 --- a/examples/demo/canvas/mptools.py +++ b/examples/demo/canvas/mptools.py @@ -13,11 +13,11 @@ Property, Tuple, CArray, - Union, ) # Chaco imports from chaco.api import BaseTool +from chaco.chaco_traits import Optional from chaco.tools.api import PanTool, DragZoom, LegendTool, RangeSelection @@ -65,11 +65,11 @@ class MPDragZoom(DragZoom): speed = 1.0 # The original dataspace points where blobs 1 and 2 went down - _orig_low = CArray # Union(None, Tuple) - _orig_high = CArray # Union(None, Tuple) + _orig_low = CArray + _orig_high = CArray # Dataspace center of the zoom action - _center_pt = Union(None, Tuple) + _center_pt = Optional(Tuple) # Maps blob ID numbers to the (x,y) coordinates that came in. _blobs = Dict() diff --git a/examples/demo/canvas/plot_clone_tool.py b/examples/demo/canvas/plot_clone_tool.py index ba90ac33c..010b5cf19 100644 --- a/examples/demo/canvas/plot_clone_tool.py +++ b/examples/demo/canvas/plot_clone_tool.py @@ -9,6 +9,7 @@ # Chaco imports from chaco.api import AbstractOverlay +from chaco.chaco_traits import Optional from enable.tools.api import DragTool @@ -34,7 +35,7 @@ class PlotCloneTool(AbstractOverlay, DragTool): capture_mouse = True # The (x,y) position of the "last" mouse position we received - _offset = Union(None, Tuple) + _offset = Optional(Tuple) # The relative position of the mouse_down_position to the origin # of the plot's coordinate system diff --git a/examples/demo/canvas/transient_plot_overlay.py b/examples/demo/canvas/transient_plot_overlay.py index 0384625d8..d56e7a3f8 100644 --- a/examples/demo/canvas/transient_plot_overlay.py +++ b/examples/demo/canvas/transient_plot_overlay.py @@ -2,6 +2,7 @@ from traits.api import Enum, Float, Instance, Tuple, Union from chaco.api import AbstractOverlay, BasePlotContainer +from chaco.chaco_traits import Optional class TransientPlotOverlay(BasePlotContainer, AbstractOverlay): @@ -19,7 +20,7 @@ class TransientPlotOverlay(BasePlotContainer, AbstractOverlay): margin = Float(10) # An offset to apply in X and Y - offset = Union(None, Tuple) + offset = Optional(Tuple) # Override default values of some inherited traits unified_draw = True