Coverage for tests/test_type_conversion.py: 100%

101 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-14 00:18 +0000

1from enum import Enum 1iabgcdhef

2from pathlib import Path 1iabgcdhef

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

4 

5import click 1iabgcdhef

6import pytest 1iabgcdhef

7import typer 1iabgcdhef

8from typer.testing import CliRunner 1iabgcdhef

9 

10from .utils import needs_py310 1iabgcdhef

11 

12runner = CliRunner() 1iabgcdhef

13 

14 

15def test_optional(): 1iabgcdhef

16 app = typer.Typer() 1iabgcdhef

17 

18 @app.command() 1iabgcdhef

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

20 if user: 1iabgcdhef

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

22 else: 

23 print("No user") 1iabgcdhef

24 

25 result = runner.invoke(app) 1iabgcdhef

26 assert result.exit_code == 0 1iabgcdhef

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

28 

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

30 assert result.exit_code == 0 1iabgcdhef

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

32 

33 

34@needs_py310 1iabgcdhef

35def test_union_type_optional(): 1abgcdhef

36 app = typer.Typer() 1abcdef

37 

38 @app.command() 1abcdef

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

40 if user: 1abcdef

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

42 else: 

43 print("No user") 1abcdef

44 

45 result = runner.invoke(app) 1abcdef

46 assert result.exit_code == 0 1abcdef

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

48 

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

50 assert result.exit_code == 0 1abcdef

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

52 

53 

54def test_optional_tuple(): 1iabgcdhef

55 app = typer.Typer() 1iabgcdhef

56 

57 @app.command() 1iabgcdhef

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

59 if number: 1iabgcdhef

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

61 else: 

62 print("No number") 1iabgcdhef

63 

64 result = runner.invoke(app) 1iabgcdhef

65 assert result.exit_code == 0 1iabgcdhef

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

67 

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

69 assert result.exit_code == 0 1iabgcdhef

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

71 

72 

73def test_no_type(): 1iabgcdhef

74 app = typer.Typer() 1iabgcdhef

75 

76 @app.command() 1iabgcdhef

77 def no_type(user): 1abgcdhef

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

79 

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

81 assert result.exit_code == 0 1iabgcdhef

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

83 

84 

85class SomeEnum(Enum): 1iabgcdhef

86 ONE = "one" 1iabgcdhef

87 TWO = "two" 1iabgcdhef

88 THREE = "three" 1iabgcdhef

89 

90 

91@pytest.mark.parametrize( 1iabgcdhef

92 "type_annotation", 

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

94) 

95def test_list_parameters_convert_to_lists(type_annotation): 1abgcdhef

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

99 app = typer.Typer() 1iabgcdhef

100 

101 @app.command() 1iabgcdhef

102 def list_conversion(container: type_annotation): 1iabgcdhef

103 assert isinstance(container, list) 1iabgcdhef

104 for element in container: 1iabgcdhef

105 assert isinstance(element, expected_element_type) 1iabgcdhef

106 

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

108 assert result.exit_code == 0 1iabgcdhef

109 

110 

111@pytest.mark.parametrize( 1iabgcdhef

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

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

125 app = typer.Typer() 1iabgcdhef

126 

127 @app.command() 1iabgcdhef

128 def tuple_recursive_conversion(container: type_annotation): 1iabgcdhef

129 assert isinstance(container, tuple) 1iabgcdhef

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

131 assert isinstance(element, expected_type) 1iabgcdhef

132 

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

134 assert result.exit_code == 0 1iabgcdhef

135 

136 

137def test_custom_parse(): 1iabgcdhef

138 app = typer.Typer() 1iabgcdhef

139 

140 @app.command() 1iabgcdhef

141 def custom_parser( 1abgcdhef

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

143 ): 

144 assert hex_value == 0x56 1iabgcdhef

145 

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

147 assert result.exit_code == 0 1iabgcdhef

148 

149 

150def test_custom_click_type(): 1iabgcdhef

151 class BaseNumberParamType(click.ParamType): 1iabgcdhef

152 name = "base_integer" 1iabgcdhef

153 

154 def convert( 1abgcdhef

155 self, 

156 value: Any, 

157 param: Optional[click.Parameter], 

158 ctx: Optional[click.Context], 

159 ) -> Any: 

160 return int(value, 0) 1iabgcdhef

161 

162 app = typer.Typer() 1iabgcdhef

163 

164 @app.command() 1iabgcdhef

165 def custom_click_type( 1abgcdhef

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

167 ): 

168 assert hex_value == 0x56 1iabgcdhef

169 

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

171 assert result.exit_code == 0 1iabgcdhef