diff --git a/CHANGES.md b/CHANGES.md index f4a7dd0..29a50d1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/docs/todo.md b/docs/todo.md index 8a0979d..83cad0e 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -2,7 +2,7 @@ ## 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 @@ -10,6 +10,7 @@ ## Desired +- project logo - use `RuleMeta.docs_url` in formatters to create links - implement xarray backend for xcube 'levels' format so can validate them too @@ -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 diff --git a/tests/_linter/test_rule_ctx_impl.py b/tests/_linter/test_rulectx.py similarity index 100% rename from tests/_linter/test_rule_ctx_impl.py rename to tests/_linter/test_rulectx.py diff --git a/tests/test_all.py b/tests/test_all.py index c0c6031..2ddc267 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -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) diff --git a/tests/test_linter.py b/tests/test_linter.py index aa5b24f..65e7e8a 100644 --- a/tests/test_linter.py +++ b/tests/test_linter.py @@ -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 @@ -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): diff --git a/xrlint/_linter/apply.py b/xrlint/_linter/apply.py index cf71cb5..4fa743c 100644 --- a/xrlint/_linter/apply.py +++ b/xrlint/_linter/apply.py @@ -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 @@ -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): diff --git a/xrlint/all.py b/xrlint/all.py index 9ed7290..2f29f03 100644 --- a/xrlint/all.py +++ b/xrlint/all.py @@ -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 @@ -61,6 +62,7 @@ "Rule", "RuleConfig", "RuleContext", + "RuleExit", "RuleMeta", "RuleOp", "RuleTest", diff --git a/xrlint/plugins/xcube/rules/increasing_time.py b/xrlint/plugins/xcube/rules/increasing_time.py index 5d52fea..f2daea4 100644 --- a/xrlint/plugins/xcube/rules/increasing_time.py +++ b/xrlint/plugins/xcube/rules/increasing_time.py @@ -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 @@ -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): diff --git a/xrlint/rule.py b/xrlint/rule.py index 4e346d4..675ded6 100644 --- a/xrlint/rule.py +++ b/xrlint/rule.py @@ -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.""" @@ -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: @@ -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: @@ -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: @@ -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 """