Skip to content

Commit 3e51a69

Browse files
committed
more docstrings and fixed ConfigList.compute_config()
1 parent c91f4f2 commit 3e51a69

3 files changed

Lines changed: 161 additions & 69 deletions

File tree

tests/test_config.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from xrlint.rule import RuleConfig
6-
from xrlint.config import Config
6+
from xrlint.config import Config, ConfigList
77

88

99
# noinspection PyMethodMayBeStatic
@@ -52,3 +52,51 @@ def test_global_ignores(self):
5252
self.assertEqual(
5353
[], Config(ignores=ignores, rules={"x": RuleConfig(0)}).global_ignores
5454
)
55+
56+
57+
class ConfigListTest(TestCase):
58+
def test_from_value(self):
59+
config_list = ConfigList.from_value([])
60+
self.assertIsInstance(config_list, ConfigList)
61+
self.assertEqual([], config_list.configs)
62+
63+
config_list_2 = ConfigList.from_value(config_list)
64+
self.assertIs(config_list_2, config_list)
65+
66+
config_list = ConfigList.from_value([{}])
67+
self.assertIsInstance(config_list, ConfigList)
68+
self.assertEqual([Config()], config_list.configs)
69+
70+
with pytest.raises(
71+
TypeError,
72+
match=(
73+
"configuration list must be of type"
74+
" ConfigList|list\\[Config|dict\\], but was dict"
75+
),
76+
):
77+
ConfigList.from_value({})
78+
79+
def test_compute_config(self):
80+
file_path = "s3://wq-services/datacubes/chl-2.zarr"
81+
82+
config_list = ConfigList([Config()])
83+
self.assertEqual(
84+
Config(name="<computed>"), config_list.compute_config(file_path)
85+
)
86+
87+
config_list = ConfigList(
88+
[
89+
Config(settings={"a": 1, "b": 1}),
90+
Config(files=["**/datacubes/*.zarr"], settings={"b": 2}),
91+
Config(ignores=["**/chl-*.*"], settings={"a": 2}),
92+
Config(ignores=["**/chl-*.txt"]), # global ignores
93+
]
94+
)
95+
self.assertEqual(
96+
Config(name="<computed>", settings={"a": 1, "b": 2}),
97+
config_list.compute_config(file_path),
98+
)
99+
100+
# global ignores
101+
file_path = "s3://wq-services/datacubes/chl-2.txt"
102+
self.assertEqual(None, config_list.compute_config(file_path))

xrlint/cli/engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def load_config(self) -> None:
5454
def verify_datasets(self) -> list[Result]:
5555
results: list[Result] = []
5656
for file_path in self.files:
57-
config = self.config_list.resolve_for_path(file_path)
57+
config = self.config_list.compute_config(file_path)
5858
if config is not None:
5959
# TODO: use config.processor
6060
linter = Linter(config=config)

xrlint/config.py

Lines changed: 111 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,53 @@ class Config(ToDictMixin):
109109
that should be available to all rules.
110110
"""
111111

112+
@classmethod
113+
def from_value(cls, value: Any) -> "Config":
114+
"""Convert given `value` into a `Config` object.
115+
116+
If `value` is already a `Config` then it is returned as-is.
117+
118+
Args:
119+
value: A `Config` object, a `dict` containing the
120+
configuration properties, or `None` which
121+
converts into an empty configuration.
122+
Returns:
123+
A `Config` object.
124+
"""
125+
if isinstance(value, Config):
126+
return value
127+
if value is None:
128+
return Config()
129+
if not isinstance(value, dict):
130+
raise TypeError(format_message_type_of("configuration", value, "dict"))
131+
if not value:
132+
return Config()
133+
134+
files = cls._parse_pattern_list(value, "files")
135+
ignores = cls._parse_pattern_list(value, "ignores")
136+
linter_options = cls._parse_options("linter_options", value)
137+
opener_options = cls._parse_options("opener_options", value)
138+
processor = cls._parse_processor(value)
139+
plugins = cls._parse_plugins(value)
140+
rules = cls._parse_rules(value)
141+
settings = cls._parse_options("settings", value)
142+
143+
return Config(
144+
name=value.get("name"),
145+
files=files,
146+
ignores=ignores,
147+
linter_options=linter_options,
148+
opener_options=opener_options,
149+
processor=processor,
150+
plugins=plugins,
151+
rules=rules,
152+
settings=settings,
153+
)
154+
112155
@property
113156
def global_ignores(self) -> list[str]:
114-
"""The list of `ignores` patterns from this configuration which
115-
are _global ignores_.
116-
"""
157+
# The list of `ignores` patterns from this configuration which
158+
# are _global ignores_.
117159
return (
118160
self.ignores
119161
if self.ignores
@@ -147,53 +189,21 @@ def get_rule(self, rule_id: str) -> "Rule":
147189

148190
return rule
149191

150-
def merge(self, config_obj: "Config", name: str = None) -> "Config":
192+
def merge(self, config: "Config", name: str = None) -> "Config":
151193
return Config(
152194
name=name,
153-
files=self._merge_pattern_lists(self.files, config_obj.files),
154-
ignores=self._merge_pattern_lists(self.ignores, config_obj.ignores),
195+
files=self._merge_pattern_lists(self.files, config.files),
196+
ignores=self._merge_pattern_lists(self.ignores, config.ignores),
155197
linter_options=self._merge_options(
156-
self.linter_options, config_obj.linter_options
198+
self.linter_options, config.linter_options
157199
),
158200
opener_options=self._merge_options(
159-
self.opener_options, config_obj.opener_options
201+
self.opener_options, config.opener_options
160202
),
161-
processor=merge_values(self.processor, config_obj.processor), # TBD!
162-
plugins=self._merge_plugin_dicts(self.plugins, config_obj.plugins),
163-
rules=self._merge_rule_dicts(self.rules, config_obj.rules),
164-
settings=self._merge_options(self.rules, config_obj.settings),
165-
)
166-
167-
@classmethod
168-
def from_value(cls, value: Any) -> "Config":
169-
if isinstance(value, Config):
170-
return value
171-
if value is None:
172-
return Config()
173-
if not isinstance(value, dict):
174-
raise TypeError(format_message_type_of("configuration", value, "dict"))
175-
if not value:
176-
return Config()
177-
178-
files = cls._parse_pattern_list(value, "files")
179-
ignores = cls._parse_pattern_list(value, "ignores")
180-
linter_options = cls._parse_options("linter_options", value)
181-
opener_options = cls._parse_options("opener_options", value)
182-
processor = cls._parse_processor(value)
183-
plugins = cls._parse_plugins(value)
184-
rules = cls._parse_rules(value)
185-
settings = cls._parse_options("settings", value)
186-
187-
return Config(
188-
name=value.get("name"),
189-
files=files,
190-
ignores=ignores,
191-
linter_options=linter_options,
192-
opener_options=opener_options,
193-
processor=processor,
194-
plugins=plugins,
195-
rules=rules,
196-
settings=settings,
203+
processor=merge_values(self.processor, config.processor), # TBD!
204+
plugins=self._merge_plugin_dicts(self.plugins, config.plugins),
205+
rules=self._merge_rule_dicts(self.rules, config.rules),
206+
settings=self._merge_options(self.settings, config.settings),
197207
)
198208

199209
@classmethod
@@ -323,9 +333,43 @@ def merge_configs(
323333

324334
@dataclass(frozen=True)
325335
class ConfigList:
336+
"""A holder for a list of `Config` objects."""
337+
326338
configs: list[Config] = field(default_factory=list)
339+
"""The list of `Config` objects."""
340+
341+
@classmethod
342+
def from_value(cls, value: Any) -> "ConfigList":
343+
"""Convert given `value` into a `ConfigList` object.
344+
345+
If `value` is already a `ConfigList` then it is returned as-is.
327346
328-
def resolve_for_path(self, path: str) -> Config | None:
347+
Args:
348+
value: A `ConfigList` object or `list` of values which can be
349+
converted into `Config` objects.
350+
Returns:
351+
A `ConfigList` object.
352+
"""
353+
if isinstance(value, ConfigList):
354+
return value
355+
if isinstance(value, list):
356+
return ConfigList([Config.from_value(c) for c in value])
357+
raise TypeError(
358+
format_message_type_of(
359+
"configuration list", value, "ConfigList|list[Config|dict]"
360+
)
361+
)
362+
363+
def compute_config(self, file_path: str) -> Config | None:
364+
"""Compute the configuration object for the given file path.
365+
366+
Args:
367+
file_path: A dataset file path.
368+
Returns:
369+
A `Config` object which may be empty, or `None`
370+
if `file_path` is not included by any `files` pattern
371+
or intentionally ignored by global `ignores`.
372+
"""
329373
# Step 1: Check against global ignores
330374
global_ignores = set()
331375
effective_configs = []
@@ -336,37 +380,37 @@ def resolve_for_path(self, path: str) -> Config | None:
336380
else:
337381
effective_configs.append(c)
338382
for p in global_ignores:
339-
if fnmatch.fnmatch(path, p):
383+
if fnmatch.fnmatch(file_path, p):
340384
return None
341385

342386
# Step 2: Check against global ignores
343-
config = Config()
387+
config = None
344388
for c in effective_configs:
345-
matches = True
389+
excluded = False
346390
if c.ignores:
347391
for p in c.ignores:
348-
matches = fnmatch.fnmatch(path, p)
349-
if not matches:
392+
excluded = fnmatch.fnmatch(file_path, p)
393+
if excluded:
350394
break
351-
if matches:
395+
included = not excluded
396+
if included:
352397
if c.files:
353398
for p in c.files:
354-
matches = fnmatch.fnmatch(path, p)
355-
if matches:
399+
included = fnmatch.fnmatch(file_path, p)
400+
if not included:
356401
break
357-
if matches:
358-
config = config.merge(c)
402+
if included:
403+
config = config.merge(c) if config is not None else c
359404

360-
return config
361-
362-
@classmethod
363-
def from_value(cls, value: Any) -> "ConfigList":
364-
if isinstance(value, ConfigList):
365-
return value
366-
if isinstance(value, list):
367-
return ConfigList([Config.from_value(c) for c in value])
368-
raise TypeError(
369-
format_message_type_of(
370-
"configuration list", value, "ConfigList|list[Config|dict]"
371-
)
405+
if config is None:
406+
return None
407+
# Exclude "files" and "ignores" because they have been used
408+
return Config(
409+
name="<computed>",
410+
linter_options=config.linter_options,
411+
opener_options=config.opener_options,
412+
processor=config.processor,
413+
plugins=config.plugins,
414+
rules=config.rules,
415+
settings=config.settings,
372416
)

0 commit comments

Comments
 (0)