Coverage for typer/utils.py: 100%

92 statements  

« 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

5 

6from typing_extensions import Annotated, get_args, get_origin, get_type_hints 1haebfcdg

7 

8from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta 1haebfcdg

9 

10 

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 

21 

22 

23class AnnotatedParamWithDefaultValueError(Exception): 1haebfcdg

24 argument_name: str 1haebfcdg

25 param_type: Type[ParameterInfo] 1haebfcdg

26 

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

30 

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 ) 

37 

38 

39class MixedAnnotatedAndDefaultStyleError(Exception): 1haebfcdg

40 argument_name: str 1haebfcdg

41 annotated_param_type: Type[ParameterInfo] 1haebfcdg

42 default_param_type: Type[ParameterInfo] 1haebfcdg

43 

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

53 

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

64 

65 

66class MultipleTyperAnnotationsError(Exception): 1haebfcdg

67 argument_name: str 1haebfcdg

68 

69 def __init__(self, argument_name: str): 1haebfcdg

70 self.argument_name = argument_name 1haebfcdg

71 

72 def __str__(self) -> str: 1haebfcdg

73 return ( 1aebfcdg

74 "Cannot specify multiple `Annotated` Typer arguments" 

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

76 ) 

77 

78 

79class DefaultFactoryAndDefaultValueError(Exception): 1haebfcdg

80 argument_name: str 1haebfcdg

81 param_type: Type[ParameterInfo] 1haebfcdg

82 

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

86 

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 ) 

93 

94 

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 ] 

106 

107 

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

113 

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

122 

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

127 

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 ) 

135 

136 parameter_info = copy(parameter_info) 1haebfcdg

137 

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

155 

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

166 

167 default = parameter_info 1haebfcdg

168 elif param.name in type_hints: 1haebfcdg

169 # Resolve forward references. 

170 annotation = type_hints[param.name] 1haebfcdg

171 

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

187 

188 params[param.name] = ParamMeta( 1haebfcdg

189 name=param.name, default=default, annotation=annotation 

190 ) 

191 return params 1haebfcdg