Coverage for typer / utils.py: 100%
99 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
1import inspect 1aecfbdg
2from collections.abc import Callable 1aecfbdg
3from copy import copy 1aecfbdg
4from typing import Any, cast 1aecfbdg
6from ._typing import Annotated, get_args, get_origin, get_type_hints 1aecfbdg
7from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta 1aecfbdg
10def _param_type_to_user_string(param_type: type[ParameterInfo]) -> str: 1aecfbdg
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: 1aecfbdg
15 return "`Option`" 1aecfbdg
16 elif param_type is ArgumentInfo: 1aecfbdg
17 return "`Argument`" 1aecfbdg
18 # This line shouldn't be reachable during normal use.
19 return f"`{param_type.__name__}`" # pragma: no cover
22class AnnotatedParamWithDefaultValueError(Exception): 1aecfbdg
23 argument_name: str 1acbd
24 param_type: type[ParameterInfo] 1acbd
26 def __init__(self, argument_name: str, param_type: type[ParameterInfo]): 1aecfbdg
27 self.argument_name = argument_name 1aecfbdg
28 self.param_type = param_type 1aecfbdg
30 def __str__(self) -> str: 1aecfbdg
31 param_type_str = _param_type_to_user_string(self.param_type) 1aecfbdg
32 return ( 1aecfbdg
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 )
38class MixedAnnotatedAndDefaultStyleError(Exception): 1aecfbdg
39 argument_name: str 1acbd
40 annotated_param_type: type[ParameterInfo] 1acbd
41 default_param_type: type[ParameterInfo] 1acbd
43 def __init__( 1aecfbdg
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 1aecfbdg
50 self.annotated_param_type = annotated_param_type 1aecfbdg
51 self.default_param_type = default_param_type 1aecfbdg
53 def __str__(self) -> str: 1aecfbdg
54 annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) 1aecfbdg
55 default_param_type_str = _param_type_to_user_string(self.default_param_type) 1aecfbdg
56 msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" 1aecfbdg
57 if self.annotated_param_type is self.default_param_type: 1aecfbdg
58 msg += " default value" 1aecfbdg
59 else:
60 msg += f" {default_param_type_str} as a default value" 1aecfbdg
61 msg += f" together for {self.argument_name!r}" 1aecfbdg
62 return msg 1aecfbdg
65class MultipleTyperAnnotationsError(Exception): 1aecfbdg
66 argument_name: str 1acbd
68 def __init__(self, argument_name: str): 1aecfbdg
69 self.argument_name = argument_name 1aecfbdg
71 def __str__(self) -> str: 1aecfbdg
72 return ( 1aecfbdg
73 "Cannot specify multiple `Annotated` Typer arguments"
74 f" for {self.argument_name!r}"
75 )
78class DefaultFactoryAndDefaultValueError(Exception): 1aecfbdg
79 argument_name: str 1acbd
80 param_type: type[ParameterInfo] 1acbd
82 def __init__(self, argument_name: str, param_type: type[ParameterInfo]): 1aecfbdg
83 self.argument_name = argument_name 1aecfbdg
84 self.param_type = param_type 1aecfbdg
86 def __str__(self) -> str: 1aecfbdg
87 param_type_str = _param_type_to_user_string(self.param_type) 1aecfbdg
88 return ( 1aecfbdg
89 "Cannot specify `default_factory` and a default value together"
90 f" for {param_type_str}"
91 )
94def _split_annotation_from_typer_annotations( 1aecfbdg
95 base_annotation: type[Any],
96) -> tuple[type[Any], list[ParameterInfo]]:
97 if get_origin(base_annotation) is not Annotated: 1aecfbdg
98 return base_annotation, [] 1aecfbdg
99 base_annotation, *maybe_typer_annotations = get_args(base_annotation) 1aecfbdg
100 return base_annotation, [ 1aecfbdg
101 annotation
102 for annotation in maybe_typer_annotations
103 if isinstance(annotation, ParameterInfo)
104 ]
107def get_params_from_function(func: Callable[..., Any]) -> dict[str, ParamMeta]: 1aecfbdg
108 signature = inspect.signature(func, eval_str=True) 1aecfbdg
109 type_hints = get_type_hints(func) 1aecfbdg
110 params = {} 1aecfbdg
111 for param in signature.parameters.values(): 1aecfbdg
112 annotation, typer_annotations = _split_annotation_from_typer_annotations( 1aecfbdg
113 param.annotation,
114 )
115 if len(typer_annotations) > 1: 1aecfbdg
116 raise MultipleTyperAnnotationsError(param.name) 1aecfbdg
118 default = param.default 1aecfbdg
119 if typer_annotations: 1aecfbdg
120 # It's something like `my_param: Annotated[str, Argument()]`
121 [parameter_info] = typer_annotations 1aecfbdg
123 # Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
124 if isinstance(param.default, ParameterInfo): 1aecfbdg
125 raise MixedAnnotatedAndDefaultStyleError( 1aecfbdg
126 argument_name=param.name,
127 annotated_param_type=type(parameter_info),
128 default_param_type=type(param.default),
129 )
131 parameter_info = copy(parameter_info) 1aecfbdg
133 # When used as a default, `Option` takes a default value and option names
134 # as positional arguments:
135 # `Option(some_value, "--some-argument", "-s")`
136 # When used in `Annotated` (ie, what this is handling), `Option` just takes
137 # option names as positional arguments:
138 # `Option("--some-argument", "-s")`
139 # In this case, the `default` attribute of `parameter_info` is actually
140 # meant to be the first item of `param_decls`.
141 if ( 1ab
142 isinstance(parameter_info, OptionInfo)
143 and parameter_info.default is not ...
144 ):
145 parameter_info.param_decls = ( 1aecfbdg
146 cast(str, parameter_info.default),
147 *(parameter_info.param_decls or ()),
148 )
149 parameter_info.default = ... 1aecfbdg
151 # Forbid `my_param: Annotated[str, Argument('some-default')]`
152 if parameter_info.default is not ...: 1aecfbdg
153 raise AnnotatedParamWithDefaultValueError( 1aecfbdg
154 param_type=type(parameter_info),
155 argument_name=param.name,
156 )
157 if param.default is not param.empty: 1aecfbdg
158 # Put the parameter's default (set by `=`) into `parameter_info`, where
159 # typer can find it.
160 parameter_info.default = param.default 1aecfbdg
162 default = parameter_info 1aecfbdg
163 elif param.name in type_hints: 1aecfbdg
164 # Resolve forward references.
165 annotation = type_hints[param.name] 1aecfbdg
167 if isinstance(default, ParameterInfo): 1aecfbdg
168 parameter_info = copy(default) 1aecfbdg
169 # Click supports `default` as either
170 # - an actual value; or
171 # - a factory function (returning a default value.)
172 # The two are not interchangeable for static typing, so typer allows
173 # specifying `default_factory`. Move the `default_factory` into `default`
174 # so click can find it.
175 if parameter_info.default is ... and parameter_info.default_factory: 1aecfbdg
176 parameter_info.default = parameter_info.default_factory 1aecfbdg
177 elif parameter_info.default_factory: 1aecfbdg
178 raise DefaultFactoryAndDefaultValueError( 1aecfbdg
179 argument_name=param.name, param_type=type(parameter_info)
180 )
181 default = parameter_info 1aecfbdg
183 params[param.name] = ParamMeta( 1aecfbdg
184 name=param.name, default=default, annotation=annotation
185 )
186 return params 1aecfbdg
189def parse_boolean_env_var(env_var_value: str | None, default: bool) -> bool: 1aecfbdg
190 if env_var_value is None: 1aecfbdg
191 return default 1aecfbdg
192 value = env_var_value.lower() 1aecfbdg
193 if value in ("y", "yes", "t", "true", "on", "1"): 1aecfbdg
194 return True 1aecfbdg
195 if value in ("n", "no", "f", "false", "off", "0"): 1aecfbdg
196 return False 1aecfbdg
197 return default 1aecfbdg