Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/bugreport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ body:
attributes:
label: Environment
description: |
Paste the output of `xr.show_versions()` between the `<details>` tags, leaving an empty line following the opening tag.
Replace the `<details>` block below with the output of `xr.show_versions(markdown=True)`, which already includes the `<details>` tags.
Older versions of xarray do not support `markdown=True`; then paste the output of `xr.show_versions()` between the `<details>` tags, leaving an empty line following the opening tag.
value: |
<details>

Expand Down
1 change: 1 addition & 0 deletions doc/api/top-level.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Miscellaneous

decode_cf
infer_freq
show_backends
show_versions
set_options
get_options
10 changes: 10 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ New Features
type defaults to ``Hashable``, so a bare ``Variable`` annotation means
``Variable[Hashable]`` and keeps its previous meaning (:pull:`11677`).
By `Michael Niklas <https://github.com/headtr1ck>`_.
- Add :py:func:`~xarray.show_backends` to print the available IO backends,
including those of other packages, and in which order they are tried
(:issue:`6577`, :pull:`11700`).
By `Nick Hodgskin <https://github.com/VeckoTheGecko>`_ and
`Michael Niklas <https://github.com/headtr1ck>`_.
- :py:func:`~xarray.show_versions` displays formatted tables in Jupyter
notebooks and gained a ``markdown`` option to print markdown that can be pasted
as is into GitHub issues. Package versions are read from the
package metadata, so the packages are no longer imported (:pull:`11700`).
By `Michael Niklas <https://github.com/headtr1ck>`_.
- The ``in`` operator on :py:class:`DataTree` now accepts the same unix-like
paths as indexing, so ``key in tree`` is ``True`` exactly when ``tree[key]``
succeeds, e.g. ``"group/subgroup" in tree`` or ``"/group/var" in tree``.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ module = [
"fsspec.*",
"h5netcdf.*",
"h5py.*",
"IPython.*",
"iris.*",
"mpl_toolkits.*",
"nc_time_axis.*",
Expand Down
2 changes: 2 additions & 0 deletions xarray/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
open_groups,
open_mfdataset,
)
from xarray.backends.plugins import show_backends
from xarray.backends.writers import save_mfdataset
from xarray.backends.zarr import open_zarr
from xarray.coding.cftime_offsets import cftime_range, date_range, date_range_like
Expand Down Expand Up @@ -114,6 +115,7 @@
"register_datatree_accessor",
"save_mfdataset",
"set_options",
"show_backends",
"show_versions",
"unify_chunks",
"where",
Expand Down
81 changes: 80 additions & 1 deletion xarray/backends/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import os
import warnings
from collections.abc import Callable
from html import escape
from importlib.metadata import entry_points
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, TextIO

from xarray.backends.common import BACKEND_ENTRYPOINTS, BackendEntrypoint
from xarray.core.options import OPTIONS
from xarray.core.utils import is_remote_uri, module_available
from xarray.util._report import package_version, show_report

if TYPE_CHECKING:
from importlib.metadata import EntryPoint, EntryPoints
Expand Down Expand Up @@ -135,6 +137,83 @@ def refresh_engines() -> None:
list_engines.cache_clear()


def _engine_sources() -> dict[str, tuple[str, str | None]]:
"""Map each engine name to its providing package and version."""
sources: dict[str, tuple[str, str | None]] = dict.fromkeys(
BACKEND_ENTRYPOINTS, ("xarray", None)
)
for ep in entry_points(group="xarray.backends"):
if ep.dist is not None:
sources[ep.name] = (ep.dist.name, ep.dist.version)
else:
sources[ep.name] = (ep.module.partition(".")[0], None)
return sources


def show_backends(file: TextIO | None = None, markdown: bool = False) -> None:
"""Print the available backends (engines) and information about them.

The backends are listed in the order in which they are tried when opening
a file without specifying the ``engine``.
Use :py:func:`xarray.backends.list_engines` to get the backend objects.
In Jupyter notebooks the backends are displayed formatted.

Parameters
----------
file : file-like, optional
print to the given file-like object. Defaults to sys.stdout.
markdown : bool, default: False
print a markdown list wrapped in a ``<details>`` block, which can be
pasted as is into a GitHub issue.
"""
engines = list_engines()
sources = _engine_sources()

text_lines = []
md_lines = []
html_items = []
for name, backend in engines.items():
package, version = sources.get(name, ("unknown", None))
summary = f"from {package}"
if version is not None:
summary += f" {version}"
details = []
if package == "xarray" and name in BACKEND_ENTRYPOINTS:
dependency = BACKEND_ENTRYPOINTS[name][0]
if dependency is not None:
details.append(f"using {dependency} {package_version(dependency)}")
if backend.supports_groups:
details.append("supports groups")
if details:
summary += f" ({', '.join(details)})"
cls_name = type(backend).__name__
text_lines.append(f"{name}: {cls_name} {summary}")
md_lines.append(f"- **{name}**: `{cls_name}` {summary}")

extra = []
html_extra = []
if backend.description:
description = " ".join(backend.description.split())
extra.append(description)
html_extra.append(escape(description))
if backend.url:
extra.append(backend.url)
url = escape(backend.url)
html_extra.append(f'<a href="{url}" target="_blank">{url}</a>')
text_lines.extend(f" {line}" for line in extra)
md_lines.extend(f" - {line}" for line in extra)
html_items.append(
f"<li><strong>{escape(name)}</strong>: <code>{escape(cls_name)}</code> "
f"{escape(summary)}<ul>{''.join(f'<li>{x}</li>' for x in html_extra)}</ul></li>"
)

empty = "No backends available."
text = "\n".join(text_lines) or empty
md = "\n".join(md_lines) or empty
html = f"<ul>{''.join(html_items)}</ul>" if html_items else f"<p>{empty}</p>"
show_report("AVAILABLE BACKENDS", text, md, html, file=file, as_markdown=markdown)


def guess_engine(
store_spec: str
| os.PathLike[Any]
Expand Down
57 changes: 57 additions & 0 deletions xarray/tests/backends/test_plugins.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import io
import subprocess
import sys
from importlib.metadata import EntryPoint, EntryPoints
Expand All @@ -8,6 +9,7 @@

import pytest

import xarray as xr
from xarray.backends import common, plugins
from xarray.core.options import OPTIONS
from xarray.tests import (
Expand Down Expand Up @@ -372,6 +374,61 @@ def test_refresh_engines() -> None:
refresh_engines()


class DummyDescribedBackend(common.BackendEntrypoint):
description = "Open dummy files"
url = "https://example.com/dummy"
supports_groups = True

def open_dataset(self, filename_or_obj, *, decoder): # type: ignore[override]
pass


def test_show_backends() -> None:
from xarray.backends import list_engines, refresh_engines

dist = mock.MagicMock()
dist.name = "dummy-pkg"
dist.version = "1.2.3"
ep = mock.MagicMock()
ep.name = "dummy"
ep.dist = dist
ep.load.return_value = DummyDescribedBackend

text = io.StringIO()
md = io.StringIO()
with mock.patch(
"xarray.backends.plugins.entry_points", return_value=EntryPoints([ep])
):
list_engines.cache_clear()
xr.show_backends(file=text)
xr.show_backends(file=md, markdown=True)
refresh_engines()

out = text.getvalue()
assert out.startswith("AVAILABLE BACKENDS\n------------------\n")
expected_dummy = (
"dummy: DummyDescribedBackend from dummy-pkg 1.2.3 (supports groups)\n"
" Open dummy files\n"
" https://example.com/dummy\n"
)
assert expected_dummy in out
assert "store: StoreBackendEntrypoint from xarray\n" in out
# backends that are not installed are not listed
assert ("\nscipy: " in out) == has_scipy
if has_scipy:
assert "scipy: ScipyBackendEntrypoint from xarray (using scipy " in out

out = md.getvalue()
assert out.startswith("<details><summary>AVAILABLE BACKENDS</summary>\n\n")
expected_dummy = (
"- **dummy**: `DummyDescribedBackend` from dummy-pkg 1.2.3 (supports groups)\n"
" - Open dummy files\n"
" - https://example.com/dummy\n"
)
assert expected_dummy in out
assert "- **store**: `StoreBackendEntrypoint` from xarray\n" in out


@requires_h5netcdf
@requires_netCDF4
@requires_zarr
Expand Down
93 changes: 92 additions & 1 deletion xarray/tests/test_print_versions.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,106 @@
from __future__ import annotations

import importlib.metadata
import io
import sys
import types
from html import escape
from unittest import mock

import pytest

import xarray
from xarray.util import _report


# importing setuptools after distutils (done by some dependencies) warns
@pytest.mark.filterwarnings("ignore:Setuptools is replacing distutils:UserWarning")
def test_show_versions() -> None:
f = io.StringIO()
xarray.show_versions(file=f)
assert "INSTALLED VERSIONS" in f.getvalue()
out = f.getvalue()
assert out.startswith("INSTALLED VERSIONS\n------------------\n")
assert f"\nxarray: {importlib.metadata.version('xarray')}\n" in out
assert "Not installed: " in out


@pytest.mark.filterwarnings("ignore:Setuptools is replacing distutils:UserWarning")
def test_show_versions_markdown() -> None:
f = io.StringIO()
xarray.show_versions(file=f, markdown=True)
out = f.getvalue()
assert out.startswith("<details><summary>INSTALLED VERSIONS</summary>\n\n")
assert out.endswith("\n</details>\n")
assert "| package " in out
assert "| xarray " in out


def test_package_version() -> None:
assert _report.package_version("xarray") == importlib.metadata.version("xarray")
assert _report.package_version("not_a_real_package_xyz") is None


def test_package_version_dist_name_differs() -> None:
with mock.patch.object(
_report, "_packages_distributions", return_value={"fake_mod": ["pytest"]}
):
assert _report.package_version("fake_mod") == pytest.__version__


def test_markdown_table() -> None:
actual = _report.markdown_table(("a", "bb"), [("x", "1 | 2"), ("long", None)])
expected = (
"| a | bb |\n| ---- | ------ |\n| x | 1 \\| 2 |\n| long | None |"
)
assert actual == expected


def test_show_report(monkeypatch) -> None:
displayed: list[dict[str, str]] = []

def display(bundle, raw=False):
assert raw
displayed.append(bundle)

display_mod = types.ModuleType("IPython.display")
display_mod.display = display # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "IPython.display", display_mod)

md = "<details><summary>TITLE</summary>\n\n*md*\n\n</details>"

def show(**kwargs) -> str:
f = io.StringIO()
_report.show_report("TITLE", "text", "*md*", "<p>x</p>", file=f, **kwargs)
return f.getvalue()

assert show() == "TITLE\n-----\ntext\n"
assert show(as_markdown=True) == md + "\n"

# in notebooks the html is displayed, unless printing is requested
monkeypatch.setattr(_report, "_in_rich_frontend", lambda: True)
_report.show_report("TITLE", "text", "*md*", "<p>x</p>")
(bundle,) = displayed
assert bundle["text/plain"] == "TITLE\n-----\ntext"
# copying the output copies the markdown
assert bundle["text/html"].startswith(f'<div data-md="{escape(md)}" oncopy="')
assert bundle["text/html"].endswith("><h4>TITLE</h4><p>x</p></div>")

assert show() == "TITLE\n-----\ntext\n"
stdout = io.StringIO()
monkeypatch.setattr(sys, "stdout", stdout)
_report.show_report("TITLE", "text", "*md*", "<p>x</p>", as_markdown=True)
assert stdout.getvalue() == md + "\n"
assert len(displayed) == 1


def test_in_rich_frontend_without_ipython(monkeypatch) -> None:
monkeypatch.delitem(sys.modules, "IPython", raising=False)
assert not _report._in_rich_frontend()


def test_html_table() -> None:
actual = _report.html_table("T<", [("a", "<b>")])
assert actual == (
'<div><strong>T&lt;</strong><table><tr><td style="text-align: left">a</td>'
'<td style="text-align: left">&lt;b&gt;</td></tr></table></div>'
)
Loading
Loading