Skip to content

Commit c44da20

Browse files
committed
image-puller: support singularity-ce & apptainer
1 parent cd01f51 commit c44da20

File tree

9 files changed

+110
-23
lines changed

9 files changed

+110
-23
lines changed

.github/workflows/ci-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
- name: Upgrade setuptools and install tox
5555
run: |
5656
pip install -U pip setuptools wheel
57-
pip install 'tox<4' tox-gh-actions
57+
pip install "tox<4" "tox-gh-actions<3"
5858
5959
- name: MyPy cache
6060
if: ${{ matrix.step == 'mypy' }}
@@ -100,7 +100,7 @@ jobs:
100100
- name: Upgrade setuptools and install tox
101101
run: |
102102
pip install -U pip setuptools wheel
103-
pip install 'tox<4' tox-gh-actions
103+
pip install "tox<4" "tox-gh-actions<3"
104104
105105
- if: ${{ matrix.step == 'pydocstyle' && github.event_name == 'pull_request'}}
106106
name: Create local branch for diff-quality for PRs

cwl_utils/image_puller.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from abc import ABC, abstractmethod
77
from typing import List
88

9+
from .singularity import get_version as get_singularity_version
10+
from .singularity import is_version_2_6 as is_singularity_version_2_6
11+
from .singularity import is_version_3_or_newer as is_singularity_version_3_or_newer
12+
913
logging.basicConfig(level=logging.INFO)
1014
_LOGGER = logging.getLogger(__name__)
1115

@@ -79,27 +83,15 @@ class SingularityImagePuller(ImagePuller):
7983
def __init__(self, req: str, save_directory: str) -> None:
8084
"""Create a Singularity-based software container image downloader."""
8185
super().__init__(req, save_directory)
82-
version = subprocess.check_output( # nosec
83-
["singularity", "--version"], universal_newlines=True
84-
)
85-
if version.startswith("singularity version "):
86-
version = version[20:]
87-
self.version = version
88-
89-
def _is_version_2_6(self) -> bool:
90-
return self.version.startswith("2.6")
91-
92-
def _is_version_3_or_newer(self) -> bool:
93-
return int(self.version[0]) >= 3
9486

9587
def get_image_name(self) -> str:
9688
"""Determine the file name appropriate to the installed version of Singularity."""
9789
image_name = self.req
9890
for char in self.CHARS_TO_REPLACE:
9991
image_name = image_name.replace(char, self.NEW_CHAR)
100-
if self._is_version_2_6():
92+
if is_singularity_version_2_6():
10193
suffix = ".img"
102-
elif self._is_version_3_or_newer():
94+
elif is_singularity_version_3_or_newer():
10395
suffix = ".sif"
10496
else:
10597
raise Exception(

cwl_utils/parser/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from ..errors import GraphTargetMissingException
1212
from . import cwl_v1_0, cwl_v1_1, cwl_v1_2
1313

14-
1514
LoadingOptions = Union[
1615
cwl_v1_0.LoadingOptions, cwl_v1_1.LoadingOptions, cwl_v1_2.LoadingOptions
1716
]

cwl_utils/parser/cwl_v1_0_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import hashlib
44
import logging
55
from collections import namedtuple
6-
from typing import Any, Dict, IO, List, MutableSequence, Optional, Tuple, Union, cast
6+
from typing import IO, Any, Dict, List, MutableSequence, Optional, Tuple, Union, cast
77

88
from ruamel import yaml
99
from schema_salad.exceptions import ValidationException

cwl_utils/parser/cwl_v1_1_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import hashlib
44
import logging
55
from collections import namedtuple
6-
from typing import Any, Dict, IO, List, MutableSequence, Optional, Tuple, Union, cast
6+
from typing import IO, Any, Dict, List, MutableSequence, Optional, Tuple, Union, cast
77

88
from ruamel import yaml
99
from schema_salad.exceptions import ValidationException

cwl_utils/parser/cwl_v1_2_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import hashlib
44
import logging
55
from collections import namedtuple
6-
from typing import Any, Dict, IO, List, MutableSequence, Optional, Tuple, Union, cast
6+
from typing import IO, Any, Dict, List, MutableSequence, Optional, Tuple, Union, cast
77

88
from ruamel import yaml
99
from schema_salad.exceptions import ValidationException

cwl_utils/parser/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from schema_salad.utils import json_dumps
1010

1111
import cwl_utils
12+
1213
from . import (
1314
Process,
1415
Workflow,

cwl_utils/singularity.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Support Singularity{,-CE} {2,3}.x or Apptainer 1.x."""
2+
import re
3+
from subprocess import check_output # nosec
4+
from typing import List, Optional, Tuple
5+
6+
from .loghandler import _logger
7+
8+
# Cached version number of singularity
9+
# This is a list containing major and minor versions as integer.
10+
# (The number of minor version digits can vary among different distributions,
11+
# therefore we need a list here.)
12+
_SINGULARITY_VERSION: Optional[List[int]] = None
13+
# Cached flavor / distribution of singularity
14+
# Can be singularity, singularity-ce or apptainer
15+
_SINGULARITY_FLAVOR: str = ""
16+
17+
18+
def get_version() -> Tuple[List[int], str]:
19+
"""
20+
Parse the output of 'singularity --version' to determine the flavor and version.
21+
22+
Both pieces of information will be cached.
23+
24+
:returns: A tuple containing:
25+
- A tuple with major and minor version numbers as integer.
26+
- A string with the name of the singularity flavor.
27+
"""
28+
global _SINGULARITY_VERSION # pylint: disable=global-statement
29+
global _SINGULARITY_FLAVOR # pylint: disable=global-statement
30+
if _SINGULARITY_VERSION is None:
31+
version_output = check_output( # nosec
32+
["singularity", "--version"], universal_newlines=True
33+
).strip()
34+
35+
version_match = re.match(r"(.+) version ([0-9\.]+)", version_output)
36+
if version_match is None:
37+
raise RuntimeError("Output of 'singularity --version' not recognized.")
38+
39+
version_string = version_match.group(2)
40+
_SINGULARITY_VERSION = [int(i) for i in version_string.split(".")]
41+
_SINGULARITY_FLAVOR = version_match.group(1)
42+
43+
_logger.debug(
44+
f"Singularity version: {version_string}" " ({_SINGULARITY_FLAVOR}."
45+
)
46+
return (_SINGULARITY_VERSION, _SINGULARITY_FLAVOR)
47+
48+
49+
def is_apptainer_1_or_newer() -> bool:
50+
"""
51+
Check if apptainer singularity distribution is version 1.0 or higher.
52+
53+
Apptainer v1.0.0 is compatible with SingularityCE 3.9.5.
54+
See: https://github.com/apptainer/apptainer/releases
55+
"""
56+
v = get_version()
57+
if v[1] != "apptainer":
58+
return False
59+
return v[0][0] >= 1
60+
61+
62+
def is_version_2_6() -> bool:
63+
"""
64+
Check if this singularity version is exactly version 2.6.
65+
66+
Also returns False if the flavor is not singularity or singularity-ce.
67+
"""
68+
v = get_version()
69+
if v[1] != "singularity" and v[1] != "singularity-ce":
70+
return False
71+
return v[0][0] == 2 and v[0][1] == 6
72+
73+
74+
def is_version_3_or_newer() -> bool:
75+
"""Check if this version is singularity version 3 or newer or equivalent."""
76+
if is_apptainer_1_or_newer():
77+
return True # this is equivalent to singularity-ce > 3.9.5
78+
v = get_version()
79+
return v[0][0] >= 3
80+
81+
82+
def is_version_3_1_or_newer() -> bool:
83+
"""Check if this version is singularity version 3.1 or newer or equivalent."""
84+
if is_apptainer_1_or_newer():
85+
return True # this is equivalent to singularity-ce > 3.9.5
86+
v = get_version()
87+
return v[0][0] >= 4 or (v[0][0] == 3 and v[0][1] >= 1)
88+
89+
90+
def is_version_3_4_or_newer() -> bool:
91+
"""Detect if Singularity v3.4+ is available."""
92+
if is_apptainer_1_or_newer():
93+
return True # this is equivalent to singularity-ce > 3.9.5
94+
v = get_version()
95+
return v[0][0] >= 4 or (v[0][0] == 3 and v[0][1] >= 4)

tox.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ commands =
5454
py{37,38,39,310,311}-mypy: make mypy
5555
py37-mypy: make mypy_3.6
5656

57-
whitelist_externals =
57+
allowlist_externals =
5858
py{36,37,38,39,310,311}-lint: flake8
5959
py{36,37,38,39,310,311}-lint: black
6060
py{36,37,38,39,310,311}-{mypy,shellcheck,lint,unit}: make
@@ -67,7 +67,7 @@ extras =
6767
py{36,37,38,39,310,311}-unit: pretty
6868

6969
[testenv:py311-pydocstyle]
70-
whitelist_externals = make
70+
allowlist_externals = make
7171
commands = make diff_pydocstyle_report
7272
deps =
7373
pydocstyle
@@ -83,5 +83,5 @@ commands =
8383
deps =
8484
twine
8585
wheel
86-
readme_renderer[md]
86+
readme_renderer[rst]
8787
skip_install = true

0 commit comments

Comments
 (0)