From 0eed012644a2e7ddf05f232bffe6109b141442f3 Mon Sep 17 00:00:00 2001 From: Connor Cozad <23ccozad@gmail.com> Date: Wed, 19 May 2021 13:37:35 -0400 Subject: [PATCH] Add copy method for declarative interface (Fixes #1719) MapPanel needed an explicity written __copy__() to make sure the copy process worked correctly. All other classes in declarative.py did not need it. A .copy() method is also provided for ease of use. --- src/metpy/plots/declarative.py | 39 ++++++++++++++++++++++++++++++++- tests/plots/test_declarative.py | 34 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/metpy/plots/declarative.py b/src/metpy/plots/declarative.py index 686da82bcb5..9d82e7f5784 100644 --- a/src/metpy/plots/declarative.py +++ b/src/metpy/plots/declarative.py @@ -4,6 +4,7 @@ """Declarative plotting tools.""" import contextlib +import copy from datetime import datetime, timedelta import matplotlib.pyplot as plt @@ -568,6 +569,10 @@ def show(self): self.draw() plt.show() + def copy(self): + """Return a copy of the panel container.""" + return copy.copy(self) + @exporter.export class MapPanel(Panel): @@ -579,7 +584,7 @@ class MapPanel(Panel): projection, graphics area, and title. """ - parent = Instance(PanelContainer) + parent = Instance(PanelContainer, allow_none=True) layout = Tuple(Int(), Int(), Int(), default_value=(1, 1, 1)) layout.__doc__ = """A tuple that contains the description (nrows, ncols, index) of the @@ -754,6 +759,27 @@ def draw(self): self.ax.set_title(title) self._need_redraw = False + def __copy__(self): + """Return a copy of this MapPanel.""" + # Create new, blank instance of MapPanel + cls = self.__class__ + obj = cls.__new__(cls) + + # Copy each attribute from current MapPanel to new MapPanel + for name in self.trait_names(): + # The 'plots' attribute is a list. + # A copy must be made for each plot in the list. + if name == 'plots': + obj.plots = [copy.copy(plot) for plot in self.plots] + else: + setattr(obj, name, getattr(self, name)) + + return obj + + def copy(self): + """Return a copy of the panel.""" + return copy.copy(self) + @exporter.export class Plots2D(HasTraits): @@ -880,6 +906,10 @@ def name(self): ret += f'@{self.level:d}' return ret + def copy(self): + """Return a copy of the plot.""" + return copy.copy(self) + @exporter.export class PlotScalar(Plots2D): @@ -1007,6 +1037,9 @@ class ColorfillTraits(HasTraits): ``None``. """ + def copy(self): + return copy.copy(self) + @exporter.export class ImagePlot(PlotScalar, ColorfillTraits): @@ -1604,3 +1637,7 @@ def _build(self): if self.vector_field_length is not None: vector_kwargs['length'] = self.vector_field_length self.handle.plot_barb(u, v, **vector_kwargs) + + def copy(self): + """Return a copy of the plot.""" + return copy.copy(self) diff --git a/tests/plots/test_declarative.py b/tests/plots/test_declarative.py index f33df021cc5..b6bae6691da 100644 --- a/tests/plots/test_declarative.py +++ b/tests/plots/test_declarative.py @@ -1184,3 +1184,37 @@ def test_panel(): pc.panel = panel assert pc.panel is panel + + +@needs_cartopy +def test_copy(): + """Test that the copy method works for all classes in `declarative.py`.""" + # Copies of plot objects + objects = [ImagePlot(), ContourPlot(), FilledContourPlot(), BarbPlot(), PlotObs()] + + for obj in objects: + obj.time = datetime.now() + copied_obj = obj.copy() + assert obj is not copied_obj + assert obj.time == copied_obj.time + + # Copies of MapPanel and PanelContainer + obj = MapPanel() + obj.title = "Sample Text" + copied_obj = obj.copy() + assert obj is not copied_obj + assert obj.title == copied_obj.title + + obj = PanelContainer() + obj.size = (10, 10) + copied_obj = obj.copy() + assert obj is not copied_obj + assert obj.size == copied_obj.size + + # Copies of plots in MapPanels should not point to same location in memory + obj = MapPanel() + obj.plots = [PlotObs(), BarbPlot(), FilledContourPlot(), ContourPlot(), ImagePlot()] + copied_obj = obj.copy() + + for i in range(len(obj.plots)): + assert obj.plots[i] is not copied_obj.plots[i]