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