Skip to content

Commit

Permalink
Merge pull request #11 from bcdev/forman-break_node_traversal
Browse files Browse the repository at this point in the history
Cancel node traversal
  • Loading branch information
forman authored Jan 8, 2025
2 parents 5647713 + 2fb3da9 commit b8f36f9
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 56 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- enhanced "simple" output format by colors and links
- new xcube rule "increasing-time"
- new xcube rule "data-var-colors"
- new `RuleExit` exception to exit rule logic and
stop further node traversal

- Version 0.0.2 (06.01.2025)
- more rules
Expand Down
7 changes: 2 additions & 5 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

## Required

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

## Desired

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

## Nice to have

Expand Down
File renamed without changes.
46 changes: 5 additions & 41 deletions tests/test_all.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,17 @@
from unittest import TestCase

expected_api = [
"AttrNode",
"AttrsNode",
"CliEngine",
"Config",
"ConfigList",
"DataArrayNode",
"DatasetNode",
"EditInfo",
"Formatter",
"FormatterContext",
"FormatterMeta",
"FormatterOp",
"FormatterRegistry",
"Linter",
"Message",
"Node",
"Plugin",
"PluginMeta",
"Processor",
"ProcessorMeta",
"ProcessorOp",
"Result",
"Rule",
"RuleConfig",
"RuleContext",
"RuleMeta",
"RuleOp",
"RuleTest",
"RuleTester",
"Suggestion",
"get_rules_meta_for_results",
"new_linter",
"version",
]


class AllTest(TestCase):
def test_api_is_complete(self):
import xrlint.all as xrl

# noinspection PyProtectedMember
from xrlint.all import __all__

# noinspection PyUnresolvedReferences
keys = sorted(
keys = set(
k
for k, v in xrl.__dict__.items()
if isinstance(k, str) and not k.startswith("_")
)
self.assertEqual(
expected_api,
keys,
)
self.assertEqual(set(__all__), keys)
11 changes: 7 additions & 4 deletions tests/test_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
from xrlint.constants import CORE_PLUGIN_NAME
from xrlint.linter import Linter
from xrlint.linter import new_linter
from xrlint.processor import ProcessorOp
from xrlint.result import Message
from xrlint.plugin import Plugin, PluginMeta
from xrlint.result import Result
from xrlint.plugin import Plugin
from xrlint.plugin import PluginMeta
from xrlint.node import (
AttrsNode,
AttrNode,
DataArrayNode,
DatasetNode,
)
from xrlint.processor import ProcessorOp
from xrlint.result import Message
from xrlint.result import Result
from xrlint.rule import RuleContext
from xrlint.rule import RuleExit
from xrlint.rule import RuleOp


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

@plugin.define_processor("multi-level-dataset")
class MultiLevelDataset(ProcessorOp):
Expand Down
15 changes: 10 additions & 5 deletions xrlint/_linter/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from xrlint.node import DataArrayNode
from xrlint.node import DatasetNode
from xrlint.rule import RuleConfig
from xrlint.rule import RuleExit
from xrlint.rule import RuleOp
from .rulectx import RuleContextImpl

Expand All @@ -29,11 +30,15 @@ def apply_rule(
# TODO: validate rule_config.args/kwargs against rule.meta.schema
# noinspection PyArgumentList
rule_op: RuleOp = rule.op_class(*rule_config.args, **rule_config.kwargs)
_visit_dataset_node(
rule_op,
context,
DatasetNode(parent=None, path="dataset", dataset=context.dataset),
)
try:
_visit_dataset_node(
rule_op,
context,
DatasetNode(parent=None, path="dataset", dataset=context.dataset),
)
except RuleExit:
# This is ok, the rule requested it.
pass


def _visit_dataset_node(rule_op: RuleOp, context: RuleContextImpl, node: DatasetNode):
Expand Down
2 changes: 2 additions & 0 deletions xrlint/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from xrlint.rule import Rule
from xrlint.rule import RuleConfig
from xrlint.rule import RuleContext
from xrlint.rule import RuleExit
from xrlint.rule import RuleMeta
from xrlint.rule import RuleOp
from xrlint.testing import RuleTest
Expand Down Expand Up @@ -61,6 +62,7 @@
"Rule",
"RuleConfig",
"RuleContext",
"RuleExit",
"RuleMeta",
"RuleOp",
"RuleTest",
Expand Down
3 changes: 2 additions & 1 deletion xrlint/plugins/xcube/rules/increasing_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from xrlint.node import DataArrayNode
from xrlint.plugins.xcube.rules import plugin
from xrlint.rule import RuleContext
from xrlint.rule import RuleExit
from xrlint.rule import RuleOp
from xrlint.util.formatting import format_count
from xrlint.util.formatting import format_seq
Expand All @@ -22,7 +23,7 @@ def data_array(self, ctx: RuleContext, node: DataArrayNode):
if not np.count_nonzero(diff_array > 0) == diff_array.size:
check_indexes(ctx, diff_array == 0, "Duplicate")
check_indexes(ctx, diff_array < 0, "Backsliding")
return True # No need to apply rule any further
raise RuleExit # No need to apply rule any further


def check_indexes(ctx, cond: np.ndarray, issue_name: str):
Expand Down
23 changes: 23 additions & 0 deletions xrlint/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ def report(
"""


class RuleExit(Exception):
"""The `RuleExit` is an exception that can be raised to
immediately cancel dataset node validation with the current rule.
Raise it from any of your `RuleOp` method implementations if further
node traversal doesn't make sense. Typical usage:
```python
if something_is_not_ok:
ctx.report("Something is not ok.")
raise RuleExit
```
"""


class RuleOp(ABC):
"""Define the specific rule verification operation."""

Expand All @@ -62,6 +77,8 @@ def dataset(self, context: RuleContext, node: DatasetNode) -> None:
Args:
context: The current rule context.
node: The dataset node.
Raises:
RuleExit: to exit rule logic and further node traversal
"""

def data_array(self, context: RuleContext, node: DataArrayNode) -> None:
Expand All @@ -70,6 +87,8 @@ def data_array(self, context: RuleContext, node: DataArrayNode) -> None:
Args:
context: The current rule context.
node: The data array (variable) node.
Raises:
RuleExit: to exit rule logic and further node traversal
"""

def attrs(self, context: RuleContext, node: AttrsNode) -> None:
Expand All @@ -78,6 +97,8 @@ def attrs(self, context: RuleContext, node: AttrsNode) -> None:
Args:
context: The current rule context.
node: The attributes node.
Raises:
RuleExit: to exit rule logic and further node traversal
"""

def attr(self, context: RuleContext, node: AttrNode) -> None:
Expand All @@ -86,6 +107,8 @@ def attr(self, context: RuleContext, node: AttrNode) -> None:
Args:
context: The current rule context.
node: The attribute node.
Raises:
RuleExit: to exit rule logic and further node traversal
"""


Expand Down

0 comments on commit b8f36f9

Please sign in to comment.