Coverage for typer / main.py: 100%
497 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-09 12:36 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-09 12:36 +0000
1import inspect 1bdeafgch
2import os 1bdeafgch
3import platform 1bdeafgch
4import shutil 1bdeafgch
5import subprocess 1bdeafgch
6import sys 1bdeafgch
7import traceback 1bdeafgch
8from collections.abc import Sequence 1bdeafgch
9from datetime import datetime 1bdeafgch
10from enum import Enum 1bdeafgch
11from functools import update_wrapper 1bdeafgch
12from pathlib import Path 1bdeafgch
13from traceback import FrameSummary, StackSummary 1bdeafgch
14from types import TracebackType 1bdeafgch
15from typing import Any, Callable, Optional, Union 1bdeafgch
16from uuid import UUID 1bdeafgch
18import click 1bdeafgch
19from typer._types import TyperChoice 1bdeafgch
21from ._typing import get_args, get_origin, is_literal_type, is_union, literal_values 1bdeafgch
22from .completion import get_completion_inspect_parameters 1bdeafgch
23from .core import ( 1bdeafgch
24 DEFAULT_MARKUP_MODE,
25 HAS_RICH,
26 MarkupMode,
27 TyperArgument,
28 TyperCommand,
29 TyperGroup,
30 TyperOption,
31)
32from .models import ( 1bdeafgch
33 AnyType,
34 ArgumentInfo,
35 CommandFunctionType,
36 CommandInfo,
37 Default,
38 DefaultPlaceholder,
39 DeveloperExceptionConfig,
40 FileBinaryRead,
41 FileBinaryWrite,
42 FileText,
43 FileTextWrite,
44 NoneType,
45 OptionInfo,
46 ParameterInfo,
47 ParamMeta,
48 Required,
49 TyperInfo,
50 TyperPath,
51)
52from .utils import get_params_from_function 1bdeafgch
54_original_except_hook = sys.excepthook 1bdeafgch
55_typer_developer_exception_attr_name = "__typer_developer_exception__" 1bdeafgch
58def except_hook( 1bdeafgch
59 exc_type: type[BaseException], exc_value: BaseException, tb: Optional[TracebackType]
60) -> None:
61 exception_config: Union[DeveloperExceptionConfig, None] = getattr( 1bdeafgch
62 exc_value, _typer_developer_exception_attr_name, None
63 )
64 standard_traceback = os.getenv( 1bdeafgch
65 "TYPER_STANDARD_TRACEBACK", os.getenv("_TYPER_STANDARD_TRACEBACK")
66 )
67 if ( 1bac
68 standard_traceback
69 or not exception_config
70 or not exception_config.pretty_exceptions_enable
71 ):
72 _original_except_hook(exc_type, exc_value, tb) 1bdeafgch
73 return 1bdeafgch
74 typer_path = os.path.dirname(__file__) 1bdeafgch
75 click_path = os.path.dirname(click.__file__) 1bdeafgch
76 internal_dir_names = [typer_path, click_path] 1bdeafgch
77 exc = exc_value 1bdeafgch
78 if HAS_RICH: 1bdeafgch
79 from . import rich_utils 1bdeafgch
81 rich_tb = rich_utils.get_traceback(exc, exception_config, internal_dir_names) 1bdeafgch
82 console_stderr = rich_utils._get_rich_console(stderr=True) 1bdeafgch
83 console_stderr.print(rich_tb) 1bdeafgch
84 return 1bdeafgch
85 tb_exc = traceback.TracebackException.from_exception(exc) 1bdeafgch
86 stack: list[FrameSummary] = [] 1bdeafgch
87 for frame in tb_exc.stack: 1bdeafgch
88 if any(frame.filename.startswith(path) for path in internal_dir_names): 1bdeafgch
89 if not exception_config.pretty_exceptions_short: 1bdeafgch
90 # Hide the line for internal libraries, Typer and Click
91 stack.append( 1bdeafgch
92 traceback.FrameSummary(
93 filename=frame.filename,
94 lineno=frame.lineno,
95 name=frame.name,
96 line="",
97 )
98 )
99 else:
100 stack.append(frame) 1bdeafgch
101 # Type ignore ref: https://github.com/python/typeshed/pull/8244
102 final_stack_summary = StackSummary.from_list(stack) 1bdeafgch
103 tb_exc.stack = final_stack_summary 1bdeafgch
104 for line in tb_exc.format(): 1bdeafgch
105 print(line, file=sys.stderr) 1bdeafgch
106 return 1bdeafgch
109def get_install_completion_arguments() -> tuple[click.Parameter, click.Parameter]: 1bdeafgch
110 install_param, show_param = get_completion_inspect_parameters() 1bdeafgch
111 click_install_param, _ = get_click_param(install_param) 1bdeafgch
112 click_show_param, _ = get_click_param(show_param) 1bdeafgch
113 return click_install_param, click_show_param 1bdeafgch
116class Typer: 1bdeafgch
117 def __init__( 1bdeafgch
118 self,
119 *,
120 name: Optional[str] = Default(None),
121 cls: Optional[type[TyperGroup]] = Default(None),
122 invoke_without_command: bool = Default(False),
123 no_args_is_help: bool = Default(False),
124 subcommand_metavar: Optional[str] = Default(None),
125 chain: bool = Default(False),
126 result_callback: Optional[Callable[..., Any]] = Default(None),
127 # Command
128 context_settings: Optional[dict[Any, Any]] = Default(None),
129 callback: Optional[Callable[..., Any]] = Default(None),
130 help: Optional[str] = Default(None),
131 epilog: Optional[str] = Default(None),
132 short_help: Optional[str] = Default(None),
133 options_metavar: str = Default("[OPTIONS]"),
134 add_help_option: bool = Default(True),
135 hidden: bool = Default(False),
136 deprecated: bool = Default(False),
137 add_completion: bool = True,
138 # Rich settings
139 rich_markup_mode: MarkupMode = DEFAULT_MARKUP_MODE,
140 rich_help_panel: Union[str, None] = Default(None),
141 suggest_commands: bool = True,
142 pretty_exceptions_enable: bool = True,
143 pretty_exceptions_show_locals: bool = True,
144 pretty_exceptions_short: bool = True,
145 ):
146 self._add_completion = add_completion 1bdeafgch
147 self.rich_markup_mode: MarkupMode = rich_markup_mode 1bdeafgch
148 self.rich_help_panel = rich_help_panel 1bdeafgch
149 self.suggest_commands = suggest_commands 1bdeafgch
150 self.pretty_exceptions_enable = pretty_exceptions_enable 1bdeafgch
151 self.pretty_exceptions_show_locals = pretty_exceptions_show_locals 1bdeafgch
152 self.pretty_exceptions_short = pretty_exceptions_short 1bdeafgch
153 self.info = TyperInfo( 1bdeafgch
154 name=name,
155 cls=cls,
156 invoke_without_command=invoke_without_command,
157 no_args_is_help=no_args_is_help,
158 subcommand_metavar=subcommand_metavar,
159 chain=chain,
160 result_callback=result_callback,
161 context_settings=context_settings,
162 callback=callback,
163 help=help,
164 epilog=epilog,
165 short_help=short_help,
166 options_metavar=options_metavar,
167 add_help_option=add_help_option,
168 hidden=hidden,
169 deprecated=deprecated,
170 )
171 self.registered_groups: list[TyperInfo] = [] 1bdeafgch
172 self.registered_commands: list[CommandInfo] = [] 1bdeafgch
173 self.registered_callback: Optional[TyperInfo] = None 1bdeafgch
175 def callback( 1bdeafgch
176 self,
177 *,
178 cls: Optional[type[TyperGroup]] = Default(None),
179 invoke_without_command: bool = Default(False),
180 no_args_is_help: bool = Default(False),
181 subcommand_metavar: Optional[str] = Default(None),
182 chain: bool = Default(False),
183 result_callback: Optional[Callable[..., Any]] = Default(None),
184 # Command
185 context_settings: Optional[dict[Any, Any]] = Default(None),
186 help: Optional[str] = Default(None),
187 epilog: Optional[str] = Default(None),
188 short_help: Optional[str] = Default(None),
189 options_metavar: Optional[str] = Default(None),
190 add_help_option: bool = Default(True),
191 hidden: bool = Default(False),
192 deprecated: bool = Default(False),
193 # Rich settings
194 rich_help_panel: Union[str, None] = Default(None),
195 ) -> Callable[[CommandFunctionType], CommandFunctionType]:
196 def decorator(f: CommandFunctionType) -> CommandFunctionType: 1bdeafgch
197 self.registered_callback = TyperInfo( 1bdeafgch
198 cls=cls,
199 invoke_without_command=invoke_without_command,
200 no_args_is_help=no_args_is_help,
201 subcommand_metavar=subcommand_metavar,
202 chain=chain,
203 result_callback=result_callback,
204 context_settings=context_settings,
205 callback=f,
206 help=help,
207 epilog=epilog,
208 short_help=short_help,
209 options_metavar=(
210 options_metavar or self._info_val_str("options_metavar")
211 ),
212 add_help_option=add_help_option,
213 hidden=hidden,
214 deprecated=deprecated,
215 rich_help_panel=rich_help_panel,
216 )
217 return f 1bdeafgch
219 return decorator 1bdeafgch
221 def command( 1bdeafgch
222 self,
223 name: Optional[str] = None,
224 *,
225 cls: Optional[type[TyperCommand]] = None,
226 context_settings: Optional[dict[Any, Any]] = None,
227 help: Optional[str] = None,
228 epilog: Optional[str] = None,
229 short_help: Optional[str] = None,
230 options_metavar: Optional[str] = None,
231 add_help_option: bool = True,
232 no_args_is_help: bool = False,
233 hidden: bool = False,
234 deprecated: bool = False,
235 # Rich settings
236 rich_help_panel: Union[str, None] = Default(None),
237 ) -> Callable[[CommandFunctionType], CommandFunctionType]:
238 if cls is None: 1bdeafgch
239 cls = TyperCommand 1bdeafgch
241 def decorator(f: CommandFunctionType) -> CommandFunctionType: 1bdeafgch
242 self.registered_commands.append( 1bdeafgch
243 CommandInfo(
244 name=name,
245 cls=cls,
246 context_settings=context_settings,
247 callback=f,
248 help=help,
249 epilog=epilog,
250 short_help=short_help,
251 options_metavar=(
252 options_metavar or self._info_val_str("options_metavar")
253 ),
254 add_help_option=add_help_option,
255 no_args_is_help=no_args_is_help,
256 hidden=hidden,
257 deprecated=deprecated,
258 # Rich settings
259 rich_help_panel=rich_help_panel,
260 )
261 )
262 return f 1bdeafgch
264 return decorator 1bdeafgch
266 def add_typer( 1bdeafgch
267 self,
268 typer_instance: "Typer",
269 *,
270 name: Optional[str] = Default(None),
271 cls: Optional[type[TyperGroup]] = Default(None),
272 invoke_without_command: bool = Default(False),
273 no_args_is_help: bool = Default(False),
274 subcommand_metavar: Optional[str] = Default(None),
275 chain: bool = Default(False),
276 result_callback: Optional[Callable[..., Any]] = Default(None),
277 # Command
278 context_settings: Optional[dict[Any, Any]] = Default(None),
279 callback: Optional[Callable[..., Any]] = Default(None),
280 help: Optional[str] = Default(None),
281 epilog: Optional[str] = Default(None),
282 short_help: Optional[str] = Default(None),
283 options_metavar: Optional[str] = Default(None),
284 add_help_option: bool = Default(True),
285 hidden: bool = Default(False),
286 deprecated: bool = Default(False),
287 # Rich settings
288 rich_help_panel: Union[str, None] = Default(None),
289 ) -> None:
290 self.registered_groups.append( 1bdeafgch
291 TyperInfo(
292 typer_instance,
293 name=name,
294 cls=cls,
295 invoke_without_command=invoke_without_command,
296 no_args_is_help=no_args_is_help,
297 subcommand_metavar=subcommand_metavar,
298 chain=chain,
299 result_callback=result_callback,
300 context_settings=context_settings,
301 callback=callback,
302 help=help,
303 epilog=epilog,
304 short_help=short_help,
305 options_metavar=(
306 options_metavar or self._info_val_str("options_metavar")
307 ),
308 add_help_option=add_help_option,
309 hidden=hidden,
310 deprecated=deprecated,
311 rich_help_panel=rich_help_panel,
312 )
313 )
315 def __call__(self, *args: Any, **kwargs: Any) -> Any: 1bdeafgch
316 if sys.excepthook != except_hook: 1bdeafgch
317 sys.excepthook = except_hook 1bdeafgch
318 try: 1bdeafgch
319 return get_command(self)(*args, **kwargs) 1bdeafgch
320 except Exception as e: 1bdeafgch
321 # Set a custom attribute to tell the hook to show nice exceptions for user
322 # code. An alternative/first implementation was a custom exception with
323 # raise custom_exc from e
324 # but that means the last error shown is the custom exception, not the
325 # actual error. This trick improves developer experience by showing the
326 # actual error last.
327 setattr( 1bdeafgch
328 e,
329 _typer_developer_exception_attr_name,
330 DeveloperExceptionConfig(
331 pretty_exceptions_enable=self.pretty_exceptions_enable,
332 pretty_exceptions_show_locals=self.pretty_exceptions_show_locals,
333 pretty_exceptions_short=self.pretty_exceptions_short,
334 ),
335 )
336 raise e 1bdeafgch
338 def _info_val_str(self, name: str) -> str: 1bdeafgch
339 val = getattr(self.info, name) 1bdeafgch
340 val_str = val.value if isinstance(val, DefaultPlaceholder) else val 1bdeafgch
341 assert isinstance(val_str, str) 1bdeafgch
342 return val_str 1bdeafgch
345def get_group(typer_instance: Typer) -> TyperGroup: 1bdeafgch
346 group = get_group_from_info( 1bdeafgch
347 TyperInfo(typer_instance),
348 pretty_exceptions_short=typer_instance.pretty_exceptions_short,
349 rich_markup_mode=typer_instance.rich_markup_mode,
350 suggest_commands=typer_instance.suggest_commands,
351 )
352 return group 1bdeafgch
355def get_command(typer_instance: Typer) -> click.Command: 1bdeafgch
356 if typer_instance._add_completion: 1bdeafgch
357 click_install_param, click_show_param = get_install_completion_arguments() 1bdeafgch
358 if ( 1bac
359 typer_instance.registered_callback
360 or typer_instance.info.callback
361 or typer_instance.registered_groups
362 or len(typer_instance.registered_commands) > 1
363 ):
364 # Create a Group
365 click_command: click.Command = get_group(typer_instance) 1bdeafgch
366 if typer_instance._add_completion: 1bdeafgch
367 click_command.params.append(click_install_param) 1bdeafgch
368 click_command.params.append(click_show_param) 1bdeafgch
369 return click_command 1bdeafgch
370 elif len(typer_instance.registered_commands) == 1: 1bdeafgch
371 # Create a single Command
372 single_command = typer_instance.registered_commands[0] 1bdeafgch
374 if not single_command.context_settings and not isinstance( 1bdeafgch
375 typer_instance.info.context_settings, DefaultPlaceholder
376 ):
377 single_command.context_settings = typer_instance.info.context_settings 1bdeafgch
379 click_command = get_command_from_info( 1bdeafgch
380 single_command,
381 pretty_exceptions_short=typer_instance.pretty_exceptions_short,
382 rich_markup_mode=typer_instance.rich_markup_mode,
383 )
384 if typer_instance._add_completion: 1bdeafgch
385 click_command.params.append(click_install_param) 1bdeafgch
386 click_command.params.append(click_show_param) 1bdeafgch
387 return click_command 1bdeafgch
388 raise RuntimeError(
389 "Could not get a command for this Typer instance"
390 ) # pragma: no cover
393def solve_typer_info_help(typer_info: TyperInfo) -> str: 1bdeafgch
394 # Priority 1: Explicit value was set in app.add_typer()
395 if not isinstance(typer_info.help, DefaultPlaceholder): 1bdeafgch
396 return inspect.cleandoc(typer_info.help or "") 1bdeafgch
397 # Priority 2: Explicit value was set in sub_app.callback()
398 try: 1bdeafgch
399 callback_help = typer_info.typer_instance.registered_callback.help 1bdeafgch
400 if not isinstance(callback_help, DefaultPlaceholder): 1bdeafgch
401 return inspect.cleandoc(callback_help or "") 1bdeafgch
402 except AttributeError: 1bdeafgch
403 pass 1bdeafgch
404 # Priority 3: Explicit value was set in sub_app = typer.Typer()
405 try: 1bdeafgch
406 instance_help = typer_info.typer_instance.info.help 1bdeafgch
407 if not isinstance(instance_help, DefaultPlaceholder): 1bdeafgch
408 return inspect.cleandoc(instance_help or "") 1bdeafgch
409 except AttributeError: 1bdeafgch
410 pass 1bdeafgch
411 # Priority 4: Implicit inference from callback docstring in app.add_typer()
412 if typer_info.callback: 1bdeafgch
413 doc = inspect.getdoc(typer_info.callback) 1bdeafgch
414 if doc: 1bdeafgch
415 return doc 1bdeafgch
416 # Priority 5: Implicit inference from callback docstring in @app.callback()
417 try: 1bdeafgch
418 callback = typer_info.typer_instance.registered_callback.callback 1bdeafgch
419 if not isinstance(callback, DefaultPlaceholder): 1bdeafgch
420 doc = inspect.getdoc(callback or "") 1bdeafgch
421 if doc: 1bdeafgch
422 return doc 1bdeafgch
423 except AttributeError: 1bdeafgch
424 pass 1bdeafgch
425 # Priority 6: Implicit inference from callback docstring in typer.Typer()
426 try: 1bdeafgch
427 instance_callback = typer_info.typer_instance.info.callback 1bdeafgch
428 if not isinstance(instance_callback, DefaultPlaceholder): 1bdeafgch
429 doc = inspect.getdoc(instance_callback) 1bdeafgch
430 if doc: 1bdeafgch
431 return doc 1bdeafgch
432 except AttributeError: 1bdeafgch
433 pass 1bdeafgch
434 # Value not set, use the default
435 return typer_info.help.value 1bdeafgch
438def solve_typer_info_defaults(typer_info: TyperInfo) -> TyperInfo: 1bdeafgch
439 values: dict[str, Any] = {} 1bdeafgch
440 for name, value in typer_info.__dict__.items(): 1bdeafgch
441 # Priority 1: Value was set in app.add_typer()
442 if not isinstance(value, DefaultPlaceholder): 1bdeafgch
443 values[name] = value 1bdeafgch
444 continue 1bdeafgch
445 # Priority 2: Value was set in @subapp.callback()
446 try: 1bdeafgch
447 callback_value = getattr( 1bdeafgch
448 typer_info.typer_instance.registered_callback, # type: ignore
449 name,
450 )
451 if not isinstance(callback_value, DefaultPlaceholder): 1bdeafgch
452 values[name] = callback_value 1bdeafgch
453 continue 1bdeafgch
454 except AttributeError: 1bdeafgch
455 pass 1bdeafgch
456 # Priority 3: Value set in subapp = typer.Typer()
457 try: 1bdeafgch
458 instance_value = getattr( 1bdeafgch
459 typer_info.typer_instance.info, # type: ignore
460 name,
461 )
462 if not isinstance(instance_value, DefaultPlaceholder): 1bdeafgch
463 values[name] = instance_value 1bdeafgch
464 continue 1bdeafgch
465 except AttributeError: 1bdeafgch
466 pass 1bdeafgch
467 # Value not set, use the default
468 values[name] = value.value 1bdeafgch
469 values["help"] = solve_typer_info_help(typer_info) 1bdeafgch
470 return TyperInfo(**values) 1bdeafgch
473def get_group_from_info( 1bdeafgch
474 group_info: TyperInfo,
475 *,
476 pretty_exceptions_short: bool,
477 suggest_commands: bool,
478 rich_markup_mode: MarkupMode,
479) -> TyperGroup:
480 assert group_info.typer_instance, ( 1bdeafgch
481 "A Typer instance is needed to generate a Click Group"
482 )
483 commands: dict[str, click.Command] = {} 1bdeafgch
484 for command_info in group_info.typer_instance.registered_commands: 1bdeafgch
485 command = get_command_from_info( 1bdeafgch
486 command_info=command_info,
487 pretty_exceptions_short=pretty_exceptions_short,
488 rich_markup_mode=rich_markup_mode,
489 )
490 if command.name: 1bdeafgch
491 commands[command.name] = command 1bdeafgch
492 for sub_group_info in group_info.typer_instance.registered_groups: 1bdeafgch
493 sub_group = get_group_from_info( 1bdeafgch
494 sub_group_info,
495 pretty_exceptions_short=pretty_exceptions_short,
496 rich_markup_mode=rich_markup_mode,
497 suggest_commands=suggest_commands,
498 )
499 if sub_group.name: 1bdeafgch
500 commands[sub_group.name] = sub_group 1bdeafgch
501 else:
502 if sub_group.callback: 1bdeafgch
503 import warnings 1bdeafgch
505 warnings.warn( 1bdeafgch
506 "The 'callback' parameter is not supported by Typer when using `add_typer` without a name",
507 stacklevel=5,
508 )
509 for sub_command_name, sub_command in sub_group.commands.items(): 1bdeafgch
510 commands[sub_command_name] = sub_command 1bdeafgch
511 solved_info = solve_typer_info_defaults(group_info) 1bdeafgch
512 ( 1bdeafgch
513 params,
514 convertors,
515 context_param_name,
516 ) = get_params_convertors_ctx_param_name_from_function(solved_info.callback)
517 cls = solved_info.cls or TyperGroup 1bdeafgch
518 assert issubclass(cls, TyperGroup), f"{cls} should be a subclass of {TyperGroup}" 1bdeafgch
519 group = cls( 1bdeafgch
520 name=solved_info.name or "",
521 commands=commands,
522 invoke_without_command=solved_info.invoke_without_command,
523 no_args_is_help=solved_info.no_args_is_help,
524 subcommand_metavar=solved_info.subcommand_metavar,
525 chain=solved_info.chain,
526 result_callback=solved_info.result_callback,
527 context_settings=solved_info.context_settings,
528 callback=get_callback(
529 callback=solved_info.callback,
530 params=params,
531 convertors=convertors,
532 context_param_name=context_param_name,
533 pretty_exceptions_short=pretty_exceptions_short,
534 ),
535 params=params,
536 help=solved_info.help,
537 epilog=solved_info.epilog,
538 short_help=solved_info.short_help,
539 options_metavar=solved_info.options_metavar,
540 add_help_option=solved_info.add_help_option,
541 hidden=solved_info.hidden,
542 deprecated=solved_info.deprecated,
543 rich_markup_mode=rich_markup_mode,
544 # Rich settings
545 rich_help_panel=solved_info.rich_help_panel,
546 suggest_commands=suggest_commands,
547 )
548 return group 1bdeafgch
551def get_command_name(name: str) -> str: 1bdeafgch
552 return name.lower().replace("_", "-") 1bdeafgch
555def get_params_convertors_ctx_param_name_from_function( 1bdeafgch
556 callback: Optional[Callable[..., Any]],
557) -> tuple[list[Union[click.Argument, click.Option]], dict[str, Any], Optional[str]]:
558 params = [] 1bdeafgch
559 convertors = {} 1bdeafgch
560 context_param_name = None 1bdeafgch
561 if callback: 1bdeafgch
562 parameters = get_params_from_function(callback) 1bdeafgch
563 for param_name, param in parameters.items(): 1bdeafgch
564 if lenient_issubclass(param.annotation, click.Context): 1bdeafgch
565 context_param_name = param_name 1bdeafgch
566 continue 1bdeafgch
567 click_param, convertor = get_click_param(param) 1bdeafgch
568 if convertor: 1bdeafgch
569 convertors[param_name] = convertor 1bdeafgch
570 params.append(click_param) 1bdeafgch
571 return params, convertors, context_param_name 1bdeafgch
574def get_command_from_info( 1bdeafgch
575 command_info: CommandInfo,
576 *,
577 pretty_exceptions_short: bool,
578 rich_markup_mode: MarkupMode,
579) -> click.Command:
580 assert command_info.callback, "A command must have a callback function" 1bdeafgch
581 name = command_info.name or get_command_name(command_info.callback.__name__) 1bdeafgch
582 use_help = command_info.help 1bdeafgch
583 if use_help is None: 1bdeafgch
584 use_help = inspect.getdoc(command_info.callback) 1bdeafgch
585 else:
586 use_help = inspect.cleandoc(use_help) 1bdeafgch
587 ( 1bdeafgch
588 params,
589 convertors,
590 context_param_name,
591 ) = get_params_convertors_ctx_param_name_from_function(command_info.callback)
592 cls = command_info.cls or TyperCommand 1bdeafgch
593 command = cls( 1bdeafgch
594 name=name,
595 context_settings=command_info.context_settings,
596 callback=get_callback(
597 callback=command_info.callback,
598 params=params,
599 convertors=convertors,
600 context_param_name=context_param_name,
601 pretty_exceptions_short=pretty_exceptions_short,
602 ),
603 params=params, # type: ignore
604 help=use_help,
605 epilog=command_info.epilog,
606 short_help=command_info.short_help,
607 options_metavar=command_info.options_metavar,
608 add_help_option=command_info.add_help_option,
609 no_args_is_help=command_info.no_args_is_help,
610 hidden=command_info.hidden,
611 deprecated=command_info.deprecated,
612 rich_markup_mode=rich_markup_mode,
613 # Rich settings
614 rich_help_panel=command_info.rich_help_panel,
615 )
616 return command 1bdeafgch
619def determine_type_convertor(type_: Any) -> Optional[Callable[[Any], Any]]: 1bdeafgch
620 convertor: Optional[Callable[[Any], Any]] = None 1bdeafgch
621 if lenient_issubclass(type_, Path): 1bdeafgch
622 convertor = param_path_convertor 1bdeafgch
623 if lenient_issubclass(type_, Enum): 1bdeafgch
624 convertor = generate_enum_convertor(type_) 1bdeafgch
625 return convertor 1bdeafgch
628def param_path_convertor(value: Optional[str] = None) -> Optional[Path]: 1bdeafgch
629 if value is not None: 1bdeafgch
630 # allow returning any subclass of Path created by an annotated parser without converting
631 # it back to a Path
632 return value if isinstance(value, Path) else Path(value) 1bdeafgch
633 return None 1bdeafgch
636def generate_enum_convertor(enum: type[Enum]) -> Callable[[Any], Any]: 1bdeafgch
637 val_map = {str(val.value): val for val in enum} 1bdeafgch
639 def convertor(value: Any) -> Any: 1bdeafgch
640 if value is not None: 1bdeafgch
641 val = str(value) 1bdeafgch
642 if val in val_map: 1bdeafgch
643 key = val_map[val] 1bdeafgch
644 return enum(key) 1bdeafgch
646 return convertor 1bdeafgch
649def generate_list_convertor( 1bdeafgch
650 convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any]
651) -> Callable[[Optional[Sequence[Any]]], Optional[list[Any]]]:
652 def internal_convertor(value: Optional[Sequence[Any]]) -> Optional[list[Any]]: 1bdeafgch
653 if (value is None) or (default_value is None and len(value) == 0): 1bdeafgch
654 return None 1bdeafgch
655 return [convertor(v) if convertor else v for v in value] 1bdeafgch
657 return internal_convertor 1bdeafgch
660def generate_tuple_convertor( 1bdeafgch
661 types: Sequence[Any],
662) -> Callable[[Optional[tuple[Any, ...]]], Optional[tuple[Any, ...]]]:
663 convertors = [determine_type_convertor(type_) for type_ in types] 1bdeafgch
665 def internal_convertor( 1bdeafgch
666 param_args: Optional[tuple[Any, ...]],
667 ) -> Optional[tuple[Any, ...]]:
668 if param_args is None: 1bdeafgch
669 return None 1bdeafgch
670 return tuple( 1bdeafgch
671 convertor(arg) if convertor else arg
672 for (convertor, arg) in zip(convertors, param_args)
673 )
675 return internal_convertor 1bdeafgch
678def get_callback( 1bdeafgch
679 *,
680 callback: Optional[Callable[..., Any]] = None,
681 params: Sequence[click.Parameter] = [],
682 convertors: Optional[dict[str, Callable[[str], Any]]] = None,
683 context_param_name: Optional[str] = None,
684 pretty_exceptions_short: bool,
685) -> Optional[Callable[..., Any]]:
686 use_convertors = convertors or {} 1bdeafgch
687 if not callback: 1bdeafgch
688 return None 1bdeafgch
689 parameters = get_params_from_function(callback) 1bdeafgch
690 use_params: dict[str, Any] = {} 1bdeafgch
691 for param_name in parameters: 1bdeafgch
692 use_params[param_name] = None 1bdeafgch
693 for param in params: 1bdeafgch
694 if param.name: 1bdeafgch
695 use_params[param.name] = param.default 1bdeafgch
697 def wrapper(**kwargs: Any) -> Any: 1bdeafgch
698 _rich_traceback_guard = pretty_exceptions_short # noqa: F841 1bdeafgch
699 for k, v in kwargs.items(): 1bdeafgch
700 if k in use_convertors: 1bdeafgch
701 use_params[k] = use_convertors[k](v) 1bdeafgch
702 else:
703 use_params[k] = v 1bdeafgch
704 if context_param_name: 1bdeafgch
705 use_params[context_param_name] = click.get_current_context() 1bdeafgch
706 return callback(**use_params) 1bdeafgch
708 update_wrapper(wrapper, callback) 1bdeafgch
709 return wrapper 1bdeafgch
712def get_click_type( 1bdeafgch
713 *, annotation: Any, parameter_info: ParameterInfo
714) -> click.ParamType:
715 if parameter_info.click_type is not None: 1bdeafgch
716 return parameter_info.click_type 1bdeafgch
718 elif parameter_info.parser is not None: 1bdeafgch
719 return click.types.FuncParamType(parameter_info.parser) 1bdeafgch
721 elif annotation is str: 1bdeafgch
722 return click.STRING 1bdeafgch
723 elif annotation is int: 1bdeafgch
724 if parameter_info.min is not None or parameter_info.max is not None: 1bdeafgch
725 min_ = None 1bdeafgch
726 max_ = None 1bdeafgch
727 if parameter_info.min is not None: 1bdeafgch
728 min_ = int(parameter_info.min) 1bdeafgch
729 if parameter_info.max is not None: 1bdeafgch
730 max_ = int(parameter_info.max) 1bdeafgch
731 return click.IntRange(min=min_, max=max_, clamp=parameter_info.clamp) 1bdeafgch
732 else:
733 return click.INT 1bdeafgch
734 elif annotation is float: 1bdeafgch
735 if parameter_info.min is not None or parameter_info.max is not None: 1bdeafgch
736 return click.FloatRange( 1bdeafgch
737 min=parameter_info.min,
738 max=parameter_info.max,
739 clamp=parameter_info.clamp,
740 )
741 else:
742 return click.FLOAT 1bdeafgch
743 elif annotation is bool: 1bdeafgch
744 return click.BOOL 1bdeafgch
745 elif annotation == UUID: 1bdeafgch
746 return click.UUID 1bdeafgch
747 elif annotation == datetime: 1bdeafgch
748 return click.DateTime(formats=parameter_info.formats) 1bdeafgch
749 elif ( 1a
750 annotation == Path
751 or parameter_info.allow_dash
752 or parameter_info.path_type
753 or parameter_info.resolve_path
754 ):
755 return TyperPath( 1bdeafgch
756 exists=parameter_info.exists,
757 file_okay=parameter_info.file_okay,
758 dir_okay=parameter_info.dir_okay,
759 writable=parameter_info.writable,
760 readable=parameter_info.readable,
761 resolve_path=parameter_info.resolve_path,
762 allow_dash=parameter_info.allow_dash,
763 path_type=parameter_info.path_type,
764 )
765 elif lenient_issubclass(annotation, FileTextWrite): 1bdeafgch
766 return click.File( 1bdeafgch
767 mode=parameter_info.mode or "w",
768 encoding=parameter_info.encoding,
769 errors=parameter_info.errors,
770 lazy=parameter_info.lazy,
771 atomic=parameter_info.atomic,
772 )
773 elif lenient_issubclass(annotation, FileText): 1bdeafgch
774 return click.File( 1bdeafgch
775 mode=parameter_info.mode or "r",
776 encoding=parameter_info.encoding,
777 errors=parameter_info.errors,
778 lazy=parameter_info.lazy,
779 atomic=parameter_info.atomic,
780 )
781 elif lenient_issubclass(annotation, FileBinaryRead): 1bdeafgch
782 return click.File( 1bdeafgch
783 mode=parameter_info.mode or "rb",
784 encoding=parameter_info.encoding,
785 errors=parameter_info.errors,
786 lazy=parameter_info.lazy,
787 atomic=parameter_info.atomic,
788 )
789 elif lenient_issubclass(annotation, FileBinaryWrite): 1bdeafgch
790 return click.File( 1bdeafgch
791 mode=parameter_info.mode or "wb",
792 encoding=parameter_info.encoding,
793 errors=parameter_info.errors,
794 lazy=parameter_info.lazy,
795 atomic=parameter_info.atomic,
796 )
797 elif lenient_issubclass(annotation, Enum): 1bdeafgch
798 # The custom TyperChoice is only needed for Click < 8.2.0, to parse the
799 # command line values matching them to the enum values. Click 8.2.0 added
800 # support for enum values but reading enum names.
801 # Passing here the list of enum values (instead of just the enum) accounts for
802 # Click < 8.2.0.
803 return TyperChoice( 1bdeafgch
804 [item.value for item in annotation],
805 case_sensitive=parameter_info.case_sensitive,
806 )
807 elif is_literal_type(annotation): 1bdeafgch
808 return click.Choice( 1bdeafgch
809 literal_values(annotation),
810 case_sensitive=parameter_info.case_sensitive,
811 )
812 raise RuntimeError(f"Type not yet supported: {annotation}") # pragma: no cover
815def lenient_issubclass( 1bdeafgch
816 cls: Any, class_or_tuple: Union[AnyType, tuple[AnyType, ...]]
817) -> bool:
818 return isinstance(cls, type) and issubclass(cls, class_or_tuple) 1bdeafgch
821def get_click_param( 1bdeafgch
822 param: ParamMeta,
823) -> tuple[Union[click.Argument, click.Option], Any]:
824 # First, find out what will be:
825 # * ParamInfo (ArgumentInfo or OptionInfo)
826 # * default_value
827 # * required
828 default_value = None 1bdeafgch
829 required = False 1bdeafgch
830 if isinstance(param.default, ParameterInfo): 1bdeafgch
831 parameter_info = param.default 1bdeafgch
832 if parameter_info.default == Required: 1bdeafgch
833 required = True 1bdeafgch
834 else:
835 default_value = parameter_info.default 1bdeafgch
836 elif param.default == Required or param.default is param.empty: 1bdeafgch
837 required = True 1bdeafgch
838 parameter_info = ArgumentInfo() 1bdeafgch
839 else:
840 default_value = param.default 1bdeafgch
841 parameter_info = OptionInfo() 1bdeafgch
842 annotation: Any
843 if param.annotation is not param.empty: 1bdeafgch
844 annotation = param.annotation 1bdeafgch
845 else:
846 annotation = str 1bdeafgch
847 main_type = annotation 1bdeafgch
848 is_list = False 1bdeafgch
849 is_tuple = False 1bdeafgch
850 parameter_type: Any = None 1bdeafgch
851 is_flag = None 1bdeafgch
852 origin = get_origin(main_type) 1bdeafgch
854 if origin is not None: 1bdeafgch
855 # Handle SomeType | None and Optional[SomeType]
856 if is_union(origin): 1bdeafgch
857 types = [] 1bdeafgch
858 for type_ in get_args(main_type): 1bdeafgch
859 if type_ is NoneType: 1bdeafgch
860 continue 1bdeafgch
861 types.append(type_) 1bdeafgch
862 assert len(types) == 1, "Typer Currently doesn't support Union types" 1bdeafgch
863 main_type = types[0] 1bdeafgch
864 origin = get_origin(main_type) 1bdeafgch
865 # Handle Tuples and Lists
866 if lenient_issubclass(origin, list): 1bdeafgch
867 main_type = get_args(main_type)[0] 1bdeafgch
868 assert not get_origin(main_type), ( 1bdeafgch
869 "List types with complex sub-types are not currently supported"
870 )
871 is_list = True 1bdeafgch
872 elif lenient_issubclass(origin, tuple): 1bdeafgch
873 types = [] 1bdeafgch
874 for type_ in get_args(main_type): 1bdeafgch
875 assert not get_origin(type_), ( 1bdeafgch
876 "Tuple types with complex sub-types are not currently supported"
877 )
878 types.append( 1bdeafgch
879 get_click_type(annotation=type_, parameter_info=parameter_info)
880 )
881 parameter_type = tuple(types) 1bdeafgch
882 is_tuple = True 1bdeafgch
883 if parameter_type is None: 1bdeafgch
884 parameter_type = get_click_type( 1bdeafgch
885 annotation=main_type, parameter_info=parameter_info
886 )
887 convertor = determine_type_convertor(main_type) 1bdeafgch
888 if is_list: 1bdeafgch
889 convertor = generate_list_convertor( 1bdeafgch
890 convertor=convertor, default_value=default_value
891 )
892 if is_tuple: 1bdeafgch
893 convertor = generate_tuple_convertor(get_args(main_type)) 1bdeafgch
894 if isinstance(parameter_info, OptionInfo): 1bdeafgch
895 if main_type is bool: 1bdeafgch
896 is_flag = True 1bdeafgch
897 # Click doesn't accept a flag of type bool, only None, and then it sets it
898 # to bool internally
899 parameter_type = None 1bdeafgch
900 default_option_name = get_command_name(param.name) 1bdeafgch
901 if is_flag: 1bdeafgch
902 default_option_declaration = ( 1bdeafgch
903 f"--{default_option_name}/--no-{default_option_name}"
904 )
905 else:
906 default_option_declaration = f"--{default_option_name}" 1bdeafgch
907 param_decls = [param.name] 1bdeafgch
908 if parameter_info.param_decls: 1bdeafgch
909 param_decls.extend(parameter_info.param_decls) 1bdeafgch
910 else:
911 param_decls.append(default_option_declaration) 1bdeafgch
912 return ( 1bdeafgch
913 TyperOption(
914 # Option
915 param_decls=param_decls,
916 show_default=parameter_info.show_default,
917 prompt=parameter_info.prompt,
918 confirmation_prompt=parameter_info.confirmation_prompt,
919 prompt_required=parameter_info.prompt_required,
920 hide_input=parameter_info.hide_input,
921 is_flag=is_flag,
922 multiple=is_list,
923 count=parameter_info.count,
924 allow_from_autoenv=parameter_info.allow_from_autoenv,
925 type=parameter_type,
926 help=parameter_info.help,
927 hidden=parameter_info.hidden,
928 show_choices=parameter_info.show_choices,
929 show_envvar=parameter_info.show_envvar,
930 # Parameter
931 required=required,
932 default=default_value,
933 callback=get_param_callback(
934 callback=parameter_info.callback, convertor=convertor
935 ),
936 metavar=parameter_info.metavar,
937 expose_value=parameter_info.expose_value,
938 is_eager=parameter_info.is_eager,
939 envvar=parameter_info.envvar,
940 shell_complete=parameter_info.shell_complete,
941 autocompletion=get_param_completion(parameter_info.autocompletion),
942 # Rich settings
943 rich_help_panel=parameter_info.rich_help_panel,
944 ),
945 convertor,
946 )
947 elif isinstance(parameter_info, ArgumentInfo): 1bdeafgch
948 param_decls = [param.name] 1bdeafgch
949 nargs = None 1bdeafgch
950 if is_list: 1bdeafgch
951 nargs = -1 1bdeafgch
952 return ( 1bdeafgch
953 TyperArgument(
954 # Argument
955 param_decls=param_decls,
956 type=parameter_type,
957 required=required,
958 nargs=nargs,
959 # TyperArgument
960 show_default=parameter_info.show_default,
961 show_choices=parameter_info.show_choices,
962 show_envvar=parameter_info.show_envvar,
963 help=parameter_info.help,
964 hidden=parameter_info.hidden,
965 # Parameter
966 default=default_value,
967 callback=get_param_callback(
968 callback=parameter_info.callback, convertor=convertor
969 ),
970 metavar=parameter_info.metavar,
971 expose_value=parameter_info.expose_value,
972 is_eager=parameter_info.is_eager,
973 envvar=parameter_info.envvar,
974 shell_complete=parameter_info.shell_complete,
975 autocompletion=get_param_completion(parameter_info.autocompletion),
976 # Rich settings
977 rich_help_panel=parameter_info.rich_help_panel,
978 ),
979 convertor,
980 )
981 raise AssertionError("A click.Parameter should be returned") # pragma: no cover
984def get_param_callback( 1bdeafgch
985 *,
986 callback: Optional[Callable[..., Any]] = None,
987 convertor: Optional[Callable[..., Any]] = None,
988) -> Optional[Callable[..., Any]]:
989 if not callback: 1bdeafgch
990 return None 1bdeafgch
991 parameters = get_params_from_function(callback) 1bdeafgch
992 ctx_name = None 1bdeafgch
993 click_param_name = None 1bdeafgch
994 value_name = None 1bdeafgch
995 untyped_names: list[str] = [] 1bdeafgch
996 for param_name, param_sig in parameters.items(): 1bdeafgch
997 if lenient_issubclass(param_sig.annotation, click.Context): 1bdeafgch
998 ctx_name = param_name 1bdeafgch
999 elif lenient_issubclass(param_sig.annotation, click.Parameter): 1bdeafgch
1000 click_param_name = param_name 1bdeafgch
1001 else:
1002 untyped_names.append(param_name) 1bdeafgch
1003 # Extract value param name first
1004 if untyped_names: 1bdeafgch
1005 value_name = untyped_names.pop() 1bdeafgch
1006 # If context and Click param were not typed (old/Click callback style) extract them
1007 if untyped_names: 1bdeafgch
1008 if ctx_name is None: 1bdeafgch
1009 ctx_name = untyped_names.pop(0) 1bdeafgch
1010 if click_param_name is None: 1bdeafgch
1011 if untyped_names: 1bdeafgch
1012 click_param_name = untyped_names.pop(0) 1bdeafgch
1013 if untyped_names: 1bdeafgch
1014 raise click.ClickException( 1bdeafgch
1015 "Too many CLI parameter callback function parameters"
1016 )
1018 def wrapper(ctx: click.Context, param: click.Parameter, value: Any) -> Any: 1bdeafgch
1019 use_params: dict[str, Any] = {} 1bdeafgch
1020 if ctx_name: 1bdeafgch
1021 use_params[ctx_name] = ctx 1bdeafgch
1022 if click_param_name: 1bdeafgch
1023 use_params[click_param_name] = param 1bdeafgch
1024 if value_name: 1bdeafgch
1025 if convertor: 1bdeafgch
1026 use_value = convertor(value) 1bdeafgch
1027 else:
1028 use_value = value 1bdeafgch
1029 use_params[value_name] = use_value 1bdeafgch
1030 return callback(**use_params) 1bdeafgch
1032 update_wrapper(wrapper, callback) 1bdeafgch
1033 return wrapper 1bdeafgch
1036def get_param_completion( 1bdeafgch
1037 callback: Optional[Callable[..., Any]] = None,
1038) -> Optional[Callable[..., Any]]:
1039 if not callback: 1bdeafgch
1040 return None 1bdeafgch
1041 parameters = get_params_from_function(callback) 1bdeafgch
1042 ctx_name = None 1bdeafgch
1043 args_name = None 1bdeafgch
1044 incomplete_name = None 1bdeafgch
1045 unassigned_params = list(parameters.values()) 1bdeafgch
1046 for param_sig in unassigned_params[:]: 1bdeafgch
1047 origin = get_origin(param_sig.annotation) 1bdeafgch
1048 if lenient_issubclass(param_sig.annotation, click.Context): 1bdeafgch
1049 ctx_name = param_sig.name 1bdeafgch
1050 unassigned_params.remove(param_sig) 1bdeafgch
1051 elif lenient_issubclass(origin, list): 1bdeafgch
1052 args_name = param_sig.name 1bdeafgch
1053 unassigned_params.remove(param_sig) 1bdeafgch
1054 elif lenient_issubclass(param_sig.annotation, str): 1bdeafgch
1055 incomplete_name = param_sig.name 1bdeafgch
1056 unassigned_params.remove(param_sig) 1bdeafgch
1057 # If there are still unassigned parameters (not typed), extract by name
1058 for param_sig in unassigned_params[:]: 1bdeafgch
1059 if ctx_name is None and param_sig.name == "ctx": 1bdeafgch
1060 ctx_name = param_sig.name 1bdeafgch
1061 unassigned_params.remove(param_sig) 1bdeafgch
1062 elif args_name is None and param_sig.name == "args": 1bdeafgch
1063 args_name = param_sig.name 1bdeafgch
1064 unassigned_params.remove(param_sig) 1bdeafgch
1065 elif incomplete_name is None and param_sig.name == "incomplete": 1bdeafgch
1066 incomplete_name = param_sig.name 1bdeafgch
1067 unassigned_params.remove(param_sig) 1bdeafgch
1068 # Extract value param name first
1069 if unassigned_params: 1bdeafgch
1070 show_params = " ".join([param.name for param in unassigned_params]) 1bdeafgch
1071 raise click.ClickException( 1bdeafgch
1072 f"Invalid autocompletion callback parameters: {show_params}"
1073 )
1075 def wrapper(ctx: click.Context, args: list[str], incomplete: Optional[str]) -> Any: 1bdeafgch
1076 use_params: dict[str, Any] = {} 1bdeafgch
1077 if ctx_name: 1bdeafgch
1078 use_params[ctx_name] = ctx 1bdeafgch
1079 if args_name: 1bdeafgch
1080 use_params[args_name] = args 1bdeafgch
1081 if incomplete_name: 1bdeafgch
1082 use_params[incomplete_name] = incomplete 1bdeafgch
1083 return callback(**use_params) 1bdeafgch
1085 update_wrapper(wrapper, callback) 1bdeafgch
1086 return wrapper 1bdeafgch
1089def run(function: Callable[..., Any]) -> None: 1bdeafgch
1090 app = Typer(add_completion=False) 1bdeafgch
1091 app.command()(function) 1bdeafgch
1092 app() 1bdeafgch
1095def _is_macos() -> bool: 1bdeafgch
1096 return platform.system() == "Darwin" 1bdeafgch
1099def _is_linux_or_bsd() -> bool: 1bdeafgch
1100 if platform.system() == "Linux": 1bdeafgch
1101 return True 1bdeafgch
1103 return "BSD" in platform.system() 1bdeafgch
1106def launch(url: str, wait: bool = False, locate: bool = False) -> int: 1bdeafgch
1107 """This function launches the given URL (or filename) in the default
1108 viewer application for this file type. If this is an executable, it
1109 might launch the executable in a new session. The return value is
1110 the exit code of the launched application. Usually, ``0`` indicates
1111 success.
1113 This function handles url in different operating systems separately:
1114 - On macOS (Darwin), it uses the 'open' command.
1115 - On Linux and BSD, it uses 'xdg-open' if available.
1116 - On Windows (and other OSes), it uses the standard webbrowser module.
1118 The function avoids, when possible, using the webbrowser module on Linux and macOS
1119 to prevent spammy terminal messages from some browsers (e.g., Chrome).
1121 Examples::
1123 typer.launch("https://typer.tiangolo.com/")
1124 typer.launch("/my/downloaded/file", locate=True)
1126 :param url: URL or filename of the thing to launch.
1127 :param wait: Wait for the program to exit before returning. This
1128 only works if the launched program blocks. In particular,
1129 ``xdg-open`` on Linux does not block.
1130 :param locate: if this is set to `True` then instead of launching the
1131 application associated with the URL it will attempt to
1132 launch a file manager with the file located. This
1133 might have weird effects if the URL does not point to
1134 the filesystem.
1135 """
1137 if url.startswith("http://") or url.startswith("https://"): 1bdeafgch
1138 if _is_macos(): 1bdeafgch
1139 return subprocess.Popen( 1bdeafgch
1140 ["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
1141 ).wait()
1143 has_xdg_open = _is_linux_or_bsd() and shutil.which("xdg-open") is not None 1bdeafgch
1145 if has_xdg_open: 1bdeafgch
1146 return subprocess.Popen( 1bdeafgch
1147 ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
1148 ).wait()
1150 import webbrowser 1bdeafgch
1152 webbrowser.open(url) 1bdeafgch
1154 return 0 1bdeafgch
1156 else:
1157 return click.launch(url) 1bdeafgch