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
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from typing import Optional
5import click
6import pytest
7import typer
8from pydantic import BaseModel
9from pydantic import Field
10from typer import Context
11from typer.models import CommandInfo
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
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
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
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
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
54 some_command_info = CommandInfo(name="some-command", callback=some_command)
55 assert get_command_help(some_command_info) == "This is a test command."
57 some_command_info.help = "Override help from docstring."
58 assert get_command_help(some_command_info) == "Override help from docstring."
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)"""
65 @click.command()
66 def some_command():
67 """This is a test command."""
68 pass
70 assert get_command_help(some_command) == "This is a test command." # type: ignore
72 some_command.help = "Override help from docstring."
73 assert get_command_help(some_command) == "Override help from docstring." # type: ignore
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")
81 @app.command(name="top-app-command")
82 def app_command() -> None:
83 """Top-level app command."""
84 pass
86 @sub_app.command(name="sub-app-command")
87 def sub_app_command() -> None:
88 """Sub-app command."""
89 pass
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
99 app.add_typer(sub_app, name="sub-app")
100 sub_app.add_typer(subsub_app) # name specified on creation above
102 commands = get_app_commands(app)
103 assert len(commands) == 3
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."
110 assert commands[1].name == "sub-app sub-sub-app sub-sub-app-command"
111 assert commands[1].help == "Sub-sub-app command."
113 assert commands[2].name == "top-app-command"
114 assert commands[2].help == "Top-level app command."
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.")
123 app = typer.Typer()
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
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."
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.")
148 app = typer.Typer()
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
159 defaults = some_command.__defaults__
160 assert defaults is None
163def test_inject_resource_options() -> None:
164 app = typer.Typer()
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
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
198def test_inject_resource_options_partial_params() -> None:
199 app = typer.Typer()
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
209 parameters = some_command.__signature__.parameters
210 assert len(parameters) == 2
211 # can test more granularly if needed
214def test_inject_resource_options_strict() -> None:
215 app = typer.Typer()
217 # Missing parameters will raise an error in strict mode
218 with pytest.raises(ValueError):
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
229def test_inject_resource_options_no_defaults() -> None:
230 app = typer.Typer()
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
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
244 # can test more granularly if needed
247# TODO: test each resource injection function individually