-
-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathapp.py
More file actions
338 lines (285 loc) · 11.1 KB
/
Copy pathapp.py
File metadata and controls
338 lines (285 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
from __future__ import annotations
import asyncio
import os
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import IO, TYPE_CHECKING, Any
from .exceptions import ExecutionError, PoeException
from .helpers.eventloop import run_async
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Mapping, Sequence
from .config import PoeConfig
from .context import RunContext
from .io import PoeIO
from .task.base import PoeTask, TaskSpecFactory
from .ui import PoeUi
class PoeThePoet:
"""
:param cwd:
The directory that poe should take as the current working directory,
this determines where to look for a pyproject.toml file, defaults to
``Path().resolve()``
:type cwd: Path, optional
:param config:
Either a dictionary with the same schema as a pyproject.toml file, or a
`PoeConfig <https://github.com/nat-n/poethepoet/blob/main/poethepoet/config/config.py>`_
object to use as an alternative to loading config from a file.
:type config: dict | PoeConfig, optional
:param output:
A stream for the application to write its own output to, defaults to sys.stdout
:type output: IO, optional
:param poetry_env_path:
The path to the poetry virtualenv. If provided then it is used by the
`PoetryExecutor <https://github.com/nat-n/poethepoet/blob/main/poethepoet/executor/poetry.py>`_,
instead of having to execute poetry in a subprocess to determine this.
:type poetry_env_path: str, optional
:param config_name:
The name of the file to load tasks and configuration from. If not set then poe
will search for config by the following file names: pyproject.toml
poe_tasks.toml poe_tasks.yaml poe_tasks.json
:type config_name: str, optional
:param program_name:
The name of the program that is being run. This is used primarily when
outputting help messages, defaults to "poe"
:type program_name: str, optional
:param env:
Optionally provide an alternative base environment for tasks to run with.
If no mapping is provided then ``os.environ`` is used.
:type env: dict, optional
:param suppress_args:
A sequence of identifiers for global arguments that should not be displayed in
the help message.
:type suppress_args: Sequence[str], optional
"""
cwd: Path
ui: PoeUi
config: PoeConfig
_task_specs: TaskSpecFactory | None = None
def __init__(
self,
cwd: Path | str | None = None,
config: Mapping[str, Any] | PoeConfig | None = None,
output: PoeIO | IO = sys.stdout,
poetry_env_path: str | None = None,
config_name: str | None = None,
program_name: str = "poe",
env: Mapping[str, str] | None = None,
suppress_args: Sequence[str] = ("legacy_project_root",),
):
from .config import PoeConfig
from .io import PoeIO
from .ui import PoeUi
self.cwd = Path(cwd) if cwd else Path().resolve()
if self.cwd and self.cwd.is_file():
config_name = self.cwd.name
self.cwd = self.cwd.parent
self.io = (
PoeIO(
parent=output,
make_default=True,
)
if isinstance(output, PoeIO)
else PoeIO(
output=output,
error=output,
make_default=True,
)
)
if isinstance(config, PoeConfig):
self.config = config
self.config._io = self.io
else:
self.config = PoeConfig(
cwd=self.cwd, table=config, config_name=config_name, io=self.io
)
self.io.configure(baseline=self.config.verbosity)
self.ui = PoeUi(
io=self.io,
program_name=program_name,
suppress_args=suppress_args,
)
self._poetry_env_path = poetry_env_path
self._env = env if env is not None else os.environ
def __call__(self, cli_args: Sequence[str], internal: bool = False) -> int:
"""
:param cli_args:
A sequence of command line arguments to pass to poe (i.e. sys.argv[1:])
:param internal:
Indicates that this is an internal call to run poe, e.g. from a
plugin hook.
"""
self.ui.parse_args(cli_args)
if self.ui["version"]:
self.ui.print_version()
return 0
try:
return run_async(self._call(internal))
except asyncio.CancelledError:
return 1
async def _call(self, internal: bool = False) -> int:
should_display_help = self.ui["help"] != Ellipsis
try:
await self.config.load(target_path=self.ui["project_root"])
self.io.configure(baseline=self.config.verbosity)
for task_spec in self.task_specs.load_all():
task_spec.validate(self.config, self.task_specs)
except PoeException as error:
if should_display_help:
self.print_help()
return 0
self.print_help(error=error)
return 1
if should_display_help:
self.print_help()
return 0
task = self.resolve_task(internal)
if not task:
return 1
if task.has_deps():
return await self._run_task_graph(task)
return await self._run_task(task)
def modify_verbosity(self, offset: int):
"""
Set the offset by which the verbosity level will be modified in all contexts.
This is an alternative to using the `-v` and `-q` flags on the CLI.
"""
self.io.configure(offset=offset)
@property
def task_specs(self):
if not self._task_specs:
from .task.base import TaskSpecFactory
self._task_specs = TaskSpecFactory(self.config)
return self._task_specs
def resolve_task(self, allow_hidden: bool = False) -> PoeTask | None:
from .io import PoeIO
from .task.base import TaskContext
task = tuple(self.ui["task"])
if not task:
try:
self.print_help(info="No task specified.")
except PoeException as error:
self.print_help(error=error)
return None
task_name = task[0]
if task_name not in self.config.get_tasks():
self.print_help(error=PoeException(f"Unrecognized task {task_name!r}"))
return None
if task_name.startswith("_") and not allow_hidden:
self.print_help(
error=PoeException(
"Tasks prefixed with `_` cannot be executed directly"
),
)
return None
task_spec = self.task_specs.get(task_name)
task_context = TaskContext(
config=self.config,
cwd=str(task_spec.source.cwd),
specs=self.task_specs,
ui=self.ui,
io=PoeIO(
parent=self.io,
baseline_verbosity=task_spec.options.get(
"verbosity", self.io._baseline_verbosity
),
),
)
return task_spec.create_task(invocation=task, ctx=task_context)
async def _run_task(self, task: PoeTask, context: RunContext | None = None) -> int:
async with self.run_context(existing=context) as context:
try:
task_run = await task.run(context=context)
await task_run.wait(suppress_errors=False)
return task_run.return_code or 0
except ExecutionError as error:
self.ui.print_error(error=error)
return 1
except PoeException as error:
self.print_help(error=error)
return 1
async def _run_task_graph(self, task: PoeTask) -> int:
from .task.graph import TaskExecutionGraph
async with self.run_context(multistage=True) as context:
try:
graph = TaskExecutionGraph(task, context)
except PoeException as error:
self.print_help(error=error)
return 1
except ExecutionError as error:
self.ui.print_error(error=error)
return 1
plan = graph.get_execution_plan()
for stage in plan:
for stage_task in stage:
if stage_task == task:
# The final sink task gets special treatment
return await self._run_task(stage_task, context)
try:
task_run = await stage_task.run(context=context)
await task_run.wait(suppress_errors=False)
if task_run.has_failure:
raise ExecutionError(
"Task graph aborted after failed task "
f"{stage_task.name!r}"
)
except PoeException as error:
self.print_help(error=error)
return 1
except ExecutionError as error:
self.ui.print_error(error=error)
return 1
return 0
@asynccontextmanager
async def run_context(
self,
multistage: bool = False,
existing: RunContext | None = None,
) -> AsyncIterator[RunContext]:
from .context import RunContext
if existing is not None:
yield existing
return
async with RunContext.scope(
config=self.config,
ui=self.ui,
env=self._env,
dry=self.ui["dry_run"],
poe_active=self._env.get("POE_ACTIVE"),
multistage=multistage,
cwd=self.cwd,
) as context:
if self._poetry_env_path:
# This allows the PoetryExecutor to use the venv from poetry directly
# this is used by the poetry plugin
context.exec_cache["poetry_virtualenv"] = self._poetry_env_path
yield context
def print_help(
self,
info: str | None = None,
error: str | PoeException | None = None,
):
from .task.args import PoeTaskArgs
if isinstance(error, str):
error = PoeException(error)
all_tasks = self.config.get_tasks()
tasks_help: dict[
str, tuple[str, Sequence[tuple[tuple[str, ...], str, str]], str | None]
] = {
task_name: (
task.get("help", ""),
PoeTaskArgs.get_help_content(
task.get("args"), task_name, suppress_errors=bool(error)
),
task.group.name if task.group else None,
)
for task_name, task in all_tasks.items()
}
groups_headings = {
task.group.name: task.group.heading
for task in all_tasks.values()
if task.group
}
self.ui.print_help(
tasks=tasks_help, groups=groups_headings, info=info, error=error
)