Skip to content

Commit

Permalink
Merge pull request #2170 from sgdecker/attribute_validation
Browse files Browse the repository at this point in the history
Validate declarative interface attributes
  • Loading branch information
dcamron authored Jan 21, 2022
2 parents d0c1845 + 9c79896 commit 906990d
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 7 deletions.
42 changes: 35 additions & 7 deletions src/metpy/plots/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import contextlib
import copy
from datetime import datetime, timedelta
from difflib import get_close_matches
from itertools import cycle
import re

Expand Down Expand Up @@ -508,6 +509,33 @@ def plot_kwargs(data):
return kwargs


class ValidationMixin:
"""Provides validation of attribute names when set by user."""

def __setattr__(self, name, value):
"""Set only permitted attributes."""
allowlist = ['ax',
'data',
'handle',
'notify_change',
'panel'
]

allowlist.extend(self.trait_names())
if name in allowlist or name.startswith('_'):
super().__setattr__(name, value)
else:
closest = get_close_matches(name, allowlist, n=1)
if closest:
alt = closest[0]
suggest = f" Perhaps you meant '{alt}'?"
else:
suggest = ''
obj = self.__class__
msg = f"'{name}' is not a valid attribute for {obj}." + suggest
raise AttributeError(msg)


class MetPyHasTraits(HasTraits):
"""Provides modification layer on HasTraits for declarative classes."""

Expand All @@ -524,7 +552,7 @@ class Panel(MetPyHasTraits):


@exporter.export
class PanelContainer(MetPyHasTraits):
class PanelContainer(MetPyHasTraits, ValidationMixin):
"""Collects panels and set complete figure related settings (e.g., size)."""

size = Union([Tuple(Union([Int(), Float()]), Union([Int(), Float()])),
Expand Down Expand Up @@ -600,7 +628,7 @@ def copy(self):


@exporter.export
class MapPanel(Panel):
class MapPanel(Panel, ValidationMixin):
"""Set figure related elements for an individual panel.
Parameters that need to be set include collecting all plotting types
Expand Down Expand Up @@ -1243,7 +1271,7 @@ class ColorfillTraits(MetPyHasTraits):


@exporter.export
class ImagePlot(PlotScalar, ColorfillTraits):
class ImagePlot(PlotScalar, ColorfillTraits, ValidationMixin):
"""Make raster image using `~matplotlib.pyplot.imshow` for satellite or colored image."""

@observe('colormap', 'image_range')
Expand Down Expand Up @@ -1298,7 +1326,7 @@ def _build(self):


@exporter.export
class ContourPlot(PlotScalar, ContourTraits):
class ContourPlot(PlotScalar, ContourTraits, ValidationMixin):
"""Make contour plots by defining specific traits."""

linecolor = Unicode('black')
Expand Down Expand Up @@ -1347,7 +1375,7 @@ def _build(self):


@exporter.export
class FilledContourPlot(PlotScalar, ColorfillTraits, ContourTraits):
class FilledContourPlot(PlotScalar, ColorfillTraits, ContourTraits, ValidationMixin):
"""Make color-filled contours plots by defining appropriate traits."""

@observe('contours', 'colorbar', 'colormap')
Expand Down Expand Up @@ -1519,7 +1547,7 @@ def draw(self):


@exporter.export
class BarbPlot(PlotVector):
class BarbPlot(PlotVector, ValidationMixin):
"""Make plots of wind barbs on a map with traits to refine the look of plotted elements."""

barblength = Float(default_value=7)
Expand Down Expand Up @@ -1555,7 +1583,7 @@ def _build(self):


@exporter.export
class PlotObs(MetPyHasTraits):
class PlotObs(MetPyHasTraits, ValidationMixin):
"""The highest level class related to plotting observed surface and upperair data.
This class collects all common methods no matter whether plotting a upper-level or
Expand Down
18 changes: 18 additions & 0 deletions tests/plots/test_declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -1713,3 +1713,21 @@ def test_drop_traitlets_dir():
assert not dir(plot_obj())[0].startswith('_')
assert 'cross_validation_lock' in dir(plot_obj)
assert 'cross_validation_lock' not in dir(plot_obj())


@needs_cartopy
def test_attribute_error_suggest():
"""Test that a mistyped attribute name raises an exception with fix."""
with pytest.raises(AttributeError) as excinfo:
panel = MapPanel()
panel.pots = []
assert "Perhaps you meant 'plots'?" in str(excinfo.value)


@needs_cartopy
def test_attribute_error_no_suggest():
"""Test that a mistyped attribute name raises an exception w/o a fix."""
with pytest.raises(AttributeError) as excinfo:
panel = MapPanel()
panel.galaxy = 'Andromeda'
assert 'Perhaps you meant' not in str(excinfo.value)

0 comments on commit 906990d

Please sign in to comment.