• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oir / startle / 23171014881

16 Mar 2026 11:35PM UTC coverage: 98.606% (+0.02%) from 98.586%
23171014881

Pull #139

github

web-flow
Merge c873cbfd2 into 24e8bde54
Pull Request #139: Tabularize usage line in help text

249 of 249 branches covered (100.0%)

Branch coverage included in aggregate %.

1307 of 1329 relevant lines covered (98.34%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.22
startle/_help.py
1
"""
2
Utilities for prettifying and formatting of help messages.
3
"""
4

5
from enum import Enum
1✔
6
from typing import Any, Literal
1✔
7

8
from rich.cells import cell_len
1✔
9
from rich.text import Text
1✔
10

11
from .arg import Arg, Name
1✔
12

13

14
class Sty:
1✔
15
    name = "bold"
1✔
16
    pos_name = "bold"
1✔
17
    opt = "green"
1✔
18
    var = "blue"
1✔
19
    literal_var = ""
1✔
20
    title = "bold underline dim"
1✔
21

22

23
def name_usage(name: Name, kind: Literal["listing", "usage line"]) -> Text:
1✔
24
    """
25
    Format the name of an argument for either detailed options table (kind: listing)
26
    or the brief usage line (kind: usage line).
27
    """
28

29
    def fmt(name: str, short: bool) -> Text:
1✔
30
        if name.startswith("<") and name.endswith(">"):
1✔
31
            # very special case for var kwargs.
32
            # TODO: maybe this should be done elsewhere / differently?
33
            name_ = name.strip("<>")
1✔
34
            return Text.assemble(
1✔
35
                ("--", f"{Sty.name} {Sty.opt} not dim"),
36
                ("<", "cyan not dim"),
37
                (name_, f"{Sty.name} cyan not dim"),
38
                (">", "cyan not dim"),
39
            )
40
        return Text(
1✔
41
            f"-{name}" if short else f"--{name}",
42
            style=f"{Sty.name} {Sty.opt} not dim",
43
        )
44

45
    if kind == "listing":
1✔
46
        name_list: list[Text] = []
1✔
47
        if name.short:
1✔
48
            name_list.append(fmt(name.short, True))
1✔
49
        if name.long:
1✔
50
            name_list.append(fmt(name.long, False))
1✔
51
        return Text("|", style=f"{Sty.opt} dim").join(name_list)
1✔
52
    else:
53
        if name.long:
1✔
54
            return fmt(name.long, False)
1✔
55
        else:
56
            return fmt(name.short, True)
×
57

58

59
def _meta(metavar: list[str] | str) -> Text:
1✔
60
    return (
1✔
61
        Text(metavar)
62
        if isinstance(metavar, str)
63
        else Text("|", style="dim").join([
64
            Text(m, style=f"{Sty.literal_var} not dim") for m in metavar
65
        ])
66
    )
67

68

69
def _repeated(text: Text) -> Text:
1✔
70
    repeat = Text("[") + text.copy() + " ...]"
1✔
71
    repeat.stylize("dim")
1✔
72
    return Text.assemble(text, " ", repeat)
1✔
73

74

75
def _pos_usage(arg: Arg) -> Text:
1✔
76
    text = Text.assemble("<", (f"{arg.name}:", Sty.pos_name), _meta(arg.metavar), ">")
1✔
77
    text.stylize(Sty.var)
1✔
78
    if arg.is_nary:
1✔
79
        text = _repeated(text)
1✔
80
    return text
1✔
81

82

83
def _opt_usage(arg: Arg, kind: Literal["listing", "usage line"]) -> Text:
1✔
84
    if isinstance(arg.metavar, list):
1✔
85
        option = _meta(arg.metavar)
1✔
86
        option.stylize(Sty.var)
1✔
87
    else:
88
        option = Text(f"<{arg.metavar}>", style=Sty.var)
1✔
89
    if arg.is_nary:
1✔
90
        option = _repeated(option)
1✔
91
    return Text.assemble(name_usage(arg.name, kind), " ", option)
1✔
92

93

94
def usage(arg: Arg, kind: Literal["listing", "usage line"] = "listing") -> Text:
1✔
95
    """
96
    Format an argument (possibly with its metavar) for either detailed options
97
    table (kind: listing) or the brief usage line (kind: usage line).
98
    """
99
    if arg.is_positional and not arg.is_named:
1✔
100
        text = _pos_usage(arg)
1✔
101
    elif arg.is_flag:
1✔
102
        if kind == "listing":
1✔
103
            text = Text.assemble(name_usage(arg.name, kind), " ")
1✔
104
        else:
105
            text = name_usage(arg.name, kind)
1✔
106
    else:
107
        text = _opt_usage(arg, kind)
1✔
108

109
    if not arg.required and kind == "usage line":
1✔
110
        text = Text.assemble("[", text, "]")
1✔
111
    return text
1✔
112

113

114
def default_value(val: Any) -> Text:
1✔
115
    if isinstance(val, str) and isinstance(val, Enum):
1✔
116
        return Text(val.value, style=Sty.opt)
1✔
117
    if isinstance(val, Enum):
1✔
118
        return Text(val.name.lower().replace("_", "-"), style=Sty.opt)
1✔
119
    if isinstance(val, str) and val == "":
1✔
120
        return Text('""', style=f"{Sty.opt} dim")
1✔
121
    return Text(str(val), style=Sty.opt)
1✔
122

123

124
def help(arg: Arg) -> Text:
1✔
125
    helptext = Text(arg.help, style="italic")
1✔
126
    delim = " " if helptext else ""
1✔
127
    if str(arg.name) == "":
1✔
128
        helptext = Text.assemble(
1✔
129
            helptext, delim, ("(unknown positional arguments)", "cyan")
130
        )
131
    elif arg.name.long == "<key>":
1✔
132
        helptext = Text.assemble(helptext, delim, ("(unknown options)", "cyan"))
1✔
133
    elif arg.is_flag:
1✔
134
        helptext = Text.assemble(helptext, delim, ("(flag)", Sty.opt))
1✔
135
    elif arg.required:
1✔
136
        helptext = Text.assemble(helptext, delim, ("(required)", "yellow"))
1✔
137
    else:
138
        if arg.default_factory is not None:
1✔
139
            # is it harmful to just call the factory here?
140
            def_val = default_value(arg.default_factory())
1✔
141
        else:
142
            def_val = default_value(arg.default)
1✔
143
        helptext = Text.assemble(
1✔
144
            helptext,
145
            delim,
146
            ("(default: ", Sty.opt),
147
            def_val,
148
            (")", Sty.opt),
149
        )
150
    return helptext
1✔
151

152

153
def var_args_usage_line(arg: Arg) -> Text:
1✔
154
    return Text.assemble("[", _pos_usage(arg), "]")
1✔
155

156

157
def var_kwargs_usage_line(arg: Arg) -> Text:
1✔
158
    return Text.assemble("[", _repeated(_opt_usage(arg, "usage line")), "]")
1✔
159

160

161
def wrap_usage(name: str, components: list[Text], width: int) -> Text:
1✔
162
    """
163
    Custom word-wrapping for usage lines that avoids splitting individual components
164
    (e.g. we want "--foo bar" or "[--foo bar]" stay together).
165
    Continuation lines are indented to align after the program name.
166

167
    This was needed because (afaik) there is no way to declare non-breaking spaces in rich,
168
    and even in that case I would prefer to use actual space char.
169
    """
170

171
    indent = len(name) + 1
1✔
172
    lines: list[Text] = []
1✔
173
    current = Text(f"{name} ")
1✔
174
    current_len = indent
1✔
175

176
    for comp in components:
1✔
177
        comp_len = cell_len(comp.plain)
1✔
178
        if current_len > indent and current_len + comp_len + 1 > width:
1✔
179
            lines.append(current)
1✔
180
            current = Text(" " * indent) + comp
1✔
181
            current_len = indent + comp_len
1✔
182
        else:
183
            if current_len > indent:
1✔
184
                current.append(" ")
1✔
185
                current_len += 1
1✔
186
            current = Text.assemble(current, comp)
1✔
187
            current_len += comp_len
1✔
188

189
    lines.append(current)
1✔
190
    return Text("\n").join(lines)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc