Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate declarative interface attributes #2170

Merged
merged 5 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1202,7 +1230,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 @@ -1257,7 +1285,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 @@ -1306,7 +1334,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 @@ -1478,7 +1506,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 @@ -1514,7 +1542,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 @@ -1668,3 +1668,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)