Coverage for tests / test_type_conversion.py: 100%

101 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-09 12:36 +0000

1from enum import Enum 1abchdefg

2from pathlib import Path 1abchdefg

3from typing import Any, Optional 1abchdefg

4 

5import click 1abchdefg

6import pytest 1abchdefg

7import typer 1abchdefg

8from typer.testing import CliRunner 1abchdefg

9 

10from .utils import needs_py310 1abchdefg

11 

12runner = CliRunner() 1abchdefg

13 

14 

15def test_optional(): 1abchdefg

16 app = typer.Typer() 1abchdefg

17 

18 @app.command() 1abchdefg

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

20 if user: 1abchdefg

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

22 else: 

23 print("No user") 1abchdefg

24 

25 result = runner.invoke(app) 1abchdefg

26 assert result.exit_code == 0 1abchdefg

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

28 

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

30 assert result.exit_code == 0 1abchdefg

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

32 

33 

34@needs_py310 1abchdefg

35def test_union_type_optional(): 1abchdefg

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(): 1abchdefg

55 app = typer.Typer() 1abchdefg

56 

57 @app.command() 1abchdefg

58 def opt(number: Optional[tuple[int, int]] = None): 1abchdefg

59 if number: 1abchdefg

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

61 else: 

62 print("No number") 1abchdefg

63 

64 result = runner.invoke(app) 1abchdefg

65 assert result.exit_code == 0 1abchdefg

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

67 

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

69 assert result.exit_code == 0 1abchdefg

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

71 

72 

73def test_no_type(): 1abchdefg

74 app = typer.Typer() 1abchdefg

75 

76 @app.command() 1abchdefg

77 def no_type(user): 1abchdefg

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

79 

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

81 assert result.exit_code == 0 1abchdefg

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

83 

84 

85class SomeEnum(Enum): 1abchdefg

86 ONE = "one" 1abchdefg

87 TWO = "two" 1abchdefg

88 THREE = "three" 1abchdefg

89 

90 

91@pytest.mark.parametrize( 1abchdefg

92 "type_annotation", 

93 [list[Path], list[SomeEnum], list[str]], 

94) 

95def test_list_parameters_convert_to_lists(type_annotation): 1abchdefg

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] 1abchdefg

99 app = typer.Typer() 1abchdefg

100 

101 @app.command() 1abchdefg

102 def list_conversion(container: type_annotation): 1abchdefg

103 assert isinstance(container, list) 1abchdefg

104 for element in container: 1abchdefg

105 assert isinstance(element, expected_element_type) 1abchdefg

106 

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

108 assert result.exit_code == 0 1abchdefg

109 

110 

111@pytest.mark.parametrize( 1abchdefg

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

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__ 1abchdefg

125 app = typer.Typer() 1abchdefg

126 

127 @app.command() 1abchdefg

128 def tuple_recursive_conversion(container: type_annotation): 1abchdefg

129 assert isinstance(container, tuple) 1abchdefg

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

131 assert isinstance(element, expected_type) 1abchdefg

132 

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

134 assert result.exit_code == 0 1abchdefg

135 

136 

137def test_custom_parse(): 1abchdefg

138 app = typer.Typer() 1abchdefg

139 

140 @app.command() 1abchdefg

141 def custom_parser( 1abchdefg

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

143 ): 

144 assert hex_value == 0x56 1abchdefg

145 

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

147 assert result.exit_code == 0 1abchdefg

148 

149 

150def test_custom_click_type(): 1abchdefg

151 class BaseNumberParamType(click.ParamType): 1abchdefg

152 name = "base_integer" 1abchdefg

153 

154 def convert( 1abchdefg

155 self, 

156 value: Any, 

157 param: Optional[click.Parameter], 

158 ctx: Optional[click.Context], 

159 ) -> Any: 

160 return int(value, 0) 1abchdefg

161 

162 app = typer.Typer() 1abchdefg

163 

164 @app.command() 1abchdefg

165 def custom_click_type( 1abchdefg

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

167 ): 

168 assert hex_value == 0x56 1abchdefg

169 

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

171 assert result.exit_code == 0 1abchdefg