Skip to content

Commit ecf9e78

Browse files
authored
Fix traceback in cylc config -i (cylc#6062)
Fix traceback in `cylc config -i` Fixes bug when trying to get a setting in a `__MANY__` section when the setting is valid but not set in the config. Better handle `ItemNotFoundError` when printing platforms config
1 parent 1fa0f14 commit ecf9e78

File tree

10 files changed

+104
-85
lines changed

10 files changed

+104
-85
lines changed

cylc/flow/cfgspec/globalcfg.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -1994,26 +1994,29 @@ def platform_dump(
19941994
"""Print informations about platforms currently defined.
19951995
"""
19961996
if print_platform_names:
1997-
with suppress(ItemNotFoundError):
1998-
self.dump_platform_names(self)
1997+
self.dump_platform_names(self)
19991998
if print_platforms:
2000-
with suppress(ItemNotFoundError):
2001-
self.dump_platform_details(self)
1999+
self.dump_platform_details(self)
20022000

20032001
@staticmethod
20042002
def dump_platform_names(cfg) -> None:
20052003
"""Print a list of defined platforms and groups.
20062004
"""
2005+
# [platforms] is always defined with at least localhost
20072006
platforms = '\n'.join(cfg.get(['platforms']).keys())
2008-
platform_groups = '\n'.join(cfg.get(['platform groups']).keys())
20092007
print(f'{PLATFORM_REGEX_TEXT}\n\nPlatforms\n---------', file=stderr)
20102008
print(platforms)
2011-
print('\n\nPlatform Groups\n--------------', file=stderr)
2009+
try:
2010+
platform_groups = '\n'.join(cfg.get(['platform groups']).keys())
2011+
except ItemNotFoundError:
2012+
return
2013+
print('\nPlatform Groups\n--------------', file=stderr)
20122014
print(platform_groups)
20132015

20142016
@staticmethod
20152017
def dump_platform_details(cfg) -> None:
20162018
"""Print platform and platform group configs.
20172019
"""
20182020
for config in ['platforms', 'platform groups']:
2019-
printcfg({config: cfg.get([config], sparse=True)})
2021+
with suppress(ItemNotFoundError):
2022+
printcfg({config: cfg.get([config], sparse=True)})

cylc/flow/config.py

+1-24
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
from cylc.flow.param_expand import NameExpander
7474
from cylc.flow.parsec.exceptions import ItemNotFoundError
7575
from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults
76-
from cylc.flow.parsec.util import replicate
76+
from cylc.flow.parsec.util import dequote, replicate
7777
from cylc.flow.pathutil import (
7878
get_workflow_name_from_id,
7979
get_cylc_run_dir,
@@ -198,29 +198,6 @@ def interpolate_template(tmpl, params_dict):
198198
raise ParamExpandError('bad template syntax')
199199

200200

201-
def dequote(string):
202-
"""Strip quotes off a string.
203-
204-
Examples:
205-
>>> dequote('"foo"')
206-
'foo'
207-
>>> dequote("'foo'")
208-
'foo'
209-
>>> dequote('foo')
210-
'foo'
211-
>>> dequote('"f')
212-
'"f'
213-
>>> dequote('f')
214-
'f'
215-
216-
"""
217-
if len(string) < 2:
218-
return string
219-
if (string[0] == string[-1]) and string.startswith(("'", '"')):
220-
return string[1:-1]
221-
return string
222-
223-
224201
class WorkflowConfig:
225202
"""Class for workflow configuration items and derived quantities."""
226203

cylc/flow/context_node.py

+24-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717

18-
from typing import Optional
18+
from typing import TYPE_CHECKING, Optional
19+
20+
if TYPE_CHECKING:
21+
# BACK COMPAT: typing_extensions.Self
22+
# FROM: Python 3.7
23+
# TO: Python 3.11
24+
from typing_extensions import Self
1925

2026

2127
class ContextNode():
@@ -93,7 +99,7 @@ def __init__(self, name: str):
9399
self._children = None
94100
self.DATA[self] = set(self.DATA)
95101

96-
def __enter__(self):
102+
def __enter__(self) -> 'Self':
97103
return self
98104

99105
def __exit__(self, *args):
@@ -129,24 +135,36 @@ def __iter__(self):
129135
def __contains__(self, name: str) -> bool:
130136
return name in self._children # type: ignore[operator] # TODO
131137

132-
def __getitem__(self, name: str):
138+
def __getitem__(self, name: str) -> 'Self':
133139
if self._children:
134140
return self._children.__getitem__(name)
135141
raise TypeError('This is not a leaf node')
136142

137-
def get(self, *names: str):
143+
def get(self, *names: str) -> 'Self':
138144
"""Retrieve the node given by the list of names.
139145
140146
Example:
141147
>>> with ContextNode('a') as a:
142-
... with ContextNode('b') as b:
148+
... with ContextNode('b'):
143149
... c = ContextNode('c')
144150
>>> a.get('b', 'c')
145151
a/b/c
152+
153+
>>> with ContextNode('a') as a:
154+
... with ContextNode('b'):
155+
... with ContextNode('__MANY__'):
156+
... c = ContextNode('c')
157+
>>> a.get('b', 'foo', 'c')
158+
a/b/__MANY__/c
146159
"""
147160
node = self
148161
for name in names:
149-
node = node[name]
162+
try:
163+
node = node[name]
164+
except KeyError as exc:
165+
if '__MANY__' not in node:
166+
raise exc
167+
node = node['__MANY__']
150168
return node
151169

152170
def __str__(self) -> str:

cylc/flow/parsec/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@ def get(self, keys: Optional[Iterable[str]] = None, sparse: bool = False):
145145
cfg = cfg[key]
146146
except (KeyError, TypeError):
147147
if (
148+
# __MANY__ setting not present:
148149
parents in self.manyparents or
150+
# setting not present in __MANY__ section:
149151
key in self.spec.get(*parents)
150152
):
151153
raise ItemNotFoundError(itemstr(parents, key))

cylc/flow/parsec/util.py

+3-12
Original file line numberDiff line numberDiff line change
@@ -406,16 +406,7 @@ def expand_many_section(config):
406406
"""
407407
ret = {}
408408
for section_name, section in config.items():
409-
expanded_names = [
410-
dequote(name.strip()).strip()
411-
for name in SECTION_EXPAND_PATTERN.findall(section_name)
412-
]
413-
for name in expanded_names:
414-
if name in ret:
415-
# already defined -> merge
416-
replicate(ret[name], section)
417-
418-
else:
419-
ret[name] = {}
420-
replicate(ret[name], section)
409+
for name in SECTION_EXPAND_PATTERN.findall(section_name):
410+
name = dequote(name.strip()).strip()
411+
replicate(ret.setdefault(name, {}), section)
421412
return ret

tests/functional/cylc-config/08-item-not-found.t

+19-11
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# Test cylc config
1919
. "$(dirname "$0")/test_header"
2020
#-------------------------------------------------------------------------------
21-
set_test_number 7
21+
set_test_number 9
2222
#-------------------------------------------------------------------------------
2323
cat >>'global.cylc' <<__HERE__
2424
[platforms]
@@ -29,21 +29,29 @@ OLD="$CYLC_CONF_PATH"
2929
export CYLC_CONF_PATH="${PWD}"
3030

3131
# Control Run
32-
run_ok "${TEST_NAME_BASE}-ok" cylc config -i "[platforms]foo"
32+
run_ok "${TEST_NAME_BASE}-ok" cylc config -i "[platforms][foo]"
3333

3434
# If item not settable in config (platforms is mis-spelled):
35-
run_fail "${TEST_NAME_BASE}-not-in-config-spec" cylc config -i "[platfroms]foo"
36-
grep_ok "InvalidConfigError" "${TEST_NAME_BASE}-not-in-config-spec.stderr"
35+
run_fail "${TEST_NAME_BASE}-not-in-config-spec" cylc config -i "[platfroms][foo]"
36+
cmp_ok "${TEST_NAME_BASE}-not-in-config-spec.stderr" << __HERE__
37+
InvalidConfigError: "platfroms" is not a valid configuration for global.cylc.
38+
__HERE__
3739

38-
# If item not defined, item not found.
40+
# If item settable in config but not set.
3941
run_fail "${TEST_NAME_BASE}-not-defined" cylc config -i "[scheduler]"
40-
grep_ok "ItemNotFoundError" "${TEST_NAME_BASE}-not-defined.stderr"
42+
cmp_ok "${TEST_NAME_BASE}-not-defined.stderr" << __HERE__
43+
ItemNotFoundError: You have not set "scheduler" in this config.
44+
__HERE__
45+
46+
run_fail "${TEST_NAME_BASE}-not-defined-2" cylc config -i "[platforms][bar]"
47+
cmp_ok "${TEST_NAME_BASE}-not-defined-2.stderr" << __HERE__
48+
ItemNotFoundError: You have not set "[platforms]bar" in this config.
49+
__HERE__
4150

42-
# If item settable in config, item not found.
43-
run_fail "${TEST_NAME_BASE}-not-defined__MULTI__" cylc config -i "[platforms]bar"
44-
grep_ok "ItemNotFoundError" "${TEST_NAME_BASE}-not-defined__MULTI__.stderr"
51+
run_fail "${TEST_NAME_BASE}-not-defined-3" cylc config -i "[platforms][foo]hosts"
52+
cmp_ok "${TEST_NAME_BASE}-not-defined-3.stderr" << __HERE__
53+
ItemNotFoundError: You have not set "[platforms][foo]hosts" in this config.
54+
__HERE__
4555

4656
rm global.cylc
4757
export CYLC_CONF_PATH="$OLD"
48-
49-
exit

tests/functional/cylc-config/09-platforms.t

-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ They are searched from the bottom up, until the first match is found.
5454
Platforms
5555
---------
5656
57-
5857
Platform Groups
5958
--------------
6059
__HEREDOC__

tests/unit/parsec/test_config.py

+42-23
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def test_validate():
156156
:return:
157157
"""
158158

159-
with Conf('myconf') as spec: # noqa: SIM117
159+
with Conf('myconf') as spec:
160160
with Conf('section'):
161161
Conf('name', VDR.V_STRING)
162162
Conf('address', VDR.V_STRING)
@@ -192,13 +192,20 @@ def parsec_config_2(tmp_path: Path):
192192
Conf('address', VDR.V_INTEGER_LIST)
193193
with Conf('allow_many'):
194194
Conf('<user defined>', VDR.V_STRING, '')
195+
with Conf('so_many'):
196+
with Conf('<thing>'):
197+
Conf('color', VDR.V_STRING)
198+
Conf('horsepower', VDR.V_INTEGER)
195199
parsec_config = ParsecConfig(spec, validator=cylc_config_validate)
196200
conf_file = tmp_path / 'myconf'
197201
conf_file.write_text("""
198202
[section]
199203
name = test
200204
[allow_many]
201205
anything = yup
206+
[so_many]
207+
[[legs]]
208+
horsepower = 123
202209
""")
203210
parsec_config.loadcfg(conf_file, "1.0")
204211
return parsec_config
@@ -213,25 +220,32 @@ def test_expand(parsec_config_2: ParsecConfig):
213220

214221
def test_get(parsec_config_2: ParsecConfig):
215222
cfg = parsec_config_2.get(keys=None, sparse=False)
216-
assert parsec_config_2.dense == cfg
223+
assert cfg == parsec_config_2.dense
217224

218225
cfg = parsec_config_2.get(keys=None, sparse=True)
219-
assert parsec_config_2.sparse == cfg
226+
assert cfg == parsec_config_2.sparse
220227

221228
cfg = parsec_config_2.get(keys=['section'], sparse=True)
222-
assert parsec_config_2.sparse['section'] == cfg
223-
224-
with pytest.raises(InvalidConfigError):
225-
parsec_config_2.get(keys=['alloy_many', 'a'], sparse=True)
226-
227-
cfg = parsec_config_2.get(keys=['section', 'name'], sparse=True)
228-
assert cfg == 'test'
229-
230-
with pytest.raises(InvalidConfigError):
231-
parsec_config_2.get(keys=['section', 'a'], sparse=True)
232-
233-
with pytest.raises(ItemNotFoundError):
234-
parsec_config_2.get(keys=['allow_many', 'a'], sparse=True)
229+
assert cfg == parsec_config_2.sparse['section']
230+
231+
232+
@pytest.mark.parametrize('keys, expected', [
233+
(['section', 'name'], 'test'),
234+
(['section', 'a'], InvalidConfigError),
235+
(['alloy_many', 'anything'], InvalidConfigError),
236+
(['allow_many', 'anything'], 'yup'),
237+
(['allow_many', 'a'], ItemNotFoundError),
238+
(['so_many', 'legs', 'horsepower'], 123),
239+
(['so_many', 'legs', 'color'], ItemNotFoundError),
240+
(['so_many', 'legs', 'a'], InvalidConfigError),
241+
(['so_many', 'teeth', 'horsepower'], ItemNotFoundError),
242+
])
243+
def test_get__sparse(parsec_config_2: ParsecConfig, keys, expected):
244+
if isinstance(expected, type) and issubclass(expected, Exception):
245+
with pytest.raises(expected):
246+
parsec_config_2.get(keys, sparse=True)
247+
else:
248+
assert parsec_config_2.get(keys, sparse=True) == expected
235249

236250

237251
def test_mdump_none(config, sample_spec, capsys):
@@ -288,12 +302,17 @@ def test_get_none(config, sample_spec):
288302

289303
def test__get_namespace_parents():
290304
"""It returns a list of parents and nothing else"""
291-
with Conf('myconfig') as myconf:
292-
with Conf('some_parent'): # noqa: SIM117
293-
with Conf('manythings'):
294-
Conf('<thing>')
295-
with Conf('other_parent'):
296-
Conf('other_thing')
305+
with Conf('myconfig.cylc') as myconf:
306+
with Conf('a'):
307+
with Conf('b'):
308+
with Conf('<c>'):
309+
with Conf('d'):
310+
Conf('<e>')
311+
with Conf('x'):
312+
Conf('y')
297313

298314
cfg = ParsecConfig(myconf)
299-
assert cfg.manyparents == [['some_parent', 'manythings']]
315+
assert cfg.manyparents == [
316+
['a', 'b'],
317+
['a', 'b', '__MANY__', 'd'],
318+
]

tests/unit/parsec/test_types.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def _inner(typ, validator):
4848
cylc.flow.parsec.config.ConfigNode
4949
5050
"""
51-
with Conf('/') as myconf: # noqa: SIM117
51+
with Conf('/') as myconf:
5252
with Conf(typ):
5353
Conf('<item>', validator)
5454
return myconf

tox.ini

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ ignore=
1515
per-file-ignores=
1616
; TYPE_CHECKING block suggestions
1717
tests/*: TC001
18+
; for clarity we don't merge 'with Conf():' context trees
19+
tests/unit/parsec/*: SIM117
1820

1921
exclude=
2022
build,

0 commit comments

Comments
 (0)