Coverage for typer/utils.py: 100%

92 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-14 00:18 +0000

1import inspect 1iaebfgcdh

2import sys 1iaebfgcdh

3from copy import copy 1iaebfgcdh

4from typing import Any, Callable, Dict, List, Tuple, Type, cast 1iaebfgcdh

5 

6from ._typing import Annotated, get_args, get_origin, get_type_hints 1iaebfgcdh

7from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta 1iaebfgcdh

8 

9 

10def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str: 1iaebfgcdh

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: 1iaebfgcdh

15 return "`Option`" 1iaebfgcdh

16 elif param_type is ArgumentInfo: 1iaebfgcdh

17 return "`Argument`" 1iaebfgcdh

18 # This line shouldn't be reachable during normal use. 

19 return f"`{param_type.__name__}`" # pragma: no cover 

20 

21 

22class AnnotatedParamWithDefaultValueError(Exception): 1iaebfgcdh

23 argument_name: str 1iaebfgcdh

24 param_type: Type[ParameterInfo] 1iaebfgcdh

25 

26 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 1iaebfgcdh

27 self.argument_name = argument_name 1iaebfgcdh

28 self.param_type = param_type 1iaebfgcdh

29 

30 def __str__(self) -> str: 1iaebfgcdh

31 param_type_str = _param_type_to_user_string(self.param_type) 1iaebfgcdh

32 return ( 1aebfgcdh

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): 1iaebfgcdh

39 argument_name: str 1iaebfgcdh

40 annotated_param_type: Type[ParameterInfo] 1iaebfgcdh

41 default_param_type: Type[ParameterInfo] 1iaebfgcdh

42 

43 def __init__( 1aebfgcdh

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 1iaebfgcdh

50 self.annotated_param_type = annotated_param_type 1iaebfgcdh

51 self.default_param_type = default_param_type 1iaebfgcdh

52 

53 def __str__(self) -> str: 1iaebfgcdh

54 annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type) 1iaebfgcdh

55 default_param_type_str = _param_type_to_user_string(self.default_param_type) 1iaebfgcdh

56 msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and" 1iaebfgcdh

57 if self.annotated_param_type is self.default_param_type: 1iaebfgcdh

58 msg += " default value" 1iaebfgcdh

59 else: 

60 msg += f" {default_param_type_str} as a default value" 1iaebfgcdh

61 msg += f" together for {self.argument_name!r}" 1iaebfgcdh

62 return msg 1iaebfgcdh

63 

64 

65class MultipleTyperAnnotationsError(Exception): 1iaebfgcdh

66 argument_name: str 1iaebfgcdh

67 

68 def __init__(self, argument_name: str): 1iaebfgcdh

69 self.argument_name = argument_name 1iaebfgcdh

70 

71 def __str__(self) -> str: 1iaebfgcdh

72 return ( 1aebfgcdh

73 "Cannot specify multiple `Annotated` Typer arguments" 

74 f" for {self.argument_name!r}" 

75 ) 

76 

77 

78class DefaultFactoryAndDefaultValueError(Exception): 1iaebfgcdh

79 argument_name: str 1iaebfgcdh

80 param_type: Type[ParameterInfo] 1iaebfgcdh

81 

82 def __init__(self, argument_name: str, param_type: Type[ParameterInfo]): 1iaebfgcdh

83 self.argument_name = argument_name 1iaebfgcdh

84 self.param_type = param_type 1iaebfgcdh

85 

86 def __str__(self) -> str: 1iaebfgcdh

87 param_type_str = _param_type_to_user_string(self.param_type) 1iaebfgcdh

88 return ( 1aebfgcdh

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( 1aebfgcdh

95 base_annotation: Type[Any], 

96) -> Tuple[Type[Any], List[ParameterInfo]]: 

97 if get_origin(base_annotation) is not Annotated: 1iaebfgcdh

98 return base_annotation, [] 1iaebfgcdh

99 base_annotation, *maybe_typer_annotations = get_args(base_annotation) 1iaebfgcdh

100 return base_annotation, [ 1iaebfgcdh

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]: 1iaebfgcdh

108 if sys.version_info >= (3, 10): 1iaebfgcdh

109 signature = inspect.signature(func, eval_str=True) 1aefgdh

110 else: 

111 signature = inspect.signature(func) 1ibc

112 

113 type_hints = get_type_hints(func) 1iaebfgcdh

114 params = {} 1iaebfgcdh

115 for param in signature.parameters.values(): 1iaebfgcdh

116 annotation, typer_annotations = _split_annotation_from_typer_annotations( 1iaebfgcdh

117 param.annotation, 

118 ) 

119 if len(typer_annotations) > 1: 1iaebfgcdh

120 raise MultipleTyperAnnotationsError(param.name) 1iaebfgcdh

121 

122 default = param.default 1iaebfgcdh

123 if typer_annotations: 1iaebfgcdh

124 # It's something like `my_param: Annotated[str, Argument()]` 

125 [parameter_info] = typer_annotations 1iaebfgcdh

126 

127 # Forbid `my_param: Annotated[str, Argument()] = Argument("...")` 

128 if isinstance(param.default, ParameterInfo): 1iaebfgcdh

129 raise MixedAnnotatedAndDefaultStyleError( 1iaebfgcdh

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) 1iaebfgcdh

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 ( 1abcd

146 isinstance(parameter_info, OptionInfo) 

147 and parameter_info.default is not ... 

148 ): 

149 parameter_info.param_decls = ( 1aebfgcdh

150 cast(str, parameter_info.default), 

151 *(parameter_info.param_decls or ()), 

152 ) 

153 parameter_info.default = ... 1iaebfgcdh

154 

155 # Forbid `my_param: Annotated[str, Argument('some-default')]` 

156 if parameter_info.default is not ...: 1iaebfgcdh

157 raise AnnotatedParamWithDefaultValueError( 1iaebfgcdh

158 param_type=type(parameter_info), 

159 argument_name=param.name, 

160 ) 

161 if param.default is not param.empty: 1iaebfgcdh

162 # Put the parameter's default (set by `=`) into `parameter_info`, where 

163 # typer can find it. 

164 parameter_info.default = param.default 1iaebfgcdh

165 

166 default = parameter_info 1iaebfgcdh

167 elif param.name in type_hints: 1iaebfgcdh

168 # Resolve forward references. 

169 annotation = type_hints[param.name] 1iaebfgcdh

170 

171 if isinstance(default, ParameterInfo): 1iaebfgcdh

172 parameter_info = copy(default) 1iaebfgcdh

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: 1iaebfgcdh

180 parameter_info.default = parameter_info.default_factory 1iaebfgcdh

181 elif parameter_info.default_factory: 1iaebfgcdh

182 raise DefaultFactoryAndDefaultValueError( 1iaebfgcdh

183 argument_name=param.name, param_type=type(parameter_info) 

184 ) 

185 default = parameter_info 1iaebfgcdh

186 

187 params[param.name] = ParamMeta( 1iaebfgcdh

188 name=param.name, default=default, annotation=annotation 

189 ) 

190 return params 1iaebfgcdh