Skip to content
14 changes: 14 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# XRLint Change History

## Version 0.5.0 (in development)

- Introduced type aliases `ConfigLike` and `ConfigObjectLike`.
- Renamed multiple components for improved clarity and consistency:
- Renamed `Config` into `ConfigObject`
- Renamed `ConfigList.configs` into `config_objects`
- Renamed `ConfigList` into `Config`
- Renamed `ConfigList.compute_config()` into `compute_config_object()`
- Renamed `Result.config` into `config_object`
- Renamed `XRLint.load_config_list()` into `init_config()`
- Renamed `XRLint.verify_datasets()` into `verify_files()`
- Added class method `from_config()` to `ConfigList`.
- Removed function `xrlint.config.merge_configs` as it was no longer used.

## Version 0.4.1 (from 2025-01-31)

### Changes
Expand Down
8 changes: 6 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This chapter provides a plain reference for the XRLint Python API.
plugin metadata represented by [PluginMeta][xrlint.plugin.PluginMeta].
- The `config` module provides classes that represent
configuration information and provide related functionality:
[Config][xrlint.config.Config] and [ConfigList][xrlint.config.ConfigList].
[Config][xrlint.config.Config] and [ConfigObject][xrlint.config.ConfigObject].
- The `rule` module provides rule related classes and functions:
[Rule][xrlint.rule.Rule] comprising rule metadata,
[RuleMeta][xrlint.rule.RuleMeta], the rule validation operations in
Expand Down Expand Up @@ -60,7 +60,11 @@ Note:

::: xrlint.config.Config

::: xrlint.config.ConfigList
::: xrlint.config.ConfigObject

::: xrlint.config.ConfigLike

::: xrlint.config.ConfigObjectLike

::: xrlint.rule.define_rule

Expand Down
8 changes: 8 additions & 0 deletions examples/check_s3_bucket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import xrlint.all as xrl

URL = "s3://xcube-test/"

xrlint = xrl.XRLint(no_config_lookup=True)
xrlint.init_config("recommended")
results = xrlint.verify_files([URL])
print(xrlint.format_results(results))
2 changes: 1 addition & 1 deletion mkruleref.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def write_plugin_rules(stream, plugin: Plugin):
stream.write("\n\n")


def get_plugin_rule_configs(plugin):
def get_plugin_rule_configs(plugin: Plugin) -> dict[str, dict[str, RuleConfig]]:
configs = plugin.configs
config_rules: dict[str, dict[str, RuleConfig]] = {}
for config_name, config_list in configs.items():
Expand Down
10 changes: 5 additions & 5 deletions tests/_linter/test_rulectx.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@

# noinspection PyProtectedMember
from xrlint._linter.rulectx import RuleContextImpl
from xrlint.config import Config
from xrlint.config import ConfigObject
from xrlint.constants import NODE_ROOT_NAME
from xrlint.result import Message, Suggestion


class RuleContextImplTest(TestCase):
def test_defaults(self):
config = Config()
config_obj = ConfigObject()
dataset = xr.Dataset()
context = RuleContextImpl(config, dataset, "./ds.zarr", None)
self.assertIs(config, context.config)
context = RuleContextImpl(config_obj, dataset, "./ds.zarr", None)
self.assertIs(config_obj, context.config)
self.assertIs(dataset, context.dataset)
self.assertEqual({}, context.settings)
self.assertEqual("./ds.zarr", context.file_path)
self.assertEqual(None, context.file_index)

def test_report(self):
context = RuleContextImpl(Config(), xr.Dataset(), "./ds.zarr", None)
context = RuleContextImpl(ConfigObject(), xr.Dataset(), "./ds.zarr", None)
with context.use_state(rule_id="no-xxx"):
context.report(
"What the heck do you mean?",
Expand Down
68 changes: 34 additions & 34 deletions tests/cli/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import pytest

from xrlint.cli.config import ConfigError, read_config_list
from xrlint.config import Config, ConfigList
from xrlint.cli.config import ConfigError, read_config
from xrlint.config import Config, ConfigObject
from xrlint.rule import RuleConfig

from .helpers import text_file
Expand Down Expand Up @@ -58,24 +58,24 @@ def new_config_py(self):

def test_read_config_yaml(self):
with text_file("config.yaml", yaml_text) as config_path:
config = read_config_list(config_path)
config = read_config(config_path)
self.assert_config_ok(config, "yaml-test")

def test_read_config_json(self):
with text_file("config.json", json_text) as config_path:
config = read_config_list(config_path)
config = read_config(config_path)
self.assert_config_ok(config, "json-test")

def test_read_config_py(self):
with text_file(self.new_config_py(), py_text) as config_path:
config = read_config_list(config_path)
config = read_config(config_path)
self.assert_config_ok(config, "py-test")

def assert_config_ok(self, config: Any, name: str):
self.assertEqual(
ConfigList(
Config(
[
Config(
ConfigObject(
name=name,
rules={
"rule-1": RuleConfig(2),
Expand All @@ -94,7 +94,7 @@ def test_read_config_invalid_arg(self):
match="configuration file must be of type str|Path|PathLike, but got None",
):
# noinspection PyTypeChecker
read_config_list(None)
read_config(None)

def test_read_config_json_with_format_error(self):
with text_file("config.json", "{") as config_path:
Expand All @@ -106,34 +106,34 @@ def test_read_config_json_with_format_error(self):
" line 1 column 2 \\(char 1\\)"
),
):
read_config_list(config_path)
read_config(config_path)

def test_read_config_yaml_with_format_error(self):
with text_file("config.yaml", "}") as config_path:
with pytest.raises(
ConfigError,
match="config.yaml: while parsing a block node",
):
read_config_list(config_path)
read_config(config_path)

def test_read_config_yaml_with_type_error(self):
with text_file("config.yaml", "97") as config_path:
with pytest.raises(
ConfigError,
match=(
r"config\.yaml\: config_list must be of"
r" type ConfigList \| list\[Config \| dict \| str\],"
r"config\.yaml\: config must be of type"
r" Config \| ConfigObjectLike \| str \| Sequence\[ConfigObjectLike \| str\],"
r" but got int"
),
):
read_config_list(config_path)
read_config(config_path)

def test_read_config_with_unknown_format(self):
with pytest.raises(
ConfigError,
match="config.toml: unsupported configuration file format",
):
read_config_list("config.toml")
read_config("config.toml")

def test_read_config_py_no_export(self):
py_code = "x = 42\n"
Expand All @@ -145,7 +145,7 @@ def test_read_config_py_no_export(self):
" not found in module 'config_1002'"
),
):
read_config_list(config_path)
read_config(config_path)

def test_read_config_py_with_value_error(self):
py_code = "def export_config():\n raise ValueError('value is useless!')\n"
Expand All @@ -154,7 +154,7 @@ def test_read_config_py_with_value_error(self):
ValueError,
match="value is useless!",
):
read_config_list(config_path)
read_config(config_path)

def test_read_config_py_with_os_error(self):
py_code = "def export_config():\n raise OSError('where is my hat?')\n"
Expand All @@ -163,47 +163,47 @@ def test_read_config_py_with_os_error(self):
ConfigError,
match="where is my hat?",
):
read_config_list(config_path)
read_config(config_path)

def test_read_config_py_with_invalid_config_list(self):
py_code = "def export_config():\n return 42\n"
with text_file(self.new_config_py(), py_code) as config_path:
with pytest.raises(
ConfigError,
match=(
r"\.py: return value of export_config\(\):"
r" config_list must be of type"
r" ConfigList \| list\[Config\ | dict \| str\],"
r"\.py: failed converting value of 'config_1003:export_config':"
r" config must be of type"
r" Config \| ConfigObjectLike \| str \| Sequence\[ConfigObjectLike \| str\],"
r" but got int"
),
):
read_config_list(config_path)
read_config(config_path)


class CliConfigResolveTest(unittest.TestCase):
def test_read_config_py(self):
self.assert_ok(
read_config_list(Path(__file__).parent / "configs" / "recommended.py")
read_config(Path(__file__).parent / "configs" / "recommended.py")
)

def test_read_config_json(self):
self.assert_ok(
read_config_list(Path(__file__).parent / "configs" / "recommended.json")
read_config(Path(__file__).parent / "configs" / "recommended.json")
)

def test_read_config_yaml(self):
self.assert_ok(
read_config_list(Path(__file__).parent / "configs" / "recommended.yaml")
read_config(Path(__file__).parent / "configs" / "recommended.yaml")
)

def assert_ok(self, config_list: ConfigList):
self.assertIsInstance(config_list, ConfigList)
self.assertEqual(7, len(config_list.configs))
config = config_list.compute_config("test.zarr")
def assert_ok(self, config: Config):
self.assertIsInstance(config, Config)
self.assertEqual(None, config.name)
self.assertIsInstance(config.plugins, dict)
self.assertEqual({"xcube"}, set(config.plugins.keys()))
self.assertIsInstance(config.rules, dict)
self.assertIn("coords-for-dims", config.rules)
self.assertIn("xcube/cube-dims-order", config.rules)
self.assertEqual(7, len(config.objects))
config_obj = config.compute_config_object("test.zarr")
self.assertIsInstance(config_obj, ConfigObject)
self.assertEqual(None, config_obj.name)
self.assertIsInstance(config_obj.plugins, dict)
self.assertEqual({"xcube"}, set(config_obj.plugins.keys()))
self.assertIsInstance(config_obj.rules, dict)
self.assertIn("coords-for-dims", config_obj.rules)
self.assertIn("xcube/cube-dims-order", config_obj.rules)
18 changes: 18 additions & 0 deletions tests/cli/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ def test_files_no_config(self):
self.assertIn("Warning: no configuration file found.\n", result.output)
self.assertEqual(1, result.exit_code)

def test_files_no_config_lookup(self):
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
result = self.xrlint("--no-config-lookup", "--no-color", *self.files)
self.assertEqual(
"\n"
"dataset1.zarr:\n"
"dataset error No rules configured or applicable.\n\n"
"dataset1.nc:\n"
"dataset error No rules configured or applicable.\n\n"
"dataset2.zarr:\n"
"dataset error No rules configured or applicable.\n\n"
"dataset2.nc:\n"
"dataset error No rules configured or applicable.\n\n"
"4 errors\n\n",
result.output,
)
self.assertEqual(1, result.exit_code)

def test_files_one_rule(self):
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
result = self.xrlint("--no-color", *self.files)
Expand Down
8 changes: 4 additions & 4 deletions tests/formatters/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from xrlint.config import Config
from xrlint.config import ConfigObject
from xrlint.formatter import FormatterContext
from xrlint.plugin import new_plugin
from xrlint.result import Message, Result, ResultStats
Expand Down Expand Up @@ -38,11 +38,11 @@ class Rule1(RuleOp):
class Rule2(RuleOp):
pass

config = Config(plugins={"test": plugin})
config_obj = ConfigObject(plugins={"test": plugin})

return [
Result.new(
config,
config_object=config_obj,
file_path="test.nc",
messages=[
Message(message="message-1", rule_id="test/rule-1", severity=2),
Expand All @@ -51,7 +51,7 @@ class Rule2(RuleOp):
],
),
Result.new(
config,
config_object=config_obj,
file_path="test-2.nc",
messages=[
Message(message="message-1", rule_id="test/rule-1", severity=1),
Expand Down
6 changes: 3 additions & 3 deletions tests/formatters/test_simple.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from unittest import TestCase

from tests.formatters.helpers import get_context
from xrlint.config import Config
from xrlint.config import ConfigObject
from xrlint.formatters.simple import Simple
from xrlint.result import Message, Result


class SimpleTest(TestCase):
errors_and_warnings = [
Result.new(
Config(),
config_object=ConfigObject(),
file_path="test1.nc",
messages=[
Message(message="what", rule_id="rule-1", severity=2),
Expand All @@ -21,7 +21,7 @@ class SimpleTest(TestCase):

warnings_only = [
Result.new(
Config(),
ConfigObject(),
file_path="test2.nc",
messages=[
Message(message="what", rule_id="rule-1", severity=1),
Expand Down
Loading