Skip to content

Commit b582cc5

Browse files
feat(template): allow renaming templates by using aliases (#3347)
1 parent 2877bd8 commit b582cc5

File tree

10 files changed

+127
-37
lines changed

10 files changed

+127
-37
lines changed

renku/command/view_model/template.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,29 @@ class TemplateViewModel:
2727

2828
def __init__(
2929
self,
30+
aliases: List[str],
3031
description: str,
3132
icon: Optional[str],
3233
id: str,
3334
immutable_files: Optional[List[str]],
3435
name: str,
36+
parameters: List[TemplateParameter],
3537
reference: Optional[str],
3638
source: Optional[str],
37-
parameters: List[TemplateParameter],
3839
version: Optional[str],
3940
versions: List[str],
4041
):
42+
self.aliases: List[str] = aliases
4143
self.description: str = description
4244
self.icon = icon
4345
self.id: str = id
4446
self.immutable_files: Optional[List[str]] = immutable_files
4547
self.name: str = name
46-
self.reference = reference
47-
self.source = source
4848
self.parameters: List[TemplateParameterViewModel] = [
4949
TemplateParameterViewModel.from_template_parameter(p) for p in parameters
5050
]
51+
self.reference = reference
52+
self.source = source
5153
self.version = version
5254
self.versions = versions
5355

@@ -62,15 +64,16 @@ def from_template(cls, template: Template) -> "TemplateViewModel":
6264
TemplateViewModel: View model for a template.
6365
"""
6466
return cls(
65-
source=template.source,
66-
reference=template.reference,
67-
version=template.version,
68-
id=template.id,
69-
name=template.name,
67+
aliases=template.aliases,
7068
description=template.description,
71-
parameters=template.parameters,
7269
icon=template.icon,
70+
id=template.id,
7371
immutable_files=template.immutable_files,
72+
name=template.name,
73+
parameters=template.parameters,
74+
reference=template.reference,
75+
source=template.source,
76+
version=template.version,
7477
versions=template.get_all_references(),
7578
)
7679

@@ -115,21 +118,31 @@ class TemplateChangeViewModel:
115118
"""A view model for resulting changes from a template set/update."""
116119

117120
def __init__(
118-
self, id: str, source: Optional[str], reference: Optional[str], version: Optional[str], file_changes: List[str]
121+
self,
122+
file_changes: List[str],
123+
id: str,
124+
old_id: Optional[str],
125+
reference: Optional[str],
126+
source: Optional[str],
127+
version: Optional[str],
119128
):
129+
self.file_changes = file_changes
120130
self.id: str = id
121-
self.source = source
131+
self.old_id: Optional[str] = old_id if old_id != id else ""
122132
self.reference = reference
133+
self.source = source
123134
self.version = version
124-
self.file_changes = file_changes
125135

126136
@classmethod
127-
def from_template(cls, template: RenderedTemplate, actions: Dict[str, FileAction]) -> "TemplateChangeViewModel":
137+
def from_template(
138+
cls, template: RenderedTemplate, actions: Dict[str, FileAction], old_id: Optional[str] = None
139+
) -> "TemplateChangeViewModel":
128140
"""Create view model from ``Template``.
129141
130142
Args:
131143
template(RenderedTemplate): Input rendered template.
132144
actions(Dict[str, FileAction]): Mapping of paths to actions taken when rendering the template.
145+
old_id(Optional[str]: Current template Id.
133146
134147
Returns:
135148
TemplateChangeViewModel: View model for the template change.
@@ -151,9 +164,10 @@ def from_template(cls, template: RenderedTemplate, actions: Dict[str, FileAction
151164
]
152165

153166
return cls(
167+
file_changes=file_changes,
154168
id=template.template.id,
155-
source=template.template.source,
169+
old_id=old_id,
156170
reference=template.template.reference,
171+
source=template.template.source,
157172
version=template.template.version,
158-
file_changes=file_changes,
159173
)

renku/core/init.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ def create_from_template_local(
376376

377377
template = Template(
378378
id=metadata["__template_id__"],
379+
aliases=[],
379380
name="",
380381
description="",
381382
parameters={},

renku/core/template/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
#
2-
# Copyright 2017-2023 - Swiss Data Science Center (SDSC)
3-
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
1+
# Copyright Swiss Data Science Center (SDSC). A partnership between
2+
# École Polytechnique Fédérale de Lausanne (EPFL) and
43
# Eidgenössische Technische Hochschule Zürich (ETHZ).
54
#
65
# Licensed under the Apache License, Version 2.0 (the "License");

renku/core/template/template.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
#
2-
# Copyright 2020 - Swiss Data Science Center (SDSC)
3-
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
1+
# Copyright Swiss Data Science Center (SDSC). A partnership between
2+
# École Polytechnique Fédérale de Lausanne (EPFL) and
43
# Eidgenössische Technische Hochschule Zürich (ETHZ).
54
#
65
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -383,14 +382,14 @@ def fetch(cls, source: Optional[str], reference: Optional[str]) -> "EmbeddedTemp
383382
from renku import __template_version__
384383

385384
template_path = importlib_resources.files("renku") / "templates"
386-
with importlib_resources.as_file(template_path) as folder:
387-
path = Path(folder)
385+
with importlib_resources.as_file(template_path) as templates:
386+
path = Path(templates)
388387

389388
return cls(path=path, source="renku", reference=__template_version__, version=__template_version__)
390389

391390
def get_all_references(self, id) -> List[str]:
392391
"""Return all available references for a template id."""
393-
template_exists = any(t.id == id for t in self.templates)
392+
template_exists = any(id == t.id or id in t.aliases for t in self.templates)
394393
return [self.reference] if template_exists and self.reference is not None else []
395394

396395
def get_latest_reference_and_version(
@@ -412,7 +411,7 @@ def get_latest_reference_and_version(
412411
def get_template(self, id, reference: Optional[str]) -> "Template":
413412
"""Return all available versions for a template id."""
414413
try:
415-
return next(t for t in self.templates if t.id == id)
414+
return next(t for t in self.templates if id == t.id or id in t.aliases)
416415
except StopIteration:
417416
raise errors.TemplateNotFoundError(f"The template with id '{id}' is not available.")
418417

@@ -495,7 +494,7 @@ def _has_template_at(self, id: str, reference: str) -> bool:
495494
except (errors.FileNotFound, errors.InvalidTemplateError):
496495
return False
497496
else:
498-
return any(t.id == id for t in manifest.templates)
497+
return any(id == t.id or id in t.aliases for t in manifest.templates)
499498

500499
def get_template(self, id, reference: Optional[str]) -> "Template":
501500
"""Return a template at a specific reference."""
@@ -515,8 +514,9 @@ def get_template(self, id, reference: Optional[str]) -> "Template":
515514
else:
516515
self.manifest = manifest
517516

518-
template = next((t for t in self.templates if t.id == id), None)
517+
template = next((t for t in self.templates if id == t.id or id in t.aliases), None)
519518
if template is None:
519+
reference = reference or "HEAD"
520520
raise errors.TemplateNotFoundError(f"The template with id '{id}' is not available at '{reference}'.")
521521

522522
return template

renku/core/template/usecase.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
#
2-
# Copyright 2020 - Swiss Data Science Center (SDSC)
3-
# A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
1+
# Copyright Swiss Data Science Center (SDSC). A partnership between
2+
# École Polytechnique Fédérale de Lausanne (EPFL) and
43
# Eidgenössische Technische Hochschule Zürich (ETHZ).
54
#
65
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -228,7 +227,9 @@ def update_template(force: bool, interactive: bool, dry_run: bool) -> Optional[T
228227
input_parameters=None,
229228
)
230229

231-
return TemplateChangeViewModel.from_template(template=rendered_template, actions=actions)
230+
return TemplateChangeViewModel.from_template(
231+
template=rendered_template, actions=actions, old_id=template_metadata.id
232+
)
232233

233234

234235
@inject.autoparams("project_gateway")

renku/domain_model/template.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def templates(self) -> List["Template"]:
130130
self._templates = [
131131
Template(
132132
id=cast(str, t.get("id") or t.get("folder")),
133+
aliases=t.get("aliases", []),
133134
name=cast(str, t.get("name")),
134135
description=cast(str, t.get("description")),
135136
parameters=cast(Dict[str, Dict[str, Any]], t.get("variables") or t.get("parameters")),
@@ -161,6 +162,8 @@ def validate(self, manifest_only: bool = False) -> List[str]:
161162
elif not isinstance(self._content, list):
162163
raise errors.InvalidTemplateError(f"Invalid manifest content type: '{type(self._content).__name__}'")
163164

165+
existing_ids: Set[str] = set()
166+
164167
# NOTE: First check if required fields exists for creating Template instances
165168
for template_entry in self._content:
166169
if not isinstance(template_entry, dict):
@@ -172,6 +175,15 @@ def validate(self, manifest_only: bool = False) -> List[str]:
172175
if not template_entry.get("id"):
173176
warnings.append(f"Template '{id}' should use 'id' attribute instead of 'folder'.")
174177

178+
# NOTE: Check for duplicate IDs and aliases
179+
aliases = {id}
180+
aliases.update(template_entry.get("aliases", []))
181+
duplicates = existing_ids.intersection(aliases)
182+
if duplicates:
183+
duplicates_str = ", ".join(sorted(f"'{d}'" for d in duplicates))
184+
raise errors.InvalidTemplateError(f"Found duplicate IDs or aliases: {duplicates_str}")
185+
existing_ids.update(aliases)
186+
175187
parameters = template_entry.get("variables")
176188
if parameters:
177189
if not isinstance(parameters, dict):
@@ -209,6 +221,7 @@ def __init__(
209221
parameters: Dict[str, Dict[str, Any]],
210222
icon: str,
211223
ssh_supported: bool,
224+
aliases: List[str],
212225
immutable_files: List[str],
213226
allow_update: bool,
214227
source: Optional[str],
@@ -226,6 +239,7 @@ def __init__(
226239
self.description: str = description
227240
self.icon = icon
228241
self.ssh_supported = ssh_supported
242+
self.aliases: List[str] = aliases
229243
self.immutable_files: List[str] = immutable_files or []
230244
self.allow_update: bool = allow_update
231245
parameters = parameters or {}
@@ -235,6 +249,9 @@ def __init__(
235249

236250
self._templates_source: Optional[TemplatesSource] = templates_source
237251

252+
def __repr__(self) -> str:
253+
return f"<Template {self.id}@{self.version}>"
254+
238255
@property
239256
def templates_source(self) -> Optional[TemplatesSource]:
240257
"""Return template's source."""

renku/ui/cli/template.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ def print_template_parameters():
334334
click.echo(print_name(" Default value: ") + print_value(p.default))
335335

336336
click.echo(print_name("Id: ") + print_value(template.id))
337+
click.echo(print_name("Id aliases: ") + print_value(template.aliases))
337338
click.echo(print_name("Name: ") + print_value(template.name))
338339
click.echo(print_name("Source: ") + print_value(template.source))
339340
click.echo(print_name("Reference: ") + print_value(to_string(template.reference)))
@@ -369,6 +370,7 @@ def _print_template_change(changes: "TemplateChangeViewModel"):
369370
print_value = functools.partial(click.style, bold=True)
370371

371372
click.echo(print_name("Id: ") + print_value(changes.id))
373+
click.echo(print_name("Old Id: ") + print_value(changes.old_id))
372374
click.echo(print_name("Source: ") + print_value(changes.source))
373375
click.echo(print_name("New reference: ") + print_value(to_string(changes.reference)))
374376
click.echo(print_name("New version: ") + print_value(to_string(changes.version)))

tests/cli/test_template.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,17 @@ def test_template_dockerfile_checksum_update(runner, project, with_injection):
480480

481481
assert not project.repository.is_dirty()
482482
assert __version__ in dockerfile.read_text()
483+
484+
485+
def test_template_update_with_renames(runner, project_with_template, templates_source, with_injection):
486+
"""Test updating a renamed template."""
487+
templates_source.rename(id="dummy", new_name="new-name", version="2.2.42")
488+
489+
result = runner.invoke(cli, ["template", "update"])
490+
491+
assert result.exit_code == 0, result.output
492+
493+
with with_injection():
494+
assert "2.2.42" == project_context.project.template_metadata.template_ref
495+
assert "new-name" == project_context.project.template_metadata.template_id
496+
assert not project_with_template.repository.is_dirty()

tests/core/models/test_template.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def test_templates_manifest():
8484
variables: {}
8585
icon: python.png
8686
- id: R
87+
aliases: ["R-minimal", "R-base"]
8788
name: R Project
8889
description: An R-based Renku project
8990
variables:
@@ -98,12 +99,14 @@ def test_templates_manifest():
9899
assert 2 == len(manifest.templates)
99100

100101
template = next(t for t in manifest.templates if t.id == "python")
102+
assert [] == template.aliases
101103
assert "Python Project" == template.name
102104
assert "A Python-based Renku project" == template.description
103105
assert "python.png" == template.icon
104106
assert [] == template.parameters
105107

106108
template = next(t for t in manifest.templates if t.id == "R")
109+
assert ["R-minimal", "R-base"] == template.aliases
107110
assert "R Project" == template.name
108111
assert "An R-based Renku project" == template.description
109112
assert "R.png" == template.icon
@@ -142,6 +145,7 @@ def test_templates_manifest_invalid_yaml(tmp_path):
142145
("- no-id: python", "Template doesn't have an id:"),
143146
("- id: python\n variables: p1", "Invalid template variable type on template 'python': 'str'"),
144147
("- id: python\n variables:\n p1: 42", "Invalid parameter type 'int' for 'p1'"),
148+
("- id: python\n name: Python\n aliases: [R]\n- id: R\n name: R\n", "Found duplicate IDs or aliases: 'R'"),
145149
],
146150
)
147151
def test_templates_manifest_invalid_content(tmp_path, content, message):

0 commit comments

Comments
 (0)