Skip to content
Open
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: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
- Load plugins from entry points allowing plugins to be discovered from installed libraries.
- Automatically generate rule documentation removing the manual need to run `mkruleref.py`.

- Fixed passing of global datatree attributes to children: attributes defined
on parent datatrees are now inherited by all descendants. (#63)


## Version 0.5.1 (from 2025-02-21)

Expand Down
110 changes: 110 additions & 0 deletions tests/test_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ def validate_datatree(self, ctx: RuleContext, node: DataTreeNode):
if len(node.datatree.data_vars) == 0:
ctx.report("DataTree does not have data variables")

@plugin.define_rule("datatree-children-must-have-title")
class DataTreeAttrsVer(RuleOp):
def validate_datatree(self, ctx: RuleContext, node: DataTreeNode):
if "title" not in node.datatree.attrs:
ctx.report("DataTree must have a least a global title")

@plugin.define_processor("multi-level-dataset")
class MultiLevelDataset(ProcessorOp):
def preprocess(
Expand Down Expand Up @@ -167,6 +173,7 @@ def test_rules_are_ok(self):
"data-var-dim-must-have-coord",
"dataset-without-data-vars",
"datatree-without-data-vars",
"datatree-children-must-have-title",
],
list(self.linter.config.objects[0].plugins["test"].rules.keys()),
)
Expand Down Expand Up @@ -308,6 +315,109 @@ def test_linter_recognized_datatree_rule(self):
self.assertEqual(5, result.error_count)
self.assertEqual(0, result.fatal_error_count)

def test_linter_missing_global_datatree_attrs(self):
result = self.linter.validate(
xr.DataTree(
children={
"measurement": xr.DataTree(
children={
"r10m": xr.DataTree(
dataset=xr.Dataset(
attrs={
"title": "10m resolution datatree",
}
)
),
"r20m": xr.DataTree(),
"r60m": xr.DataTree(),
}
)
},
),
rules={"test/datatree-children-must-have-title": 2},
)

self.assertEqual(
[
Message(
message="DataTree must have a least a global title",
node_path="dt",
rule_id="test/datatree-children-must-have-title",
severity=2,
fatal=None,
fix=None,
suggestions=None,
),
Message(
message="DataTree must have a least a global title",
node_path="dt/measurement",
rule_id="test/datatree-children-must-have-title",
severity=2,
fatal=None,
fix=None,
suggestions=None,
),
Message(
message="DataTree must have a least a global title",
node_path="dt/measurement/r20m",
rule_id="test/datatree-children-must-have-title",
severity=2,
fatal=None,
fix=None,
suggestions=None,
),
Message(
message="DataTree must have a least a global title",
node_path="dt/measurement/r60m",
rule_id="test/datatree-children-must-have-title",
severity=2,
fatal=None,
fix=None,
suggestions=None,
),
],
result.messages,
)
self.assertEqual(0, result.warning_count)
self.assertEqual(4, result.error_count)
self.assertEqual(0, result.fatal_error_count)

def test_linter_global_datatree_attrs(self):
result = self.linter.validate(
xr.DataTree(
dataset=xr.Dataset(
attrs={
"title": "Global datatree title",
}
),
children={
"measurement": xr.DataTree(
children={
"r10m": xr.DataTree(
dataset=xr.Dataset(
attrs={
"title": "10m resolution datatree",
}
)
),
"r20m": xr.DataTree(),
"r60m": xr.DataTree(),
}
)
},
),
rules={"test/datatree-children-must-have-title": 2},
)

print(result.messages)
self.assertEqual(
[],
result.messages,
)
self.assertEqual(0, result.warning_count)
self.assertEqual(0, result.error_count)
self.assertEqual(0, result.fatal_error_count)

def test_linter_real_life_scenario(self):
dataset = xr.Dataset(
attrs={
Expand Down
14 changes: 13 additions & 1 deletion xrlint/_linter/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,33 @@ def apply_rule(


def _visit_datatree_node(rule_op: RuleOp, context: RuleContextImpl, node: DataTreeNode):
# Get a copy of the current node's attrs.
# These will be merged into each child's attrs so that attributes
# defined on parent nodes are inherited by all descendants.
attrs = node.datatree.attrs.copy()

with context.use_state(node=node):
rule_op.validate_datatree(context, node)

if node.datatree.is_leaf:
# Inherit attrs from the parent datatree into the child dataset
dataset = node.datatree.dataset.copy()
dataset.attrs = {**attrs, **dataset.attrs}
_visit_dataset_node(
rule_op,
context,
DatasetNode(
parent=node,
path=f"{node.path}/{node.datatree.name}",
name=node.datatree.name,
dataset=node.datatree.dataset,
dataset=dataset,
),
)
else:
for name, datatree in node.datatree.children.items():
# Inherit attrs from the parent datatree into the child datatree
datatree = datatree.copy()
datatree.attrs = {**attrs, **datatree.attrs}
_visit_datatree_node(
rule_op,
context,
Expand Down