Coverage for tests / test_type_conversion.py: 100%

99 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-26 21:46 +0000

1from enum import Enum 1abcdefg

2from pathlib import Path 1abcdefg

3from typing import Any 1abcdefg

4 

5import click 1abcdefg

6import pytest 1abcdefg

7import typer 1abcdefg

8from typer.testing import CliRunner 1abcdefg

9 

10runner = CliRunner() 1abcdefg

11 

12 

13def test_optional(): 1abcdefg

14 app = typer.Typer() 1abcdefg

15 

16 @app.command() 1abcdefg

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

18 if user: 1abcdefg

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

20 else: 

21 print("No user") 1abcdefg

22 

23 result = runner.invoke(app) 1abcdefg

24 assert result.exit_code == 0 1abcdefg

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

26 

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

28 assert result.exit_code == 0 1abcdefg

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

30 

31 

32def test_union_type_optional(): 1abcdefg

33 app = typer.Typer() 1abcdefg

34 

35 @app.command() 1abcdefg

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

37 if user: 1abcdefg

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

39 else: 

40 print("No user") 1abcdefg

41 

42 result = runner.invoke(app) 1abcdefg

43 assert result.exit_code == 0 1abcdefg

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

45 

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

47 assert result.exit_code == 0 1abcdefg

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

49 

50 

51def test_optional_tuple(): 1abcdefg

52 app = typer.Typer() 1abcdefg

53 

54 @app.command() 1abcdefg

55 def opt(number: tuple[int, int] | None = None): 1abcdefg

56 if number: 1abcdefg

57 print(f"Number: {number}") 1abcdefg

58 else: 

59 print("No number") 1abcdefg

60 

61 result = runner.invoke(app) 1abcdefg

62 assert result.exit_code == 0 1abcdefg

63 assert "No number" in result.output 1abcdefg

64 

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

66 assert result.exit_code == 0 1abcdefg

67 assert "Number: (4, 2)" in result.output 1abcdefg

68 

69 

70def test_no_type(): 1abcdefg

71 app = typer.Typer() 1abcdefg

72 

73 @app.command() 1abcdefg

74 def no_type(user): 1abcdefg

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

76 

77 result = runner.invoke(app, ["Camila"]) 1abcdefg

78 assert result.exit_code == 0 1abcdefg

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

80 

81 

82class SomeEnum(Enum): 1abcdefg

83 ONE = "one" 1abcdefg

84 TWO = "two" 1abcdefg

85 THREE = "three" 1abcdefg

86 

87 

88@pytest.mark.parametrize( 1abcdefg

89 "type_annotation", 

90 [list[Path], list[SomeEnum], list[str]], 

91) 

92def test_list_parameters_convert_to_lists(type_annotation): 1abcdefg

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

94 # should not be inadvertently converted to tuples 

95 expected_element_type = type_annotation.__args__[0] 1abcdefg

96 app = typer.Typer() 1abcdefg

97 

98 @app.command() 1abcdefg

99 def list_conversion(container: type_annotation): 1abcdefg

100 assert isinstance(container, list) 1abcdefg

101 for element in container: 1abcdefg

102 assert isinstance(element, expected_element_type) 1abcdefg

103 

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

105 assert result.exit_code == 0 1abcdefg

106 

107 

108@pytest.mark.parametrize( 1abcdefg

109 "type_annotation", 

110 [ 

111 tuple[str, str], 

112 tuple[str, Path], 

113 tuple[Path, Path], 

114 tuple[str, SomeEnum], 

115 tuple[SomeEnum, SomeEnum], 

116 ], 

117) 

118def test_tuple_parameter_elements_are_converted_recursively(type_annotation): 1abcdefg

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

120 # should be recursively converted by Typer 

121 expected_element_types = type_annotation.__args__ 1abcdefg

122 app = typer.Typer() 1abcdefg

123 

124 @app.command() 1abcdefg

125 def tuple_recursive_conversion(container: type_annotation): 1abcdefg

126 assert isinstance(container, tuple) 1abcdefg

127 for element, expected_type in zip( 1abcdefg

128 container, expected_element_types, strict=True 

129 ): 

130 assert isinstance(element, expected_type) 1abcdefg

131 

132 result = runner.invoke(app, ["one", "two"]) 1abcdefg

133 assert result.exit_code == 0 1abcdefg

134 

135 

136def test_custom_parse(): 1abcdefg

137 app = typer.Typer() 1abcdefg

138 

139 @app.command() 1abcdefg

140 def custom_parser( 1abcdefg

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

142 ): 

143 assert hex_value == 0x56 1abcdefg

144 

145 result = runner.invoke(app, ["0x56"]) 1abcdefg

146 assert result.exit_code == 0 1abcdefg

147 

148 

149def test_custom_click_type(): 1abcdefg

150 class BaseNumberParamType(click.ParamType): 1abcdefg

151 name = "base_integer" 1abcdefg

152 

153 def convert( 1abcdefg

154 self, 

155 value: Any, 

156 param: click.Parameter | None, 

157 ctx: click.Context | None, 

158 ) -> Any: 

159 return int(value, 0) 1abcdefg

160 

161 app = typer.Typer() 1abcdefg

162 

163 @app.command() 1abcdefg

164 def custom_click_type( 1abcdefg

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

166 ): 

167 assert hex_value == 0x56 1abcdefg

168 

169 result = runner.invoke(app, ["0x56"]) 1abcdefg

170 assert result.exit_code == 0 1abcdefg