[Git][debian-gis-team/xarray-datatree][upstream] New upstream version 0.0.14

Antonio Valentino (@antonio.valentino) gitlab at salsa.debian.org
Sat Jan 27 11:46:34 GMT 2024



Antonio Valentino pushed to branch upstream at Debian GIS Project / xarray-datatree


Commits:
497d39b2 by Antonio Valentino at 2024-01-27T11:40:36+00:00
New upstream version 0.0.14
- - - - -


20 changed files:

- .github/workflows/pypipublish.yaml
- .gitignore
- .pre-commit-config.yaml
- ci/doc.yml
- datatree/datatree.py
- datatree/io.py
- datatree/mapping.py
- datatree/testing.py
- datatree/tests/test_io.py
- datatree/tests/test_mapping.py
- datatree/tests/test_treenode.py
- datatree/treenode.py
- + docs/README.md
- docs/source/api.rst
- docs/source/conf.py
- docs/source/data-structures.rst
- docs/source/hierarchical-data.rst
- docs/source/index.rst
- docs/source/whats-new.rst
- pyproject.toml


Changes:

=====================================
.github/workflows/pypipublish.yaml
=====================================
@@ -22,7 +22,7 @@ jobs:
       - uses: actions/checkout at v4
         with:
           fetch-depth: 0
-      - uses: actions/setup-python at v4
+      - uses: actions/setup-python at v5
         name: Install Python
         with:
           python-version: 3.9
@@ -39,7 +39,7 @@ jobs:
           python -m build --sdist --wheel .
 
 
-      - uses: actions/upload-artifact at v3
+      - uses: actions/upload-artifact at v4
         with:
           name: releases
           path: dist
@@ -48,11 +48,11 @@ jobs:
     needs: build-artifacts
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/setup-python at v4
+      - uses: actions/setup-python at v5
         name: Install Python
         with:
           python-version: '3.10'
-      - uses: actions/download-artifact at v3
+      - uses: actions/download-artifact at v4
         with:
           name: releases
           path: dist
@@ -72,12 +72,12 @@ jobs:
     if: github.event_name == 'release'
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/download-artifact at v3
+      - uses: actions/download-artifact at v4
         with:
           name: releases
           path: dist
       - name: Publish package to PyPI
-        uses: pypa/gh-action-pypi-publish at v1.8.10
+        uses: pypa/gh-action-pypi-publish at v1.8.11
         with:
           user: ${{ secrets.PYPI_USERNAME }}
           password: ${{ secrets.PYPI_PASSWORD }}


=====================================
.gitignore
=====================================
@@ -131,3 +131,6 @@ dmypy.json
 
 # version
 _version.py
+
+# Ignore vscode specific settings
+.vscode/


=====================================
.pre-commit-config.yaml
=====================================
@@ -3,23 +3,23 @@ ci:
   autoupdate_schedule: monthly
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.4.0
+    rev: v4.5.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
       - id: check-yaml
   # isort should run before black as black sometimes tweaks the isort output
   - repo: https://github.com/PyCQA/isort
-    rev: 5.12.0
+    rev: 5.13.2
     hooks:
       - id: isort
   # https://github.com/python/black#version-control-integration
   - repo: https://github.com/psf/black
-    rev: 23.9.1
+    rev: 23.12.1
     hooks:
       - id: black
   - repo: https://github.com/keewis/blackdoc
-    rev: v0.3.8
+    rev: v0.3.9
     hooks:
       - id: blackdoc
   - repo: https://github.com/PyCQA/flake8
@@ -32,7 +32,7 @@ repos:
   #     - id: velin
   #       args: ["--write", "--compact"]
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.5.1
+    rev: v1.8.0
     hooks:
       - id: mypy
         # Copied from setup.cfg


=====================================
ci/doc.yml
=====================================
@@ -13,8 +13,8 @@ dependencies:
   - sphinx-book-theme >= 0.0.38
   - nbsphinx
   - sphinxcontrib-srclinks
+  - pickleshare
   - pydata-sphinx-theme>=0.4.3
-  - numpydoc
   - ipython
   - h5netcdf
   - zarr


=====================================
datatree/datatree.py
=====================================
@@ -1496,7 +1496,12 @@ class DataTree(
         )
 
     def to_zarr(
-        self, store, mode: str = "w", encoding=None, consolidated: bool = True, **kwargs
+        self,
+        store,
+        mode: str = "w-",
+        encoding=None,
+        consolidated: bool = True,
+        **kwargs,
     ):
         """
         Write datatree contents to a Zarr store.
@@ -1505,7 +1510,7 @@ class DataTree(
         ----------
         store : MutableMapping, str or Path, optional
             Store or path to directory in file system
-        mode : {{"w", "w-", "a", "r+", None}, default: "w"
+        mode : {{"w", "w-", "a", "r+", None}, default: "w-"
             Persistence mode: “w” means create (overwrite if exists); “w-” means create (fail if exists);
             “a” means override existing variables (create if does not exist); “r+” means modify existing
             array values only (raise an error if any metadata or shapes would change). The default mode


=====================================
datatree/io.py
=====================================
@@ -176,7 +176,7 @@ def _create_empty_zarr_group(store, group, mode):
 def _datatree_to_zarr(
     dt: DataTree,
     store,
-    mode: str = "w",
+    mode: str = "w-",
     encoding=None,
     consolidated: bool = True,
     **kwargs,


=====================================
datatree/mapping.py
=====================================
@@ -206,14 +206,17 @@ def map_over_subtree(func: Callable) -> Callable:
                 node_of_first_tree.path
             )(func)
 
-            # Now we can call func on the data in this particular set of corresponding nodes
-            results = (
-                func_with_error_context(
+            if node_of_first_tree.has_data:
+                # call func on the data in this particular set of corresponding nodes
+                results = func_with_error_context(
                     *node_args_as_datasetviews, **node_kwargs_as_datasetviews
                 )
-                if node_of_first_tree.has_data
-                else None
-            )
+            elif node_of_first_tree.has_attrs:
+                # propagate attrs
+                results = node_of_first_tree.ds
+            else:
+                # nothing to propagate so use fastpath to create empty node in new tree
+                results = None
 
             # TODO implement mapping over multiple trees in-place using if conditions from here on?
             out_data_objects[node_of_first_tree.path] = results
@@ -279,7 +282,7 @@ def _handle_errors_with_path_context(path):
 def add_note(err: BaseException, msg: str) -> None:
     # TODO: remove once python 3.10 can be dropped
     if sys.version_info < (3, 11):
-        err.__notes__ = getattr(err, "__notes__", []) + [msg]
+        err.__notes__ = getattr(err, "__notes__", []) + [msg]  # type: ignore[attr-defined]
     else:
         err.add_note(msg)
 


=====================================
datatree/testing.py
=====================================
@@ -1,4 +1,4 @@
-from xarray.testing import ensure_warnings
+from xarray.testing.assertions import ensure_warnings
 
 from .datatree import DataTree
 from .formatting import diff_tree_repr


=====================================
datatree/tests/test_io.py
=====================================
@@ -1,4 +1,5 @@
 import pytest
+import zarr.errors
 
 from datatree.io import open_datatree
 from datatree.testing import assert_equal
@@ -109,3 +110,11 @@ class TestIO:
         with pytest.warns(RuntimeWarning, match="consolidated"):
             roundtrip_dt = open_datatree(filepath, engine="zarr")
         assert_equal(original_dt, roundtrip_dt)
+
+    @requires_zarr
+    def test_to_zarr_default_write_mode(self, tmpdir, simple_datatree):
+        simple_datatree.to_zarr(tmpdir)
+
+        # with default settings, to_zarr should not overwrite an existing dir
+        with pytest.raises(zarr.errors.ContainsGroupError):
+            simple_datatree.to_zarr(tmpdir)


=====================================
datatree/tests/test_mapping.py
=====================================
@@ -264,6 +264,17 @@ class TestMapOverSubTree:
 
         dt.map_over_subtree(check_for_data)
 
+    def test_keep_attrs_on_empty_nodes(self, create_test_datatree):
+        # GH278
+        dt = create_test_datatree()
+        dt["set1/set2"].attrs["foo"] = "bar"
+
+        def empty_func(ds):
+            return ds
+
+        result = dt.map_over_subtree(empty_func)
+        assert result["set1/set2"].attrs == dt["set1/set2"].attrs
+
     @pytest.mark.xfail(
         reason="probably some bug in pytests handling of exception notes"
     )


=====================================
datatree/tests/test_treenode.py
=====================================
@@ -95,7 +95,7 @@ class TestFamilyTree:
         michael = TreeNode(children={"Tony": tony})
         vito = TreeNode(children={"Michael": michael})
         assert tony.root is vito
-        assert tony.lineage == (tony, michael, vito)
+        assert tony.parents == (michael, vito)
         assert tony.ancestors == (vito, michael, tony)
 
 
@@ -279,12 +279,15 @@ class TestIterators:
 
 
 class TestAncestry:
+    def test_parents(self):
+        _, leaf = create_test_tree()
+        expected = ["e", "b", "a"]
+        assert [node.name for node in leaf.parents] == expected
+
     def test_lineage(self):
         _, leaf = create_test_tree()
-        lineage = leaf.lineage
         expected = ["f", "e", "b", "a"]
-        for node, expected_name in zip(lineage, expected):
-            assert node.name == expected_name
+        assert [node.name for node in leaf.lineage] == expected
 
     def test_ancestors(self):
         _, leaf = create_test_tree()


=====================================
datatree/treenode.py
=====================================
@@ -121,8 +121,7 @@ class TreeNode(Generic[Tree]):
                 )
 
     def _is_descendant_of(self, node: Tree) -> bool:
-        _self, *lineage = list(node.lineage)
-        return any(n is self for n in lineage)
+        return any(n is self for n in node.parents)
 
     def _detach(self, parent: Tree | None) -> None:
         if parent is not None:
@@ -236,26 +235,53 @@ class TreeNode(Generic[Tree]):
         """Method call after attaching `children`."""
         pass
 
-    def iter_lineage(self: Tree) -> Iterator[Tree]:
+    def _iter_parents(self: Tree) -> Iterator[Tree]:
         """Iterate up the tree, starting from the current node."""
-        node: Tree | None = self
+        node: Tree | None = self.parent
         while node is not None:
             yield node
             node = node.parent
 
+    def iter_lineage(self: Tree) -> Tuple[Tree, ...]:
+        """Iterate up the tree, starting from the current node."""
+        from warnings import warn
+
+        warn(
+            "`iter_lineage` has been deprecated, and in the future will raise an error."
+            "Please use `parents` from now on.",
+            DeprecationWarning,
+        )
+        return tuple((self, *self.parents))
+
     @property
     def lineage(self: Tree) -> Tuple[Tree, ...]:
         """All parent nodes and their parent nodes, starting with the closest."""
-        return tuple(self.iter_lineage())
+        from warnings import warn
+
+        warn(
+            "`lineage` has been deprecated, and in the future will raise an error."
+            "Please use `parents` from now on.",
+            DeprecationWarning,
+        )
+        return self.iter_lineage()
+
+    @property
+    def parents(self: Tree) -> Tuple[Tree, ...]:
+        """All parent nodes and their parent nodes, starting with the closest."""
+        return tuple(self._iter_parents())
 
     @property
     def ancestors(self: Tree) -> Tuple[Tree, ...]:
         """All parent nodes and their parent nodes, starting with the most distant."""
-        if self.parent is None:
-            return (self,)
-        else:
-            ancestors = tuple(reversed(list(self.lineage)))
-            return ancestors
+
+        from warnings import warn
+
+        warn(
+            "`ancestors` has been deprecated, and in the future will raise an error."
+            "Please use `parents`. Example: `tuple(reversed(node.parents))`",
+            DeprecationWarning,
+        )
+        return tuple((*reversed(self.parents), self))
 
     @property
     def root(self: Tree) -> Tree:
@@ -351,7 +377,7 @@ class TreeNode(Generic[Tree]):
         depth
         width
         """
-        return len(self.ancestors) - 1
+        return len(self.parents)
 
     @property
     def depth(self: Tree) -> int:
@@ -591,9 +617,9 @@ class NamedNode(TreeNode, Generic[Tree]):
         if self.is_root:
             return "/"
         else:
-            root, *ancestors = self.ancestors
+            root, *ancestors = tuple(reversed(self.parents))
             # don't include name of root because (a) root might not have a name & (b) we want path relative to root.
-            names = [node.name for node in ancestors]
+            names = [*(node.name for node in ancestors), self.name]
             return "/" + "/".join(names)
 
     def relative_to(self: NamedNode, other: NamedNode) -> str:
@@ -608,7 +634,7 @@ class NamedNode(TreeNode, Generic[Tree]):
             )
 
         this_path = NodePath(self.path)
-        if other.path in list(ancestor.path for ancestor in self.lineage):
+        if other.path in list(parent.path for parent in (self, *self.parents)):
             return str(this_path.relative_to(other.path))
         else:
             common_ancestor = self.find_common_ancestor(other)
@@ -623,18 +649,17 @@ class NamedNode(TreeNode, Generic[Tree]):
 
         Raise ValueError if they are not in the same tree.
         """
-        common_ancestor = None
-        for node in other.iter_lineage():
-            if node.path in [ancestor.path for ancestor in self.ancestors]:
-                common_ancestor = node
-                break
+        if self is other:
+            return self
 
-        if not common_ancestor:
-            raise NotFoundInTreeError(
-                "Cannot find common ancestor because nodes do not lie within the same tree"
-            )
+        other_paths = [op.path for op in other.parents]
+        for parent in (self, *self.parents):
+            if parent.path in other_paths:
+                return parent
 
-        return common_ancestor
+        raise NotFoundInTreeError(
+            "Cannot find common ancestor because nodes do not lie within the same tree"
+        )
 
     def _path_to_ancestor(self, ancestor: NamedNode) -> NodePath:
         """Return the relative path from this node to the given ancestor node"""
@@ -643,12 +668,12 @@ class NamedNode(TreeNode, Generic[Tree]):
             raise NotFoundInTreeError(
                 "Cannot find relative path to ancestor because nodes do not lie within the same tree"
             )
-        if ancestor.path not in list(a.path for a in self.ancestors):
+        if ancestor.path not in list(a.path for a in (self, *self.parents)):
             raise NotFoundInTreeError(
                 "Cannot find relative path to ancestor because given node is not an ancestor of this node"
             )
 
-        lineage_paths = list(ancestor.path for ancestor in self.lineage)
-        generation_gap = list(lineage_paths).index(ancestor.path)
-        path_upwards = "../" * generation_gap if generation_gap > 0 else "/"
+        parents_paths = list(parent.path for parent in (self, *self.parents))
+        generation_gap = list(parents_paths).index(ancestor.path)
+        path_upwards = "../" * generation_gap if generation_gap > 0 else "."
         return NodePath(path_upwards)


=====================================
docs/README.md
=====================================
@@ -0,0 +1,14 @@
+# README - docs
+
+## Build the documentation locally
+
+```bash
+cd docs # From project's root
+make clean
+rm -rf source/generated # remove autodoc artefacts, that are not removed by `make clean`
+make html
+```
+
+## Access the documentation locally
+
+Open `docs/_build/html/index.html` in a web browser


=====================================
docs/source/api.rst
=====================================
@@ -10,10 +10,13 @@ DataTree
 Creating a DataTree
 -------------------
 
+Methods of creating a datatree.
+
 .. autosummary::
    :toctree: generated/
 
    DataTree
+   DataTree.from_dict
 
 Tree Attributes
 ---------------
@@ -38,6 +41,7 @@ Attributes relating to the recursive tree-like structure of a ``DataTree``.
    DataTree.descendants
    DataTree.siblings
    DataTree.lineage
+   DataTree.parents
    DataTree.ancestors
    DataTree.groups
 
@@ -57,7 +61,6 @@ This interface echoes that of ``xarray.Dataset``.
    DataTree.attrs
    DataTree.encoding
    DataTree.indexes
-   DataTree.chunks
    DataTree.nbytes
    DataTree.ds
    DataTree.to_dataset
@@ -66,12 +69,7 @@ This interface echoes that of ``xarray.Dataset``.
    DataTree.is_empty
    DataTree.is_hollow
 
-..
-
-   Missing:
-   ``DataTree.chunksizes``
-
-Dictionary interface
+Dictionary Interface
 --------------------
 
 ``DataTree`` objects also have a dict-like interface mapping keys to either ``xarray.DataArray``s or to child ``DataTree`` nodes.
@@ -101,11 +99,36 @@ For manipulating, traversing, navigating, or mapping over the tree structure.
    DataTree.relative_to
    DataTree.iter_lineage
    DataTree.find_common_ancestor
+   DataTree.map_over_subtree
    map_over_subtree
    DataTree.pipe
    DataTree.match
    DataTree.filter
 
+Pathlib-like Interface
+----------------------
+
+``DataTree`` objects deliberately echo some of the API of `pathlib.PurePath`.
+
+.. autosummary::
+   :toctree: generated/
+
+   DataTree.name
+   DataTree.parent
+   DataTree.parents
+   DataTree.relative_to
+
+Missing:
+
+..
+
+   ``DataTree.glob``
+   ``DataTree.joinpath``
+   ``DataTree.with_name``
+   ``DataTree.walk``
+   ``DataTree.rename``
+   ``DataTree.replace``
+
 DataTree Contents
 -----------------
 
@@ -254,9 +277,7 @@ Methods copied from :py:class:`numpy.ndarray` objects, here applying to the data
    DataTree.clip
    DataTree.conj
    DataTree.conjugate
-   DataTree.imag
    DataTree.round
-   DataTree.real
    DataTree.rank
 
 Reshaping and reorganising
@@ -282,13 +303,12 @@ Plotting
 I/O
 ===
 
-Create or
+Open a datatree from an on-disk store or serialize the tree.
 
 .. autosummary::
    :toctree: generated/
 
    open_datatree
-   DataTree.from_dict
    DataTree.to_dict
    DataTree.to_netcdf
    DataTree.to_zarr


=====================================
docs/source/conf.py
=====================================
@@ -39,7 +39,6 @@ sys.path.insert(0, parent)
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
-    "numpydoc",
     "sphinx.ext.autodoc",
     "sphinx.ext.viewcode",
     "sphinx.ext.linkcode",
@@ -57,8 +56,8 @@ extensions = [
 ]
 
 extlinks = {
-    "issue": ("https://github.com/TomNicholas/datatree/issues/%s", "GH#"),
-    "pull": ("https://github.com/TomNicholas/datatree/pull/%s", "GH#"),
+    "issue": ("https://github.com/xarray-contrib/datatree/issues/%s", "GH#%s"),
+    "pull": ("https://github.com/xarray-contrib/datatree/pull/%s", "GH#%s"),
 }
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ["_templates", sphinx_autosummary_accessors.templates_path]
@@ -66,6 +65,69 @@ templates_path = ["_templates", sphinx_autosummary_accessors.templates_path]
 # Generate the API documentation when building
 autosummary_generate = True
 
+
+# Napoleon configurations
+
+napoleon_google_docstring = False
+napoleon_numpy_docstring = True
+napoleon_use_param = False
+napoleon_use_rtype = False
+napoleon_preprocess_types = True
+napoleon_type_aliases = {
+    # general terms
+    "sequence": ":term:`sequence`",
+    "iterable": ":term:`iterable`",
+    "callable": ":py:func:`callable`",
+    "dict_like": ":term:`dict-like <mapping>`",
+    "dict-like": ":term:`dict-like <mapping>`",
+    "path-like": ":term:`path-like <path-like object>`",
+    "mapping": ":term:`mapping`",
+    "file-like": ":term:`file-like <file-like object>`",
+    # special terms
+    # "same type as caller": "*same type as caller*",  # does not work, yet
+    # "same type as values": "*same type as values*",  # does not work, yet
+    # stdlib type aliases
+    "MutableMapping": "~collections.abc.MutableMapping",
+    "sys.stdout": ":obj:`sys.stdout`",
+    "timedelta": "~datetime.timedelta",
+    "string": ":class:`string <str>`",
+    # numpy terms
+    "array_like": ":term:`array_like`",
+    "array-like": ":term:`array-like <array_like>`",
+    "scalar": ":term:`scalar`",
+    "array": ":term:`array`",
+    "hashable": ":term:`hashable <name>`",
+    # matplotlib terms
+    "color-like": ":py:func:`color-like <matplotlib.colors.is_color_like>`",
+    "matplotlib colormap name": ":doc:`matplotlib colormap name <matplotlib:gallery/color/colormap_reference>`",
+    "matplotlib axes object": ":py:class:`matplotlib axes object <matplotlib.axes.Axes>`",
+    "colormap": ":py:class:`colormap <matplotlib.colors.Colormap>`",
+    # objects without namespace: xarray
+    "DataArray": "~xarray.DataArray",
+    "Dataset": "~xarray.Dataset",
+    "Variable": "~xarray.Variable",
+    "DatasetGroupBy": "~xarray.core.groupby.DatasetGroupBy",
+    "DataArrayGroupBy": "~xarray.core.groupby.DataArrayGroupBy",
+    # objects without namespace: numpy
+    "ndarray": "~numpy.ndarray",
+    "MaskedArray": "~numpy.ma.MaskedArray",
+    "dtype": "~numpy.dtype",
+    "ComplexWarning": "~numpy.ComplexWarning",
+    # objects without namespace: pandas
+    "Index": "~pandas.Index",
+    "MultiIndex": "~pandas.MultiIndex",
+    "CategoricalIndex": "~pandas.CategoricalIndex",
+    "TimedeltaIndex": "~pandas.TimedeltaIndex",
+    "DatetimeIndex": "~pandas.DatetimeIndex",
+    "Series": "~pandas.Series",
+    "DataFrame": "~pandas.DataFrame",
+    "Categorical": "~pandas.Categorical",
+    "Path": "~~pathlib.Path",
+    # objects with abbreviated namespace (from pandas)
+    "pd.Index": "~pandas.Index",
+    "pd.NaT": "~pandas.NaT",
+}
+
 # The suffix of source filenames.
 source_suffix = ".rst"
 
@@ -177,11 +239,6 @@ html_theme_options = {
 # pixels large.
 # html_favicon = None
 
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
-
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
 # html_last_updated_fmt = '%b %d, %Y'


=====================================
docs/source/data-structures.rst
=====================================
@@ -75,7 +75,7 @@ Again these are not normally used unless explicitly accessed by the user.
 Creating a DataTree
 ~~~~~~~~~~~~~~~~~~~
 
-One way to create a create a ``DataTree`` from scratch is to create each node individually,
+One way to create a ``DataTree`` from scratch is to create each node individually,
 specifying the nodes' relationship to one another as you create each one.
 
 The ``DataTree`` constructor takes:


=====================================
docs/source/hierarchical-data.rst
=====================================
@@ -369,25 +369,6 @@ You can see this tree is similar to the ``dt`` object above, except that it is m
 
 (If you want to keep the name of the root node, you will need to add the ``name`` kwarg to :py:class:`from_dict`, i.e. ``DataTree.from_dict(non_empty_nodes, name=dt.root.name)``.)
 
-.. _Tree Contents:
-
-Tree Contents
--------------
-
-Hollow Trees
-~~~~~~~~~~~~
-
-A concept that can sometimes be useful is that of a "Hollow Tree", which means a tree with data stored only at the leaf nodes.
-This is useful because certain useful tree manipulation operations only make sense for hollow trees.
-
-You can check if a tree is a hollow tree by using the :py:meth:`~DataTree.is_hollow` property.
-We can see that the Simpson's family is not hollow because the data variable ``"age"`` is present at some nodes which
-have children (i.e. Abe and Homer).
-
-.. ipython:: python
-
-    simpsons.is_hollow
-
 .. _manipulating trees:
 
 Manipulating Trees
@@ -412,6 +393,7 @@ We can use :py:meth:`DataTree.match` for this:
         }
     )
     result = dt.match("*/B")
+    result
 
 We can also subset trees by the contents of the nodes.
 :py:meth:`DataTree.filter` retains only the nodes of a tree that meet a certain condition.
@@ -443,6 +425,25 @@ The result is a new tree, containing only the nodes matching the condition.
 
 (Yes, under the hood :py:meth:`~DataTree.filter` is just syntactic sugar for the pattern we showed you in :ref:`iterating over trees` !)
 
+.. _Tree Contents:
+
+Tree Contents
+-------------
+
+Hollow Trees
+~~~~~~~~~~~~
+
+A concept that can sometimes be useful is that of a "Hollow Tree", which means a tree with data stored only at the leaf nodes.
+This is useful because certain useful tree manipulation operations only make sense for hollow trees.
+
+You can check if a tree is a hollow tree by using the :py:class:`~DataTree.is_hollow` property.
+We can see that the Simpson's family is not hollow because the data variable ``"age"`` is present at some nodes which
+have children (i.e. Abe and Homer).
+
+.. ipython:: python
+
+    simpsons.is_hollow
+
 .. _tree computation:
 
 Computation
@@ -539,7 +540,7 @@ See that the same change (fast-forwarding by adding 10 years to the age of each
 Mapping Custom Functions Over Trees
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-You can map custom computation over each node in a tree using :py:func:`map_over_subtree`.
+You can map custom computation over each node in a tree using :py:meth:`DataTree.map_over_subtree`.
 You can map any function, so long as it takes `xarray.Dataset` objects as one (or more) of the input arguments,
 and returns one (or more) xarray datasets.
 
@@ -559,10 +560,13 @@ Then calculate the RMS value of these signals:
 
 .. ipython:: python
 
-    rms(readings)
+    voltages.map_over_subtree(rms)
 
 .. _multiple trees:
 
+We can also use the :py:func:`map_over_subtree` decorator to promote a function which accepts datasets into one which
+accepts datatrees.
+
 Operating on Multiple Trees
 ---------------------------
 
@@ -596,7 +600,7 @@ Notice that corresponding tree nodes do not need to have the same name or contai
 Arithmetic Between Multiple Trees
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Arithmetic operations like multiplication are binary operations, so as long as we have wo isomorphic trees,
+Arithmetic operations like multiplication are binary operations, so as long as we have two isomorphic trees,
 we can do arithmetic between them.
 
 .. ipython:: python


=====================================
docs/source/index.rst
=====================================
@@ -50,7 +50,6 @@ Please raise any thoughts, issues, suggestions or bugs, no matter how small or l
    Reading and Writing Files <io>
    API Reference <api>
    Terminology <terminology>
-   How do I ... <howdoi>
    Contributing Guide <contributing>
    What's New <whats-new>
    GitHub repository <https://github.com/xarray-contrib/datatree>


=====================================
docs/source/whats-new.rst
=====================================
@@ -15,9 +15,51 @@ What's New
 
     np.random.seed(123456)
 
+.. _whats-new.v0.0.14:
+
+v0.0.14 (unreleased)
+--------------------
+
+New Features
+~~~~~~~~~~~~
+
+Breaking changes
+~~~~~~~~~~~~~~~~
+
+- Renamed `DataTree.lineage` to `DataTree.parents` to match `pathlib` vocabulary
+  (:issue:`283`, :pull:`286`)
+- Minimum required version of xarray is now 2023.12.0, i.e. the latest version.
+  This is required to prevent recent changes to xarray's internals from breaking datatree.
+  (:issue:`293`, :pull:`294`)
+  By `Tom Nicholas <https://github.com/TomNicholas>`_.
+- Change default write mode of :py:meth:`DataTree.to_zarr` to ``'w-'`` to match ``xarray``
+  default and prevent accidental directory overwrites. (:issue:`274`, :pull:`275`)
+  By `Sam Levang <https://github.com/slevang>`_.
+
+Deprecations
+~~~~~~~~~~~~
+
+- Renamed `DataTree.lineage` to `DataTree.parents` to match `pathlib` vocabulary
+  (:issue:`283`, :pull:`286`). `lineage` is now deprecated and use of `parents` is encouraged.
+  By `Etienne Schalk <https://github.com/etienneschalk>`_.
+
+Bug fixes
+~~~~~~~~~
+- Keep attributes on nodes containing no data in :py:func:`map_over_subtree`. (:issue:`278`, :pull:`279`)
+  By `Sam Levang <https://github.com/slevang>`_.
+
+Documentation
+~~~~~~~~~~~~~
+- Use ``napoleon`` instead of ``numpydoc`` to align with xarray documentation
+  (:issue:`284`, :pull:`298`).
+  By `Etienne Schalk <https://github.com/etienneschalk>`_.
+
+Internal Changes
+~~~~~~~~~~~~~~~~
+
 .. _whats-new.v0.0.13:
 
-v0.0.13 (unreleased)
+v0.0.13 (27/10/2023)
 --------------------
 
 New Features
@@ -39,9 +81,6 @@ Breaking changes
 - Disallow altering of given dataset inside function called by :py:func:`map_over_subtree` (:pull:`269`, reverts part of :pull:`194`).
   By `Tom Nicholas <https://github.com/TomNicholas>`_.
 
-Deprecations
-~~~~~~~~~~~~
-
 Bug fixes
 ~~~~~~~~~
 
@@ -335,7 +374,7 @@ Breaking changes
 - Removes the option to delete all data in a node by assigning None to the node (in favour of deleting data by replacing
   the node's ``.ds`` attribute with an empty Dataset), or to create a new empty node in the same way (in favour of
   assigning an empty DataTree object instead).
-- Removes the ability to create a new node by assigning a ``Dataset`` object to ``DataTree.__setitem__`.
+- Removes the ability to create a new node by assigning a ``Dataset`` object to ``DataTree.__setitem__``.
 - Several other minor API changes such as ``.pathstr`` -> ``.path``, and ``from_dict``'s dictionary argument now being
   required. (:pull:`76`)
   By `Tom Nicholas <https://github.com/TomNicholas>`_.


=====================================
pyproject.toml
=====================================
@@ -19,7 +19,7 @@ classifiers = [
 ]
 requires-python = ">=3.9"
 dependencies = [
-    "xarray >=2022.6.0",
+    "xarray >=2023.12.0",
     "packaging",
 ]
 dynamic = ["version"]



View it on GitLab: https://salsa.debian.org/debian-gis-team/xarray-datatree/-/commit/497d39b228013ab8a35c58aa5cc1fd029cb886d5

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/xarray-datatree/-/commit/497d39b228013ab8a35c58aa5cc1fd029cb886d5
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20240127/499ea7f4/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list