Skip to content

Commit c92254a

Browse files
authored
Merge pull request #721 from BrianPugh/bugfix/bool-alias-negative
Bugfix/bool alias negative
2 parents aaab351 + a8f123a commit c92254a

20 files changed

+196
-205
lines changed

cyclopts/help/formatters/html.py

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,43 +113,28 @@ def _format_command_panel(self, entries: list["HelpEntry"], console: Optional["C
113113
self._output.write('<ul class="commands-list">\n')
114114

115115
for entry in entries:
116-
# Get command name(s)
117-
names = []
118-
if entry.names:
119-
names.extend(entry.names)
120-
if entry.shorts:
121-
names.extend(entry.shorts)
122-
123-
# Generate anchor link if we have app context
124-
if self.app_name and names:
125-
# Build the anchor ID for this command
126-
primary_name = names[0]
127-
aliases = names[1:]
116+
names = entry.all_options
117+
if not names:
118+
name_html = ""
119+
elif self.app_name:
120+
# Generate anchor link
121+
primary_name, aliases = names[0], names[1:]
128122
if self.command_chain:
129-
# We're in a subcommand, build full chain
130123
full_chain = self.command_chain + [primary_name]
131124
anchor_id = f"{self.app_name}-{'-'.join(full_chain[1:])}".lower()
132125
else:
133-
# Top-level command
134126
anchor_id = f"{self.app_name}-{primary_name}".lower()
135-
136-
# Create linked command name with aliases in parentheses
137127
name_html = f'<a href="#{anchor_id}"><code>{escape_html(primary_name)}</code></a>'
138128
if aliases:
139-
# Add aliases in parentheses
140129
aliases_str = ", ".join(escape_html(n) for n in aliases)
141130
name_html = f"{name_html} ({aliases_str})"
142131
else:
143-
# Fallback to non-linked format with aliases in parentheses
144-
if names:
145-
primary_name = names[0]
146-
aliases = names[1:]
147-
name_html = f"<code>{escape_html(primary_name)}</code>"
148-
if aliases:
149-
aliases_str = ", ".join(escape_html(n) for n in aliases)
150-
name_html = f"{name_html} ({aliases_str})"
151-
else:
152-
name_html = ""
132+
# Non-linked format with aliases in parentheses
133+
primary_name, aliases = names[0], names[1:]
134+
name_html = f"<code>{escape_html(primary_name)}</code>"
135+
if aliases:
136+
aliases_str = ", ".join(escape_html(n) for n in aliases)
137+
name_html = f"{name_html} ({aliases_str})"
153138

154139
desc_html = escape_html(extract_text(entry.description, console))
155140

@@ -177,15 +162,8 @@ def _format_parameter_panel(self, entries: list["HelpEntry"], console: Optional[
177162
self._output.write('<ul class="parameters-list">\n')
178163

179164
for entry in entries:
180-
# Build parameter names
181-
names = []
182-
if entry.names:
183-
names.extend(entry.names)
184-
if entry.shorts:
185-
names.extend(entry.shorts)
186-
187165
# Format name with code tags
188-
if names:
166+
if names := entry.all_options:
189167
name_html = ", ".join(f"<code>{escape_html(n)}</code>" for n in names)
190168
else:
191169
name_html = ""

cyclopts/help/formatters/markdown.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,9 @@ def _format_command_panel(self, entries: list["HelpEntry"], console: Optional["C
102102
"""
103103
# Always use list style for Typer-like output
104104
for entry in entries:
105-
# Get command name(s)
106-
names = []
107-
if entry.names:
108-
names.extend(entry.names)
109-
if entry.shorts:
110-
names.extend(entry.shorts)
111-
112-
if names:
105+
if names := entry.all_options:
113106
# Use first name as primary, show aliases in parentheses
114-
primary_name = names[0]
115-
aliases = names[1:]
107+
primary_name, aliases = names[0], names[1:]
116108
if aliases:
117109
name_display = f"{primary_name} ({', '.join(aliases)})"
118110
else:
@@ -138,14 +130,7 @@ def _format_parameter_panel(self, entries: list["HelpEntry"], console: Optional[
138130
"""
139131
# Always use list style for Typer-like output
140132
for entry in entries:
141-
# Build parameter names
142-
names = []
143-
if entry.names:
144-
names.extend(entry.names)
145-
if entry.shorts:
146-
names.extend(entry.shorts)
147-
148-
if names:
133+
if names := entry.all_options:
149134
# Separate positional names from option names
150135
positional_names = [n for n in names if not n.startswith("-")]
151136
short_opts = [n for n in names if n.startswith("-") and not n.startswith("--")]

cyclopts/help/formatters/plain.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,15 @@ def __call__(
109109

110110
# Print each entry in the panel
111111
for entry in panel.entries:
112-
# Extract the components
113-
# Join names and shorts if they are tuples
114-
names_text = " ".join(entry.names) if entry.names else ""
115-
shorts_text = " ".join(entry.shorts) if entry.shorts else ""
116112
desc = _to_plain_text(entry.description, console)
117113

118114
# Format the entry line
119-
if names_text or shorts_text:
120-
# Handle parameters section specially
115+
if entry.all_options:
121116
if panel.format == "parameter":
122-
self._format_parameter_entry(entry.names, entry.shorts, desc, console, entry)
117+
self._format_parameter_entry(entry.all_options, desc, console, entry)
123118
else:
124-
# For commands or other panels
125-
self._format_command_entry(entry.names, entry.shorts, desc, console)
119+
# Command formatter needs separate longs/shorts for its specific layout
120+
self._format_command_entry(entry.positive_names, entry.positive_shorts, desc, console)
126121

127122
# Add trailing newline for visual separation between panels
128123
console.print()
@@ -179,8 +174,7 @@ def render_description(
179174

180175
def _format_parameter_entry(
181176
self,
182-
names: tuple[str, ...],
183-
shorts: tuple[str, ...],
177+
options: tuple[str, ...],
184178
desc: str,
185179
console: "Console",
186180
entry: "HelpEntry",
@@ -189,21 +183,16 @@ def _format_parameter_entry(
189183
190184
Parameters
191185
----------
192-
names : tuple[str, ...]
193-
Parameter long names.
194-
shorts : tuple[str, ...]
195-
Short forms of the parameter.
186+
options : tuple[str, ...]
187+
All parameter options in display order.
196188
desc : str
197189
Parameter description.
198190
console : ~rich.console.Console
199191
Console to print to.
200192
entry : HelpEntry
201193
The full help entry with metadata fields.
202194
"""
203-
# Combine all names and shorts
204-
all_options = list(names) + list(shorts)
205-
206-
if not all_options:
195+
if not options:
207196
return
208197

209198
# Build the description with metadata
@@ -228,22 +217,13 @@ def _format_parameter_entry(
228217

229218
full_desc = " ".join(desc_parts)
230219

231-
# Format output based on number of options
232-
if len(all_options) > 1:
233-
# Multiple options - show them all on first line with description
234-
options_str = ", ".join(all_options)
235-
if full_desc:
236-
text = f"{options_str}: {full_desc}"
237-
else:
238-
text = options_str
239-
self._print_plain(console, textwrap.indent(text, self.indent))
220+
# Format: "option1, option2, ...: description"
221+
options_str = ", ".join(options)
222+
if full_desc:
223+
text = f"{options_str}: {full_desc}"
240224
else:
241-
# Single option
242-
if full_desc:
243-
text = f"{all_options[0]}: {full_desc}"
244-
else:
245-
text = all_options[0]
246-
self._print_plain(console, textwrap.indent(text, self.indent))
225+
text = options_str
226+
self._print_plain(console, textwrap.indent(text, self.indent))
247227

248228
def _format_command_entry(
249229
self,

cyclopts/help/formatters/rst.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,17 +97,9 @@ def _format_command_panel(self, entries: list["HelpEntry"], console: Optional["C
9797
Console for text extraction.
9898
"""
9999
for entry in entries:
100-
# Get command name(s)
101-
names = []
102-
if entry.names:
103-
names.extend(entry.names)
104-
if entry.shorts:
105-
names.extend(entry.shorts)
106-
107-
if names:
100+
if names := entry.all_options:
108101
# Use first name as primary, show aliases in parentheses
109-
primary_name = names[0]
110-
aliases = names[1:]
102+
primary_name, aliases = names[0], names[1:]
111103
if aliases:
112104
name_display = f"{primary_name} ({', '.join(aliases)})"
113105
else:
@@ -140,14 +132,7 @@ def _format_parameter_panel(self, entries: list["HelpEntry"], console: Optional[
140132
Console for text extraction.
141133
"""
142134
for entry in entries:
143-
# Build parameter names
144-
names = []
145-
if entry.names:
146-
names.extend(entry.names)
147-
if entry.shorts:
148-
names.extend(entry.shorts)
149-
150-
if names:
135+
if names := entry.all_options:
151136
# Determine if we should display as positional based on requirement and default
152137
is_positional = entry.required and entry.default is None and not any(n.startswith("-") for n in names)
153138

cyclopts/help/help.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,32 @@ def _text_factory():
6161
class HelpEntry:
6262
"""Container for help table entry data."""
6363

64-
names: tuple[str, ...] = ()
65-
"""Long option names (e.g., "--verbose", "--help")."""
64+
positive_names: tuple[str, ...] = ()
65+
"""Positive long option names (e.g., "--verbose", "--dry-run")."""
6666

67-
shorts: tuple[str, ...] = ()
68-
"""Short option names (e.g., "-v", "-h")."""
67+
positive_shorts: tuple[str, ...] = ()
68+
"""Positive short option names (e.g., "-v", "-n")."""
69+
70+
negative_names: tuple[str, ...] = ()
71+
"""Negative long option names (e.g., "--no-verbose", "--no-dry-run")."""
72+
73+
negative_shorts: tuple[str, ...] = ()
74+
"""Negative short option names (e.g., "-N"). Rarely used."""
75+
76+
@property
77+
def names(self) -> tuple[str, ...]:
78+
"""All long option names (positive + negative). For backward compatibility."""
79+
return self.positive_names + self.negative_names
80+
81+
@property
82+
def shorts(self) -> tuple[str, ...]:
83+
"""All short option names (positive + negative). For backward compatibility."""
84+
return self.positive_shorts + self.negative_shorts
85+
86+
@property
87+
def all_options(self) -> tuple[str, ...]:
88+
"""All options in display order: positive longs, positive shorts, negative longs, negative shorts."""
89+
return self.positive_names + self.positive_shorts + self.negative_names + self.negative_shorts
6990

7091
description: Any = None
7192
"""Help text description for this entry.
@@ -373,12 +394,12 @@ def help_append(text, style):
373394
if arg_name != options[0]:
374395
options = [arg_name, *options]
375396

376-
short_options, long_options = [], []
377-
for option in options:
378-
if _is_short(option):
379-
short_options.append(option)
380-
else:
381-
long_options.append(option)
397+
# Split options into positive/negative and long/short categories.
398+
negatives = set(argument.negatives)
399+
positive_names = [o for o in options if o not in negatives and not _is_short(o)]
400+
positive_shorts = [o for o in options if o not in negatives and _is_short(o)]
401+
negative_names = [o for o in options if o in negatives and not _is_short(o)]
402+
negative_shorts = [o for o in options if o in negatives and _is_short(o)]
382403

383404
help_description = InlineText.from_format(argument.parameter.help, format=format)
384405

@@ -428,9 +449,11 @@ def help_append(text, style):
428449

429450
# populate row
430451
entry = HelpEntry(
431-
names=tuple(long_options),
452+
positive_names=tuple(positive_names),
453+
positive_shorts=tuple(positive_shorts),
454+
negative_names=tuple(negative_names),
455+
negative_shorts=tuple(negative_shorts),
432456
description=help_description,
433-
shorts=tuple(short_options),
434457
required=argument.required,
435458
type=resolve_annotated(argument.field_info.annotation),
436459
choices=choices,
@@ -470,13 +493,14 @@ def format_command_entries(apps_with_names: Iterable, format: str) -> list[HelpE
470493
app = registered_command.app
471494
if not app.show:
472495
continue
496+
# Commands don't have negative variants, so all names are "positive"
473497
short_names, long_names = [], []
474498
for name in names:
475499
short_names.append(name) if _is_short(name) else long_names.append(name)
476500

477501
entry = HelpEntry(
478-
names=tuple(long_names),
479-
shorts=tuple(short_names),
502+
positive_names=tuple(long_names),
503+
positive_shorts=tuple(short_names),
480504
description=InlineText.from_format(docstring_parse(app.help, format).short_description, format=format),
481505
sort_key=resolve_callables(app.sort_key, app),
482506
)

cyclopts/help/specs.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,9 @@ def __call__(self, entry: "HelpEntry") -> "RenderableType":
5151
-------
5252
~rich.console.RenderableType
5353
Combined names and shorts, optionally wrapped.
54+
Order: positive_names, positive_shorts, negative_names, negative_shorts
5455
"""
55-
names_str = " ".join(entry.names) if entry.names else ""
56-
shorts_str = " ".join(entry.shorts) if entry.shorts else ""
57-
58-
if names_str and shorts_str:
59-
text = names_str + " " + shorts_str
60-
else:
61-
text = names_str or shorts_str
56+
text = " ".join(entry.all_options)
6257

6358
if self.max_width is None:
6459
return text
@@ -108,8 +103,8 @@ def __call__(self, entry: "HelpEntry") -> "RenderableType":
108103
~rich.console.RenderableType
109104
Primary command name with aliases in parentheses.
110105
"""
111-
primary = entry.names[0]
112-
aliases = list(entry.names[1:]) + list(entry.shorts)
106+
primary = entry.all_options[0] if entry.all_options else ""
107+
aliases = list(entry.all_options[1:])
113108

114109
if aliases:
115110
text = f"{primary} ({', '.join(aliases)})"

tests/__snapshots__/test_docs_snapshots/TestRstSnapshots.test_admin_commands_rst.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Complex CLI application for comprehensive documentation testing.
1818
``--verbose, -v``
1919
Verbosity level (-v, -vv, -vvv). [Default: ``0``]
2020

21-
``--quiet, --no-quiet, -q``
21+
``--quiet, -q, --no-quiet``
2222
Suppress non-essential output. [Default: ``False``]
2323

2424
``--log-level``
@@ -209,7 +209,7 @@ Delete a user.
209209

210210
**Parameters:**
211211

212-
``--force, --no-force, -f``
212+
``--force, -f, --no-force``
213213
Skip confirmation prompt. [Default: ``False``]
214214

215215
``--backup, --no-backup``

tests/__snapshots__/test_docs_snapshots/TestRstSnapshots.test_data_commands_rst.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Complex CLI application for comprehensive documentation testing.
1818
``--verbose, -v``
1919
Verbosity level (-v, -vv, -vvv). [Default: ``0``]
2020

21-
``--quiet, --no-quiet, -q``
21+
``--quiet, -q, --no-quiet``
2222
Suppress non-essential output. [Default: ``False``]
2323

2424
``--log-level``

tests/__snapshots__/test_docs_snapshots/TestRstSnapshots.test_flattened_commands_rst.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Complex CLI application for comprehensive documentation testing.
1818
``--verbose, -v``
1919
Verbosity level (-v, -vv, -vvv). [Default: ``0``]
2020

21-
``--quiet, --no-quiet, -q``
21+
``--quiet, -q, --no-quiet``
2222
Suppress non-essential output. [Default: ``False``]
2323

2424
``--log-level``
@@ -150,7 +150,7 @@ Delete a user.
150150

151151
**Parameters:**
152152

153-
``--force, --no-force, -f``
153+
``--force, -f, --no-force``
154154
Skip confirmation prompt. [Default: ``False``]
155155

156156
``--backup, --no-backup``

0 commit comments

Comments
 (0)