Coverage for tests/test_type_conversion.py: 100%

101 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-12-19 20:29 +0000

1from enum import Enum 1abchdeifg

2from pathlib import Path 1abchdeifg

3from typing import Any, List, Optional, Tuple 1abchdeifg

4 

5import click 1abchdeifg

6import pytest 1abchdeifg

7import typer 1abchdeifg

8from typer.testing import CliRunner 1abchdeifg

9 

10from .utils import needs_py310 1abchdeifg

11 

12runner = CliRunner() 1abchdeifg

13 

14 

15def test_optional(): 1abchdeifg

16 app = typer.Typer() 1abchdeifg

17 

18 @app.command() 1abchdeifg

19 def opt(user: Optional[str] = None): 1abchdeifg

20 if user: 1abchdeifg

21 print(f"User: {user}") 1abchdeifg

22 else: 

23 print("No user") 1abchdeifg

24 

25 result = runner.invoke(app) 1abchdeifg

26 assert result.exit_code == 0 1abchdeifg

27 assert "No user" in result.output 1abchdeifg

28 

29 result = runner.invoke(app, ["--user", "Camila"]) 1abchdeifg

30 assert result.exit_code == 0 1abchdeifg

31 assert "User: Camila" in result.output 1abchdeifg

32 

33 

34@needs_py310 1abchdeifg

35def test_union_type_optional(): 1abchdeifg

36 app = typer.Typer() 1abcdefg

37 

38 @app.command() 1abcdefg

39 def opt(user: str | None = None): 1abcdefg

40 if user: 1abcdefg

41 print(f"User: {user}") 1abcdefg

42 else: 

43 print("No user") 1abcdefg

44 

45 result = runner.invoke(app) 1abcdefg

46 assert result.exit_code == 0 1abcdefg

47 assert "No user" in result.output 1abcdefg

48 

49 result = runner.invoke(app, ["--user", "Camila"]) 1abcdefg

50 assert result.exit_code == 0 1abcdefg

51 assert "User: Camila" in result.output 1abcdefg

52 

53 

54def test_optional_tuple(): 1abchdeifg

55 app = typer.Typer() 1abchdeifg

56 

57 @app.command() 1abchdeifg

58 def opt(number: Optional[Tuple[int, int]] = None): 1abchdeifg

59 if number: 1abchdeifg

60 print(f"Number: {number}") 1abchdeifg

61 else: 

62 print("No number") 1abchdeifg

63 

64 result = runner.invoke(app) 1abchdeifg

65 assert result.exit_code == 0 1abchdeifg

66 assert "No number" in result.output 1abchdeifg

67 

68 result = runner.invoke(app, ["--number", "4", "2"]) 1abchdeifg

69 assert result.exit_code == 0 1abchdeifg

70 assert "Number: (4, 2)" in result.output 1abchdeifg

71 

72 

73def test_no_type(): 1abchdeifg

74 app = typer.Typer() 1abchdeifg

75 

76 @app.command() 1abchdeifg

77 def no_type(user): 1abchdeifg

78 print(f"User: {user}") 1abchdeifg

79 

80 result = runner.invoke(app, ["Camila"]) 1abchdeifg

81 assert result.exit_code == 0 1abchdeifg

82 assert "User: Camila" in result.output 1abchdeifg

83 

84 

85class SomeEnum(Enum): 1abchdeifg

86 ONE = "one" 1abchdeifg

87 TWO = "two" 1abchdeifg

88 THREE = "three" 1abchdeifg

89 

90 

91@pytest.mark.parametrize( 1abchdeifg

92 "type_annotation", 

93 [List[Path], List[SomeEnum], List[str]], 

94) 

95def test_list_parameters_convert_to_lists(type_annotation): 1abchdeifg

96 # Lists containing objects that are converted by Click (i.e. not Path or Enum) 

97 # should not be inadvertently converted to tuples 

98 expected_element_type = type_annotation.__args__[0] 1abchdeifg

99 app = typer.Typer() 1abchdeifg

100 

101 @app.command() 1abchdeifg

102 def list_conversion(container: type_annotation): 1abchdeifg

103 assert isinstance(container, list) 1abchdeifg

104 for element in container: 1abchdeifg

105 assert isinstance(element, expected_element_type) 1abchdeifg

106 

107 result = runner.invoke(app, ["one", "two", "three"]) 1abchdeifg

108 assert result.exit_code == 0 1abchdeifg

109 

110 

111@pytest.mark.parametrize( 1abchdeifg

112 "type_annotation", 

113 [ 

114 Tuple[str, str], 

115 Tuple[str, Path], 

116 Tuple[Path, Path], 

117 Tuple[str, SomeEnum], 

118 Tuple[SomeEnum, SomeEnum], 

119 ], 

120) 

121def test_tuple_parameter_elements_are_converted_recursively(type_annotation): 1abchdeifg

122 # Tuple elements that aren't converted by Click (i.e. Path or Enum) 

123 # should be recursively converted by Typer 

124 expected_element_types = type_annotation.__args__ 1abchdeifg

125 app = typer.Typer() 1abchdeifg

126 

127 @app.command() 1abchdeifg

128 def tuple_recursive_conversion(container: type_annotation): 1abchdeifg

129 assert isinstance(container, tuple) 1abchdeifg

130 for element, expected_type in zip(container, expected_element_types): 1abchdeifg

131 assert isinstance(element, expected_type) 1abchdeifg

132 

133 result = runner.invoke(app, ["one", "two"]) 1abchdeifg

134 assert result.exit_code == 0 1abchdeifg

135 

136 

137def test_custom_parse(): 1abchdeifg

138 app = typer.Typer() 1abchdeifg

139 

140 @app.command() 1abchdeifg

141 def custom_parser( 1abchdeifg

142 hex_value: int = typer.Argument(None, parser=lambda x: int(x, 0)), 

143 ): 

144 assert hex_value == 0x56 1abchdeifg

145 

146 result = runner.invoke(app, ["0x56"]) 1abchdeifg

147 assert result.exit_code == 0 1abchdeifg

148 

149 

150def test_custom_click_type(): 1abchdeifg

151 class BaseNumberParamType(click.ParamType): 1abchdeifg

152 name = "base_integer" 1abchdeifg

153 

154 def convert( 1abchdeifg

155 self, 

156 value: Any, 

157 param: Optional[click.Parameter], 

158 ctx: Optional[click.Context], 

159 ) -> Any: 

160 return int(value, 0) 1abchdeifg

161 

162 app = typer.Typer() 1abchdeifg

163 

164 @app.command() 1abchdeifg

165 def custom_click_type( 1abchdeifg

166 hex_value: int = typer.Argument(None, click_type=BaseNumberParamType()), 

167 ): 

168 assert hex_value == 0x56 1abchdeifg

169 

170 result = runner.invoke(app, ["0x56"]) 1abchdeifg

171 assert result.exit_code == 0 1abchdeifg