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

74 statements  

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

2 

3from .config import Config 

4from .parser import ParserContext, ParseResult 

5from .tasks import Call, Task 

6from .util import debug 

7 

8if TYPE_CHECKING: 

9 from .collection import Collection 

10 from .runners import Result 

11 

12 

13class Executor: 

14 """ 

15 An execution strategy for Task objects. 

16 

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

18 behavior. 

19 

20 .. versionadded:: 1.0 

21 """ 

22 

23 def __init__( 

24 self, 

25 collection: "Collection", 

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

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

28 ) -> None: 

29 """ 

30 Initialize executor with handles to necessary data structures. 

31 

32 :param collection: 

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

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

35 

36 :param config: 

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

38 empty `.Config` if not given. 

39 

40 :param core: 

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

42 Defaults to ``None``. 

43 

44 .. versionchanged:: 3.0 

45 The ``core`` attribute now defaults to an 'empty' `.ParseResult` if 

46 ``None`` was given. 

47 """ 

48 self.collection = collection 

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

50 self.core = core if core is not None else ParseResult() 

51 

52 def execute( 

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

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

55 """ 

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

57 

58 :param tasks: 

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

60 may take one of the following forms: 

61 

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

63 name may contain dotted syntax appropriate for calling namespaced 

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

65 without arguments. 

66 

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

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

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

70 

71 [ 

72 ('task1', {}), 

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

74 ... 

75 ] 

76 

77 is equivalent, roughly, to:: 

78 

79 task1() 

80 task2(arg1='val1') 

81 

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

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

84 task kwargs (again following the above specifications). 

85 

86 .. note:: 

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

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

89 instead, if defined. 

90 

91 :returns: 

92 A dict mapping task objects to their return values. 

93 

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

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

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

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

98 

99 .. versionadded:: 1.0 

100 """ 

101 # Normalize input 

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

103 calls = self.normalize(tasks) 

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

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

106 # behave differently 

107 direct = list(calls) 

108 # Expand pre/post tasks 

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

110 expanded = self.expand_calls(calls) 

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

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

113 try: 

114 dedupe = self.config.tasks.dedupe 

115 except AttributeError: 

116 dedupe = True 

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

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

119 # Execute 

120 results = {} 

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

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

123 # moment... 

124 for call in calls: 

125 autoprint = call in direct and call.autoprint 

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

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

128 # modifications across the lifetime of the session. 

129 config = self.config 

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

131 # (collection & shell env) 

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

133 # (Fabric 2 or other subclassing libs only) 

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

135 config.load_collection(collection_config) 

136 config.load_shell_env() 

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

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

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

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

141 context = call.make_context(config, core_parse_result=self.core) 

142 args = (context, *call.args) 

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

144 if autoprint: 

145 print(result) 

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

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

148 results[call.task] = result 

149 return results 

150 

151 def normalize( 

152 self, 

153 tasks: Tuple[ 

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

155 ], 

156 ) -> List["Call"]: 

157 """ 

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

159 

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

161 

162 .. versionadded:: 1.0 

163 """ 

164 calls = [] 

165 for task in tasks: 

166 name: Optional[str] 

167 if isinstance(task, str): 

168 name = task 

169 kwargs = {} 

170 elif isinstance(task, ParserContext): 

171 name = task.name 

172 kwargs = task.as_kwargs 

173 else: 

174 name, kwargs = task 

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

176 calls.append(c) 

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

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

179 return calls 

180 

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

182 """ 

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

184 

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

186 

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

188 

189 .. versionadded:: 1.0 

190 """ 

191 deduped = [] 

192 debug("Deduplicating tasks...") 

193 for call in calls: 

194 if call not in deduped: 

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

196 deduped.append(call) 

197 else: 

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

199 return deduped 

200 

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

202 """ 

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

204 

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

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

207 

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

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

210 similar. 

211 

212 .. versionadded:: 1.0 

213 """ 

214 ret = [] 

215 for call in calls: 

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

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

218 if isinstance(call, Task): 

219 call = Call(call) 

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

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

222 # config)... 

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

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

225 # itself_ right??? 

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

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

228 # tasks or parameterization...? 

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

230 ret.append(call) 

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

232 return ret