99import hashlib
1010from pathlib import Path
1111import shutil
12- from typing import Any , Dict , Iterable , Optional , Self , Tuple , Type , Union
12+ from typing import Any , ClassVar , Dict , Iterable , Optional , Protocol , Self , Tuple , Type , Union , runtime_checkable
1313import subprocess as sp
1414
1515from snakemake_interface_software_deployment_plugins .settings import (
@@ -123,8 +123,19 @@ def __str__(self) -> str:
123123 ...
124124
125125
126- class EnvBase (ABC ):
127- _cache : Dict [Tuple [Type ["EnvBase" ], Optional ["EnvBase" ]], Any ] = {}
126+ class EnvBase (Protocol ):
127+ _cache : ClassVar [Dict [Tuple [Type ["EnvBase" ], Optional ["EnvBase" ]], Any ]] = {}
128+ spec : EnvSpecBase
129+ within : Optional ["EnvBase" ]
130+ settings : Optional [SoftwareDeploymentSettingsBase ]
131+ shell_executable : str
132+ tempdir : Path
133+ _cache_prefix : Path
134+ _deployment_prefix : Path
135+ _pinfile_prefix : Path
136+ _managed_hash_store : Optional [str ] = None
137+ _managed_deployment_hash_store : Optional [str ] = None
138+ _obj_hash : Optional [int ] = None
128139
129140 def __init__ (
130141 self ,
@@ -142,9 +153,6 @@ def __init__(
142153 self .settings : Optional [SoftwareDeploymentSettingsBase ] = settings
143154 self .shell_executable : str = shell_executable
144155 self .tempdir = tempdir
145- self ._managed_hash_store : Optional [str ] = None
146- self ._managed_deployment_hash_store : Optional [str ] = None
147- self ._obj_hash : Optional [int ] = None
148156 self ._deployment_prefix : Path = deployment_prefix
149157 self ._cache_prefix : Path = cache_prefix
150158 self ._pinfile_prefix : Path = pinfile_prefix
@@ -177,6 +185,21 @@ def decorate_shellcmd(self, cmd: str) -> str:
177185 """
178186 ...
179187
188+ def is_deployable (self ) -> bool :
189+ """Overwrite this in case the deployability of the environment depends on
190+ the spec or settings."""
191+ return isinstance (self , DeployableEnvBase )
192+
193+ def is_pinnable (self ) -> bool :
194+ """Overwrite this in case the pinability of the environment depends on
195+ the spec or settings."""
196+ return isinstance (self , PinnableEnvBase )
197+
198+ def is_cacheable (self ) -> bool :
199+ """Overwrite this in case the cacheability of the environment depends on
200+ the spec or settings."""
201+ return isinstance (self , CacheableEnvBase )
202+
180203 @abstractmethod
181204 def record_hash (self , hash_object ) -> None :
182205 """Update given hash object (using hash_object.update()) such that it changes
@@ -241,7 +264,8 @@ def __eq__(self, other) -> bool:
241264 )
242265
243266
244- class PinnableEnvBase (ABC ):
267+ @runtime_checkable
268+ class PinnableEnvBase (EnvBase , Protocol ):
245269 @classmethod
246270 @abstractmethod
247271 def pinfile_extension (cls ) -> str : ...
@@ -256,7 +280,6 @@ async def pin(self) -> None:
256280
257281 @property
258282 def pinfile (self ) -> Path :
259- assert isinstance (self , EnvBase )
260283 ext = self .pinfile_extension ()
261284 if not ext .startswith ("." ):
262285 raise ValueError ("pinfile_extension must start with a dot." )
@@ -265,7 +288,8 @@ def pinfile(self) -> Path:
265288 )
266289
267290
268- class CacheableEnvBase (ABC ):
291+ @runtime_checkable
292+ class CacheableEnvBase (EnvBase , Protocol ):
269293 async def get_cache_assets (self ) -> Iterable [str ]: ...
270294
271295 @abstractmethod
@@ -277,12 +301,10 @@ async def cache_assets(self) -> None:
277301
278302 @property
279303 def cache_path (self ) -> Path :
280- assert isinstance (self , EnvBase )
281304 return self ._cache_prefix
282305
283306 async def remove_cache (self ) -> None :
284307 """Remove the cached environment assets."""
285- assert isinstance (self , EnvBase )
286308 for asset in await self .get_cache_assets ():
287309 asset_path = self .cache_path / asset
288310 if asset_path .exists ():
@@ -297,7 +319,8 @@ async def remove_cache(self) -> None:
297319 )
298320
299321
300- class DeployableEnvBase (ABC ):
322+ @runtime_checkable
323+ class DeployableEnvBase (EnvBase , Protocol ):
301324 @abstractmethod
302325 def is_deployment_path_portable (self ) -> bool :
303326 """Return whether the deployment path matters for the environment, i.e.
@@ -325,7 +348,6 @@ def record_deployment_hash(self, hash_object) -> None:
325348 deployment is senstivive to the path (e.g. in case of conda, which patches
326349 the RPATH in binaries).
327350 """
328- assert isinstance (self , EnvBase )
329351 self .record_hash (hash_object )
330352 if not self .is_deployment_path_portable ():
331353 hash_object .update (str (self ._deployment_prefix ).encode ())
@@ -337,24 +359,21 @@ def remove(self) -> None:
337359
338360 def managed_remove (self ) -> None :
339361 """Remove the deployed environment, handling exceptions."""
340- assert isinstance (self , EnvBase )
341362 try :
342363 self .remove ()
343364 except Exception as e :
344365 raise WorkflowError (f"Removal of { self .spec } failed: { e } " )
345366
346367 async def managed_deploy (self ) -> None :
347- assert isinstance (self , EnvBase )
348368 try :
349369 await self .deploy ()
350370 except Exception as e :
351371 raise WorkflowError (f"Deployment of { self .spec } failed: { e } " )
352372
353373 def deployment_hash (self ) -> str :
354- assert isinstance (self , EnvBase )
355374 return self ._managed_generic_hash ("deployment_hash" )
356375
357376 @property
358377 def deployment_path (self ) -> Path :
359- assert isinstance ( self , EnvBase ) and self ._deployment_prefix is not None
378+ assert self ._deployment_prefix is not None
360379 return self ._deployment_prefix / self .deployment_hash ()
0 commit comments