From ba48fbcd6ee14e0bbd8887a970a1125fde6769f0 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Thu, 24 Oct 2019 12:48:45 -0400 Subject: [PATCH 01/11] Html repr (#3425) * add CSS style and internal functions for html repr * move CSS code to its own file in a new static directory * add repr of array objects + some refactoring and fixes * add _repr_html_ methods to dataset, dataarray and variable * fix encoding issue in read CSS * fix some CSS for compatibility with notebook (tested 5.2) * use CSS grid + add icons to show/hide attrs and data repr * Changing title of icons to make tooltips better * Adding option to set repr back to classic * Adding support for multiindexes * Getting rid of some spans and fixing alignment * Forgot to check in css [skip ci] * Overflow on hover * Cleaning up css * Fixing indentation * Replacing + icon with db icon * Unifying input css * Renaming stylesheet [skip ci] * Improving styling of attributes * Using the repr functions * Using dask array _repr_html_ * Fixing alignment of Dimensions * Make sure to include subdirs in package * Adding static to manifest * Trying to include css files * Fixing css discrepancies in colab * Adding in lots of escapes and also f-strings * Adding some tests for formatting_html * linting * classic -> text * linting more * Adding tests for new option * Trying to get better coverage * reformatting * Fixing up test * Last tests hopefully * Fixing dask test to work with lower version * More black * Added what's new section * classic -> text Co-Authored-By: Deepak Cherian * Fixing up dt/dl for jlab * Directly change dl objects for attrs section --- MANIFEST.in | 1 + doc/whats-new.rst | 6 + setup.py | 4 +- xarray/core/common.py | 10 +- xarray/core/dataset.py | 7 + xarray/core/formatting_html.py | 274 ++++++++++++++++++++ xarray/core/options.py | 7 + xarray/static/css/style.css | 310 +++++++++++++++++++++++ xarray/static/html/icons-svg-inline.html | 17 ++ xarray/tests/test_formatting_html.py | 132 ++++++++++ xarray/tests/test_options.py | 37 +++ 11 files changed, 802 insertions(+), 3 deletions(-) mode change 100644 => 100755 setup.py create mode 100644 xarray/core/formatting_html.py create mode 100644 xarray/static/css/style.css create mode 100644 xarray/static/html/icons-svg-inline.html create mode 100644 xarray/tests/test_formatting_html.py diff --git a/MANIFEST.in b/MANIFEST.in index a006660e5fb..4d5c34f622c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,3 +6,4 @@ prune doc/generated global-exclude .DS_Store include versioneer.py include xarray/_version.py +recursive-include xarray/static * diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 9d3e64badb8..12bed8f332e 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -36,6 +36,12 @@ New Features ``pip install git+https://github.com/andrewgsavage/pint.git@refs/pull/6/head)``. Even with it, interaction with non-numpy array libraries, e.g. dask or sparse, is broken. +- Added new :py:meth:`Dataset._repr_html_` and :py:meth:`DataArray._repr_html_` to improve + representation of objects in jupyter. By default this feature is turned off + for now. Enable it with :py:meth:`xarray.set_options(display_style="html")`. + (:pull:`3425`) by `Benoit Bovy `_ and + `Julia Signell `_. + Bug fixes ~~~~~~~~~ - Fix regression introduced in v0.14.0 that would cause a crash if dask is installed diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 08d4f54764f..cba0c74aa3a --- a/setup.py +++ b/setup.py @@ -104,5 +104,7 @@ tests_require=TESTS_REQUIRE, url=URL, packages=find_packages(), - package_data={"xarray": ["py.typed", "tests/data/*"]}, + package_data={ + "xarray": ["py.typed", "tests/data/*", "static/css/*", "static/html/*"] + }, ) diff --git a/xarray/core/common.py b/xarray/core/common.py index 45d860a1797..1a8cf34ed39 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -1,5 +1,6 @@ import warnings from contextlib import suppress +from html import escape from textwrap import dedent from typing import ( Any, @@ -18,10 +19,10 @@ import numpy as np import pandas as pd -from . import dtypes, duck_array_ops, formatting, ops +from . import dtypes, duck_array_ops, formatting, formatting_html, ops from .arithmetic import SupportsArithmetic from .npcompat import DTypeLike -from .options import _get_keep_attrs +from .options import OPTIONS, _get_keep_attrs from .pycompat import dask_array_type from .rolling_exp import RollingExp from .utils import Frozen, ReprObject, either_dict_or_kwargs @@ -134,6 +135,11 @@ def __array__(self: Any, dtype: DTypeLike = None) -> np.ndarray: def __repr__(self) -> str: return formatting.array_repr(self) + def _repr_html_(self): + if OPTIONS["display_style"] == "text": + return f"
{escape(repr(self))}
" + return formatting_html.array_repr(self) + def _iter(self: Any) -> Iterator[Any]: for n in range(len(self)): yield self[n] diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 12d5cbdc9f3..eba580f84bd 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -3,6 +3,7 @@ import sys import warnings from collections import defaultdict +from html import escape from numbers import Number from pathlib import Path from typing import ( @@ -39,6 +40,7 @@ dtypes, duck_array_ops, formatting, + formatting_html, groupby, ops, resample, @@ -1619,6 +1621,11 @@ def to_zarr( def __repr__(self) -> str: return formatting.dataset_repr(self) + def _repr_html_(self): + if OPTIONS["display_style"] == "text": + return f"
{escape(repr(self))}
" + return formatting_html.dataset_repr(self) + def info(self, buf=None) -> None: """ Concise summary of a Dataset variables and attributes. diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py new file mode 100644 index 00000000000..b03ecc12962 --- /dev/null +++ b/xarray/core/formatting_html.py @@ -0,0 +1,274 @@ +import uuid +import pkg_resources +from collections import OrderedDict +from functools import partial +from html import escape + +from .formatting import inline_variable_array_repr, short_data_repr + + +CSS_FILE_PATH = "/".join(("static", "css", "style.css")) +CSS_STYLE = pkg_resources.resource_string("xarray", CSS_FILE_PATH).decode("utf8") + + +ICONS_SVG_PATH = "/".join(("static", "html", "icons-svg-inline.html")) +ICONS_SVG = pkg_resources.resource_string("xarray", ICONS_SVG_PATH).decode("utf8") + + +def short_data_repr_html(array): + """Format "data" for DataArray and Variable.""" + internal_data = getattr(array, "variable", array)._data + if hasattr(internal_data, "_repr_html_"): + return internal_data._repr_html_() + return escape(short_data_repr(array)) + + +def format_dims(dims, coord_names): + if not dims: + return "" + + dim_css_map = { + k: " class='xr-has-index'" if k in coord_names else "" for k, v in dims.items() + } + + dims_li = "".join( + f"
  • " f"{escape(dim)}: {size}
  • " + for dim, size in dims.items() + ) + + return f"
      {dims_li}
    " + + +def summarize_attrs(attrs): + attrs_dl = "".join( + f"
    {escape(k)} :
    " f"
    {escape(str(v))}
    " + for k, v in attrs.items() + ) + + return f"
    {attrs_dl}
    " + + +def _icon(icon_name): + # icon_name should be defined in xarray/static/html/icon-svg-inline.html + return ( + "" + "" + "" + "".format(icon_name) + ) + + +def _summarize_coord_multiindex(name, coord): + preview = f"({', '.join(escape(l) for l in coord.level_names)})" + return summarize_variable( + name, coord, is_index=True, dtype="MultiIndex", preview=preview + ) + + +def summarize_coord(name, var): + is_index = name in var.dims + if is_index: + coord = var.variable.to_index_variable() + if coord.level_names is not None: + coords = {} + coords[name] = _summarize_coord_multiindex(name, coord) + for lname in coord.level_names: + var = coord.get_level_variable(lname) + coords[lname] = summarize_variable(lname, var) + return coords + + return {name: summarize_variable(name, var, is_index)} + + +def summarize_coords(variables): + coords = {} + for k, v in variables.items(): + coords.update(**summarize_coord(k, v)) + + vars_li = "".join(f"
  • {v}
  • " for v in coords.values()) + + return f"
      {vars_li}
    " + + +def summarize_variable(name, var, is_index=False, dtype=None, preview=None): + variable = var.variable if hasattr(var, "variable") else var + + cssclass_idx = " class='xr-has-index'" if is_index else "" + dims_str = f"({', '.join(escape(dim) for dim in var.dims)})" + name = escape(name) + dtype = dtype or var.dtype + + # "unique" ids required to expand/collapse subsections + attrs_id = "attrs-" + str(uuid.uuid4()) + data_id = "data-" + str(uuid.uuid4()) + disabled = "" if len(var.attrs) else "disabled" + + preview = preview or escape(inline_variable_array_repr(variable, 35)) + attrs_ul = summarize_attrs(var.attrs) + data_repr = short_data_repr_html(variable) + + attrs_icon = _icon("icon-file-text2") + data_icon = _icon("icon-database") + + return ( + f"
    {name}
    " + f"
    {dims_str}
    " + f"
    {dtype}
    " + f"
    {preview}
    " + f"" + f"" + f"" + f"" + f"
    {attrs_ul}
    " + f"
    {data_repr}
    " + ) + + +def summarize_vars(variables): + vars_li = "".join( + f"
  • {summarize_variable(k, v)}
  • " + for k, v in variables.items() + ) + + return f"
      {vars_li}
    " + + +def collapsible_section( + name, inline_details="", details="", n_items=None, enabled=True, collapsed=False +): + # "unique" id to expand/collapse the section + data_id = "section-" + str(uuid.uuid4()) + + has_items = n_items is not None and n_items + n_items_span = "" if n_items is None else f" ({n_items})" + enabled = "" if enabled and has_items else "disabled" + collapsed = "" if collapsed or not has_items else "checked" + tip = " title='Expand/collapse section'" if enabled else "" + + return ( + f"" + f"" + f"
    {inline_details}
    " + f"
    {details}
    " + ) + + +def _mapping_section(mapping, name, details_func, max_items_collapse, enabled=True): + n_items = len(mapping) + collapsed = n_items >= max_items_collapse + + return collapsible_section( + name, + details=details_func(mapping), + n_items=n_items, + enabled=enabled, + collapsed=collapsed, + ) + + +def dim_section(obj): + dim_list = format_dims(obj.dims, list(obj.coords)) + + return collapsible_section( + "Dimensions", inline_details=dim_list, enabled=False, collapsed=True + ) + + +def array_section(obj): + # "unique" id to expand/collapse the section + data_id = "section-" + str(uuid.uuid4()) + collapsed = "" + preview = escape(inline_variable_array_repr(obj.variable, max_width=70)) + data_repr = short_data_repr_html(obj) + data_icon = _icon("icon-database") + + return ( + "
    " + f"" + f"" + f"
    {preview}
    " + f"
    {data_repr}
    " + "
    " + ) + + +coord_section = partial( + _mapping_section, + name="Coordinates", + details_func=summarize_coords, + max_items_collapse=25, +) + + +datavar_section = partial( + _mapping_section, + name="Data variables", + details_func=summarize_vars, + max_items_collapse=15, +) + + +attr_section = partial( + _mapping_section, + name="Attributes", + details_func=summarize_attrs, + max_items_collapse=10, +) + + +def _obj_repr(header_components, sections): + header = f"
    {''.join(h for h in header_components)}
    " + sections = "".join(f"
  • {s}
  • " for s in sections) + + return ( + "
    " + f"{ICONS_SVG}" + "
    " + f"{header}" + f"
      {sections}
    " + "
    " + "
    " + ) + + +def array_repr(arr): + dims = OrderedDict((k, v) for k, v in zip(arr.dims, arr.shape)) + + obj_type = "xarray.{}".format(type(arr).__name__) + arr_name = "'{}'".format(arr.name) if getattr(arr, "name", None) else "" + coord_names = list(arr.coords) if hasattr(arr, "coords") else [] + + header_components = [ + "
    {}
    ".format(obj_type), + "
    {}
    ".format(arr_name), + format_dims(dims, coord_names), + ] + + sections = [array_section(arr)] + + if hasattr(arr, "coords"): + sections.append(coord_section(arr.coords)) + + sections.append(attr_section(arr.attrs)) + + return _obj_repr(header_components, sections) + + +def dataset_repr(ds): + obj_type = "xarray.{}".format(type(ds).__name__) + + header_components = [f"
    {escape(obj_type)}
    "] + + sections = [ + dim_section(ds), + coord_section(ds.coords), + datavar_section(ds.data_vars), + attr_section(ds.attrs), + ] + + return _obj_repr(header_components, sections) diff --git a/xarray/core/options.py b/xarray/core/options.py index 2f464a33fb1..72f9ad8e1fa 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -8,6 +8,7 @@ CMAP_SEQUENTIAL = "cmap_sequential" CMAP_DIVERGENT = "cmap_divergent" KEEP_ATTRS = "keep_attrs" +DISPLAY_STYLE = "display_style" OPTIONS = { @@ -19,9 +20,11 @@ CMAP_SEQUENTIAL: "viridis", CMAP_DIVERGENT: "RdBu_r", KEEP_ATTRS: "default", + DISPLAY_STYLE: "text", } _JOIN_OPTIONS = frozenset(["inner", "outer", "left", "right", "exact"]) +_DISPLAY_OPTIONS = frozenset(["text", "html"]) def _positive_integer(value): @@ -35,6 +38,7 @@ def _positive_integer(value): FILE_CACHE_MAXSIZE: _positive_integer, WARN_FOR_UNCLOSED_FILES: lambda value: isinstance(value, bool), KEEP_ATTRS: lambda choice: choice in [True, False, "default"], + DISPLAY_STYLE: _DISPLAY_OPTIONS.__contains__, } @@ -98,6 +102,9 @@ class set_options: attrs, ``False`` to always discard them, or ``'default'`` to use original logic that attrs should only be kept in unambiguous circumstances. Default: ``'default'``. + - ``display_style``: display style to use in jupyter for xarray objects. + Default: ``'text'``. Other options are ``'html'``. + You can use ``set_options`` either as a context manager: diff --git a/xarray/static/css/style.css b/xarray/static/css/style.css new file mode 100644 index 00000000000..536b8ab6103 --- /dev/null +++ b/xarray/static/css/style.css @@ -0,0 +1,310 @@ +/* CSS stylesheet for displaying xarray objects in jupyterlab. + * + */ + +.xr-wrap { + min-width: 300px; + max-width: 700px; +} + +.xr-header { + padding-top: 6px; + padding-bottom: 6px; + margin-bottom: 4px; + border-bottom: solid 1px #ddd; +} + +.xr-header > div, +.xr-header > ul { + display: inline; + margin-top: 0; + margin-bottom: 0; +} + +.xr-obj-type, +.xr-array-name { + margin-left: 2px; + margin-right: 10px; +} + +.xr-obj-type { + color: #555; +} + +.xr-array-name { + color: #000; +} + +.xr-sections { + padding-left: 0 !important; + display: grid; + grid-template-columns: 150px auto auto 1fr 20px 20px; +} + +.xr-section-item { + display: contents; +} + +.xr-section-item input { + display: none; +} + +.xr-section-item input + label { + color: #ccc; +} + +.xr-section-item input:enabled + label { + cursor: pointer; + color: #555; +} + +.xr-section-item input:enabled + label:hover { + color: #000; +} + +.xr-section-summary { + grid-column: 1; + color: #555; + font-weight: 500; +} + +.xr-section-summary > span { + display: inline-block; + padding-left: 0.5em; +} + +.xr-section-summary-in:disabled + label { + color: #555; +} + +.xr-section-summary-in + label:before { + display: inline-block; + content: '►'; + font-size: 11px; + width: 15px; + text-align: center; +} + +.xr-section-summary-in:disabled + label:before { + color: #ccc; +} + +.xr-section-summary-in:checked + label:before { + content: '▼'; +} + +.xr-section-summary-in:checked + label > span { + display: none; +} + +.xr-section-summary, +.xr-section-inline-details { + padding-top: 4px; + padding-bottom: 4px; +} + +.xr-section-inline-details { + grid-column: 2 / -1; +} + +.xr-section-details { + display: none; + grid-column: 1 / -1; + margin-bottom: 5px; +} + +.xr-section-summary-in:checked ~ .xr-section-details { + display: contents; +} + +.xr-array-wrap { + grid-column: 1 / -1; + display: grid; + grid-template-columns: 20px auto; +} + +.xr-array-wrap > label { + grid-column: 1; + vertical-align: top; +} + +.xr-preview { + color: #888; +} + +.xr-array-preview, +.xr-array-data { + padding: 0 5px !important; + grid-column: 2; +} + +.xr-array-data, +.xr-array-in:checked ~ .xr-array-preview { + display: none; +} + +.xr-array-in:checked ~ .xr-array-data, +.xr-array-preview { + display: inline-block; +} + +.xr-dim-list { + display: inline-block !important; + list-style: none; + padding: 0 !important; + margin: 0; +} + +.xr-dim-list li { + display: inline-block; + padding: 0; + margin: 0; +} + +.xr-dim-list:before { + content: '('; +} + +.xr-dim-list:after { + content: ')'; +} + +.xr-dim-list li:not(:last-child):after { + content: ','; + padding-right: 5px; +} + +.xr-has-index { + font-weight: bold; +} + +.xr-var-list, +.xr-var-item { + display: contents; +} + +.xr-var-item > div, +.xr-var-item label, +.xr-var-item > .xr-var-name span { + background-color: #fcfcfc; + margin-bottom: 0; +} + +.xr-var-item > .xr-var-name:hover span { + padding-right: 5px; +} + +.xr-var-list > li:nth-child(odd) > div, +.xr-var-list > li:nth-child(odd) > label, +.xr-var-list > li:nth-child(odd) > .xr-var-name span { + background-color: #efefef; +} + +.xr-var-name { + grid-column: 1; +} + +.xr-var-dims { + grid-column: 2; +} + +.xr-var-dtype { + grid-column: 3; + text-align: right; + color: #555; +} + +.xr-var-preview { + grid-column: 4; +} + +.xr-var-name, +.xr-var-dims, +.xr-var-dtype, +.xr-preview, +.xr-attrs dt { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 10px; +} + +.xr-var-name:hover, +.xr-var-dims:hover, +.xr-var-dtype:hover, +.xr-attrs dt:hover { + overflow: visible; + width: auto; + z-index: 1; +} + +.xr-var-attrs, +.xr-var-data { + display: none; + background-color: #fff !important; + padding-bottom: 5px !important; +} + +.xr-var-attrs-in:checked ~ .xr-var-attrs, +.xr-var-data-in:checked ~ .xr-var-data { + display: block; +} + +.xr-var-data > table { + float: right; +} + +.xr-var-name span, +.xr-var-data, +.xr-attrs { + padding-left: 25px !important; +} + +.xr-attrs, +.xr-var-attrs, +.xr-var-data { + grid-column: 1 / -1; +} + +dl.xr-attrs { + padding: 0; + margin: 0; + display: grid; + grid-template-columns: 125px auto; +} + +.xr-attrs dt, dd { + padding: 0; + margin: 0; + float: left; + padding-right: 10px; + width: auto; +} + +.xr-attrs dt { + font-weight: normal; + grid-column: 1; +} + +.xr-attrs dt:hover span { + display: inline-block; + background: #fff; + padding-right: 10px; +} + +.xr-attrs dd { + grid-column: 2; + white-space: pre-wrap; + word-break: break-all; +} + +.xr-icon-database, +.xr-icon-file-text2 { + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1.5em !important; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; +} diff --git a/xarray/static/html/icons-svg-inline.html b/xarray/static/html/icons-svg-inline.html new file mode 100644 index 00000000000..c44f89c4304 --- /dev/null +++ b/xarray/static/html/icons-svg-inline.html @@ -0,0 +1,17 @@ + + + +Show/Hide data repr + + + + + +Show/Hide attributes + + + + + + + diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py new file mode 100644 index 00000000000..e7f54b22d06 --- /dev/null +++ b/xarray/tests/test_formatting_html.py @@ -0,0 +1,132 @@ +from distutils.version import LooseVersion + +import numpy as np +import pandas as pd +import pytest + +import xarray as xr +from xarray.core import formatting_html as fh + + +@pytest.fixture +def dataarray(): + return xr.DataArray(np.random.RandomState(0).randn(4, 6)) + + +@pytest.fixture +def dask_dataarray(dataarray): + pytest.importorskip("dask") + return dataarray.chunk() + + +@pytest.fixture +def multiindex(): + mindex = pd.MultiIndex.from_product( + [["a", "b"], [1, 2]], names=("level_1", "level_2") + ) + return xr.Dataset({}, {"x": mindex}) + + +@pytest.fixture +def dataset(): + times = pd.date_range("2000-01-01", "2001-12-31", name="time") + annual_cycle = np.sin(2 * np.pi * (times.dayofyear.values / 365.25 - 0.28)) + + base = 10 + 15 * annual_cycle.reshape(-1, 1) + tmin_values = base + 3 * np.random.randn(annual_cycle.size, 3) + tmax_values = base + 10 + 3 * np.random.randn(annual_cycle.size, 3) + + return xr.Dataset( + { + "tmin": (("time", "location"), tmin_values), + "tmax": (("time", "location"), tmax_values), + }, + {"time": times, "location": ["", "IN", "IL"]}, + attrs={"description": "Test data."}, + ) + + +def test_short_data_repr_html(dataarray): + data_repr = fh.short_data_repr_html(dataarray) + assert data_repr.startswith("array") + + +def test_short_data_repr_html_dask(dask_dataarray): + import dask + + if LooseVersion(dask.__version__) < "2.0.0": + assert not hasattr(dask_dataarray.data, "_repr_html_") + data_repr = fh.short_data_repr_html(dask_dataarray) + assert ( + data_repr + == "dask.array<xarray-<this-array>, shape=(4, 6), dtype=float64, chunksize=(4, 6)>" + ) + else: + assert hasattr(dask_dataarray.data, "_repr_html_") + data_repr = fh.short_data_repr_html(dask_dataarray) + assert data_repr == dask_dataarray.data._repr_html_() + + +def test_format_dims_no_dims(): + dims, coord_names = {}, [] + formatted = fh.format_dims(dims, coord_names) + assert formatted == "" + + +def test_format_dims_unsafe_dim_name(): + dims, coord_names = {"": 3, "y": 2}, [] + formatted = fh.format_dims(dims, coord_names) + assert "<x>" in formatted + + +def test_format_dims_non_index(): + dims, coord_names = {"x": 3, "y": 2}, ["time"] + formatted = fh.format_dims(dims, coord_names) + assert "class='xr-has-index'" not in formatted + + +def test_format_dims_index(): + dims, coord_names = {"x": 3, "y": 2}, ["x"] + formatted = fh.format_dims(dims, coord_names) + assert "class='xr-has-index'" in formatted + + +def test_summarize_attrs_with_unsafe_attr_name_and_value(): + attrs = {"": 3, "y": ""} + formatted = fh.summarize_attrs(attrs) + assert "
    <x> :
    " in formatted + assert "
    y :
    " in formatted + assert "
    3
    " in formatted + assert "
    <pd.DataFrame>
    " in formatted + + +def test_repr_of_dataarray(dataarray): + formatted = fh.array_repr(dataarray) + assert "dim_0" in formatted + # has an expandable data section + assert formatted.count("class='xr-array-in' type='checkbox' >") == 1 + # coords and attrs don't have an items so they'll be be disabled and collapsed + assert ( + formatted.count("class='xr-section-summary-in' type='checkbox' disabled >") == 2 + ) + + +def test_summary_of_multiindex_coord(multiindex): + idx = multiindex.x.variable.to_index_variable() + formatted = fh._summarize_coord_multiindex("foo", idx) + assert "(level_1, level_2)" in formatted + assert "MultiIndex" in formatted + assert "foo" in formatted + + +def test_repr_of_multiindex(multiindex): + formatted = fh.dataset_repr(multiindex) + assert "(x)" in formatted + + +def test_repr_of_dataset(dataset): + formatted = fh.dataset_repr(dataset) + # coords, attrs, and data_vars are expanded + assert ( + formatted.count("class='xr-section-summary-in' type='checkbox' checked>") == 3 + ) diff --git a/xarray/tests/test_options.py b/xarray/tests/test_options.py index 2aa77ecd6b3..f155acbf494 100644 --- a/xarray/tests/test_options.py +++ b/xarray/tests/test_options.py @@ -67,6 +67,16 @@ def test_nested_options(): assert OPTIONS["display_width"] == original +def test_display_style(): + original = "text" + assert OPTIONS["display_style"] == original + with pytest.raises(ValueError): + xarray.set_options(display_style="invalid_str") + with xarray.set_options(display_style="html"): + assert OPTIONS["display_style"] == "html" + assert OPTIONS["display_style"] == original + + def create_test_dataset_attrs(seed=0): ds = create_test_data(seed) ds.attrs = {"attr1": 5, "attr2": "history", "attr3": {"nested": "more_info"}} @@ -164,3 +174,30 @@ def test_merge_attr_retention(self): # option doesn't affect this result = merge([da1, da2]) assert result.attrs == original_attrs + + def test_display_style_text(self): + ds = create_test_dataset_attrs() + text = ds._repr_html_() + assert text.startswith("
    ")
    +        assert "'nested'" in text
    +        assert "<xarray.Dataset>" in text
    +
    +    def test_display_style_html(self):
    +        ds = create_test_dataset_attrs()
    +        with xarray.set_options(display_style="html"):
    +            html = ds._repr_html_()
    +            assert html.startswith("
    ") + assert "'nested'" in html + + def test_display_dataarray_style_text(self): + da = create_test_dataarray_attrs() + text = da._repr_html_() + assert text.startswith("
    ")
    +        assert "<xarray.DataArray 'var1'" in text
    +
    +    def test_display_dataarray_style_html(self):
    +        da = create_test_dataarray_attrs()
    +        with xarray.set_options(display_style="html"):
    +            html = da._repr_html_()
    +            assert html.startswith("
    ") + assert "#x27;nested'" in html From bb0a5a2b1c71f7c2622543406ccc82ddbb290ece Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Thu, 24 Oct 2019 17:50:19 -0400 Subject: [PATCH 02/11] Escaping dtypes (#3444) * Escaping dtypes * Reformatting --- xarray/core/formatting_html.py | 2 +- xarray/tests/test_formatting_html.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py index b03ecc12962..dbebbcf4fbe 100644 --- a/xarray/core/formatting_html.py +++ b/xarray/core/formatting_html.py @@ -96,7 +96,7 @@ def summarize_variable(name, var, is_index=False, dtype=None, preview=None): cssclass_idx = " class='xr-has-index'" if is_index else "" dims_str = f"({', '.join(escape(dim) for dim in var.dims)})" name = escape(name) - dtype = dtype or var.dtype + dtype = dtype or escape(str(var.dtype)) # "unique" ids required to expand/collapse subsections attrs_id = "attrs-" + str(uuid.uuid4()) diff --git a/xarray/tests/test_formatting_html.py b/xarray/tests/test_formatting_html.py index e7f54b22d06..fea24ff93f8 100644 --- a/xarray/tests/test_formatting_html.py +++ b/xarray/tests/test_formatting_html.py @@ -130,3 +130,5 @@ def test_repr_of_dataset(dataset): assert ( formatted.count("class='xr-section-summary-in' type='checkbox' checked>") == 3 ) + assert "<U4" in formatted + assert "<IA>" in formatted From 79b3cdd3822c79ad2ee267f4d5082cd91c7f714c Mon Sep 17 00:00:00 2001 From: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Date: Fri, 25 Oct 2019 11:15:46 -0400 Subject: [PATCH 03/11] change ALL_DIMS to equal ellipsis (#3418) * change ALL_DIMS to equal ... * changed references & added whatsnew * Update xarray/core/groupby.py Co-Authored-By: Deepak Cherian * Update xarray/core/groupby.py Co-Authored-By: Deepak Cherian * note in readme --- doc/examples/multidimensional-coords.rst | 2 +- doc/groupby.rst | 16 +++++++++++----- doc/whats-new.rst | 5 +++++ xarray/core/common.py | 4 ++-- xarray/core/dataset.py | 5 ++--- xarray/core/groupby.py | 16 ++++++++-------- xarray/core/variable.py | 2 +- xarray/tests/test_dask.py | 4 ++-- xarray/tests/test_dataarray.py | 14 +++++++------- xarray/tests/test_dataset.py | 13 ++++++------- xarray/tests/test_groupby.py | 6 +++--- xarray/tests/test_plot.py | 6 +++--- xarray/tests/test_sparse.py | 8 ++++---- 13 files changed, 55 insertions(+), 46 deletions(-) diff --git a/doc/examples/multidimensional-coords.rst b/doc/examples/multidimensional-coords.rst index a5084043977..55569b7662a 100644 --- a/doc/examples/multidimensional-coords.rst +++ b/doc/examples/multidimensional-coords.rst @@ -107,7 +107,7 @@ function to specify the output coordinates of the group. lat_center = np.arange(1, 90, 2) # group according to those bins and take the mean Tair_lat_mean = (ds.Tair.groupby_bins('xc', lat_bins, labels=lat_center) - .mean(xr.ALL_DIMS)) + .mean(...)) # plot the result @savefig xarray_multidimensional_coords_14_1.png width=5in Tair_lat_mean.plot(); diff --git a/doc/groupby.rst b/doc/groupby.rst index e1d88e289d2..52a27f4f160 100644 --- a/doc/groupby.rst +++ b/doc/groupby.rst @@ -116,7 +116,13 @@ dimensions *other than* the provided one: .. ipython:: python - ds.groupby('x').std(xr.ALL_DIMS) + ds.groupby('x').std(...) + +.. note:: + + We use an ellipsis (`...`) here to indicate we want to reduce over all + other dimensions + First and last ~~~~~~~~~~~~~~ @@ -127,7 +133,7 @@ values for group along the grouped dimension: .. ipython:: python - ds.groupby('letters').first(xr.ALL_DIMS) + ds.groupby('letters').first(...) By default, they skip missing values (control this with ``skipna``). @@ -142,7 +148,7 @@ coordinates. For example: .. ipython:: python - alt = arr.groupby('letters').mean(xr.ALL_DIMS) + alt = arr.groupby('letters').mean(...) alt ds.groupby('letters') - alt @@ -195,7 +201,7 @@ __ http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html#_two_dimen 'lat': (['ny','nx'], [[10,10],[20,20]] ),}, dims=['ny','nx']) da - da.groupby('lon').sum(xr.ALL_DIMS) + da.groupby('lon').sum(...) da.groupby('lon').apply(lambda x: x - x.mean(), shortcut=False) Because multidimensional groups have the ability to generate a very large @@ -213,4 +219,4 @@ applying your function, and then unstacking the result: .. ipython:: python stacked = da.stack(gridcell=['ny', 'nx']) - stacked.groupby('gridcell').sum(xr.ALL_DIMS).unstack('gridcell') + stacked.groupby('gridcell').sum(...).unstack('gridcell') diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 12bed8f332e..ac60994d35b 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -25,6 +25,11 @@ Breaking changes New Features ~~~~~~~~~~~~ +- Changed `xr.ALL_DIMS` to equal python's `Ellipsis` (`...`), and changed internal usages to use + `...` directly. As before, you can use this to instruct a `groupby` operation + to reduce over all dimensions. While we have no plans to remove `xr.ALL_DIMS`, we suggest + using `...`. + By `Maximilian Roos `_ - Added integration tests against `pint `_. (:pull:`3238`) by `Justus Magin `_. diff --git a/xarray/core/common.py b/xarray/core/common.py index 1a8cf34ed39..d372115ea57 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -25,10 +25,10 @@ from .options import OPTIONS, _get_keep_attrs from .pycompat import dask_array_type from .rolling_exp import RollingExp -from .utils import Frozen, ReprObject, either_dict_or_kwargs +from .utils import Frozen, either_dict_or_kwargs # Used as a sentinel value to indicate a all dimensions -ALL_DIMS = ReprObject("") +ALL_DIMS = ... C = TypeVar("C") diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index eba580f84bd..55ac0bc6135 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -49,7 +49,6 @@ ) from .alignment import _broadcast_helper, _get_broadcast_dims_map_common_coords, align from .common import ( - ALL_DIMS, DataWithCoords, ImplementsDatasetReduce, _contains_datetime_like_objects, @@ -4037,7 +4036,7 @@ def reduce( Dataset with this object's DataArrays replaced with new DataArrays of summarized data and the indicated dimension(s) removed. """ - if dim is None or dim is ALL_DIMS: + if dim is None or dim is ...: dims = set(self.dims) elif isinstance(dim, str) or not isinstance(dim, Iterable): dims = {dim} @@ -5002,7 +5001,7 @@ def quantile( if isinstance(dim, str): dims = {dim} - elif dim is None or dim is ALL_DIMS: + elif dim in [None, ...]: dims = set(self.dims) else: dims = set(dim) diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index 52eb17df18d..68bd28ddb12 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -7,7 +7,7 @@ from . import dtypes, duck_array_ops, nputils, ops from .arithmetic import SupportsArithmetic -from .common import ALL_DIMS, ImplementsArrayReduce, ImplementsDatasetReduce +from .common import ImplementsArrayReduce, ImplementsDatasetReduce from .concat import concat from .formatting import format_array_flat from .options import _get_keep_attrs @@ -712,7 +712,7 @@ def quantile(self, q, dim=None, interpolation="linear", keep_attrs=None): q : float in range of [0,1] (or sequence of floats) Quantile to compute, which must be between 0 and 1 inclusive. - dim : xarray.ALL_DIMS, str or sequence of str, optional + dim : `...`, str or sequence of str, optional Dimension(s) over which to apply quantile. Defaults to the grouped dimension. interpolation : {'linear', 'lower', 'higher', 'midpoint', 'nearest'} @@ -769,7 +769,7 @@ def reduce( Function which can be called in the form `func(x, axis=axis, **kwargs)` to return the result of collapsing an np.ndarray over an integer valued axis. - dim : xarray.ALL_DIMS, str or sequence of str, optional + dim : `...`, str or sequence of str, optional Dimension(s) over which to apply `func`. axis : int or sequence of int, optional Axis(es) over which to apply `func`. Only one of the 'dimension' @@ -794,9 +794,9 @@ def reduce( if keep_attrs is None: keep_attrs = _get_keep_attrs(default=False) - if dim is not ALL_DIMS and dim not in self.dims: + if dim is not ... and dim not in self.dims: raise ValueError( - "cannot reduce over dimension %r. expected either xarray.ALL_DIMS to reduce over all dimensions or one or more of %r." + "cannot reduce over dimension %r. expected either '...' to reduce over all dimensions or one or more of %r." % (dim, self.dims) ) @@ -867,7 +867,7 @@ def reduce(self, func, dim=None, keep_attrs=None, **kwargs): Function which can be called in the form `func(x, axis=axis, **kwargs)` to return the result of collapsing an np.ndarray over an integer valued axis. - dim : xarray.ALL_DIMS, str or sequence of str, optional + dim : `...`, str or sequence of str, optional Dimension(s) over which to apply `func`. axis : int or sequence of int, optional Axis(es) over which to apply `func`. Only one of the 'dimension' @@ -895,9 +895,9 @@ def reduce(self, func, dim=None, keep_attrs=None, **kwargs): def reduce_dataset(ds): return ds.reduce(func, dim, keep_attrs, **kwargs) - if dim is not ALL_DIMS and dim not in self.dims: + if dim is not ... and dim not in self.dims: raise ValueError( - "cannot reduce over dimension %r. expected either xarray.ALL_DIMS to reduce over all dimensions or one or more of %r." + "cannot reduce over dimension %r. expected either '...' to reduce over all dimensions or one or more of %r." % (dim, self.dims) ) diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 37672cd82d9..93ad1eafb97 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -1450,7 +1450,7 @@ def reduce( Array with summarized data and the indicated dimension(s) removed. """ - if dim is common.ALL_DIMS: + if dim == ...: dim = None if dim is not None and axis is not None: raise ValueError("cannot supply both 'axis' and 'dim' arguments") diff --git a/xarray/tests/test_dask.py b/xarray/tests/test_dask.py index ae8f43cb66d..50517ae3c9c 100644 --- a/xarray/tests/test_dask.py +++ b/xarray/tests/test_dask.py @@ -435,8 +435,8 @@ def test_groupby(self): u = self.eager_array v = self.lazy_array - expected = u.groupby("x").mean(xr.ALL_DIMS) - actual = v.groupby("x").mean(xr.ALL_DIMS) + expected = u.groupby("x").mean(...) + actual = v.groupby("x").mean(...) self.assertLazyAndAllClose(expected, actual) def test_groupby_first(self): diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index a3a2f55f6cc..b13527bc098 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -13,7 +13,7 @@ from xarray.coding.times import CFDatetimeCoder from xarray.convert import from_cdms2 from xarray.core import dtypes -from xarray.core.common import ALL_DIMS, full_like +from xarray.core.common import full_like from xarray.tests import ( LooseVersion, ReturnItem, @@ -2443,8 +2443,8 @@ def test_groupby_sum(self): "abc": Variable(["abc"], np.array(["a", "b", "c"])), } )["foo"] - assert_allclose(expected_sum_all, grouped.reduce(np.sum, dim=ALL_DIMS)) - assert_allclose(expected_sum_all, grouped.sum(ALL_DIMS)) + assert_allclose(expected_sum_all, grouped.reduce(np.sum, dim=...)) + assert_allclose(expected_sum_all, grouped.sum(...)) expected = DataArray( [ @@ -2456,7 +2456,7 @@ def test_groupby_sum(self): ) actual = array["y"].groupby("abc").apply(np.sum) assert_allclose(expected, actual) - actual = array["y"].groupby("abc").sum(ALL_DIMS) + actual = array["y"].groupby("abc").sum(...) assert_allclose(expected, actual) expected_sum_axis1 = Dataset( @@ -2590,9 +2590,9 @@ def test_groupby_math(self): assert_identical(expected, actual) grouped = array.groupby("abc") - expected_agg = (grouped.mean(ALL_DIMS) - np.arange(3)).rename(None) + expected_agg = (grouped.mean(...) - np.arange(3)).rename(None) actual = grouped - DataArray(range(3), [("abc", ["a", "b", "c"])]) - actual_agg = actual.groupby("abc").mean(ALL_DIMS) + actual_agg = actual.groupby("abc").mean(...) assert_allclose(expected_agg, actual_agg) with raises_regex(TypeError, "only support binary ops"): @@ -2698,7 +2698,7 @@ def test_groupby_multidim(self): ("lon", DataArray([5, 28, 23], coords=[("lon", [30.0, 40.0, 50.0])])), ("lat", DataArray([16, 40], coords=[("lat", [10.0, 20.0])])), ]: - actual_sum = array.groupby(dim).sum(ALL_DIMS) + actual_sum = array.groupby(dim).sum(...) assert_identical(expected_sum, actual_sum) def test_groupby_multidim_apply(self): diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index 006d6881b5a..b3ffdf68e3f 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -11,7 +11,6 @@ import xarray as xr from xarray import ( - ALL_DIMS, DataArray, Dataset, IndexVariable, @@ -3327,7 +3326,7 @@ def test_groupby_reduce(self): expected = data.mean("y") expected["yonly"] = expected["yonly"].variable.set_dims({"x": 3}) - actual = data.groupby("x").mean(ALL_DIMS) + actual = data.groupby("x").mean(...) assert_allclose(expected, actual) actual = data.groupby("x").mean("y") @@ -3336,12 +3335,12 @@ def test_groupby_reduce(self): letters = data["letters"] expected = Dataset( { - "xy": data["xy"].groupby(letters).mean(ALL_DIMS), + "xy": data["xy"].groupby(letters).mean(...), "xonly": (data["xonly"].mean().variable.set_dims({"letters": 2})), "yonly": data["yonly"].groupby(letters).mean(), } ) - actual = data.groupby("letters").mean(ALL_DIMS) + actual = data.groupby("letters").mean(...) assert_allclose(expected, actual) def test_groupby_math(self): @@ -3404,14 +3403,14 @@ def test_groupby_math_virtual(self): {"x": ("t", [1, 2, 3])}, {"t": pd.date_range("20100101", periods=3)} ) grouped = ds.groupby("t.day") - actual = grouped - grouped.mean(ALL_DIMS) + actual = grouped - grouped.mean(...) expected = Dataset({"x": ("t", [0, 0, 0])}, ds[["t", "t.day"]]) assert_identical(actual, expected) def test_groupby_nan(self): # nan should be excluded from groupby ds = Dataset({"foo": ("x", [1, 2, 3, 4])}, {"bar": ("x", [1, 1, 2, np.nan])}) - actual = ds.groupby("bar").mean(ALL_DIMS) + actual = ds.groupby("bar").mean(...) expected = Dataset({"foo": ("bar", [1.5, 3]), "bar": [1, 2]}) assert_identical(actual, expected) @@ -3421,7 +3420,7 @@ def test_groupby_order(self): for vn in ["a", "b", "c"]: ds[vn] = DataArray(np.arange(10), dims=["t"]) data_vars_ref = list(ds.data_vars.keys()) - ds = ds.groupby("t").mean(ALL_DIMS) + ds = ds.groupby("t").mean(...) data_vars = list(ds.data_vars.keys()) assert data_vars == data_vars_ref # coords are now at the end of the list, so the test below fails diff --git a/xarray/tests/test_groupby.py b/xarray/tests/test_groupby.py index be494c4ae2b..a6de41beb66 100644 --- a/xarray/tests/test_groupby.py +++ b/xarray/tests/test_groupby.py @@ -147,11 +147,11 @@ def test_da_groupby_quantile(): [("x", [1, 1, 1, 2, 2]), ("y", [0, 0, 1])], ) - actual_x = array.groupby("x").quantile(0, dim=xr.ALL_DIMS) + actual_x = array.groupby("x").quantile(0, dim=...) expected_x = xr.DataArray([1, 4], [("x", [1, 2])]) assert_identical(expected_x, actual_x) - actual_y = array.groupby("y").quantile(0, dim=xr.ALL_DIMS) + actual_y = array.groupby("y").quantile(0, dim=...) expected_y = xr.DataArray([1, 22], [("y", [0, 1])]) assert_identical(expected_y, actual_y) @@ -177,7 +177,7 @@ def test_da_groupby_quantile(): ) g = foo.groupby(foo.time.dt.month) - actual = g.quantile(0, dim=xr.ALL_DIMS) + actual = g.quantile(0, dim=...) expected = xr.DataArray( [ 0.0, diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 3ac45a9720f..7deabd46eae 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -417,7 +417,7 @@ def test_convenient_facetgrid_4d(self): def test_coord_with_interval(self): bins = [-1, 0, 1, 2] - self.darray.groupby_bins("dim_0", bins).mean(xr.ALL_DIMS).plot() + self.darray.groupby_bins("dim_0", bins).mean(...).plot() class TestPlot1D(PlotTestCase): @@ -502,7 +502,7 @@ def test_step(self): def test_coord_with_interval_step(self): bins = [-1, 0, 1, 2] - self.darray.groupby_bins("dim_0", bins).mean(xr.ALL_DIMS).plot.step() + self.darray.groupby_bins("dim_0", bins).mean(...).plot.step() assert len(plt.gca().lines[0].get_xdata()) == ((len(bins) - 1) * 2) @@ -544,7 +544,7 @@ def test_plot_nans(self): def test_hist_coord_with_interval(self): ( self.darray.groupby_bins("dim_0", [-1, 0, 1, 2]) - .mean(xr.ALL_DIMS) + .mean(...) .plot.hist(range=(-1, 2)) ) diff --git a/xarray/tests/test_sparse.py b/xarray/tests/test_sparse.py index bd26b96f6d4..73c4b9b8c74 100644 --- a/xarray/tests/test_sparse.py +++ b/xarray/tests/test_sparse.py @@ -756,8 +756,8 @@ def test_dot(self): def test_groupby(self): x1 = self.ds_xr x2 = self.sp_xr - m1 = x1.groupby("x").mean(xr.ALL_DIMS) - m2 = x2.groupby("x").mean(xr.ALL_DIMS) + m1 = x1.groupby("x").mean(...) + m2 = x2.groupby("x").mean(...) assert isinstance(m2.data, sparse.SparseArray) assert np.allclose(m1.data, m2.data.todense()) @@ -772,8 +772,8 @@ def test_groupby_first(self): def test_groupby_bins(self): x1 = self.ds_xr x2 = self.sp_xr - m1 = x1.groupby_bins("x", bins=[0, 3, 7, 10]).sum(xr.ALL_DIMS) - m2 = x2.groupby_bins("x", bins=[0, 3, 7, 10]).sum(xr.ALL_DIMS) + m1 = x1.groupby_bins("x", bins=[0, 3, 7, 10]).sum(...) + m2 = x2.groupby_bins("x", bins=[0, 3, 7, 10]).sum(...) assert isinstance(m2.data, sparse.SparseArray) assert np.allclose(m1.data, m2.data.todense()) From 63cc85759ac25605c8398d904d055df5dc538b94 Mon Sep 17 00:00:00 2001 From: Benoit Bovy Date: Fri, 25 Oct 2019 17:40:46 +0200 Subject: [PATCH 04/11] add icomoon license (#3448) --- README.rst | 3 + licenses/ICOMOON_LICENSE | 395 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 licenses/ICOMOON_LICENSE diff --git a/README.rst b/README.rst index 53f51392a1a..5ee7234f221 100644 --- a/README.rst +++ b/README.rst @@ -138,4 +138,7 @@ under a "3-clause BSD" license: xarray also bundles portions of CPython, which is available under the "Python Software Foundation License" in xarray/core/pycompat.py. +xarray uses icons from the icomoon package (free version), which is +available under the "CC BY 4.0" license. + The full text of these licenses are included in the licenses directory. diff --git a/licenses/ICOMOON_LICENSE b/licenses/ICOMOON_LICENSE new file mode 100644 index 00000000000..4ea99c213c5 --- /dev/null +++ b/licenses/ICOMOON_LICENSE @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. From fb0cf7b5fe56519a933ffcecbce9e9327fe236a6 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Fri, 25 Oct 2019 15:01:11 -0600 Subject: [PATCH 05/11] Another groupby.reduce bugfix. (#3403) * Another groupby.reduce bugfix. Fixes #3402 * Add whats-new. * Use is_scalar instead * bugfix * fix whats-new * Update xarray/core/groupby.py Co-Authored-By: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> --- doc/whats-new.rst | 4 +++ xarray/core/groupby.py | 27 +++++++++------- xarray/tests/test_dataarray.py | 9 ------ xarray/tests/test_groupby.py | 56 +++++++++++++++++++++++++--------- 4 files changed, 61 insertions(+), 35 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index ac60994d35b..dea110b5e46 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -55,6 +55,10 @@ Bug fixes - Sync with cftime by removing `dayofwk=-1` for cftime>=1.0.4. By `Anderson Banihirwe `_. +- Fix :py:meth:`xarray.core.groupby.DataArrayGroupBy.reduce` and + :py:meth:`xarray.core.groupby.DatasetGroupBy.reduce` when reducing over multiple dimensions. + (:issue:`3402`). By `Deepak Cherian `_ + Documentation ~~~~~~~~~~~~~ diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index 68bd28ddb12..62c055fed51 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -15,6 +15,7 @@ from .utils import ( either_dict_or_kwargs, hashable, + is_scalar, maybe_wrap_array, peek_at, safe_cast_to_index, @@ -22,6 +23,18 @@ from .variable import IndexVariable, Variable, as_variable +def check_reduce_dims(reduce_dims, dimensions): + + if reduce_dims is not ...: + if is_scalar(reduce_dims): + reduce_dims = [reduce_dims] + if any([dim not in dimensions for dim in reduce_dims]): + raise ValueError( + "cannot reduce over dimensions %r. expected either '...' to reduce over all dimensions or one or more of %r." + % (reduce_dims, dimensions) + ) + + def unique_value_groups(ar, sort=True): """Group an array by its unique values. @@ -794,15 +807,11 @@ def reduce( if keep_attrs is None: keep_attrs = _get_keep_attrs(default=False) - if dim is not ... and dim not in self.dims: - raise ValueError( - "cannot reduce over dimension %r. expected either '...' to reduce over all dimensions or one or more of %r." - % (dim, self.dims) - ) - def reduce_array(ar): return ar.reduce(func, dim, axis, keep_attrs=keep_attrs, **kwargs) + check_reduce_dims(dim, self.dims) + return self.apply(reduce_array, shortcut=shortcut) @@ -895,11 +904,7 @@ def reduce(self, func, dim=None, keep_attrs=None, **kwargs): def reduce_dataset(ds): return ds.reduce(func, dim, keep_attrs, **kwargs) - if dim is not ... and dim not in self.dims: - raise ValueError( - "cannot reduce over dimension %r. expected either '...' to reduce over all dimensions or one or more of %r." - % (dim, self.dims) - ) + check_reduce_dims(dim, self.dims) return self.apply(reduce_dataset) diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index b13527bc098..101bb44660c 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -2560,15 +2560,6 @@ def change_metadata(x): expected = change_metadata(expected) assert_equal(expected, actual) - def test_groupby_reduce_dimension_error(self): - array = self.make_groupby_example_array() - grouped = array.groupby("y") - with raises_regex(ValueError, "cannot reduce over dimension 'y'"): - grouped.mean() - - grouped = array.groupby("y", squeeze=False) - assert_identical(array, grouped.mean()) - def test_groupby_math(self): array = self.make_groupby_example_array() for squeeze in [True, False]: diff --git a/xarray/tests/test_groupby.py b/xarray/tests/test_groupby.py index a6de41beb66..d74d684dc54 100644 --- a/xarray/tests/test_groupby.py +++ b/xarray/tests/test_groupby.py @@ -5,7 +5,23 @@ import xarray as xr from xarray.core.groupby import _consolidate_slices -from . import assert_identical, raises_regex +from . import assert_allclose, assert_identical, raises_regex + + +@pytest.fixture +def dataset(): + ds = xr.Dataset( + {"foo": (("x", "y", "z"), np.random.randn(3, 4, 2))}, + {"x": ["a", "b", "c"], "y": [1, 2, 3, 4], "z": [1, 2]}, + ) + ds["boo"] = (("z", "y"), [["f", "g", "h", "j"]] * 2) + + return ds + + +@pytest.fixture +def array(dataset): + return dataset["foo"] def test_consolidate_slices(): @@ -21,25 +37,17 @@ def test_consolidate_slices(): _consolidate_slices([slice(3), 4]) -def test_groupby_dims_property(): - ds = xr.Dataset( - {"foo": (("x", "y", "z"), np.random.randn(3, 4, 2))}, - {"x": ["a", "bcd", "c"], "y": [1, 2, 3, 4], "z": [1, 2]}, - ) +def test_groupby_dims_property(dataset): + assert dataset.groupby("x").dims == dataset.isel(x=1).dims + assert dataset.groupby("y").dims == dataset.isel(y=1).dims - assert ds.groupby("x").dims == ds.isel(x=1).dims - assert ds.groupby("y").dims == ds.isel(y=1).dims - - stacked = ds.stack({"xy": ("x", "y")}) + stacked = dataset.stack({"xy": ("x", "y")}) assert stacked.groupby("xy").dims == stacked.isel(xy=0).dims -def test_multi_index_groupby_apply(): +def test_multi_index_groupby_apply(dataset): # regression test for GH873 - ds = xr.Dataset( - {"foo": (("x", "y"), np.random.randn(3, 4))}, - {"x": ["a", "b", "c"], "y": [1, 2, 3, 4]}, - ) + ds = dataset.isel(z=1, drop=True)[["foo"]] doubled = 2 * ds group_doubled = ( ds.stack(space=["x", "y"]) @@ -276,6 +284,24 @@ def test_groupby_grouping_errors(): dataset.to_array().groupby(dataset.foo * np.nan) +def test_groupby_reduce_dimension_error(array): + grouped = array.groupby("y") + with raises_regex(ValueError, "cannot reduce over dimensions"): + grouped.mean() + + with raises_regex(ValueError, "cannot reduce over dimensions"): + grouped.mean("huh") + + with raises_regex(ValueError, "cannot reduce over dimensions"): + grouped.mean(("x", "y", "asd")) + + grouped = array.groupby("y", squeeze=False) + assert_identical(array, grouped.mean()) + + assert_identical(array.mean("x"), grouped.reduce(np.mean, "x")) + assert_allclose(array.mean(["x", "z"]), grouped.reduce(np.mean, ["x", "z"])) + + def test_groupby_bins_timeseries(): ds = xr.Dataset() ds["time"] = xr.DataArray( From 02288b4e0cb4e300e402d96bbbdba68db6eeb41f Mon Sep 17 00:00:00 2001 From: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Date: Mon, 28 Oct 2019 17:12:48 -0400 Subject: [PATCH 06/11] Allow ellipsis (...) in transpose (#3421) * infix_dims function * implement transpose with ellipsis * also infix in dataarray * check errors centrally, remove boilerplate from transpose methods * whatsnew * docs * remove old comments * generator->iterator * test for differently ordered dimensions --- doc/reshaping.rst | 4 +++- doc/whats-new.rst | 4 ++++ setup.cfg | 5 ++++- xarray/core/dataarray.py | 7 +------ xarray/core/dataset.py | 4 ++-- xarray/core/utils.py | 25 +++++++++++++++++++++++++ xarray/core/variable.py | 2 ++ xarray/tests/__init__.py | 3 +++ xarray/tests/test_dataarray.py | 4 ++++ xarray/tests/test_dataset.py | 27 +++++++++++++++++++++++++-- xarray/tests/test_utils.py | 24 ++++++++++++++++++++++++ xarray/tests/test_variable.py | 3 +++ 12 files changed, 100 insertions(+), 12 deletions(-) diff --git a/doc/reshaping.rst b/doc/reshaping.rst index 51202f9be41..455a24f9216 100644 --- a/doc/reshaping.rst +++ b/doc/reshaping.rst @@ -18,12 +18,14 @@ Reordering dimensions --------------------- To reorder dimensions on a :py:class:`~xarray.DataArray` or across all variables -on a :py:class:`~xarray.Dataset`, use :py:meth:`~xarray.DataArray.transpose`: +on a :py:class:`~xarray.Dataset`, use :py:meth:`~xarray.DataArray.transpose`. An +ellipsis (`...`) can be use to represent all other dimensions: .. ipython:: python ds = xr.Dataset({'foo': (('x', 'y', 'z'), [[[42]]]), 'bar': (('y', 'z'), [[24]])}) ds.transpose('y', 'z', 'x') + ds.transpose(..., 'x') # equivalent ds.transpose() # reverses all dimensions Expand and squeeze dimensions diff --git a/doc/whats-new.rst b/doc/whats-new.rst index dea110b5e46..cced7276ff3 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -25,6 +25,10 @@ Breaking changes New Features ~~~~~~~~~~~~ +- :py:meth:`Dataset.transpose` and :py:meth:`DataArray.transpose` now support an ellipsis (`...`) + to represent all 'other' dimensions. For example, to move one dimension to the front, + use `.transpose('x', ...)`. (:pull:`3421`) + By `Maximilian Roos `_ - Changed `xr.ALL_DIMS` to equal python's `Ellipsis` (`...`), and changed internal usages to use `...` directly. As before, you can use this to instruct a `groupby` operation to reduce over all dimensions. While we have no plans to remove `xr.ALL_DIMS`, we suggest diff --git a/setup.cfg b/setup.cfg index eee8b2477b2..fec2ca6bbe4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -117,4 +117,7 @@ tag_prefix = v parentdir_prefix = xarray- [aliases] -test = pytest \ No newline at end of file +test = pytest + +[pytest-watch] +nobeep = True \ No newline at end of file diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index 5fccb9236e8..33dcad13204 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -1863,12 +1863,7 @@ def transpose(self, *dims: Hashable, transpose_coords: bool = None) -> "DataArra Dataset.transpose """ if dims: - if set(dims) ^ set(self.dims): - raise ValueError( - "arguments to transpose (%s) must be " - "permuted array dimensions (%s)" % (dims, tuple(self.dims)) - ) - + dims = tuple(utils.infix_dims(dims, self.dims)) variable = self.variable.transpose(*dims) if transpose_coords: coords: Dict[Hashable, Variable] = {} diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 55ac0bc6135..2a0464515c6 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -3712,14 +3712,14 @@ def transpose(self, *dims: Hashable) -> "Dataset": DataArray.transpose """ if dims: - if set(dims) ^ set(self.dims): + if set(dims) ^ set(self.dims) and ... not in dims: raise ValueError( "arguments to transpose (%s) must be " "permuted dataset dimensions (%s)" % (dims, tuple(self.dims)) ) ds = self.copy() for name, var in self._variables.items(): - var_dims = tuple(dim for dim in dims if dim in var.dims) + var_dims = tuple(dim for dim in dims if dim in (var.dims + (...,))) ds._variables[name] = var.transpose(*var_dims) return ds diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 6befe0b5efc..492c595a887 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -10,6 +10,7 @@ AbstractSet, Any, Callable, + Collection, Container, Dict, Hashable, @@ -660,6 +661,30 @@ def __len__(self) -> int: return len(self._data) - num_hidden +def infix_dims(dims_supplied: Collection, dims_all: Collection) -> Iterator: + """ + Resolves a supplied list containing an ellispsis representing other items, to + a generator with the 'realized' list of all items + """ + if ... in dims_supplied: + if len(set(dims_all)) != len(dims_all): + raise ValueError("Cannot use ellipsis with repeated dims") + if len([d for d in dims_supplied if d == ...]) > 1: + raise ValueError("More than one ellipsis supplied") + other_dims = [d for d in dims_all if d not in dims_supplied] + for d in dims_supplied: + if d == ...: + yield from other_dims + else: + yield d + else: + if set(dims_supplied) ^ set(dims_all): + raise ValueError( + f"{dims_supplied} must be a permuted list of {dims_all}, unless `...` is included" + ) + yield from dims_supplied + + def get_temp_dimname(dims: Container[Hashable], new_dim: Hashable) -> Hashable: """ Get an new dimension name based on new_dim, that is not used in dims. If the same name exists, we add an underscore(s) in the head. diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 93ad1eafb97..7d03fd58d39 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -25,6 +25,7 @@ OrderedSet, decode_numpy_dict_values, either_dict_or_kwargs, + infix_dims, ensure_us_time_resolution, ) @@ -1228,6 +1229,7 @@ def transpose(self, *dims) -> "Variable": """ if len(dims) == 0: dims = self.dims[::-1] + dims = tuple(infix_dims(dims, self.dims)) axes = self.get_axis_num(dims) if len(dims) < 2: # no need to transpose if only one dimension return self.copy(deep=False) diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index 88476e5e730..f85a33f7a3c 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -158,18 +158,21 @@ def source_ndarray(array): def assert_equal(a, b): + __tracebackhide__ = True xarray.testing.assert_equal(a, b) xarray.testing._assert_internal_invariants(a) xarray.testing._assert_internal_invariants(b) def assert_identical(a, b): + __tracebackhide__ = True xarray.testing.assert_identical(a, b) xarray.testing._assert_internal_invariants(a) xarray.testing._assert_internal_invariants(b) def assert_allclose(a, b, **kwargs): + __tracebackhide__ = True xarray.testing.assert_allclose(a, b, **kwargs) xarray.testing._assert_internal_invariants(a) xarray.testing._assert_internal_invariants(b) diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index 101bb44660c..ad474d533be 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -2068,6 +2068,10 @@ def test_transpose(self): ) assert_equal(expected, actual) + # same as previous but with ellipsis + actual = da.transpose("z", ..., "x", transpose_coords=True) + assert_equal(expected, actual) + with pytest.raises(ValueError): da.transpose("x", "y") diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index b3ffdf68e3f..647eb733adb 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -4675,6 +4675,10 @@ def test_dataset_transpose(self): ) assert_identical(expected, actual) + actual = ds.transpose(...) + expected = ds + assert_identical(expected, actual) + actual = ds.transpose("x", "y") expected = ds.apply(lambda x: x.transpose("x", "y", transpose_coords=True)) assert_identical(expected, actual) @@ -4690,13 +4694,32 @@ def test_dataset_transpose(self): expected_dims = tuple(d for d in new_order if d in ds[k].dims) assert actual[k].dims == expected_dims - with raises_regex(ValueError, "arguments to transpose"): + # same as above but with ellipsis + new_order = ("dim2", "dim3", "dim1", "time") + actual = ds.transpose("dim2", "dim3", ...) + for k in ds.variables: + expected_dims = tuple(d for d in new_order if d in ds[k].dims) + assert actual[k].dims == expected_dims + + with raises_regex(ValueError, "permuted"): ds.transpose("dim1", "dim2", "dim3") - with raises_regex(ValueError, "arguments to transpose"): + with raises_regex(ValueError, "permuted"): ds.transpose("dim1", "dim2", "dim3", "time", "extra_dim") assert "T" not in dir(ds) + def test_dataset_ellipsis_transpose_different_ordered_vars(self): + # https://github.com/pydata/xarray/issues/1081#issuecomment-544350457 + ds = Dataset( + dict( + a=(("w", "x", "y", "z"), np.ones((2, 3, 4, 5))), + b=(("x", "w", "y", "z"), np.zeros((3, 2, 4, 5))), + ) + ) + result = ds.transpose(..., "z", "y") + assert list(result["a"].dims) == list("wxzy") + assert list(result["b"].dims) == list("xwzy") + def test_dataset_retains_period_index_on_transpose(self): ds = create_test_data() diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py index c36e8a1775d..5bb9deaf240 100644 --- a/xarray/tests/test_utils.py +++ b/xarray/tests/test_utils.py @@ -275,3 +275,27 @@ def test_either_dict_or_kwargs(): with pytest.raises(ValueError, match=r"foo"): result = either_dict_or_kwargs(dict(a=1), dict(a=1), "foo") + + +@pytest.mark.parametrize( + ["supplied", "all_", "expected"], + [ + (list("abc"), list("abc"), list("abc")), + (["a", ..., "c"], list("abc"), list("abc")), + (["a", ...], list("abc"), list("abc")), + (["c", ...], list("abc"), list("cab")), + ([..., "b"], list("abc"), list("acb")), + ([...], list("abc"), list("abc")), + ], +) +def test_infix_dims(supplied, all_, expected): + result = list(utils.infix_dims(supplied, all_)) + assert result == expected + + +@pytest.mark.parametrize( + ["supplied", "all_"], [([..., ...], list("abc")), ([...], list("aac"))] +) +def test_infix_dims_errors(supplied, all_): + with pytest.raises(ValueError): + list(utils.infix_dims(supplied, all_)) diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py index 78723eda013..528027ed149 100644 --- a/xarray/tests/test_variable.py +++ b/xarray/tests/test_variable.py @@ -1280,6 +1280,9 @@ def test_transpose(self): w2 = Variable(["d", "b", "c", "a"], np.einsum("abcd->dbca", x)) assert w2.shape == (5, 3, 4, 2) assert_identical(w2, w.transpose("d", "b", "c", "a")) + assert_identical(w2, w.transpose("d", ..., "a")) + assert_identical(w2, w.transpose("d", "b", "c", ...)) + assert_identical(w2, w.transpose(..., "b", "c", "a")) assert_identical(w, w2.transpose("a", "b", "c", "d")) w3 = Variable(["b", "c", "d", "a"], np.einsum("abcd->bcda", x)) assert_identical(w, w3.transpose("a", "b", "c", "d")) From c955449d4d5c7ef6b2607af13df4abed778a4c61 Mon Sep 17 00:00:00 2001 From: Deepak Cherian Date: Mon, 28 Oct 2019 17:46:40 -0600 Subject: [PATCH 07/11] Drop groups associated with nans in group variable (#3406) * Drop nans in grouped variable. * Add NaTs * whats-new * fix merge. * fix whats-new * fix test --- doc/whats-new.rst | 7 ++-- xarray/core/groupby.py | 7 ++++ xarray/tests/test_groupby.py | 80 ++++++++++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index cced7276ff3..73618782460 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -55,15 +55,14 @@ Bug fixes ~~~~~~~~~ - Fix regression introduced in v0.14.0 that would cause a crash if dask is installed but cloudpickle isn't (:issue:`3401`) by `Rhys Doyle `_ - -- Sync with cftime by removing `dayofwk=-1` for cftime>=1.0.4. +- Fix grouping over variables with NaNs. (:issue:`2383`, :pull:`3406`). + By `Deepak Cherian `_. +- Sync with cftime by removing `dayofwk=-1` for cftime>=1.0.4. By `Anderson Banihirwe `_. - - Fix :py:meth:`xarray.core.groupby.DataArrayGroupBy.reduce` and :py:meth:`xarray.core.groupby.DatasetGroupBy.reduce` when reducing over multiple dimensions. (:issue:`3402`). By `Deepak Cherian `_ - Documentation ~~~~~~~~~~~~~ diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index 62c055fed51..c3f712b31ac 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -361,6 +361,13 @@ def __init__( group_indices = [slice(i, i + 1) for i in group_indices] unique_coord = group else: + if group.isnull().any(): + # drop any NaN valued groups. + # also drop obj values where group was NaN + # Use where instead of reindex to account for duplicate coordinate labels. + obj = obj.where(group.notnull(), drop=True) + group = group.dropna(group_dim) + # look through group to find the unique values unique_values, group_indices = unique_value_groups( safe_cast_to_index(group), sort=(bins is None) diff --git a/xarray/tests/test_groupby.py b/xarray/tests/test_groupby.py index d74d684dc54..e2216547ac8 100644 --- a/xarray/tests/test_groupby.py +++ b/xarray/tests/test_groupby.py @@ -5,7 +5,7 @@ import xarray as xr from xarray.core.groupby import _consolidate_slices -from . import assert_allclose, assert_identical, raises_regex +from . import assert_allclose, assert_equal, assert_identical, raises_regex @pytest.fixture @@ -48,14 +48,14 @@ def test_groupby_dims_property(dataset): def test_multi_index_groupby_apply(dataset): # regression test for GH873 ds = dataset.isel(z=1, drop=True)[["foo"]] - doubled = 2 * ds - group_doubled = ( + expected = 2 * ds + actual = ( ds.stack(space=["x", "y"]) .groupby("space") .apply(lambda x: 2 * x) .unstack("space") ) - assert doubled.equals(group_doubled) + assert_equal(expected, actual) def test_multi_index_groupby_sum(): @@ -66,7 +66,7 @@ def test_multi_index_groupby_sum(): ) expected = ds.sum("z") actual = ds.stack(space=["x", "y"]).groupby("space").sum("z").unstack("space") - assert expected.equals(actual) + assert_equal(expected, actual) def test_groupby_da_datetime(): @@ -86,7 +86,7 @@ def test_groupby_da_datetime(): expected = xr.DataArray( [3, 7], coords=dict(reference_date=reference_dates), dims="reference_date" ) - assert actual.equals(expected) + assert_equal(expected, actual) def test_groupby_duplicate_coordinate_labels(): @@ -94,7 +94,7 @@ def test_groupby_duplicate_coordinate_labels(): array = xr.DataArray([1, 2, 3], [("x", [1, 1, 2])]) expected = xr.DataArray([3, 3], [("x", [1, 2])]) actual = array.groupby("x").sum() - assert expected.equals(actual) + assert_equal(expected, actual) def test_groupby_input_mutation(): @@ -263,6 +263,72 @@ def test_groupby_repr_datetime(obj): assert actual == expected +def test_groupby_drops_nans(): + # GH2383 + # nan in 2D data variable (requires stacking) + ds = xr.Dataset( + { + "variable": (("lat", "lon", "time"), np.arange(60.0).reshape((4, 3, 5))), + "id": (("lat", "lon"), np.arange(12.0).reshape((4, 3))), + }, + coords={"lat": np.arange(4), "lon": np.arange(3), "time": np.arange(5)}, + ) + + ds["id"].values[0, 0] = np.nan + ds["id"].values[3, 0] = np.nan + ds["id"].values[-1, -1] = np.nan + + grouped = ds.groupby(ds.id) + + # non reduction operation + expected = ds.copy() + expected.variable.values[0, 0, :] = np.nan + expected.variable.values[-1, -1, :] = np.nan + expected.variable.values[3, 0, :] = np.nan + actual = grouped.apply(lambda x: x).transpose(*ds.variable.dims) + assert_identical(actual, expected) + + # reduction along grouped dimension + actual = grouped.mean() + stacked = ds.stack({"xy": ["lat", "lon"]}) + expected = ( + stacked.variable.where(stacked.id.notnull()).rename({"xy": "id"}).to_dataset() + ) + expected["id"] = stacked.id.values + assert_identical(actual, expected.dropna("id").transpose(*actual.dims)) + + # reduction operation along a different dimension + actual = grouped.mean("time") + expected = ds.mean("time").where(ds.id.notnull()) + assert_identical(actual, expected) + + # NaN in non-dimensional coordinate + array = xr.DataArray([1, 2, 3], [("x", [1, 2, 3])]) + array["x1"] = ("x", [1, 1, np.nan]) + expected = xr.DataArray(3, [("x1", [1])]) + actual = array.groupby("x1").sum() + assert_equal(expected, actual) + + # NaT in non-dimensional coordinate + array["t"] = ( + "x", + [ + np.datetime64("2001-01-01"), + np.datetime64("2001-01-01"), + np.datetime64("NaT"), + ], + ) + expected = xr.DataArray(3, [("t", [np.datetime64("2001-01-01")])]) + actual = array.groupby("t").sum() + assert_equal(expected, actual) + + # test for repeated coordinate labels + array = xr.DataArray([0, 1, 2, 4, 3, 4], [("x", [np.nan, 1, 1, np.nan, 2, np.nan])]) + expected = xr.DataArray([3, 3], [("x", [1, 2])]) + actual = array.groupby("x").sum() + assert_equal(expected, actual) + + def test_groupby_grouping_errors(): dataset = xr.Dataset({"foo": ("x", [1, 1, 1])}, {"x": [1, 2, 3]}) with raises_regex(ValueError, "None of the data falls within bins with edges"): From 43d07b7b1d389a4bfc95c920149f4caa78653e81 Mon Sep 17 00:00:00 2001 From: Julia Signell Date: Mon, 28 Oct 2019 23:47:27 -0400 Subject: [PATCH 08/11] jupyterlab dark theme (#3443) --- xarray/static/css/style.css | 43 +++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/xarray/static/css/style.css b/xarray/static/css/style.css index 536b8ab6103..7e382de3b5b 100644 --- a/xarray/static/css/style.css +++ b/xarray/static/css/style.css @@ -2,6 +2,17 @@ * */ +:root { + --xr-font-color0: var(--jp-content-font-color0, rgba(0, 0, 0, 1)); + --xr-font-color2: var(--jp-content-font-color2, rgba(0, 0, 0, 0.54)); + --xr-font-color3: var(--jp-content-font-color3, rgba(0, 0, 0, 0.38)); + --xr-border-color: var(--jp-border-color2, #e0e0e0); + --xr-disabled-color: var(--jp-layout-color3, #bdbdbd); + --xr-background-color: var(--jp-layout-color0, white); + --xr-background-color-row-even: var(--jp-layout-color1, white); + --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee); +} + .xr-wrap { min-width: 300px; max-width: 700px; @@ -11,7 +22,7 @@ padding-top: 6px; padding-bottom: 6px; margin-bottom: 4px; - border-bottom: solid 1px #ddd; + border-bottom: solid 1px var(--xr-border-color); } .xr-header > div, @@ -28,11 +39,7 @@ } .xr-obj-type { - color: #555; -} - -.xr-array-name { - color: #000; + color: var(--xr-font-color2); } .xr-sections { @@ -50,21 +57,21 @@ } .xr-section-item input + label { - color: #ccc; + color: var(--xr-disabled-color); } .xr-section-item input:enabled + label { cursor: pointer; - color: #555; + color: var(--xr-font-color2); } .xr-section-item input:enabled + label:hover { - color: #000; + color: var(--xr-font-color0); } .xr-section-summary { grid-column: 1; - color: #555; + color: var(--xr-font-color2); font-weight: 500; } @@ -74,7 +81,7 @@ } .xr-section-summary-in:disabled + label { - color: #555; + color: var(--xr-font-color2); } .xr-section-summary-in + label:before { @@ -86,7 +93,7 @@ } .xr-section-summary-in:disabled + label:before { - color: #ccc; + color: var(--xr-disabled-color); } .xr-section-summary-in:checked + label:before { @@ -129,7 +136,7 @@ } .xr-preview { - color: #888; + color: var(--xr-font-color3); } .xr-array-preview, @@ -186,7 +193,7 @@ .xr-var-item > div, .xr-var-item label, .xr-var-item > .xr-var-name span { - background-color: #fcfcfc; + background-color: var(--xr-background-color-row-even); margin-bottom: 0; } @@ -197,7 +204,7 @@ .xr-var-list > li:nth-child(odd) > div, .xr-var-list > li:nth-child(odd) > label, .xr-var-list > li:nth-child(odd) > .xr-var-name span { - background-color: #efefef; + background-color: var(--xr-background-color-row-odd); } .xr-var-name { @@ -211,7 +218,7 @@ .xr-var-dtype { grid-column: 3; text-align: right; - color: #555; + color: var(--xr-font-color2); } .xr-var-preview { @@ -241,7 +248,7 @@ .xr-var-attrs, .xr-var-data { display: none; - background-color: #fff !important; + background-color: var(--xr-background-color) !important; padding-bottom: 5px !important; } @@ -288,7 +295,7 @@ dl.xr-attrs { .xr-attrs dt:hover span { display: inline-block; - background: #fff; + background: var(--xr-background-color); padding-right: 10px; } From 74ca69a3b7b53d2b8cc8c88ddaf0fe8c6c7bbf6c Mon Sep 17 00:00:00 2001 From: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Date: Tue, 29 Oct 2019 10:49:16 -0400 Subject: [PATCH 09/11] Remove deprecated behavior from dataset.drop docstring (#3451) * remove deprecated behavior from dataset.drop docstring * remove a few warnings too * actually keep original form but test for warnings --- xarray/core/dataset.py | 1 - xarray/tests/test_dataset.py | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 2a0464515c6..3ca9dd14fae 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -3542,7 +3542,6 @@ def drop( # noqa: F811 ---------- labels : hashable or iterable of hashables Name(s) of variables or index labels to drop. - If dim is not None, labels can be any array-like. dim : None or hashable, optional Dimension along which to drop index labels. By default (if ``dim is None``), drops variables rather than index labels. diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index 647eb733adb..dfb3da89569 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -2117,25 +2117,31 @@ def test_drop_variables(self): def test_drop_index_labels(self): data = Dataset({"A": (["x", "y"], np.random.randn(2, 3)), "x": ["a", "b"]}) - actual = data.drop(["a"], "x") + with pytest.warns(DeprecationWarning): + actual = data.drop(["a"], "x") expected = data.isel(x=[1]) assert_identical(expected, actual) - actual = data.drop(["a", "b"], "x") + with pytest.warns(DeprecationWarning): + actual = data.drop(["a", "b"], "x") expected = data.isel(x=slice(0, 0)) assert_identical(expected, actual) with pytest.raises(KeyError): # not contained in axis - data.drop(["c"], dim="x") + with pytest.warns(DeprecationWarning): + data.drop(["c"], dim="x") - actual = data.drop(["c"], dim="x", errors="ignore") + with pytest.warns(DeprecationWarning): + actual = data.drop(["c"], dim="x", errors="ignore") assert_identical(data, actual) with pytest.raises(ValueError): - data.drop(["c"], dim="x", errors="wrong_value") + with pytest.warns(DeprecationWarning): + data.drop(["c"], dim="x", errors="wrong_value") - actual = data.drop(["a", "b", "c"], "x", errors="ignore") + with pytest.warns(DeprecationWarning): + actual = data.drop(["a", "b", "c"], "x", errors="ignore") expected = data.isel(x=slice(0, 0)) assert_identical(expected, actual) From cb5eef1ad17e36626e2556bc2cfaf5c74aedf807 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Tue, 29 Oct 2019 11:30:54 -0400 Subject: [PATCH 10/11] Remove outdated code related to compatibility with netcdftime (#3450) * Remove code leftover from the netcdftime -> cftime transition * Add a what's new note * black formatting * Add more detail to what's new note * More minor edits to what's new note --- doc/whats-new.rst | 5 + xarray/coding/times.py | 43 +------ xarray/tests/__init__.py | 4 - xarray/tests/test_accessor_dt.py | 30 ++--- xarray/tests/test_cftimeindex.py | 10 +- xarray/tests/test_coding_times.py | 184 ++++++++++++------------------ xarray/tests/test_conventions.py | 10 +- xarray/tests/test_utils.py | 13 +-- 8 files changed, 100 insertions(+), 199 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 73618782460..82355a6bda4 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -22,6 +22,11 @@ Breaking changes ~~~~~~~~~~~~~~~~ - Minimum cftime version is now 1.0.3. By `Deepak Cherian `_. +- All leftover support for dates from non-standard calendars through netcdftime, the + module included in versions of netCDF4 prior to 1.4 that eventually became the + cftime package, has been removed in favor of relying solely on the standalone + cftime package (:pull:`3450`). By `Spencer Clark + `_. New Features ~~~~~~~~~~~~ diff --git a/xarray/coding/times.py b/xarray/coding/times.py index 0174088064b..965ddd8f043 100644 --- a/xarray/coding/times.py +++ b/xarray/coding/times.py @@ -39,34 +39,6 @@ ) -def _import_cftime(): - """ - helper function handle the transition to netcdftime/cftime - as a stand-alone package - """ - try: - import cftime - except ImportError: - # in netCDF4 the num2date/date2num function are top-level api - try: - import netCDF4 as cftime - except ImportError: - raise ImportError("Failed to import cftime") - return cftime - - -def _require_standalone_cftime(): - """Raises an ImportError if the standalone cftime is not found""" - try: - import cftime # noqa: F401 - except ImportError: - raise ImportError( - "Decoding times with non-standard calendars " - "or outside the pandas.Timestamp-valid range " - "requires the standalone cftime package." - ) - - def _netcdf_to_numpy_timeunit(units): units = units.lower() if not units.endswith("s"): @@ -119,16 +91,11 @@ def _decode_cf_datetime_dtype(data, units, calendar, use_cftime): def _decode_datetime_with_cftime(num_dates, units, calendar): - cftime = _import_cftime() + import cftime - if cftime.__name__ == "cftime": - return np.asarray( - cftime.num2date(num_dates, units, calendar, only_use_cftime_datetimes=True) - ) - else: - # Must be using num2date from an old version of netCDF4 which - # does not have the only_use_cftime_datetimes option. - return np.asarray(cftime.num2date(num_dates, units, calendar)) + return np.asarray( + cftime.num2date(num_dates, units, calendar, only_use_cftime_datetimes=True) + ) def _decode_datetime_with_pandas(flat_num_dates, units, calendar): @@ -354,7 +321,7 @@ def _encode_datetime_with_cftime(dates, units, calendar): This method is more flexible than xarray's parsing using datetime64[ns] arrays but also slower because it loops over each element. """ - cftime = _import_cftime() + import cftime if np.issubdtype(dates.dtype, np.datetime64): # numpy's broken datetime conversion only works for us precision diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index f85a33f7a3c..6592360cdf2 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -78,10 +78,6 @@ def LooseVersion(vstring): requires_scipy_or_netCDF4 = pytest.mark.skipif( not has_scipy_or_netCDF4, reason="requires scipy or netCDF4" ) -has_cftime_or_netCDF4 = has_cftime or has_netCDF4 -requires_cftime_or_netCDF4 = pytest.mark.skipif( - not has_cftime_or_netCDF4, reason="requires cftime or netCDF4" -) try: import_seaborn() has_seaborn = True diff --git a/xarray/tests/test_accessor_dt.py b/xarray/tests/test_accessor_dt.py index 0058747db71..5fe5b8c3f59 100644 --- a/xarray/tests/test_accessor_dt.py +++ b/xarray/tests/test_accessor_dt.py @@ -7,10 +7,8 @@ from . import ( assert_array_equal, assert_equal, - has_cftime, - has_cftime_or_netCDF4, - has_dask, raises_regex, + requires_cftime, requires_dask, ) @@ -199,7 +197,7 @@ def times_3d(times): ) -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize( "field", ["year", "month", "day", "hour", "dayofyear", "dayofweek"] ) @@ -217,7 +215,7 @@ def test_field_access(data, field): assert_equal(result, expected) -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime def test_cftime_strftime_access(data): """ compare cftime formatting against datetime formatting """ date_format = "%Y%m%d%H" @@ -232,8 +230,8 @@ def test_cftime_strftime_access(data): assert_equal(result, expected) -@pytest.mark.skipif(not has_dask, reason="dask not installed") -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime +@requires_dask @pytest.mark.parametrize( "field", ["year", "month", "day", "hour", "dayofyear", "dayofweek"] ) @@ -254,8 +252,8 @@ def test_dask_field_access_1d(data, field): assert_equal(result.compute(), expected) -@pytest.mark.skipif(not has_dask, reason="dask not installed") -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime +@requires_dask @pytest.mark.parametrize( "field", ["year", "month", "day", "hour", "dayofyear", "dayofweek"] ) @@ -286,7 +284,7 @@ def cftime_date_type(calendar): return _all_cftime_date_types()[calendar] -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime def test_seasons(cftime_date_type): dates = np.array([cftime_date_type(2000, month, 15) for month in range(1, 13)]) dates = xr.DataArray(dates) @@ -307,15 +305,3 @@ def test_seasons(cftime_date_type): seasons = xr.DataArray(seasons) assert_array_equal(seasons.values, dates.dt.season.values) - - -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime or netCDF4 not installed") -def test_dt_accessor_error_netCDF4(cftime_date_type): - da = xr.DataArray( - [cftime_date_type(1, 1, 1), cftime_date_type(2, 1, 1)], dims=["time"] - ) - if not has_cftime: - with pytest.raises(TypeError): - da.dt.month - else: - da.dt.month diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index e49dc72abdd..a8ee3c97042 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -15,7 +15,7 @@ ) from xarray.tests import assert_array_equal, assert_identical -from . import has_cftime, has_cftime_or_netCDF4, raises_regex, requires_cftime +from . import raises_regex, requires_cftime from .test_coding_times import ( _ALL_CALENDARS, _NON_STANDARD_CALENDARS, @@ -653,7 +653,7 @@ def test_indexing_in_dataframe_iloc(df, index): assert result.equals(expected) -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime def test_concat_cftimeindex(date_type): da1 = xr.DataArray( [1.0, 2.0], coords=[[date_type(1, 1, 1), date_type(1, 2, 1)]], dims=["time"] @@ -663,11 +663,7 @@ def test_concat_cftimeindex(date_type): ) da = xr.concat([da1, da2], dim="time") - if has_cftime: - assert isinstance(da.indexes["time"], CFTimeIndex) - else: - assert isinstance(da.indexes["time"], pd.Index) - assert not isinstance(da.indexes["time"], CFTimeIndex) + assert isinstance(da.indexes["time"], CFTimeIndex) @requires_cftime diff --git a/xarray/tests/test_coding_times.py b/xarray/tests/test_coding_times.py index 021d76e2b11..d012fb36c35 100644 --- a/xarray/tests/test_coding_times.py +++ b/xarray/tests/test_coding_times.py @@ -8,7 +8,6 @@ from xarray import DataArray, Dataset, Variable, coding, decode_cf from xarray.coding.times import ( - _import_cftime, cftime_to_nptime, decode_cf_datetime, encode_cf_datetime, @@ -19,15 +18,7 @@ from xarray.core.common import contains_cftime_datetimes from xarray.testing import assert_equal -from . import ( - arm_xfail, - assert_array_equal, - has_cftime, - has_cftime_or_netCDF4, - has_dask, - requires_cftime, - requires_cftime_or_netCDF4, -) +from . import arm_xfail, assert_array_equal, has_cftime, requires_cftime, requires_dask _NON_STANDARD_CALENDARS_SET = { "noleap", @@ -79,10 +70,8 @@ def _all_cftime_date_types(): - try: - import cftime - except ImportError: - import netcdftime as cftime + import cftime + return { "noleap": cftime.DatetimeNoLeap, "365_day": cftime.DatetimeNoLeap, @@ -95,16 +84,14 @@ def _all_cftime_date_types(): } -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize(["num_dates", "units", "calendar"], _CF_DATETIME_TESTS) def test_cf_datetime(num_dates, units, calendar): - cftime = _import_cftime() - if cftime.__name__ == "cftime": - expected = cftime.num2date( - num_dates, units, calendar, only_use_cftime_datetimes=True - ) - else: - expected = cftime.num2date(num_dates, units, calendar) + import cftime + + expected = cftime.num2date( + num_dates, units, calendar, only_use_cftime_datetimes=True + ) min_y = np.ravel(np.atleast_1d(expected))[np.nanargmin(num_dates)].year max_y = np.ravel(np.atleast_1d(expected))[np.nanargmax(num_dates)].year if min_y >= 1678 and max_y < 2262: @@ -138,15 +125,12 @@ def test_cf_datetime(num_dates, units, calendar): assert_array_equal(num_dates, np.around(encoded, 1)) -@requires_cftime_or_netCDF4 +@requires_cftime def test_decode_cf_datetime_overflow(): # checks for # https://github.com/pydata/pandas/issues/14068 # https://github.com/pydata/xarray/issues/975 - try: - from cftime import DatetimeGregorian - except ImportError: - from netcdftime import DatetimeGregorian + from cftime import DatetimeGregorian datetime = DatetimeGregorian units = "days since 2000-01-01 00:00:00" @@ -171,7 +155,7 @@ def test_decode_cf_datetime_non_standard_units(): assert_array_equal(actual, expected) -@requires_cftime_or_netCDF4 +@requires_cftime def test_decode_cf_datetime_non_iso_strings(): # datetime strings that are _almost_ ISO compliant but not quite, # but which cftime.num2date can still parse correctly @@ -190,10 +174,10 @@ def test_decode_cf_datetime_non_iso_strings(): assert (abs_diff <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _STANDARD_CALENDARS) def test_decode_standard_calendar_inside_timestamp_range(calendar): - cftime = _import_cftime() + import cftime units = "days since 0001-01-01" times = pd.date_range("2001-04-01-00", end="2001-04-30-23", freq="H") @@ -210,21 +194,18 @@ def test_decode_standard_calendar_inside_timestamp_range(calendar): assert (abs_diff <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _NON_STANDARD_CALENDARS) def test_decode_non_standard_calendar_inside_timestamp_range(calendar): - cftime = _import_cftime() + import cftime + units = "days since 0001-01-01" times = pd.date_range("2001-04-01-00", end="2001-04-30-23", freq="H") non_standard_time = cftime.date2num(times.to_pydatetime(), units, calendar=calendar) - if cftime.__name__ == "cftime": - expected = cftime.num2date( - non_standard_time, units, calendar=calendar, only_use_cftime_datetimes=True - ) - else: - expected = cftime.num2date(non_standard_time, units, calendar=calendar) - + expected = cftime.num2date( + non_standard_time, units, calendar=calendar, only_use_cftime_datetimes=True + ) expected_dtype = np.dtype("O") actual = coding.times.decode_cf_datetime( @@ -238,24 +219,19 @@ def test_decode_non_standard_calendar_inside_timestamp_range(calendar): assert (abs_diff <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _ALL_CALENDARS) def test_decode_dates_outside_timestamp_range(calendar): + import cftime from datetime import datetime - cftime = _import_cftime() - units = "days since 0001-01-01" times = [datetime(1, 4, 1, h) for h in range(1, 5)] time = cftime.date2num(times, units, calendar=calendar) - if cftime.__name__ == "cftime": - expected = cftime.num2date( - time, units, calendar=calendar, only_use_cftime_datetimes=True - ) - else: - expected = cftime.num2date(time, units, calendar=calendar) - + expected = cftime.num2date( + time, units, calendar=calendar, only_use_cftime_datetimes=True + ) expected_date_type = type(expected[0]) with warnings.catch_warnings(): @@ -269,7 +245,7 @@ def test_decode_dates_outside_timestamp_range(calendar): assert (abs_diff <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _STANDARD_CALENDARS) def test_decode_standard_calendar_single_element_inside_timestamp_range(calendar): units = "days since 0001-01-01" @@ -280,7 +256,7 @@ def test_decode_standard_calendar_single_element_inside_timestamp_range(calendar assert actual.dtype == np.dtype("M8[ns]") -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _NON_STANDARD_CALENDARS) def test_decode_non_standard_calendar_single_element_inside_timestamp_range(calendar): units = "days since 0001-01-01" @@ -291,10 +267,11 @@ def test_decode_non_standard_calendar_single_element_inside_timestamp_range(cale assert actual.dtype == np.dtype("O") -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _NON_STANDARD_CALENDARS) def test_decode_single_element_outside_timestamp_range(calendar): - cftime = _import_cftime() + import cftime + units = "days since 0001-01-01" for days in [1, 1470376]: for num_time in [days, [days], [[days]]]: @@ -304,20 +281,16 @@ def test_decode_single_element_outside_timestamp_range(calendar): num_time, units, calendar=calendar ) - if cftime.__name__ == "cftime": - expected = cftime.num2date( - days, units, calendar, only_use_cftime_datetimes=True - ) - else: - expected = cftime.num2date(days, units, calendar) - + expected = cftime.num2date( + days, units, calendar, only_use_cftime_datetimes=True + ) assert isinstance(actual.item(), type(expected)) -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _STANDARD_CALENDARS) def test_decode_standard_calendar_multidim_time_inside_timestamp_range(calendar): - cftime = _import_cftime() + import cftime units = "days since 0001-01-01" times1 = pd.date_range("2001-04-01", end="2001-04-05", freq="D") @@ -343,10 +316,10 @@ def test_decode_standard_calendar_multidim_time_inside_timestamp_range(calendar) assert (abs_diff2 <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _NON_STANDARD_CALENDARS) def test_decode_nonstandard_calendar_multidim_time_inside_timestamp_range(calendar): - cftime = _import_cftime() + import cftime units = "days since 0001-01-01" times1 = pd.date_range("2001-04-01", end="2001-04-05", freq="D") @@ -382,13 +355,12 @@ def test_decode_nonstandard_calendar_multidim_time_inside_timestamp_range(calend assert (abs_diff2 <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", _ALL_CALENDARS) def test_decode_multidim_time_outside_timestamp_range(calendar): + import cftime from datetime import datetime - cftime = _import_cftime() - units = "days since 0001-01-01" times1 = [datetime(1, 4, day) for day in range(1, 6)] times2 = [datetime(1, 5, day) for day in range(1, 6)] @@ -398,16 +370,8 @@ def test_decode_multidim_time_outside_timestamp_range(calendar): mdim_time[:, 0] = time1 mdim_time[:, 1] = time2 - if cftime.__name__ == "cftime": - expected1 = cftime.num2date( - time1, units, calendar, only_use_cftime_datetimes=True - ) - expected2 = cftime.num2date( - time2, units, calendar, only_use_cftime_datetimes=True - ) - else: - expected1 = cftime.num2date(time1, units, calendar) - expected2 = cftime.num2date(time2, units, calendar) + expected1 = cftime.num2date(time1, units, calendar, only_use_cftime_datetimes=True) + expected2 = cftime.num2date(time2, units, calendar, only_use_cftime_datetimes=True) with warnings.catch_warnings(): warnings.filterwarnings("ignore", "Unable to decode time axis") @@ -424,46 +388,38 @@ def test_decode_multidim_time_outside_timestamp_range(calendar): assert (abs_diff2 <= np.timedelta64(1, "s")).all() -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("calendar", ["360_day", "all_leap", "366_day"]) def test_decode_non_standard_calendar_single_element(calendar): - cftime = _import_cftime() + import cftime + units = "days since 0001-01-01" - try: - dt = cftime.netcdftime.datetime(2001, 2, 29) - except AttributeError: - # Must be using the standalone cftime library - dt = cftime.datetime(2001, 2, 29) + dt = cftime.datetime(2001, 2, 29) num_time = cftime.date2num(dt, units, calendar) actual = coding.times.decode_cf_datetime(num_time, units, calendar=calendar) - if cftime.__name__ == "cftime": - expected = np.asarray( - cftime.num2date(num_time, units, calendar, only_use_cftime_datetimes=True) - ) - else: - expected = np.asarray(cftime.num2date(num_time, units, calendar)) + expected = np.asarray( + cftime.num2date(num_time, units, calendar, only_use_cftime_datetimes=True) + ) assert actual.dtype == np.dtype("O") assert expected == actual -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime def test_decode_360_day_calendar(): - cftime = _import_cftime() + import cftime + calendar = "360_day" # ensure leap year doesn't matter for year in [2010, 2011, 2012, 2013, 2014]: units = f"days since {year}-01-01" num_times = np.arange(100) - if cftime.__name__ == "cftime": - expected = cftime.num2date( - num_times, units, calendar, only_use_cftime_datetimes=True - ) - else: - expected = cftime.num2date(num_times, units, calendar) + expected = cftime.num2date( + num_times, units, calendar, only_use_cftime_datetimes=True + ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") @@ -477,7 +433,7 @@ def test_decode_360_day_calendar(): @arm_xfail -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize( ["num_dates", "units", "expected_list"], [ @@ -499,7 +455,7 @@ def test_cf_datetime_nan(num_dates, units, expected_list): assert_array_equal(expected, actual) -@requires_cftime_or_netCDF4 +@requires_cftime def test_decoded_cf_datetime_array_2d(): # regression test for GH1229 variable = Variable( @@ -548,7 +504,7 @@ def test_infer_datetime_units(dates, expected): ] -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize( "calendar", _NON_STANDARD_CALENDARS + ["gregorian", "proleptic_gregorian"] ) @@ -622,7 +578,7 @@ def test_infer_timedelta_units(deltas, expected): assert expected == coding.times.infer_timedelta_units(deltas) -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize( ["date_args", "expected"], [ @@ -649,7 +605,7 @@ def test_decode_cf(calendar): ds[v].attrs["units"] = "days since 2001-01-01" ds[v].attrs["calendar"] = calendar - if not has_cftime_or_netCDF4 and calendar not in _STANDARD_CALENDARS: + if not has_cftime and calendar not in _STANDARD_CALENDARS: with pytest.raises(ValueError): ds = decode_cf(ds) else: @@ -703,7 +659,7 @@ def test_decode_cf_time_bounds(): _update_bounds_attributes(ds.variables) -@requires_cftime_or_netCDF4 +@requires_cftime def test_encode_time_bounds(): time = pd.date_range("2000-01-16", periods=1) @@ -749,7 +705,7 @@ def calendar(request): @pytest.fixture() def times(calendar): - cftime = _import_cftime() + import cftime return cftime.num2date( np.arange(4), @@ -779,24 +735,24 @@ def times_3d(times): ) -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime def test_contains_cftime_datetimes_1d(data): assert contains_cftime_datetimes(data.time) -@pytest.mark.skipif(not has_dask, reason="dask not installed") -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime +@requires_dask def test_contains_cftime_datetimes_dask_1d(data): assert contains_cftime_datetimes(data.time.chunk()) -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime def test_contains_cftime_datetimes_3d(times_3d): assert contains_cftime_datetimes(times_3d) -@pytest.mark.skipif(not has_dask, reason="dask not installed") -@pytest.mark.skipif(not has_cftime, reason="cftime not installed") +@requires_cftime +@requires_dask def test_contains_cftime_datetimes_dask_3d(times_3d): assert contains_cftime_datetimes(times_3d.chunk()) @@ -806,13 +762,13 @@ def test_contains_cftime_datetimes_non_cftimes(non_cftime_data): assert not contains_cftime_datetimes(non_cftime_data) -@pytest.mark.skipif(not has_dask, reason="dask not installed") +@requires_dask @pytest.mark.parametrize("non_cftime_data", [DataArray([]), DataArray([1, 2])]) def test_contains_cftime_datetimes_non_cftimes_dask(non_cftime_data): assert not contains_cftime_datetimes(non_cftime_data.chunk()) -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime @pytest.mark.parametrize("shape", [(24,), (8, 3), (2, 4, 3)]) def test_encode_cf_datetime_overflow(shape): # Test for fix to GH 2272 @@ -837,7 +793,7 @@ def test_encode_cf_datetime_pandas_min(): assert calendar == expected_calendar -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime def test_time_units_with_timezone_roundtrip(calendar): # Regression test for GH 2649 expected_units = "days since 2000-01-01T00:00:00-05:00" diff --git a/xarray/tests/test_conventions.py b/xarray/tests/test_conventions.py index 42b2a679347..09002e252b4 100644 --- a/xarray/tests/test_conventions.py +++ b/xarray/tests/test_conventions.py @@ -21,7 +21,7 @@ from . import ( assert_array_equal, raises_regex, - requires_cftime_or_netCDF4, + requires_cftime, requires_dask, requires_netCDF4, ) @@ -81,7 +81,7 @@ def test_decode_cf_with_conflicting_fill_missing_value(): assert_identical(actual, expected) -@requires_cftime_or_netCDF4 +@requires_cftime class TestEncodeCFVariable: def test_incompatible_attributes(self): invalid_vars = [ @@ -144,7 +144,7 @@ def test_string_object_warning(self): assert_identical(original, encoded) -@requires_cftime_or_netCDF4 +@requires_cftime class TestDecodeCF: def test_dataset(self): original = Dataset( @@ -226,7 +226,7 @@ def test_invalid_time_units_raises_eagerly(self): with raises_regex(ValueError, "unable to decode time"): decode_cf(ds) - @requires_cftime_or_netCDF4 + @requires_cftime def test_dataset_repr_with_netcdf4_datetimes(self): # regression test for #347 attrs = {"units": "days since 0001-01-01", "calendar": "noleap"} @@ -239,7 +239,7 @@ def test_dataset_repr_with_netcdf4_datetimes(self): ds = decode_cf(Dataset({"time": ("time", [0, 1], attrs)})) assert "(time) datetime64[ns]" in repr(ds) - @requires_cftime_or_netCDF4 + @requires_cftime def test_decode_cf_datetime_transition_to_invalid(self): # manually create dataset with not-decoded date from datetime import datetime diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py index 5bb9deaf240..af87b94393d 100644 --- a/xarray/tests/test_utils.py +++ b/xarray/tests/test_utils.py @@ -9,7 +9,7 @@ from xarray.core import duck_array_ops, utils from xarray.core.utils import either_dict_or_kwargs -from . import assert_array_equal, has_cftime, has_cftime_or_netCDF4, requires_dask +from . import assert_array_equal, requires_cftime, requires_dask from .test_coding_times import _all_cftime_date_types @@ -39,17 +39,12 @@ def test_safe_cast_to_index(): assert expected.dtype == actual.dtype -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime def test_safe_cast_to_index_cftimeindex(): date_types = _all_cftime_date_types() for date_type in date_types.values(): dates = [date_type(1, 1, day) for day in range(1, 20)] - - if has_cftime: - expected = CFTimeIndex(dates) - else: - expected = pd.Index(dates) - + expected = CFTimeIndex(dates) actual = utils.safe_cast_to_index(np.array(dates)) assert_array_equal(expected, actual) assert expected.dtype == actual.dtype @@ -57,7 +52,7 @@ def test_safe_cast_to_index_cftimeindex(): # Test that datetime.datetime objects are never used in a CFTimeIndex -@pytest.mark.skipif(not has_cftime_or_netCDF4, reason="cftime not installed") +@requires_cftime def test_safe_cast_to_index_datetime_datetime(): dates = [datetime(1, 1, day) for day in range(1, 20)] From 278d2e6af6abd933dd1d43ac3ae70bc306412ae1 Mon Sep 17 00:00:00 2001 From: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Date: Tue, 29 Oct 2019 11:34:33 -0400 Subject: [PATCH 11/11] upgrade black verison to 19.10b0 (#3456) --- xarray/backends/api.py | 2 +- xarray/core/alignment.py | 2 +- xarray/core/combine.py | 2 +- xarray/core/computation.py | 8 ++++---- xarray/core/concat.py | 4 ++-- xarray/core/dataarray.py | 2 +- xarray/core/dataset.py | 2 +- xarray/core/groupby.py | 6 +++--- xarray/core/indexing.py | 4 ++-- xarray/core/merge.py | 4 ++-- xarray/core/variable.py | 4 ++-- xarray/plot/plot.py | 8 ++++---- xarray/tests/test_cftime_offsets.py | 2 +- xarray/tests/test_dataarray.py | 8 ++++---- xarray/tests/test_dataset.py | 6 +++--- 15 files changed, 32 insertions(+), 32 deletions(-) diff --git a/xarray/backends/api.py b/xarray/backends/api.py index 199516116b0..d23594fc675 100644 --- a/xarray/backends/api.py +++ b/xarray/backends/api.py @@ -677,7 +677,7 @@ def open_dataarray( "then select the variable you want." ) else: - data_array, = dataset.data_vars.values() + (data_array,) = dataset.data_vars.values() data_array._file_obj = dataset._file_obj diff --git a/xarray/core/alignment.py b/xarray/core/alignment.py index 1a33cb955c3..41ff5a3b32d 100644 --- a/xarray/core/alignment.py +++ b/xarray/core/alignment.py @@ -252,7 +252,7 @@ def align( if not indexes and len(objects) == 1: # fast path for the trivial case - obj, = objects + (obj,) = objects return (obj.copy(deep=copy),) all_indexes = defaultdict(list) diff --git a/xarray/core/combine.py b/xarray/core/combine.py index 19c327ec597..3308dcef285 100644 --- a/xarray/core/combine.py +++ b/xarray/core/combine.py @@ -954,7 +954,7 @@ def _auto_concat( "supply the ``concat_dim`` argument " "explicitly" ) - dim, = concat_dims + (dim,) = concat_dims return concat( datasets, dim=dim, diff --git a/xarray/core/computation.py b/xarray/core/computation.py index 1393d76f283..2ab2ab78416 100644 --- a/xarray/core/computation.py +++ b/xarray/core/computation.py @@ -145,7 +145,7 @@ def result_name(objects: list) -> Any: names = {getattr(obj, "name", _DEFAULT_NAME) for obj in objects} names.discard(_DEFAULT_NAME) if len(names) == 1: - name, = names + (name,) = names else: name = None return name @@ -187,7 +187,7 @@ def build_output_coords( if len(coords_list) == 1 and not exclude_dims: # we can skip the expensive merge - unpacked_coords, = coords_list + (unpacked_coords,) = coords_list merged_vars = dict(unpacked_coords.variables) else: # TODO: save these merged indexes, instead of re-computing them later @@ -237,7 +237,7 @@ def apply_dataarray_vfunc( for variable, coords in zip(result_var, result_coords) ) else: - coords, = result_coords + (coords,) = result_coords out = DataArray(result_var, coords, name=name, fastpath=True) return out @@ -384,7 +384,7 @@ def apply_dataset_vfunc( if signature.num_outputs > 1: out = tuple(_fast_dataset(*args) for args in zip(result_vars, list_of_coords)) else: - coord_vars, = list_of_coords + (coord_vars,) = list_of_coords out = _fast_dataset(result_vars, coord_vars) if keep_attrs and isinstance(first_obj, Dataset): diff --git a/xarray/core/concat.py b/xarray/core/concat.py index bcab136de8d..0d19990bdd0 100644 --- a/xarray/core/concat.py +++ b/xarray/core/concat.py @@ -148,10 +148,10 @@ def _calc_concat_dim_coord(dim): dim = dim_name elif not isinstance(dim, DataArray): coord = as_variable(dim).to_index_variable() - dim, = coord.dims + (dim,) = coord.dims else: coord = dim - dim, = coord.dims + (dim,) = coord.dims return dim, coord diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index 33dcad13204..0c220acaee0 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -616,7 +616,7 @@ def _level_coords(self) -> Dict[Hashable, Hashable]: if var.ndim == 1 and isinstance(var, IndexVariable): level_names = var.level_names if level_names is not None: - dim, = var.dims + (dim,) = var.dims level_coords.update({lname: dim for lname in level_names}) return level_coords diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 3ca9dd14fae..05d9772cb7a 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -4066,7 +4066,7 @@ def reduce( if len(reduce_dims) == 1: # unpack dimensions for the benefit of functions # like np.argmin which can't handle tuple arguments - reduce_dims, = reduce_dims + (reduce_dims,) = reduce_dims elif len(reduce_dims) == var.ndim: # prefer to aggregate over axis=None rather than # axis=(0, 1) if they will be equivalent, because diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index c3f712b31ac..353566eb345 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -321,7 +321,7 @@ def __init__( raise ValueError("`group` must have a name") group, obj, stacked_dim, inserted_dims = _ensure_1d(group, obj) - group_dim, = group.dims + (group_dim,) = group.dims expected_size = obj.sizes[group_dim] if group.size != expected_size: @@ -470,7 +470,7 @@ def _infer_concat_args(self, applied_example): else: coord = self._unique_coord positions = None - dim, = coord.dims + (dim,) = coord.dims if isinstance(coord, _DummyGroup): coord = None return coord, dim, positions @@ -644,7 +644,7 @@ def _concat_shortcut(self, applied, dim, positions=None): def _restore_dim_order(self, stacked): def lookup_order(dimension): if dimension == self._group.name: - dimension, = self._group.dims + (dimension,) = self._group.dims if dimension in self._obj.dims: axis = self._obj.get_axis_num(dimension) else: diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py index b9809a8d2b9..f48c9e72af1 100644 --- a/xarray/core/indexing.py +++ b/xarray/core/indexing.py @@ -212,7 +212,7 @@ def get_dim_indexers(data_obj, indexers): level_indexers = defaultdict(dict) dim_indexers = {} for key, label in indexers.items(): - dim, = data_obj[key].dims + (dim,) = data_obj[key].dims if key != dim: # assume here multi-index level indexer level_indexers[dim][key] = label @@ -1368,7 +1368,7 @@ def __getitem__( if isinstance(key, tuple) and len(key) == 1: # unpack key so it can index a pandas.Index object (pandas.Index # objects don't like tuples) - key, = key + (key,) = key if getattr(key, "ndim", 0) > 1: # Return np-array if multidimensional return NumpyIndexingAdapter(self.array.values)[indexer] diff --git a/xarray/core/merge.py b/xarray/core/merge.py index db5ef9531df..389ceb155f7 100644 --- a/xarray/core/merge.py +++ b/xarray/core/merge.py @@ -277,7 +277,7 @@ def append_all(variables, indexes): def collect_from_coordinates( - list_of_coords: "List[Coordinates]" + list_of_coords: "List[Coordinates]", ) -> Dict[Hashable, List[MergeElement]]: """Collect variables and indexes to be merged from Coordinate objects.""" grouped: Dict[Hashable, List[Tuple[Variable, pd.Index]]] = {} @@ -320,7 +320,7 @@ def merge_coordinates_without_align( def determine_coords( - list_of_mappings: Iterable["DatasetLike"] + list_of_mappings: Iterable["DatasetLike"], ) -> Tuple[Set[Hashable], Set[Hashable]]: """Given a list of dicts with xarray object values, identify coordinates. diff --git a/xarray/core/variable.py b/xarray/core/variable.py index 7d03fd58d39..b7abdc7c462 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -1526,7 +1526,7 @@ def concat(cls, variables, dim="concat_dim", positions=None, shortcut=False): along the given dimension. """ if not isinstance(dim, str): - dim, = dim.dims + (dim,) = dim.dims # can't do this lazily: we need to loop through variables at least # twice @@ -1996,7 +1996,7 @@ def concat(cls, variables, dim="concat_dim", positions=None, shortcut=False): arrays, if possible. """ if not isinstance(dim, str): - dim, = dim.dims + (dim,) = dim.dims variables = list(variables) first_var = variables[0] diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index a288f195e32..ca68f617144 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -83,8 +83,8 @@ def _infer_line_data(darray, x, y, hue): ) else: - xdim, = darray[xname].dims - huedim, = darray[huename].dims + (xdim,) = darray[xname].dims + (huedim,) = darray[huename].dims yplt = darray.transpose(xdim, huedim) else: @@ -102,8 +102,8 @@ def _infer_line_data(darray, x, y, hue): ) else: - ydim, = darray[yname].dims - huedim, = darray[huename].dims + (ydim,) = darray[yname].dims + (huedim,) = darray[huename].dims xplt = darray.transpose(ydim, huedim) huelabel = label_from_attrs(darray[huename]) diff --git a/xarray/tests/test_cftime_offsets.py b/xarray/tests/test_cftime_offsets.py index 142769dbbe7..343e059f53c 100644 --- a/xarray/tests/test_cftime_offsets.py +++ b/xarray/tests/test_cftime_offsets.py @@ -1187,5 +1187,5 @@ def test_dayofyear_after_cftime_range(freq): def test_cftime_range_standard_calendar_refers_to_gregorian(): from cftime import DatetimeGregorian - result, = cftime_range("2000", periods=1) + (result,) = cftime_range("2000", periods=1) assert isinstance(result, DatetimeGregorian) diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py index ad474d533be..4b3ffdc021a 100644 --- a/xarray/tests/test_dataarray.py +++ b/xarray/tests/test_dataarray.py @@ -3125,11 +3125,11 @@ def test_align_copy(self): # Trivial align - 1 element x = DataArray([1, 2, 3], coords=[("a", [1, 2, 3])]) - x2, = align(x, copy=False) + (x2,) = align(x, copy=False) assert_identical(x, x2) assert source_ndarray(x2.data) is source_ndarray(x.data) - x2, = align(x, copy=True) + (x2,) = align(x, copy=True) assert_identical(x, x2) assert source_ndarray(x2.data) is not source_ndarray(x.data) @@ -3214,7 +3214,7 @@ def test_align_indexes(self): assert_identical(expected_x2, x2) assert_identical(expected_y2, y2) - x2, = align(x, join="outer", indexes={"a": [-2, 7, 10, -1]}) + (x2,) = align(x, join="outer", indexes={"a": [-2, 7, 10, -1]}) expected_x2 = DataArray([3, np.nan, 2, 1], coords=[("a", [-2, 7, 10, -1])]) assert_identical(expected_x2, x2) @@ -3293,7 +3293,7 @@ def test_broadcast_arrays_nocopy(self): assert source_ndarray(x2.data) is source_ndarray(x.data) # single-element broadcast (trivial case) - x2, = broadcast(x) + (x2,) = broadcast(x) assert_identical(x, x2) assert source_ndarray(x2.data) is source_ndarray(x.data) diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index dfb3da89569..eab6040e17e 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -1945,7 +1945,7 @@ def test_align_nocopy(self): def test_align_indexes(self): x = Dataset({"foo": DataArray([1, 2, 3], dims="x", coords=[("x", [1, 2, 3])])}) - x2, = align(x, indexes={"x": [2, 3, 1]}) + (x2,) = align(x, indexes={"x": [2, 3, 1]}) expected_x2 = Dataset( {"foo": DataArray([2, 3, 1], dims="x", coords={"x": [2, 3, 1]})} ) @@ -1973,7 +1973,7 @@ def test_broadcast(self): }, {"c": ("x", [4])}, ) - actual, = broadcast(ds) + (actual,) = broadcast(ds) assert_identical(expected, actual) ds_x = Dataset({"foo": ("x", [1])}) @@ -1995,7 +1995,7 @@ def test_broadcast_nocopy(self): x = Dataset({"foo": (("x", "y"), [[1, 1]])}) y = Dataset({"bar": ("y", [2, 3])}) - actual_x, = broadcast(x) + (actual_x,) = broadcast(x) assert_identical(x, actual_x) assert source_ndarray(actual_x["foo"].data) is source_ndarray(x["foo"].data)