Skip to content

Commit d08c334

Browse files
authored
Ensure no variable intent conflict when building a model (#57)
* ensure no variable intent conflict when building a model * fix missing import * update what's new
1 parent 86729d4 commit d08c334

File tree

3 files changed

+47
-1
lines changed

3 files changed

+47
-1
lines changed

doc/whats_new.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Release Notes
66
v0.3.0 (Unreleased)
77
-------------------
88

9+
Enhancements
10+
~~~~~~~~~~~~
11+
12+
- Ensure that there is no ``intent`` conflict between the variables
13+
declared in a model. This check is explicit at Model creation and a
14+
more meaningful error message is shown when it fails (:issue:`57`).
15+
916

1017
v0.2.1 (7 November 2018)
1118
------------------------

xsimlab/model.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,34 @@ def set_process_keys(self):
145145
if od_key is not None:
146146
p_obj.__xsimlab_od_keys__[var.name] = od_key
147147

148+
def ensure_no_intent_conflict(self):
149+
"""Raise an error if more than one variable with
150+
intent='out' targets the same variable.
151+
152+
"""
153+
filter_out = lambda var: (
154+
var.metadata['intent'] == VarIntent.OUT and
155+
var.metadata['var_type'] != VarType.ON_DEMAND
156+
)
157+
158+
targets = defaultdict(list)
159+
160+
for p_name, p_obj in self._processes_obj.items():
161+
for var in filter_variables(p_obj, func=filter_out).values():
162+
target_key = p_obj.__xsimlab_store_keys__.get(var.name)
163+
targets[target_key].append((p_name, var.name))
164+
165+
conflicts = {k: v for k, v in targets.items() if len(v) > 1}
166+
167+
if conflicts:
168+
conflicts_str = {k: ' and '.join(["'{}.{}'".format(*i) for i in v])
169+
for k, v in conflicts.items()}
170+
msg = '\n'.join(["'{}.{}' set by: {}".format(*k, v)
171+
for k, v in conflicts_str.items()])
172+
173+
raise ValueError(
174+
"Conflict(s) found in given variable intents:\n" + msg)
175+
148176
def get_all_variables(self):
149177
"""Get all variables in the model as a list of
150178
``(process_name, var_name)`` tuples.
@@ -364,6 +392,8 @@ def __init__(self, processes):
364392
self._all_vars = builder.get_all_variables()
365393
self._all_vars_dict = None
366394

395+
builder.ensure_no_intent_conflict()
396+
367397
self._input_vars = builder.get_input_variables()
368398
self._input_vars_dict = None
369399

xsimlab/tests/test_model.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
import xsimlab as xs
4-
from xsimlab.tests.fixture_model import AddOnDemand, InitProfile
4+
from xsimlab.tests.fixture_model import AddOnDemand, InitProfile, Profile
55

66

77
class TestModelBuilder(object):
@@ -52,6 +52,15 @@ def test_get_all_variables(self, model):
5252
assert all([p_name in model for p_name, _ in model.all_vars])
5353
assert ('profile', 'u') in model.all_vars
5454

55+
def test_ensure_no_intent_conflict(self, model):
56+
@xs.process
57+
class Foo(object):
58+
u = xs.foreign(Profile, 'u', intent='out')
59+
60+
with pytest.raises(ValueError) as excinfo:
61+
invalid_model = model.update_processes({'foo': Foo})
62+
assert "Conflict(s)" in str(excinfo.value)
63+
5564
def test_get_input_variables(self, model):
5665
expected = {('init_profile', 'n_points'),
5766
('roll', 'shift'),

0 commit comments

Comments
 (0)