Skip to content

Commit ff3bfd7

Browse files
committed
introduced DataTreeNode; added _linter.validate._visit_datatree_node(); added rule.RuleOp.validate_datatree()
1 parent 1426d5f commit ff3bfd7

File tree

11 files changed

+153
-57
lines changed

11 files changed

+153
-57
lines changed

tests/_linter/test_rulectx.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
# noinspection PyProtectedMember
1010
from xrlint._linter.rulectx import RuleContextImpl
1111
from xrlint.config import ConfigObject
12-
from xrlint.constants import NODE_ROOT_NAME
12+
from xrlint.constants import DATASET_ROOT_NAME
1313
from xrlint.result import Message, Suggestion
1414

1515

@@ -39,7 +39,7 @@ def test_report(self):
3939
[
4040
Message(
4141
message="What the heck do you mean?",
42-
node_path=NODE_ROOT_NAME,
42+
node_path=DATASET_ROOT_NAME,
4343
rule_id="no-xxx",
4444
severity=2,
4545
suggestions=[
@@ -48,7 +48,7 @@ def test_report(self):
4848
),
4949
Message(
5050
message="You said it.",
51-
node_path=NODE_ROOT_NAME,
51+
node_path=DATASET_ROOT_NAME,
5252
rule_id="no-xxx",
5353
severity=2,
5454
fatal=True,

tests/cli/test_main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def xrlint(self, *args: tuple[str, ...]) -> click.testing.Result:
6666
runner = CliRunner()
6767
result = runner.invoke(main, args)
6868
if not isinstance(result.exception, SystemExit):
69+
import traceback
70+
71+
traceback.print_exception(result.exception)
6972
self.assertIsNone(result.exception)
7073
return result
7174

tests/plugins/core/rules/test_access_latency.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# noinspection PyProtectedMember
1111
from xrlint._linter.rulectx import RuleContextImpl
1212
from xrlint.config import ConfigObject
13+
from xrlint.constants import DATASET_ROOT_NAME
1314
from xrlint.node import DatasetNode
1415
from xrlint.plugins.core.rules.access_latency import AccessLatency
1516
from xrlint.result import Message
@@ -33,8 +34,9 @@ def invoke_op(
3334
access_latency=access_latency,
3435
)
3536
node = DatasetNode(
36-
path="dataset",
3737
parent=None,
38+
path=DATASET_ROOT_NAME,
39+
name=DATASET_ROOT_NAME,
3840
dataset=ctx.dataset,
3941
)
4042
rule_op = (
@@ -59,7 +61,7 @@ def test_invalid(self):
5961
[
6062
Message(
6163
message="Access latency exceeds threshold: 3.2 > 2.5 seconds.",
62-
node_path="dataset",
64+
node_path=DATASET_ROOT_NAME,
6365
severity=2,
6466
)
6567
],
@@ -71,7 +73,7 @@ def test_invalid(self):
7173
[
7274
Message(
7375
message="Access latency exceeds threshold: 0.2 > 0.1 seconds.",
74-
node_path="dataset",
76+
node_path=DATASET_ROOT_NAME,
7577
severity=2,
7678
)
7779
],

tests/test_linter.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import xarray as xr
99

1010
from xrlint.config import Config, ConfigObject
11-
from xrlint.constants import CORE_PLUGIN_NAME, NODE_ROOT_NAME
11+
from xrlint.constants import CORE_PLUGIN_NAME, DATASET_ROOT_NAME
1212
from xrlint.linter import Linter, new_linter
1313
from xrlint.node import AttrNode, AttrsNode, DatasetNode, VariableNode
1414
from xrlint.plugin import new_plugin
@@ -179,7 +179,7 @@ def test_linter_respects_rule_severity_error(self):
179179
messages=[
180180
Message(
181181
message="Dataset does not have data variables",
182-
node_path="dataset",
182+
node_path=DATASET_ROOT_NAME,
183183
rule_id="test/dataset-without-data-vars",
184184
severity=2,
185185
)
@@ -204,7 +204,7 @@ def test_linter_respects_rule_severity_warn(self):
204204
messages=[
205205
Message(
206206
message="Dataset does not have data variables",
207-
node_path="dataset",
207+
node_path=DATASET_ROOT_NAME,
208208
rule_id="test/dataset-without-data-vars",
209209
severity=1,
210210
)
@@ -238,7 +238,7 @@ def test_linter_recognized_unknown_rule(self):
238238
Message(
239239
message="unknown rule 'test/dataset-is-fast'",
240240
rule_id="test/dataset-is-fast",
241-
node_path=NODE_ROOT_NAME,
241+
node_path=DATASET_ROOT_NAME,
242242
severity=2,
243243
fatal=True,
244244
)
@@ -294,13 +294,13 @@ def test_linter_real_life_scenario(self):
294294
messages=[
295295
Message(
296296
message="Attribute name with space: 'created at'",
297-
node_path="dataset.attrs['created at']",
297+
node_path=f"{DATASET_ROOT_NAME}.attrs['created at']",
298298
rule_id="test/no-space-in-attr-name",
299299
severity=2,
300300
),
301301
Message(
302302
message="Empty attributes",
303-
node_path="dataset.data_vars['tsm'].attrs",
303+
node_path=f"{DATASET_ROOT_NAME}.data_vars['tsm'].attrs",
304304
rule_id="test/no-empty-attrs",
305305
severity=1,
306306
),
@@ -310,7 +310,7 @@ def test_linter_real_life_scenario(self):
310310
"variable 'chl' is missing a "
311311
"coordinate variable"
312312
),
313-
node_path="dataset.data_vars['chl']",
313+
node_path=f"{DATASET_ROOT_NAME}.data_vars['chl']",
314314
rule_id="test/data-var-dim-must-have-coord",
315315
severity=2,
316316
),
@@ -320,7 +320,7 @@ def test_linter_real_life_scenario(self):
320320
"variable 'tsm' is missing a "
321321
"coordinate variable"
322322
),
323-
node_path="dataset.data_vars['tsm']",
323+
node_path=f"{DATASET_ROOT_NAME}.data_vars['tsm']",
324324
rule_id="test/data-var-dim-must-have-coord",
325325
severity=2,
326326
),
@@ -342,13 +342,13 @@ def test_processor_ok(self):
342342
[
343343
Message(
344344
message="Dataset does not have data variables",
345-
node_path="dataset[0]",
345+
node_path=f"{DATASET_ROOT_NAME}[0]",
346346
rule_id="test/dataset-without-data-vars",
347347
severity=1,
348348
),
349349
Message(
350350
message="Dataset does not have data variables",
351-
node_path="dataset[1]",
351+
node_path=f"{DATASET_ROOT_NAME}[1]",
352352
rule_id="test/dataset-without-data-vars",
353353
severity=1,
354354
),
@@ -371,7 +371,7 @@ def test_processor_fail(self):
371371
message="bad checksum",
372372
severity=2,
373373
fatal=True,
374-
node_path=NODE_ROOT_NAME,
374+
node_path=DATASET_ROOT_NAME,
375375
)
376376
],
377377
result.messages,

xrlint/_linter/apply.py

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,95 @@
22
# This software is distributed under the terms and conditions of the
33
# MIT license (https://mit-license.org/).
44

5-
from xrlint.node import AttrNode, AttrsNode, DatasetNode, VariableNode
5+
from xrlint.node import AttrNode, AttrsNode, DataTreeNode, DatasetNode, VariableNode
66
from xrlint.rule import RuleConfig, RuleExit, RuleOp
77

8-
from ..constants import NODE_ROOT_NAME
8+
from ..constants import DATASET_ROOT_NAME, DATATREE_ROOT_NAME
99
from .rulectx import RuleContextImpl
1010

1111

1212
def apply_rule(
13-
context: RuleContextImpl,
13+
ctx: RuleContextImpl,
1414
rule_id: str,
1515
rule_config: RuleConfig,
1616
):
1717
"""Apply rule given by `rule_id` to dataset given in
1818
`context` using rule configuration `rule_config`.
1919
"""
2020
try:
21-
rule = context.config.get_rule(rule_id)
21+
rule = ctx.config.get_rule(rule_id)
2222
except ValueError as e:
23-
context.report(f"{e}", fatal=True)
23+
ctx.report(f"{e}", fatal=True)
2424
return
2525

2626
if rule_config.severity == 0:
2727
# rule is off
2828
return
2929

30-
with context.use_state(severity=rule_config.severity):
30+
with ctx.use_state(severity=rule_config.severity):
3131
# TODO: validate rule_config.args/kwargs against rule.meta.schema
3232
# noinspection PyArgumentList
3333
rule_op: RuleOp = rule.op_class(*rule_config.args, **rule_config.kwargs)
3434
try:
35+
if ctx.datatree is not None:
36+
name = (
37+
DATATREE_ROOT_NAME
38+
if ctx.file_index is None
39+
else f"{DATATREE_ROOT_NAME}[{ctx.file_index}]"
40+
)
41+
_visit_datatree_node(
42+
rule_op,
43+
ctx,
44+
DataTreeNode(
45+
parent=None, path=name, name=name, datatree=ctx.datatree
46+
),
47+
)
48+
else:
49+
name = (
50+
DATASET_ROOT_NAME
51+
if ctx.file_index is None
52+
else f"{DATASET_ROOT_NAME}[{ctx.file_index}]"
53+
)
54+
_visit_dataset_node(
55+
rule_op,
56+
ctx,
57+
DatasetNode(parent=None, path=name, name=name, dataset=ctx.dataset),
58+
)
59+
except RuleExit:
60+
# This is ok, the rule requested it.
61+
pass
62+
63+
64+
def _visit_datatree_node(rule_op: RuleOp, context: RuleContextImpl, node: DataTreeNode):
65+
with context.use_state(node=node):
66+
rule_op.validate_datatree(context, node)
67+
if node.datatree.is_leaf:
3568
_visit_dataset_node(
3669
rule_op,
3770
context,
3871
DatasetNode(
39-
parent=None,
40-
path=(
41-
NODE_ROOT_NAME
42-
if context.file_index is None
43-
else f"{NODE_ROOT_NAME}[{context.file_index}]"
44-
),
45-
dataset=context.dataset,
72+
parent=node,
73+
path=f"{node.path}/{node.datatree.name}",
74+
name=node.datatree.name,
75+
dataset=node.datatree.dataset,
4676
),
4777
)
48-
except RuleExit:
49-
# This is ok, the rule requested it.
50-
pass
78+
else:
79+
for name, datatree in node.datatree.children.items():
80+
_visit_datatree_node(
81+
rule_op,
82+
context,
83+
DataTreeNode(
84+
parent=node,
85+
path=f"{node.path}/{name}",
86+
name=name,
87+
datatree=datatree,
88+
),
89+
)
5190

5291

5392
def _visit_dataset_node(rule_op: RuleOp, context: RuleContextImpl, node: DatasetNode):
54-
with context.use_state(node=node):
93+
with context.use_state(dataset=node.dataset, node=node):
5594
rule_op.validate_dataset(context, node)
5695
_visit_attrs_node(
5796
rule_op,

xrlint/_linter/rulectx.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import xarray as xr
99

1010
from xrlint.config import ConfigObject
11-
from xrlint.constants import NODE_ROOT_NAME, SEVERITY_ERROR
11+
from xrlint.constants import DATASET_ROOT_NAME, SEVERITY_ERROR
1212
from xrlint.node import Node
1313
from xrlint.result import Message, Suggestion
1414
from xrlint.rule import RuleContext
@@ -18,16 +18,26 @@ class RuleContextImpl(RuleContext):
1818
def __init__(
1919
self,
2020
config: ConfigObject,
21-
dataset: xr.Dataset,
21+
dataset: xr.Dataset | xr.DataTree,
2222
file_path: str,
2323
file_index: int | None,
2424
access_latency: float | None,
2525
):
26-
assert config is not None
27-
assert dataset is not None
28-
assert file_path is not None
26+
assert isinstance(config, ConfigObject)
27+
assert isinstance(dataset, (xr.Dataset | xr.DataTree))
28+
assert isinstance(file_path, str)
2929
assert file_index is None or isinstance(file_index, int)
30+
assert access_latency is None or isinstance(access_latency, float)
31+
if isinstance(dataset, xr.DataTree):
32+
datatree = dataset
33+
dataset = None
34+
if datatree.is_leaf:
35+
dataset = datatree.dataset
36+
datatree = None
37+
else:
38+
datatree = None
3039
self._config = config
40+
self._datatree = datatree
3141
self._dataset = dataset
3242
self._file_path = file_path
3343
self._file_index = file_index
@@ -46,9 +56,17 @@ def settings(self) -> dict[str, Any]:
4656
return self._config.settings or {}
4757

4858
@property
49-
def dataset(self) -> xr.Dataset:
59+
def datatree(self) -> xr.DataTree | None:
60+
return self._datatree
61+
62+
@property
63+
def dataset(self) -> xr.Dataset | None:
5064
return self._dataset
5165

66+
@dataset.setter
67+
def dataset(self, value: xr.Dataset) -> None:
68+
self._dataset = value
69+
5270
@property
5371
def file_path(self) -> str:
5472
return self._file_path
@@ -76,7 +94,7 @@ def report(
7694
fatal=fatal,
7795
suggestions=suggestions,
7896
rule_id=self.rule_id,
79-
node_path=self.node.path if self.node is not None else NODE_ROOT_NAME,
97+
node_path=self.node.path if self.node is not None else DATASET_ROOT_NAME,
8098
severity=self.severity,
8199
)
82100
self.messages.append(m)

0 commit comments

Comments
 (0)