snakemake-interface-software-deployment-plugins 0.2.3__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (11) hide show
  1. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/PKG-INFO +1 -1
  2. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/pyproject.toml +2 -1
  3. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/snakemake_interface_software_deployment_plugins/__init__.py +95 -27
  4. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/snakemake_interface_software_deployment_plugins/registry/__init__.py +18 -1
  5. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/snakemake_interface_software_deployment_plugins/registry/plugin.py +2 -0
  6. snakemake_interface_software_deployment_plugins-0.4.0/snakemake_interface_software_deployment_plugins/settings.py +46 -0
  7. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/snakemake_interface_software_deployment_plugins/tests.py +25 -2
  8. snakemake_interface_software_deployment_plugins-0.2.3/snakemake_interface_software_deployment_plugins/settings.py +0 -17
  9. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/LICENSE +0 -0
  10. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/README.md +0 -0
  11. {snakemake_interface_software_deployment_plugins-0.2.3 → snakemake_interface_software_deployment_plugins-0.4.0}/snakemake_interface_software_deployment_plugins/_common.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snakemake-interface-software-deployment-plugins
3
- Version: 0.2.3
3
+ Version: 0.4.0
4
4
  Summary: This package provides a stable interface for interactions between Snakemake and its software deployment plugins.
5
5
  License: MIT
6
6
  Author: Johannes Köster
@@ -5,7 +5,7 @@ license = "MIT"
5
5
  name = "snakemake-interface-software-deployment-plugins"
6
6
  packages = [{include = "snakemake_interface_software_deployment_plugins"}]
7
7
  readme = "README.md"
8
- version = "0.2.3"
8
+ version = "0.4.0"
9
9
 
10
10
  [tool.poetry.dependencies]
11
11
  argparse-dataclass = "^2.0.0"
@@ -17,6 +17,7 @@ coverage = {extras = ["toml"], version = "^6.3.1"}
17
17
  flake8-bugbear = "^22.1.11"
18
18
  pytest = "^7.0"
19
19
  ruff = "^0.9.9"
20
+ snakemake-software-deployment-plugin-envmodules = "^0.1.2"
20
21
 
21
22
  [tool.coverage.run]
22
23
  omit = [".*", "*/site-packages/*"]
@@ -5,11 +5,10 @@ __license__ = "MIT"
5
5
 
6
6
  from abc import ABC, abstractmethod
7
7
  from copy import copy
8
- from dataclasses import dataclass, field, fields
8
+ from dataclasses import dataclass, field
9
9
  import hashlib
10
10
  from pathlib import Path
11
- import sys
12
- from typing import Any, ClassVar, Dict, Optional, Tuple, Type
11
+ from typing import Any, ClassVar, Dict, Iterable, Optional, Self, Tuple, Type
13
12
  import subprocess as sp
14
13
 
15
14
  from snakemake_interface_software_deployment_plugins.settings import (
@@ -17,28 +16,91 @@ from snakemake_interface_software_deployment_plugins.settings import (
17
16
  )
18
17
 
19
18
 
20
- _MANAGED_FIELDS = {
21
- "settings",
22
- "_managed_hash_store",
23
- "_managed_deployment_hash_store",
24
- "_obj_hash",
25
- }
19
+ @dataclass
20
+ class SoftwareReport:
21
+ name: str
22
+ version: Optional[str] = None
23
+ is_secondary: bool = False
26
24
 
27
25
 
28
- @dataclass
29
26
  class EnvSpecBase(ABC):
30
- within: Optional["EnvSpecBase"]
31
- fallback: Optional["EnvSpecBase"]
27
+ def __init__(self):
28
+ self.within: Optional["EnvSpecBase"] = None
29
+ self.fallback: Optional["EnvSpecBase"] = None
30
+ self.kind: str = self.__class__.__module__.common_settings.provides
31
+ self._obj_hash: Optional[int] = None
32
32
 
33
33
  @classmethod
34
34
  def env_cls(cls):
35
- return sys.modules[__name__].EnvBase
35
+ return cls.__module__.EnvBase
36
+
37
+ @abstractmethod
38
+ def identity_attributes(self) -> Iterable[str]:
39
+ """Yield the attributes of the subclass that uniquely identify the
40
+ environment spec. These are used for hashing and equality comparison.
41
+ """
42
+ ...
43
+
44
+ @abstractmethod
45
+ def source_path_attributes(self) -> Iterable[str]:
46
+ """Return iterable of attributes of the subclass that represent paths that are
47
+ supposed to be interpreted as being relative to the defining rule.
48
+
49
+ For example, this would be attributes pointing to conda environment files.
50
+ """
51
+ ...
52
+
53
+ def has_source_paths(self) -> bool:
54
+ if any(self.source_path_attributes()):
55
+ return True
56
+ if self.within is not None and self.within.has_source_paths():
57
+ return True
58
+ if self.fallback is not None and self.fallback.has_source_paths():
59
+ return True
60
+ return False
61
+
62
+ def modify_source_paths(self, modify_func) -> Self:
63
+ if self.has_source_paths():
64
+ self_or_copied = copy(self)
65
+ else:
66
+ return self
67
+ for attr in self_or_copied.source_path_attributes():
68
+ setattr(self_or_copied, attr, modify_func(getattr(self_or_copied, attr)))
69
+
70
+ if self_or_copied.within is not None:
71
+ self_or_copied.within = self_or_copied.within.modify_source_paths(
72
+ modify_func
73
+ )
74
+
75
+ if self_or_copied.fallback is not None:
76
+ self_or_copied.fallback = self_or_copied.fallback.modify_source_paths(
77
+ modify_func
78
+ )
79
+ return self_or_copied
36
80
 
37
81
  def __or__(self, other: "EnvSpecBase") -> "EnvSpecBase":
38
82
  copied = copy(self)
39
83
  copied.fallback = other
84
+ copied._obj_hash = None
40
85
  return copied
41
86
 
87
+ def managed_identity_attributes(self) -> Iterable[str]:
88
+ yield from self.identity_attributes()
89
+ yield "kind"
90
+ yield "within"
91
+ yield "fallback"
92
+
93
+ def __hash__(self) -> int:
94
+ return hash(
95
+ tuple(getattr(self, attr) for attr in self.managed_identity_attributes())
96
+ )
97
+
98
+ def __eq__(self, other) -> bool:
99
+ return self.__class__ == other.__class__ and all(
100
+ getattr(self, attr) == getattr(other, attr)
101
+ for attr in self.managed_identity_attributes()
102
+ )
103
+
42
104
 
43
105
  @dataclass
44
106
  class EnvBase:
@@ -80,9 +142,21 @@ class EnvBase:
80
142
 
81
143
  @abstractmethod
82
144
  def record_hash(self, hash_object) -> None:
83
- """Update given hash object such that it changes whenever the environment
84
- specified via self.spec could potentially contain a different set of
85
- software (in terms of versions or packages).
145
+ """Update given hash object (using hash_object.update()) such that it changes
146
+ whenever the environment specified via self.spec could potentially contain a
147
+ different set of software (in terms of versions or packages).
148
+ """
149
+ ...
150
+
151
+ @abstractmethod
152
+ def report_software(self) -> Iterable[SoftwareReport]:
153
+ """Report the software contained in the environment. This should be a list of
154
+ snakemake_interface_software_deployment_plugins.SoftwareReport data class.
155
+ Use SoftwareReport.is_secondary = True if the software is just some
156
+ less important technical dependency. This allows Snakemake's report to
157
+ hide those for clarity. In case of containers, it is also valid to
158
+ return the container URI as a "software".
159
+ Return an empty tuple () if no software can be reported.
86
160
  """
87
161
  ...
88
162
 
@@ -119,20 +193,14 @@ class EnvBase:
119
193
  def __hash__(self) -> int:
120
194
  # take the hash of all fields by settings, _managed_hash_store and _managed_deployment_hash_store
121
195
  if self._obj_hash is None:
122
- self._obj_hash = hash(
123
- tuple(
124
- getattr(self, field.name)
125
- for field in fields(self)
126
- if field.name not in _MANAGED_FIELDS
127
- )
128
- )
196
+ self._obj_hash = hash(self.hash())
129
197
  return self._obj_hash
130
198
 
131
199
  def __eq__(self, other) -> bool:
132
- return self.__class__ == other.__class__ and all(
133
- getattr(self, field.name) == getattr(other, field.name)
134
- for field in fields(self)
135
- if field.name not in _MANAGED_FIELDS
200
+ return (
201
+ self.__class__ == other.__class__
202
+ and self.spec == other.spec
203
+ and self.hash() == other.hash()
136
204
  )
137
205
 
138
206
 
@@ -6,6 +6,7 @@ __license__ = "MIT"
6
6
  import types
7
7
  from typing import Mapping
8
8
  from snakemake_interface_software_deployment_plugins.settings import (
9
+ CommonSettings,
9
10
  SoftwareDeploymentSettingsBase,
10
11
  )
11
12
 
@@ -16,7 +17,11 @@ from snakemake_interface_common.plugin_registry.attribute_types import (
16
17
  )
17
18
  from snakemake_interface_software_deployment_plugins.registry.plugin import Plugin
18
19
  from snakemake_interface_common.plugin_registry import PluginRegistryBase
19
- from snakemake_interface_software_deployment_plugins import EnvBase, _common as common
20
+ from snakemake_interface_software_deployment_plugins import (
21
+ EnvBase,
22
+ EnvSpecBase,
23
+ _common as common,
24
+ )
20
25
 
21
26
 
22
27
  class SoftwareDeploymentPluginRegistry(PluginRegistryBase):
@@ -30,14 +35,21 @@ class SoftwareDeploymentPluginRegistry(PluginRegistryBase):
30
35
  """Load a plugin by name."""
31
36
  return Plugin(
32
37
  _name=name,
38
+ common_settings=module.common_settings,
33
39
  _software_deployment_settings_cls=getattr(
34
40
  module, "SoftwareDeploymentSettings", None
35
41
  ),
36
42
  _env_cls=module.EnvBase,
43
+ _env_spec_cls=module.EnvSpecBase,
37
44
  )
38
45
 
39
46
  def expected_attributes(self) -> Mapping[str, AttributeType]:
40
47
  return {
48
+ "common_settings": AttributeType(
49
+ cls=CommonSettings,
50
+ mode=AttributeMode.REQUIRED,
51
+ kind=AttributeKind.OBJECT,
52
+ ),
41
53
  "SoftwareDeploymentSettings": AttributeType(
42
54
  cls=SoftwareDeploymentSettingsBase,
43
55
  mode=AttributeMode.OPTIONAL,
@@ -48,4 +60,9 @@ class SoftwareDeploymentPluginRegistry(PluginRegistryBase):
48
60
  mode=AttributeMode.REQUIRED,
49
61
  kind=AttributeKind.CLASS,
50
62
  ),
63
+ "EnvSpec": AttributeType(
64
+ cls=EnvSpecBase,
65
+ mode=AttributeMode.REQUIRED,
66
+ kind=AttributeKind.CLASS,
67
+ ),
51
68
  }
@@ -7,6 +7,7 @@ from dataclasses import dataclass
7
7
  from typing import Optional, Type
8
8
  from snakemake_interface_software_deployment_plugins import EnvBase, EnvSpecBase
9
9
  from snakemake_interface_software_deployment_plugins.settings import (
10
+ CommonSettings,
10
11
  SoftwareDeploymentSettingsBase,
11
12
  )
12
13
  import snakemake_interface_software_deployment_plugins._common as common
@@ -16,6 +17,7 @@ from snakemake_interface_common.plugin_registry.plugin import PluginBase
16
17
 
17
18
  @dataclass
18
19
  class Plugin(PluginBase):
20
+ common_settings: CommonSettings
19
21
  _software_deployment_settings_cls: Optional[Type[SoftwareDeploymentSettingsBase]]
20
22
  _env_cls: Type[EnvBase]
21
23
  _env_spec_cls: Type[EnvSpecBase]
@@ -0,0 +1,46 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ import snakemake_interface_common.plugin_registry.plugin
5
+
6
+
7
+ @dataclass
8
+ class SoftwareDeploymentSettingsBase(
9
+ snakemake_interface_common.plugin_registry.plugin.SettingsBase
10
+ ):
11
+ """Base class for software deployment settings.
12
+
13
+ Software deployment plugins can define a subclass of this class,
14
+ named 'SoftwareDeploymentProviderSettings'.
15
+ """
16
+
17
+ pass
18
+
19
+
20
+ @dataclass
21
+ class CommonSettings:
22
+ """Common settings for software deployment plugins.
23
+
24
+ This class is used to define common settings for software deployment plugins.
25
+
26
+ Attributes
27
+ ----------
28
+ provides : str
29
+ The kind of the software environment provided (e.g. conda, container).
30
+ This should not return something describing the tool to provide the software
31
+ environment but the resulting environment itself. For example,
32
+ it should return "conda" instead of mamba, rattler, pixi etc., or
33
+ "container" instead of docker, singularity, podman, or
34
+ "envmodules" instead of lmod, environment-modules, etc.
35
+ Snakemake will ensure that the user only activates one plugin per provided
36
+ kind.
37
+ """
38
+
39
+ provides: str
40
+
41
+ def __post_init__(self):
42
+ if not self.provides.isidentifier():
43
+ raise ValueError(
44
+ "CommonSettings.provides must be a valid Python identifier, but "
45
+ f"is {self.provides}."
46
+ )
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from pathlib import Path
2
3
  from typing import Optional, Type
3
4
  import subprocess as sp
4
5
 
@@ -9,6 +10,7 @@ from snakemake_interface_software_deployment_plugins import (
9
10
  DeployableEnvBase,
10
11
  EnvBase,
11
12
  EnvSpecBase,
13
+ SoftwareReport,
12
14
  )
13
15
  from snakemake_interface_software_deployment_plugins.settings import (
14
16
  SoftwareDeploymentSettingsBase,
@@ -46,10 +48,10 @@ class TestSoftwareDeploymentBase(ABC):
46
48
  """
47
49
  ...
48
50
 
51
+ @abstractmethod
49
52
  def get_software_deployment_provider_settings(
50
53
  self,
51
- ) -> Optional[SoftwareDeploymentSettingsBase]:
52
- return None
54
+ ) -> Optional[SoftwareDeploymentSettingsBase]: ...
53
55
 
54
56
  def test_shellcmd(self, tmp_path):
55
57
  env = self._get_env(tmp_path)
@@ -83,6 +85,27 @@ class TestSoftwareDeploymentBase(ABC):
83
85
  env.archive()
84
86
  assert any((tmp_path / "{_TEST_SDM_NAME}-archive").iterdir())
85
87
 
88
+ def test_report_software(self, tmp_path):
89
+ env = self._get_env(tmp_path)
90
+ rep = env.report_software()
91
+ assert all(isinstance(s, SoftwareReport) for s in rep)
92
+
93
+ def test_identity_attributes(self):
94
+ spec = self.get_env_spec()
95
+ assert all(
96
+ isinstance(attr, str) and hasattr(spec, attr)
97
+ for attr in spec.identity_attributes()
98
+ )
99
+
100
+ def test_source_path_attributes(self):
101
+ spec = self.get_env_spec()
102
+ assert all(
103
+ isinstance(attr, str)
104
+ and hasattr(spec, attr)
105
+ and isinstance(getattr(spec, attr), (Path, str))
106
+ for attr in spec.source_path_attributes()
107
+ )
108
+
86
109
  def _get_env(self, tmp_path) -> EnvBase:
87
110
  env_cls = self.get_env_cls()
88
111
  spec = self.get_env_spec()
@@ -1,17 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
-
4
- import snakemake_interface_common.plugin_registry.plugin
5
-
6
-
7
- @dataclass
8
- class SoftwareDeploymentSettingsBase(
9
- snakemake_interface_common.plugin_registry.plugin.SettingsBase
10
- ):
11
- """Base class for software deployment settings.
12
-
13
- Software deployment plugins can define a subclass of this class,
14
- named 'SoftwareDeploymentProviderSettings'.
15
- """
16
-
17
- pass