Skip to content

Python API

This chapter provides a plain reference for the XRLint Python API.

Overview

Note: the xrlint.all convenience module exports all of the above from a single module.

CLI API

xrlint.cli.engine.XRLint

Bases: FormatterContext

This class provides the engine behind the XRLint CLI application. It represents the highest level component in the Python API.

Source code in xrlint\cli\engine.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
class XRLint(FormatterContext):
    """This class provides the engine behind the XRLint
    CLI application.
    It represents the highest level component in the Python API.
    """

    # noinspection PyShadowingBuiltins
    def __init__(
        self,
        no_config_lookup: int = False,
        config_path: str | None = None,
        plugin_specs: tuple[str, ...] = (),
        rule_specs: tuple[str, ...] = (),
        output_format: str = DEFAULT_OUTPUT_FORMAT,
        output_path: str | None = None,
        output_styled: bool = True,
        max_warnings: int = DEFAULT_MAX_WARNINGS,
    ):
        self.no_config_lookup = no_config_lookup
        self.config_path = config_path
        self.plugin_specs = plugin_specs
        self.rule_specs = rule_specs
        self.output_format = output_format
        self.output_path = output_path
        self.output_styled = output_styled
        self.max_warnings = max_warnings
        self._result_stats = ResultStats()
        self.config = Config()

    @property
    def max_warnings_exceeded(self) -> bool:
        """`True` if the maximum number of warnings has been exceeded."""
        return self._result_stats.warning_count > self.max_warnings

    @property
    def result_stats(self) -> ResultStats:
        """Get current result statistics."""
        return self._result_stats

    def init_config(self, *extra_configs: ConfigLike) -> None:
        """Initialize configuration.
        The function will load the configuration list from a specified
        configuration file, if any.
        Otherwise, it will search for the default configuration files
        in the current working directory.

        Args:
            *extra_configs: Variable number of configuration-like arguments.
                For more information see the
                [ConfigLike][xrlint.config.ConfigLike] type alias.
                If provided, `extra_configs` will be appended to configuration
                read from configration files and passed as command line options.
        """
        plugins = {}
        for plugin_spec in self.plugin_specs:
            plugin = Plugin.from_value(plugin_spec)
            plugins[plugin.meta.name] = plugin

        rules = {}
        for rule_spec in self.rule_specs:
            rule = yaml.load(rule_spec, Loader=yaml.SafeLoader)
            rules.update(rule)

        file_config = None

        if self.config_path:
            try:
                file_config = read_config(self.config_path)
            except (FileNotFoundError, ConfigError) as e:
                raise click.ClickException(f"{e}") from e
        elif not self.no_config_lookup:
            for config_path in DEFAULT_CONFIG_FILES:
                try:
                    file_config = read_config(config_path)
                    break
                except FileNotFoundError:
                    pass
                except ConfigError as e:
                    raise click.ClickException(f"{e}") from e
            if file_config is None:
                click.echo("Warning: no configuration file found.")

        core_config_obj = get_core_config_object()
        core_config_obj.plugins.update(plugins)

        base_configs = []
        if file_config is not None:
            base_configs += file_config.objects
        if rules:
            base_configs += [{"rules": rules}]

        self.config = Config.from_config(core_config_obj, *base_configs, *extra_configs)
        if not any(co.rules for co in self.config.objects):
            raise click.ClickException("no rules configured")

    def compute_config_for_file(self, file_path: str) -> ConfigObject | None:
        """Compute the configuration object for the given file.

        Args:
            file_path: A file path or URL.

        Returns:
            A configuration object or `None` if no item
                in the configuration list applies.
        """
        return self.config.compute_config_object(file_path)

    def print_config_for_file(self, file_path: str) -> None:
        """Print computed configuration object for the given file.

        Args:
            file_path: A file path or URL.
        """
        config = self.compute_config_for_file(file_path)
        config_json_obj = config.to_json() if config is not None else None
        click.echo(json.dumps(config_json_obj, indent=2))

    def validate_files(self, files: Iterable[str]) -> Iterator[Result]:
        """Validate given files or directories which may also be given as URLs.
        The function produces a validation result for each file.

        Args:
            files: Iterable of files.

        Returns:
            Iterator of reports.
        """
        linter = Linter()
        for file_path, config in self.get_files(files):
            yield linter.validate(file_path, config=config)

    def get_files(
        self, file_paths: Iterable[str]
    ) -> Iterator[tuple[str, ConfigObject]]:
        """Provide an iterator for the list of files or directories and their
        computed configurations.

        Directories in `files` that are not ignored and not recognized by any
        file pattern will be recursively traversed.

        Args:
            file_paths: Iterable of files or directories.

        Returns:
            An iterator of pairs comprising a file or directory path
                and its computed configuration.
        """
        config, global_filter = self.config.split_global_filter(
            default=DEFAULT_GLOBAL_FILTER
        )

        def compute_config_object(p: str):
            return config.compute_config_object(p) if global_filter.accept(p) else None

        for file_path in file_paths:
            _fs, root = fsspec.url_to_fs(file_path)
            fs: fsspec.AbstractFileSystem = _fs
            is_local = isinstance(fs, fsspec.implementations.local.LocalFileSystem)

            config_obj = compute_config_object(file_path)
            if config_obj is not None:
                yield file_path, config_obj
                continue

            if fs.isdir(root):
                for path, dirs, files in fs.walk(root, topdown=True):
                    for d in list(dirs):
                        d_path = f"{path}/{d}"
                        d_path = d_path if is_local else fs.unstrip_protocol(d_path)
                        c = compute_config_object(d_path)
                        if c is not None:
                            dirs.remove(d)
                            yield d_path, c

                    for f in files:
                        f_path = f"{path}/{f}"
                        f_path = f_path if is_local else fs.unstrip_protocol(f_path)
                        c = compute_config_object(f_path)
                        if c is not None:
                            yield f_path, c

    def format_results(self, results: Iterable[Result]) -> str:
        """Format the given results.

        Args:
            results: Iterable of results.

        Returns:
            A report in plain text.
        """
        output_format = (
            self.output_format if self.output_format else DEFAULT_OUTPUT_FORMAT
        )
        formatters = export_formatters()
        formatter = formatters.get(output_format)
        if formatter is None:
            raise click.ClickException(
                f"unknown format {output_format!r}."
                f" The available formats are"
                f" {', '.join(repr(k) for k in formatters.keys())}."
            )
        # Here we could pass and validate format-specific args/kwargs
        #   against formatter.meta.schema
        if output_format == "simple":
            formatter_kwargs = {
                "styled": self.output_styled and self.output_path is None,
                "output": self.output_path is None,
            }
        else:
            formatter_kwargs = {}
        # noinspection PyArgumentList
        formatter_op = formatter.op_class(**formatter_kwargs)
        return formatter_op.format(self, self._result_stats.collect(results))

    def write_report(self, report: str) -> None:
        """Write the validation report provided as plain text."""
        if self.output_path:
            with fsspec.open(self.output_path, mode="w") as f:
                f.write(report)
        elif self.output_format != "simple":
            # The simple formatters outputs incrementally to console
            print(report)

    @classmethod
    def init_config_file(cls) -> None:
        """Write an initial configuration file.
        The file is written into the current working directory.
        """
        file_path = DEFAULT_CONFIG_FILE_YAML
        if os.path.exists(file_path):
            raise click.ClickException(f"file {file_path} already exists.")
        with open(file_path, "w") as f:
            f.write(INIT_CONFIG_YAML)
        click.echo(f"Configuration template written to {file_path}")

Attributes

xrlint.cli.engine.XRLint.max_warnings_exceeded property

True if the maximum number of warnings has been exceeded.

xrlint.cli.engine.XRLint.result_stats property

Get current result statistics.

Functions

xrlint.cli.engine.XRLint.init_config(*extra_configs)

Initialize configuration. The function will load the configuration list from a specified configuration file, if any. Otherwise, it will search for the default configuration files in the current working directory.

Parameters:

  • *extra_configs (ConfigLike, default: () ) –

    Variable number of configuration-like arguments. For more information see the ConfigLike type alias. If provided, extra_configs will be appended to configuration read from configration files and passed as command line options.

Source code in xrlint\cli\engine.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def init_config(self, *extra_configs: ConfigLike) -> None:
    """Initialize configuration.
    The function will load the configuration list from a specified
    configuration file, if any.
    Otherwise, it will search for the default configuration files
    in the current working directory.

    Args:
        *extra_configs: Variable number of configuration-like arguments.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
            If provided, `extra_configs` will be appended to configuration
            read from configration files and passed as command line options.
    """
    plugins = {}
    for plugin_spec in self.plugin_specs:
        plugin = Plugin.from_value(plugin_spec)
        plugins[plugin.meta.name] = plugin

    rules = {}
    for rule_spec in self.rule_specs:
        rule = yaml.load(rule_spec, Loader=yaml.SafeLoader)
        rules.update(rule)

    file_config = None

    if self.config_path:
        try:
            file_config = read_config(self.config_path)
        except (FileNotFoundError, ConfigError) as e:
            raise click.ClickException(f"{e}") from e
    elif not self.no_config_lookup:
        for config_path in DEFAULT_CONFIG_FILES:
            try:
                file_config = read_config(config_path)
                break
            except FileNotFoundError:
                pass
            except ConfigError as e:
                raise click.ClickException(f"{e}") from e
        if file_config is None:
            click.echo("Warning: no configuration file found.")

    core_config_obj = get_core_config_object()
    core_config_obj.plugins.update(plugins)

    base_configs = []
    if file_config is not None:
        base_configs += file_config.objects
    if rules:
        base_configs += [{"rules": rules}]

    self.config = Config.from_config(core_config_obj, *base_configs, *extra_configs)
    if not any(co.rules for co in self.config.objects):
        raise click.ClickException("no rules configured")
xrlint.cli.engine.XRLint.compute_config_for_file(file_path)

Compute the configuration object for the given file.

Parameters:

  • file_path (str) –

    A file path or URL.

Returns:

  • ConfigObject | None

    A configuration object or None if no item in the configuration list applies.

Source code in xrlint\cli\engine.py
133
134
135
136
137
138
139
140
141
142
143
def compute_config_for_file(self, file_path: str) -> ConfigObject | None:
    """Compute the configuration object for the given file.

    Args:
        file_path: A file path or URL.

    Returns:
        A configuration object or `None` if no item
            in the configuration list applies.
    """
    return self.config.compute_config_object(file_path)
xrlint.cli.engine.XRLint.print_config_for_file(file_path)

Print computed configuration object for the given file.

Parameters:

  • file_path (str) –

    A file path or URL.

Source code in xrlint\cli\engine.py
145
146
147
148
149
150
151
152
153
def print_config_for_file(self, file_path: str) -> None:
    """Print computed configuration object for the given file.

    Args:
        file_path: A file path or URL.
    """
    config = self.compute_config_for_file(file_path)
    config_json_obj = config.to_json() if config is not None else None
    click.echo(json.dumps(config_json_obj, indent=2))
xrlint.cli.engine.XRLint.validate_files(files)

Validate given files or directories which may also be given as URLs. The function produces a validation result for each file.

Parameters:

  • files (Iterable[str]) –

    Iterable of files.

Returns:

  • Iterator[Result]

    Iterator of reports.

Source code in xrlint\cli\engine.py
155
156
157
158
159
160
161
162
163
164
165
166
167
def validate_files(self, files: Iterable[str]) -> Iterator[Result]:
    """Validate given files or directories which may also be given as URLs.
    The function produces a validation result for each file.

    Args:
        files: Iterable of files.

    Returns:
        Iterator of reports.
    """
    linter = Linter()
    for file_path, config in self.get_files(files):
        yield linter.validate(file_path, config=config)
xrlint.cli.engine.XRLint.get_files(file_paths)

Provide an iterator for the list of files or directories and their computed configurations.

Directories in files that are not ignored and not recognized by any file pattern will be recursively traversed.

Parameters:

  • file_paths (Iterable[str]) –

    Iterable of files or directories.

Returns:

  • Iterator[tuple[str, ConfigObject]]

    An iterator of pairs comprising a file or directory path and its computed configuration.

Source code in xrlint\cli\engine.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def get_files(
    self, file_paths: Iterable[str]
) -> Iterator[tuple[str, ConfigObject]]:
    """Provide an iterator for the list of files or directories and their
    computed configurations.

    Directories in `files` that are not ignored and not recognized by any
    file pattern will be recursively traversed.

    Args:
        file_paths: Iterable of files or directories.

    Returns:
        An iterator of pairs comprising a file or directory path
            and its computed configuration.
    """
    config, global_filter = self.config.split_global_filter(
        default=DEFAULT_GLOBAL_FILTER
    )

    def compute_config_object(p: str):
        return config.compute_config_object(p) if global_filter.accept(p) else None

    for file_path in file_paths:
        _fs, root = fsspec.url_to_fs(file_path)
        fs: fsspec.AbstractFileSystem = _fs
        is_local = isinstance(fs, fsspec.implementations.local.LocalFileSystem)

        config_obj = compute_config_object(file_path)
        if config_obj is not None:
            yield file_path, config_obj
            continue

        if fs.isdir(root):
            for path, dirs, files in fs.walk(root, topdown=True):
                for d in list(dirs):
                    d_path = f"{path}/{d}"
                    d_path = d_path if is_local else fs.unstrip_protocol(d_path)
                    c = compute_config_object(d_path)
                    if c is not None:
                        dirs.remove(d)
                        yield d_path, c

                for f in files:
                    f_path = f"{path}/{f}"
                    f_path = f_path if is_local else fs.unstrip_protocol(f_path)
                    c = compute_config_object(f_path)
                    if c is not None:
                        yield f_path, c
xrlint.cli.engine.XRLint.format_results(results)

Format the given results.

Parameters:

  • results (Iterable[Result]) –

    Iterable of results.

Returns:

  • str

    A report in plain text.

Source code in xrlint\cli\engine.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def format_results(self, results: Iterable[Result]) -> str:
    """Format the given results.

    Args:
        results: Iterable of results.

    Returns:
        A report in plain text.
    """
    output_format = (
        self.output_format if self.output_format else DEFAULT_OUTPUT_FORMAT
    )
    formatters = export_formatters()
    formatter = formatters.get(output_format)
    if formatter is None:
        raise click.ClickException(
            f"unknown format {output_format!r}."
            f" The available formats are"
            f" {', '.join(repr(k) for k in formatters.keys())}."
        )
    # Here we could pass and validate format-specific args/kwargs
    #   against formatter.meta.schema
    if output_format == "simple":
        formatter_kwargs = {
            "styled": self.output_styled and self.output_path is None,
            "output": self.output_path is None,
        }
    else:
        formatter_kwargs = {}
    # noinspection PyArgumentList
    formatter_op = formatter.op_class(**formatter_kwargs)
    return formatter_op.format(self, self._result_stats.collect(results))
xrlint.cli.engine.XRLint.write_report(report)

Write the validation report provided as plain text.

Source code in xrlint\cli\engine.py
252
253
254
255
256
257
258
259
def write_report(self, report: str) -> None:
    """Write the validation report provided as plain text."""
    if self.output_path:
        with fsspec.open(self.output_path, mode="w") as f:
            f.write(report)
    elif self.output_format != "simple":
        # The simple formatters outputs incrementally to console
        print(report)
xrlint.cli.engine.XRLint.init_config_file() classmethod

Write an initial configuration file. The file is written into the current working directory.

Source code in xrlint\cli\engine.py
261
262
263
264
265
266
267
268
269
270
271
@classmethod
def init_config_file(cls) -> None:
    """Write an initial configuration file.
    The file is written into the current working directory.
    """
    file_path = DEFAULT_CONFIG_FILE_YAML
    if os.path.exists(file_path):
        raise click.ClickException(f"file {file_path} already exists.")
    with open(file_path, "w") as f:
        f.write(INIT_CONFIG_YAML)
    click.echo(f"Configuration template written to {file_path}")

Linter API

xrlint.linter.new_linter(*configs, **config_props)

Create a new Linter with the core plugin included and the given additional configuration.

Parameters:

  • *configs (ConfigLike, default: () ) –

    Variable number of configuration-like arguments. For more information see the ConfigLike type alias.

  • **config_props (Any, default: {} ) –

    Individual configuration object properties. For more information refer to the properties of a ConfigObject.

Returns:

  • Linter

    A new linter instance

Source code in xrlint\linter.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def new_linter(*configs: ConfigLike, **config_props: Any) -> "Linter":
    """Create a new `Linter` with the core plugin included and the
     given additional configuration.

    Args:
        *configs: Variable number of configuration-like arguments.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
        **config_props: Individual configuration object properties.
            For more information refer to the properties of a
            [ConfigObject][xrlint.config.ConfigObject].

    Returns:
        A new linter instance
    """
    return Linter(get_core_config_object(), *configs, **config_props)

xrlint.linter.Linter

The linter.

Using the constructor directly creates an empty linter with no configuration - even without the core plugin and its predefined rule configurations. If you want a linter with core plugin included use the new_linter() function.

Parameters:

  • *configs (ConfigLike, default: () ) –

    Variable number of configuration-like arguments. For more information see the ConfigLike type alias.

  • **config_props (Any, default: {} ) –

    Individual configuration object properties. For more information refer to the properties of a ConfigObject.

Source code in xrlint\linter.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class Linter:
    """The linter.

    Using the constructor directly creates an empty linter
    with no configuration - even without the core plugin and
    its predefined rule configurations.
    If you want a linter with core plugin included use the
    `new_linter()` function.

    Args:
        *configs: Variable number of configuration-like arguments.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
        **config_props: Individual configuration object properties.
            For more information refer to the properties of a
            [ConfigObject][xrlint.config.ConfigObject].
    """

    def __init__(self, *configs: ConfigLike, **config_props: Any):
        self._config = Config.from_config(*configs, config_props)

    @property
    def config(self) -> Config:
        """Get this linter's configuration."""
        return self._config

    def validate(
        self,
        dataset: Any,
        *,
        file_path: str | None = None,
        config: ConfigLike = None,
        **config_props: Any,
    ) -> Result:
        """Validate a dataset against applicable rules.

        Args:
            dataset: The dataset. Can be a `xr.Dataset` or `xr.DataTree`
                instance or a file path, or any dataset source that can
                be opened using `xarray.open_dataset()`
                or `xarray.open_datatree()`.
            file_path: Optional file path used for formatting
                messages. Useful if `dataset` is not a file path.
            config: Optional configuration-like value.
                For more information see the
                [ConfigLike][xrlint.config.ConfigLike] type alias.
            **config_props: Individual configuration object properties.
                For more information refer to the properties of a
                [ConfigObject][xrlint.config.ConfigObject].

        Returns:
            Result of the validation.
        """
        if not file_path:
            if isinstance(dataset, (xr.Dataset, xr.DataTree)):
                file_path = file_path or _get_file_path_for_dataset(dataset)
            else:
                file_path = file_path or _get_file_path_for_source(dataset)

        config = Config.from_config(self._config, config, config_props)
        config_obj = config.compute_config_object(file_path)
        if config_obj is None or not config_obj.rules:
            return Result(
                file_path=file_path,
                messages=[
                    new_fatal_message(
                        f"No configuration given or matches {file_path!r}.",
                    )
                ],
            )

        return validate_dataset(config_obj, dataset, file_path)

Attributes

xrlint.linter.Linter.config property

Get this linter's configuration.

Functions

xrlint.linter.Linter.validate(dataset, *, file_path=None, config=None, **config_props)

Validate a dataset against applicable rules.

Parameters:

  • dataset (Any) –

    The dataset. Can be a xr.Dataset or xr.DataTree instance or a file path, or any dataset source that can be opened using xarray.open_dataset() or xarray.open_datatree().

  • file_path (str | None, default: None ) –

    Optional file path used for formatting messages. Useful if dataset is not a file path.

  • config (ConfigLike, default: None ) –

    Optional configuration-like value. For more information see the ConfigLike type alias.

  • **config_props (Any, default: {} ) –

    Individual configuration object properties. For more information refer to the properties of a ConfigObject.

Returns:

  • Result

    Result of the validation.

Source code in xrlint\linter.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def validate(
    self,
    dataset: Any,
    *,
    file_path: str | None = None,
    config: ConfigLike = None,
    **config_props: Any,
) -> Result:
    """Validate a dataset against applicable rules.

    Args:
        dataset: The dataset. Can be a `xr.Dataset` or `xr.DataTree`
            instance or a file path, or any dataset source that can
            be opened using `xarray.open_dataset()`
            or `xarray.open_datatree()`.
        file_path: Optional file path used for formatting
            messages. Useful if `dataset` is not a file path.
        config: Optional configuration-like value.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
        **config_props: Individual configuration object properties.
            For more information refer to the properties of a
            [ConfigObject][xrlint.config.ConfigObject].

    Returns:
        Result of the validation.
    """
    if not file_path:
        if isinstance(dataset, (xr.Dataset, xr.DataTree)):
            file_path = file_path or _get_file_path_for_dataset(dataset)
        else:
            file_path = file_path or _get_file_path_for_source(dataset)

    config = Config.from_config(self._config, config, config_props)
    config_obj = config.compute_config_object(file_path)
    if config_obj is None or not config_obj.rules:
        return Result(
            file_path=file_path,
            messages=[
                new_fatal_message(
                    f"No configuration given or matches {file_path!r}.",
                )
            ],
        )

    return validate_dataset(config_obj, dataset, file_path)

Plugin API

xrlint.plugin.new_plugin(name, version='0.0.0', ref=None, docs_url=None, rules=None, processors=None, configs=None)

Create a new plugin object that can contribute rules, processors, and predefined configurations to XRLint.

Parameters:

  • name (str) –

    Plugin name. Required.

  • version (str, default: '0.0.0' ) –

    Plugin version. Defaults to "0.0.0".

  • ref (str | None, default: None ) –

    Plugin reference. Optional.

  • docs_url (str | None, default: None ) –

    Plugin documentation URL. Optional.

  • rules (dict[str, Rule] | None, default: None ) –

    A dictionary containing the definitions of custom rules. Optional.

  • processors (dict[str, Processor] | None, default: None ) –

    A dictionary containing custom processors. Optional.

  • configs (dict[str, ConfigObject] | None, default: None ) –

    A dictionary containing predefined configurations. Optional.

Source code in xrlint\plugin.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def new_plugin(
    name: str,
    version: str = "0.0.0",
    ref: str | None = None,
    docs_url: str | None = None,
    rules: dict[str, Rule] | None = None,
    processors: dict[str, Processor] | None = None,
    configs: dict[str, ConfigObject] | None = None,
) -> Plugin:
    """Create a new plugin object that can contribute rules, processors,
    and predefined configurations to XRLint.

    Args:
        name: Plugin name. Required.
        version: Plugin version. Defaults to `"0.0.0"`.
        ref: Plugin reference. Optional.
        docs_url: Plugin documentation URL. Optional.
        rules: A dictionary containing the definitions of custom rules. Optional.
        processors: A dictionary containing custom processors. Optional.
        configs: A dictionary containing predefined configurations. Optional.
    """
    return Plugin(
        meta=PluginMeta(name=name, version=version, ref=ref, docs_url=docs_url),
        rules=rules or {},
        processors=processors or {},
        configs=configs or {},
    )

xrlint.plugin.Plugin dataclass

Bases: MappingConstructible, JsonSerializable

A plugin that can contribute rules, processors, and predefined configurations to XRLint.

Use the factory new_plugin to create plugin instances.

Source code in xrlint\plugin.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
@dataclass(frozen=True, kw_only=True)
class Plugin(MappingConstructible, JsonSerializable):
    """A plugin that can contribute rules, processors,
    and predefined configurations to XRLint.

    Use the factory [new_plugin][xrlint.plugin.new_plugin]
    to create plugin instances.
    """

    meta: PluginMeta
    """Information about the plugin."""

    rules: dict[str, Rule] = field(default_factory=dict)
    """A dictionary containing the definitions of custom rules."""

    processors: dict[str, Processor] = field(default_factory=dict)
    """A dictionary containing named processors.
    """

    configs: dict[str, list[ConfigObject]] = field(default_factory=dict)
    """A dictionary containing named configuration lists."""

    def define_rule(
        self,
        name: str,
        version: str = "0.0.0",
        schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None,
        type: Literal["problem", "suggestion", "layout"] = "problem",
        description: str | None = None,
        docs_url: str | None = None,
        op_class: Type[RuleOp] | None = None,
    ) -> Callable[[Any], Type[RuleOp]] | None:
        """Decorator to define a plugin rule.
        The method registers a new rule with the plugin.

        Refer to [define_rule][xrlint.rule.define_rule]
        for details.
        """
        return define_rule(
            name=name,
            version=version,
            schema=schema,
            type=type,
            description=description,
            docs_url=docs_url,
            op_class=op_class,
            registry=self.rules,
        )

    def define_processor(
        self,
        name: str | None = None,
        version: str = "0.0.0",
        op_class: Type[ProcessorOp] | None = None,
    ):
        """Decorator to define a plugin processor.
        The method registers a new processor with the plugin.

        Refer to [define_processor][xrlint.processor.define_processor]
        for details.
        """
        return define_processor(
            name=name,
            version=version,
            op_class=op_class,
            registry=self.processors,
        )

    def define_config(self, name: str, config: ConfigLike) -> Config:
        """Define a named configuration.

        Args:
            name: The name of the configuration.
            config: A configuration-like value.
                For more information see the
                [ConfigLike][xrlint.config.ConfigLike] type alias.

        Returns:
            The configuration.
        """
        config = Config.from_value(config)
        self.configs[name] = list(config.objects)
        return config

    @classmethod
    def _from_str(cls, value: str, value_name: str) -> "Plugin":
        plugin, plugin_ref = import_value(
            value, "export_plugin", factory=Plugin.from_value
        )
        plugin.meta.ref = plugin_ref
        return plugin

    @classmethod
    def value_name(cls) -> str:
        return "plugin"

    @classmethod
    def value_type_name(cls) -> str:
        return "Plugin | dict | str"

    def to_json(self, value_name: str | None = None) -> JsonValue:
        if self.meta.ref:
            return self.meta.ref
        return super().to_json(value_name=value_name)

Attributes

xrlint.plugin.Plugin.meta instance-attribute

Information about the plugin.

xrlint.plugin.Plugin.rules = field(default_factory=dict) class-attribute instance-attribute

A dictionary containing the definitions of custom rules.

xrlint.plugin.Plugin.processors = field(default_factory=dict) class-attribute instance-attribute

A dictionary containing named processors.

xrlint.plugin.Plugin.configs = field(default_factory=dict) class-attribute instance-attribute

A dictionary containing named configuration lists.

Functions

xrlint.plugin.Plugin.define_rule(name, version='0.0.0', schema=None, type='problem', description=None, docs_url=None, op_class=None)

Decorator to define a plugin rule. The method registers a new rule with the plugin.

Refer to define_rule for details.

Source code in xrlint\plugin.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def define_rule(
    self,
    name: str,
    version: str = "0.0.0",
    schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None,
    type: Literal["problem", "suggestion", "layout"] = "problem",
    description: str | None = None,
    docs_url: str | None = None,
    op_class: Type[RuleOp] | None = None,
) -> Callable[[Any], Type[RuleOp]] | None:
    """Decorator to define a plugin rule.
    The method registers a new rule with the plugin.

    Refer to [define_rule][xrlint.rule.define_rule]
    for details.
    """
    return define_rule(
        name=name,
        version=version,
        schema=schema,
        type=type,
        description=description,
        docs_url=docs_url,
        op_class=op_class,
        registry=self.rules,
    )
xrlint.plugin.Plugin.define_processor(name=None, version='0.0.0', op_class=None)

Decorator to define a plugin processor. The method registers a new processor with the plugin.

Refer to define_processor for details.

Source code in xrlint\plugin.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def define_processor(
    self,
    name: str | None = None,
    version: str = "0.0.0",
    op_class: Type[ProcessorOp] | None = None,
):
    """Decorator to define a plugin processor.
    The method registers a new processor with the plugin.

    Refer to [define_processor][xrlint.processor.define_processor]
    for details.
    """
    return define_processor(
        name=name,
        version=version,
        op_class=op_class,
        registry=self.processors,
    )
xrlint.plugin.Plugin.define_config(name, config)

Define a named configuration.

Parameters:

  • name (str) –

    The name of the configuration.

  • config (ConfigLike) –

    A configuration-like value. For more information see the ConfigLike type alias.

Returns:

  • Config

    The configuration.

Source code in xrlint\plugin.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def define_config(self, name: str, config: ConfigLike) -> Config:
    """Define a named configuration.

    Args:
        name: The name of the configuration.
        config: A configuration-like value.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.

    Returns:
        The configuration.
    """
    config = Config.from_value(config)
    self.configs[name] = list(config.objects)
    return config

xrlint.plugin.PluginMeta dataclass

Bases: MappingConstructible, JsonSerializable

Plugin metadata.

Source code in xrlint\plugin.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@dataclass(kw_only=True)
class PluginMeta(MappingConstructible, JsonSerializable):
    """Plugin metadata."""

    name: str
    """Plugin name."""

    version: str = "0.0.0"
    """Plugin version."""

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

    docs_url: str | None = None
    """Plugin documentation URL."""

    @classmethod
    def value_name(cls) -> str:
        return "plugin_meta"

    @classmethod
    def value_type_name(cls) -> str:
        return "PluginMeta | dict"

Attributes

xrlint.plugin.PluginMeta.name instance-attribute

Plugin name.

xrlint.plugin.PluginMeta.version = '0.0.0' class-attribute instance-attribute

Plugin version.

xrlint.plugin.PluginMeta.ref = None class-attribute instance-attribute

Plugin reference. Specifies the location from where the plugin can be dynamically imported. Must have the form ":", if given.

xrlint.plugin.PluginMeta.docs_url = None class-attribute instance-attribute

Plugin documentation URL.

Configuration API

xrlint.config.Config dataclass

Bases: ValueConstructible, JsonSerializable

Represents a XRLint configuration. A Config instance basically manages a sequence of configuration objects.

You should not use the class constructor directly. Instead, use the Config.from_value() function.

Source code in xrlint\config.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
@dataclass(frozen=True)
class Config(ValueConstructible, JsonSerializable):
    """Represents a XRLint configuration.
    A `Config` instance basically manages a sequence of
    [configuration objects][xrlint.config.ConfigObject].

    You should not use the class constructor directly.
    Instead, use the `Config.from_value()` function.
    """

    objects: list[ConfigObject] = field(default_factory=list)
    """The configuration objects."""

    def split_global_filter(
        self, default: FileFilter | None = None
    ) -> tuple["Config", FileFilter]:
        """Get a global file filter for this configuration list."""
        global_filter = FileFilter(
            default.files if default else (),
            default.ignores if default else (),
        )
        objects = []
        for co in self.objects:
            if co.empty and not co.file_filter.empty:
                global_filter = global_filter.merge(co.file_filter)
            else:
                objects.append(co)
        return Config(objects=objects), global_filter

    def compute_config_object(self, file_path: str) -> ConfigObject | None:
        """Compute the configuration object for the given file path.

        Args:
            file_path: A dataset file path.

        Returns:
            A `Config` object which may be empty, or `None`
                if `file_path` is not included by any `files` pattern
                or intentionally ignored by global `ignores`.
        """

        config_obj = None
        for co in self.objects:
            if co.file_filter.empty or co.file_filter.accept(file_path):
                config_obj = config_obj.merge(co) if config_obj is not None else co

        if config_obj is None:
            return None

        # Note, computed configurations do not have "files" and "ignores"
        return ConfigObject(
            linter_options=config_obj.linter_options,
            opener_options=config_obj.opener_options,
            processor=config_obj.processor,
            plugins=config_obj.plugins,
            rules=config_obj.rules,
            settings=config_obj.settings,
        )

    @classmethod
    def from_config(
        cls,
        *configs: ConfigLike,
        value_name: str | None = None,
    ) -> "Config":
        """Convert variable arguments of configuration-like objects
        into a new `Config` instance.

        Args:
            *configs: Variable number of configuration-like arguments.
                For more information see the
                [ConfigLike][xrlint.config.ConfigLike] type alias.
            value_name: The value's name used for reporting errors.

        Returns:
            A new `Config` instance.
        """
        value_name = value_name or cls.value_name()
        objects: list[ConfigObject] = []
        plugins: dict[str, Plugin] = {}
        for i, config_like in enumerate(configs):
            new_objects = None
            if isinstance(config_like, str):
                if CORE_PLUGIN_NAME not in plugins:
                    plugins.update({CORE_PLUGIN_NAME: get_core_plugin()})
                new_objects = cls._get_named_config(config_like, plugins).objects
            elif isinstance(config_like, Config):
                new_objects = config_like.objects
            elif isinstance(config_like, (list, tuple)):
                new_objects = cls.from_config(
                    *config_like, value_name=f"{value_name}[{i}]"
                ).objects
            elif config_like:
                new_objects = [
                    ConfigObject.from_value(
                        config_like, value_name=f"{value_name}[{i}]"
                    )
                ]
            if new_objects:
                for co in new_objects:
                    objects.append(co)
                    plugins.update(co.plugins if co.plugins else {})
        return cls(objects=objects)

    @classmethod
    def from_value(cls, value: ConfigLike, value_name: str | None = None) -> "Config":
        """Convert given `value` into a `Config` object.

        If `value` is already a `Config` then it is returned as-is.

        Args:
            value: A configuration-like value. For more information
                see the [ConfigLike][xrlint.config.ConfigLike] type alias.
            value_name: The value's name used for reporting errors.
        Returns:
            A `Config` object.
        """
        if isinstance(value, (ConfigObject, dict)):
            return Config(objects=[ConfigObject.from_value(value)])
        return super().from_value(value, value_name=value_name)

    @classmethod
    def _from_sequence(cls, value: Sequence, value_name: str) -> "Config":
        return cls.from_config(*value, value_name=value_name)

    @classmethod
    def value_name(cls) -> str:
        return "config"

    @classmethod
    def value_type_name(cls) -> str:
        return "Config | ConfigObjectLike | str | Sequence[ConfigObjectLike | str]"

    @classmethod
    def _get_named_config(
        cls, config_spec: str, plugins: dict[str, "Plugin"]
    ) -> "Config":
        plugin_name, config_name = (
            config_spec.split("/", maxsplit=1)
            if "/" in config_spec
            else (CORE_PLUGIN_NAME, config_spec)
        )
        plugin: Plugin | None = plugins.get(plugin_name)
        if plugin is None or not plugin.configs or config_name not in plugin.configs:
            raise ValueError(f"configuration {config_spec!r} not found")
        return Config.from_value(plugin.configs[config_name])

Attributes

xrlint.config.Config.objects = field(default_factory=list) class-attribute instance-attribute

The configuration objects.

Functions

xrlint.config.Config.split_global_filter(default=None)

Get a global file filter for this configuration list.

Source code in xrlint\config.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def split_global_filter(
    self, default: FileFilter | None = None
) -> tuple["Config", FileFilter]:
    """Get a global file filter for this configuration list."""
    global_filter = FileFilter(
        default.files if default else (),
        default.ignores if default else (),
    )
    objects = []
    for co in self.objects:
        if co.empty and not co.file_filter.empty:
            global_filter = global_filter.merge(co.file_filter)
        else:
            objects.append(co)
    return Config(objects=objects), global_filter
xrlint.config.Config.compute_config_object(file_path)

Compute the configuration object for the given file path.

Parameters:

  • file_path (str) –

    A dataset file path.

Returns:

  • ConfigObject | None

    A Config object which may be empty, or None if file_path is not included by any files pattern or intentionally ignored by global ignores.

Source code in xrlint\config.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def compute_config_object(self, file_path: str) -> ConfigObject | None:
    """Compute the configuration object for the given file path.

    Args:
        file_path: A dataset file path.

    Returns:
        A `Config` object which may be empty, or `None`
            if `file_path` is not included by any `files` pattern
            or intentionally ignored by global `ignores`.
    """

    config_obj = None
    for co in self.objects:
        if co.file_filter.empty or co.file_filter.accept(file_path):
            config_obj = config_obj.merge(co) if config_obj is not None else co

    if config_obj is None:
        return None

    # Note, computed configurations do not have "files" and "ignores"
    return ConfigObject(
        linter_options=config_obj.linter_options,
        opener_options=config_obj.opener_options,
        processor=config_obj.processor,
        plugins=config_obj.plugins,
        rules=config_obj.rules,
        settings=config_obj.settings,
    )
xrlint.config.Config.from_config(*configs, value_name=None) classmethod

Convert variable arguments of configuration-like objects into a new Config instance.

Parameters:

  • *configs (ConfigLike, default: () ) –

    Variable number of configuration-like arguments. For more information see the ConfigLike type alias.

  • value_name (str | None, default: None ) –

    The value's name used for reporting errors.

Returns:

  • Config

    A new Config instance.

Source code in xrlint\config.py
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
@classmethod
def from_config(
    cls,
    *configs: ConfigLike,
    value_name: str | None = None,
) -> "Config":
    """Convert variable arguments of configuration-like objects
    into a new `Config` instance.

    Args:
        *configs: Variable number of configuration-like arguments.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
        value_name: The value's name used for reporting errors.

    Returns:
        A new `Config` instance.
    """
    value_name = value_name or cls.value_name()
    objects: list[ConfigObject] = []
    plugins: dict[str, Plugin] = {}
    for i, config_like in enumerate(configs):
        new_objects = None
        if isinstance(config_like, str):
            if CORE_PLUGIN_NAME not in plugins:
                plugins.update({CORE_PLUGIN_NAME: get_core_plugin()})
            new_objects = cls._get_named_config(config_like, plugins).objects
        elif isinstance(config_like, Config):
            new_objects = config_like.objects
        elif isinstance(config_like, (list, tuple)):
            new_objects = cls.from_config(
                *config_like, value_name=f"{value_name}[{i}]"
            ).objects
        elif config_like:
            new_objects = [
                ConfigObject.from_value(
                    config_like, value_name=f"{value_name}[{i}]"
                )
            ]
        if new_objects:
            for co in new_objects:
                objects.append(co)
                plugins.update(co.plugins if co.plugins else {})
    return cls(objects=objects)
xrlint.config.Config.from_value(value, value_name=None) classmethod

Convert given value into a Config object.

If value is already a Config then it is returned as-is.

Parameters:

  • value (ConfigLike) –

    A configuration-like value. For more information see the ConfigLike type alias.

  • value_name (str | None, default: None ) –

    The value's name used for reporting errors.

Returns: A Config object.

Source code in xrlint\config.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
@classmethod
def from_value(cls, value: ConfigLike, value_name: str | None = None) -> "Config":
    """Convert given `value` into a `Config` object.

    If `value` is already a `Config` then it is returned as-is.

    Args:
        value: A configuration-like value. For more information
            see the [ConfigLike][xrlint.config.ConfigLike] type alias.
        value_name: The value's name used for reporting errors.
    Returns:
        A `Config` object.
    """
    if isinstance(value, (ConfigObject, dict)):
        return Config(objects=[ConfigObject.from_value(value)])
    return super().from_value(value, value_name=value_name)

xrlint.config.ConfigObject dataclass

Bases: MappingConstructible, JsonSerializable

Configuration object.

Configuration objects are the items managed by a configuration.

You should not use the class constructor directly. Instead, use the ConfigObject.from_value() function.

Source code in xrlint\config.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
@dataclass(frozen=True, kw_only=True)
class ConfigObject(MappingConstructible, JsonSerializable):
    """Configuration object.

    Configuration objects are the items managed by a
    [configuration][xrlint.config.Config].

    You should not use the class constructor directly.
    Instead, use the `ConfigObject.from_value()` function.
    """

    name: str | None = None
    """A name for the configuration object.
    This is used in error messages and config inspector to help identify
    which configuration object is being used.
    """

    files: list[str] | None = None
    """An array of glob patterns indicating the files that the
    configuration object should apply to. If not specified,
    the configuration object applies to all files matched
    by any other configuration object.

    When a configuration object contains only the files property
    without accompanying rules or settings, it effectively acts as
    a _global file filter_. This means that XRLint will recognize
    and process only the files matching these patterns, thereby
    limiting its scope to the specified files. The inbuilt
    global file filters are `["**/*.zarr", "**/*.nc"]`.
    """

    ignores: list[str] | None = None
    """An array of glob patterns indicating the files that the
    configuration object should not apply to. If not specified,
    the configuration object applies to all files matched by `files`.
    If `ignores` is used without any other keys in the configuration
    object, then the patterns act as _global ignores_.
    """

    linter_options: dict[str, Any] | None = None
    """A dictionary containing options related to the linting process."""

    opener_options: dict[str, Any] | None = None
    """A dictionary containing options that are passed to
    the dataset opener.
    """

    processor: Union["ProcessorOp", str, None] = None
    """Either an object compatible with the `ProcessorOp`
    interface or a string indicating the name of a processor inside
    of a plugin (i.e., `"pluginName/processorName"`).
    """

    plugins: dict[str, "Plugin"] | None = None
    """A dictionary containing a name-value mapping of plugin names to
    plugin objects. When `files` is specified, these plugins are only
    available to the matching files.
    """

    rules: dict[str, "RuleConfig"] | None = None
    """A dictionary containing the configured rules.
    When `files` or `ignores` are specified, these rule configurations
    are only available to the matching files.
    """

    settings: dict[str, Any] | None = None
    """A dictionary containing name-value pairs of information
    that should be available to all rules.
    """

    @cached_property
    def file_filter(self) -> FileFilter:
        """The file filter specified by this configuration. May be empty."""
        return FileFilter.from_patterns(self.files, self.ignores)

    @cached_property
    def empty(self) -> bool:
        """`True` if this configuration object does not configure anything.
        Note, it could still contribute to a global file filter if its
        `files` and `ignores` options are set."""
        return not (
            self.linter_options
            or self.opener_options
            or self.processor
            or self.plugins
            or self.rules
            or self.settings
        )

    def get_plugin(self, plugin_name: str) -> "Plugin":
        """Get the plugin for given plugin name `plugin_name`."""
        plugin = (self.plugins or {}).get(plugin_name)
        if plugin is None:
            raise ValueError(f"unknown plugin {plugin_name!r}")
        return plugin

    def get_rule(self, rule_id: str) -> "Rule":
        """Get the rule for the given rule identifier `rule_id`.

        Args:
            rule_id: The rule identifier including plugin namespace,
                if any. Format `<rule-name>` (builtin rules) or
                `<plugin-name>/<rule-name>`.

        Returns:
            A `Rule` object.

        Raises:
            ValueError: If either the plugin is unknown in this
                configuration or the rule name is unknown.
        """
        plugin_name, rule_name = split_config_spec(rule_id)
        plugin = self.get_plugin(plugin_name)
        rule = (plugin.rules or {}).get(rule_name)
        if rule is None:
            raise ValueError(f"unknown rule {rule_id!r}")
        return rule

    def get_processor_op(
        self, processor_spec: Union["ProcessorOp", str]
    ) -> "ProcessorOp":
        """Get the processor operation for the given
        processor identifier `processor_spec`.
        """
        from xrlint.processor import Processor, ProcessorOp

        if isinstance(processor_spec, ProcessorOp):
            return processor_spec

        plugin_name, processor_name = split_config_spec(processor_spec)
        plugin = self.get_plugin(plugin_name)
        processor: Processor | None = (plugin.processors or {}).get(processor_name)
        if processor is None:
            raise ValueError(f"unknown processor {processor_spec!r}")
        return processor.op_class()

    def merge(self, config: "ConfigObject", name: str = None) -> "ConfigObject":
        return ConfigObject(
            name=name,
            files=self._merge_pattern_lists(self.files, config.files),
            ignores=self._merge_pattern_lists(self.ignores, config.ignores),
            linter_options=self._merge_options(
                self.linter_options, config.linter_options
            ),
            opener_options=self._merge_options(
                self.opener_options, config.opener_options
            ),
            processor=merge_values(self.processor, config.processor),
            plugins=self._merge_plugin_dicts(self.plugins, config.plugins),
            rules=self._merge_rule_dicts(self.rules, config.rules),
            settings=self._merge_options(self.settings, config.settings),
        )

    @classmethod
    def _merge_rule_dicts(
        cls,
        rules1: dict[str, "RuleConfig"] | None,
        rules2: dict[str, "RuleConfig"] | None,
    ) -> dict[str, "RuleConfig"] | None:
        from xrlint.rule import RuleConfig

        def merge_items(r1: RuleConfig, r2: RuleConfig) -> RuleConfig:
            if r1.severity == r2.severity:
                return RuleConfig(
                    r2.severity,
                    merge_arrays(r1.args, r2.args),
                    merge_dicts(r1.kwargs, r2.kwargs),
                )
            return r2

        return merge_dicts(rules1, rules2, merge_items=merge_items)

    @classmethod
    def _merge_pattern_lists(
        cls, patterns1: list[str] | None, patterns2: list[str] | None
    ) -> list[str] | None:
        return merge_set_lists(patterns1, patterns2)

    @classmethod
    def _merge_options(
        cls, settings1: dict[str, Any] | None, settings2: dict[str, Any] | None
    ) -> dict[str, Any] | None:
        return merge_dicts(settings1, settings2, merge_items=merge_values)

    @classmethod
    def _merge_plugin_dicts(
        cls,
        plugins1: dict[str, "Plugin"] | None,
        plugins2: dict[str, "Plugin"] | None,
    ) -> dict[str, "RuleConfig"] | None:
        from xrlint.plugin import Plugin

        def merge_items(_p1: Plugin, p2: Plugin) -> Plugin:
            return p2

        return merge_dicts(plugins1, plugins2, merge_items=merge_items)

    @classmethod
    def _from_none(cls, value_name: str) -> "ConfigObject":
        return ConfigObject()

    @classmethod
    def forward_refs(cls) -> dict[str, type]:
        from xrlint.plugin import Plugin
        from xrlint.processor import Processor, ProcessorOp
        from xrlint.rule import Rule, RuleConfig

        return {
            "Processor": Processor,
            "ProcessorOp": ProcessorOp,
            "Plugin": Plugin,
            "Rule": Rule,
            "RuleConfig": RuleConfig,
        }

    @classmethod
    def value_name(cls) -> str:
        return "config_obj"

    @classmethod
    def value_type_name(cls) -> str:
        return "ConfigObject | dict | None"

Attributes

xrlint.config.ConfigObject.name = None class-attribute instance-attribute

A name for the configuration object. This is used in error messages and config inspector to help identify which configuration object is being used.

xrlint.config.ConfigObject.files = None class-attribute instance-attribute

An array of glob patterns indicating the files that the configuration object should apply to. If not specified, the configuration object applies to all files matched by any other configuration object.

When a configuration object contains only the files property without accompanying rules or settings, it effectively acts as a global file filter. This means that XRLint will recognize and process only the files matching these patterns, thereby limiting its scope to the specified files. The inbuilt global file filters are ["**/*.zarr", "**/*.nc"].

xrlint.config.ConfigObject.ignores = None class-attribute instance-attribute

An array of glob patterns indicating the files that the configuration object should not apply to. If not specified, the configuration object applies to all files matched by files. If ignores is used without any other keys in the configuration object, then the patterns act as global ignores.

xrlint.config.ConfigObject.linter_options = None class-attribute instance-attribute

A dictionary containing options related to the linting process.

xrlint.config.ConfigObject.opener_options = None class-attribute instance-attribute

A dictionary containing options that are passed to the dataset opener.

xrlint.config.ConfigObject.processor = None class-attribute instance-attribute

Either an object compatible with the ProcessorOp interface or a string indicating the name of a processor inside of a plugin (i.e., "pluginName/processorName").

xrlint.config.ConfigObject.plugins = None class-attribute instance-attribute

A dictionary containing a name-value mapping of plugin names to plugin objects. When files is specified, these plugins are only available to the matching files.

xrlint.config.ConfigObject.rules = None class-attribute instance-attribute

A dictionary containing the configured rules. When files or ignores are specified, these rule configurations are only available to the matching files.

xrlint.config.ConfigObject.settings = None class-attribute instance-attribute

A dictionary containing name-value pairs of information that should be available to all rules.

xrlint.config.ConfigObject.file_filter cached property

The file filter specified by this configuration. May be empty.

xrlint.config.ConfigObject.empty cached property

True if this configuration object does not configure anything. Note, it could still contribute to a global file filter if its files and ignores options are set.

Functions

xrlint.config.ConfigObject.get_plugin(plugin_name)

Get the plugin for given plugin name plugin_name.

Source code in xrlint\config.py
163
164
165
166
167
168
def get_plugin(self, plugin_name: str) -> "Plugin":
    """Get the plugin for given plugin name `plugin_name`."""
    plugin = (self.plugins or {}).get(plugin_name)
    if plugin is None:
        raise ValueError(f"unknown plugin {plugin_name!r}")
    return plugin
xrlint.config.ConfigObject.get_rule(rule_id)

Get the rule for the given rule identifier rule_id.

Parameters:

  • rule_id (str) –

    The rule identifier including plugin namespace, if any. Format <rule-name> (builtin rules) or <plugin-name>/<rule-name>.

Returns:

  • Rule

    A Rule object.

Raises:

  • ValueError

    If either the plugin is unknown in this configuration or the rule name is unknown.

Source code in xrlint\config.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def get_rule(self, rule_id: str) -> "Rule":
    """Get the rule for the given rule identifier `rule_id`.

    Args:
        rule_id: The rule identifier including plugin namespace,
            if any. Format `<rule-name>` (builtin rules) or
            `<plugin-name>/<rule-name>`.

    Returns:
        A `Rule` object.

    Raises:
        ValueError: If either the plugin is unknown in this
            configuration or the rule name is unknown.
    """
    plugin_name, rule_name = split_config_spec(rule_id)
    plugin = self.get_plugin(plugin_name)
    rule = (plugin.rules or {}).get(rule_name)
    if rule is None:
        raise ValueError(f"unknown rule {rule_id!r}")
    return rule
xrlint.config.ConfigObject.get_processor_op(processor_spec)

Get the processor operation for the given processor identifier processor_spec.

Source code in xrlint\config.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def get_processor_op(
    self, processor_spec: Union["ProcessorOp", str]
) -> "ProcessorOp":
    """Get the processor operation for the given
    processor identifier `processor_spec`.
    """
    from xrlint.processor import Processor, ProcessorOp

    if isinstance(processor_spec, ProcessorOp):
        return processor_spec

    plugin_name, processor_name = split_config_spec(processor_spec)
    plugin = self.get_plugin(plugin_name)
    processor: Processor | None = (plugin.processors or {}).get(processor_name)
    if processor is None:
        raise ValueError(f"unknown processor {processor_spec!r}")
    return processor.op_class()

xrlint.config.ConfigLike = Union['Config', ConfigObjectLike, str, Sequence[ConfigObjectLike | str]] module-attribute

Type alias for values that can represent configurations. Can be either a Config instance, or a configuration object like value, or a named plugin configuration, or a sequence of the latter two.

xrlint.config.ConfigObjectLike = Union['ConfigObject', Mapping[str, Any], None] module-attribute

Type alias for values that can represent configuration objects. Can be either a ConfigObject instance, or a mapping (e.g. dict) with properties defined by ConfigObject, or None (empty configuration object).

Rule API

xrlint.rule.define_rule(name=None, version='0.0.0', type='problem', description=None, docs_url=None, schema=None, registry=None, op_class=None)

Define a rule.

This function can be used to decorate your rule operation class definitions. When used as a decorator, the decorated operator class will receive a meta attribute of type RuleMeta. In addition, the registry if given, will be updated using name as key and a new Rule as value.

Parameters:

  • name (str | None, default: None ) –

    Rule name, see RuleMeta.

  • version (str, default: '0.0.0' ) –

    Rule version, see RuleMeta.

  • type (Literal['problem', 'suggestion', 'layout'], default: 'problem' ) –

    Rule type, see RuleMeta.

  • description (str | None, default: None ) –

    Rule description, see RuleMeta.

  • docs_url (str | None, default: None ) –

    Rule documentation URL, see RuleMeta.

  • schema (dict[str, Any] | list[dict[str, Any]] | bool | None, default: None ) –

    Rule operation arguments schema, see RuleMeta.

  • registry (MutableMapping[str, Rule] | None, default: None ) –

    Rule registry. Can be provided to register the defined rule using its name.

  • op_class (Type[RuleOp] | None, default: None ) –

    Rule operation class. Must not be provided if this function is used as a class decorator.

Returns:

  • Callable[[Any], Type[RuleOp]] | Rule

    A decorator function, if op_class is None otherwise the value of op_class.

Raises:

  • TypeError

    If either op_class or the decorated object is not a class derived from RuleOp.

Source code in xrlint\rule.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def define_rule(
    name: str | None = None,
    version: str = "0.0.0",
    type: Literal["problem", "suggestion", "layout"] = "problem",
    description: str | None = None,
    docs_url: str | None = None,
    schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None,
    registry: MutableMapping[str, Rule] | None = None,
    op_class: Type[RuleOp] | None = None,
) -> Callable[[Any], Type[RuleOp]] | Rule:
    """Define a rule.

    This function can be used to decorate your rule operation class
    definitions. When used as a decorator, the decorated operator class
    will receive a `meta` attribute of type [RuleMeta][xrlint.rule.RuleMeta].
    In addition, the `registry` if given, will be updated using `name`
    as key and a new [Rule][xrlint.rule.Rule] as value.

    Args:
        name: Rule name, see [RuleMeta][xrlint.rule.RuleMeta].
        version: Rule version, see [RuleMeta][xrlint.rule.RuleMeta].
        type: Rule type, see [RuleMeta][xrlint.rule.RuleMeta].
        description: Rule description,
            see [RuleMeta][xrlint.rule.RuleMeta].
        docs_url: Rule documentation URL,
            see [RuleMeta][xrlint.rule.RuleMeta].
        schema: Rule operation arguments schema,
            see [RuleMeta][xrlint.rule.RuleMeta].
        registry: Rule registry. Can be provided to register the
            defined rule using its `name`.
        op_class: Rule operation class. Must not be provided
            if this function is used as a class decorator.

    Returns:
        A decorator function, if `op_class` is `None` otherwise
            the value of `op_class`.

    Raises:
        TypeError: If either `op_class` or the decorated object is not
            a class derived from [RuleOp][xrlint.rule.RuleOp].
    """
    return Rule.define_operation(
        op_class,
        registry=registry,
        meta_kwargs=dict(
            name=name,
            version=version,
            description=description,
            docs_url=docs_url,
            type=type if type else "problem",
            schema=schema,
        ),
    )

xrlint.rule.Rule dataclass

Bases: Operation

A rule comprises rule metadata and a reference to the class that implements the rule's logic.

Instances of this class can be easily created and added to a plugin by using the decorator @define_rule of the Plugin class.

Parameters:

  • meta (RuleMeta) –

    the rule's metadata

  • op_class (Type[RuleOp]) –

    the class that implements the rule's logic

Source code in xrlint\rule.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
@dataclass(frozen=True)
class Rule(Operation):
    """A rule comprises rule metadata and a reference to the
    class that implements the rule's logic.

    Instances of this class can be easily created and added to a plugin
    by using the decorator `@define_rule` of the `Plugin` class.

    Args:
        meta: the rule's metadata
        op_class: the class that implements the rule's logic
    """

    meta: RuleMeta
    """Rule metadata of type `RuleMeta`."""

    op_class: Type[RuleOp]
    """The class the implements the rule's validation operation.
    The class must implement the `RuleOp` interface.
    """

    @classmethod
    def meta_class(cls) -> Type:
        return RuleMeta

    @classmethod
    def op_base_class(cls) -> Type:
        return RuleOp

    @classmethod
    def value_name(cls) -> str:
        return "rule"

Attributes

xrlint.rule.Rule.meta instance-attribute

Rule metadata of type RuleMeta.

xrlint.rule.Rule.op_class instance-attribute

The class the implements the rule's validation operation. The class must implement the RuleOp interface.

xrlint.rule.RuleMeta dataclass

Bases: OperationMeta

Rule metadata.

Source code in xrlint\rule.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
@dataclass(kw_only=True)
class RuleMeta(OperationMeta):
    """Rule metadata."""

    name: str
    """Rule name. Mandatory."""

    version: str = "0.0.0"
    """Rule version. Defaults to `0.0.0`."""

    description: str | None = None
    """Rule description."""

    docs_url: str | None = None
    """Rule documentation URL."""

    schema: dict[str, Any] | list[dict[str, Any]] | bool | None = None
    """JSON Schema used to specify and validate the rule operation
    options.

    It can take the following values:

    - Use `None` (the default) to indicate that the rule operation
      as no options at all.
    - Use a schema to indicate that the rule operation
      takes keyword arguments only.
      The schema's type must be `"object"`.
    - Use a list of schemas to indicate that the rule operation
      takes positional arguments only.
      If given, the number of schemas in the list specifies the
      number of positional arguments that must be configured.
    """

    type: Literal["problem", "suggestion", "layout"] = "problem"
    """Rule type. Defaults to `"problem"`.

    The type field can have one of the following values:

    - `"problem"`: Indicates that the rule addresses datasets that are
      likely to cause errors or unexpected behavior during runtime.
      These issues usually represent real bugs or potential runtime problems.
    - `"suggestion"`: Used for rules that suggest structural improvements
      or enforce best practices. These issues are not necessarily bugs, but
      following the suggestions may lead to more readable, maintainable, or
      consistent datasets.
    - `"layout"`: Specifies that the rule enforces consistent stylistic
      aspects of dataset formatting, e.g., whitespaces in names.
      Issues with layout rules are often automatically fixable
      (not supported yet).

    Primarily serves to categorize the rule's purpose for the benefit
    of developers and tools that consume XRLint output.
    It doesn’t directly affect the linting logic - that part is handled
    by the rule’s implementation and its configured severity.
    """

    @classmethod
    def value_name(cls) -> str:
        return "rule_meta"

    @classmethod
    def value_type_name(cls) -> str:
        return "RuleMeta | dict"

Attributes

xrlint.rule.RuleMeta.name instance-attribute

Rule name. Mandatory.

xrlint.rule.RuleMeta.version = '0.0.0' class-attribute instance-attribute

Rule version. Defaults to 0.0.0.

xrlint.rule.RuleMeta.description = None class-attribute instance-attribute

Rule description.

xrlint.rule.RuleMeta.docs_url = None class-attribute instance-attribute

Rule documentation URL.

xrlint.rule.RuleMeta.schema = None class-attribute instance-attribute

JSON Schema used to specify and validate the rule operation options.

It can take the following values:

  • Use None (the default) to indicate that the rule operation as no options at all.
  • Use a schema to indicate that the rule operation takes keyword arguments only. The schema's type must be "object".
  • Use a list of schemas to indicate that the rule operation takes positional arguments only. If given, the number of schemas in the list specifies the number of positional arguments that must be configured.
xrlint.rule.RuleMeta.type = 'problem' class-attribute instance-attribute

Rule type. Defaults to "problem".

The type field can have one of the following values:

  • "problem": Indicates that the rule addresses datasets that are likely to cause errors or unexpected behavior during runtime. These issues usually represent real bugs or potential runtime problems.
  • "suggestion": Used for rules that suggest structural improvements or enforce best practices. These issues are not necessarily bugs, but following the suggestions may lead to more readable, maintainable, or consistent datasets.
  • "layout": Specifies that the rule enforces consistent stylistic aspects of dataset formatting, e.g., whitespaces in names. Issues with layout rules are often automatically fixable (not supported yet).

Primarily serves to categorize the rule's purpose for the benefit of developers and tools that consume XRLint output. It doesn’t directly affect the linting logic - that part is handled by the rule’s implementation and its configured severity.

xrlint.rule.RuleOp

Bases: ABC

Define the specific rule validation operations.

Source code in xrlint\rule.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class RuleOp(ABC):
    """Define the specific rule validation operations."""

    def validate_datatree(self, ctx: RuleContext, node: DataTreeNode) -> None:
        """Validate the given datatree node.

        Args:
            ctx: The current rule context.
            node: The datatree node.

        Raises:
            RuleExit: to exit rule logic and further node traversal
        """

    def validate_dataset(self, ctx: RuleContext, node: DatasetNode) -> None:
        """Validate the given dataset node.

        Args:
            ctx: The current rule context.
            node: The dataset node.

        Raises:
            RuleExit: to exit rule logic and further node traversal
        """

    def validate_variable(self, ctx: RuleContext, node: VariableNode) -> None:
        """Validate the given data array (variable) node.

        Args:
            ctx: The current rule context.
            node: The data array (variable) node.

        Raises:
            RuleExit: to exit rule logic and further node traversal
        """

    def validate_attrs(self, ctx: RuleContext, node: AttrsNode) -> None:
        """Validate the given attributes node.

        Args:
            ctx: The current rule context.
            node: The attributes node.

        Raises:
            RuleExit: to exit rule logic and further node traversal
        """

    def validate_attr(self, ctx: RuleContext, node: AttrNode) -> None:
        """Validate the given attribute node.

        Args:
            ctx: The current rule context.
            node: The attribute node.

        Raises:
            RuleExit: to exit rule logic and further node traversal
        """

Functions

xrlint.rule.RuleOp.validate_datatree(ctx, node)

Validate the given datatree node.

Parameters:

Raises:

  • RuleExit

    to exit rule logic and further node traversal

Source code in xrlint\rule.py
89
90
91
92
93
94
95
96
97
98
def validate_datatree(self, ctx: RuleContext, node: DataTreeNode) -> None:
    """Validate the given datatree node.

    Args:
        ctx: The current rule context.
        node: The datatree node.

    Raises:
        RuleExit: to exit rule logic and further node traversal
    """
xrlint.rule.RuleOp.validate_dataset(ctx, node)

Validate the given dataset node.

Parameters:

Raises:

  • RuleExit

    to exit rule logic and further node traversal

Source code in xrlint\rule.py
100
101
102
103
104
105
106
107
108
109
def validate_dataset(self, ctx: RuleContext, node: DatasetNode) -> None:
    """Validate the given dataset node.

    Args:
        ctx: The current rule context.
        node: The dataset node.

    Raises:
        RuleExit: to exit rule logic and further node traversal
    """
xrlint.rule.RuleOp.validate_variable(ctx, node)

Validate the given data array (variable) node.

Parameters:

Raises:

  • RuleExit

    to exit rule logic and further node traversal

Source code in xrlint\rule.py
111
112
113
114
115
116
117
118
119
120
def validate_variable(self, ctx: RuleContext, node: VariableNode) -> None:
    """Validate the given data array (variable) node.

    Args:
        ctx: The current rule context.
        node: The data array (variable) node.

    Raises:
        RuleExit: to exit rule logic and further node traversal
    """
xrlint.rule.RuleOp.validate_attrs(ctx, node)

Validate the given attributes node.

Parameters:

Raises:

  • RuleExit

    to exit rule logic and further node traversal

Source code in xrlint\rule.py
122
123
124
125
126
127
128
129
130
131
def validate_attrs(self, ctx: RuleContext, node: AttrsNode) -> None:
    """Validate the given attributes node.

    Args:
        ctx: The current rule context.
        node: The attributes node.

    Raises:
        RuleExit: to exit rule logic and further node traversal
    """
xrlint.rule.RuleOp.validate_attr(ctx, node)

Validate the given attribute node.

Parameters:

Raises:

  • RuleExit

    to exit rule logic and further node traversal

Source code in xrlint\rule.py
133
134
135
136
137
138
139
140
141
142
def validate_attr(self, ctx: RuleContext, node: AttrNode) -> None:
    """Validate the given attribute node.

    Args:
        ctx: The current rule context.
        node: The attribute node.

    Raises:
        RuleExit: to exit rule logic and further node traversal
    """

xrlint.rule.RuleContext

Bases: ABC

The context passed to a RuleOp instance.

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.

Source code in xrlint\rule.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class RuleContext(ABC):
    """The context passed to a [RuleOp][xrlint.rule.RuleOp] instance.

    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 file_path(self) -> str:
        """The current dataset's file path."""

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

    @property
    @abstractmethod
    def dataset(self) -> xr.Dataset:
        """The current dataset."""

    @property
    @abstractmethod
    def access_latency(self) -> float | None:
        """The time in seconds that it took for opening the dataset.
        `None` if the dataset has not been opened from `file_path`.
        """

    @abstractmethod
    def report(
        self,
        message: str,
        *,
        fatal: bool | None = None,
        suggestions: list[Suggestion | str] | None = None,
    ):
        """Report an issue.

        Args:
            message: mandatory message text
            fatal: True, if a fatal error is reported.
            suggestions: A list of suggestions for the user
                on how to fix the reported issue. Items may
                be of type `Suggestion` or `str`.
        """

Attributes

xrlint.rule.RuleContext.file_path abstractmethod property

The current dataset's file path.

xrlint.rule.RuleContext.settings abstractmethod property

Applicable subset of settings from configuration settings.

xrlint.rule.RuleContext.dataset abstractmethod property

The current dataset.

xrlint.rule.RuleContext.access_latency abstractmethod property

The time in seconds that it took for opening the dataset. None if the dataset has not been opened from file_path.

Functions

xrlint.rule.RuleContext.report(message, *, fatal=None, suggestions=None) abstractmethod

Report an issue.

Parameters:

  • message (str) –

    mandatory message text

  • fatal (bool | None, default: None ) –

    True, if a fatal error is reported.

  • suggestions (list[Suggestion | str] | None, default: None ) –

    A list of suggestions for the user on how to fix the reported issue. Items may be of type Suggestion or str.

Source code in xrlint\rule.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@abstractmethod
def report(
    self,
    message: str,
    *,
    fatal: bool | None = None,
    suggestions: list[Suggestion | str] | None = None,
):
    """Report an issue.

    Args:
        message: mandatory message text
        fatal: True, if a fatal error is reported.
        suggestions: A list of suggestions for the user
            on how to fix the reported issue. Items may
            be of type `Suggestion` or `str`.
    """

xrlint.rule.RuleExit

Bases: 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:

if something_is_not_ok:
    ctx.report("Something is not ok.")
    raise RuleExit
Source code in xrlint\rule.py
71
72
73
74
75
76
77
78
79
80
81
82
83
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
    ```
    """

Dataset Node API

xrlint.node.Node dataclass

Bases: ABC

Abstract base class for nodes passed to the methods of a rule operation xrlint.rule.RuleOp.

Source code in xrlint\node.py
12
13
14
15
16
17
18
19
20
21
@dataclass(frozen=True, kw_only=True)
class Node(ABC):
    """Abstract base class for nodes passed to the methods of a
    rule operation [xrlint.rule.RuleOp][]."""

    path: str
    """Node path. So users find where in the tree the issue occurred."""

    parent: Union["Node", None]
    """Node parent. `None` for root nodes."""

Attributes

xrlint.node.Node.path instance-attribute

Node path. So users find where in the tree the issue occurred.

xrlint.node.Node.parent instance-attribute

Node parent. None for root nodes.

xrlint.node.XarrayNode dataclass

Bases: Node

Base class for xr.Dataset nodes.

Source code in xrlint\node.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@dataclass(frozen=True, kw_only=True)
class XarrayNode(Node):
    """Base class for `xr.Dataset` nodes."""

    def in_coords(self) -> bool:
        """Return `True` if this node is in `xr.Dataset.coords`."""
        return ".coords[" in self.path

    def in_data_vars(self) -> bool:
        """Return `True` if this node is a `xr.Dataset.data_vars`."""
        return ".data_vars[" in self.path

    def in_root(self) -> bool:
        """Return `True` if this node is a direct child of the dataset."""
        return not self.in_coords() and not self.in_data_vars()

Functions

xrlint.node.XarrayNode.in_coords()

Return True if this node is in xr.Dataset.coords.

Source code in xrlint\node.py
28
29
30
def in_coords(self) -> bool:
    """Return `True` if this node is in `xr.Dataset.coords`."""
    return ".coords[" in self.path
xrlint.node.XarrayNode.in_data_vars()

Return True if this node is a xr.Dataset.data_vars.

Source code in xrlint\node.py
32
33
34
def in_data_vars(self) -> bool:
    """Return `True` if this node is a `xr.Dataset.data_vars`."""
    return ".data_vars[" in self.path
xrlint.node.XarrayNode.in_root()

Return True if this node is a direct child of the dataset.

Source code in xrlint\node.py
36
37
38
def in_root(self) -> bool:
    """Return `True` if this node is a direct child of the dataset."""
    return not self.in_coords() and not self.in_data_vars()

xrlint.node.DataTreeNode dataclass

Bases: XarrayNode

DataTree node.

Source code in xrlint\node.py
41
42
43
44
45
46
47
48
49
@dataclass(frozen=True, kw_only=True)
class DataTreeNode(XarrayNode):
    """DataTree node."""

    name: Hashable
    """The name of the datatree."""

    datatree: xr.DataTree
    """The `xarray.DataTree` instance."""

Attributes

xrlint.node.DataTreeNode.name instance-attribute

The name of the datatree.

xrlint.node.DataTreeNode.datatree instance-attribute

The xarray.DataTree instance.

xrlint.node.DatasetNode dataclass

Bases: XarrayNode

Dataset node.

Source code in xrlint\node.py
52
53
54
55
56
57
58
59
60
@dataclass(frozen=True, kw_only=True)
class DatasetNode(XarrayNode):
    """Dataset node."""

    name: Hashable
    """The name of the dataset."""

    dataset: xr.Dataset
    """The `xarray.Dataset` instance."""

Attributes

xrlint.node.DatasetNode.name instance-attribute

The name of the dataset.

xrlint.node.DatasetNode.dataset instance-attribute

The xarray.Dataset instance.

xrlint.node.VariableNode dataclass

Bases: XarrayNode

Variable node. Could be a coordinate or data variable. If you need to distinguish, you can use expression node.name in ctx.dataset.coords.

Source code in xrlint\node.py
63
64
65
66
67
68
69
70
71
72
73
74
75
@dataclass(frozen=True, kw_only=True)
class VariableNode(XarrayNode):
    """Variable node.
    Could be a coordinate or data variable.
    If you need to distinguish, you can use expression
    `node.name in ctx.dataset.coords`.
    """

    name: Hashable
    """The name of the variable."""

    array: xr.DataArray
    """The `xarray.DataArray` instance."""

Attributes

xrlint.node.VariableNode.name instance-attribute

The name of the variable.

xrlint.node.VariableNode.array instance-attribute

The xarray.DataArray instance.

xrlint.node.AttrsNode dataclass

Bases: XarrayNode

Attributes node.

Source code in xrlint\node.py
78
79
80
81
82
83
@dataclass(frozen=True, kw_only=True)
class AttrsNode(XarrayNode):
    """Attributes node."""

    attrs: dict[str, Any]
    """Attributes dictionary."""

Attributes

xrlint.node.AttrsNode.attrs instance-attribute

Attributes dictionary.

xrlint.node.AttrNode dataclass

Bases: XarrayNode

Attribute node.

Source code in xrlint\node.py
86
87
88
89
90
91
92
93
94
@dataclass(frozen=True, kw_only=True)
class AttrNode(XarrayNode):
    """Attribute node."""

    name: str
    """Attribute name."""

    value: Any
    """Attribute value."""

Attributes

xrlint.node.AttrNode.name instance-attribute

Attribute name.

xrlint.node.AttrNode.value instance-attribute

Attribute value.

Processor API

xrlint.processor.define_processor(name=None, version='0.0.0', registry=None, op_class=None)

Define a processor.

This function can be used to decorate your processor operation class definitions. When used as a decorator, the decorated operator class will receive a meta attribute of type ProcessorMeta. In addition, the registry if given, will be updated using name as key and a new Processor as value.

Parameters:

  • name (str | None, default: None ) –

    Processor name, see ProcessorMeta.

  • version (str, default: '0.0.0' ) –

    Processor version, see ProcessorMeta.

  • registry (dict[str, Processor] | None, default: None ) –

    Processor registry. Can be provided to register the defined processor using its name.

  • op_class (Type[ProcessorOp] | None, default: None ) –

    Processor operation class. Must be None if this function is used as a class decorator.

Returns:

  • Callable[[Any], Type[ProcessorOp]] | Processor

    A decorator function, if op_class is None otherwise the value of op_class.

Raises:

  • TypeError

    If either op_class or the decorated object is not a a class derived from ProcessorOp.

Source code in xrlint\processor.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def define_processor(
    name: str | None = None,
    version: str = "0.0.0",
    registry: dict[str, Processor] | None = None,
    op_class: Type[ProcessorOp] | None = None,
) -> Callable[[Any], Type[ProcessorOp]] | Processor:
    """Define a processor.

    This function can be used to decorate your processor operation class
    definitions. When used as a decorator, the decorated operator class
    will receive a `meta` attribute of type
    [ProcessorMeta][xrlint.processor.ProcessorMeta].
    In addition, the `registry` if given, will be updated using `name`
    as key and a new [Processor][xrlint.processor.Processor] as value.

    Args:
        name: Processor name,
            see [ProcessorMeta][xrlint.processor.ProcessorMeta].
        version: Processor version,
            see [ProcessorMeta][xrlint.processor.ProcessorMeta].
        registry: Processor registry. Can be provided to register the
            defined processor using its `name`.
        op_class: Processor operation class. Must be `None`
            if this function is used as a class decorator.

    Returns:
        A decorator function, if `op_class` is `None` otherwise
            the value of `op_class`.

    Raises:
        TypeError: If either `op_class` or the decorated object is not a
            a class derived from [ProcessorOp][xrlint.processor.ProcessorOp].
    """
    return Processor.define_operation(
        op_class, registry=registry, meta_kwargs=dict(name=name, version=version)
    )

xrlint.processor.Processor dataclass

Bases: Operation

Processors tell XRLint how to process files other than standard xarray datasets.

Source code in xrlint\processor.py
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
@dataclass(frozen=True, kw_only=True)
class Processor(Operation):
    """Processors tell XRLint how to process files other than
    standard xarray datasets.
    """

    meta: ProcessorMeta
    """Information about the processor."""

    op_class: Type[ProcessorOp]
    """A class that implements the processor operations."""

    # Not yet:
    # supports_auto_fix: bool = False
    # """`True` if this processor supports auto-fixing of datasets."""

    @classmethod
    def meta_class(cls) -> Type:
        return ProcessorMeta

    @classmethod
    def op_base_class(cls) -> Type:
        return ProcessorOp

    @classmethod
    def value_name(cls) -> str:
        return "processor"

Attributes

xrlint.processor.Processor.meta instance-attribute

Information about the processor.

xrlint.processor.Processor.op_class instance-attribute

A class that implements the processor operations.

xrlint.processor.ProcessorMeta dataclass

Bases: OperationMeta

Processor metadata.

Source code in xrlint\processor.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@dataclass(kw_only=True)
class ProcessorMeta(OperationMeta):
    """Processor metadata."""

    name: str
    """Processor name."""

    version: str = "0.0.0"
    """Processor version."""

    """Processor description. Optional."""
    description: str | None = None

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

    @classmethod
    def value_name(cls) -> str:
        return "processor_meta"

    @classmethod
    def value_type_name(cls) -> str:
        return "ProcessorMeta | dict"

Attributes

xrlint.processor.ProcessorMeta.name instance-attribute

Processor name.

xrlint.processor.ProcessorMeta.version = '0.0.0' class-attribute instance-attribute

Processor version.

xrlint.processor.ProcessorMeta.ref = None class-attribute instance-attribute

Processor reference. Specifies the location from where the processor can be dynamically imported. Must have the form ":", if given.

xrlint.processor.ProcessorOp

Bases: ABC

Implements the processor operations.

Source code in xrlint\processor.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class ProcessorOp(ABC):
    """Implements the processor operations."""

    @abstractmethod
    def preprocess(
        self, file_path: str, opener_options: dict[str, Any]
    ) -> list[tuple[xr.Dataset | xr.DataTree, str]]:
        """Pre-process a dataset given by its `file_path` and `opener_options`.
        In this method you use the `file_path` to read zero, one, or more
        datasets to lint.

        Args:
            file_path: A file path
            opener_options: The configuration's `opener_options`.

        Returns:
            A list of (dataset or datatree, file_path) pairs
        """

    @abstractmethod
    def postprocess(
        self, messages: list[list[Message]], file_path: str
    ) -> list[Message]:
        """Post-process the outputs of each dataset from `preprocess()`.

        Args:
            messages: contains two-dimensional array of ´Message´ objects
                where each top-level array item contains array of lint messages
                related to the dataset that was returned in array from
                `preprocess()` method
            file_path: The corresponding file path

        Returns:
            A one-dimensional array (list) of the messages you want to keep
        """

Functions

xrlint.processor.ProcessorOp.preprocess(file_path, opener_options) abstractmethod

Pre-process a dataset given by its file_path and opener_options. In this method you use the file_path to read zero, one, or more datasets to lint.

Parameters:

  • file_path (str) –

    A file path

  • opener_options (dict[str, Any]) –

    The configuration's opener_options.

Returns:

  • list[tuple[Dataset | DataTree, str]]

    A list of (dataset or datatree, file_path) pairs

Source code in xrlint\processor.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@abstractmethod
def preprocess(
    self, file_path: str, opener_options: dict[str, Any]
) -> list[tuple[xr.Dataset | xr.DataTree, str]]:
    """Pre-process a dataset given by its `file_path` and `opener_options`.
    In this method you use the `file_path` to read zero, one, or more
    datasets to lint.

    Args:
        file_path: A file path
        opener_options: The configuration's `opener_options`.

    Returns:
        A list of (dataset or datatree, file_path) pairs
    """
xrlint.processor.ProcessorOp.postprocess(messages, file_path) abstractmethod

Post-process the outputs of each dataset from preprocess().

Parameters:

  • messages (list[list[Message]]) –

    contains two-dimensional array of ´Message´ objects where each top-level array item contains array of lint messages related to the dataset that was returned in array from preprocess() method

  • file_path (str) –

    The corresponding file path

Returns:

  • list[Message]

    A one-dimensional array (list) of the messages you want to keep

Source code in xrlint\processor.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@abstractmethod
def postprocess(
    self, messages: list[list[Message]], file_path: str
) -> list[Message]:
    """Post-process the outputs of each dataset from `preprocess()`.

    Args:
        messages: contains two-dimensional array of ´Message´ objects
            where each top-level array item contains array of lint messages
            related to the dataset that was returned in array from
            `preprocess()` method
        file_path: The corresponding file path

    Returns:
        A one-dimensional array (list) of the messages you want to keep
    """

Result API

xrlint.result.Result dataclass

Bases: JsonSerializable

The aggregated information of linting a dataset.

Source code in xrlint\result.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
@dataclass(kw_only=True, frozen=True)
class Result(JsonSerializable):
    """The aggregated information of linting a dataset."""

    file_path: str
    """The absolute path to the file of this result.
    This is the string "<dataset>" if the file path is unknown
    (when you didn't pass the `file_path` option to the
    `xrlint.lint_dataset()` method).
    """

    config_object: Union["ConfigObject", None] = None
    """The configuration object that produced this result 
    together with `file_path`.
    """

    messages: list[Message] = field(default_factory=list)
    """The array of message objects."""

    @cached_property
    def warning_count(self) -> int:
        """The number of warnings. This includes fixable warnings."""
        return sum(1 if m.severity == SEVERITY_WARN else 0 for m in self.messages)

    @cached_property
    def error_count(self) -> int:
        """The number of errors. This includes fixable errors
        and fatal errors.
        """
        return sum(1 if m.severity == SEVERITY_ERROR else 0 for m in self.messages)

    @cached_property
    def fatal_error_count(self) -> int:
        """The number of fatal errors."""
        return sum(1 if m.fatal else 0 for m in self.messages)

    def to_html(self) -> str:
        from xrlint.formatters.html import format_result

        return "\n".join(format_result(self))

    def _repr_html_(self) -> str:
        return self.to_html()

    def get_docs_url_for_rule(self, rule_id: str) -> str | None:
        from xrlint.config import split_config_spec

        plugin_name, rule_name = split_config_spec(rule_id)
        if plugin_name == CORE_PLUGIN_NAME or plugin_name == "xcube":
            return f"{CORE_DOCS_URL}#{rule_name}"
        try:
            plugin = self.config_object.get_plugin(plugin_name)
            rule = self.config_object.get_rule(rule_id)
            return rule.meta.docs_url or plugin.meta.docs_url
        except ValueError:
            return None

Attributes

xrlint.result.Result.file_path instance-attribute

The absolute path to the file of this result. This is the string "" if the file path is unknown (when you didn't pass the file_path option to the xrlint.lint_dataset() method).

xrlint.result.Result.config_object = None class-attribute instance-attribute

The configuration object that produced this result together with file_path.

xrlint.result.Result.messages = field(default_factory=list) class-attribute instance-attribute

The array of message objects.

xrlint.result.Result.warning_count cached property

The number of warnings. This includes fixable warnings.

xrlint.result.Result.error_count cached property

The number of errors. This includes fixable errors and fatal errors.

xrlint.result.Result.fatal_error_count cached property

The number of fatal errors.

xrlint.result.Message dataclass

Bases: JsonSerializable

Source code in xrlint\result.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@dataclass()
class Message(JsonSerializable):
    message: str
    """The error message."""

    node_path: str | None = None
    """Node path within the dataset.
    This property is None if the message does not
    apply to a certain dataset node.
    """

    rule_id: str | None = None
    """The rule name that generated this lint message.
    If this message is generated by the xrlint core
    rather than rules, this is None.
    """

    severity: Literal[1, 2] | None = None
    """The severity of this message.
    `1` means warning and `2` means error.
    """

    fatal: bool | None = None
    """True if this is a fatal error unrelated to a rule,
    like a parsing error.
    """

    fix: EditInfo | None = None
    """The EditInfo object of auto-fix.
    This property is None if this
    message is not fixable.

    Not used yet.
    """

    suggestions: list[Suggestion] | None = None
    """The list of suggestions. Each suggestion is the pair
    of a description and an EditInfo object to fix the dataset.
    API users such as editor integrations can choose one of them
    to fix the problem of this message.
    This property is None if this message does not have any suggestions.
    """

Attributes

xrlint.result.Message.message instance-attribute

The error message.

xrlint.result.Message.node_path = None class-attribute instance-attribute

Node path within the dataset. This property is None if the message does not apply to a certain dataset node.

xrlint.result.Message.rule_id = None class-attribute instance-attribute

The rule name that generated this lint message. If this message is generated by the xrlint core rather than rules, this is None.

xrlint.result.Message.severity = None class-attribute instance-attribute

The severity of this message. 1 means warning and 2 means error.

xrlint.result.Message.fatal = None class-attribute instance-attribute

True if this is a fatal error unrelated to a rule, like a parsing error.

xrlint.result.Message.fix = None class-attribute instance-attribute

The EditInfo object of auto-fix. This property is None if this message is not fixable.

Not used yet.

xrlint.result.Message.suggestions = None class-attribute instance-attribute

The list of suggestions. Each suggestion is the pair of a description and an EditInfo object to fix the dataset. API users such as editor integrations can choose one of them to fix the problem of this message. This property is None if this message does not have any suggestions.

xrlint.result.Suggestion dataclass

Bases: ValueConstructible, JsonSerializable

Source code in xrlint\result.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@dataclass(frozen=True)
class Suggestion(ValueConstructible, JsonSerializable):
    desc: str
    """Description of the suggestion."""

    data: dict[str, None] | None = None
    """Data that can be referenced in the description."""

    fix: EditInfo | None = None
    """Not used yet."""

    @classmethod
    def _from_str(cls, value: str, value_name: str | None = None) -> "Suggestion":
        return Suggestion(value)

Attributes

xrlint.result.Suggestion.desc instance-attribute

Description of the suggestion.

xrlint.result.Suggestion.data = None class-attribute instance-attribute

Data that can be referenced in the description.

xrlint.result.Suggestion.fix = None class-attribute instance-attribute

Not used yet.

Testing API

xrlint.testing.RuleTester

Utility that helps testing rules.

Parameters:

  • config (ConfigLike, default: None ) –

    Optional configuration-like value. For more information see the ConfigLike type alias.

  • **config_props (Any, default: {} ) –

    Individual configuration object properties. For more information refer to the properties of a ConfigObject.

Source code in xrlint\testing.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
class RuleTester:
    """Utility that helps testing rules.

    Args:
        config: Optional configuration-like value.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
        **config_props: Individual configuration object properties.
            For more information refer to the properties of a
            [ConfigObject][xrlint.config.ConfigObject].
    """

    def __init__(self, *, config: ConfigLike = None, **config_props: Any):
        self._config = config
        self._config_props = config_props

    def run(
        self,
        rule_name: str,
        rule_op_class: Type[RuleOp],
        *,
        valid: list[RuleTest] | None = None,
        invalid: list[RuleTest] | None = None,
    ):
        """Run the given tests in `valid` and `invalid`
        against the given rule.

        Args:
            rule_name: the rule's name
            rule_op_class: a class derived from `RuleOp`
            valid: list of tests that expect no reported problems
            invalid: list of tests that expect reported problems

        Raises:
            AssertionError: if one of the checks fails
        """
        tests = self._create_tests(
            rule_name, rule_op_class, valid=valid, invalid=invalid
        )
        for test in tests.values():
            print(f"Rule {rule_name!r}: running {test.__name__}()...")
            # noinspection PyTypeChecker
            test(None)

    @classmethod
    def define_test(
        cls,
        rule_name: str,
        rule_op_class: Type[RuleOp],
        *,
        valid: list[RuleTest] | None = None,
        invalid: list[RuleTest] | None = None,
        config: ConfigLike = None,
        **config_props: Any,
    ) -> Type[unittest.TestCase]:
        """Create a `unittest.TestCase` class for the given rule and tests.

        The returned class is derived from `unittest.TestCase`
        and contains a test method for each of the tests in
        `valid` and `invalid`.

        Args:
            rule_name: the rule's name
            rule_op_class: the class derived from `RuleOp`
            valid: list of tests that expect no reported problems
            invalid: list of tests that expect reported problems
            config: Optional configuration-like value.
                For more information see the
                [ConfigLike][xrlint.config.ConfigLike] type alias.
            **config_props: Individual configuration object properties.
                For more information refer to the properties of a
                [ConfigObject][xrlint.config.ConfigObject].

        Returns:
            A new class derived from `unittest.TestCase`.
        """
        tester = RuleTester(config=config, **config_props)
        tests = tester._create_tests(
            rule_name, rule_op_class, valid=valid, invalid=invalid
        )
        # noinspection PyTypeChecker
        return type(f"{rule_op_class.__name__}Test", (unittest.TestCase,), tests)

    def _create_tests(
        self,
        rule_name: str,
        rule_op_class: Type[RuleOp],
        valid: list[RuleTest] | None,
        invalid: list[RuleTest] | None,
    ) -> dict[str, Callable[[unittest.TestCase | None], None]]:
        if hasattr(rule_op_class, "meta"):
            tests = dict([self._create_name_test(rule_name, rule_op_class)])
        else:
            tests = {}

        def make_args(checks: list[RuleTest] | None, mode: Literal["valid", "invalid"]):
            return [(check, index, mode) for index, check in enumerate(checks or [])]

        tests |= dict(
            self._create_test(rule_name, rule_op_class, *args)
            for args in make_args(valid, "valid") + make_args(invalid, "invalid")
        )
        return tests

    # noinspection PyMethodMayBeStatic
    def _create_name_test(
        self,
        rule_name: str,
        rule_op_class: Type[RuleOp],
    ) -> tuple[str, Callable]:
        test_id = "test_rule_meta"

        def test_fn(_self: unittest.TestCase):
            rule_meta: RuleMeta = getattr(rule_op_class, "meta")
            assert rule_meta.name == rule_name, (
                f"rule name expected to be {rule_name!r}, but was {rule_meta.name!r}"
            )

        test_fn.__name__ = test_id
        return test_id, test_fn

    def _create_test(
        self,
        rule_name: str,
        rule_op_class: Type[RuleOp],
        test: RuleTest,
        test_index: int,
        test_mode: Literal["valid", "invalid"],
    ) -> tuple[str, Callable]:
        test_id = _format_test_id(test, test_index, test_mode)

        def test_fn(_self: unittest.TestCase):
            error_message = self._test_rule(
                rule_name, rule_op_class, test, test_id, test_mode
            )
            if error_message:
                raise AssertionError(error_message)

        test_fn.__name__ = test_id
        return test_id, test_fn

    def _test_rule(
        self,
        rule_name: str,
        rule_op_class: Type[RuleOp],
        test: RuleTest,
        test_id: str,
        test_mode: Literal["valid", "invalid"],
    ) -> str | None:
        # Note, the rule's code cannot and should not depend
        # on the currently configured severity.
        # There is also no way for a rule to obtain the severity.
        severity = SEVERITY_ERROR
        linter = Linter(self._config, self._config_props)
        result = linter.validate(
            test.dataset,
            plugins={
                _PLUGIN_NAME: (
                    new_plugin(
                        name=_PLUGIN_NAME,
                        rules={
                            rule_name: Rule(
                                meta=RuleMeta(name=rule_name),
                                op_class=rule_op_class,
                            )
                        },
                    )
                )
            },
            rules={
                f"{_PLUGIN_NAME}/{rule_name}": (
                    [severity, *(test.args or ()), (test.kwargs or {})]
                    if test.args or test.kwargs
                    else severity
                )
            },
        )

        result_message_count = len(result.messages)

        expected = test.expected
        expected_message_count = 0
        expected_messages = None

        if test_mode == "valid":
            assert expected is None, (
                f"{test_id}: you cannot provide the keyword argument"
                f" `expected` for a RuleTest in 'valid' mode."
            )
        else:
            if isinstance(expected, int):
                expected_message_count = max(1, expected)
                expected_messages = None
            elif isinstance(expected, list):
                expected_message_count = len(expected)
                expected_messages = expected
            assert expected_message_count > 0, (
                f"{test_id}: you must provide a valid keyword argument"
                f" `expected` for a RuleTest in 'invalid' mode. Pass a list"
                f" of expected message or str objects or an int specifying"
                f" the expected number of messages."
            )

        lines: list[str] = []
        if result_message_count == expected_message_count:
            if expected_messages is None:
                return None
            all_ok = True
            texts = map(_get_message_text, expected_messages)
            result_texts = map(_get_message_text, result.messages)
            for i, (expected_text, result_text) in enumerate(zip(texts, result_texts)):
                if expected_text != result_text:
                    all_ok = False
                    lines.append(f"Message {i}:")
                    lines.append(f"  Expected: {expected_text}")
                    lines.append(f"  Actual: {result_text}")
            if all_ok:
                return None

        else:
            if expected_messages:
                lines.append(
                    f"Expected {format_item(expected_message_count, 'message')}:"
                )
                for i, text in enumerate(map(_get_message_text, expected_messages)):
                    lines.append(f"  {i}: {text}")
            if result.messages:
                lines.append(f"Actual {format_item(result_message_count, 'message')}:")
                for i, text in enumerate(map(_get_message_text, result.messages)):
                    lines.append(f"  {i}: {text}")

        result_text = format_problems(result.error_count, result.warning_count)
        if expected_message_count == result_message_count:
            problem_text = (
                f"got {result_text} as expected, but encountered message mismatch"
            )
        else:
            expected_text = format_count(expected_message_count, "problem")
            problem_text = f"expected {expected_text}, but got {result_text}"

        messages_text = "\n".join(lines)
        messages_text = (":\n" + messages_text) if messages_text else "."
        return f"Rule {rule_name!r}: {test_id}: {problem_text}{messages_text}"

Functions

xrlint.testing.RuleTester.run(rule_name, rule_op_class, *, valid=None, invalid=None)

Run the given tests in valid and invalid against the given rule.

Parameters:

  • rule_name (str) –

    the rule's name

  • rule_op_class (Type[RuleOp]) –

    a class derived from RuleOp

  • valid (list[RuleTest] | None, default: None ) –

    list of tests that expect no reported problems

  • invalid (list[RuleTest] | None, default: None ) –

    list of tests that expect reported problems

Raises:

  • AssertionError

    if one of the checks fails

Source code in xrlint\testing.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def run(
    self,
    rule_name: str,
    rule_op_class: Type[RuleOp],
    *,
    valid: list[RuleTest] | None = None,
    invalid: list[RuleTest] | None = None,
):
    """Run the given tests in `valid` and `invalid`
    against the given rule.

    Args:
        rule_name: the rule's name
        rule_op_class: a class derived from `RuleOp`
        valid: list of tests that expect no reported problems
        invalid: list of tests that expect reported problems

    Raises:
        AssertionError: if one of the checks fails
    """
    tests = self._create_tests(
        rule_name, rule_op_class, valid=valid, invalid=invalid
    )
    for test in tests.values():
        print(f"Rule {rule_name!r}: running {test.__name__}()...")
        # noinspection PyTypeChecker
        test(None)
xrlint.testing.RuleTester.define_test(rule_name, rule_op_class, *, valid=None, invalid=None, config=None, **config_props) classmethod

Create a unittest.TestCase class for the given rule and tests.

The returned class is derived from unittest.TestCase and contains a test method for each of the tests in valid and invalid.

Parameters:

  • rule_name (str) –

    the rule's name

  • rule_op_class (Type[RuleOp]) –

    the class derived from RuleOp

  • valid (list[RuleTest] | None, default: None ) –

    list of tests that expect no reported problems

  • invalid (list[RuleTest] | None, default: None ) –

    list of tests that expect reported problems

  • config (ConfigLike, default: None ) –

    Optional configuration-like value. For more information see the ConfigLike type alias.

  • **config_props (Any, default: {} ) –

    Individual configuration object properties. For more information refer to the properties of a ConfigObject.

Returns:

  • Type[TestCase]

    A new class derived from unittest.TestCase.

Source code in xrlint\testing.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@classmethod
def define_test(
    cls,
    rule_name: str,
    rule_op_class: Type[RuleOp],
    *,
    valid: list[RuleTest] | None = None,
    invalid: list[RuleTest] | None = None,
    config: ConfigLike = None,
    **config_props: Any,
) -> Type[unittest.TestCase]:
    """Create a `unittest.TestCase` class for the given rule and tests.

    The returned class is derived from `unittest.TestCase`
    and contains a test method for each of the tests in
    `valid` and `invalid`.

    Args:
        rule_name: the rule's name
        rule_op_class: the class derived from `RuleOp`
        valid: list of tests that expect no reported problems
        invalid: list of tests that expect reported problems
        config: Optional configuration-like value.
            For more information see the
            [ConfigLike][xrlint.config.ConfigLike] type alias.
        **config_props: Individual configuration object properties.
            For more information refer to the properties of a
            [ConfigObject][xrlint.config.ConfigObject].

    Returns:
        A new class derived from `unittest.TestCase`.
    """
    tester = RuleTester(config=config, **config_props)
    tests = tester._create_tests(
        rule_name, rule_op_class, valid=valid, invalid=invalid
    )
    # noinspection PyTypeChecker
    return type(f"{rule_op_class.__name__}Test", (unittest.TestCase,), tests)

xrlint.testing.RuleTest dataclass

A rule test case.

Source code in xrlint\testing.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@dataclass(frozen=True, kw_only=True)
class RuleTest:
    """A rule test case."""

    dataset: xr.Dataset
    """The dataset to verify."""

    name: str | None = None
    """A name that helps identifying the test case."""

    args: tuple | list | None = None
    """Optional positional arguments passed to the rule operation's constructor."""

    kwargs: dict[str, Any] | None = None
    """Optional keyword arguments passed to the rule operation's constructor."""

    expected: list[Message | str] | int | None = None
    """Expected messages.
    Either a list of expected message objects or
    the number of expected messages.
    Must not be provided for valid checks
    and must be provided for invalid checks.
    """

Attributes

xrlint.testing.RuleTest.dataset instance-attribute

The dataset to verify.

xrlint.testing.RuleTest.name = None class-attribute instance-attribute

A name that helps identifying the test case.

xrlint.testing.RuleTest.args = None class-attribute instance-attribute

Optional positional arguments passed to the rule operation's constructor.

xrlint.testing.RuleTest.kwargs = None class-attribute instance-attribute

Optional keyword arguments passed to the rule operation's constructor.

xrlint.testing.RuleTest.expected = None class-attribute instance-attribute

Expected messages. Either a list of expected message objects or the number of expected messages. Must not be provided for valid checks and must be provided for invalid checks.