Coverage for typer/utils.py: 100%
93 statements
« prev ^ index » next coverage.py v7.5.4, created at 2024-06-24 00:17 +0000
« prev ^ index » next coverage.py v7.5.4, created at 2024-06-24 00:17 +0000
1import inspect 1haebfcdg
2import sys 1haebfcdg
3from copy import copy 1haebfcdg
4from typing import Any, Callable, Dict, List, Tuple, Type, cast, get_type_hints 1haebfcdg
6from typing_extensions import Annotated 1haebfcdg
8from ._typing import get_args, get_origin 1haebfcdg
9from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta 1haebfcdg
12def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str: 1haebfcdg
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: 1haebfcdg
17 return "`Option`" 1haebfcdg
18 elif param_type is ArgumentInfo: 1haebfcdg
19 return "`Argument`" 1haebfcdg
20 # This line shouldn't be reachable during normal use.
21 return f"`{param_type.__name__}`" # pragma: no cover
24class AnnotatedParamWithDefaultValueError(Exception): 1haebfcdg
25 argument_name: str 1haebfcdg
26 param_type: Type[ParameterInfo] 1haebfcdg
28 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 1haebfcdg
29 self.argument_name = argument_name 1haebfcdg
30 self.param_type = param_type 1haebfcdg
32 def __str__(self) -> str: 1haebfcdg
33 param_type_str = _param_type_to_user_string(self.param_type) 1haebfcdg
34 return ( 1aebfcdg
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 )
40class MixedAnnotatedAndDefaultStyleError(Exception): 1haebfcdg
41 argument_name: str 1haebfcdg
42 annotated_param_type: Type[ParameterInfo] 1haebfcdg
43 default_param_type: Type[ParameterInfo] 1haebfcdg
45 def __init__( 1aebfcdg
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 1haebfcdg
52 self.annotated_param_type = annotated_param_type 1haebfcdg
53 self.default_param_type = default_param_type 1haebfcdg
55 def __str__(self) -> str: 1haebfcdg
56 annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) 1haebfcdg
57 default_param_type_str = _param_type_to_user_string(self.default_param_type) 1haebfcdg
58 msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" 1haebfcdg
59 if self.annotated_param_type is self.default_param_type: 1haebfcdg
60 msg += " default value" 1haebfcdg
61 else:
62 msg += f" {default_param_type_str} as a default value" 1haebfcdg
63 msg += f" together for {self.argument_name!r}" 1haebfcdg
64 return msg 1haebfcdg
67class MultipleTyperAnnotationsError(Exception): 1haebfcdg
68 argument_name: str 1haebfcdg
70 def __init__(self, argument_name: str): 1haebfcdg
71 self.argument_name = argument_name 1haebfcdg
73 def __str__(self) -> str: 1haebfcdg
74 return ( 1aebfcdg
75 "Cannot specify multiple `Annotated` Typer arguments"
76 f" for {self.argument_name!r}"
77 )
80class DefaultFactoryAndDefaultValueError(Exception): 1haebfcdg
81 argument_name: str 1haebfcdg
82 param_type: Type[ParameterInfo] 1haebfcdg
84 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 1haebfcdg
85 self.argument_name = argument_name 1haebfcdg
86 self.param_type = param_type 1haebfcdg
88 def __str__(self) -> str: 1haebfcdg
89 param_type_str = _param_type_to_user_string(self.param_type) 1haebfcdg
90 return ( 1aebfcdg
91 "Cannot specify `default_factory` and a default value together"
92 f" for {param_type_str}"
93 )
96def _split_annotation_from_typer_annotations( 1aebfcdg
97 base_annotation: Type[Any],
98) -> Tuple[Type[Any], List[ParameterInfo]]:
99 if get_origin(base_annotation) is not Annotated: # type: ignore 1haebfcdg
100 return base_annotation, [] 1haebfcdg
101 base_annotation, *maybe_typer_annotations = get_args(base_annotation) 1haebfcdg
102 return base_annotation, [ 1haebfcdg
103 annotation
104 for annotation in maybe_typer_annotations
105 if isinstance(annotation, ParameterInfo)
106 ]
109def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: 1haebfcdg
110 if sys.version_info >= (3, 10): 1haebfcdg
111 signature = inspect.signature(func, eval_str=True) 1aefdg
112 else:
113 signature = inspect.signature(func) 1hbc
115 type_hints = get_type_hints(func) 1haebfcdg
116 params = {} 1haebfcdg
117 for param in signature.parameters.values(): 1haebfcdg
118 annotation, typer_annotations = _split_annotation_from_typer_annotations( 1haebfcdg
119 param.annotation,
120 )
121 if len(typer_annotations) > 1: 1haebfcdg
122 raise MultipleTyperAnnotationsError(param.name) 1haebfcdg
124 default = param.default 1haebfcdg
125 if typer_annotations: 1haebfcdg
126 # It's something like `my_param: Annotated[str, Argument()]`
127 [parameter_info] = typer_annotations 1haebfcdg
129 # Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
130 if isinstance(param.default, ParameterInfo): 1haebfcdg
131 raise MixedAnnotatedAndDefaultStyleError( 1haebfcdg
132 argument_name=param.name,
133 annotated_param_type=type(parameter_info),
134 default_param_type=type(param.default),
135 )
137 parameter_info = copy(parameter_info) 1haebfcdg
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 ( 1abcd
148 isinstance(parameter_info, OptionInfo)
149 and parameter_info.default is not ...
150 ):
151 parameter_info.param_decls = ( 1aebfcdg
152 cast(str, parameter_info.default),
153 *(parameter_info.param_decls or ()),
154 )
155 parameter_info.default = ... 1haebfcdg
157 # Forbid `my_param: Annotated[str, Argument('some-default')]`
158 if parameter_info.default is not ...: 1haebfcdg
159 raise AnnotatedParamWithDefaultValueError( 1haebfcdg
160 param_type=type(parameter_info),
161 argument_name=param.name,
162 )
163 if param.default is not param.empty: 1haebfcdg
164 # Put the parameter's default (set by `=`) into `parameter_info`, where
165 # typer can find it.
166 parameter_info.default = param.default 1haebfcdg
168 default = parameter_info 1haebfcdg
169 elif param.name in type_hints: 1haebfcdg
170 # Resolve forward references.
171 annotation = type_hints[param.name] 1haebfcdg
173 if isinstance(default, ParameterInfo): 1haebfcdg
174 parameter_info = copy(default) 1haebfcdg
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: 1haebfcdg
182 parameter_info.default = parameter_info.default_factory 1haebfcdg
183 elif parameter_info.default_factory: 1haebfcdg
184 raise DefaultFactoryAndDefaultValueError( 1haebfcdg
185 argument_name=param.name, param_type=type(parameter_info)
186 )
187 default = parameter_info 1haebfcdg
189 params[param.name] = ParamMeta( 1haebfcdg
190 name=param.name, default=default, annotation=annotation
191 )
192 return params 1haebfcdg