typer-extensions 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typer_extensions/__init__.py +9 -0
- typer_extensions/_version.py +3 -0
- typer_extensions/core.py +538 -0
- typer_extensions/format.py +104 -0
- typer_extensions/py.typed +0 -0
- typer_extensions-0.2.1.dist-info/METADATA +506 -0
- typer_extensions-0.2.1.dist-info/RECORD +9 -0
- typer_extensions-0.2.1.dist-info/WHEEL +4 -0
- typer_extensions-0.2.1.dist-info/licenses/LICENSE +19 -0
typer_extensions/core.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"""Core ExtendedTyper class extending typer.Typer with alias support"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Callable, Optional, Protocol, Union, cast
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import typer.main
|
|
8
|
+
from click import Command, Context, Group
|
|
9
|
+
from typer.core import TyperGroup
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HasName(Protocol):
|
|
13
|
+
"""Protocol for objects that have a name attribute"""
|
|
14
|
+
|
|
15
|
+
__name__: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AliasedGroup(TyperGroup):
|
|
19
|
+
"""Custom Click Group that handles command aliases"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*args: Any,
|
|
24
|
+
aliased_typer: Optional["ExtendedTyper"] = None,
|
|
25
|
+
**kwargs: Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialise the AliasedGroup
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
aliased_typer: Reference to the ExtendedTyper instance for alias resolution
|
|
31
|
+
*args, **kwargs: Arguments passed to Click Group
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
self.aliased_typer = aliased_typer
|
|
35
|
+
|
|
36
|
+
def get_command(self, ctx: Context, cmd_name: str) -> Optional[Command]:
|
|
37
|
+
"""Override Click's get_command to support aliases
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
ctx: The Click context
|
|
41
|
+
cmd_name: The name of the command
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The command if found, None otherwise
|
|
45
|
+
"""
|
|
46
|
+
if self.aliased_typer is None:
|
|
47
|
+
return super().get_command(ctx, cmd_name)
|
|
48
|
+
|
|
49
|
+
# Try to resolve as an active alias first
|
|
50
|
+
primary_cmd = self.aliased_typer._resolve_alias(cmd_name)
|
|
51
|
+
if primary_cmd is not None:
|
|
52
|
+
return super().get_command(ctx, primary_cmd)
|
|
53
|
+
|
|
54
|
+
cmd = super().get_command(ctx, cmd_name)
|
|
55
|
+
if cmd is not None:
|
|
56
|
+
return cmd
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def format_help(self, ctx: Context, formatter: Any) -> None:
|
|
61
|
+
"""Override TyperGroup's format_help to inject aliases into Rich output
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
ctx: The Click context
|
|
65
|
+
formatter: The Click HelpFormatter instance
|
|
66
|
+
"""
|
|
67
|
+
# Check if Rich formatting is enabled
|
|
68
|
+
if not hasattr(self, "rich_markup_mode") or self.rich_markup_mode is None:
|
|
69
|
+
return super().format_help(ctx, formatter)
|
|
70
|
+
|
|
71
|
+
from typer import rich_utils
|
|
72
|
+
|
|
73
|
+
# Store original _print_commands_panel
|
|
74
|
+
original_print = rich_utils._print_commands_panel
|
|
75
|
+
|
|
76
|
+
def custom_print_commands_panel(
|
|
77
|
+
*,
|
|
78
|
+
name: str,
|
|
79
|
+
commands: list[Command],
|
|
80
|
+
markup_mode: Any,
|
|
81
|
+
console: Any,
|
|
82
|
+
cmd_len: int,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Wrapper that modifies command names to include aliases
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: The name of the command group
|
|
88
|
+
commands: The list of commands in the group
|
|
89
|
+
markup_mode: The markup mode for the console output
|
|
90
|
+
console: The console instance for output
|
|
91
|
+
cmd_len: The length of the longest command name
|
|
92
|
+
"""
|
|
93
|
+
modified_commands = []
|
|
94
|
+
max_len = cmd_len
|
|
95
|
+
|
|
96
|
+
for command in commands:
|
|
97
|
+
if (
|
|
98
|
+
self.aliased_typer
|
|
99
|
+
and self.aliased_typer._show_aliases_in_help
|
|
100
|
+
and command.name in self.aliased_typer._command_aliases
|
|
101
|
+
):
|
|
102
|
+
from .format import format_command_with_aliases
|
|
103
|
+
|
|
104
|
+
formatted_name = format_command_with_aliases(
|
|
105
|
+
command.name,
|
|
106
|
+
self.aliased_typer._command_aliases[command.name],
|
|
107
|
+
display_format=self.aliased_typer._alias_display_format,
|
|
108
|
+
max_num=self.aliased_typer._max_num_aliases,
|
|
109
|
+
separator=self.aliased_typer._alias_separator,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Longest formatted name for correct column width
|
|
113
|
+
max_len = max(max_len, len(formatted_name))
|
|
114
|
+
|
|
115
|
+
# Temporary command object with the formatted name
|
|
116
|
+
cmd_copy = command
|
|
117
|
+
cmd_copy.name = formatted_name
|
|
118
|
+
|
|
119
|
+
modified_commands.append(cmd_copy)
|
|
120
|
+
else:
|
|
121
|
+
modified_commands.append(command)
|
|
122
|
+
|
|
123
|
+
# Call the original with modified commands & cmd_len
|
|
124
|
+
original_print(
|
|
125
|
+
name=name,
|
|
126
|
+
commands=modified_commands,
|
|
127
|
+
markup_mode=markup_mode,
|
|
128
|
+
console=console,
|
|
129
|
+
cmd_len=max_len,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Temporarily replace the function
|
|
133
|
+
rich_utils._print_commands_panel = custom_print_commands_panel # type: ignore[assignment]
|
|
134
|
+
try:
|
|
135
|
+
# Call parent's format_help with custom_print_commands_panel
|
|
136
|
+
super().format_help(ctx, formatter)
|
|
137
|
+
finally:
|
|
138
|
+
# Restore original function
|
|
139
|
+
rich_utils._print_commands_panel = original_print # type: ignore[assignment]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Store original function
|
|
143
|
+
_original_get_group_from_info = typer.main.get_group_from_info
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _aliased_get_group_from_info(
|
|
147
|
+
typer_info: "typer.main.TyperInfo",
|
|
148
|
+
**kwargs: Any,
|
|
149
|
+
) -> Union[AliasedGroup, TyperGroup]:
|
|
150
|
+
"""Custom version of get_group_from_info that returns AliasedGroup for ExtendedTyper instances
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
typer_info: The TyperInfo instance containing information about the Typer instance
|
|
154
|
+
**kwargs: Additional keyword arguments
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
An AliasedGroup if the Typer instance is an ExtendedTyper, otherwise a standard TyperGroup
|
|
158
|
+
"""
|
|
159
|
+
# Call original function to get standard TyperGroup
|
|
160
|
+
group = _original_get_group_from_info(typer_info, **kwargs)
|
|
161
|
+
|
|
162
|
+
# If Typer instance is ExtendedTyper, wrap it in an AliasedGroup
|
|
163
|
+
if isinstance(typer_info.typer_instance, ExtendedTyper):
|
|
164
|
+
aliased_typer = typer_info.typer_instance
|
|
165
|
+
aliased_group = AliasedGroup(
|
|
166
|
+
name=group.name,
|
|
167
|
+
callback=group.callback,
|
|
168
|
+
params=group.params,
|
|
169
|
+
help=group.help,
|
|
170
|
+
epilog=group.epilog,
|
|
171
|
+
short_help=group.short_help,
|
|
172
|
+
options_metavar=group.options_metavar,
|
|
173
|
+
subcommand_metavar=group.subcommand_metavar,
|
|
174
|
+
chain=group.chain,
|
|
175
|
+
result_callback=group.result_callback,
|
|
176
|
+
context_settings=group.context_settings,
|
|
177
|
+
aliased_typer=aliased_typer,
|
|
178
|
+
rich_markup_mode=group.rich_markup_mode,
|
|
179
|
+
rich_help_panel=group.rich_help_panel,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
for name, cmd in group.commands.items():
|
|
183
|
+
aliased_group.add_command(cmd, name=name)
|
|
184
|
+
|
|
185
|
+
return aliased_group
|
|
186
|
+
|
|
187
|
+
# Standard TyperGroup
|
|
188
|
+
return group
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Apply monkey-patch to Typer's group creation function
|
|
192
|
+
typer.main.get_group_from_info = _aliased_get_group_from_info # type: ignore[assignment]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ExtendedTyper(typer.Typer):
|
|
196
|
+
"""Typer application with alias support"""
|
|
197
|
+
|
|
198
|
+
# Expose Typer's Argument and Option
|
|
199
|
+
Argument = staticmethod(typer.Argument)
|
|
200
|
+
Option = staticmethod(typer.Option)
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
*args: Any,
|
|
205
|
+
alias_case_sensitive: Optional[bool] = None,
|
|
206
|
+
show_aliases_in_help: bool = True,
|
|
207
|
+
alias_display_format: str = "({aliases})",
|
|
208
|
+
alias_separator: str = ", ",
|
|
209
|
+
max_num_aliases: int = 3,
|
|
210
|
+
**kwargs: Any,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Initialise ExtendedTyper
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
*args: Positional arguments for Typer
|
|
216
|
+
alias_case_sensitive: Whether aliases are case sensitive. If None (default), will match
|
|
217
|
+
Typer's case_sensitive setting from context_settings (defaults to True)
|
|
218
|
+
show_aliases_in_help: Whether to show aliases in help
|
|
219
|
+
alias_display_format: Format string for displaying aliases in help
|
|
220
|
+
Must include the placeholder '{aliases}'
|
|
221
|
+
alias_separator: Separator for displaying aliases in help (default: ', ')
|
|
222
|
+
max_num_aliases: Maximum number of aliases to display before truncating with '+ N more'
|
|
223
|
+
**kwargs: Keyword arguments for Typer
|
|
224
|
+
"""
|
|
225
|
+
kwargs.setdefault("rich_markup_mode", "rich")
|
|
226
|
+
kwargs.setdefault("rich_help_panel", True)
|
|
227
|
+
super().__init__(*args, **kwargs)
|
|
228
|
+
|
|
229
|
+
# Sync with Typer's case_sensitive setting if not explicitly set
|
|
230
|
+
if alias_case_sensitive is None:
|
|
231
|
+
context_settings = kwargs.get("context_settings") or {}
|
|
232
|
+
typer_case_sensitive = context_settings.get("case_sensitive", True)
|
|
233
|
+
self._alias_case_sensitive = typer_case_sensitive
|
|
234
|
+
else:
|
|
235
|
+
self._alias_case_sensitive = alias_case_sensitive
|
|
236
|
+
|
|
237
|
+
self._show_aliases_in_help = show_aliases_in_help
|
|
238
|
+
self._alias_display_format = alias_display_format
|
|
239
|
+
self._alias_separator = alias_separator
|
|
240
|
+
self._max_num_aliases = max_num_aliases
|
|
241
|
+
|
|
242
|
+
# Mapping of command names to aliases (O(1) lookup)
|
|
243
|
+
self._command_aliases: dict[str, list[str]] = {}
|
|
244
|
+
self._alias_to_command: dict[str, str] = {}
|
|
245
|
+
|
|
246
|
+
def _normalise_name(self, name: str) -> str:
|
|
247
|
+
"""Normalise command/alias name based on case sensitivity
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
name: The command/alias name to normalise
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The normalised command/alias name (lowercase if case insensitive)
|
|
254
|
+
"""
|
|
255
|
+
return name.lower() if not self._alias_case_sensitive else name
|
|
256
|
+
|
|
257
|
+
def _register_alias(self, command_name: str, alias: str) -> None:
|
|
258
|
+
"""Register an alias for a command
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
command_name: The name of the command
|
|
262
|
+
alias: The alias to register
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
ValueError: If the alias conflicts with an existing command/alias
|
|
266
|
+
"""
|
|
267
|
+
if not alias or not isinstance(alias, str):
|
|
268
|
+
raise ValueError("Alias must be a non-empty string")
|
|
269
|
+
|
|
270
|
+
if any(c.isspace() for c in alias):
|
|
271
|
+
raise ValueError("Alias cannot contain whitespace")
|
|
272
|
+
|
|
273
|
+
if not re.match(r"^[\w\-]+$", alias, re.UNICODE):
|
|
274
|
+
raise ValueError(
|
|
275
|
+
"Alias must only contain alphanumeric characters, dashes, and underscores (Unicode allowed)"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
normalised_alias = self._normalise_name(alias)
|
|
279
|
+
normalised_cmd = self._normalise_name(command_name)
|
|
280
|
+
|
|
281
|
+
# Check if alias is the same as command name
|
|
282
|
+
if normalised_alias == normalised_cmd:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"Alias '{alias}' cannot be the same as command name '{command_name}'"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Check if alias is already registered
|
|
288
|
+
if normalised_alias in self._alias_to_command:
|
|
289
|
+
existing_cmd = self._alias_to_command[normalised_alias]
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Alias '{alias}' is already registered for command '{existing_cmd}'"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Register the alias
|
|
295
|
+
self._alias_to_command[normalised_alias] = command_name
|
|
296
|
+
|
|
297
|
+
# Add alias to command mapping
|
|
298
|
+
if command_name not in self._command_aliases:
|
|
299
|
+
self._command_aliases[command_name] = []
|
|
300
|
+
self._command_aliases[command_name].append(alias)
|
|
301
|
+
|
|
302
|
+
def _register_command_with_aliases(
|
|
303
|
+
self,
|
|
304
|
+
func: Callable[..., Any],
|
|
305
|
+
name: str,
|
|
306
|
+
aliases: Optional[list[str]] = None,
|
|
307
|
+
**kwargs: Any,
|
|
308
|
+
) -> Command:
|
|
309
|
+
"""Register a command with aliases
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
func: The command function
|
|
313
|
+
name: The command name
|
|
314
|
+
aliases: List of aliases for the command
|
|
315
|
+
**kwargs: Additional keyword arguments for command registration
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
The registered Click Command object
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
ValueError: If any aliases conflict with existing commands/aliases
|
|
322
|
+
"""
|
|
323
|
+
aliases = aliases or []
|
|
324
|
+
|
|
325
|
+
self.command(name, **kwargs)(func)
|
|
326
|
+
|
|
327
|
+
for alias in aliases:
|
|
328
|
+
self._register_alias(name, alias)
|
|
329
|
+
|
|
330
|
+
click_obj = typer.main.get_command(self)
|
|
331
|
+
if isinstance(click_obj, Group):
|
|
332
|
+
command = click_obj.commands[name]
|
|
333
|
+
else:
|
|
334
|
+
command = click_obj
|
|
335
|
+
|
|
336
|
+
return command
|
|
337
|
+
|
|
338
|
+
def _resolve_alias(self, name: str) -> Optional[str]:
|
|
339
|
+
"""Resolve a command/alias name to its primary command name
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
name: The command/alias name to resolve
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
The primary command name if found, or None if not found or removed
|
|
346
|
+
"""
|
|
347
|
+
normalised_name = self._normalise_name(name)
|
|
348
|
+
|
|
349
|
+
return self._alias_to_command.get(normalised_name)
|
|
350
|
+
|
|
351
|
+
def get_command(self, ctx: Context, cmd_name: str) -> Optional[Command]:
|
|
352
|
+
"""Programmatically retrieve a command by its name/alias
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
ctx: The context in which the command is being invoked
|
|
356
|
+
cmd_name: The name or alias of the command to retrieve
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
The command if found, else None
|
|
360
|
+
"""
|
|
361
|
+
if not hasattr(self, "_group") or self._group is None:
|
|
362
|
+
if not hasattr(self, "_command") or self._command is None:
|
|
363
|
+
# Trigger CLI build
|
|
364
|
+
click_obj = typer.main.get_command(self)
|
|
365
|
+
|
|
366
|
+
if hasattr(click_obj, "commands"):
|
|
367
|
+
self._group = click_obj
|
|
368
|
+
else:
|
|
369
|
+
self._command = click_obj
|
|
370
|
+
|
|
371
|
+
primary_cmd = self._resolve_alias(cmd_name)
|
|
372
|
+
effective_name = primary_cmd if primary_cmd is not None else cmd_name
|
|
373
|
+
|
|
374
|
+
# Single command apps
|
|
375
|
+
if hasattr(self, "_command"):
|
|
376
|
+
command = self._command
|
|
377
|
+
if command.name == effective_name:
|
|
378
|
+
return command
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
# Multi-command apps
|
|
382
|
+
group = cast(Group, self._group)
|
|
383
|
+
if effective_name in group.commands:
|
|
384
|
+
return group.commands[effective_name]
|
|
385
|
+
return group.get_command(ctx, effective_name)
|
|
386
|
+
|
|
387
|
+
def command_with_aliases(
|
|
388
|
+
self,
|
|
389
|
+
name: Optional[Union[str, Callable[..., Any]]] = None,
|
|
390
|
+
*,
|
|
391
|
+
aliases: Optional[list[str]] = None,
|
|
392
|
+
**kwargs: Any,
|
|
393
|
+
) -> Callable[[Callable[..., Any]], Command]:
|
|
394
|
+
"""Decorator to register a command with aliases, similar to Typer's @app.command decorator
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
name: The name of the command - if not provided, inferred from the function name
|
|
398
|
+
aliases: A list of aliases for the command
|
|
399
|
+
**kwargs: Additional keyword arguments for command registration
|
|
400
|
+
(e.g. help, hidden, deprecated, context_settings, etc.)
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
A decorator that registers the command with the specified name and aliases
|
|
404
|
+
"""
|
|
405
|
+
# Decorator used without parentheses (name inferred)
|
|
406
|
+
if callable(name):
|
|
407
|
+
func = name
|
|
408
|
+
|
|
409
|
+
return self._register_command_with_aliases(
|
|
410
|
+
func, name=cast(HasName, func).__name__, aliases=None, **kwargs
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Standard decorator with parentheses
|
|
414
|
+
def decorator(func: Callable[..., Any]) -> Command:
|
|
415
|
+
"""Decorator to register a command with aliases
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
func: The command function
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
The registered Click Command object
|
|
422
|
+
"""
|
|
423
|
+
if isinstance(name, str) and name:
|
|
424
|
+
command_name = name
|
|
425
|
+
else:
|
|
426
|
+
command_name = cast(HasName, func).__name__
|
|
427
|
+
|
|
428
|
+
return self._register_command_with_aliases(
|
|
429
|
+
func, name=command_name, aliases=aliases, **kwargs
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
return decorator
|
|
433
|
+
|
|
434
|
+
def add_aliased_command(
|
|
435
|
+
self,
|
|
436
|
+
func: Callable[..., Any],
|
|
437
|
+
name: Optional[str] = None,
|
|
438
|
+
aliases: Optional[list[str]] = None,
|
|
439
|
+
**kwargs: Any,
|
|
440
|
+
) -> Command:
|
|
441
|
+
"""Programmatically register a command with aliases
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
func: The command function
|
|
445
|
+
name: The name of the command - if not provided, inferred from the function name
|
|
446
|
+
aliases: A list of aliases for the command
|
|
447
|
+
**kwargs: Additional keyword arguments for command registration
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
The registered Click Command object
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
ValueError: If any alias conflicts with existing commands/aliases
|
|
454
|
+
"""
|
|
455
|
+
if isinstance(name, str) and name:
|
|
456
|
+
command_name = name
|
|
457
|
+
else:
|
|
458
|
+
command_name = cast(HasName, func).__name__
|
|
459
|
+
|
|
460
|
+
return self._register_command_with_aliases(
|
|
461
|
+
func, name=command_name, aliases=aliases, **kwargs
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def add_alias(self, command_name: str, alias: str) -> None:
|
|
465
|
+
"""Programmatically add an alias to an existing command
|
|
466
|
+
|
|
467
|
+
This does not allow adding aliases to single-command applications, in line with Typer's design principle of treating single-commands apps as the default command, making aliases redundant
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
command_name: The name of the existing command
|
|
471
|
+
alias: The alias to add
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
ValueError: If the command doesn't exist, is a single-command app, or the alias conflicts with existing commands/aliases
|
|
475
|
+
"""
|
|
476
|
+
# Get the underlying Click group
|
|
477
|
+
click_obj = typer.main.get_command(self)
|
|
478
|
+
|
|
479
|
+
if not isinstance(click_obj, Group):
|
|
480
|
+
raise ValueError("Cannot add aliases to single-command applications")
|
|
481
|
+
|
|
482
|
+
existing_command = click_obj.get_command(Context(click_obj), command_name)
|
|
483
|
+
if existing_command is None:
|
|
484
|
+
raise ValueError(f"Command '{command_name}' does not exist")
|
|
485
|
+
|
|
486
|
+
self._register_alias(command_name, alias)
|
|
487
|
+
|
|
488
|
+
def remove_alias(self, alias: str) -> bool:
|
|
489
|
+
"""Programmatically remove an alias from an existing command
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
alias: The alias to remove
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
True if the alias was removed, False if it doesn't exist
|
|
496
|
+
"""
|
|
497
|
+
normalised_alias = self._normalise_name(alias)
|
|
498
|
+
|
|
499
|
+
if normalised_alias not in self._alias_to_command:
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
primary_name = self._alias_to_command[normalised_alias]
|
|
503
|
+
del self._alias_to_command[normalised_alias]
|
|
504
|
+
|
|
505
|
+
if primary_name in self._command_aliases:
|
|
506
|
+
try:
|
|
507
|
+
self._command_aliases[primary_name].remove(alias)
|
|
508
|
+
|
|
509
|
+
if not self._command_aliases[primary_name]:
|
|
510
|
+
del self._command_aliases[primary_name]
|
|
511
|
+
|
|
512
|
+
except ValueError:
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
return True
|
|
516
|
+
|
|
517
|
+
def get_aliases(self, command_name: str) -> list[str]:
|
|
518
|
+
"""Retrieve the list of aliases for a given command
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
command_name: The name of the command
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
A list of aliases for the command, or an empty list if no aliases or command doesn't exist
|
|
525
|
+
"""
|
|
526
|
+
if command_name in self._command_aliases:
|
|
527
|
+
return self._command_aliases[command_name].copy()
|
|
528
|
+
return []
|
|
529
|
+
|
|
530
|
+
def list_commands_with_aliases(self) -> dict[str, list[str]]:
|
|
531
|
+
"""List all commands and their aliases
|
|
532
|
+
|
|
533
|
+
Returns a dictionary mapping command names to their aliases - only includes commands with aliases and returns a copy, so modifications won't affect the original
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
A dictionary mapping command names to their aliases, or an empty dictionary if no commands have aliases
|
|
537
|
+
"""
|
|
538
|
+
return {cmd: aliases.copy() for cmd, aliases in self._command_aliases.items()}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Help text formatting utilities"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def truncate_aliases(
|
|
7
|
+
aliases: list[str],
|
|
8
|
+
max_num: int,
|
|
9
|
+
separator: str = ", ",
|
|
10
|
+
) -> str:
|
|
11
|
+
"""Truncate the list of aliases to a maximum number, join with a separator, and truncate with '+ N more' if needed
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
aliases: The list of aliases to truncate
|
|
15
|
+
max_num: The maximum number of aliases to display
|
|
16
|
+
separator: The separator to use when joining aliases, defaults to ', '
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The formatted string of aliases, truncated if necessary
|
|
20
|
+
"""
|
|
21
|
+
if not aliases:
|
|
22
|
+
return ""
|
|
23
|
+
|
|
24
|
+
# Handle negative max_num edge case
|
|
25
|
+
if max_num < 0:
|
|
26
|
+
max_num = 0
|
|
27
|
+
|
|
28
|
+
if len(aliases) <= max_num:
|
|
29
|
+
return separator.join(aliases)
|
|
30
|
+
|
|
31
|
+
visible = aliases[:max_num]
|
|
32
|
+
hidden = len(aliases) - max_num
|
|
33
|
+
|
|
34
|
+
return f"{separator.join(visible)}{separator if visible else ''}+{hidden} more"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_command_with_aliases(
|
|
38
|
+
command_name: str,
|
|
39
|
+
aliases: list[str],
|
|
40
|
+
*,
|
|
41
|
+
display_format: str = "({aliases})",
|
|
42
|
+
max_num: int = 3,
|
|
43
|
+
separator: str = ", ",
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Format a command with its aliases with specified format
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
command_name: The name of the command
|
|
49
|
+
aliases: The list of aliases for the command
|
|
50
|
+
display_format: The format string for displaying aliases
|
|
51
|
+
max_num: The maximum number of aliases to display
|
|
52
|
+
separator: The separator to use between aliases
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The formatted command string with aliases
|
|
56
|
+
"""
|
|
57
|
+
if not aliases:
|
|
58
|
+
return command_name
|
|
59
|
+
|
|
60
|
+
aliases_str = truncate_aliases(aliases, max_num, separator)
|
|
61
|
+
|
|
62
|
+
aliases_display = display_format.format(aliases=aliases_str)
|
|
63
|
+
|
|
64
|
+
return f"{command_name} {aliases_display}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def format_commands_section(
|
|
68
|
+
commands: list[tuple[str, Optional[str]]],
|
|
69
|
+
command_aliases: dict[str, list[str]],
|
|
70
|
+
*,
|
|
71
|
+
display_format: str = "({aliases})",
|
|
72
|
+
max_num: int = 3,
|
|
73
|
+
separator: str = ", ",
|
|
74
|
+
) -> list[tuple[str, Optional[str]]]:
|
|
75
|
+
"""Format a list of commands with their aliases
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
commands: The list of commands to format
|
|
79
|
+
command_aliases: A dictionary mapping command names to their aliases
|
|
80
|
+
display_format: The format string for displaying aliases
|
|
81
|
+
max_num: The maximum number of aliases to display
|
|
82
|
+
separator: The separator to use between aliases
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A list of (formatted_command, help text) tuples
|
|
86
|
+
"""
|
|
87
|
+
formatted_commands = []
|
|
88
|
+
|
|
89
|
+
for cmd_name, help_text in commands:
|
|
90
|
+
if cmd_name in command_aliases:
|
|
91
|
+
aliases = command_aliases[cmd_name]
|
|
92
|
+
formatted_cmd = format_command_with_aliases(
|
|
93
|
+
cmd_name,
|
|
94
|
+
aliases,
|
|
95
|
+
display_format=display_format,
|
|
96
|
+
max_num=max_num,
|
|
97
|
+
separator=separator,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
formatted_cmd = cmd_name
|
|
101
|
+
|
|
102
|
+
formatted_commands.append((formatted_cmd, help_text))
|
|
103
|
+
|
|
104
|
+
return formatted_commands
|
|
File without changes
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typer-extensions
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Command aliases for Typer CLI applications
|
|
5
|
+
Project-URL: Homepage, https://github.com/rdawebb/typer-extensions
|
|
6
|
+
Project-URL: Documentation, https://github.com/rdawebb/typer-extensions#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/rdawebb/typer-extensions
|
|
8
|
+
Project-URL: Changelog, https://github.com/rdawebb/typer-extensions/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/rdawebb/typer-extensions/issues
|
|
10
|
+
Author: Rob Webb
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: aliases,cli,click,command-line,typer
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: System :: Shells
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: click<9.0.0,>=8.0.0
|
|
29
|
+
Requires-Dist: typer>=0.9.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: prek<1.0.0,>=0.2.27; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov<8.0.0,>=7.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest<10.0.0,>=8.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff<15.0.0,>=0.14.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: rust-just<2.0.0,>=1.46.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ty<0.1.0,>=0.0.10; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# typer-extensions
|
|
40
|
+
|
|
41
|
+
**Command aliases for Typer CLI applications with grouped help text display**
|
|
42
|
+
|
|
43
|
+
[](https://github.com/rdawebb/typer-extensions/actions)
|
|
44
|
+
[](https://pypi.org/project/typer-extensions/)
|
|
45
|
+
[](https://pypi.org/project/typer-extensions/)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Overview
|
|
51
|
+
|
|
52
|
+
`typer-extensions` extends [Typer](https://typer.tiangolo.com/) to provide simple drop-in support for command aliases. Instead of duplicating commands, hiding commands, or maintaining wrapper functions, define aliases directly and have them displayed cleanly in help text.
|
|
53
|
+
|
|
54
|
+
100% backwards compatible with Typer & existing Typer apps!
|
|
55
|
+
|
|
56
|
+
### The Problem
|
|
57
|
+
|
|
58
|
+
Standard Typer requires duplicating or hiding commands to create aliases:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from typer import Typer
|
|
62
|
+
|
|
63
|
+
app = Typer()
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def list_items():
|
|
67
|
+
"""List all items."""
|
|
68
|
+
print("Listing...")
|
|
69
|
+
|
|
70
|
+
# Registered as separate command, hidden in help
|
|
71
|
+
@app.command("ls", hidden=True)
|
|
72
|
+
def ls_items():
|
|
73
|
+
list_items()
|
|
74
|
+
|
|
75
|
+
@app.command("l")("list") # Duplicate!
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Help output:**
|
|
79
|
+
```
|
|
80
|
+
Commands:
|
|
81
|
+
list List all items.
|
|
82
|
+
l List all items.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Issues:**
|
|
86
|
+
- Code duplication
|
|
87
|
+
- Help text shows commands & aliases separately
|
|
88
|
+
- Hidden 'aliases' not shown in help text
|
|
89
|
+
- Maintenance burden
|
|
90
|
+
|
|
91
|
+
### The Solution
|
|
92
|
+
|
|
93
|
+
With `typer-extensions`:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from typer_extensions import ExtendedTyper
|
|
97
|
+
|
|
98
|
+
app = ExtendedTyper()
|
|
99
|
+
|
|
100
|
+
@app.command_with_aliases("list", aliases=["ls", "l"])
|
|
101
|
+
def list_items():
|
|
102
|
+
"""List all items."""
|
|
103
|
+
print("Listing...")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Help output:**
|
|
107
|
+
```
|
|
108
|
+
Commands:
|
|
109
|
+
list (ls, l) List all items.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**All work identically:**
|
|
113
|
+
- `app list`
|
|
114
|
+
- `app ls`
|
|
115
|
+
- `app l`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Features
|
|
120
|
+
|
|
121
|
+
✨ **Decorator-based alias registration** - Clean, intuitive syntax
|
|
122
|
+
|
|
123
|
+
🔧 **Programmatic API** - Dynamic alias management at runtime
|
|
124
|
+
|
|
125
|
+
📋 **Grouped help display** - Aliases shown with their primary command
|
|
126
|
+
|
|
127
|
+
⚙️ **Highly configurable** - Customise format, separators, truncation
|
|
128
|
+
|
|
129
|
+
🎯 **Type-safe** - Full type hints and editor support
|
|
130
|
+
|
|
131
|
+
🔄 **Fully backwards compatible** - Works with all Typer/Click features and existing Typer apps
|
|
132
|
+
|
|
133
|
+
✅ **Shell completion ready** - Alias support doesn't interfere with existing shell completion
|
|
134
|
+
|
|
135
|
+
🧪 **Well-tested** - Tested on Python 3.9-3.14 with 100% test coverage
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Installation
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install typer-extensions
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Requirements:**
|
|
146
|
+
- Python 3.9+
|
|
147
|
+
- typer >= 0.9.0 (recommend installing the latest version)
|
|
148
|
+
- click >= 8.0.0
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Quick Start
|
|
153
|
+
|
|
154
|
+
### Basic Usage
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from typer_extensions import ExtendedTyper
|
|
158
|
+
|
|
159
|
+
app = ExtendedTyper()
|
|
160
|
+
|
|
161
|
+
@app.command_with_aliases("list", aliases=["ls", "l"])
|
|
162
|
+
def list_items():
|
|
163
|
+
"""List all items."""
|
|
164
|
+
print("Listing items...")
|
|
165
|
+
|
|
166
|
+
@app.command_with_aliases("delete", aliases=["rm", "remove"])
|
|
167
|
+
def delete_item(name: str):
|
|
168
|
+
"""Delete an item."""
|
|
169
|
+
print(f"Deleting {name}")
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
app()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Run it:**
|
|
176
|
+
```bash
|
|
177
|
+
$ python app.py --help
|
|
178
|
+
Commands:
|
|
179
|
+
list (ls, l) List all items.
|
|
180
|
+
delete (rm, remove) Delete an item.
|
|
181
|
+
|
|
182
|
+
$ python app.py ls
|
|
183
|
+
Listing items...
|
|
184
|
+
|
|
185
|
+
$ python app.py rm test.txt
|
|
186
|
+
Deleting test.txt
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
> 💡 **Want to learn more?** Check the [API Reference](docs/API_REFERENCE.md) for detailed configuration options, or see the [User Guide](docs/USER_GUIDE.md) for patterns and best practices.
|
|
190
|
+
|
|
191
|
+
### Drop-in Compatibility
|
|
192
|
+
|
|
193
|
+
Have an existing Typer project? Add alias support without changing your code:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# Before (regular Typer)
|
|
197
|
+
from typer import Typer
|
|
198
|
+
app = Typer()
|
|
199
|
+
|
|
200
|
+
# After (with typer-extensions)
|
|
201
|
+
from typer_extensions import ExtendedTyper
|
|
202
|
+
app = ExtendedTyper()
|
|
203
|
+
|
|
204
|
+
# That's it! Everything else stays the same.
|
|
205
|
+
# Existing commands, shell completion and help text configuration still work exactly as before.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Once migrated, you can add aliases to new commands using `@app.command_with_aliases()`, or to existing commands using the programmatic API.
|
|
209
|
+
|
|
210
|
+
See [Migration Guide](docs/MIGRATION.md) for detailed migration strategies and patterns.
|
|
211
|
+
|
|
212
|
+
### Advanced Features
|
|
213
|
+
|
|
214
|
+
**Programmatic alias management:**
|
|
215
|
+
```python
|
|
216
|
+
# Add aliases dynamically
|
|
217
|
+
app.add_alias("list", "dir")
|
|
218
|
+
|
|
219
|
+
# Remove aliases
|
|
220
|
+
app.remove_alias("dir")
|
|
221
|
+
|
|
222
|
+
# Query aliases
|
|
223
|
+
aliases = app.get_aliases("list") # ["ls", "l"]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Custom help formatting:**
|
|
227
|
+
```python
|
|
228
|
+
app = ExtendedTyper(
|
|
229
|
+
alias_display_format="[{aliases}]", # Use brackets
|
|
230
|
+
alias_separator=" | ", # Pipe separator
|
|
231
|
+
max_aliases_inline=2, # Show max 2, then "+N more"
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Configuration-based aliases:**
|
|
236
|
+
```python
|
|
237
|
+
# Load aliases from config
|
|
238
|
+
config = {"list": ["ls", "l", "dir"]}
|
|
239
|
+
for cmd, aliases in config.items():
|
|
240
|
+
for alias in aliases:
|
|
241
|
+
app.add_alias(cmd, alias)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Documentation
|
|
247
|
+
|
|
248
|
+
📚 **[User Guide](docs/USER_GUIDE.md)** - Tutorials and common patterns
|
|
249
|
+
|
|
250
|
+
📖 **[API Reference](docs/API_REFERENCE.md)** - Complete API documentation
|
|
251
|
+
|
|
252
|
+
🔄 **[Migration Guide](docs/MIGRATION.md)** - Migrating from standard Typer
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Examples
|
|
257
|
+
|
|
258
|
+
All examples are in the [`examples/`](examples/) directory:
|
|
259
|
+
|
|
260
|
+
- **[basic_usage.py](examples/basic_usage.py)** - Simple CLI with aliases
|
|
261
|
+
- **[advanced_usage.py](examples/advanced_usage.py)** - Git-like CLI with options
|
|
262
|
+
- **[programmatic_usage.py](examples/programmatic_usage.py)** - Dynamic alias management
|
|
263
|
+
- **[help_formatting.py](examples/help_formatting.py)** - Customising help display
|
|
264
|
+
- **[argument_option_usage.py](examples/argument_option_usage.py)** - Using Typer's Argument & Option
|
|
265
|
+
|
|
266
|
+
**Run any example:**
|
|
267
|
+
```bash
|
|
268
|
+
python examples/basic_usage.py --help
|
|
269
|
+
python examples/basic_usage.py ls
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Real-World Use Cases
|
|
275
|
+
|
|
276
|
+
### Git-like CLI
|
|
277
|
+
```python
|
|
278
|
+
@app.command_with_aliases("checkout", aliases=["co"])
|
|
279
|
+
def checkout(branch: str):
|
|
280
|
+
"""Switch branches."""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
@app.command_with_aliases("status", aliases=["st"])
|
|
284
|
+
def status():
|
|
285
|
+
"""Show status."""
|
|
286
|
+
...
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Package Manager
|
|
290
|
+
```python
|
|
291
|
+
@app.command_with_aliases("install", aliases=["i", "add"])
|
|
292
|
+
def install(package: str):
|
|
293
|
+
"""Install a package."""
|
|
294
|
+
...
|
|
295
|
+
|
|
296
|
+
@app.command_with_aliases("remove", aliases=["rm", "uninstall"])
|
|
297
|
+
def remove(package: str):
|
|
298
|
+
"""Remove a package."""
|
|
299
|
+
...
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Cross-Platform Commands
|
|
303
|
+
```python
|
|
304
|
+
@app.command("list")
|
|
305
|
+
def list_files():
|
|
306
|
+
"""List files."""
|
|
307
|
+
...
|
|
308
|
+
|
|
309
|
+
# Add platform-specific aliases
|
|
310
|
+
if platform.system() == "Windows":
|
|
311
|
+
app.add_alias("list", "dir")
|
|
312
|
+
else:
|
|
313
|
+
app.add_alias("list", "ls")
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## API Overview
|
|
319
|
+
|
|
320
|
+
### Decorator Registration
|
|
321
|
+
```python
|
|
322
|
+
@app.command_with_aliases(name, aliases=[...])
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Programmatic Registration
|
|
326
|
+
```python
|
|
327
|
+
app.add_aliased_command(func, name, aliases=[...])
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Alias Management
|
|
331
|
+
```python
|
|
332
|
+
app.add_alias(command, alias) # Add alias
|
|
333
|
+
app.remove_alias(alias) → bool # Remove alias
|
|
334
|
+
app.get_aliases(command) → list # Query aliases
|
|
335
|
+
app.list_commands_with_aliases() → dict # All mappings
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Configuration
|
|
339
|
+
```python
|
|
340
|
+
ExtendedTyper(
|
|
341
|
+
alias_case_sensitive=None, # Case-sensitive matching (default True per Typer)
|
|
342
|
+
show_aliases_in_help=True, # Display aliases in help
|
|
343
|
+
alias_display_format="({aliases})", # Display format
|
|
344
|
+
alias_separator=", ", # Between aliases
|
|
345
|
+
max_num_aliases=3, # Before truncation
|
|
346
|
+
)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Development
|
|
352
|
+
|
|
353
|
+
### Setup
|
|
354
|
+
|
|
355
|
+
**Standard setup with pip:**
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
git clone https://github.com/rdawebb/typer-extensions.git
|
|
359
|
+
cd typer-extensions
|
|
360
|
+
python -m venv venv
|
|
361
|
+
source venv/bin/activate # or venv\Scripts\activate on Windows
|
|
362
|
+
pip install -e ".[dev]"
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Or with uv (recommended):**
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
git clone https://github.com/rdawebb/typer-extensions.git
|
|
369
|
+
cd typer-extensions
|
|
370
|
+
uv sync --all-extras
|
|
371
|
+
source .venv/bin/activate
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Quick commands with justfile:**
|
|
375
|
+
|
|
376
|
+
If you have [just](https://github.com/casey/just) installed (included in `[dev]`), common tasks are available:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
just test # Run tests
|
|
380
|
+
just lint # Run linter
|
|
381
|
+
just format # Format code
|
|
382
|
+
just type # Check type safety
|
|
383
|
+
just test-cov # Full test suite with coverage
|
|
384
|
+
just help # List all available commands
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
If you prefer to use `just` without the dev setup, you can install it globally via `pip install rust-just` or your system package manager.
|
|
388
|
+
|
|
389
|
+
See `Justfile` for all available commands.
|
|
390
|
+
|
|
391
|
+
### Testing
|
|
392
|
+
|
|
393
|
+
**With justfile (recommended):**
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
just check # Run linting, formatting, and type checks
|
|
397
|
+
just test # Run tests
|
|
398
|
+
just test-cov # Run tests with coverage report
|
|
399
|
+
just pre # Full pre-commit check (all checks + tests)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Or with direct commands:**
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
pytest # Run all tests
|
|
406
|
+
pytest --cov # With coverage
|
|
407
|
+
pytest -v # Verbose output
|
|
408
|
+
ruff check . # Lint
|
|
409
|
+
ruff format . # Format
|
|
410
|
+
ty check src/ # Type check
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Contributing
|
|
414
|
+
|
|
415
|
+
Contributions are welcome! Please open an issue, ask a question, or submit a pull request.
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Project Status
|
|
420
|
+
|
|
421
|
+
**Current Version:** 0.2.1 (Beta)
|
|
422
|
+
|
|
423
|
+
✅ **Core Features Complete:**
|
|
424
|
+
- Alias registration (decorator + programmatic)
|
|
425
|
+
- Help text formatting
|
|
426
|
+
- Dynamic alias management
|
|
427
|
+
- Full test coverage
|
|
428
|
+
|
|
429
|
+
🚧 **In Development:**
|
|
430
|
+
- Dynamic config file loading with import/export
|
|
431
|
+
- Shell completion enhancement and typo suggestions
|
|
432
|
+
- Documentation site
|
|
433
|
+
- Performance optimisations
|
|
434
|
+
|
|
435
|
+
📋 **Planned Features:**
|
|
436
|
+
- Shared & chained subcommand aliases
|
|
437
|
+
- Per-alias help text
|
|
438
|
+
- Dataclass, Pydantic & Attrs support
|
|
439
|
+
- Custom themes and help text formatting
|
|
440
|
+
- Argument & Option customisation
|
|
441
|
+
|
|
442
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Why typer-extensions?
|
|
447
|
+
|
|
448
|
+
**For Users:**
|
|
449
|
+
- ⚡ Faster workflows with short aliases
|
|
450
|
+
- 🎯 Familiar commands (git-like shortcuts)
|
|
451
|
+
- 📖 Clear help text showing all options
|
|
452
|
+
|
|
453
|
+
**For Developers:**
|
|
454
|
+
- 🧹 DRY - no code duplication
|
|
455
|
+
- 🔧 Flexible - static or dynamic aliases
|
|
456
|
+
- 🎨 Customisable - match your style
|
|
457
|
+
- ✅ Tested - reliable, stable, and type-safe
|
|
458
|
+
|
|
459
|
+
**Compared to Alternatives:**
|
|
460
|
+
- **Plain Typer:** Requires command duplication or hiding
|
|
461
|
+
- **click-aliases:** Click-specific, no Typer integration
|
|
462
|
+
- **Custom solutions:** Reinventing the wheel
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Related Projects
|
|
467
|
+
|
|
468
|
+
- **[Typer](https://typer.tiangolo.com/)** - The CLI framework this extends
|
|
469
|
+
- **[Click](https://click.palletsprojects.com/)** - The underlying library
|
|
470
|
+
- **[Rich](https://rich.readthedocs.io/)** - Beautiful terminal formatting (used by Typer)
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## License
|
|
475
|
+
|
|
476
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Acknowledgments
|
|
481
|
+
|
|
482
|
+
Built on the excellent [Typer](https://typer.tiangolo.com/) framework by Sebastián Ramírez.
|
|
483
|
+
|
|
484
|
+
Inspired by Git's command aliasing and various CLI tools that make shortcuts feel natural.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Support
|
|
489
|
+
|
|
490
|
+
- 🐛 **Issues:** [GitHub Issues](https://github.com/rdawebb/typer-extensions/issues)
|
|
491
|
+
- 💬 **Discussions:** [GitHub Discussions](https://github.com/rdawebb/typer-extensions/discussions)
|
|
492
|
+
- 📧 **Contact:** Create an issue for questions
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Links
|
|
497
|
+
|
|
498
|
+
- **GitHub:** https://github.com/rdawebb/typer-extensions
|
|
499
|
+
- **PyPI:** https://pypi.org/project/typer-extensions/
|
|
500
|
+
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
<p align="center">
|
|
505
|
+
<i>Making CLI aliases natural and maintainable</i>
|
|
506
|
+
</p>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
typer_extensions/__init__.py,sha256=kmkNxOvj1__xZMSjRsIzKcy2HXuSnmf0ygK8NRUsMJA,222
|
|
2
|
+
typer_extensions/_version.py,sha256=Q2J7zRwFHCkJjeBWiJBf8XlhWEMS3Yaf38zyC6lSXJY,70
|
|
3
|
+
typer_extensions/core.py,sha256=nQ4bLyDWAg7u1EDq1EWOT1DQZ-5zF0QT96BozXw8q9Y,19188
|
|
4
|
+
typer_extensions/format.py,sha256=TjpwinoCj1rMNuledPvHkSBntupjQQD0mMoJiwYycws,3000
|
|
5
|
+
typer_extensions/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
typer_extensions-0.2.1.dist-info/METADATA,sha256=Dtxw__rUspP50nK_FFcwcKyDuC7boDNY5WvYMlj97bQ,13068
|
|
7
|
+
typer_extensions-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
8
|
+
typer_extensions-0.2.1.dist-info/licenses/LICENSE,sha256=DLjGYQ3mgvK3gtIfFbGR-kW87JH5uVfYUHS-GecmeJM,1052
|
|
9
|
+
typer_extensions-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Rob Webb
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|