Coverage for typer/utils.py: 100%

93 statements  

« 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

5 

6from typing_extensions import Annotated 1haebfcdg

7 

8from ._typing import get_args, get_origin 1haebfcdg

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

10 

11 

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 

22 

23 

24class AnnotatedParamWithDefaultValueError(Exception): 1haebfcdg

25 argument_name: str 1haebfcdg

26 param_type: Type[ParameterInfo] 1haebfcdg

27 

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

31 

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 ) 

38 

39 

40class MixedAnnotatedAndDefaultStyleError(Exception): 1haebfcdg

41 argument_name: str 1haebfcdg

42 annotated_param_type: Type[ParameterInfo] 1haebfcdg

43 default_param_type: Type[ParameterInfo] 1haebfcdg

44 

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

54 

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

65 

66 

67class MultipleTyperAnnotationsError(Exception): 1haebfcdg

68 argument_name: str 1haebfcdg

69 

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

71 self.argument_name = argument_name 1haebfcdg

72 

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

74 return ( 1aebfcdg

75 "Cannot specify multiple `Annotated` Typer arguments" 

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

77 ) 

78 

79 

80class DefaultFactoryAndDefaultValueError(Exception): 1haebfcdg

81 argument_name: str 1haebfcdg

82 param_type: Type[ParameterInfo] 1haebfcdg

83 

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

87 

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 ) 

94 

95 

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 ] 

107 

108 

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

114 

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

123 

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

128 

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 ) 

136 

137 parameter_info = copy(parameter_info) 1haebfcdg

138 

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

156 

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

167 

168 default = parameter_info 1haebfcdg

169 elif param.name in type_hints: 1haebfcdg

170 # Resolve forward references. 

171 annotation = type_hints[param.name] 1haebfcdg

172 

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

188 

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

190 name=param.name, default=default, annotation=annotation 

191 ) 

192 return params 1haebfcdg