typer.utils
1import inspect 2import sys 3from copy import copy 4from typing import Any, Callable, Dict, List, Tuple, Type, cast 5 6from ._typing import Annotated, get_args, get_origin, get_type_hints 7from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta 8 9 10def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str: 11 # Render a `ParameterInfo` subclass for use in error messages. 12 # User code doesn't call `*Info` directly, so errors should present the classes how 13 # they were (probably) defined in the user code. 14 if param_type is OptionInfo: 15 return "`Option`" 16 elif param_type is ArgumentInfo: 17 return "`Argument`" 18 # This line shouldn't be reachable during normal use. 19 return f"`{param_type.__name__}`" # pragma: no cover 20 21 22class AnnotatedParamWithDefaultValueError(Exception): 23 argument_name: str 24 param_type: Type[ParameterInfo] 25 26 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 27 self.argument_name = argument_name 28 self.param_type = param_type 29 30 def __str__(self) -> str: 31 param_type_str = _param_type_to_user_string(self.param_type) 32 return ( 33 f"{param_type_str} default value cannot be set in `Annotated`" 34 f" for {self.argument_name!r}. Set the default value with `=` instead." 35 ) 36 37 38class MixedAnnotatedAndDefaultStyleError(Exception): 39 argument_name: str 40 annotated_param_type: Type[ParameterInfo] 41 default_param_type: Type[ParameterInfo] 42 43 def __init__( 44 self, 45 argument_name: str, 46 annotated_param_type: Type[ParameterInfo], 47 default_param_type: Type[ParameterInfo], 48 ): 49 self.argument_name = argument_name 50 self.annotated_param_type = annotated_param_type 51 self.default_param_type = default_param_type 52 53 def __str__(self) -> str: 54 annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) 55 default_param_type_str = _param_type_to_user_string(self.default_param_type) 56 msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" 57 if self.annotated_param_type is self.default_param_type: 58 msg += " default value" 59 else: 60 msg += f" {default_param_type_str} as a default value" 61 msg += f" together for {self.argument_name!r}" 62 return msg 63 64 65class MultipleTyperAnnotationsError(Exception): 66 argument_name: str 67 68 def __init__(self, argument_name: str): 69 self.argument_name = argument_name 70 71 def __str__(self) -> str: 72 return ( 73 "Cannot specify multiple `Annotated` Typer arguments" 74 f" for {self.argument_name!r}" 75 ) 76 77 78class DefaultFactoryAndDefaultValueError(Exception): 79 argument_name: str 80 param_type: Type[ParameterInfo] 81 82 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 83 self.argument_name = argument_name 84 self.param_type = param_type 85 86 def __str__(self) -> str: 87 param_type_str = _param_type_to_user_string(self.param_type) 88 return ( 89 "Cannot specify `default_factory` and a default value together" 90 f" for {param_type_str}" 91 ) 92 93 94def _split_annotation_from_typer_annotations( 95 base_annotation: Type[Any], 96) -> Tuple[Type[Any], List[ParameterInfo]]: 97 if get_origin(base_annotation) is not Annotated: 98 return base_annotation, [] 99 base_annotation, *maybe_typer_annotations = get_args(base_annotation) 100 return base_annotation, [ 101 annotation 102 for annotation in maybe_typer_annotations 103 if isinstance(annotation, ParameterInfo) 104 ] 105 106 107def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: 108 if sys.version_info >= (3, 10): 109 signature = inspect.signature(func, eval_str=True) 110 else: 111 signature = inspect.signature(func) 112 113 type_hints = get_type_hints(func) 114 params = {} 115 for param in signature.parameters.values(): 116 annotation, typer_annotations = _split_annotation_from_typer_annotations( 117 param.annotation, 118 ) 119 if len(typer_annotations) > 1: 120 raise MultipleTyperAnnotationsError(param.name) 121 122 default = param.default 123 if typer_annotations: 124 # It's something like `my_param: Annotated[str, Argument()]` 125 [parameter_info] = typer_annotations 126 127 # Forbid `my_param: Annotated[str, Argument()] = Argument("...")` 128 if isinstance(param.default, ParameterInfo): 129 raise MixedAnnotatedAndDefaultStyleError( 130 argument_name=param.name, 131 annotated_param_type=type(parameter_info), 132 default_param_type=type(param.default), 133 ) 134 135 parameter_info = copy(parameter_info) 136 137 # When used as a default, `Option` takes a default value and option names 138 # as positional arguments: 139 # `Option(some_value, "--some-argument", "-s")` 140 # When used in `Annotated` (ie, what this is handling), `Option` just takes 141 # option names as positional arguments: 142 # `Option("--some-argument", "-s")` 143 # In this case, the `default` attribute of `parameter_info` is actually 144 # meant to be the first item of `param_decls`. 145 if ( 146 isinstance(parameter_info, OptionInfo) 147 and parameter_info.default is not ... 148 ): 149 parameter_info.param_decls = ( 150 cast(str, parameter_info.default), 151 *(parameter_info.param_decls or ()), 152 ) 153 parameter_info.default = ... 154 155 # Forbid `my_param: Annotated[str, Argument('some-default')]` 156 if parameter_info.default is not ...: 157 raise AnnotatedParamWithDefaultValueError( 158 param_type=type(parameter_info), 159 argument_name=param.name, 160 ) 161 if param.default is not param.empty: 162 # Put the parameter's default (set by `=`) into `parameter_info`, where 163 # typer can find it. 164 parameter_info.default = param.default 165 166 default = parameter_info 167 elif param.name in type_hints: 168 # Resolve forward references. 169 annotation = type_hints[param.name] 170 171 if isinstance(default, ParameterInfo): 172 parameter_info = copy(default) 173 # Click supports `default` as either 174 # - an actual value; or 175 # - a factory function (returning a default value.) 176 # The two are not interchangeable for static typing, so typer allows 177 # specifying `default_factory`. Move the `default_factory` into `default` 178 # so click can find it. 179 if parameter_info.default is ... and parameter_info.default_factory: 180 parameter_info.default = parameter_info.default_factory 181 elif parameter_info.default_factory: 182 raise DefaultFactoryAndDefaultValueError( 183 argument_name=param.name, param_type=type(parameter_info) 184 ) 185 default = parameter_info 186 187 params[param.name] = ParamMeta( 188 name=param.name, default=default, annotation=annotation 189 ) 190 return params
class
AnnotatedParamWithDefaultValueError(builtins.Exception):
23class AnnotatedParamWithDefaultValueError(Exception): 24 argument_name: str 25 param_type: Type[ParameterInfo] 26 27 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 28 self.argument_name = argument_name 29 self.param_type = param_type 30 31 def __str__(self) -> str: 32 param_type_str = _param_type_to_user_string(self.param_type) 33 return ( 34 f"{param_type_str} default value cannot be set in `Annotated`" 35 f" for {self.argument_name!r}. Set the default value with `=` instead." 36 )
Common base class for all non-exit exceptions.
AnnotatedParamWithDefaultValueError(argument_name: str, param_type: Type[typer.models.ParameterInfo])
param_type: Type[typer.models.ParameterInfo]
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args
class
MixedAnnotatedAndDefaultStyleError(builtins.Exception):
39class MixedAnnotatedAndDefaultStyleError(Exception): 40 argument_name: str 41 annotated_param_type: Type[ParameterInfo] 42 default_param_type: Type[ParameterInfo] 43 44 def __init__( 45 self, 46 argument_name: str, 47 annotated_param_type: Type[ParameterInfo], 48 default_param_type: Type[ParameterInfo], 49 ): 50 self.argument_name = argument_name 51 self.annotated_param_type = annotated_param_type 52 self.default_param_type = default_param_type 53 54 def __str__(self) -> str: 55 annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) 56 default_param_type_str = _param_type_to_user_string(self.default_param_type) 57 msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" 58 if self.annotated_param_type is self.default_param_type: 59 msg += " default value" 60 else: 61 msg += f" {default_param_type_str} as a default value" 62 msg += f" together for {self.argument_name!r}" 63 return msg
Common base class for all non-exit exceptions.
MixedAnnotatedAndDefaultStyleError( argument_name: str, annotated_param_type: Type[typer.models.ParameterInfo], default_param_type: Type[typer.models.ParameterInfo])
annotated_param_type: Type[typer.models.ParameterInfo]
default_param_type: Type[typer.models.ParameterInfo]
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args
class
MultipleTyperAnnotationsError(builtins.Exception):
66class MultipleTyperAnnotationsError(Exception): 67 argument_name: str 68 69 def __init__(self, argument_name: str): 70 self.argument_name = argument_name 71 72 def __str__(self) -> str: 73 return ( 74 "Cannot specify multiple `Annotated` Typer arguments" 75 f" for {self.argument_name!r}" 76 )
Common base class for all non-exit exceptions.
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args
class
DefaultFactoryAndDefaultValueError(builtins.Exception):
79class DefaultFactoryAndDefaultValueError(Exception): 80 argument_name: str 81 param_type: Type[ParameterInfo] 82 83 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 84 self.argument_name = argument_name 85 self.param_type = param_type 86 87 def __str__(self) -> str: 88 param_type_str = _param_type_to_user_string(self.param_type) 89 return ( 90 "Cannot specify `default_factory` and a default value together" 91 f" for {param_type_str}" 92 )
Common base class for all non-exit exceptions.
DefaultFactoryAndDefaultValueError(argument_name: str, param_type: Type[typer.models.ParameterInfo])
param_type: Type[typer.models.ParameterInfo]
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args
108def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: 109 if sys.version_info >= (3, 10): 110 signature = inspect.signature(func, eval_str=True) 111 else: 112 signature = inspect.signature(func) 113 114 type_hints = get_type_hints(func) 115 params = {} 116 for param in signature.parameters.values(): 117 annotation, typer_annotations = _split_annotation_from_typer_annotations( 118 param.annotation, 119 ) 120 if len(typer_annotations) > 1: 121 raise MultipleTyperAnnotationsError(param.name) 122 123 default = param.default 124 if typer_annotations: 125 # It's something like `my_param: Annotated[str, Argument()]` 126 [parameter_info] = typer_annotations 127 128 # Forbid `my_param: Annotated[str, Argument()] = Argument("...")` 129 if isinstance(param.default, ParameterInfo): 130 raise MixedAnnotatedAndDefaultStyleError( 131 argument_name=param.name, 132 annotated_param_type=type(parameter_info), 133 default_param_type=type(param.default), 134 ) 135 136 parameter_info = copy(parameter_info) 137 138 # When used as a default, `Option` takes a default value and option names 139 # as positional arguments: 140 # `Option(some_value, "--some-argument", "-s")` 141 # When used in `Annotated` (ie, what this is handling), `Option` just takes 142 # option names as positional arguments: 143 # `Option("--some-argument", "-s")` 144 # In this case, the `default` attribute of `parameter_info` is actually 145 # meant to be the first item of `param_decls`. 146 if ( 147 isinstance(parameter_info, OptionInfo) 148 and parameter_info.default is not ... 149 ): 150 parameter_info.param_decls = ( 151 cast(str, parameter_info.default), 152 *(parameter_info.param_decls or ()), 153 ) 154 parameter_info.default = ... 155 156 # Forbid `my_param: Annotated[str, Argument('some-default')]` 157 if parameter_info.default is not ...: 158 raise AnnotatedParamWithDefaultValueError( 159 param_type=type(parameter_info), 160 argument_name=param.name, 161 ) 162 if param.default is not param.empty: 163 # Put the parameter's default (set by `=`) into `parameter_info`, where 164 # typer can find it. 165 parameter_info.default = param.default 166 167 default = parameter_info 168 elif param.name in type_hints: 169 # Resolve forward references. 170 annotation = type_hints[param.name] 171 172 if isinstance(default, ParameterInfo): 173 parameter_info = copy(default) 174 # Click supports `default` as either 175 # - an actual value; or 176 # - a factory function (returning a default value.) 177 # The two are not interchangeable for static typing, so typer allows 178 # specifying `default_factory`. Move the `default_factory` into `default` 179 # so click can find it. 180 if parameter_info.default is ... and parameter_info.default_factory: 181 parameter_info.default = parameter_info.default_factory 182 elif parameter_info.default_factory: 183 raise DefaultFactoryAndDefaultValueError( 184 argument_name=param.name, param_type=type(parameter_info) 185 ) 186 default = parameter_info 187 188 params[param.name] = ParamMeta( 189 name=param.name, default=default, annotation=annotation 190 ) 191 return params