poethepoet.app

  1import os
  2import sys
  3from pathlib import Path
  4from typing import (
  5    IO,
  6    TYPE_CHECKING,
  7    Any,
  8    Dict,
  9    Mapping,
 10    Optional,
 11    Sequence,
 12    Tuple,
 13    Union,
 14)
 15
 16from .exceptions import ExecutionError, PoeException
 17
 18if TYPE_CHECKING:
 19    from .config import PoeConfig
 20    from .context import RunContext
 21    from .task.base import PoeTask
 22    from .ui import PoeUi
 23
 24
 25class PoeThePoet:
 26    """
 27    :param cwd:
 28        The directory that poe should take as the current working directory,
 29        this determines where to look for a pyproject.toml file, defaults to
 30        ``Path().resolve()``
 31    :type cwd: Path, optional
 32    :param config:
 33        Either a dictionary with the same schema as a pyproject.toml file, or a
 34        `PoeConfig <https://github.com/nat-n/poethepoet/blob/main/poethepoet/config/config.py>`_
 35        object to use as an alternative to loading config from a file.
 36    :type config: dict | PoeConfig, optional
 37    :param output:
 38        A stream for the application to write its own output to, defaults to sys.stdout
 39    :type output: IO, optional
 40    :param poetry_env_path:
 41        The path to the poetry virtualenv. If provided then it is used by the
 42        `PoetryExecutor <https://github.com/nat-n/poethepoet/blob/main/poethepoet/executor/poetry.py>`_,
 43        instead of having to execute poetry in a subprocess to determine this.
 44    :type poetry_env_path: str, optional
 45    :param config_name:
 46        The name of the file to load tasks and configuration from. If not set then poe
 47        will search for config by the following file names: pyproject.toml
 48        poe_tasks.toml poe_tasks.yaml poe_tasks.json
 49    :type config_name: str, optional
 50    :param program_name:
 51        The name of the program that is being run. This is used primarily when
 52        outputting help messages, defaults to "poe"
 53    :type program_name: str, optional
 54    """
 55
 56    cwd: Path
 57    ui: "PoeUi"
 58    config: "PoeConfig"
 59
 60    _task_specs: Optional[Dict[str, "PoeTask.TaskSpec"]] = None
 61
 62    def __init__(
 63        self,
 64        cwd: Optional[Union[Path, str]] = None,
 65        config: Optional[Union[Mapping[str, Any], "PoeConfig"]] = None,
 66        output: IO = sys.stdout,
 67        poetry_env_path: Optional[str] = None,
 68        config_name: Optional[str] = None,
 69        program_name: str = "poe",
 70    ):
 71        from .config import PoeConfig
 72        from .ui import PoeUi
 73
 74        self.cwd = Path(cwd) if cwd else Path().resolve()
 75
 76        if self.cwd and self.cwd.is_file():
 77            config_name = self.cwd.name
 78            self.cwd = self.cwd.parent
 79
 80        self.config = (
 81            config
 82            if isinstance(config, PoeConfig)
 83            else PoeConfig(cwd=self.cwd, table=config, config_name=config_name)
 84        )
 85        self.ui = PoeUi(output=output, program_name=program_name)
 86        self._poetry_env_path = poetry_env_path
 87
 88    def __call__(self, cli_args: Sequence[str], internal: bool = False) -> int:
 89        """
 90        :param cli_args:
 91            A sequence of command line arguments to pass to poe (i.e. sys.argv[1:])
 92        :param internal:
 93            Indicates that this is an internal call to run poe, e.g. from a
 94            plugin hook.
 95        """
 96
 97        self.ui.parse_args(cli_args)
 98
 99        if self.ui["version"]:
100            self.ui.print_version()
101            return 0
102
103        try:
104            self.config.load(target_path=self.ui["project_root"])
105            for task_spec in self.task_specs.load_all():
106                task_spec.validate(self.config, self.task_specs)
107        except PoeException as error:
108            if self.ui["help"]:
109                self.print_help()
110                return 0
111            self.print_help(error=error)
112            return 1
113
114        self.ui.set_default_verbosity(self.config.verbosity)
115
116        if self.ui["help"]:
117            self.print_help()
118            return 0
119
120        task = self.resolve_task(internal)
121        if not task:
122            return 1
123
124        if task.has_deps():
125            return self.run_task_graph(task) or 0
126        else:
127            return self.run_task(task) or 0
128
129    @property
130    def task_specs(self):
131        if not self._task_specs:
132            from .task.base import TaskSpecFactory
133
134            self._task_specs = TaskSpecFactory(self.config)
135        return self._task_specs
136
137    def resolve_task(self, allow_hidden: bool = False) -> Optional["PoeTask"]:
138        from .task.base import TaskContext
139
140        task = tuple(self.ui["task"])
141        if not task:
142            self.print_help(info="No task specified.")
143            return None
144
145        task_name = task[0]
146        if task_name not in self.config.task_names:
147            self.print_help(error=PoeException(f"Unrecognised task {task_name!r}"))
148            return None
149
150        if task_name.startswith("_") and not allow_hidden:
151            self.print_help(
152                error=PoeException(
153                    "Tasks prefixed with `_` cannot be executed directly"
154                ),
155            )
156            return None
157
158        task_spec = self.task_specs.get(task_name)
159        return task_spec.create_task(
160            invocation=task,
161            ctx=TaskContext(
162                config=self.config,
163                cwd=str(task_spec.source.cwd),
164                specs=self.task_specs,
165                ui=self.ui,
166            ),
167        )
168
169    def run_task(
170        self, task: "PoeTask", context: Optional["RunContext"] = None
171    ) -> Optional[int]:
172        if context is None:
173            context = self.get_run_context()
174        try:
175            return task.run(context=context)
176        except ExecutionError as error:
177            self.ui.print_error(error=error)
178            return 1
179        except PoeException as error:
180            self.print_help(error=error)
181            return 1
182
183    def run_task_graph(self, task: "PoeTask") -> Optional[int]:
184        from .task.graph import TaskExecutionGraph
185
186        context = self.get_run_context(multistage=True)
187        graph = TaskExecutionGraph(task, context)
188        plan = graph.get_execution_plan()
189
190        for stage in plan:
191            for stage_task in stage:
192                if stage_task == task:
193                    # The final sink task gets special treatment
194                    return self.run_task(stage_task, context)
195
196                try:
197                    task_result = stage_task.run(context=context)
198                    if task_result:
199                        raise ExecutionError(
200                            f"Task graph aborted after failed task {stage_task.name!r}"
201                        )
202                except PoeException as error:
203                    self.print_help(error=error)
204                    return 1
205                except ExecutionError as error:
206                    self.ui.print_error(error=error)
207                    return 1
208        return 0
209
210    def get_run_context(self, multistage: bool = False) -> "RunContext":
211        from .context import RunContext
212
213        result = RunContext(
214            config=self.config,
215            ui=self.ui,
216            env=os.environ,
217            dry=self.ui["dry_run"],
218            poe_active=os.environ.get("POE_ACTIVE"),
219            multistage=multistage,
220            cwd=self.cwd,
221        )
222        if self._poetry_env_path:
223            # This allows the PoetryExecutor to use the venv from poetry directly
224            result.exec_cache["poetry_virtualenv"] = self._poetry_env_path
225        return result
226
227    def print_help(
228        self,
229        info: Optional[str] = None,
230        error: Optional[Union[str, PoeException]] = None,
231    ):
232        from .task.args import PoeTaskArgs
233
234        if isinstance(error, str):
235            error = PoeException(error)
236
237        tasks_help: Dict[
238            str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]
239        ] = {
240            task_name: (
241                (
242                    content.get("help", ""),
243                    PoeTaskArgs.get_help_content(content.get("args")),
244                )
245                if isinstance(content, dict)
246                else ("", tuple())
247            )
248            for task_name, content in self.config.tasks.items()
249        }
250
251        self.ui.print_help(tasks=tasks_help, info=info, error=error)
class PoeThePoet:
 26class PoeThePoet:
 27    """
 28    :param cwd:
 29        The directory that poe should take as the current working directory,
 30        this determines where to look for a pyproject.toml file, defaults to
 31        ``Path().resolve()``
 32    :type cwd: Path, optional
 33    :param config:
 34        Either a dictionary with the same schema as a pyproject.toml file, or a
 35        `PoeConfig <https://github.com/nat-n/poethepoet/blob/main/poethepoet/config/config.py>`_
 36        object to use as an alternative to loading config from a file.
 37    :type config: dict | PoeConfig, optional
 38    :param output:
 39        A stream for the application to write its own output to, defaults to sys.stdout
 40    :type output: IO, optional
 41    :param poetry_env_path:
 42        The path to the poetry virtualenv. If provided then it is used by the
 43        `PoetryExecutor <https://github.com/nat-n/poethepoet/blob/main/poethepoet/executor/poetry.py>`_,
 44        instead of having to execute poetry in a subprocess to determine this.
 45    :type poetry_env_path: str, optional
 46    :param config_name:
 47        The name of the file to load tasks and configuration from. If not set then poe
 48        will search for config by the following file names: pyproject.toml
 49        poe_tasks.toml poe_tasks.yaml poe_tasks.json
 50    :type config_name: str, optional
 51    :param program_name:
 52        The name of the program that is being run. This is used primarily when
 53        outputting help messages, defaults to "poe"
 54    :type program_name: str, optional
 55    """
 56
 57    cwd: Path
 58    ui: "PoeUi"
 59    config: "PoeConfig"
 60
 61    _task_specs: Optional[Dict[str, "PoeTask.TaskSpec"]] = None
 62
 63    def __init__(
 64        self,
 65        cwd: Optional[Union[Path, str]] = None,
 66        config: Optional[Union[Mapping[str, Any], "PoeConfig"]] = None,
 67        output: IO = sys.stdout,
 68        poetry_env_path: Optional[str] = None,
 69        config_name: Optional[str] = None,
 70        program_name: str = "poe",
 71    ):
 72        from .config import PoeConfig
 73        from .ui import PoeUi
 74
 75        self.cwd = Path(cwd) if cwd else Path().resolve()
 76
 77        if self.cwd and self.cwd.is_file():
 78            config_name = self.cwd.name
 79            self.cwd = self.cwd.parent
 80
 81        self.config = (
 82            config
 83            if isinstance(config, PoeConfig)
 84            else PoeConfig(cwd=self.cwd, table=config, config_name=config_name)
 85        )
 86        self.ui = PoeUi(output=output, program_name=program_name)
 87        self._poetry_env_path = poetry_env_path
 88
 89    def __call__(self, cli_args: Sequence[str], internal: bool = False) -> int:
 90        """
 91        :param cli_args:
 92            A sequence of command line arguments to pass to poe (i.e. sys.argv[1:])
 93        :param internal:
 94            Indicates that this is an internal call to run poe, e.g. from a
 95            plugin hook.
 96        """
 97
 98        self.ui.parse_args(cli_args)
 99
100        if self.ui["version"]:
101            self.ui.print_version()
102            return 0
103
104        try:
105            self.config.load(target_path=self.ui["project_root"])
106            for task_spec in self.task_specs.load_all():
107                task_spec.validate(self.config, self.task_specs)
108        except PoeException as error:
109            if self.ui["help"]:
110                self.print_help()
111                return 0
112            self.print_help(error=error)
113            return 1
114
115        self.ui.set_default_verbosity(self.config.verbosity)
116
117        if self.ui["help"]:
118            self.print_help()
119            return 0
120
121        task = self.resolve_task(internal)
122        if not task:
123            return 1
124
125        if task.has_deps():
126            return self.run_task_graph(task) or 0
127        else:
128            return self.run_task(task) or 0
129
130    @property
131    def task_specs(self):
132        if not self._task_specs:
133            from .task.base import TaskSpecFactory
134
135            self._task_specs = TaskSpecFactory(self.config)
136        return self._task_specs
137
138    def resolve_task(self, allow_hidden: bool = False) -> Optional["PoeTask"]:
139        from .task.base import TaskContext
140
141        task = tuple(self.ui["task"])
142        if not task:
143            self.print_help(info="No task specified.")
144            return None
145
146        task_name = task[0]
147        if task_name not in self.config.task_names:
148            self.print_help(error=PoeException(f"Unrecognised task {task_name!r}"))
149            return None
150
151        if task_name.startswith("_") and not allow_hidden:
152            self.print_help(
153                error=PoeException(
154                    "Tasks prefixed with `_` cannot be executed directly"
155                ),
156            )
157            return None
158
159        task_spec = self.task_specs.get(task_name)
160        return task_spec.create_task(
161            invocation=task,
162            ctx=TaskContext(
163                config=self.config,
164                cwd=str(task_spec.source.cwd),
165                specs=self.task_specs,
166                ui=self.ui,
167            ),
168        )
169
170    def run_task(
171        self, task: "PoeTask", context: Optional["RunContext"] = None
172    ) -> Optional[int]:
173        if context is None:
174            context = self.get_run_context()
175        try:
176            return task.run(context=context)
177        except ExecutionError as error:
178            self.ui.print_error(error=error)
179            return 1
180        except PoeException as error:
181            self.print_help(error=error)
182            return 1
183
184    def run_task_graph(self, task: "PoeTask") -> Optional[int]:
185        from .task.graph import TaskExecutionGraph
186
187        context = self.get_run_context(multistage=True)
188        graph = TaskExecutionGraph(task, context)
189        plan = graph.get_execution_plan()
190
191        for stage in plan:
192            for stage_task in stage:
193                if stage_task == task:
194                    # The final sink task gets special treatment
195                    return self.run_task(stage_task, context)
196
197                try:
198                    task_result = stage_task.run(context=context)
199                    if task_result:
200                        raise ExecutionError(
201                            f"Task graph aborted after failed task {stage_task.name!r}"
202                        )
203                except PoeException as error:
204                    self.print_help(error=error)
205                    return 1
206                except ExecutionError as error:
207                    self.ui.print_error(error=error)
208                    return 1
209        return 0
210
211    def get_run_context(self, multistage: bool = False) -> "RunContext":
212        from .context import RunContext
213
214        result = RunContext(
215            config=self.config,
216            ui=self.ui,
217            env=os.environ,
218            dry=self.ui["dry_run"],
219            poe_active=os.environ.get("POE_ACTIVE"),
220            multistage=multistage,
221            cwd=self.cwd,
222        )
223        if self._poetry_env_path:
224            # This allows the PoetryExecutor to use the venv from poetry directly
225            result.exec_cache["poetry_virtualenv"] = self._poetry_env_path
226        return result
227
228    def print_help(
229        self,
230        info: Optional[str] = None,
231        error: Optional[Union[str, PoeException]] = None,
232    ):
233        from .task.args import PoeTaskArgs
234
235        if isinstance(error, str):
236            error = PoeException(error)
237
238        tasks_help: Dict[
239            str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]
240        ] = {
241            task_name: (
242                (
243                    content.get("help", ""),
244                    PoeTaskArgs.get_help_content(content.get("args")),
245                )
246                if isinstance(content, dict)
247                else ("", tuple())
248            )
249            for task_name, content in self.config.tasks.items()
250        }
251
252        self.ui.print_help(tasks=tasks_help, info=info, error=error)
Parameters
  • 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()
  • config: Either a dictionary with the same schema as a pyproject.toml file, or a PoeConfig object to use as an alternative to loading config from a file.
  • output: A stream for the application to write its own output to, defaults to sys.stdout
  • poetry_env_path: The path to the poetry virtualenv. If provided then it is used by the PoetryExecutor , instead of having to execute poetry in a subprocess to determine this.
  • 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
  • program_name: The name of the program that is being run. This is used primarily when outputting help messages, defaults to "poe"
PoeThePoet( cwd: Union[pathlib.Path, str, NoneType] = None, config: Union[Mapping[str, Any], poethepoet.config.PoeConfig, NoneType] = None, output: <class 'IO'> = <_io.StringIO object>, poetry_env_path: Optional[str] = None, config_name: Optional[str] = None, program_name: str = 'poe')
63    def __init__(
64        self,
65        cwd: Optional[Union[Path, str]] = None,
66        config: Optional[Union[Mapping[str, Any], "PoeConfig"]] = None,
67        output: IO = sys.stdout,
68        poetry_env_path: Optional[str] = None,
69        config_name: Optional[str] = None,
70        program_name: str = "poe",
71    ):
72        from .config import PoeConfig
73        from .ui import PoeUi
74
75        self.cwd = Path(cwd) if cwd else Path().resolve()
76
77        if self.cwd and self.cwd.is_file():
78            config_name = self.cwd.name
79            self.cwd = self.cwd.parent
80
81        self.config = (
82            config
83            if isinstance(config, PoeConfig)
84            else PoeConfig(cwd=self.cwd, table=config, config_name=config_name)
85        )
86        self.ui = PoeUi(output=output, program_name=program_name)
87        self._poetry_env_path = poetry_env_path
task_specs
130    @property
131    def task_specs(self):
132        if not self._task_specs:
133            from .task.base import TaskSpecFactory
134
135            self._task_specs = TaskSpecFactory(self.config)
136        return self._task_specs
def resolve_task( self, allow_hidden: bool = False) -> Optional[poethepoet.task.base.PoeTask]:
138    def resolve_task(self, allow_hidden: bool = False) -> Optional["PoeTask"]:
139        from .task.base import TaskContext
140
141        task = tuple(self.ui["task"])
142        if not task:
143            self.print_help(info="No task specified.")
144            return None
145
146        task_name = task[0]
147        if task_name not in self.config.task_names:
148            self.print_help(error=PoeException(f"Unrecognised task {task_name!r}"))
149            return None
150
151        if task_name.startswith("_") and not allow_hidden:
152            self.print_help(
153                error=PoeException(
154                    "Tasks prefixed with `_` cannot be executed directly"
155                ),
156            )
157            return None
158
159        task_spec = self.task_specs.get(task_name)
160        return task_spec.create_task(
161            invocation=task,
162            ctx=TaskContext(
163                config=self.config,
164                cwd=str(task_spec.source.cwd),
165                specs=self.task_specs,
166                ui=self.ui,
167            ),
168        )
def run_task( self, task: poethepoet.task.base.PoeTask, context: Optional[poethepoet.context.RunContext] = None) -> Optional[int]:
170    def run_task(
171        self, task: "PoeTask", context: Optional["RunContext"] = None
172    ) -> Optional[int]:
173        if context is None:
174            context = self.get_run_context()
175        try:
176            return task.run(context=context)
177        except ExecutionError as error:
178            self.ui.print_error(error=error)
179            return 1
180        except PoeException as error:
181            self.print_help(error=error)
182            return 1
def run_task_graph(self, task: poethepoet.task.base.PoeTask) -> Optional[int]:
184    def run_task_graph(self, task: "PoeTask") -> Optional[int]:
185        from .task.graph import TaskExecutionGraph
186
187        context = self.get_run_context(multistage=True)
188        graph = TaskExecutionGraph(task, context)
189        plan = graph.get_execution_plan()
190
191        for stage in plan:
192            for stage_task in stage:
193                if stage_task == task:
194                    # The final sink task gets special treatment
195                    return self.run_task(stage_task, context)
196
197                try:
198                    task_result = stage_task.run(context=context)
199                    if task_result:
200                        raise ExecutionError(
201                            f"Task graph aborted after failed task {stage_task.name!r}"
202                        )
203                except PoeException as error:
204                    self.print_help(error=error)
205                    return 1
206                except ExecutionError as error:
207                    self.ui.print_error(error=error)
208                    return 1
209        return 0
def get_run_context(self, multistage: bool = False) -> poethepoet.context.RunContext:
211    def get_run_context(self, multistage: bool = False) -> "RunContext":
212        from .context import RunContext
213
214        result = RunContext(
215            config=self.config,
216            ui=self.ui,
217            env=os.environ,
218            dry=self.ui["dry_run"],
219            poe_active=os.environ.get("POE_ACTIVE"),
220            multistage=multistage,
221            cwd=self.cwd,
222        )
223        if self._poetry_env_path:
224            # This allows the PoetryExecutor to use the venv from poetry directly
225            result.exec_cache["poetry_virtualenv"] = self._poetry_env_path
226        return result
def print_help( self, info: Optional[str] = None, error: Union[str, poethepoet.exceptions.PoeException, NoneType] = None):
228    def print_help(
229        self,
230        info: Optional[str] = None,
231        error: Optional[Union[str, PoeException]] = None,
232    ):
233        from .task.args import PoeTaskArgs
234
235        if isinstance(error, str):
236            error = PoeException(error)
237
238        tasks_help: Dict[
239            str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]
240        ] = {
241            task_name: (
242                (
243                    content.get("help", ""),
244                    PoeTaskArgs.get_help_content(content.get("args")),
245                )
246                if isinstance(content, dict)
247                else ("", tuple())
248            )
249            for task_name, content in self.config.tasks.items()
250        }
251
252        self.ui.print_help(tasks=tasks_help, info=info, error=error)