Skip to content
This repository has been archived by the owner on Oct 24, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' into os.PathLike
Browse files Browse the repository at this point in the history
  • Loading branch information
TomNicholas authored Jan 22, 2024
2 parents d2545cb + 03b0a8c commit a87b27a
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 87 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/pypipublish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
name: Install Python
with:
python-version: 3.9
Expand All @@ -39,7 +39,7 @@ jobs:
python -m build --sdist --wheel .
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: releases
path: dist
Expand All @@ -48,11 +48,11 @@ jobs:
needs: build-artifacts
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
name: Install Python
with:
python-version: '3.10'
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: releases
path: dist
Expand All @@ -72,12 +72,12 @@ jobs:
if: github.event_name == 'release'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: releases
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.10
uses: pypa/gh-action-pypi-publish@v1.8.11
with:
user: ${{ secrets.PYPI_USERNAME }}
password: ${{ secrets.PYPI_PASSWORD }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ dmypy.json

# version
_version.py

# Ignore vscode specific settings
.vscode/
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ repos:
- 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.10.1
rev: 23.12.1
hooks:
- id: black
- repo: https://github.com/keewis/blackdoc
Expand All @@ -32,7 +32,7 @@ repos:
# - id: velin
# args: ["--write", "--compact"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.6.1
rev: v1.8.0
hooks:
- id: mypy
# Copied from setup.cfg
Expand Down
2 changes: 1 addition & 1 deletion ci/doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions datatree/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,17 @@ def _map_over_subtree(*args, **kwargs) -> DataTree | Tuple[DataTree, ...]:
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
Expand Down Expand Up @@ -279,7 +282,7 @@ def wrapper(*args, **kwargs):
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)

Expand Down
2 changes: 1 addition & 1 deletion datatree/testing.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 11 additions & 0 deletions datatree/tests/test_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,17 @@ def check_for_data(ds):

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"
)
Expand Down
11 changes: 7 additions & 4 deletions datatree/tests/test_treenode.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_ancestors(self):
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)


Expand Down Expand Up @@ -279,12 +279,15 @@ def test_levelorderiter(self):


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()
Expand Down
81 changes: 53 additions & 28 deletions datatree/treenode.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ def _check_loop(self, new_parent: Tree | None) -> None:
)

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:
Expand Down Expand Up @@ -240,26 +239,53 @@ def _post_attach_children(self: Tree, children: Mapping[str, Tree]) -> None:
"""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:
Expand Down Expand Up @@ -355,7 +381,7 @@ def level(self: Tree) -> int:
depth
width
"""
return len(self.ancestors) - 1
return len(self.parents)

@property
def depth(self: Tree) -> int:
Expand Down Expand Up @@ -593,9 +619,9 @@ def path(self) -> str:
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:
Expand All @@ -610,7 +636,7 @@ def relative_to(self: NamedNode, other: NamedNode) -> str:
)

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)
Expand All @@ -625,18 +651,17 @@ def find_common_ancestor(self, other: NamedNode) -> NamedNode:
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"""
Expand All @@ -645,12 +670,12 @@ def _path_to_ancestor(self, ancestor: NamedNode) -> NodePath:
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)
14 changes: 14 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit a87b27a

Please sign in to comment.