Skip to content

Commit ac461f8

Browse files
committed
Removed Result.new(), added test for linting data trees
1 parent 46e93c9 commit ac461f8

File tree

9 files changed

+153
-103
lines changed

9 files changed

+153
-103
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
### Adjustments and Enhancements
66

7+
- Added support for validating Zarr and HDF-5 groups and their items.
8+
Rules can now validate `xarray.DataTree` objects originating
9+
from `xarray.open_datatree()` by implementing
10+
rule operation method `RuleOp.validate_datatree(ctx, node)`. (#54)
11+
712
- Added a new core rule `access-latency` that can be used to check the
813
time it takes to open a dataset.
914

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ authors = [
1010
]
1111
description = "A linter for xarray datasets."
1212
keywords = [
13-
"xarray"
13+
"xarray", "data-science", "cf", "metadata"
1414
]
1515
license = {text = "MIT"}
1616
requires-python = ">=3.10"

tests/formatters/helpers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ class Rule2(RuleOp):
4545
config_obj = ConfigObject(plugins={"test": plugin})
4646

4747
return [
48-
Result.new(
49-
config_object=config_obj,
48+
Result(
5049
file_path="test.nc",
50+
config_object=config_obj,
5151
messages=[
5252
Message(
5353
message="message-1",
@@ -64,9 +64,9 @@ class Rule2(RuleOp):
6464
Message(message="message-3", fatal=True),
6565
],
6666
),
67-
Result.new(
67+
Result(
68+
file_path="test.nc",
6869
config_object=config_obj,
69-
file_path="test-2.nc",
7070
messages=[
7171
Message(message="message-1", rule_id="test/rule-1", severity=1),
7272
Message(message="message-2", rule_id="test/rule-2", severity=2),

tests/formatters/test_simple.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313
class SimpleTest(TestCase):
1414
errors_and_warnings = [
15-
Result.new(
16-
config_object=ConfigObject(),
15+
Result(
1716
file_path="test1.nc",
17+
config_object=ConfigObject(),
1818
messages=[
1919
Message(message="what", rule_id="rule-1", severity=2),
2020
Message(message="is", fatal=True),
@@ -24,9 +24,9 @@ class SimpleTest(TestCase):
2424
]
2525

2626
warnings_only = [
27-
Result.new(
28-
ConfigObject(),
27+
Result(
2928
file_path="test2.nc",
29+
config_object=ConfigObject(),
3030
messages=[
3131
Message(message="what", rule_id="rule-1", severity=1),
3232
Message(message="happened?", rule_id="rule-2", severity=1),

tests/test_linter.py

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from xrlint.config import Config, ConfigObject
1111
from xrlint.constants import CORE_PLUGIN_NAME, DATASET_ROOT_NAME
1212
from xrlint.linter import Linter, new_linter
13-
from xrlint.node import AttrNode, AttrsNode, DatasetNode, VariableNode
13+
from xrlint.node import AttrNode, AttrsNode, DatasetNode, VariableNode, DataTreeNode
1414
from xrlint.plugin import new_plugin
1515
from xrlint.processor import ProcessorOp
1616
from xrlint.result import Message, Result
@@ -97,6 +97,7 @@ def assert_result_ok(self, result: Result, expected_message: str):
9797

9898

9999
class LinterValidateTest(TestCase):
100+
# noinspection PyUnusedLocal
100101
def setUp(self):
101102
plugin = new_plugin(name="test")
102103

@@ -113,7 +114,7 @@ def validate_attrs(self, ctx: RuleContext, node: AttrsNode):
113114
ctx.report("Empty attributes")
114115

115116
@plugin.define_rule("data-var-dim-must-have-coord")
116-
class DataArrayVer(RuleOp):
117+
class VariableVer(RuleOp):
117118
def validate_variable(self, ctx: RuleContext, node: VariableNode):
118119
if node.in_data_vars():
119120
for dim_name in node.array.dims:
@@ -131,6 +132,12 @@ def validate_dataset(self, ctx: RuleContext, node: DatasetNode):
131132
ctx.report("Dataset does not have data variables")
132133
raise RuleExit # no need to traverse further
133134

135+
@plugin.define_rule("datatree-without-data-vars")
136+
class DataTreeVer(RuleOp):
137+
def validate_datatree(self, ctx: RuleContext, node: DataTreeNode):
138+
if len(node.datatree.data_vars) == 0:
139+
ctx.report("DataTree does not have data variables")
140+
134141
@plugin.define_processor("multi-level-dataset")
135142
class MultiLevelDataset(ProcessorOp):
136143
def preprocess(
@@ -159,6 +166,7 @@ def test_rules_are_ok(self):
159166
"no-empty-attrs",
160167
"data-var-dim-must-have-coord",
161168
"dataset-without-data-vars",
169+
"datatree-without-data-vars",
162170
],
163171
list(self.linter.config.objects[0].plugins["test"].rules.keys()),
164172
)
@@ -171,11 +179,6 @@ def test_linter_respects_rule_severity_error(self):
171179
Result(
172180
config_object=result.config_object,
173181
file_path="<dataset>",
174-
warning_count=0,
175-
error_count=1,
176-
fatal_error_count=0,
177-
fixable_warning_count=0,
178-
fixable_error_count=0,
179182
messages=[
180183
Message(
181184
message="Dataset does not have data variables",
@@ -187,6 +190,9 @@ def test_linter_respects_rule_severity_error(self):
187190
),
188191
result,
189192
)
193+
self.assertEqual(0, result.warning_count)
194+
self.assertEqual(1, result.error_count)
195+
self.assertEqual(0, result.fatal_error_count)
190196

191197
def test_linter_respects_rule_severity_warn(self):
192198
result = self.linter.validate(
@@ -196,11 +202,6 @@ def test_linter_respects_rule_severity_warn(self):
196202
Result(
197203
config_object=result.config_object,
198204
file_path="<dataset>",
199-
warning_count=1,
200-
error_count=0,
201-
fatal_error_count=0,
202-
fixable_warning_count=0,
203-
fixable_error_count=0,
204205
messages=[
205206
Message(
206207
message="Dataset does not have data variables",
@@ -212,6 +213,9 @@ def test_linter_respects_rule_severity_warn(self):
212213
),
213214
result,
214215
)
216+
self.assertEqual(1, result.warning_count)
217+
self.assertEqual(0, result.error_count)
218+
self.assertEqual(0, result.fatal_error_count)
215219

216220
def test_linter_respects_rule_severity_off(self):
217221
result = self.linter.validate(
@@ -221,15 +225,13 @@ def test_linter_respects_rule_severity_off(self):
221225
Result(
222226
config_object=result.config_object,
223227
file_path="<dataset>",
224-
warning_count=0,
225-
error_count=0,
226-
fatal_error_count=0,
227-
fixable_warning_count=0,
228-
fixable_error_count=0,
229228
messages=[],
230229
),
231230
result,
232231
)
232+
self.assertEqual(0, result.warning_count)
233+
self.assertEqual(0, result.error_count)
234+
self.assertEqual(0, result.fatal_error_count)
233235

234236
def test_linter_recognized_unknown_rule(self):
235237
result = self.linter.validate(xr.Dataset(), rules={"test/dataset-is-fast": 2})
@@ -246,6 +248,66 @@ def test_linter_recognized_unknown_rule(self):
246248
result.messages,
247249
)
248250

251+
def test_linter_recognized_datatree_rule(self):
252+
result = self.linter.validate(
253+
xr.DataTree(
254+
children={
255+
"measurement": xr.DataTree(
256+
children={
257+
"r10m": xr.DataTree(),
258+
"r20m": xr.DataTree(),
259+
"r60m": xr.DataTree(),
260+
}
261+
)
262+
}
263+
),
264+
rules={"test/datatree-without-data-vars": 2},
265+
)
266+
self.assertEqual(
267+
[
268+
Message(
269+
message="DataTree does not have data variables",
270+
node_path="dt",
271+
rule_id="test/datatree-without-data-vars",
272+
severity=2,
273+
fatal=None,
274+
fix=None,
275+
suggestions=None,
276+
),
277+
Message(
278+
message="DataTree does not have data variables",
279+
node_path="dt/measurement",
280+
rule_id="test/datatree-without-data-vars",
281+
severity=2,
282+
fatal=None,
283+
fix=None,
284+
suggestions=None,
285+
),
286+
Message(
287+
message="DataTree does not have data variables",
288+
node_path="dt/measurement/r10m",
289+
rule_id="test/datatree-without-data-vars",
290+
severity=2,
291+
),
292+
Message(
293+
message="DataTree does not have data variables",
294+
node_path="dt/measurement/r20m",
295+
rule_id="test/datatree-without-data-vars",
296+
severity=2,
297+
),
298+
Message(
299+
message="DataTree does not have data variables",
300+
node_path="dt/measurement/r60m",
301+
rule_id="test/datatree-without-data-vars",
302+
severity=2,
303+
),
304+
],
305+
result.messages,
306+
)
307+
self.assertEqual(0, result.warning_count)
308+
self.assertEqual(5, result.error_count)
309+
self.assertEqual(0, result.fatal_error_count)
310+
249311
def test_linter_real_life_scenario(self):
250312
dataset = xr.Dataset(
251313
attrs={
@@ -286,11 +348,6 @@ def test_linter_real_life_scenario(self):
286348
Result(
287349
config_object=result.config_object,
288350
file_path="chl-tsm.zarr",
289-
warning_count=1,
290-
error_count=3,
291-
fatal_error_count=0,
292-
fixable_warning_count=0,
293-
fixable_error_count=0,
294351
messages=[
295352
Message(
296353
message="Attribute name with space: 'created at'",
@@ -328,6 +385,9 @@ def test_linter_real_life_scenario(self):
328385
),
329386
result,
330387
)
388+
self.assertEqual(1, result.warning_count)
389+
self.assertEqual(3, result.error_count)
390+
self.assertEqual(0, result.fatal_error_count)
331391

332392
def test_processor_ok(self):
333393
result = self.linter.validate(

tests/test_result.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,24 @@ class MyRule2(RuleOp):
3232
config_obj = ConfigObject(plugins={"test": plugin})
3333
rules_meta = get_rules_meta_for_results(
3434
results=[
35-
Result.new(
36-
config_object=config_obj,
35+
Result(
3736
file_path="test.zarr",
37+
config_object=config_obj,
3838
messages=[Message(message="m 1", rule_id="test/my-rule-1")],
3939
),
40-
Result.new(
41-
config_object=config_obj,
40+
Result(
4241
file_path="test.zarr",
42+
config_object=config_obj,
4343
messages=[Message(message="m 2", rule_id="test/my-rule-2")],
4444
),
45-
Result.new(
46-
config_object=config_obj,
45+
Result(
4746
file_path="test.zarr",
47+
config_object=config_obj,
4848
messages=[Message(message="m 3", rule_id="test/my-rule-1")],
4949
),
50-
Result.new(
51-
config_object=config_obj,
50+
Result(
5251
file_path="test.zarr",
52+
config_object=config_obj,
5353
messages=[Message(message="m 4", rule_id="test/my-rule-2")],
5454
),
5555
]
@@ -66,19 +66,19 @@ class MyRule2(RuleOp):
6666
)
6767

6868
def test_repr_html(self):
69-
result = Result.new(
70-
config_object=ConfigObject(),
69+
result = Result(
7170
file_path="test.zarr",
71+
config_object=ConfigObject(),
7272
messages=[],
7373
)
7474
html = result._repr_html_()
7575
self.assertIsInstance(html, str)
7676
self.assertIn("ok", html)
7777
self.assertIn("<div", html)
7878

79-
result = Result.new(
80-
config_object=ConfigObject(),
79+
result = Result(
8180
file_path="test.zarr",
81+
config_object=ConfigObject(),
8282
messages=[Message(message="m 1", rule_id="test/my-rule-1")],
8383
)
8484
html = result._repr_html_()
@@ -108,23 +108,26 @@ def test_collect(self):
108108
self.assertEqual(0, stats.result_count)
109109

110110
results = [
111-
Result.new(
111+
Result(
112+
file_path="test.zarr",
112113
messages=[
113114
Message("R1 M1", severity=1),
114115
Message("R1 M2", severity=2),
115-
]
116+
],
116117
),
117-
Result.new(
118+
Result(
119+
file_path="test.zarr",
118120
messages=[
119121
Message("R2 M1", severity=2),
120-
]
122+
],
121123
),
122-
Result.new(
124+
Result(
125+
file_path="test.zarr",
123126
messages=[
124127
Message("R3 M1", severity=1),
125128
Message("R3 M2", severity=2),
126129
Message("R3 M3", severity=2),
127-
]
130+
],
128131
),
129132
]
130133

xrlint/_linter/validate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ def validate_dataset(config_obj: ConfigObject, dataset: Any, file_path: str):
1919
assert isinstance(config_obj, ConfigObject)
2020
assert dataset is not None
2121
assert isinstance(file_path, str)
22-
if isinstance(dataset, xr.Dataset):
22+
if isinstance(dataset, (xr.Dataset, xr.DataTree)):
2323
messages = _validate_dataset(config_obj, dataset, file_path, None, None)
2424
else:
2525
messages = _open_and_validate_dataset(config_obj, dataset, file_path)
26-
return Result.new(config_object=config_obj, messages=messages, file_path=file_path)
26+
return Result(file_path=file_path, config_object=config_obj, messages=messages)
2727

2828

2929
def _validate_dataset(

0 commit comments

Comments
 (0)