Skip to content

Commit 615381f

Browse files
committed
feat: support executable cli flags
1 parent ecd6bb2 commit 615381f

12 files changed

Lines changed: 710 additions & 65 deletions

File tree

docs/api/schema.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
:exclude-members: __init__, __new__
2222
```
2323

24+
```{eval-rst}
25+
.. autoclass:: interfacy.ExecutableFlag
26+
:exclude-members: __init__, __new__
27+
```
28+
2429
```{eval-rst}
2530
.. autoclass:: interfacy.schema.schema.Argument
2631
:exclude-members: __init__, __new__

interfacy/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from typing import TYPE_CHECKING
44

55
from interfacy.argparse_backend.argparser import Argparser
6+
from interfacy.executable_flag import ExecutableFlag
67
from interfacy.group import CommandGroup
78

89
if TYPE_CHECKING: # pragma: no cover
910
from interfacy.click_backend import ClickParser as ClickParser
1011

11-
__all__ = ["Argparser", "ClickParser", "CommandGroup"]
12+
__all__ = ["Argparser", "ClickParser", "CommandGroup", "ExecutableFlag"]
1213

1314

1415
def __getattr__(name: str) -> type[ClickParser]:

interfacy/appearance/renderer.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from stdl.st import ansi_len, with_style
77

88
from interfacy.appearance.layout import HelpLayout
9+
from interfacy.executable_flag import ExecutableFlag, executable_flag_to_argument
910
from interfacy.schema.schema import Argument, ArgumentKind, Command, ParserSchema, ValueShape
1011
from interfacy.util import get_terminal_width
1112

@@ -81,6 +82,7 @@ def render_parser_help(self, schema: ParserSchema, prog: str) -> str:
8182
prog,
8283
parser_description=schema.description,
8384
parser_epilog=schema.epilog,
85+
parser_executable_flags=schema.executable_flags,
8486
)
8587

8688
return self._render_multi_command_help(schema, prog)
@@ -92,6 +94,7 @@ def render_command_help(
9294
*,
9395
parser_description: str | None = None,
9496
parser_epilog: str | None = None,
97+
parser_executable_flags: list[ExecutableFlag] | None = None,
9598
) -> str:
9699
"""
97100
Render help text for one command schema.
@@ -101,23 +104,26 @@ def render_command_help(
101104
prog (str): Program name or invocation prefix.
102105
parser_description (str | None): Optional parser-level description override.
103106
parser_epilog (str | None): Optional parser-level epilog text.
107+
parser_executable_flags (list[ExecutableFlag] | None): Parser-level executable
108+
flags to merge into single-command help output.
104109
"""
105110
layout = self.layout
106111
all_args = command.initializer + command.parameters
107112
positionals = [a for a in all_args if a.kind == ArgumentKind.POSITIONAL]
108-
options = [a for a in all_args if a.kind == ArgumentKind.OPTION]
109-
options = layout.order_option_arguments_for_help(
110-
options,
113+
options = self._ordered_option_arguments(
114+
[a for a in all_args if a.kind == ArgumentKind.OPTION],
115+
command.executable_flags,
116+
parser_executable_flags=parser_executable_flags,
111117
rules=command.help_option_sort_effective,
112118
)
113119
help_arg = self._get_help_argument()
114120

115121
layout.prepare_default_field_width_for_arguments(
116-
[*([help_arg] if help_arg is not None else []), *all_args]
122+
[*([help_arg] if help_arg is not None else []), *positionals, *options]
117123
)
118124

119125
sections: list[str] = []
120-
usage = self._build_usage(command, prog)
126+
usage = self._build_usage(command, prog, parser_executable_flags=parser_executable_flags)
121127
description = parser_description or command.description
122128
self._append_usage_and_description(sections=sections, usage=usage, description=description)
123129

@@ -234,13 +240,22 @@ def _render_multi_command_help(self, schema: ParserSchema, prog: str) -> str:
234240
sections.append(schema.description)
235241

236242
help_arg = self._get_help_argument()
237-
if help_arg is not None:
238-
layout.prepare_default_field_width_for_arguments([help_arg])
239-
heading = self._style_section_heading("options")
240-
help_line = self._normalize_help_only_option_line(
241-
layout.format_argument(help_arg), help_arg
243+
root_options = self._ordered_option_arguments(
244+
[],
245+
schema.executable_flags,
246+
rules=schema.help_option_sort_effective,
247+
)
248+
root_options_with_help = [*([help_arg] if help_arg is not None else []), *root_options]
249+
if root_options_with_help:
250+
layout.prepare_default_field_width_for_arguments(root_options_with_help)
251+
sections.append(
252+
self._render_argument_section(
253+
"options",
254+
root_options_with_help,
255+
normalize_help_only=help_arg is not None and not root_options,
256+
)
257+
or ""
242258
)
243-
sections.append(f"{heading}\n{self._indent(help_line)}")
244259

245260
if schema.commands_help:
246261
sections.append(schema.commands_help)
@@ -252,12 +267,19 @@ def _render_multi_command_help(self, schema: ParserSchema, prog: str) -> str:
252267

253268
return "\n\n".join(sections) + "\n"
254269

255-
def _build_usage(self, command: Command, prog: str) -> str:
270+
def _build_usage(
271+
self,
272+
command: Command,
273+
prog: str,
274+
*,
275+
parser_executable_flags: list[ExecutableFlag] | None = None,
276+
) -> str:
256277
all_args = command.initializer + command.parameters
257278
positionals = [a for a in all_args if a.kind == ArgumentKind.POSITIONAL]
258-
options = [a for a in all_args if a.kind == ArgumentKind.OPTION]
259-
options = self.layout.order_option_arguments_for_help(
260-
options,
279+
options = self._ordered_option_arguments(
280+
[a for a in all_args if a.kind == ArgumentKind.OPTION],
281+
command.executable_flags,
282+
parser_executable_flags=parser_executable_flags,
261283
rules=command.help_option_sort_effective,
262284
)
263285
compact_options_usage = self.layout.compact_options_usage
@@ -314,6 +336,23 @@ def _build_usage(self, command: Command, prog: str) -> str:
314336

315337
return f"{usage_prefix}{usage_text}"
316338

339+
def _ordered_option_arguments(
340+
self,
341+
options: list[Argument],
342+
executable_flags: list[ExecutableFlag],
343+
*,
344+
parser_executable_flags: list[ExecutableFlag] | None = None,
345+
rules: list[object] | None = None,
346+
) -> list[Argument]:
347+
flag_arguments = [
348+
executable_flag_to_argument(flag)
349+
for flag in [*(parser_executable_flags or []), *executable_flags]
350+
]
351+
return self.layout.order_option_arguments_for_help(
352+
[*options, *flag_arguments],
353+
rules=rules,
354+
)
355+
317356
def _usage_token_for_subcommands(self, command: Command) -> str:
318357
token = self.layout.get_subcommand_usage_token()
319358
if "{command}" not in token or not command.subcommands:

0 commit comments

Comments
 (0)