Coverage for typer/utils.py: 100%
92 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-09 18:26 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-09 18:26 +0000
1import inspect 1haebfcdg
2import sys 1haebfcdg
3from copy import copy 1haebfcdg
4from typing import Any, Callable, Dict, List, Tuple, Type, cast 1haebfcdg
6from typing_extensions import Annotated, get_args, get_origin, get_type_hints 1haebfcdg
8from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta 1haebfcdg
11def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str: 1haebfcdg
12 # Render a `ParameterInfo` subclass for use in error messages.
13 # User code doesn't call `*Info` directly, so errors should present the classes how
14 # they were (probably) defined in the user code.
15 if param_type is OptionInfo: 1haebfcdg
16 return "`Option`" 1haebfcdg
17 elif param_type is ArgumentInfo: 1haebfcdg
18 return "`Argument`" 1haebfcdg
19 # This line shouldn't be reachable during normal use.
20 return f"`{param_type.__name__}`" # pragma: no cover
23class AnnotatedParamWithDefaultValueError(Exception): 1haebfcdg
24 argument_name: str 1haebfcdg
25 param_type: Type[ParameterInfo] 1haebfcdg
27 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 1haebfcdg
28 self.argument_name = argument_name 1haebfcdg
29 self.param_type = param_type 1haebfcdg
31 def __str__(self) -> str: 1haebfcdg
32 param_type_str = _param_type_to_user_string(self.param_type) 1haebfcdg
33 return ( 1aebfcdg
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 )
39class MixedAnnotatedAndDefaultStyleError(Exception): 1haebfcdg
40 argument_name: str 1haebfcdg
41 annotated_param_type: Type[ParameterInfo] 1haebfcdg
42 default_param_type: Type[ParameterInfo] 1haebfcdg
44 def __init__( 1aebfcdg
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 1haebfcdg
51 self.annotated_param_type = annotated_param_type 1haebfcdg
52 self.default_param_type = default_param_type 1haebfcdg
54 def __str__(self) -> str: 1haebfcdg
55 annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) 1haebfcdg
56 default_param_type_str = _param_type_to_user_string(self.default_param_type) 1haebfcdg
57 msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" 1haebfcdg
58 if self.annotated_param_type is self.default_param_type: 1haebfcdg
59 msg += " default value" 1haebfcdg
60 else:
61 msg += f" {default_param_type_str} as a default value" 1haebfcdg
62 msg += f" together for {self.argument_name!r}" 1haebfcdg
63 return msg 1haebfcdg
66class MultipleTyperAnnotationsError(Exception): 1haebfcdg
67 argument_name: str 1haebfcdg
69 def __init__(self, argument_name: str): 1haebfcdg
70 self.argument_name = argument_name 1haebfcdg
72 def __str__(self) -> str: 1haebfcdg
73 return ( 1aebfcdg
74 "Cannot specify multiple `Annotated` Typer arguments"
75 f" for {self.argument_name!r}"
76 )
79class DefaultFactoryAndDefaultValueError(Exception): 1haebfcdg
80 argument_name: str 1haebfcdg
81 param_type: Type[ParameterInfo] 1haebfcdg
83 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 1haebfcdg
84 self.argument_name = argument_name 1haebfcdg
85 self.param_type = param_type 1haebfcdg
87 def __str__(self) -> str: 1haebfcdg
88 param_type_str = _param_type_to_user_string(self.param_type) 1haebfcdg
89 return ( 1aebfcdg
90 "Cannot specify `default_factory` and a default value together"
91 f" for {param_type_str}"
92 )
95def _split_annotation_from_typer_annotations( 1aebfcdg
96 base_annotation: Type[Any],
97) -> Tuple[Type[Any], List[ParameterInfo]]:
98 if get_origin(base_annotation) is not Annotated: 1haebfcdg
99 return base_annotation, [] 1haebfcdg
100 base_annotation, *maybe_typer_annotations = get_args(base_annotation) 1haebfcdg
101 return base_annotation, [ 1haebfcdg
102 annotation
103 for annotation in maybe_typer_annotations
104 if isinstance(annotation, ParameterInfo)
105 ]
108def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]: 1haebfcdg
109 if sys.version_info >= (3, 10): 1haebfcdg
110 signature = inspect.signature(func, eval_str=True) 1aefdg
111 else:
112 signature = inspect.signature(func) 1hbc
114 type_hints = get_type_hints(func) 1haebfcdg
115 params = {} 1haebfcdg
116 for param in signature.parameters.values(): 1haebfcdg
117 annotation, typer_annotations = _split_annotation_from_typer_annotations( 1haebfcdg
118 param.annotation,
119 )
120 if len(typer_annotations) > 1: 1haebfcdg
121 raise MultipleTyperAnnotationsError(param.name) 1haebfcdg
123 default = param.default 1haebfcdg
124 if typer_annotations: 1haebfcdg
125 # It's something like `my_param: Annotated[str, Argument()]`
126 [parameter_info] = typer_annotations 1haebfcdg
128 # Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
129 if isinstance(param.default, ParameterInfo): 1haebfcdg
130 raise MixedAnnotatedAndDefaultStyleError( 1haebfcdg
131 argument_name=param.name,
132 annotated_param_type=type(parameter_info),
133 default_param_type=type(param.default),
134 )
136 parameter_info = copy(parameter_info) 1haebfcdg
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 ( 1abcd
147 isinstance(parameter_info, OptionInfo)
148 and parameter_info.default is not ...
149 ):
150 parameter_info.param_decls = ( 1aebfcdg
151 cast(str, parameter_info.default),
152 *(parameter_info.param_decls or ()),
153 )
154 parameter_info.default = ... 1haebfcdg
156 # Forbid `my_param: Annotated[str, Argument('some-default')]`
157 if parameter_info.default is not ...: 1haebfcdg
158 raise AnnotatedParamWithDefaultValueError( 1haebfcdg
159 param_type=type(parameter_info),
160 argument_name=param.name,
161 )
162 if param.default is not param.empty: 1haebfcdg
163 # Put the parameter's default (set by `=`) into `parameter_info`, where
164 # typer can find it.
165 parameter_info.default = param.default 1haebfcdg
167 default = parameter_info 1haebfcdg
168 elif param.name in type_hints: 1haebfcdg
169 # Resolve forward references.
170 annotation = type_hints[param.name] 1haebfcdg
172 if isinstance(default, ParameterInfo): 1haebfcdg
173 parameter_info = copy(default) 1haebfcdg
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: 1haebfcdg
181 parameter_info.default = parameter_info.default_factory 1haebfcdg
182 elif parameter_info.default_factory: 1haebfcdg
183 raise DefaultFactoryAndDefaultValueError( 1haebfcdg
184 argument_name=param.name, param_type=type(parameter_info)
185 )
186 default = parameter_info 1haebfcdg
188 params[param.name] = ParamMeta( 1haebfcdg
189 name=param.name, default=default, annotation=annotation
190 )
191 return params 1haebfcdg