Skip to content

Commit cb1304b

Browse files
committed
REF: More detailed runtime checks for input spec
1 parent 374cdec commit cb1304b

File tree

2 files changed

+57
-44
lines changed

2 files changed

+57
-44
lines changed

pydra/engine/specs.py

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -156,55 +156,69 @@ def check_fields_input_spec(self):
156156
157157
"""
158158
fields = attr_fields(self)
159-
names = []
160-
require_to_check = {}
161-
for fld in fields:
162-
mdata = fld.metadata
163-
# checking if the mandatory field is provided
164-
if getattr(self, fld.name) is attr.NOTHING:
165-
if mdata.get("mandatory"):
166-
# checking if the mandatory field is provided elsewhere in the xor list
167-
in_exclusion_list = mdata.get("xor") is not None
168-
alreday_populated = in_exclusion_list and [
169-
getattr(self, el)
170-
for el in mdata["xor"]
171-
if (getattr(self, el) is not attr.NOTHING)
172-
]
173-
if (
174-
alreday_populated
175-
): # another input satisfies mandatory attribute via xor condition
176-
continue
159+
160+
for field in fields:
161+
field_is_mandatory = bool(field.metadata.get("mandatory"))
162+
field_is_unset = getattr(self, field.name) is attr.NOTHING
163+
164+
# Collect alternative fields associated with this field.
165+
alternative_fields = {
166+
name: getattr(self, name) is not attr.NOTHING
167+
for name in field.metadata.get("xor", [])
168+
if name != field.name
169+
}
170+
171+
# Collect required fields associated with this field.
172+
required_fields = {
173+
name: getattr(self, name) is not attr.NOTHING
174+
for name in field.metadata.get("requires", [])
175+
if name != field.name
176+
}
177+
178+
# Raise error if field is mandatory and unset
179+
# or no suitable alternative is provided.
180+
if field_is_unset:
181+
if field_is_mandatory:
182+
if alternative_fields:
183+
if any(alternative_fields.values()):
184+
# Alternative fields found, skip other checks.
185+
continue
186+
else:
187+
raise AttributeError(
188+
f"{field.name} is mandatory and unset, "
189+
"but no value provided by "
190+
f"{list(alternative_fields.keys())}."
191+
)
177192
else:
178193
raise AttributeError(
179-
f"{fld.name} is mandatory, but no value provided"
194+
f"{field.name} is mandatory, but no value provided."
180195
)
181196
else:
197+
# Field is not set, check the next one.
182198
continue
183-
names.append(fld.name)
184199

185-
# checking if fields meet the xor and requires are
186-
if "xor" in mdata:
187-
if [el for el in mdata["xor"] if (el in names and el != fld.name)]:
188-
raise AttributeError(
189-
f"{fld.name} is mutually exclusive with {mdata['xor']}"
190-
)
200+
# Raise error if multiple alternatives are set.
201+
if alternative_fields and any(alternative_fields.values()):
202+
set_alternative_fields = [
203+
name for name, is_set in alternative_fields.items() if is_set
204+
]
205+
raise AttributeError(
206+
f"{field.name} is mutually exclusive with {set_alternative_fields}"
207+
)
191208

192-
if "requires" in mdata:
193-
if [el for el in mdata["requires"] if el not in names]:
194-
# will check after adding all fields to names
195-
require_to_check[fld.name] = mdata["requires"]
209+
# Raise error if any required field is unset.
210+
if required_fields and not all(required_fields.values()):
211+
unset_required_fields = [
212+
name for name, is_set in required_fields.items() if not is_set
213+
]
214+
raise AttributeError(f"{field.name} requires {unset_required_fields}")
196215

197216
if (
198-
fld.type in [File, Directory]
199-
or "pydra.engine.specs.File" in str(fld.type)
200-
or "pydra.engine.specs.Directory" in str(fld.type)
217+
field.type in [File, Directory]
218+
or "pydra.engine.specs.File" in str(field.type)
219+
or "pydra.engine.specs.Directory" in str(field.type)
201220
):
202-
self._file_check(fld)
203-
204-
for nm, required in require_to_check.items():
205-
required_notfound = [el for el in required if el not in names]
206-
if required_notfound:
207-
raise AttributeError(f"{nm} requires {required_notfound}")
221+
self._file_check(field)
208222

209223
def _file_check(self, field):
210224
"""checking if the file exists"""

pydra/engine/tests/test_shelltask_inputspec.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,7 +2204,8 @@ def test_task_inputs_mandatory_with_xOR_zero_mandatory_raises_error():
22042204
task.inputs.input_2 = attr.NOTHING
22052205
with pytest.raises(Exception) as excinfo:
22062206
task.inputs.check_fields_input_spec()
2207-
assert "input_1 is mandatory, but no value provided" in str(excinfo.value)
2207+
assert "input_1 is mandatory" in str(excinfo.value)
2208+
assert "no value provided by ['input_2', 'input_3']" in str(excinfo.value)
22082209
assert excinfo.type is AttributeError
22092210

22102211

@@ -2216,9 +2217,7 @@ def test_task_inputs_mandatory_with_xOR_two_mandatories_raises_error():
22162217

22172218
with pytest.raises(Exception) as excinfo:
22182219
task.inputs.check_fields_input_spec()
2219-
assert "input_2 is mutually exclusive with ('input_1', 'input_2'" in str(
2220-
excinfo.value
2221-
)
2220+
assert "input_1 is mutually exclusive with ['input_2']" in str(excinfo.value)
22222221
assert excinfo.type is AttributeError
22232222

22242223

@@ -2231,7 +2230,7 @@ def test_task_inputs_mandatory_with_xOR_3_mandatories_raises_error():
22312230

22322231
with pytest.raises(Exception) as excinfo:
22332232
task.inputs.check_fields_input_spec()
2234-
assert "input_2 is mutually exclusive with ('input_1', 'input_2', 'input_3'" in str(
2233+
assert "input_1 is mutually exclusive with ['input_2', 'input_3']" in str(
22352234
excinfo.value
22362235
)
22372236
assert excinfo.type is AttributeError

0 commit comments

Comments
 (0)