Skip to content

Commit 80e4a38

Browse files
committed
Improve custom_implementations algorithm further and add more tests
1 parent 49a4592 commit 80e4a38

2 files changed

Lines changed: 157 additions & 17 deletions

File tree

‎ymmsl/v0_2/resolver.py‎

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,11 @@ def resolve_impls(
158158
rename_local_impls(config.programs, module, ylocals)
159159
rename_local_impls(config.models, module, ylocals)
160160
resolve_impl_imports(config, ylocals, ctx)
161-
update_local_implementations(config, ylocals)
162161
config.imports = [i for i in config.imports if i.kind != ImportKind.IMPLEMENTATION]
163162

164-
return apply_custom_implementations(config, module, ylocals, ctx)
163+
overwritten_impls = apply_custom_implementations(config, module, ylocals, ctx)
164+
update_local_implementations(config, ylocals)
165+
return overwritten_impls
165166

166167

167168
T = TypeVar('T', bound='Implementation')
@@ -225,16 +226,6 @@ def resolve_impl_imports(
225226
ctx.pop_import()
226227

227228

228-
def update_local_implementations(
229-
config: Configuration, ylocals: Dict[Reference, Reference]) -> None:
230-
"""Updates names of local implementations to their full names."""
231-
for model in config.models.values():
232-
for cmp in model.components.values():
233-
if cmp.implementation:
234-
if cmp.implementation in ylocals:
235-
cmp.implementation = ylocals[cmp.implementation]
236-
237-
238229
def apply_custom_implementations(
239230
config: Configuration, module: Reference, ylocals: Dict[Reference, Reference],
240231
ctx: ResolutionContext) -> Set[Reference]:
@@ -268,6 +259,8 @@ def impl_hint_msg(
268259
overwritten_implementations = set()
269260
copied_paths = set()
270261

262+
# Pre-copy any models that will be updated, if they were imported and we therefore
263+
# cannot modify them in place without interfering with other uses of the same model.
271264
for key, value in config.custom_implementations.items():
272265
base_model_name = Reference([key[0]])
273266
if ylocals.get(base_model_name) not in config.models:
@@ -282,9 +275,6 @@ def impl_hint_msg(
282275
f' Unknown implementation "{value}" in custom_implementations'
283276
f' "{key}: {value}". {impl_hint_msg(value)}')
284277

285-
path = key[1:]
286-
new_impl = ylocals[value] if value is not None else None
287-
288278
# Before modifying it, we copy the model from its original a.b.c.Model to
289279
# <module>.Model so that the changes don't affect other uses of it, or the
290280
# cached version.
@@ -299,8 +289,13 @@ def impl_hint_msg(
299289
config.models[new_name] = m
300290
ylocals[base_model_name] = m.name
301291
overwritten_implementations.add(orig_name)
302-
else:
303-
m = orig_model
292+
293+
for key, value in config.custom_implementations.items():
294+
base_model_name = Reference([key[0]])
295+
path = key[1:]
296+
new_impl = ylocals[value] if value is not None else None
297+
298+
m = config.models[ylocals[base_model_name]]
304299

305300
# Now we can walk down the components and copy-and-rename the models along the
306301
# path, again to avoid making unintended changes elsewhere
@@ -319,6 +314,8 @@ def impl_hint_msg(
319314
f' which does not have a component named {str(component)}.')
320315

321316
orig_impl = m.components[component].implementation
317+
if orig_impl in ylocals:
318+
orig_impl = ylocals[orig_impl]
322319
if orig_impl is None or orig_impl not in config.models:
323320
raise RuntimeError(
324321
ctx.trace() +
@@ -372,6 +369,16 @@ def impl_hint_msg(
372369
return overwritten_implementations
373370

374371

372+
def update_local_implementations(
373+
config: Configuration, ylocals: Dict[Reference, Reference]) -> None:
374+
"""Updates names of local implementations to their full names."""
375+
for model in config.models.values():
376+
for cmp in model.components.values():
377+
if cmp.implementation:
378+
if cmp.implementation in ylocals:
379+
cmp.implementation = ylocals[cmp.implementation]
380+
381+
375382
def find_impls(
376383
config: Configuration, name: Reference, ctx: ResolutionContext
377384
) -> List[Implementation]:

‎ymmsl/v0_2/tests/test_resolver.py‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
else:
1818
from importlib.metadata import EntryPoints, EntryPoint
1919

20+
Ref = Reference
21+
2022

2123
@pytest.fixture
2224
def env_ymmsl_path() -> Generator[None, None, None]:
@@ -199,6 +201,137 @@ def test_apply_custom_implementations_set_none(env_ymmsl_path: None) -> None:
199201
assert model.components[Reference('micro')].implementation is None
200202

201203

204+
def test_apply_custom_implementations_no_hidden_copies() -> None:
205+
ymmsl = (
206+
'ymmsl_version: v0.2\n'
207+
'description: |\n'
208+
' Testing that all local references point to the same object\n'
209+
'models:\n'
210+
' A:\n'
211+
' description: Model A\n'
212+
' components:\n'
213+
' c1:\n'
214+
' ports: {}\n'
215+
' description: Component c1\n'
216+
' B:\n'
217+
' description: Model B\n'
218+
' components:\n'
219+
' c2:\n'
220+
' ports: {}\n'
221+
' description: Component c2\n'
222+
' c3:\n'
223+
' ports: {}\n'
224+
' description: Component c3\n'
225+
'programs:\n'
226+
' p:\n'
227+
' ports:\n'
228+
' description: A program\n'
229+
' executable: /home/user/p\n'
230+
)
231+
232+
config = load(ymmsl)
233+
assert isinstance(config, Configuration)
234+
235+
config.models[Ref('A')].components[Ref('c1')].implementation = Ref('p')
236+
config.custom_implementations[Reference('B.c2')] = Reference('A')
237+
config.custom_implementations[Reference('B.c3')] = Reference('A')
238+
239+
resolve(Reference('no_copies'), config)
240+
241+
242+
# Same thing but using custom implementations for everything
243+
config = load(ymmsl)
244+
assert isinstance(config, Configuration)
245+
246+
config.custom_implementations[Reference('A.c1')] = Reference('p')
247+
config.custom_implementations[Reference('B.c2')] = Reference('A')
248+
config.custom_implementations[Reference('B.c3')] = Reference('A')
249+
250+
resolve(Reference('no_copies'), config)
251+
252+
b = config.models[Reference('no_copies.B')]
253+
assert b.components[Reference('c2')].implementation == 'no_copies.A'
254+
assert b.components[Reference('c3')].implementation == 'no_copies.A'
255+
256+
a = config.models[Reference('no_copies.A')]
257+
assert a.components[Reference('c1')].implementation == 'no_copies.p'
258+
259+
# this should yield the same result as above, because all references to A point to
260+
# the same object
261+
config = load(ymmsl)
262+
assert isinstance(config, Configuration)
263+
264+
config.custom_implementations[Reference('B.c2')] = Reference('A')
265+
config.custom_implementations[Reference('B.c3')] = Reference('A')
266+
config.custom_implementations[Reference('A.c1')] = Reference('p')
267+
268+
resolve(Reference('no_copies2'), config)
269+
270+
b = config.models[Reference('no_copies2.B')]
271+
assert b.components[Reference('c2')].implementation == 'no_copies2.A'
272+
assert b.components[Reference('c3')].implementation == 'no_copies2.A'
273+
274+
a = config.models[Reference('no_copies2.A')]
275+
assert a.components[Reference('c1')].implementation == 'no_copies2.p'
276+
277+
278+
def test_apply_custom_implementations_everything_localised() -> None:
279+
ymmsl = (
280+
'ymmsl_version: v0.2\n'
281+
'description: |\n'
282+
' Testing that local and imported models are treated the same when\n'
283+
' customised.\n'
284+
'imports:\n'
285+
'- from a.e import implementation test_model\n'
286+
'models:\n'
287+
' A:\n'
288+
' description: Model A\n'
289+
' components:\n'
290+
' c1:\n'
291+
' ports: {}\n'
292+
' description: Component c1\n'
293+
' B:\n'
294+
' description: Model B\n'
295+
' components:\n'
296+
' macro:\n'
297+
' ports: {}\n'
298+
' description: Component c2\n'
299+
'programs:\n'
300+
' p:\n'
301+
' ports:\n'
302+
' description: A program\n'
303+
' executable: /home/user/p\n'
304+
)
305+
306+
config = load(ymmsl)
307+
assert isinstance(config, Configuration)
308+
309+
config.custom_implementations[Ref('A.c1')] = Ref('test_model')
310+
config.custom_implementations[Ref('test_model.macro')] = Ref('p')
311+
312+
resolve(Reference('el'), config)
313+
314+
c1 = config.models[Ref('el.A')].components[Ref('c1')]
315+
assert c1.implementation == 'el.test_model'
316+
317+
test_model = config.models[Ref('el.test_model')]
318+
assert test_model.components[Ref('macro')].implementation == 'el.p'
319+
320+
config = load(ymmsl)
321+
assert isinstance(config, Configuration)
322+
323+
config.custom_implementations[Ref('A.c1')] = Ref('B')
324+
config.custom_implementations[Ref('B.macro')] = Ref('p')
325+
326+
resolve(Reference('el'), config)
327+
328+
c1 = config.models[Ref('el.A')].components[Ref('c1')]
329+
assert c1.implementation == 'el.B'
330+
331+
b = config.models[Ref('el.B')]
332+
assert b.components[Ref('macro')].implementation == 'el.p'
333+
334+
202335
def test_apply_custom_implementations_errors(env_ymmsl_path: None) -> None:
203336
ymmsl = (
204337
'ymmsl_version: v0.2\n'

0 commit comments

Comments
 (0)