Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/invoke/executor.py: 16%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

75 statements  

1from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union 

2 

3from .config import Config 

4from .parser import ParserContext 

5from .util import debug 

6from .tasks import Call, Task 

7 

8if TYPE_CHECKING: 

9 from .collection import Collection 

10 from .runners import Result 

11 from .parser import ParseResult 

12 

13 

14class Executor: 

15 """ 

16 An execution strategy for Task objects. 

17 

18 Subclasses may override various extension points to change, add or remove 

19 behavior. 

20 

21 .. versionadded:: 1.0 

22 """ 

23 

24 def __init__( 

25 self, 

26 collection: "Collection", 

27 config: Optional["Config"] = None, 

28 core: Optional["ParseResult"] = None, 

29 ) -> None: 

30 """ 

31 Initialize executor with handles to necessary data structures. 

32 

33 :param collection: 

34 A `.Collection` used to look up requested tasks (and their default 

35 config data, if any) by name during execution. 

36 

37 :param config: 

38 An optional `.Config` holding configuration state. Defaults to an 

39 empty `.Config` if not given. 

40 

41 :param core: 

42 An optional `.ParseResult` holding parsed core program arguments. 

43 Defaults to ``None``. 

44 """ 

45 self.collection = collection 

46 self.config = config if config is not None else Config() 

47 self.core = core 

48 

49 def execute( 

50 self, *tasks: Union[str, Tuple[str, Dict[str, Any]], ParserContext] 

51 ) -> Dict["Task", "Result"]: 

52 """ 

53 Execute one or more ``tasks`` in sequence. 

54 

55 :param tasks: 

56 An all-purpose iterable of "tasks to execute", each member of which 

57 may take one of the following forms: 

58 

59 **A string** naming a task from the Executor's `.Collection`. This 

60 name may contain dotted syntax appropriate for calling namespaced 

61 tasks, e.g. ``subcollection.taskname``. Such tasks are executed 

62 without arguments. 

63 

64 **A two-tuple** whose first element is a task name string (as 

65 above) and whose second element is a dict suitable for use as 

66 ``**kwargs`` when calling the named task. E.g.:: 

67 

68 [ 

69 ('task1', {}), 

70 ('task2', {'arg1': 'val1'}), 

71 ... 

72 ] 

73 

74 is equivalent, roughly, to:: 

75 

76 task1() 

77 task2(arg1='val1') 

78 

79 **A `.ParserContext`** instance, whose ``.name`` attribute is used 

80 as the task name and whose ``.as_kwargs`` attribute is used as the 

81 task kwargs (again following the above specifications). 

82 

83 .. note:: 

84 When called without any arguments at all (i.e. when ``*tasks`` 

85 is empty), the default task from ``self.collection`` is used 

86 instead, if defined. 

87 

88 :returns: 

89 A dict mapping task objects to their return values. 

90 

91 This dict may include pre- and post-tasks if any were executed. For 

92 example, in a collection with a ``build`` task depending on another 

93 task named ``setup``, executing ``build`` will result in a dict 

94 with two keys, one for ``build`` and one for ``setup``. 

95 

96 .. versionadded:: 1.0 

97 """ 

98 # Normalize input 

99 debug("Examining top level tasks {!r}".format([x for x in tasks])) 

100 calls = self.normalize(tasks) 

101 debug("Tasks (now Calls) with kwargs: {!r}".format(calls)) 

102 # Obtain copy of directly-given tasks since they should sometimes 

103 # behave differently 

104 direct = list(calls) 

105 # Expand pre/post tasks 

106 # TODO: may make sense to bundle expansion & deduping now eh? 

107 expanded = self.expand_calls(calls) 

108 # Get some good value for dedupe option, even if config doesn't have 

109 # the tree we expect. (This is a concession to testing.) 

110 try: 

111 dedupe = self.config.tasks.dedupe 

112 except AttributeError: 

113 dedupe = True 

114 # Dedupe across entire run now that we know about all calls in order 

115 calls = self.dedupe(expanded) if dedupe else expanded 

116 # Execute 

117 results = {} 

118 # TODO: maybe clone initial config here? Probably not necessary, 

119 # especially given Executor is not designed to execute() >1 time at the 

120 # moment... 

121 for call in calls: 

122 autoprint = call in direct and call.autoprint 

123 debug("Executing {!r}".format(call)) 

124 # Hand in reference to our config, which will preserve user 

125 # modifications across the lifetime of the session. 

126 config = self.config 

127 # But make sure we reset its task-sensitive levels each time 

128 # (collection & shell env) 

129 # TODO: load_collection needs to be skipped if task is anonymous 

130 # (Fabric 2 or other subclassing libs only) 

131 collection_config = self.collection.configuration(call.called_as) 

132 config.load_collection(collection_config) 

133 config.load_shell_env() 

134 debug("Finished loading collection & shell env configs") 

135 # Get final context from the Call (which will know how to generate 

136 # an appropriate one; e.g. subclasses might use extra data from 

137 # being parameterized), handing in this config for use there. 

138 context = call.make_context(config) 

139 args = (context, *call.args) 

140 result = call.task(*args, **call.kwargs) 

141 if autoprint: 

142 print(result) 

143 # TODO: handle the non-dedupe case / the same-task-different-args 

144 # case, wherein one task obj maps to >1 result. 

145 results[call.task] = result 

146 return results 

147 

148 def normalize( 

149 self, 

150 tasks: Tuple[ 

151 Union[str, Tuple[str, Dict[str, Any]], ParserContext], ... 

152 ], 

153 ) -> List["Call"]: 

154 """ 

155 Transform arbitrary task list w/ various types, into `.Call` objects. 

156 

157 See docstring for `~.Executor.execute` for details. 

158 

159 .. versionadded:: 1.0 

160 """ 

161 calls = [] 

162 for task in tasks: 

163 name: Optional[str] 

164 if isinstance(task, str): 

165 name = task 

166 kwargs = {} 

167 elif isinstance(task, ParserContext): 

168 name = task.name 

169 kwargs = task.as_kwargs 

170 else: 

171 name, kwargs = task 

172 c = Call(self.collection[name], kwargs=kwargs, called_as=name) 

173 calls.append(c) 

174 if not tasks and self.collection.default is not None: 

175 calls = [Call(self.collection[self.collection.default])] 

176 return calls 

177 

178 def dedupe(self, calls: List["Call"]) -> List["Call"]: 

179 """ 

180 Deduplicate a list of `tasks <.Call>`. 

181 

182 :param calls: An iterable of `.Call` objects representing tasks. 

183 

184 :returns: A list of `.Call` objects. 

185 

186 .. versionadded:: 1.0 

187 """ 

188 deduped = [] 

189 debug("Deduplicating tasks...") 

190 for call in calls: 

191 if call not in deduped: 

192 debug("{!r}: no duplicates found, ok".format(call)) 

193 deduped.append(call) 

194 else: 

195 debug("{!r}: found in list already, skipping".format(call)) 

196 return deduped 

197 

198 def expand_calls(self, calls: List["Call"]) -> List["Call"]: 

199 """ 

200 Expand a list of `.Call` objects into a near-final list of same. 

201 

202 The default implementation of this method simply adds a task's 

203 pre/post-task list before/after the task itself, as necessary. 

204 

205 Subclasses may wish to do other things in addition (or instead of) the 

206 above, such as multiplying the `calls <.Call>` by argument vectors or 

207 similar. 

208 

209 .. versionadded:: 1.0 

210 """ 

211 ret = [] 

212 for call in calls: 

213 # Normalize to Call (this method is sometimes called with pre/post 

214 # task lists, which may contain 'raw' Task objects) 

215 if isinstance(call, Task): 

216 call = Call(call) 

217 debug("Expanding task-call {!r}".format(call)) 

218 # TODO: this is where we _used_ to call Executor.config_for(call, 

219 # config)... 

220 # TODO: now we may need to preserve more info like where the call 

221 # came from, etc, but I feel like that shit should go _on the call 

222 # itself_ right??? 

223 # TODO: we _probably_ don't even want the config in here anymore, 

224 # we want this to _just_ be about the recursion across pre/post 

225 # tasks or parameterization...? 

226 ret.extend(self.expand_calls(call.pre)) 

227 ret.append(call) 

228 ret.extend(self.expand_calls(call.post)) 

229 return ret