Skip to content

Commit b1596ff

Browse files
authored
Merge pull request #16 from bcdev/forman-virtual_plugins
Supporting virtual plugins and ease deserialization
2 parents 64e58e1 + 92d816a commit b1596ff

34 files changed

+1872
-361
lines changed

CHANGES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
## Version 0.2.0 (in development)
44

55
- Make all docstrings comply to google-style
6+
- Rule description is now your `RuleOp`'s docstring
7+
if `description` is not explicitly provided.
8+
- Supporting _virtual plugins_: plugins provided by Python
9+
dictionaries with rules defined by the `RuleOp` classes.
10+
- Added more configuration examples in the `examples` folder.
11+
- Introduced utilities `ValueConstructible` and
12+
derived `MappingConstructible` which greatly simplify
13+
flexible instantiation of configuration objects and their
14+
children from Python and JSON/YAML values.
615

716
## Version 0.1.0 (09.01.2025)
817

docs/api.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,23 @@ This chapter provides a plain reference for the XRLint Python API.
1616
- The `config` module provides classes that represent
1717
configuration information and provide related functionality:
1818
[Config][xrlint.config.Config] and [ConfigList][xrlint.config.ConfigList].
19-
- The `rule` module provides rule related classes:
19+
- The `rule` module provides rule related classes and functions:
2020
[Rule][xrlint.rule.Rule] comprising rule metadata
2121
[RuleMeta][xrlint.rule.RuleMeta] and the rule operation
2222
[RuleOp][xrlint.rule.RuleOp], as well as related to the latter
2323
[RuleContext][xrlint.rule.RuleContext] and [RuleExit][xrlint.rule.RuleExit].
24+
Decorator [define_rule][xrlint.rule.define_rule] allows defining rules.
2425
- The `node` module defines the nodes passed to [xrlint.rule.RuleOp]:
2526
base classes [None][xrlint.node.Node], [XarrayNode][xrlint.node.XarrayNode]
2627
and the specific [DatasetNode][xrlint.node.DatasetNode],
2728
[DataArray][xrlint.node.DataArrayNode], [AttrsNode][xrlint.node.AttrsNode],
2829
and [AttrNode][xrlint.node.AttrNode] nodes.
29-
- The `processor` module provides processor related classes:
30+
- The `processor` module provides processor related classes and functions:
3031
[Processor][xrlint.processor.Processor] comprising processor metadata
3132
[ProcessorMeta][xrlint.processor.ProcessorMeta]
3233
and the processor operation [ProcessorOp][xrlint.processor.ProcessorOp].
34+
Decorator [define_processor][xrlint.processor.define_processor] allows defining
35+
processors.
3336
- The `result` module provides data classes that are used to
3437
represent validation results:
3538
[Result][xrlint.result.Result] composed of [Messages][xrlint.result.Message],
@@ -51,6 +54,8 @@ Note: the `xrlint.all` convenience module exports all of the above from a
5154

5255
::: xrlint.config.ConfigList
5356

57+
::: xrlint.rule.define_rule
58+
5459
::: xrlint.rule.Rule
5560

5661
::: xrlint.rule.RuleMeta
@@ -77,6 +82,8 @@ Note: the `xrlint.all` convenience module exports all of the above from a
7782

7883
::: xrlint.plugin.PluginMeta
7984

85+
::: xrlint.processor.define_processor
86+
8087
::: xrlint.processor.Processor
8188

8289
::: xrlint.processor.ProcessorMeta

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: xrlint
1+
name: xrlint-310
22
channels:
33
- conda-forge
44
dependencies:

examples/__init__.py

Whitespace-only changes.
Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
# XRLint configuration file example
1+
"""
2+
This configuration example shows how to define and use a plugin
3+
using the `Plugin` class and its `define_rule()` decorator method.
4+
"""
25

36
from xrlint.config import Config
47
from xrlint.node import DatasetNode
@@ -11,21 +14,24 @@
1114
plugin = Plugin(
1215
meta=PluginMeta(name="hello-plugin", version="1.0.0"),
1316
configs={
17+
# "configs" entries must be `Config` objects!
1418
"recommended": Config.from_value(
1519
{
1620
"rules": {
17-
"hello/good-title": "error",
21+
"hello/good-title": "warn",
22+
# Configure more rules here...
1823
},
1924
}
20-
)
25+
),
26+
# Add more configurations here...
2127
},
2228
)
2329

2430

25-
@plugin.define_rule(
26-
"good-title", description=f"Dataset title should be 'Hello World!'."
27-
)
31+
@plugin.define_rule("good-title")
2832
class GoodTitle(RuleOp):
33+
"""Dataset title should be 'Hello World!'."""
34+
2935
def dataset(self, ctx: RuleContext, node: DatasetNode):
3036
good_title = "Hello World!"
3137
if node.dataset.attrs.get("title") != good_title:
@@ -35,16 +41,19 @@ def dataset(self, ctx: RuleContext, node: DatasetNode):
3541
)
3642

3743

44+
# Define more rules here...
45+
46+
3847
def export_configs():
3948
return [
40-
{
41-
"files": ["**/*.zarr", "**/*.nc"],
42-
},
49+
# Use "hello" plugin
4350
{
4451
"plugins": {
4552
"hello": plugin,
4653
},
4754
},
55+
# Use recommended settings from xrlint
4856
"recommended",
57+
# Use recommended settings from "hello" plugin
4958
"hello/recommended",
5059
]

examples/rule_testing.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
This example demonstrates how to develop new rules.
3+
"""
4+
5+
import xarray as xr
6+
7+
from xrlint.node import DatasetNode
8+
from xrlint.rule import RuleContext
9+
from xrlint.rule import RuleOp
10+
from xrlint.rule import define_rule
11+
from xrlint.testing import RuleTest
12+
from xrlint.testing import RuleTester
13+
14+
15+
@define_rule("good-title")
16+
class GoodTitle(RuleOp):
17+
"""Dataset title should be 'Hello World!'."""
18+
19+
def dataset(self, ctx: RuleContext, node: DatasetNode):
20+
good_title = "Hello World!"
21+
if node.dataset.attrs.get("title") != good_title:
22+
ctx.report(
23+
"Attribute 'title' wrong.",
24+
suggestions=[f"Rename it to {good_title!r}."],
25+
)
26+
27+
28+
# -----------------
29+
# In another module
30+
# -----------------
31+
32+
tester = RuleTester()
33+
34+
valid_dataset = xr.Dataset(attrs=dict(title="Hello World!"))
35+
invalid_dataset = xr.Dataset(attrs=dict(title="Hello Hamburg!"))
36+
37+
# Run test directly
38+
tester.run(
39+
"good-title",
40+
GoodTitle,
41+
valid=[RuleTest(dataset=valid_dataset)],
42+
invalid=[RuleTest(dataset=invalid_dataset)],
43+
)
44+
45+
# or define a test class derived from unitest.TestCase
46+
GoodTitleTest = tester.define_test(
47+
"good-title",
48+
GoodTitle,
49+
valid=[RuleTest(dataset=valid_dataset)],
50+
invalid=[RuleTest(dataset=invalid_dataset)],
51+
)

examples/virtual_plugin_config.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
This configuration example demonstrates how to
3+
define and use "virtual" plugins. Such plugins
4+
can be defined inside a configuration item.
5+
"""
6+
7+
from xrlint.node import DatasetNode
8+
from xrlint.rule import RuleContext
9+
from xrlint.rule import RuleOp
10+
from xrlint.rule import define_rule
11+
12+
13+
@define_rule("good-title", description="Dataset title should be 'Hello World!'.")
14+
class GoodTitle(RuleOp):
15+
def dataset(self, ctx: RuleContext, node: DatasetNode):
16+
good_title = "Hello World!"
17+
if node.dataset.attrs.get("title") != good_title:
18+
ctx.report(
19+
"Attribute 'title' wrong.",
20+
suggestions=[f"Rename it to {good_title!r}."],
21+
)
22+
23+
24+
# Define more rules here...
25+
26+
27+
def export_configs():
28+
return [
29+
# Define and use "hello" plugin
30+
{
31+
"plugins": {
32+
"hello": {
33+
"meta": {
34+
"name": "hello",
35+
"version": "1.0.0",
36+
},
37+
"rules": {
38+
"good-title": GoodTitle,
39+
# Add more rules here...
40+
},
41+
"configs": {
42+
"recommended": {
43+
"rules": {
44+
"hello/good-title": "warn",
45+
# Configure more rules here...
46+
},
47+
},
48+
# Add more configurations here...
49+
},
50+
},
51+
}
52+
},
53+
# Use recommended settings from xrlint
54+
"recommended",
55+
# Use recommended settings from "hello" plugin
56+
"hello/recommended",
57+
]

tests/cli/configs/recommended.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
- recommended
44
- xcube/recommended
55
- rules:
6-
"dataset-title-attr": error
7-
"xcube/single-grid-mapping": off
6+
"dataset-title-attr": "error"
7+
"xcube/single-grid-mapping": "off"

tests/cli/test_config.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_read_config_invalid_arg(self):
9393
with pytest.raises(
9494
TypeError,
9595
match="configuration file must be of type str|Path|PathLike,"
96-
" but was None",
96+
" but got None",
9797
):
9898
# noinspection PyTypeChecker
9999
read_config_list(None)
@@ -125,7 +125,7 @@ def test_read_config_yaml_with_type_error(self):
125125
match=(
126126
"'config.yaml: configuration list must be of"
127127
" type ConfigList|list\\[Config|dict|str\\],"
128-
" but was dict'"
128+
" but got dict'"
129129
),
130130
):
131131
read_config_list(config_path)
@@ -142,7 +142,10 @@ def test_read_config_py_no_export(self):
142142
with text_file(self.new_config_py(), py_code) as config_path:
143143
with pytest.raises(
144144
ConfigError,
145-
match=".py: missing export_configs()",
145+
match=(
146+
"config_1002.py: attribute 'export_configs'"
147+
" not found in module 'config_1002'"
148+
),
146149
):
147150
read_config_list(config_path)
148151

@@ -173,7 +176,7 @@ def test_read_config_py_with_invalid_config_list(self):
173176
".py: return value of export_configs\\(\\):"
174177
" configuration list must be of type"
175178
" ConfigList|list\\[Config|dict|str\\],"
176-
" but was int"
179+
" but got int"
177180
),
178181
):
179182
read_config_list(config_path)

tests/cli/test_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def test_print_config_option(self):
192192
"{\n"
193193
' "name": "<computed>",\n'
194194
' "plugins": {\n'
195-
' "__core__": "xrlint.plugins.core"\n'
195+
' "__core__": "xrlint.plugins.core:export_plugin"\n'
196196
" },\n"
197197
' "rules": {\n'
198198
' "dataset-title-attr": 2\n'

0 commit comments

Comments
 (0)