Skip to content

Commit 0ef1d6b

Browse files
muffatovsoch
andauthored
Implemented "default_version==false" for TCL (#517)
* Implemented "default_version" for TCL * refactor to use shared logic for default version template loading I am also adding tests for both tcl and lmod default versions * correct default version cases * refactor to allow default_version to have multiple options * change order of defaults for default_version * proposal for handling default version on uninstall instead of parsing each case, we can simply pretend that the last module installed (determined by the creation or modify date) was just installed, and for all variants of default_version this will work as it will either write this version as latest, or just keep the dummy version. The exception is for first installed, for which we need to pass a boolean that says "force calling this the first install." of course linux does not do creation date well, so we do our best with modify date instead. Note I have not tested this but am pushing the commit to further discuss! * Clarified the logic in write_version_file and update_version_file - New _set_default_version method to set the default version - write_version_file and update_version_file now decide which is the first/last installed version (no more _default_version_first_installed) * Expanded _cleanup to delete directories if they only contain ".version" * TCL is now supported * Merged update_version_file into write_version_file * write_version_file can now detect the first/last version on its own * Renamed the config file value to "module_sys" to match the other option * Made can_be_deleted more generic, by taking the list of files to ignore as a parameter * Reintroduced the tag parameter Made it explicit that the tag write_version_file gets represents the version being installed * Renamed _sys_module_default_version to _module_sys_default_version for consistency * We can set default as None since it's not required. * Use `latest_tag_installed` if possible Co-authored-by: Matthieu Muffato <[email protected]> Co-authored-by: vsoch <[email protected]>
1 parent 6fbddb7 commit 0ef1d6b

File tree

12 files changed

+258
-22
lines changed

12 files changed

+258
-22
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,4 @@
290290

291291

292292
def setup(app):
293-
app.add_stylesheet("sphinx-argparse.css")
293+
app.add_css_file("sphinx-argparse.css")

docs/getting_started/developer-guide.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ For these global scripts, the user can select to use it in their settings.yaml.
257257
We will eventually write a command to list global wrappers available, so if you add a new one future users will know
258258
about it. For alias wrapper scripts, the following variables are passed for rendering:
259259

260-
.. list-table:: Title
260+
.. list-table:: Wrapper Script Variables
261261
:widths: 15 15 40 30
262262
:header-rows: 1
263263

@@ -306,7 +306,7 @@ If you want to write a custom container.yaml script:
306306

307307
The following variables are passed for rendering.
308308

309-
.. list-table:: Title
309+
.. list-table:: Container YAML Alias Variables
310310
:widths: 15 15 40 30
311311
:header-rows: 1
312312

@@ -523,7 +523,7 @@ Registry Yaml Fields
523523

524524
Fields include:
525525

526-
.. list-table:: Title
526+
.. list-table:: Registry YAML Fields
527527
:widths: 25 65 10
528528
:header-rows: 1
529529

@@ -571,7 +571,7 @@ A complete table of features is shown here. The
571571

572572
Fields include:
573573

574-
.. list-table:: Title
574+
.. list-table:: Features
575575
:widths: 20 20 20 10 10 10
576576
:header-rows: 1
577577

docs/getting_started/user-guide.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ variable replacement. A summary table of variables is included below, and then f
178178
- a timestamp to keep track of when you last saved
179179
- never
180180
* - default_version
181-
- A boolean to indicate generating a .version file (LMOD or lua modules only)
182-
- true
181+
- Should a default version be used?
182+
- module_sys
183183
* - singularity_module
184184
- if defined, add to module script to load this Singularity module first
185185
- null
@@ -360,6 +360,18 @@ you can add or remove entries via the config variable ``registry``
360360
and "set" is used to set a key value pair.
361361

362362

363+
Default Version
364+
---------------
365+
366+
The default version setting is there to support you telling shpc how you want module versions to be selected.
367+
There are four options:
368+
369+
- ``null`` do not set any kind of default version, it will be manually controlled by the installer (``false`` also supported for backwards compatibility)
370+
- ``module_sys``: allow the module software to choose (``true`` also supported for backwards compatibility)
371+
- ``last_installed``: always set default version to the last version installed
372+
- ``first_installed``: only set default version for the first installed
373+
374+
363375
Module Names
364376
------------
365377

shpc/main/modules/base.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
from datetime import datetime
1414
import os
15-
from pathlib import Path
1615
import shutil
1716
import subprocess
1817
import sys
@@ -23,6 +22,8 @@
2322

2423
class ModuleBase(BaseClient):
2524
def __init__(self, **kwargs):
25+
26+
# Files for module software to generate depending on user setting
2627
super(ModuleBase, self).__init__(**kwargs)
2728
self.here = os.path.dirname(inspect.getfile(self.__class__))
2829

@@ -68,7 +69,7 @@ def _cleanup(self, module_dir):
6869

6970
# If directories above it are empty, remove
7071
while module_dir != self.settings.module_base:
71-
if not os.path.exists(module_dir) or os.listdir(module_dir):
72+
if not utils.can_be_deleted(module_dir, [".version"]):
7273
break
7374
shutil.rmtree(module_dir)
7475
module_dir = os.path.dirname(module_dir)
@@ -125,6 +126,13 @@ def _uninstall(self, module_dir, name, force=False):
125126
else:
126127
logger.warning("%s does not exist." % name)
127128

129+
# parent of versioned directory has module .version
130+
module_dir = os.path.dirname(module_dir)
131+
132+
# update the default version file, if other versions still present
133+
if os.path.exists(module_dir):
134+
self.write_version_file(module_dir)
135+
128136
def _test_setup(self, tmpdir):
129137
"""
130138
Setup tests, including changes to settings or test directory
@@ -353,12 +361,8 @@ def install(self, name, tag=None, **kwargs):
353361
shutil.copyfile(container_path, container_dest)
354362
container_path = container_dest
355363

356-
# Add a .version file to indicate the level of versioning (not for tcl)
357-
if self.module_extension != "tcl" and self.settings.default_version == True:
358-
version_dir = os.path.join(self.settings.module_base, uri)
359-
version_file = os.path.join(version_dir, ".version")
360-
if not os.path.exists(version_file):
361-
Path(version_file).touch()
364+
# Add a .version file to indicate the level of versioning
365+
self.write_version_file(uri, tag.name)
362366

363367
# For Singularity this is a path, podman is a uri. If None is returned
364368
# there was an error and we cleanup
@@ -410,3 +414,53 @@ def install(self, name, tag=None, **kwargs):
410414
name = "%s:%s" % (name, tag.name)
411415
logger.info("Module %s was created." % name)
412416
return container_path
417+
418+
# Module software can choose how to handle each of these cases
419+
def _no_default_version(self, version_file, tag):
420+
return
421+
422+
def _module_sys_default_version(self, version_file, tag):
423+
return
424+
425+
def _set_default_version(self, version_file, tag):
426+
"""
427+
Set the default version to the given tag
428+
"""
429+
template = self._load_template("default_version")
430+
utils.write_file(version_file, template.render(version=tag))
431+
432+
def write_version_file(self, uri, latest_tag_installed=None):
433+
"""
434+
Create the .version file, if there is a template for it.
435+
"""
436+
version_dir = os.path.join(self.settings.module_base, uri)
437+
version_file = os.path.join(version_dir, ".version")
438+
439+
# No default versions
440+
if self.settings.default_version in [False, None]:
441+
return self._no_default_version(version_file, latest_tag_installed)
442+
443+
# allow the module software to control versions
444+
if self.settings.default_version in [True, "module_sys"]:
445+
return self._module_sys_default_version(version_file, latest_tag_installed)
446+
447+
# First or last installed
448+
if latest_tag_installed and (self.settings.default_version == "last_installed"):
449+
tag = latest_tag_installed
450+
else:
451+
# The versions we actually have
452+
found = [x for x in os.listdir(version_dir) if x != ".version"]
453+
if len(found) == 1:
454+
tag = found[0]
455+
else:
456+
if self.settings.default_version == "first_installed":
457+
selector = min
458+
else:
459+
selector = max
460+
tag = selector(
461+
found,
462+
key=lambda x: utils.creation_date(os.path.join(version_dir, x)),
463+
)
464+
465+
# Write the .version file
466+
return self._set_default_version(version_file, tag)

shpc/main/modules/lmod.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__license__ = "MPL 2.0"
44

55
from .base import ModuleBase
6+
import shpc.utils as utils
67

78

89
class Client(ModuleBase):
@@ -12,3 +13,13 @@ def __init__(self, **kwargs):
1213
"""
1314
super(Client, self).__init__(**kwargs)
1415
self.module_extension = "lua"
16+
17+
def _module_sys_default_version(self, version_file, tag=None):
18+
"""
19+
default version (default version in module_sys or True).
20+
We generate a file with a non-existent version number.
21+
"""
22+
template = self._load_template("default_version")
23+
utils.write_file(version_file, template.render())
24+
25+
# LMOD False or null, don't generate a .version file

shpc/main/modules/tcl.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__license__ = "MPL 2.0"
44

55
from .base import ModuleBase
6+
import shpc.utils as utils
67

78

89
class Client(ModuleBase):
@@ -12,3 +13,13 @@ def __init__(self, **kwargs):
1213
"""
1314
super(Client, self).__init__(**kwargs)
1415
self.module_extension = "tcl"
16+
17+
def _no_default_version(self, version_file, tag=None):
18+
"""
19+
No default version (default version in False or None).
20+
We generate a file with a non-existent version number.
21+
"""
22+
template = self._load_template("default_version")
23+
utils.write_file(version_file, template.render())
24+
25+
# TCL module_sys or True default version, don't generate a .version file
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#%Module
2+
set ModulesVersion "{% if version %}{{ version }}{% else %}please_specify_a_version_number{% endif %}"

shpc/main/schemas.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,15 @@
129129
"module_name": {"type": "string"},
130130
"config_editor": {"type": "string"},
131131
"environment_file": {"type": "string"},
132-
"default_version": {"type": "boolean"},
132+
"default_version": {
133+
"oneOf": [
134+
{"type": ["null", "boolean"]},
135+
{
136+
"type": "string",
137+
"enum": ["module_sys", "last_installed", "first_installed"],
138+
},
139+
]
140+
},
133141
"enable_tty": {"type": "boolean"},
134142
"wrapper_scripts": wrapper_scripts,
135143
"container_tech": {"type": "string", "enum": ["singularity", "podman", "docker"]},

shpc/settings.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ module_base: $root_dir/modules
2727
# This is where you might add a prefix to your module names, if desired.
2828
module_name: '{{ parsed_name.tool }}'
2929

30-
# Create a .version file for LMOD in the module folder
31-
default_version: true
30+
# When multiple versions are available and none requested, allow module picking one itself
31+
# module_sys: allow the module software to decide
32+
# null: do nothing, versions will be specified manually
33+
# last_installed: use the last installed as the default
34+
# first_installed: use the first installed
35+
default_version: module_sys
3236

3337
# store containers separately from module files
3438
# It's recommended to do this for faster loading

shpc/tests/test_client.py

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,111 @@ def test_features(tmp_path, module_sys, module_file):
9999
assert "--nv" in content
100100

101101

102+
@pytest.mark.parametrize(
103+
"default_version",
104+
[True, False, "module_sys", None, "first_installed", "last_installed"],
105+
)
106+
def test_tcl_default_version(tmp_path, default_version):
107+
"""
108+
Test tcl default versions.
109+
110+
True or module_sys: no .version file
111+
False or None: .version file with faux number
112+
first_installed: we maintain first installed version number
113+
last_installed: version is updated to last installed
114+
"""
115+
client = init_client(str(tmp_path), "tcl", "singularity")
116+
117+
# Customize config settings
118+
client.settings.set("default_version", default_version)
119+
120+
# Install known tag
121+
client.install("python:3.9.2-alpine")
122+
123+
# Get paths
124+
module_dir = os.path.join(client.settings.module_base, "python")
125+
version_file = os.path.join(module_dir, ".version")
126+
127+
if default_version in ["module_sys", True]:
128+
assert not os.path.exists(version_file)
129+
130+
elif default_version in [False, None]:
131+
assert os.path.exists(version_file)
132+
content = shpc.utils.read_file(version_file)
133+
assert "please_specify_a_version_number" in content
134+
135+
elif default_version == "first_installed":
136+
assert os.path.exists(version_file)
137+
content = shpc.utils.read_file(version_file)
138+
assert "3.9.2-alpine" in content
139+
client.install("python:3.9.5-alpine")
140+
content = shpc.utils.read_file(version_file)
141+
assert "3.9.2-alpine" in content
142+
143+
elif default_version == "last_installed":
144+
assert os.path.exists(version_file)
145+
content = shpc.utils.read_file(version_file)
146+
assert "3.9.2-alpine" in content
147+
client.install("python:3.9.5-alpine")
148+
content = shpc.utils.read_file(version_file)
149+
assert "3.9.5-alpine" in content
150+
151+
152+
@pytest.mark.parametrize(
153+
"default_version",
154+
[True, False, "module_sys", None, "first_installed", "last_installed"],
155+
)
156+
def test_lmod_default_version(tmp_path, default_version):
157+
"""
158+
Test lmod (lua) default versions.
159+
160+
True or module_sys: file with non-existent version number
161+
False or None: no .version file
162+
first_installed: we maintain first installed version number
163+
last_installed: version is updated to last installed
164+
"""
165+
client = init_client(str(tmp_path), "lmod", "singularity")
166+
167+
# Customize config settings
168+
client.settings.set("default_version", default_version)
169+
170+
# Install known tag
171+
client.install("python:3.9.2-alpine")
172+
173+
# Get paths
174+
module_dir = os.path.join(client.settings.module_base, "python")
175+
version_file = os.path.join(module_dir, ".version")
176+
177+
if default_version in ["module_sys", True]:
178+
assert os.path.exists(version_file)
179+
content = shpc.utils.read_file(version_file)
180+
assert "please_specify_a_version_number" in content
181+
182+
elif default_version in [False, None]:
183+
assert not os.path.exists(version_file)
184+
185+
elif default_version == "first_installed":
186+
assert os.path.exists(version_file)
187+
content = shpc.utils.read_file(version_file)
188+
assert "3.9.2-alpine" in content
189+
client.install("python:3.9.5-alpine")
190+
content = shpc.utils.read_file(version_file)
191+
assert "3.9.2-alpine" in content
192+
193+
elif default_version == "last_installed":
194+
assert os.path.exists(version_file)
195+
content = shpc.utils.read_file(version_file)
196+
assert "3.9.2-alpine" in content
197+
client.install("python:3.9.5-alpine")
198+
content = shpc.utils.read_file(version_file)
199+
assert "3.9.5-alpine" in content
200+
201+
102202
@pytest.mark.parametrize("module_sys", [("lmod"), ("tcl")])
103203
def test_docgen(tmp_path, module_sys):
104-
"""Test docgen"""
204+
"""
205+
Test docgen
206+
"""
105207
client = init_client(str(tmp_path), module_sys, "singularity")
106208
client.install("python:3.9.2-slim")
107209
out = io.StringIO()
@@ -133,7 +235,9 @@ def test_inspect(tmp_path, module_sys, container_tech):
133235

134236
@pytest.mark.parametrize("module_sys", [("lmod"), ("tcl")])
135237
def test_namespace_and_show(tmp_path, module_sys):
136-
"""Test namespace and show"""
238+
"""
239+
Test namespace and show
240+
"""
137241
client = init_client(str(tmp_path), module_sys, "singularity")
138242
client.show("vanessa/salad:latest")
139243

@@ -154,7 +258,9 @@ def test_namespace_and_show(tmp_path, module_sys):
154258
],
155259
)
156260
def test_check(tmp_path, module_sys, container_tech):
157-
"""Test check"""
261+
"""
262+
Test check
263+
"""
158264
client = init_client(str(tmp_path), module_sys, container_tech)
159265
client.install("vanessa/salad:latest")
160266
client.check("vanessa/salad:latest")

0 commit comments

Comments
 (0)