poethepoet.ui

  1import os
  2import sys
  3from typing import IO, TYPE_CHECKING, List, Mapping, Optional, Sequence, Tuple, Union
  4
  5from .__version__ import __version__
  6from .exceptions import ConfigValidationError, ExecutionError, PoeException
  7
  8if TYPE_CHECKING:
  9    from argparse import ArgumentParser, Namespace
 10
 11    from pastel import Pastel
 12
 13
 14def guess_ansi_support(file):
 15    if os.environ.get("NO_COLOR", "0")[0] != "0":
 16        # https://no-color.org/
 17        return False
 18
 19    if (
 20        os.environ.get("GITHUB_ACTIONS", "false") == "true"
 21        and "PYTEST_CURRENT_TEST" not in os.environ
 22    ):
 23        return True
 24
 25    return (
 26        (sys.platform != "win32" or "ANSICON" in os.environ)
 27        and hasattr(file, "isatty")
 28        and file.isatty()
 29    )
 30
 31
 32STDOUT_ANSI_SUPPORT = guess_ansi_support(sys.stdout)
 33
 34
 35class PoeUi:
 36    args: "Namespace"
 37    _color: "Pastel"
 38
 39    def __init__(self, output: IO, program_name: str = "poe"):
 40        self.output = output
 41        self.program_name = program_name
 42        self._init_colors()
 43
 44    def _init_colors(self):
 45        from pastel import Pastel
 46
 47        self._color = Pastel(guess_ansi_support(self.output))
 48        self._color.add_style("u", "default", options="underline")
 49        self._color.add_style("hl", "light_gray")
 50        self._color.add_style("em", "cyan")
 51        self._color.add_style("em2", "cyan", options="italic")
 52        self._color.add_style("em3", "blue")
 53        self._color.add_style("h2", "default", options="bold")
 54        self._color.add_style("h2-dim", "default", options="dark")
 55        self._color.add_style("action", "light_blue")
 56        self._color.add_style("error", "light_red", options="bold")
 57
 58    def __getitem__(self, key: str):
 59        """Provide easy access to arguments"""
 60        return getattr(self.args, key, None)
 61
 62    def build_parser(self) -> "ArgumentParser":
 63        import argparse
 64
 65        parser = argparse.ArgumentParser(
 66            prog=self.program_name,
 67            description="Poe the Poet: A task runner that works well with poetry.",
 68            add_help=False,
 69            allow_abbrev=False,
 70        )
 71
 72        parser.add_argument(
 73            "-h",
 74            "--help",
 75            dest="help",
 76            action="store_true",
 77            default=False,
 78            help="Show this help page and exit",
 79        )
 80
 81        parser.add_argument(
 82            "--version",
 83            dest="version",
 84            action="store_true",
 85            default=False,
 86            help="Print the version and exit",
 87        )
 88
 89        parser.add_argument(
 90            "-v",
 91            "--verbose",
 92            dest="increase_verbosity",
 93            action="count",
 94            default=0,
 95            help="Increase command output (repeatable)",
 96        )
 97
 98        parser.add_argument(
 99            "-q",
100            "--quiet",
101            dest="decrease_verbosity",
102            action="count",
103            default=0,
104            help="Decrease command output (repeatable)",
105        )
106
107        parser.add_argument(
108            "-d",
109            "--dry-run",
110            dest="dry_run",
111            action="store_true",
112            default=False,
113            help="Print the task contents but don't actually run it",
114        )
115
116        parser.add_argument(
117            "-C",
118            "--directory",
119            dest="project_root",
120            metavar="PATH",
121            type=str,
122            default=os.environ.get("POE_PROJECT_DIR", None),
123            help="Specify where to find the pyproject.toml",
124        )
125
126        parser.add_argument(
127            "-e",
128            "--executor",
129            dest="executor",
130            metavar="EXECUTOR",
131            type=str,
132            default="",
133            help="Override the default task executor",
134        )
135
136        # legacy --root parameter, keep for backwards compatibility but help output is
137        # suppressed
138        parser.add_argument(
139            "--root",
140            dest="project_root",
141            metavar="PATH",
142            type=str,
143            default=None,
144            help=argparse.SUPPRESS,
145        )
146
147        ansi_group = parser.add_mutually_exclusive_group()
148        ansi_group.add_argument(
149            "--ansi",
150            dest="ansi",
151            action="store_true",
152            default=STDOUT_ANSI_SUPPORT,
153            help="Force enable ANSI output",
154        )
155        ansi_group.add_argument(
156            "--no-ansi",
157            dest="ansi",
158            action="store_false",
159            default=STDOUT_ANSI_SUPPORT,
160            help="Force disable ANSI output",
161        )
162
163        parser.add_argument("task", default=tuple(), nargs=argparse.REMAINDER)
164
165        return parser
166
167    def parse_args(self, cli_args: Sequence[str]):
168        self.parser = self.build_parser()
169        self.args = self.parser.parse_args(cli_args)
170        self.verbosity: int = self["increase_verbosity"] - self["decrease_verbosity"]
171        self._color.with_colors(self.args.ansi)
172
173    def set_default_verbosity(self, default_verbosity: int):
174        self.verbosity += default_verbosity
175
176    def print_help(
177        self,
178        tasks: Optional[
179            Mapping[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]]
180        ] = None,
181        info: Optional[str] = None,
182        error: Optional[PoeException] = None,
183    ):
184        # TODO: See if this can be done nicely with a custom HelpFormatter
185
186        # Ignore verbosity mode if help flag is set
187        verbosity = 0 if self["help"] else self.verbosity
188
189        result: List[Union[str, Sequence[str]]] = []
190        if verbosity >= 0:
191            result.append(
192                (
193                    "Poe the Poet - A task runner that works well with poetry.",
194                    f"version <em>{__version__}</em>",
195                )
196            )
197
198        if info:
199            result.append(f"{f'<em2>Result: {info}</em2>'}")
200
201        if error:
202            # TODO: send this to stderr instead?
203            error_lines = []
204            if isinstance(error, ConfigValidationError):
205                if error.task_name:
206                    if error.context:
207                        error_lines.append(
208                            f"{error.context} in task {error.task_name!r}"
209                        )
210                    else:
211                        error_lines.append(f"Invalid task {error.task_name!r}")
212                    if error.filename:
213                        error_lines[-1] += f" in file {error.filename}"
214                elif error.global_option:
215                    error_lines.append(f"Invalid global option {error.global_option!r}")
216                    if error.filename:
217                        error_lines[-1] += f" in file {error.filename}"
218            error_lines.extend(error.msg.split("\n"))
219            if error.cause:
220                error_lines.append(error.cause)
221            if error.__cause__:
222                error_lines.append(f"From: {error.__cause__!r}")
223
224            result.append(self._format_error_lines(error_lines))
225
226        if verbosity >= 0:
227            result.append(
228                (
229                    "<h2>Usage:</h2>",
230                    f"  <u>{self.program_name}</u>"
231                    " [global options]"
232                    " task [task arguments]",
233                )
234            )
235
236            # Use argparse for optional args
237            formatter = self.parser.formatter_class(prog=self.parser.prog)
238            action_group = self.parser._action_groups[1]
239            formatter.start_section(action_group.title)
240            formatter.add_arguments(action_group._group_actions)
241            formatter.end_section()
242            result.append(
243                ("<h2>Global options:</h2>", *formatter.format_help().split("\n")[1:])
244            )
245
246            if tasks:
247                max_task_len = max(
248                    max(
249                        len(task),
250                        max(
251                            [
252                                len(", ".join(str(opt) for opt in opts))
253                                for (opts, _, _) in args
254                            ]
255                            or (0,)
256                        )
257                        + 2,
258                    )
259                    for task, (_, args) in tasks.items()
260                )
261                col_width = max(20, min(30, max_task_len))
262
263                tasks_section = ["<h2>Configured tasks:</h2>"]
264                for task, (help_text, args_help) in tasks.items():
265                    if task.startswith("_"):
266                        continue
267                    tasks_section.append(
268                        f"  <em>{self._padr(task, col_width)}</em>  "
269                        f"{self._align(help_text, col_width)}"
270                    )
271                    for options, arg_help_text, default in args_help:
272                        formatted_options = ", ".join(str(opt) for opt in options)
273                        task_arg_help = [
274                            "   ",
275                            f"<em3>{self._padr(formatted_options, col_width-1)}</em3>",
276                        ]
277                        if arg_help_text:
278                            task_arg_help.append(self._align(arg_help_text, col_width))
279                        if default:
280                            if "\n" in (arg_help_text or ""):
281                                task_arg_help.append(
282                                    self._align(f"\n{default}", col_width, strip=False)
283                                )
284                            else:
285                                task_arg_help.append(default)
286                        tasks_section.append(" ".join(task_arg_help))
287
288                result.append(tasks_section)
289
290            else:
291                result.append("<h2-dim>NO TASKS CONFIGURED</h2-dim>")
292
293        if error and os.environ.get("POE_DEBUG", "0") == "1":
294            import traceback
295
296            result.append("".join(traceback.format_exception(error)).strip())
297
298        self._print(
299            "\n\n".join(
300                section if isinstance(section, str) else "\n".join(section).strip("\n")
301                for section in result
302            )
303            + "\n"
304            + ("\n" if verbosity >= 0 else "")
305        )
306
307    @staticmethod
308    def _align(text: str, width: int, *, strip: bool = True) -> str:
309        text = text.replace("\n", "\n" + " " * (width + 4))
310        return text.strip() if strip else text
311
312    @staticmethod
313    def _padr(text: str, width: int):
314        if len(text) >= width:
315            return text
316        return text + " " * (width - len(text))
317
318    def print_msg(self, message: str, verbosity=0, end="\n"):
319        if verbosity <= self.verbosity:
320            self._print(message, end=end)
321
322    def print_error(self, error: Union[PoeException, ExecutionError]):
323        error_lines = error.msg.split("\n")
324        if error.cause:
325            error_lines.append(f"From: {error.cause}")
326        if error.__cause__:
327            error_lines.append(f"From: {error.__cause__!r}")
328
329        for line in self._format_error_lines(error_lines):
330            self._print(line)
331
332        if os.environ.get("POE_DEBUG", "0") == "1":
333            import traceback
334
335            self._print("".join(traceback.format_exception(error)).strip())
336
337    def _format_error_lines(self, lines: Sequence[str]):
338        return (
339            f"<error>Error: {lines[0]}</error>",
340            *(f"<error>     | {line}</error>" for line in lines[1:]),
341        )
342
343    def print_version(self):
344        if self.verbosity >= 0:
345            result = f"Poe the Poet - version: <em>{__version__}</em>\n"
346        else:
347            result = f"{__version__}\n"
348        self._print(result)
349
350    def _print(self, message: str, *, end: str = "\n"):
351        print(self._color.colorize(message), end=end, file=self.output, flush=True)
def guess_ansi_support(file):
15def guess_ansi_support(file):
16    if os.environ.get("NO_COLOR", "0")[0] != "0":
17        # https://no-color.org/
18        return False
19
20    if (
21        os.environ.get("GITHUB_ACTIONS", "false") == "true"
22        and "PYTEST_CURRENT_TEST" not in os.environ
23    ):
24        return True
25
26    return (
27        (sys.platform != "win32" or "ANSICON" in os.environ)
28        and hasattr(file, "isatty")
29        and file.isatty()
30    )
STDOUT_ANSI_SUPPORT = True
class PoeUi:
 36class PoeUi:
 37    args: "Namespace"
 38    _color: "Pastel"
 39
 40    def __init__(self, output: IO, program_name: str = "poe"):
 41        self.output = output
 42        self.program_name = program_name
 43        self._init_colors()
 44
 45    def _init_colors(self):
 46        from pastel import Pastel
 47
 48        self._color = Pastel(guess_ansi_support(self.output))
 49        self._color.add_style("u", "default", options="underline")
 50        self._color.add_style("hl", "light_gray")
 51        self._color.add_style("em", "cyan")
 52        self._color.add_style("em2", "cyan", options="italic")
 53        self._color.add_style("em3", "blue")
 54        self._color.add_style("h2", "default", options="bold")
 55        self._color.add_style("h2-dim", "default", options="dark")
 56        self._color.add_style("action", "light_blue")
 57        self._color.add_style("error", "light_red", options="bold")
 58
 59    def __getitem__(self, key: str):
 60        """Provide easy access to arguments"""
 61        return getattr(self.args, key, None)
 62
 63    def build_parser(self) -> "ArgumentParser":
 64        import argparse
 65
 66        parser = argparse.ArgumentParser(
 67            prog=self.program_name,
 68            description="Poe the Poet: A task runner that works well with poetry.",
 69            add_help=False,
 70            allow_abbrev=False,
 71        )
 72
 73        parser.add_argument(
 74            "-h",
 75            "--help",
 76            dest="help",
 77            action="store_true",
 78            default=False,
 79            help="Show this help page and exit",
 80        )
 81
 82        parser.add_argument(
 83            "--version",
 84            dest="version",
 85            action="store_true",
 86            default=False,
 87            help="Print the version and exit",
 88        )
 89
 90        parser.add_argument(
 91            "-v",
 92            "--verbose",
 93            dest="increase_verbosity",
 94            action="count",
 95            default=0,
 96            help="Increase command output (repeatable)",
 97        )
 98
 99        parser.add_argument(
100            "-q",
101            "--quiet",
102            dest="decrease_verbosity",
103            action="count",
104            default=0,
105            help="Decrease command output (repeatable)",
106        )
107
108        parser.add_argument(
109            "-d",
110            "--dry-run",
111            dest="dry_run",
112            action="store_true",
113            default=False,
114            help="Print the task contents but don't actually run it",
115        )
116
117        parser.add_argument(
118            "-C",
119            "--directory",
120            dest="project_root",
121            metavar="PATH",
122            type=str,
123            default=os.environ.get("POE_PROJECT_DIR", None),
124            help="Specify where to find the pyproject.toml",
125        )
126
127        parser.add_argument(
128            "-e",
129            "--executor",
130            dest="executor",
131            metavar="EXECUTOR",
132            type=str,
133            default="",
134            help="Override the default task executor",
135        )
136
137        # legacy --root parameter, keep for backwards compatibility but help output is
138        # suppressed
139        parser.add_argument(
140            "--root",
141            dest="project_root",
142            metavar="PATH",
143            type=str,
144            default=None,
145            help=argparse.SUPPRESS,
146        )
147
148        ansi_group = parser.add_mutually_exclusive_group()
149        ansi_group.add_argument(
150            "--ansi",
151            dest="ansi",
152            action="store_true",
153            default=STDOUT_ANSI_SUPPORT,
154            help="Force enable ANSI output",
155        )
156        ansi_group.add_argument(
157            "--no-ansi",
158            dest="ansi",
159            action="store_false",
160            default=STDOUT_ANSI_SUPPORT,
161            help="Force disable ANSI output",
162        )
163
164        parser.add_argument("task", default=tuple(), nargs=argparse.REMAINDER)
165
166        return parser
167
168    def parse_args(self, cli_args: Sequence[str]):
169        self.parser = self.build_parser()
170        self.args = self.parser.parse_args(cli_args)
171        self.verbosity: int = self["increase_verbosity"] - self["decrease_verbosity"]
172        self._color.with_colors(self.args.ansi)
173
174    def set_default_verbosity(self, default_verbosity: int):
175        self.verbosity += default_verbosity
176
177    def print_help(
178        self,
179        tasks: Optional[
180            Mapping[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]]
181        ] = None,
182        info: Optional[str] = None,
183        error: Optional[PoeException] = None,
184    ):
185        # TODO: See if this can be done nicely with a custom HelpFormatter
186
187        # Ignore verbosity mode if help flag is set
188        verbosity = 0 if self["help"] else self.verbosity
189
190        result: List[Union[str, Sequence[str]]] = []
191        if verbosity >= 0:
192            result.append(
193                (
194                    "Poe the Poet - A task runner that works well with poetry.",
195                    f"version <em>{__version__}</em>",
196                )
197            )
198
199        if info:
200            result.append(f"{f'<em2>Result: {info}</em2>'}")
201
202        if error:
203            # TODO: send this to stderr instead?
204            error_lines = []
205            if isinstance(error, ConfigValidationError):
206                if error.task_name:
207                    if error.context:
208                        error_lines.append(
209                            f"{error.context} in task {error.task_name!r}"
210                        )
211                    else:
212                        error_lines.append(f"Invalid task {error.task_name!r}")
213                    if error.filename:
214                        error_lines[-1] += f" in file {error.filename}"
215                elif error.global_option:
216                    error_lines.append(f"Invalid global option {error.global_option!r}")
217                    if error.filename:
218                        error_lines[-1] += f" in file {error.filename}"
219            error_lines.extend(error.msg.split("\n"))
220            if error.cause:
221                error_lines.append(error.cause)
222            if error.__cause__:
223                error_lines.append(f"From: {error.__cause__!r}")
224
225            result.append(self._format_error_lines(error_lines))
226
227        if verbosity >= 0:
228            result.append(
229                (
230                    "<h2>Usage:</h2>",
231                    f"  <u>{self.program_name}</u>"
232                    " [global options]"
233                    " task [task arguments]",
234                )
235            )
236
237            # Use argparse for optional args
238            formatter = self.parser.formatter_class(prog=self.parser.prog)
239            action_group = self.parser._action_groups[1]
240            formatter.start_section(action_group.title)
241            formatter.add_arguments(action_group._group_actions)
242            formatter.end_section()
243            result.append(
244                ("<h2>Global options:</h2>", *formatter.format_help().split("\n")[1:])
245            )
246
247            if tasks:
248                max_task_len = max(
249                    max(
250                        len(task),
251                        max(
252                            [
253                                len(", ".join(str(opt) for opt in opts))
254                                for (opts, _, _) in args
255                            ]
256                            or (0,)
257                        )
258                        + 2,
259                    )
260                    for task, (_, args) in tasks.items()
261                )
262                col_width = max(20, min(30, max_task_len))
263
264                tasks_section = ["<h2>Configured tasks:</h2>"]
265                for task, (help_text, args_help) in tasks.items():
266                    if task.startswith("_"):
267                        continue
268                    tasks_section.append(
269                        f"  <em>{self._padr(task, col_width)}</em>  "
270                        f"{self._align(help_text, col_width)}"
271                    )
272                    for options, arg_help_text, default in args_help:
273                        formatted_options = ", ".join(str(opt) for opt in options)
274                        task_arg_help = [
275                            "   ",
276                            f"<em3>{self._padr(formatted_options, col_width-1)}</em3>",
277                        ]
278                        if arg_help_text:
279                            task_arg_help.append(self._align(arg_help_text, col_width))
280                        if default:
281                            if "\n" in (arg_help_text or ""):
282                                task_arg_help.append(
283                                    self._align(f"\n{default}", col_width, strip=False)
284                                )
285                            else:
286                                task_arg_help.append(default)
287                        tasks_section.append(" ".join(task_arg_help))
288
289                result.append(tasks_section)
290
291            else:
292                result.append("<h2-dim>NO TASKS CONFIGURED</h2-dim>")
293
294        if error and os.environ.get("POE_DEBUG", "0") == "1":
295            import traceback
296
297            result.append("".join(traceback.format_exception(error)).strip())
298
299        self._print(
300            "\n\n".join(
301                section if isinstance(section, str) else "\n".join(section).strip("\n")
302                for section in result
303            )
304            + "\n"
305            + ("\n" if verbosity >= 0 else "")
306        )
307
308    @staticmethod
309    def _align(text: str, width: int, *, strip: bool = True) -> str:
310        text = text.replace("\n", "\n" + " " * (width + 4))
311        return text.strip() if strip else text
312
313    @staticmethod
314    def _padr(text: str, width: int):
315        if len(text) >= width:
316            return text
317        return text + " " * (width - len(text))
318
319    def print_msg(self, message: str, verbosity=0, end="\n"):
320        if verbosity <= self.verbosity:
321            self._print(message, end=end)
322
323    def print_error(self, error: Union[PoeException, ExecutionError]):
324        error_lines = error.msg.split("\n")
325        if error.cause:
326            error_lines.append(f"From: {error.cause}")
327        if error.__cause__:
328            error_lines.append(f"From: {error.__cause__!r}")
329
330        for line in self._format_error_lines(error_lines):
331            self._print(line)
332
333        if os.environ.get("POE_DEBUG", "0") == "1":
334            import traceback
335
336            self._print("".join(traceback.format_exception(error)).strip())
337
338    def _format_error_lines(self, lines: Sequence[str]):
339        return (
340            f"<error>Error: {lines[0]}</error>",
341            *(f"<error>     | {line}</error>" for line in lines[1:]),
342        )
343
344    def print_version(self):
345        if self.verbosity >= 0:
346            result = f"Poe the Poet - version: <em>{__version__}</em>\n"
347        else:
348            result = f"{__version__}\n"
349        self._print(result)
350
351    def _print(self, message: str, *, end: str = "\n"):
352        print(self._color.colorize(message), end=end, file=self.output, flush=True)
PoeUi(output: <class 'IO'>, program_name: str = 'poe')
40    def __init__(self, output: IO, program_name: str = "poe"):
41        self.output = output
42        self.program_name = program_name
43        self._init_colors()
args: argparse.Namespace
output
program_name
def build_parser(self) -> argparse.ArgumentParser:
 63    def build_parser(self) -> "ArgumentParser":
 64        import argparse
 65
 66        parser = argparse.ArgumentParser(
 67            prog=self.program_name,
 68            description="Poe the Poet: A task runner that works well with poetry.",
 69            add_help=False,
 70            allow_abbrev=False,
 71        )
 72
 73        parser.add_argument(
 74            "-h",
 75            "--help",
 76            dest="help",
 77            action="store_true",
 78            default=False,
 79            help="Show this help page and exit",
 80        )
 81
 82        parser.add_argument(
 83            "--version",
 84            dest="version",
 85            action="store_true",
 86            default=False,
 87            help="Print the version and exit",
 88        )
 89
 90        parser.add_argument(
 91            "-v",
 92            "--verbose",
 93            dest="increase_verbosity",
 94            action="count",
 95            default=0,
 96            help="Increase command output (repeatable)",
 97        )
 98
 99        parser.add_argument(
100            "-q",
101            "--quiet",
102            dest="decrease_verbosity",
103            action="count",
104            default=0,
105            help="Decrease command output (repeatable)",
106        )
107
108        parser.add_argument(
109            "-d",
110            "--dry-run",
111            dest="dry_run",
112            action="store_true",
113            default=False,
114            help="Print the task contents but don't actually run it",
115        )
116
117        parser.add_argument(
118            "-C",
119            "--directory",
120            dest="project_root",
121            metavar="PATH",
122            type=str,
123            default=os.environ.get("POE_PROJECT_DIR", None),
124            help="Specify where to find the pyproject.toml",
125        )
126
127        parser.add_argument(
128            "-e",
129            "--executor",
130            dest="executor",
131            metavar="EXECUTOR",
132            type=str,
133            default="",
134            help="Override the default task executor",
135        )
136
137        # legacy --root parameter, keep for backwards compatibility but help output is
138        # suppressed
139        parser.add_argument(
140            "--root",
141            dest="project_root",
142            metavar="PATH",
143            type=str,
144            default=None,
145            help=argparse.SUPPRESS,
146        )
147
148        ansi_group = parser.add_mutually_exclusive_group()
149        ansi_group.add_argument(
150            "--ansi",
151            dest="ansi",
152            action="store_true",
153            default=STDOUT_ANSI_SUPPORT,
154            help="Force enable ANSI output",
155        )
156        ansi_group.add_argument(
157            "--no-ansi",
158            dest="ansi",
159            action="store_false",
160            default=STDOUT_ANSI_SUPPORT,
161            help="Force disable ANSI output",
162        )
163
164        parser.add_argument("task", default=tuple(), nargs=argparse.REMAINDER)
165
166        return parser
def parse_args(self, cli_args: Sequence[str]):
168    def parse_args(self, cli_args: Sequence[str]):
169        self.parser = self.build_parser()
170        self.args = self.parser.parse_args(cli_args)
171        self.verbosity: int = self["increase_verbosity"] - self["decrease_verbosity"]
172        self._color.with_colors(self.args.ansi)
def set_default_verbosity(self, default_verbosity: int):
174    def set_default_verbosity(self, default_verbosity: int):
175        self.verbosity += default_verbosity
def print_help( self, tasks: Optional[Mapping[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]]] = None, info: Optional[str] = None, error: Optional[poethepoet.exceptions.PoeException] = None):
177    def print_help(
178        self,
179        tasks: Optional[
180            Mapping[str, Tuple[str, Sequence[Tuple[Tuple[str, ...], str, str]]]]
181        ] = None,
182        info: Optional[str] = None,
183        error: Optional[PoeException] = None,
184    ):
185        # TODO: See if this can be done nicely with a custom HelpFormatter
186
187        # Ignore verbosity mode if help flag is set
188        verbosity = 0 if self["help"] else self.verbosity
189
190        result: List[Union[str, Sequence[str]]] = []
191        if verbosity >= 0:
192            result.append(
193                (
194                    "Poe the Poet - A task runner that works well with poetry.",
195                    f"version <em>{__version__}</em>",
196                )
197            )
198
199        if info:
200            result.append(f"{f'<em2>Result: {info}</em2>'}")
201
202        if error:
203            # TODO: send this to stderr instead?
204            error_lines = []
205            if isinstance(error, ConfigValidationError):
206                if error.task_name:
207                    if error.context:
208                        error_lines.append(
209                            f"{error.context} in task {error.task_name!r}"
210                        )
211                    else:
212                        error_lines.append(f"Invalid task {error.task_name!r}")
213                    if error.filename:
214                        error_lines[-1] += f" in file {error.filename}"
215                elif error.global_option:
216                    error_lines.append(f"Invalid global option {error.global_option!r}")
217                    if error.filename:
218                        error_lines[-1] += f" in file {error.filename}"
219            error_lines.extend(error.msg.split("\n"))
220            if error.cause:
221                error_lines.append(error.cause)
222            if error.__cause__:
223                error_lines.append(f"From: {error.__cause__!r}")
224
225            result.append(self._format_error_lines(error_lines))
226
227        if verbosity >= 0:
228            result.append(
229                (
230                    "<h2>Usage:</h2>",
231                    f"  <u>{self.program_name}</u>"
232                    " [global options]"
233                    " task [task arguments]",
234                )
235            )
236
237            # Use argparse for optional args
238            formatter = self.parser.formatter_class(prog=self.parser.prog)
239            action_group = self.parser._action_groups[1]
240            formatter.start_section(action_group.title)
241            formatter.add_arguments(action_group._group_actions)
242            formatter.end_section()
243            result.append(
244                ("<h2>Global options:</h2>", *formatter.format_help().split("\n")[1:])
245            )
246
247            if tasks:
248                max_task_len = max(
249                    max(
250                        len(task),
251                        max(
252                            [
253                                len(", ".join(str(opt) for opt in opts))
254                                for (opts, _, _) in args
255                            ]
256                            or (0,)
257                        )
258                        + 2,
259                    )
260                    for task, (_, args) in tasks.items()
261                )
262                col_width = max(20, min(30, max_task_len))
263
264                tasks_section = ["<h2>Configured tasks:</h2>"]
265                for task, (help_text, args_help) in tasks.items():
266                    if task.startswith("_"):
267                        continue
268                    tasks_section.append(
269                        f"  <em>{self._padr(task, col_width)}</em>  "
270                        f"{self._align(help_text, col_width)}"
271                    )
272                    for options, arg_help_text, default in args_help:
273                        formatted_options = ", ".join(str(opt) for opt in options)
274                        task_arg_help = [
275                            "   ",
276                            f"<em3>{self._padr(formatted_options, col_width-1)}</em3>",
277                        ]
278                        if arg_help_text:
279                            task_arg_help.append(self._align(arg_help_text, col_width))
280                        if default:
281                            if "\n" in (arg_help_text or ""):
282                                task_arg_help.append(
283                                    self._align(f"\n{default}", col_width, strip=False)
284                                )
285                            else:
286                                task_arg_help.append(default)
287                        tasks_section.append(" ".join(task_arg_help))
288
289                result.append(tasks_section)
290
291            else:
292                result.append("<h2-dim>NO TASKS CONFIGURED</h2-dim>")
293
294        if error and os.environ.get("POE_DEBUG", "0") == "1":
295            import traceback
296
297            result.append("".join(traceback.format_exception(error)).strip())
298
299        self._print(
300            "\n\n".join(
301                section if isinstance(section, str) else "\n".join(section).strip("\n")
302                for section in result
303            )
304            + "\n"
305            + ("\n" if verbosity >= 0 else "")
306        )
def print_msg(self, message: str, verbosity=0, end='\n'):
319    def print_msg(self, message: str, verbosity=0, end="\n"):
320        if verbosity <= self.verbosity:
321            self._print(message, end=end)
def print_error( self, error: Union[poethepoet.exceptions.PoeException, poethepoet.exceptions.ExecutionError]):
323    def print_error(self, error: Union[PoeException, ExecutionError]):
324        error_lines = error.msg.split("\n")
325        if error.cause:
326            error_lines.append(f"From: {error.cause}")
327        if error.__cause__:
328            error_lines.append(f"From: {error.__cause__!r}")
329
330        for line in self._format_error_lines(error_lines):
331            self._print(line)
332
333        if os.environ.get("POE_DEBUG", "0") == "1":
334            import traceback
335
336            self._print("".join(traceback.format_exception(error)).strip())
def print_version(self):
344    def print_version(self):
345        if self.verbosity >= 0:
346            result = f"Poe the Poet - version: <em>{__version__}</em>\n"
347        else:
348            result = f"{__version__}\n"
349        self._print(result)