Skip to content

Commit 5975388

Browse files
committed
Further improve unknown projection error message
1 parent 60bb13d commit 5975388

File tree

4 files changed

+67
-51
lines changed

4 files changed

+67
-51
lines changed

proplot/axes/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@
2727
# Register projections with package prefix to avoid conflicts
2828
# NOTE: We integrate with cartopy and basemap rather than using matplotlib's
2929
# native projection system. Therefore axes names are not part of public API.
30-
CLASSES = {} # track valid names
31-
for _cls in (CartesianAxes, PolarAxes, _BasemapAxes, _CartopyAxes, ThreeAxes):
30+
_cls_dict = {} # track valid names
31+
for _cls in (CartesianAxes, PolarAxes, _CartopyAxes, _BasemapAxes, ThreeAxes):
3232
for _name in (_cls._name, *_cls._name_aliases):
3333
with context._state_context(_cls, name='proplot_' + _name):
3434
mproj.register_projection(_cls)
35-
CLASSES[_name] = _cls
35+
_cls_dict[_name] = _cls
36+
_cls_table = '\n'.join(
37+
' ' + key + ' ' * (max(map(len, _cls_dict)) - len(key) + 7)
38+
+ ('GeoAxes' if cls.__name__[:1] == '_' else cls.__name__)
39+
for key, cls in _cls_dict.items()
40+
)

proplot/axes/geo.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -406,20 +406,20 @@ def __init__(self, *args, **kwargs):
406406
*args
407407
Passed to `matplotlib.axes.Axes`.
408408
autoextent : bool, optional
409-
*For cartopy axes only*. Whether to automatically adjust map bounds based
410-
on plotted content or enforce a global map extent (or a map bounded at the
411-
equator for polar projections). The extent can subsequently by adjusted
412-
with the `~GeoAxes.format` keywords `lonlim`, `latlim`, and `boundinglat`,
413-
or with `~cartopy.mpl.geoaxes.GeoAxes.set_extent`.
409+
*For cartopy axes only*. Whether to automatically adjust map bounds
410+
based on plotted content or enforce a global map extent (or a map bounded
411+
at the equator for polar projections). The extent can subsequently by
412+
adjusted with the `~GeoAxes.format` keywords `lonlim`, `latlim`, and
413+
`boundinglat`, or with `~cartopy.mpl.geoaxes.GeoAxes.set_extent`.
414414
Default is :rc:`cartopy.autoextent`.
415415
circular : bool, optional
416416
*For cartopy axes only*. Whether to bound polar projections with circles
417417
rather than squares. Note that outer gridline labels cannot be added to
418418
circularly bounded polar projections. Default is :rc:`cartopy.circular`.
419-
map_projection : `~mpl_toolkits.basemap.Basemap` or `~cartopy.crs.Projection`
420-
The cartopy or basemap projection instance. This is passed automatically
421-
when calling axes-creation commands
422-
like `~proplot.figure.Figure.add_subplot`.
419+
map_projection : `~cartopy.crs.Projection` or `~mpl_toolkits.basemap.Basemap`
420+
The cartopy or basemap projection instance. This is
421+
passed automatically when calling axes-creation
422+
commands like `~proplot.figure.Figure.add_subplot`.
423423
%(geo.format)s
424424
425425
Other parameters

proplot/constructor.py

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,13 @@
293293
+ ', '.join(map(repr, PROJS_MISSING))
294294
+ ' . Please consider updating cartopy.'
295295
)
296+
PROJS_TABLE = (
297+
'The known cartopy projection classes are:\n'
298+
+ '\n'.join(
299+
' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__
300+
for key, cls in PROJS.items()
301+
)
302+
)
296303

297304
# Geographic feature properties
298305
FEATURES_CARTOPY = { # positional arguments passed to NaturalEarthFeature
@@ -1393,6 +1400,7 @@ def Proj(name, basemap=None, **kwargs):
13931400
basemap = _not_none(basemap, rc['basemap'])
13941401
is_crs = Projection is not object and isinstance(name, Projection)
13951402
is_basemap = Basemap is not object and isinstance(name, Basemap)
1403+
include_axes = kwargs.pop('include_axes', False) # for error message
13961404
if is_crs or is_basemap:
13971405
proj = name
13981406
package = 'cartopy' if is_crs else 'basemap'
@@ -1429,18 +1437,18 @@ def Proj(name, basemap=None, **kwargs):
14291437
if 'latlim' in kwargs:
14301438
kwargs['llcrnrlat'], kwargs['urcrnrlat'] = kwargs.pop('latlim')
14311439
name = PROJ_ALIASES.get(name, name)
1432-
kwproj = PROJ_DEFAULTS.get(name, {}).copy()
1433-
kwproj.update(kwargs)
1434-
kwproj.setdefault('fix_aspect', True)
1435-
if kwproj.get('lon_0', 0) > 0:
1436-
kwproj['lon_0'] -= 360
1440+
proj_kw = PROJ_DEFAULTS.get(name, {}).copy()
1441+
proj_kw.update(kwargs)
1442+
proj_kw.setdefault('fix_aspect', True)
1443+
if proj_kw.get('lon_0', 0) > 0:
1444+
proj_kw['lon_0'] -= 360
14371445
if name[:2] in ('np', 'sp'):
1438-
kwproj.setdefault('round', True)
1446+
proj_kw.setdefault('round', True)
14391447
if name == 'geos':
1440-
kwproj.setdefault('rsphere', (6378137.00, 6356752.3142))
1448+
proj_kw.setdefault('rsphere', (6378137.00, 6356752.3142))
14411449
reso = _not_none(
1442-
reso=kwproj.pop('reso', None),
1443-
resolution=kwproj.pop('resolution', None),
1450+
reso=proj_kw.pop('reso', None),
1451+
resolution=proj_kw.pop('resolution', None),
14441452
default=rc['reso']
14451453
)
14461454
if reso in RESOS_BASEMAP:
@@ -1451,43 +1459,45 @@ def Proj(name, basemap=None, **kwargs):
14511459
+ ', '.join(map(repr, RESOS_BASEMAP))
14521460
+ '.'
14531461
)
1454-
kwproj.update({'resolution': reso, 'projection': name})
1462+
proj_kw.update({'resolution': reso, 'projection': name})
14551463
try:
1456-
proj = mbasemap.Basemap(**kwproj) # will raise helpful warning
1464+
proj = mbasemap.Basemap(**proj_kw) # will raise helpful warning
14571465
except ValueError as err:
1458-
msg = str(err)
1459-
msg = msg.replace('projection', 'basemap projection')
1460-
raise ValueError(msg) from None
1466+
message = str(err)
1467+
message = message.strip()
1468+
message = message.replace('projection', 'basemap projection')
1469+
message = message.replace('supported', 'known')
1470+
if include_axes:
1471+
from . import axes as paxes # avoid circular imports
1472+
message = message.replace('projection.', 'projection or axes subclass.')
1473+
message += '\nThe known axes subclasses are:\n' + paxes._cls_table
1474+
raise ValueError(message) from None
14611475

14621476
# Cartopy
14631477
# NOTE: Error message matches basemap invalid projection message
14641478
else:
14651479
import cartopy.crs as ccrs # noqa: F401
14661480
package = 'cartopy'
1467-
kwproj = {
1468-
PROJ_ALIASES_KW.get(key, key): value
1469-
for key, value in kwargs.items()
1470-
}
1471-
if name in PROJS:
1481+
proj_kw = {PROJ_ALIASES_KW.get(key, key): value for key, value in kwargs.items()} # noqa: E501
1482+
if 'boundinglat' in proj_kw:
1483+
raise ValueError('"boundinglat" must be passed to the ax.format() command for cartopy axes.') # noqa: E501
1484+
try:
14721485
crs = PROJS[name]
1473-
else:
1474-
maxlen = max(map(len, PROJS))
1475-
raise ValueError(
1476-
f'{name!r} is an unknown cartopy projection class.\n'
1477-
'The known cartopy projection classes are:\n'
1478-
+ '\n'.join(
1479-
' ' + key + ' ' * (maxlen - len(key) + 6) + crs.__name__
1480-
for key, crs in PROJS.items()
1481-
)
1486+
except KeyError:
1487+
message = f'{name!r} is an unknown cartopy projection class.\n'
1488+
message += 'The known cartopy projection classes are:\n'
1489+
message += '\n'.join(
1490+
' ' + key + ' ' * (max(map(len, PROJS)) - len(key) + 10) + cls.__name__
1491+
for key, cls in PROJS.items()
14821492
)
1493+
if include_axes:
1494+
from . import axes as paxes # avoid circular imports
1495+
message = message.replace('projection.', 'projection or axes subclass.')
1496+
message += '\nThe known axes subclasses are:\n' + paxes._cls_table
1497+
raise ValueError(message) from None
14831498
if name == 'geos': # fix common mistake
1484-
kwproj.pop('central_latitude', None)
1485-
if 'boundinglat' in kwproj:
1486-
raise ValueError(
1487-
'"boundinglat" must be passed to the ax.format() command '
1488-
'for cartopy axes.'
1489-
)
1490-
proj = crs(**kwproj)
1499+
proj_kw.pop('central_latitude', None)
1500+
proj = crs(**proj_kw)
14911501

14921502
proj._proj_package = package
14931503
return proj

proplot/figure.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -789,14 +789,15 @@ def _parse_proj(
789789
and constructor.Basemap is object
790790
):
791791
raise ValueError(
792-
f'Invalid projection name {proj!r}. Valid axes subclasses are '
793-
+ ', '.join(map(repr, paxes.CLASSES)) + '. If you are trying to '
794-
+ 'create a geographic axes then cartopy or basemap must be installed.'
792+
f'Invalid projection name {proj!r}. If you are trying to create a '
793+
'GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap '
794+
'projection then cartopy or basemap must be installed. Otherwise the '
795+
f'known axes subclasses are:\n{paxes._cls_table}'
795796
)
796797
# Search geographic projections
797798
# NOTE: Also raises errors due to unexpected projection type
798799
if name is None:
799-
proj = constructor.Proj(proj, basemap=basemap, **proj_kw)
800+
proj = constructor.Proj(proj, basemap=basemap, include_axes=True, **proj_kw)
800801
name = proj._proj_package
801802
kwargs['map_projection'] = proj
802803

0 commit comments

Comments
 (0)