Skip to content

Commit b8f36f9

Browse files
authored
Merge pull request #11 from bcdev/forman-break_node_traversal
Cancel node traversal
2 parents 5647713 + 2fb3da9 commit b8f36f9

File tree

9 files changed

+53
-56
lines changed

9 files changed

+53
-56
lines changed

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- enhanced "simple" output format by colors and links
66
- new xcube rule "increasing-time"
77
- new xcube rule "data-var-colors"
8+
- new `RuleExit` exception to exit rule logic and
9+
stop further node traversal
810

911
- Version 0.0.2 (06.01.2025)
1012
- more rules

docs/todo.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
## Required
44

5-
- populate `core` plugin by more rules
5+
- populate `core` plugin by more rules, see CF site and `cf-check` tool
66
- populate `xcube` plugin by more rules
77
- add `docs`
88
- use mkdocstrings ref syntax in docstrings
99
- provide configuration examples (use as tests?)
1010

1111
## Desired
1212

13+
- project logo
1314
- use `RuleMeta.docs_url` in formatters to create links
1415
- implement xarray backend for xcube 'levels' format
1516
so can validate them too
@@ -22,10 +23,6 @@
2223
- support rule op args/kwargs schema validation
2324
- support CLI option `--print-config FILE`, see ESLint
2425
- Support `RuleTest.expected`, it is currently unused
25-
- Allow `RuleOp` methods to return `True` to finish
26-
node validation with the current rule on current dataset.
27-
In this case the linter interrupts traversing the
28-
dataset node tree.
2926

3027
## Nice to have
3128

tests/test_all.py

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,17 @@
11
from unittest import TestCase
22

3-
expected_api = [
4-
"AttrNode",
5-
"AttrsNode",
6-
"CliEngine",
7-
"Config",
8-
"ConfigList",
9-
"DataArrayNode",
10-
"DatasetNode",
11-
"EditInfo",
12-
"Formatter",
13-
"FormatterContext",
14-
"FormatterMeta",
15-
"FormatterOp",
16-
"FormatterRegistry",
17-
"Linter",
18-
"Message",
19-
"Node",
20-
"Plugin",
21-
"PluginMeta",
22-
"Processor",
23-
"ProcessorMeta",
24-
"ProcessorOp",
25-
"Result",
26-
"Rule",
27-
"RuleConfig",
28-
"RuleContext",
29-
"RuleMeta",
30-
"RuleOp",
31-
"RuleTest",
32-
"RuleTester",
33-
"Suggestion",
34-
"get_rules_meta_for_results",
35-
"new_linter",
36-
"version",
37-
]
38-
393

404
class AllTest(TestCase):
415
def test_api_is_complete(self):
426
import xrlint.all as xrl
437

8+
# noinspection PyProtectedMember
9+
from xrlint.all import __all__
10+
4411
# noinspection PyUnresolvedReferences
45-
keys = sorted(
12+
keys = set(
4613
k
4714
for k, v in xrl.__dict__.items()
4815
if isinstance(k, str) and not k.startswith("_")
4916
)
50-
self.assertEqual(
51-
expected_api,
52-
keys,
53-
)
17+
self.assertEqual(set(__all__), keys)

tests/test_linter.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
from xrlint.constants import CORE_PLUGIN_NAME
88
from xrlint.linter import Linter
99
from xrlint.linter import new_linter
10-
from xrlint.processor import ProcessorOp
11-
from xrlint.result import Message
12-
from xrlint.plugin import Plugin, PluginMeta
13-
from xrlint.result import Result
10+
from xrlint.plugin import Plugin
11+
from xrlint.plugin import PluginMeta
1412
from xrlint.node import (
1513
AttrsNode,
1614
AttrNode,
1715
DataArrayNode,
1816
DatasetNode,
1917
)
18+
from xrlint.processor import ProcessorOp
19+
from xrlint.result import Message
20+
from xrlint.result import Result
2021
from xrlint.rule import RuleContext
22+
from xrlint.rule import RuleExit
2123
from xrlint.rule import RuleOp
2224

2325

@@ -84,6 +86,7 @@ class DatasetVer(RuleOp):
8486
def dataset(self, ctx: RuleContext, node: DatasetNode):
8587
if len(node.dataset.data_vars) == 0:
8688
ctx.report("Dataset does not have data variables")
89+
raise RuleExit # no need to traverse further
8790

8891
@plugin.define_processor("multi-level-dataset")
8992
class MultiLevelDataset(ProcessorOp):

xrlint/_linter/apply.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from xrlint.node import DataArrayNode
44
from xrlint.node import DatasetNode
55
from xrlint.rule import RuleConfig
6+
from xrlint.rule import RuleExit
67
from xrlint.rule import RuleOp
78
from .rulectx import RuleContextImpl
89

@@ -29,11 +30,15 @@ def apply_rule(
2930
# TODO: validate rule_config.args/kwargs against rule.meta.schema
3031
# noinspection PyArgumentList
3132
rule_op: RuleOp = rule.op_class(*rule_config.args, **rule_config.kwargs)
32-
_visit_dataset_node(
33-
rule_op,
34-
context,
35-
DatasetNode(parent=None, path="dataset", dataset=context.dataset),
36-
)
33+
try:
34+
_visit_dataset_node(
35+
rule_op,
36+
context,
37+
DatasetNode(parent=None, path="dataset", dataset=context.dataset),
38+
)
39+
except RuleExit:
40+
# This is ok, the rule requested it.
41+
pass
3742

3843

3944
def _visit_dataset_node(rule_op: RuleOp, context: RuleContextImpl, node: DatasetNode):

xrlint/all.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from xrlint.rule import Rule
2727
from xrlint.rule import RuleConfig
2828
from xrlint.rule import RuleContext
29+
from xrlint.rule import RuleExit
2930
from xrlint.rule import RuleMeta
3031
from xrlint.rule import RuleOp
3132
from xrlint.testing import RuleTest
@@ -61,6 +62,7 @@
6162
"Rule",
6263
"RuleConfig",
6364
"RuleContext",
65+
"RuleExit",
6466
"RuleMeta",
6567
"RuleOp",
6668
"RuleTest",

xrlint/plugins/xcube/rules/increasing_time.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from xrlint.node import DataArrayNode
44
from xrlint.plugins.xcube.rules import plugin
55
from xrlint.rule import RuleContext
6+
from xrlint.rule import RuleExit
67
from xrlint.rule import RuleOp
78
from xrlint.util.formatting import format_count
89
from xrlint.util.formatting import format_seq
@@ -22,7 +23,7 @@ def data_array(self, ctx: RuleContext, node: DataArrayNode):
2223
if not np.count_nonzero(diff_array > 0) == diff_array.size:
2324
check_indexes(ctx, diff_array == 0, "Duplicate")
2425
check_indexes(ctx, diff_array < 0, "Backsliding")
25-
return True # No need to apply rule any further
26+
raise RuleExit # No need to apply rule any further
2627

2728

2829
def check_indexes(ctx, cond: np.ndarray, issue_name: str):

xrlint/rule.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ def report(
5353
"""
5454

5555

56+
class RuleExit(Exception):
57+
"""The `RuleExit` is an exception that can be raised to
58+
immediately cancel dataset node validation with the current rule.
59+
60+
Raise it from any of your `RuleOp` method implementations if further
61+
node traversal doesn't make sense. Typical usage:
62+
63+
```python
64+
if something_is_not_ok:
65+
ctx.report("Something is not ok.")
66+
raise RuleExit
67+
```
68+
"""
69+
70+
5671
class RuleOp(ABC):
5772
"""Define the specific rule verification operation."""
5873

@@ -62,6 +77,8 @@ def dataset(self, context: RuleContext, node: DatasetNode) -> None:
6277
Args:
6378
context: The current rule context.
6479
node: The dataset node.
80+
Raises:
81+
RuleExit: to exit rule logic and further node traversal
6582
"""
6683

6784
def data_array(self, context: RuleContext, node: DataArrayNode) -> None:
@@ -70,6 +87,8 @@ def data_array(self, context: RuleContext, node: DataArrayNode) -> None:
7087
Args:
7188
context: The current rule context.
7289
node: The data array (variable) node.
90+
Raises:
91+
RuleExit: to exit rule logic and further node traversal
7392
"""
7493

7594
def attrs(self, context: RuleContext, node: AttrsNode) -> None:
@@ -78,6 +97,8 @@ def attrs(self, context: RuleContext, node: AttrsNode) -> None:
7897
Args:
7998
context: The current rule context.
8099
node: The attributes node.
100+
Raises:
101+
RuleExit: to exit rule logic and further node traversal
81102
"""
82103

83104
def attr(self, context: RuleContext, node: AttrNode) -> None:
@@ -86,6 +107,8 @@ def attr(self, context: RuleContext, node: AttrNode) -> None:
86107
Args:
87108
context: The current rule context.
88109
node: The attribute node.
110+
Raises:
111+
RuleExit: to exit rule logic and further node traversal
89112
"""
90113

91114

0 commit comments

Comments
 (0)