Coverage for tests/utils/test_commands.py: 100%

128 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-09 12:09 +0100

1from __future__ import annotations 

2 

3from typing import Optional 

4 

5import click 

6import pytest 

7import typer 

8from pydantic import BaseModel 

9from pydantic import Field 

10from typer import Context 

11from typer.models import CommandInfo 

12 

13from harbor_cli.utils.commands import get_app_commands 

14from harbor_cli.utils.commands import get_command_help 

15from harbor_cli.utils.commands import get_parent_ctx 

16from harbor_cli.utils.commands import inject_help 

17from harbor_cli.utils.commands import inject_resource_options 

18 

19 

20def test_get_parent_ctx() -> None: 

21 ctx_top_level = Context(click.Command(name="top-level")) 

22 ctx_mid_level = Context(click.Command(name="mid-level")) 

23 ctx_bot_level = Context(click.Command(name="bot-level")) 

24 ctx_mid_level.parent = ctx_top_level 

25 ctx_bot_level.parent = ctx_mid_level 

26 assert get_parent_ctx(ctx_bot_level) == ctx_top_level 

27 assert get_parent_ctx(ctx_mid_level) == ctx_top_level 

28 assert get_parent_ctx(ctx_top_level) == ctx_top_level 

29 

30 

31def test_get_parent_ctx_top_level() -> None: 

32 ctx_top_level = Context(click.Command(name="top-level")) 

33 assert get_parent_ctx(ctx_top_level) == ctx_top_level 

34 

35 

36def test_get_parent_ctx_insane_nesting() -> None: 

37 ctx = Context(click.Command(name="top-level")) 

38 prev = ctx 

39 for i in range(100): 

40 c = Context(click.Command(name=f"level-{i}")) 

41 c.parent = prev 

42 prev = c 

43 assert get_parent_ctx(c) == ctx 

44 

45 

46def test_get_command_help_typer() -> None: 

47 # Typer does some magic in its decorator, so we can't just add the @app.command 

48 # decorator and pass the function to get_command_help. 

49 # Instead we mock a CommandInfo object with this function as the callback. 

50 def some_command(): 

51 """This is a test command.""" 

52 pass 

53 

54 some_command_info = CommandInfo(name="some-command", callback=some_command) 

55 assert get_command_help(some_command_info) == "This is a test command." 

56 

57 some_command_info.help = "Override help from docstring." 

58 assert get_command_help(some_command_info) == "Override help from docstring." 

59 

60 

61def test_get_command_help_click() -> None: 

62 """Test get_command_help using a click Command object (not intended behavior, 

63 but it has the same interface as typer.models.CommandInfo)""" 

64 

65 @click.command() 

66 def some_command(): 

67 """This is a test command.""" 

68 pass 

69 

70 assert get_command_help(some_command) == "This is a test command." # type: ignore 

71 

72 some_command.help = "Override help from docstring." 

73 assert get_command_help(some_command) == "Override help from docstring." # type: ignore 

74 

75 

76def test_get_app_commands() -> None: 

77 app = typer.Typer(name="top-app") 

78 sub_app = typer.Typer() # name specified when adding to app below 

79 subsub_app = typer.Typer(name="sub-sub-app") 

80 

81 @app.command(name="top-app-command") 

82 def app_command() -> None: 

83 """Top-level app command.""" 

84 pass 

85 

86 @sub_app.command(name="sub-app-command") 

87 def sub_app_command() -> None: 

88 """Sub-app command.""" 

89 pass 

90 

91 # Help text in decorator 

92 @subsub_app.command(name="sub-sub-app-command", help="Sub-sub-app command.") 

93 def sub_sub_app_command( 

94 argument: str = typer.Argument(..., help="Positional argument"), 

95 option: Optional[int] = typer.Option(None, "-o", "--option", help="Option"), 

96 ) -> None: 

97 pass 

98 

99 app.add_typer(sub_app, name="sub-app") 

100 sub_app.add_typer(subsub_app) # name specified on creation above 

101 

102 commands = get_app_commands(app) 

103 assert len(commands) == 3 

104 

105 # The commands are returned in alphabetical order 

106 # TODO: test command signature + params when added to CommandSummary 

107 assert commands[0].name == "sub-app sub-app-command" 

108 assert commands[0].help == "Sub-app command." 

109 

110 assert commands[1].name == "sub-app sub-sub-app sub-sub-app-command" 

111 assert commands[1].help == "Sub-sub-app command." 

112 

113 assert commands[2].name == "top-app-command" 

114 assert commands[2].help == "Top-level app command." 

115 

116 

117def test_inject_help() -> None: 

118 class TestModel(BaseModel): 

119 field1: str = Field(..., description="This is field 1.") 

120 field2: int = Field(..., description="This is field 2.") 

121 field3: bool = Field(..., description="This is field 3.") 

122 

123 app = typer.Typer() 

124 

125 @app.command(name="some-command") 

126 @inject_help(TestModel) 

127 def some_command( 

128 field1: str = typer.Option(...), 

129 field2: int = typer.Option(...), 

130 field3: bool = typer.Option(...), 

131 ) -> None: 

132 pass 

133 

134 defaults = some_command.__defaults__ 

135 assert defaults is not None 

136 assert len(defaults) == 3 

137 assert defaults[0].help == "This is field 1." 

138 assert defaults[1].help == "This is field 2." 

139 assert defaults[2].help == "This is field 3." 

140 

141 

142def test_inject_help_no_defaults() -> None: 

143 class TestModel(BaseModel): 

144 field1: str = Field(..., description="This is field 1.") 

145 field2: int = Field(..., description="This is field 2.") 

146 field3: bool = Field(..., description="This is field 3.") 

147 

148 app = typer.Typer() 

149 

150 @app.command(name="some-command") 

151 @inject_help(TestModel) 

152 def some_command( 

153 field1: str, 

154 field2: int, 

155 field3: bool, 

156 ) -> None: 

157 pass 

158 

159 defaults = some_command.__defaults__ 

160 assert defaults is None 

161 

162 

163def test_inject_resource_options() -> None: 

164 app = typer.Typer() 

165 

166 @app.command(name="some-command") 

167 @inject_resource_options 

168 def some_command( 

169 query: Optional[str], 

170 sort: Optional[str], 

171 page: int, 

172 # with parameter default 

173 page_size: int = typer.Option(123), 

174 # ellipsis signifies that we should inject the default 

175 limit: Optional[int] = ..., 

176 ) -> None: 

177 pass 

178 

179 parameters = some_command.__signature__.parameters 

180 assert len(parameters) == 5 

181 assert parameters["query"].default.help == "Query parameters to filter the results." 

182 assert ( 

183 parameters["sort"].default.help 

184 == "Sorting order of the results. Example: [green]'name,-id'[/] to sort by name ascending and id descending." 

185 ) 

186 assert parameters["page"].default.help == "(Advanced) Page to begin fetching from." 

187 assert ( 

188 parameters["page_size"].default.help 

189 == "(Advanced) Number of results to fetch per API call." 

190 ) 

191 assert parameters["page_size"].default.default == 123 

192 assert parameters["limit"].default.help == "Maximum number of results to fetch." 

193 assert ( 

194 parameters["limit"].default.default is None 

195 ) # ellipsis means default (None in this case) is injected 

196 

197 

198def test_inject_resource_options_partial_params() -> None: 

199 app = typer.Typer() 

200 

201 @app.command(name="some-command") 

202 @inject_resource_options 

203 def some_command( 

204 query: Optional[str], 

205 page_size: int = typer.Option(123), 

206 ) -> None: 

207 pass 

208 

209 parameters = some_command.__signature__.parameters 

210 assert len(parameters) == 2 

211 # can test more granularly if needed 

212 

213 

214def test_inject_resource_options_strict() -> None: 

215 app = typer.Typer() 

216 

217 # Missing parameters will raise an error in strict mode 

218 with pytest.raises(ValueError): 

219 

220 @app.command(name="some-command") 

221 @inject_resource_options(strict=True) 

222 def some_command( 

223 query: Optional[str], 

224 page_size: int = typer.Option(123), 

225 ) -> None: 

226 pass 

227 

228 

229def test_inject_resource_options_no_defaults() -> None: 

230 app = typer.Typer() 

231 

232 @app.command(name="some-command") 

233 @inject_resource_options(use_defaults=False) 

234 def some_command( 

235 page_size: int = typer.Option(123), 

236 ) -> None: 

237 pass 

238 

239 parameters = some_command.__signature__.parameters 

240 assert len(parameters) == 1 

241 assert parameters["page_size"].default.default != 123 

242 assert parameters["page_size"].default.default == 10 

243 

244 # can test more granularly if needed 

245 

246 

247# TODO: test each resource injection function individually