Coverage for src/shephex/executor/executor.py: 98%

48 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-06-20 14:13 +0200

1""" 

2Executor base class. 

3""" 

4from abc import ABC, abstractmethod 

5from pathlib import Path 

6from typing import List, Optional, Sequence, Union 

7 

8from shephex.decorators import disable_decorators 

9from shephex.experiment import DryResult, Experiment, ExperimentResult 

10from shephex.experiment.chain_iterator import ChainableExperimentIterator 

11from shephex.experiment.status import Pending, Status 

12 

13 

14class Executor(ABC): 

15 """ 

16 Executor base class. 

17 """ 

18 def __init__(self) -> None: 

19 pass 

20 

21 def execute( 

22 self, 

23 experiments: Union[Experiment, Sequence[Experiment]], 

24 dry: bool = False, 

25 execution_directory: Optional[Union[Path, str]] = None, 

26 valid_statuses: Optional[Sequence[Status]] = None, 

27 ) -> Union[ExperimentResult, List[ExperimentResult]]: 

28 """ 

29 Execute a set of experiments. 

30 

31 Parameters 

32 ----------- 

33 experiments: Experiment or Sequence[Experiment] 

34 The experiments to be executed. 

35 dry: bool 

36 If True, the experiments will not be executed, only information about 

37 them will be printed. 

38 """ 

39 if isinstance(experiments, Experiment): 

40 experiments = [experiments] 

41 elif isinstance(experiments, ChainableExperimentIterator): 

42 experiments = list(experiments) 

43 

44 if valid_statuses is None: 

45 valid_statuses = [Pending()] 

46 

47 valid_experiments = [] 

48 for experiment in experiments: 

49 if experiment.status in valid_statuses: 

50 valid_experiments.append(experiment) 

51 else: 

52 print(f"Experiment {experiment.identifier} has status {experiment.status}, skipping.") 

53 

54 for experiment in valid_experiments: 

55 if not experiment.shephex_directory.exists(): 

56 experiment.dump() 

57 

58 results = self._sequence_execute( 

59 valid_experiments, dry=dry, execution_directory=execution_directory 

60 ) 

61 

62 return results 

63 

64 def _sequence_execute( 

65 self, 

66 experiments: Sequence[Experiment], 

67 dry: bool = False, 

68 execution_directory: Optional[Union[Path, str]] = None, 

69 ) -> Sequence[ExperimentResult]: 

70 results = [] 

71 for experiment in experiments: 

72 result = self._execute( 

73 experiment, dry=dry, execution_directory=execution_directory 

74 ) 

75 results.append(result) 

76 return results 

77 

78 @abstractmethod 

79 def _single_execute( 

80 self, 

81 experiment: Experiment, 

82 dry: bool = False, 

83 execution_directory: Optional[Union[Path, str]] = None, 

84 ) -> ExperimentResult: 

85 raise NotImplementedError # pragma: no cover 

86 

87 def _execute( 

88 self, 

89 experiment: Experiment, 

90 dry: bool = False, 

91 execution_directory: Optional[Union[Path, str]] = None, 

92 ) -> ExperimentResult: 

93 result = self._single_execute( 

94 experiment, dry=dry, execution_directory=execution_directory 

95 ) 

96 return result 

97 

98 

99class LocalExecutor(Executor): 

100 """ 

101 Executor that runs the experiment locally, in the current process (or subprocess), 

102 without any parallelization. 

103 """ 

104 

105 def __init__(self) -> None: 

106 super().__init__() 

107 

108 def _single_execute( 

109 self, 

110 experiment: Experiment, 

111 dry: bool = False, 

112 execution_directory: Optional[Union[Path, str]] = None, 

113 ) -> ExperimentResult: 

114 

115 if dry: 

116 print(f'Experiment {experiment.identifier} to be executed locally.') 

117 return DryResult() 

118 

119 with disable_decorators(): 

120 result = experiment._execute(execution_directory=execution_directory) 

121 

122 return result