88from dataclasses import dataclass , field
99import hashlib
1010from pathlib import Path
11+ import shutil
1112from typing import Any , Dict , Iterable , Optional , Self , Tuple , Type , Union
1213import subprocess as sp
1314
1415from snakemake_interface_software_deployment_plugins .settings import (
1516 SoftwareDeploymentSettingsBase ,
1617)
18+ from snakemake_interface_common .exceptions import WorkflowError
1719
1820
1921@dataclass
@@ -115,6 +117,11 @@ def __eq__(self, other) -> bool:
115117 for attr in self .managed_identity_attributes ()
116118 )
117119
120+ @abstractmethod
121+ def __str__ (self ) -> str :
122+ """Return a string representation of the environment spec."""
123+ ...
124+
118125
119126class EnvBase (ABC ):
120127 _cache : Dict [Tuple [Type ["EnvBase" ], Optional ["EnvBase" ]], Any ] = {}
@@ -126,8 +133,9 @@ def __init__(
126133 settings : Optional [SoftwareDeploymentSettingsBase ],
127134 shell_executable : str ,
128135 tempdir : Path ,
129- deployment_prefix : Optional [Path ] = None ,
130- archive_prefix : Optional [Path ] = None ,
136+ cache_prefix : Path ,
137+ deployment_prefix : Path ,
138+ pinfile_prefix : Path ,
131139 ):
132140 self .spec : EnvSpecBase = spec
133141 self .within : Optional ["EnvBase" ] = within
@@ -137,8 +145,9 @@ def __init__(
137145 self ._managed_hash_store : Optional [str ] = None
138146 self ._managed_deployment_hash_store : Optional [str ] = None
139147 self ._obj_hash : Optional [int ] = None
140- self ._deployment_prefix : Optional [Path ] = deployment_prefix
141- self ._archive_prefix : Optional [Path ] = archive_prefix
148+ self ._deployment_prefix : Path = deployment_prefix
149+ self ._cache_prefix : Path = cache_prefix
150+ self ._pinfile_prefix : Path = pinfile_prefix
142151 self .__post_init__ ()
143152
144153 def __post_init__ (self ) -> None : # noqa B027
@@ -232,9 +241,72 @@ def __eq__(self, other) -> bool:
232241 )
233242
234243
244+ class PinnableEnvBase (ABC ):
245+ @classmethod
246+ @abstractmethod
247+ def pinfile_extension (cls ) -> str : ...
248+
249+ @abstractmethod
250+ def pin (self ) -> None :
251+ """Pin the environment to potentially more concrete versions than defined.
252+ Only implement this base class if pinning makes sense for your kind of
253+ environment. Pinfile has to be written to self.pinfile.
254+ """
255+ ...
256+
257+ @property
258+ def pinfile (self ) -> Path :
259+ assert isinstance (self , EnvBase )
260+ ext = self .pinfile_extension ()
261+ if not ext .startswith ("." ):
262+ raise ValueError ("pinfile_extension must start with a dot." )
263+ return (self ._pinfile_prefix / self .hash ()).with_suffix (
264+ self .pinfile_extension ()
265+ )
266+
267+
268+ class CacheableEnvBase (ABC ):
269+ async def get_cache_assets (self ) -> Iterable [str ]: ...
270+
271+ @abstractmethod
272+ def cache_assets (self ) -> None :
273+ """Determine environment assets and store any associated information or data to
274+ self.cache_path.
275+ """
276+ ...
277+
278+ @property
279+ def cache_path (self ) -> Path :
280+ assert isinstance (self , EnvBase )
281+ return self ._cache_prefix
282+
283+ async def remove_cache (self ) -> None :
284+ """Remove the cached environment assets."""
285+ assert isinstance (self , EnvBase )
286+ for asset in await self .get_cache_assets ():
287+ asset_path = self .cache_path / asset
288+ if asset_path .exists ():
289+ try :
290+ if asset_path .is_dir ():
291+ shutil .rmtree (asset_path )
292+ else :
293+ asset_path .unlink ()
294+ except Exception as e :
295+ raise WorkflowError (
296+ f"Removal of cache asset { asset_path } for { self .spec } failed: { e } "
297+ )
298+
299+
235300class DeployableEnvBase (ABC ):
236301 @abstractmethod
237- def is_deployment_path_portable (self ) -> bool : ...
302+ def is_deployment_path_portable (self ) -> bool :
303+ """Return whether the deployment path matters for the environment, i.e.
304+ whether the environment is portable. If this returns False, the deployment
305+ path is considered for the deployment hash. For example, conda environments are not
306+ portable because they hardcode the path in binaries, while containers are
307+ portable.
308+ """
309+ ...
238310
239311 @abstractmethod
240312 async def deploy (self ) -> None :
@@ -253,6 +325,7 @@ def record_deployment_hash(self, hash_object) -> None:
253325 deployment is senstivive to the path (e.g. in case of conda, which patches
254326 the RPATH in binaries).
255327 """
328+ assert isinstance (self , EnvBase )
256329 self .record_hash (hash_object )
257330 if not self .is_deployment_path_portable ():
258331 hash_object .update (str (self ._deployment_prefix ).encode ())
@@ -262,31 +335,26 @@ def remove(self) -> None:
262335 """Remove the deployed environment."""
263336 ...
264337
265- def managed_deploy (self ) -> None :
266- if isinstance (self , ArchiveableEnvBase ) and self .archive_path .exists ():
267- self .deploy_from_archive ()
268- else :
269- self .deploy ()
338+ def managed_remove (self ) -> None :
339+ """Remove the deployed environment, handling exceptions."""
340+ assert isinstance (self , EnvBase )
341+ try :
342+ self .remove ()
343+ except Exception as e :
344+ raise WorkflowError (f"Removal of { self .spec } failed: { e } " )
345+
346+ async def managed_deploy (self ) -> None :
347+ assert isinstance (self , EnvBase )
348+ try :
349+ await self .deploy ()
350+ except Exception as e :
351+ raise WorkflowError (f"Deployment of { self .spec } failed: { e } " )
270352
271353 def deployment_hash (self ) -> str :
354+ assert isinstance (self , EnvBase )
272355 return self ._managed_generic_hash ("deployment_hash" )
273356
274357 @property
275358 def deployment_path (self ) -> Path :
359+ assert isinstance (self , EnvBase ) and self ._deployment_prefix is not None
276360 return self ._deployment_prefix / self .deployment_hash ()
277-
278-
279- class ArchiveableEnvBase (ABC ):
280- @abstractmethod
281- async def archive (self ) -> None :
282- """Archive the environment to self.archive_path.
283-
284- When issuing shell commands, the environment should use
285- self.run_cmd(cmd: str) in order to ensure that it runs within eventual
286- parent environments (e.g. a container or an env module).
287- """
288- ...
289-
290- @property
291- def archive_path (self ) -> Path :
292- return self ._archive_prefix / self .hash ()
0 commit comments