Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# XRLint Change History

## Version 0.2.0 (in development)
## Version 0.2.0 (14.01.2025)

- Make all docstrings comply to google-style
- Rule description is now your `RuleOp`'s docstring
if `description` is not explicitly provided.
- Supporting _virtual plugins_: plugins provided by Python
dictionaries with rules defined by the `RuleOp` classes.
- Added more configuration examples in the `examples` folder.
- Introduced utilities `ValueConstructible` and
- All `xcube` rules now have references into the
xcube dataset specification.
- Introduced mixin classes `ValueConstructible` and
derived `MappingConstructible` which greatly simplify
flexible instantiation of configuration objects and their
children from Python and JSON/YAML values.
flexible instantiation of XRLint's configuration objects
and their children from Python and JSON/YAML values.
- Made all docstrings comply to google-style.

## Version 0.1.0 (09.01.2025)

Expand Down
6 changes: 6 additions & 0 deletions docs/rule-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ Contained in: `all`-:material-lightning-bolt: `recommended`-:material-alert:
### :material-bug: `any-spatial-data-var`

A datacube should have spatial data variables.
[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#data-model-and-format)

Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:

### :material-bug: `cube-dims-order`

Order of dimensions in spatio-temporal datacube variables should be [time, ..., y, x].
[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#data-model-and-format)

Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:

Expand All @@ -56,24 +58,28 @@ Contained in: `all`-:material-lightning-bolt: `recommended`-:material-alert:
### :material-lightbulb: `grid-mapping-naming`

Grid mapping variables should be called 'spatial_ref' or 'crs' for compatibility with rioxarray and other packages.
[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#spatial-reference)

Contained in: `all`-:material-lightning-bolt: `recommended`-:material-alert:

### :material-bug: `increasing-time`

Time coordinate labels should be monotonically increasing.
[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#temporal-reference)

Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:

### :material-bug: `lat-lon-naming`

Latitude and longitude coordinates and dimensions should be called 'lat' and 'lon'.
[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#spatial-reference)

Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:

### :material-bug: `single-grid-mapping`

A single grid mapping shall be used for all spatial data variables of a datacube.
[More information.](https://xcube.readthedocs.io/en/latest/cubespec.html#spatial-reference)

Contained in: `all`-:material-lightning-bolt: `recommended`-:material-lightning-bolt:

9 changes: 4 additions & 5 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
## Desired

- project logo
- if configuration for given FILE is empty,
report an error, see TODO in CLI main tests
- use `RuleMeta.docs_url` in formatters to create links
- implement xarray backend for xcube 'levels' format
so can validate them too
- support validating xcube 'levels' format. Options:
- implement xarray backend so we can open them using `xr.open_dataset`
with `opener_options: {"engine": "xc-levels"}`.
- implement a `xrlint.processor.Processor` for that purpose.
- add some more tests so we reach 99% coverage
- support rule op args/kwargs schema validation
- Support `RuleTest.expected`, it is currently unused
Expand Down
12 changes: 11 additions & 1 deletion xrlint/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,26 @@ def format(
"""


@dataclass(frozen=True, kw_only=True)
@dataclass(kw_only=True)
class FormatterMeta:
"""Formatter metadata."""

name: str
"""Formatter name."""

version: str = "0.0.0"
"""Formatter version."""

schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None
"""Formatter options schema."""

ref: str | None = None
"""Formatter reference.
Specifies the location from where the formatter can be
dynamically imported.
Must have the form "<module>:<attr>", if given.
"""


@dataclass(frozen=True, kw_only=True)
class Formatter:
Expand All @@ -69,6 +78,7 @@ class FormatterRegistry(Mapping[str, Formatter]):
def __init__(self):
self._registrations = {}

# TODO: fix this code duplication in define_rule()
def define_formatter(
self,
name: str | None = None,
Expand Down
7 changes: 4 additions & 3 deletions xrlint/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ class PluginMeta(MappingConstructible, JsonSerializable):
"""Plugin version."""

ref: str | None = None
"""Plugin module reference.
Specifies the location from where the plugin can be loaded.
Must have the form "<module>:<attr>".
"""Plugin reference.
Specifies the location from where the plugin can be
dynamically imported.
Must have the form "<module>:<attr>", if given.
"""

@classmethod
Expand Down
3 changes: 3 additions & 0 deletions xrlint/plugins/xcube/rules/any_spatial_data_var.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
version="1.0.0",
type="problem",
description="A datacube should have spatial data variables.",
docs_url=(
"https://xcube.readthedocs.io/en/latest/cubespec.html#data-model-and-format"
),
)
class AnySpatialDataVar(RuleOp):
def dataset(self, ctx: RuleContext, node: DatasetNode):
Expand Down
3 changes: 3 additions & 0 deletions xrlint/plugins/xcube/rules/cube_dims_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
f"Order of dimensions in spatio-temporal datacube variables"
f" should be [{T_NAME}, ..., {Y_NAME}, {X_NAME}]."
),
docs_url=(
"https://xcube.readthedocs.io/en/latest/cubespec.html#data-model-and-format"
),
)
class CubeDimsOrder(RuleOp):
def data_array(self, ctx: RuleContext, node: DataArrayNode):
Expand Down
1 change: 1 addition & 0 deletions xrlint/plugins/xcube/rules/grid_mapping_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
f"Grid mapping variables should be called {GM_NAMES_TEXT}"
f" for compatibility with rioxarray and other packages."
),
docs_url="https://xcube.readthedocs.io/en/latest/cubespec.html#spatial-reference",
)
class GridMappingNaming(RuleOp):
def dataset(self, ctx: RuleContext, node: DatasetNode):
Expand Down
3 changes: 3 additions & 0 deletions xrlint/plugins/xcube/rules/increasing_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
version="1.0.0",
type="problem",
description="Time coordinate labels should be monotonically increasing.",
docs_url=(
"https://xcube.readthedocs.io/en/latest/cubespec.html#temporal-reference"
),
)
class IncreasingTime(RuleOp):
def data_array(self, ctx: RuleContext, node: DataArrayNode):
Expand Down
1 change: 1 addition & 0 deletions xrlint/plugins/xcube/rules/lat_lon_naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
f"Latitude and longitude coordinates and dimensions"
f" should be called {LAT_NAME !r} and {LON_NAME !r}."
),
docs_url="https://xcube.readthedocs.io/en/latest/cubespec.html#spatial-reference",
)
class LatLonNaming(RuleOp):
def dataset(self, ctx: RuleContext, node: DatasetNode):
Expand Down
1 change: 1 addition & 0 deletions xrlint/plugins/xcube/rules/single_grid_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"A single grid mapping shall be used for all"
" spatial data variables of a datacube."
),
docs_url="https://xcube.readthedocs.io/en/latest/cubespec.html#spatial-reference",
)
class SingleGridMapping(RuleOp):
def dataset(self, ctx: RuleContext, node: DatasetNode):
Expand Down
44 changes: 23 additions & 21 deletions xrlint/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def postprocess(
"""


@dataclass(frozen=True, kw_only=True)
@dataclass(kw_only=True)
class ProcessorMeta(MappingConstructible):
"""Processor metadata."""

Expand All @@ -59,9 +59,10 @@ class ProcessorMeta(MappingConstructible):
"""Processor version."""

ref: str | None = None
"""Processor module reference.
Specifies the location from where the processor can be loaded.
Must have the form "<module>:<attr>".
"""Processor reference.
Specifies the location from where the processor can be
dynamically imported.
Must have the form "<module>:<attr>", if given.
"""

@classmethod
Expand All @@ -86,29 +87,30 @@ class Processor(MappingConstructible):
# """`True` if this processor supports auto-fixing of datasets."""

@classmethod
def _from_class(
cls, value: Type[ProcessorOp], name: str | None = None
) -> "Processor":
# TODO: see code duplication in Rule._from_class()
try:
# Note, the value.meta attribute is set by
# the define_rule
# noinspection PyUnresolvedReferences
return Processor(meta=value.meta, op_class=value)
except AttributeError:
raise ValueError(
f"missing processor metadata, apply define_processor()"
f" to class {value.__name__}"
)
def _from_type(cls, value: Type[ProcessorOp], value_name: str) -> "Processor":
# TODO: no test covers Processor._from_type
if issubclass(value, ProcessorOp):
# TODO: fix code duplication in Rule._from_class()
try:
# Note, the value.meta attribute is set by
# the define_rule
# noinspection PyUnresolvedReferences
return Processor(meta=value.meta, op_class=value)
except AttributeError:
raise ValueError(
f"missing processor metadata, apply define_processor()"
f" to class {value.__name__}"
)
return super()._from_type(value, value_name)

@classmethod
def _from_str(cls, value: str, name: str | None = None) -> "Processor":
def _from_str(cls, value: str, value_name: str) -> "Processor":
processor, processor_ref = import_value(
value,
"export_processor",
factory=Processor.from_value,
expected_type=type,
)
# noinspection PyUnresolvedReferences
processor.meta.ref = processor_ref
return processor

Expand All @@ -117,7 +119,7 @@ def _get_value_type_name(cls) -> str:
return "str | dict | Processor | Type[ProcessorOp]"


# TODO: see code duplication in define_rule()
# TODO: fix this code duplication in define_rule()
def define_processor(
name: str | None = None,
version: str = "0.0.0",
Expand Down
35 changes: 20 additions & 15 deletions xrlint/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,28 @@


class RuleContext(ABC):
"""The context passed to a [xrlint.rule.RuleOp][] instance.
"""The context passed to a [RuleOp][xrlint.rule.RuleOp] instance.

You should never create instances of this class yourself.
Instances of this interface are passed to the `RuleOp`'s
methods.
Instances of this interface are passed to the validation
methods of your `RuleOp`.
There should be no reason to create instances of this class
yourself.
"""

@property
@abstractmethod
def settings(self) -> dict[str, Any]:
"""Configuration settings."""
def file_path(self) -> str:
"""The current dataset's file path."""

@property
@abstractmethod
def dataset(self) -> xr.Dataset:
"""Get the current dataset."""
def settings(self) -> dict[str, Any]:
"""Applicable subset of settings from configuration `settings`."""

@property
@abstractmethod
def file_path(self) -> str:
"""Get the current dataset's file path."""
def dataset(self) -> xr.Dataset:
"""The current dataset."""

@abstractmethod
def report(
Expand Down Expand Up @@ -184,7 +185,11 @@ class RuleMeta(MappingConstructible, JsonSerializable):
"""

ref: str | None = None
"""Reference to the origin."""
"""Rule reference.
Specifies the location from where the rule can be
dynamically imported.
Must have the form "<module>:<attr>", if given.
"""

@classmethod
def _get_value_type_name(cls) -> str:
Expand Down Expand Up @@ -220,7 +225,7 @@ class that implements the rule's logic.
"""

@classmethod
def _from_str(cls, value: str, name: str) -> "Rule":
def _from_str(cls, value: str, value_name: str) -> "Rule":
rule, rule_ref = import_value(value, "export_rule", factory=Rule.from_value)
rule.meta.ref = rule_ref
return rule
Expand Down Expand Up @@ -307,13 +312,13 @@ def _from_int(cls, value: int, name: str) -> "RuleConfig":
return RuleConfig(cls._convert_severity(value))

@classmethod
def _from_str(cls, value: str, name: str) -> "RuleConfig":
def _from_str(cls, value: str, value_name: str) -> "RuleConfig":
return RuleConfig(cls._convert_severity(value))

@classmethod
def _from_sequence(cls, value: Sequence, name: str) -> "RuleConfig":
def _from_sequence(cls, value: Sequence, value_name: str) -> "RuleConfig":
if not value:
raise ValueError()
raise ValueError(f"{value_name} must not be empty")
severity = cls._convert_severity(value[0])
options = value[1:]
if not options:
Expand Down
4 changes: 2 additions & 2 deletions xrlint/util/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def from_value(cls, value: Any, value_name: str | None = None) -> T:
return cls._from_mapping(value, value_name)
if isinstance(value, Sequence):
return cls._from_sequence(value, value_name)
if isinstance(value, type):
if isclass(value) and issubclass(value, cls):
if isclass(value):
if issubclass(value, cls):
return cls._from_class(value, value_name)
else:
return cls._from_type(value, value_name)
Expand Down
2 changes: 1 addition & 1 deletion xrlint/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = "0.2.0.dev0"
version = "0.2.0"
Loading