Skip to content

Commit 85ebee0

Browse files
committed
Refactored and renamed CliEngine into XRLint. Documented the class.
1 parent 8bcea8a commit 85ebee0

File tree

15 files changed

+288
-149
lines changed

15 files changed

+288
-149
lines changed

CHANGES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
## Version 0.1.0 (in development)
44

5-
- Added CLI option `--print-config FILE`, see same option in ESLint
5+
- Added CLI option `--print-config PATH`, see same option in ESLint
66
- XRLint CLI now outputs single results immediately to console,
77
instead only after all results have been collected.
8+
- Refactored and renamed `CliEngine` into `XRLint`. Documented the class.
89

910

1011
## Early development snapshots

docs/todo.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@
1313
- project logo
1414
- if configuration for given FILE is empty,
1515
report an error, see TODO in CLI main tests
16-
- rename `xrlint.cli.CliEngine` into `xrlint.cli.XRLint`
17-
(with similar API as the `ESLint` class) and export it
18-
from `xrlint.all`. Value of `FILES` should be passed to
19-
`verify_datasets()` methods.
2016
- use `RuleMeta.docs_url` in formatters to create links
2117
- implement xarray backend for xcube 'levels' format
2218
so can validate them too

tests/cli/test_main.py

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import shutil
44
from unittest import TestCase
55

6+
import click.testing
67
from click.testing import CliRunner
78
import xarray as xr
89

@@ -17,8 +18,6 @@
1718
dataset-title-attr: error
1819
"""
1920

20-
no_match_config_yaml = "[]"
21-
2221

2322
# noinspection PyTypeChecker
2423
class CliMainTest(TestCase):
@@ -55,23 +54,32 @@ def tearDownClass(cls):
5554
os.chdir(cls.last_cwd)
5655
shutil.rmtree(cls.temp_dir)
5756

58-
def test_no_files(self):
57+
def xrlint(self, *args: tuple[str, ...]) -> click.testing.Result:
5958
runner = CliRunner()
60-
result = runner.invoke(main)
61-
self.assertIn("No dataset files provided.", result.output)
62-
self.assertEqual(1, result.exit_code)
59+
result = runner.invoke(main, args)
60+
if not isinstance(result.exception, SystemExit):
61+
self.assertIsNone(None, result.exception)
62+
return result
6363

64-
def test_files_no_rules(self):
65-
runner = CliRunner()
66-
result = runner.invoke(main, self.files)
67-
self.assertIn("Warning: no configuration file found.", result.output)
68-
self.assertIn("No rules configured or applicable.", result.output)
64+
def test_no_files_no_config(self):
65+
result = self.xrlint()
66+
self.assertEqual("", result.output)
67+
self.assertEqual(0, result.exit_code)
68+
69+
def test_config_no_files(self):
70+
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
71+
result = self.xrlint()
72+
self.assertEqual("", result.output)
73+
self.assertEqual(0, result.exit_code)
74+
75+
def test_files_no_config(self):
76+
result = self.xrlint(*self.files)
77+
self.assertIn("Warning: no configuration file found.\n", result.output)
6978
self.assertEqual(1, result.exit_code)
7079

7180
def test_files_one_rule(self):
7281
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
73-
runner = CliRunner()
74-
result = runner.invoke(main, ["--no-color"] + self.files)
82+
result = self.xrlint("--no-color", *self.files)
7583
self.assertEqual(
7684
"\n"
7785
"dataset1.zarr - ok\n\n"
@@ -84,16 +92,14 @@ def test_files_one_rule(self):
8492
self.assertEqual(0, result.exit_code)
8593

8694
with text_file(DEFAULT_CONFIG_FILE_YAML, self.fail_config_yaml):
87-
runner = CliRunner()
88-
result = runner.invoke(main, self.files)
95+
result = self.xrlint(*self.files)
8996
self.assertIn("Missing metadata, attributes are empty.", result.output)
9097
self.assertIn("no-empty-attrs", result.output)
9198
self.assertEqual(1, result.exit_code)
9299

93100
def test_dir_one_rule(self):
94101
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
95-
runner = CliRunner()
96-
result = runner.invoke(main, ["--no-color", "."])
102+
result = self.xrlint("--no-color", ".")
97103
prefix = self.temp_dir.replace("\\", "/")
98104
self.assertIn(f"{prefix}/dataset1.zarr - ok\n\n", result.output)
99105
self.assertIn(f"{prefix}/dataset1.nc - ok\n\n", result.output)
@@ -103,16 +109,15 @@ def test_dir_one_rule(self):
103109
self.assertEqual(0, result.exit_code)
104110

105111
with text_file(DEFAULT_CONFIG_FILE_YAML, self.fail_config_yaml):
106-
runner = CliRunner()
107-
result = runner.invoke(main, self.files)
112+
result = self.xrlint(*self.files)
108113
self.assertIn("Missing metadata, attributes are empty.", result.output)
109114
self.assertIn("no-empty-attrs", result.output)
110115
self.assertEqual(1, result.exit_code)
111116

112117
def test_color_no_color(self):
113118
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
114-
runner = CliRunner()
115-
result = runner.invoke(main, ["--no-color"] + self.files)
119+
result = self.xrlint("--no-color", *self.files)
120+
self.assertIsNone(result.exception)
116121
self.assertEqual(
117122
"\n"
118123
"dataset1.zarr - ok\n\n"
@@ -138,66 +143,68 @@ def test_color_no_color(self):
138143
self.assertEqual(0, result.exit_code)
139144

140145
def test_files_with_rule_option(self):
141-
runner = CliRunner()
142-
result = runner.invoke(
143-
main,
144-
[
145-
"--rule",
146-
"no-empty-attrs: error",
147-
]
148-
+ self.files,
149-
)
146+
result = self.xrlint("--rule", "no-empty-attrs: error", *self.files)
150147
self.assertIn("Missing metadata, attributes are empty.", result.output)
151148
self.assertIn("no-empty-attrs", result.output)
152149
self.assertEqual(1, result.exit_code)
153150

154151
def test_files_with_plugin_and_rule_options(self):
155-
runner = CliRunner()
156-
result = runner.invoke(
157-
main,
158-
[
159-
"--plugin",
160-
"xrlint.plugins.xcube",
161-
"--rule",
162-
"xcube/any-spatial-data-var: error",
163-
]
164-
+ self.files,
152+
result = self.xrlint(
153+
"--plugin",
154+
"xrlint.plugins.xcube",
155+
"--rule",
156+
"xcube/any-spatial-data-var: error",
157+
*self.files,
165158
)
166159
self.assertIn("No spatial data variables found.", result.output)
167160
self.assertIn("xcube/any-spatial-data-var", result.output)
168161
self.assertEqual(1, result.exit_code)
169162

170163
def test_files_with_output_file(self):
171164
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
172-
runner = CliRunner()
173-
result = runner.invoke(main, ["-o", "memory://report.txt"] + self.files)
165+
result = self.xrlint("-o", "memory://report.txt", *self.files)
174166
self.assertEqual("", result.output)
175167
self.assertEqual(0, result.exit_code)
176168

177169
def test_files_but_config_file_missing(self):
178-
runner = CliRunner()
179-
result = runner.invoke(main, ["-c", "pippo.py"] + self.files)
170+
result = self.xrlint("-c", "pippo.py", *self.files)
180171
self.assertIn("Error: file not found: pippo.py", result.output)
181172
self.assertEqual(1, result.exit_code)
182173

183174
def test_files_with_format_option(self):
184175
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
185-
runner = CliRunner()
186-
result = runner.invoke(main, ["-f", "json"] + self.files)
176+
result = self.xrlint("-f", "json", *self.files)
187177
self.assertIn('"results": [\n', result.output)
188178
self.assertEqual(0, result.exit_code)
189179

190180
def test_file_does_not_match(self):
191181
with text_file(DEFAULT_CONFIG_FILE_YAML, no_match_config_yaml):
192-
runner = CliRunner()
193-
result = runner.invoke(main, ["test.zarr"])
182+
result = self.xrlint("test.zarr")
194183
# TODO: make this assertion work
195184
# self.assertIn("No configuration matches this file.", result.output)
196185
self.assertEqual(1, result.exit_code)
197186

187+
def test_print_config_option(self):
188+
with text_file(DEFAULT_CONFIG_FILE_YAML, self.ok_config_yaml):
189+
result = self.xrlint("--print-config", "dataset2.zarr")
190+
self.assertEqual(
191+
(
192+
"{\n"
193+
' "name": "<computed>",\n'
194+
' "plugins": {\n'
195+
' "__core__": "xrlint.plugins.core"\n'
196+
" },\n"
197+
' "rules": {\n'
198+
' "dataset-title-attr": 2\n'
199+
" }\n"
200+
"}\n"
201+
),
202+
result.output,
203+
)
204+
self.assertEqual(0, result.exit_code)
205+
198206
def test_files_with_invalid_format_option(self):
199-
runner = CliRunner()
200-
result = runner.invoke(main, ["-f", "foo"] + self.files)
207+
result = self.xrlint("-f", "foo", *self.files)
201208
self.assertIn(
202209
"Error: unknown format 'foo'. The available formats are '", result.output
203210
)
@@ -208,8 +215,7 @@ def test_init(self):
208215
exists = os.path.exists(config_file)
209216
self.assertFalse(exists)
210217
try:
211-
runner = CliRunner()
212-
result = runner.invoke(main, ["--init"])
218+
result = self.xrlint("--init")
213219
self.assertEqual(
214220
f"Configuration template written to {config_file}\n", result.output
215221
)
@@ -225,8 +231,7 @@ def test_init_exists(self):
225231
exists = os.path.exists(config_file)
226232
self.assertFalse(exists)
227233
with text_file(config_file, ""):
228-
runner = CliRunner()
229-
result = runner.invoke(main, ["--init"])
234+
result = self.xrlint("--init")
230235
self.assertEqual(
231236
f"Error: file {config_file} already exists.\n", result.output
232237
)

tests/formatters/helpers.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
from xrlint.config import Config
2+
from xrlint.formatter import FormatterContext
23
from xrlint.plugin import Plugin
34
from xrlint.plugin import PluginMeta
4-
from xrlint.result import Message
5+
from xrlint.result import Message, ResultStats
56
from xrlint.result import Result
67
from xrlint.rule import RuleOp
78

89

10+
class FormatterContextImpl(FormatterContext):
11+
12+
def __init__(self, max_warnings: int = -1):
13+
self._max_warnings = max_warnings
14+
self._result_stats = ResultStats()
15+
16+
@property
17+
def max_warnings_exceeded(self) -> bool:
18+
return self._result_stats.warning_count > self._max_warnings
19+
20+
@property
21+
def result_stats(self) -> ResultStats:
22+
return self._result_stats
23+
24+
25+
def get_context(max_warnings: int = -1) -> FormatterContext:
26+
return FormatterContextImpl(max_warnings)
27+
28+
929
def get_test_results():
1030

1131
plugin = Plugin(meta=PluginMeta(name="test"))

tests/formatters/test_html.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from unittest import TestCase
22

3-
from xrlint.formatter import FormatterContext
43
from xrlint.formatters.html import Html
5-
from .helpers import get_test_results
4+
from .helpers import get_test_results, get_context
65

76

87
class HtmlTest(TestCase):
98
def test_html(self):
109
results = get_test_results()
1110
formatter = Html()
1211
text = formatter.format(
13-
context=FormatterContext(),
12+
context=get_context(),
1413
results=results,
1514
)
1615
self.assertIsInstance(text, str)
@@ -20,7 +19,7 @@ def test_html_with_meta(self):
2019
results = get_test_results()
2120
formatter = Html(with_meta=True)
2221
text = formatter.format(
23-
context=FormatterContext(),
22+
context=get_context(),
2423
results=results,
2524
)
2625
self.assertIsInstance(text, str)

tests/formatters/test_json.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from unittest import TestCase
22

3-
from xrlint.formatter import FormatterContext
43
from xrlint.formatters.json import Json
5-
from .helpers import get_test_results
4+
from .helpers import get_test_results, get_context
65

76

87
class JsonTest(TestCase):
98
def test_json(self):
109
results = get_test_results()
1110
formatter = Json()
1211
text = formatter.format(
13-
context=FormatterContext(),
12+
context=get_context(),
1413
results=results,
1514
)
1615
self.assertIn('"results": [', text)
@@ -19,7 +18,7 @@ def test_json_with_meta(self):
1918
results = get_test_results()
2019
formatter = Json(with_meta=True)
2120
text = formatter.format(
22-
context=FormatterContext(),
21+
context=get_context(),
2322
results=results,
2423
)
2524
self.assertIn('"results": [', text)

tests/formatters/test_markdown.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
import pytest
44

5-
from xrlint.formatter import FormatterContext
65
from xrlint.formatters.markdown import Markdown
7-
from .helpers import get_test_results
6+
from .helpers import get_test_results, get_context
87

98

109
class MarkdownTest(TestCase):
@@ -13,6 +12,6 @@ def test_markdown(self):
1312
formatter = Markdown()
1413
with pytest.raises(NotImplementedError):
1514
formatter.format(
16-
context=FormatterContext(),
15+
context=get_context(),
1716
results=get_test_results(),
1817
)

tests/formatters/test_simple.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest import TestCase
22

3+
from tests.formatters.helpers import get_context
34
from xrlint.config import Config
4-
from xrlint.formatter import FormatterContext
55
from xrlint.formatters.simple import Simple
66
from xrlint.result import Message
77
from xrlint.result import Result
@@ -23,7 +23,7 @@ class SimpleTest(TestCase):
2323
def test_no_color(self):
2424
formatter = Simple(styled=False)
2525
text = formatter.format(
26-
context=FormatterContext(),
26+
context=get_context(),
2727
results=self.results,
2828
)
2929
self.assert_output_ok(text)
@@ -32,7 +32,7 @@ def test_no_color(self):
3232
def test_color(self):
3333
formatter = Simple(styled=True)
3434
text = formatter.format(
35-
context=FormatterContext(),
35+
context=get_context(),
3636
results=self.results,
3737
)
3838
self.assert_output_ok(text)

0 commit comments

Comments
 (0)