Skip to content

Commit 56d8c36

Browse files
committed
Merge branch 'main' into coarsen_reshape
* main: Improve error message for guess engine (pydata#5455) Refactor dataset groupby tests (pydata#5506) DOC: zarr note on encoding (pydata#5427) Allow plotting categorical data (pydata#5464)
2 parents 4b69c9f + eea7673 commit 56d8c36

19 files changed

+349
-257
lines changed

doc/whats-new.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ New Features
4646
By `Thomas Hirtz <https://github.com/thomashirtz>`_.
4747
- allow passing a function to ``combine_attrs`` (:pull:`4896`).
4848
By `Justus Magin <https://github.com/keewis>`_.
49+
- Allow plotting categorical data (:pull:`5464`).
50+
By `Jimmy Westling <https://github.com/illviljan>`_.
4951

5052
Breaking changes
5153
~~~~~~~~~~~~~~~~

xarray/backends/cfgrib_.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ def get_encoding(self):
9494

9595

9696
class CfgribfBackendEntrypoint(BackendEntrypoint):
97+
available = has_cfgrib
98+
9799
def guess_can_open(self, filename_or_obj):
98100
try:
99101
_, ext = os.path.splitext(filename_or_obj)
@@ -147,5 +149,4 @@ def open_dataset(
147149
return ds
148150

149151

150-
if has_cfgrib:
151-
BACKEND_ENTRYPOINTS["cfgrib"] = CfgribfBackendEntrypoint
152+
BACKEND_ENTRYPOINTS["cfgrib"] = CfgribfBackendEntrypoint

xarray/backends/h5netcdf_.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ def close(self, **kwargs):
337337

338338

339339
class H5netcdfBackendEntrypoint(BackendEntrypoint):
340+
available = has_h5netcdf
341+
340342
def guess_can_open(self, filename_or_obj):
341343
magic_number = try_read_magic_number_from_file_or_path(filename_or_obj)
342344
if magic_number is not None:
@@ -394,5 +396,4 @@ def open_dataset(
394396
return ds
395397

396398

397-
if has_h5netcdf:
398-
BACKEND_ENTRYPOINTS["h5netcdf"] = H5netcdfBackendEntrypoint
399+
BACKEND_ENTRYPOINTS["h5netcdf"] = H5netcdfBackendEntrypoint

xarray/backends/netCDF4_.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,8 @@ def close(self, **kwargs):
512512

513513

514514
class NetCDF4BackendEntrypoint(BackendEntrypoint):
515+
available = has_netcdf4
516+
515517
def guess_can_open(self, filename_or_obj):
516518
if isinstance(filename_or_obj, str) and is_remote_uri(filename_or_obj):
517519
return True
@@ -573,5 +575,4 @@ def open_dataset(
573575
return ds
574576

575577

576-
if has_netcdf4:
577-
BACKEND_ENTRYPOINTS["netcdf4"] = NetCDF4BackendEntrypoint
578+
BACKEND_ENTRYPOINTS["netcdf4"] = NetCDF4BackendEntrypoint

xarray/backends/plugins.py

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ def sort_backends(backend_entrypoints):
8181

8282

8383
def build_engines(pkg_entrypoints):
84-
backend_entrypoints = BACKEND_ENTRYPOINTS.copy()
84+
backend_entrypoints = {}
85+
for backend_name, backend in BACKEND_ENTRYPOINTS.items():
86+
if backend.available:
87+
backend_entrypoints[backend_name] = backend
8588
pkg_entrypoints = remove_duplicates(pkg_entrypoints)
8689
external_backend_entrypoints = backends_dict_from_pkg(pkg_entrypoints)
8790
backend_entrypoints.update(external_backend_entrypoints)
@@ -101,30 +104,49 @@ def guess_engine(store_spec):
101104

102105
for engine, backend in engines.items():
103106
try:
104-
if backend.guess_can_open and backend.guess_can_open(store_spec):
107+
if backend.guess_can_open(store_spec):
105108
return engine
106109
except Exception:
107110
warnings.warn(f"{engine!r} fails while guessing", RuntimeWarning)
108111

109-
installed = [k for k in engines if k != "store"]
110-
if installed:
111-
raise ValueError(
112-
"did not find a match in any of xarray's currently installed IO "
113-
f"backends {installed}. Consider explicitly selecting one of the "
114-
"installed backends via the ``engine`` parameter to "
115-
"xarray.open_dataset(), or installing additional IO dependencies:\n"
116-
"http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n"
117-
"http://xarray.pydata.org/en/stable/user-guide/io.html"
118-
)
112+
compatible_engines = []
113+
for engine, backend_cls in BACKEND_ENTRYPOINTS.items():
114+
try:
115+
backend = backend_cls()
116+
if backend.guess_can_open(store_spec):
117+
compatible_engines.append(engine)
118+
except Exception:
119+
warnings.warn(f"{engine!r} fails while guessing", RuntimeWarning)
120+
121+
installed_engines = [k for k in engines if k != "store"]
122+
if not compatible_engines:
123+
if installed_engines:
124+
error_msg = (
125+
"did not find a match in any of xarray's currently installed IO "
126+
f"backends {installed_engines}. Consider explicitly selecting one of the "
127+
"installed engines via the ``engine`` parameter, or installing "
128+
"additional IO dependencies, see:\n"
129+
"http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n"
130+
"http://xarray.pydata.org/en/stable/user-guide/io.html"
131+
)
132+
else:
133+
error_msg = (
134+
"xarray is unable to open this file because it has no currently "
135+
"installed IO backends. Xarray's read/write support requires "
136+
"installing optional IO dependencies, see:\n"
137+
"http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n"
138+
"http://xarray.pydata.org/en/stable/user-guide/io"
139+
)
119140
else:
120-
raise ValueError(
121-
"xarray is unable to open this file because it has no currently "
122-
"installed IO backends. Xarray's read/write support requires "
123-
"installing optional dependencies:\n"
124-
"http://xarray.pydata.org/en/stable/getting-started-guide/installing.html\n"
125-
"http://xarray.pydata.org/en/stable/user-guide/io.html"
141+
error_msg = (
142+
"found the following matches with the input file in xarray's IO "
143+
f"backends: {compatible_engines}. But their dependencies may not be installed, see:\n"
144+
"http://xarray.pydata.org/en/stable/user-guide/io.html \n"
145+
"http://xarray.pydata.org/en/stable/getting-started-guide/installing.html"
126146
)
127147

148+
raise ValueError(error_msg)
149+
128150

129151
def get_backend(engine):
130152
"""Select open_dataset method based on current engine."""

xarray/backends/pseudonetcdf_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def close(self):
102102

103103

104104
class PseudoNetCDFBackendEntrypoint(BackendEntrypoint):
105+
available = has_pseudonetcdf
105106

106107
# *args and **kwargs are not allowed in open_backend_dataset_ kwargs,
107108
# unless the open_dataset_parameters are explicity defined like this:
@@ -153,5 +154,4 @@ def open_dataset(
153154
return ds
154155

155156

156-
if has_pseudonetcdf:
157-
BACKEND_ENTRYPOINTS["pseudonetcdf"] = PseudoNetCDFBackendEntrypoint
157+
BACKEND_ENTRYPOINTS["pseudonetcdf"] = PseudoNetCDFBackendEntrypoint

xarray/backends/pydap_.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def get_dimensions(self):
110110

111111

112112
class PydapBackendEntrypoint(BackendEntrypoint):
113+
available = has_pydap
114+
113115
def guess_can_open(self, filename_or_obj):
114116
return isinstance(filename_or_obj, str) and is_remote_uri(filename_or_obj)
115117

@@ -154,5 +156,4 @@ def open_dataset(
154156
return ds
155157

156158

157-
if has_pydap:
158-
BACKEND_ENTRYPOINTS["pydap"] = PydapBackendEntrypoint
159+
BACKEND_ENTRYPOINTS["pydap"] = PydapBackendEntrypoint

xarray/backends/pynio_.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ def close(self):
9999

100100

101101
class PynioBackendEntrypoint(BackendEntrypoint):
102+
available = has_pynio
103+
102104
def open_dataset(
103105
self,
104106
filename_or_obj,
@@ -112,13 +114,13 @@ def open_dataset(
112114
mode="r",
113115
lock=None,
114116
):
117+
filename_or_obj = _normalize_path(filename_or_obj)
115118
store = NioDataStore(
116119
filename_or_obj,
117120
mode=mode,
118121
lock=lock,
119122
)
120123

121-
filename_or_obj = _normalize_path(filename_or_obj)
122124
store_entrypoint = StoreBackendEntrypoint()
123125
with close_on_error(store):
124126
ds = store_entrypoint.open_dataset(
@@ -134,5 +136,4 @@ def open_dataset(
134136
return ds
135137

136138

137-
if has_pynio:
138-
BACKEND_ENTRYPOINTS["pynio"] = PynioBackendEntrypoint
139+
BACKEND_ENTRYPOINTS["pynio"] = PynioBackendEntrypoint

xarray/backends/scipy_.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ def close(self):
238238

239239

240240
class ScipyBackendEntrypoint(BackendEntrypoint):
241+
available = has_scipy
242+
241243
def guess_can_open(self, filename_or_obj):
242244

243245
magic_number = try_read_magic_number_from_file_or_path(filename_or_obj)
@@ -290,5 +292,4 @@ def open_dataset(
290292
return ds
291293

292294

293-
if has_scipy:
294-
BACKEND_ENTRYPOINTS["scipy"] = ScipyBackendEntrypoint
295+
BACKEND_ENTRYPOINTS["scipy"] = ScipyBackendEntrypoint

xarray/backends/store.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55

66
class StoreBackendEntrypoint(BackendEntrypoint):
7+
available = True
8+
79
def guess_can_open(self, filename_or_obj):
810
return isinstance(filename_or_obj, AbstractDataStore)
911

xarray/backends/zarr.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,15 @@ def open_zarr(
785785

786786

787787
class ZarrBackendEntrypoint(BackendEntrypoint):
788+
available = has_zarr
789+
790+
def guess_can_open(self, filename_or_obj):
791+
try:
792+
_, ext = os.path.splitext(filename_or_obj)
793+
except TypeError:
794+
return False
795+
return ext in {".zarr"}
796+
788797
def open_dataset(
789798
self,
790799
filename_or_obj,
@@ -840,5 +849,4 @@ def open_dataset(
840849
return ds
841850

842851

843-
if has_zarr:
844-
BACKEND_ENTRYPOINTS["zarr"] = ZarrBackendEntrypoint
852+
BACKEND_ENTRYPOINTS["zarr"] = ZarrBackendEntrypoint

xarray/core/dataset.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,10 @@ def to_zarr(
20012001
If not other chunks are found, Zarr uses its own heuristics to
20022002
choose automatic chunk sizes.
20032003
2004+
encoding:
2005+
The encoding attribute (if exists) of the DataArray(s) will be
2006+
used. Override any existing encodings by providing the ``encoding`` kwarg.
2007+
20042008
See Also
20052009
--------
20062010
:ref:`io.zarr`

xarray/plot/plot.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -925,18 +925,26 @@ def imshow(x, y, z, ax, **kwargs):
925925
"imshow requires 1D coordinates, try using pcolormesh or contour(f)"
926926
)
927927

928-
# Centering the pixels- Assumes uniform spacing
929-
try:
930-
xstep = (x[1] - x[0]) / 2.0
931-
except IndexError:
932-
# Arbitrary default value, similar to matplotlib behaviour
933-
xstep = 0.1
934-
try:
935-
ystep = (y[1] - y[0]) / 2.0
936-
except IndexError:
937-
ystep = 0.1
938-
left, right = x[0] - xstep, x[-1] + xstep
939-
bottom, top = y[-1] + ystep, y[0] - ystep
928+
def _center_pixels(x):
929+
"""Center the pixels on the coordinates."""
930+
if np.issubdtype(x.dtype, str):
931+
# When using strings as inputs imshow converts it to
932+
# integers. Choose extent values which puts the indices in
933+
# in the center of the pixels:
934+
return 0 - 0.5, len(x) - 0.5
935+
936+
try:
937+
# Center the pixels assuming uniform spacing:
938+
xstep = 0.5 * (x[1] - x[0])
939+
except IndexError:
940+
# Arbitrary default value, similar to matplotlib behaviour:
941+
xstep = 0.1
942+
943+
return x[0] - xstep, x[-1] + xstep
944+
945+
# Center the pixels:
946+
left, right = _center_pixels(x)
947+
top, bottom = _center_pixels(y)
940948

941949
defaults = {"origin": "upper", "interpolation": "nearest"}
942950

@@ -967,6 +975,13 @@ def imshow(x, y, z, ax, **kwargs):
967975

968976
primitive = ax.imshow(z, **defaults)
969977

978+
# If x or y are strings the ticklabels have been replaced with
979+
# integer indices. Replace them back to strings:
980+
for axis, v in [("x", x), ("y", y)]:
981+
if np.issubdtype(v.dtype, str):
982+
getattr(ax, f"set_{axis}ticks")(np.arange(len(v)))
983+
getattr(ax, f"set_{axis}ticklabels")(v)
984+
970985
return primitive
971986

972987

@@ -1011,9 +1026,13 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs):
10111026
else:
10121027
infer_intervals = True
10131028

1014-
if infer_intervals and (
1015-
(np.shape(x)[0] == np.shape(z)[1])
1016-
or ((x.ndim > 1) and (np.shape(x)[1] == np.shape(z)[1]))
1029+
if (
1030+
infer_intervals
1031+
and not np.issubdtype(x.dtype, str)
1032+
and (
1033+
(np.shape(x)[0] == np.shape(z)[1])
1034+
or ((x.ndim > 1) and (np.shape(x)[1] == np.shape(z)[1]))
1035+
)
10171036
):
10181037
if len(x.shape) == 1:
10191038
x = _infer_interval_breaks(x, check_monotonic=True)
@@ -1022,7 +1041,11 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs):
10221041
x = _infer_interval_breaks(x, axis=1)
10231042
x = _infer_interval_breaks(x, axis=0)
10241043

1025-
if infer_intervals and (np.shape(y)[0] == np.shape(z)[0]):
1044+
if (
1045+
infer_intervals
1046+
and not np.issubdtype(y.dtype, str)
1047+
and (np.shape(y)[0] == np.shape(z)[0])
1048+
):
10261049
if len(y.shape) == 1:
10271050
y = _infer_interval_breaks(y, check_monotonic=True)
10281051
else:

xarray/plot/utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,14 @@ def _ensure_plottable(*args):
604604
Raise exception if there is anything in args that can't be plotted on an
605605
axis by matplotlib.
606606
"""
607-
numpy_types = [np.floating, np.integer, np.timedelta64, np.datetime64, np.bool_]
607+
numpy_types = [
608+
np.floating,
609+
np.integer,
610+
np.timedelta64,
611+
np.datetime64,
612+
np.bool_,
613+
np.str_,
614+
]
608615
other_types = [datetime]
609616
try:
610617
import cftime

0 commit comments

Comments
 (0)