Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 165 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import inspect
from typing import Annotated
from typing import Annotated, get_args

import pytest
import typer
from typer.models import ArgumentInfo, OptionInfo
from typer.testing import CliRunner

from qcore import cli
Expand Down Expand Up @@ -137,3 +138,166 @@ def implicit_command(
result = runner.invoke(app, ["5", "--param2", "z"])
assert result.exit_code == 0
assert result.stdout.strip() == "Implicit 5 z"


def test_from_docstring_implicit_signature() -> None:
"""Check the rewritten signature for implicit arguments and options."""

app = typer.Typer()

@cli.from_docstring(app)
def command(
documented: int,
undocumented, # noqa: ANN001
opt: float = 1.5,
undoc_opt=2, # noqa: ANN001
) -> None:
"""Summary.

Parameters
----------
documented : int
Documented argument.
opt : float
Documented option.
"""

params = inspect.signature(command).parameters

documented = params["documented"]
assert isinstance(documented.default, ArgumentInfo)
assert documented.default.default is ...
assert documented.default.help == "Documented argument."
assert documented.annotation is int

undocumented = params["undocumented"]
assert isinstance(undocumented.default, ArgumentInfo)
assert undocumented.default.help == ""
assert undocumented.annotation is str

opt = params["opt"]
assert isinstance(opt.default, OptionInfo)
assert opt.default.default == 1.5
assert opt.default.help == "Documented option."
assert opt.annotation is float

undoc_opt = params["undoc_opt"]
assert isinstance(undoc_opt.default, OptionInfo)
assert undoc_opt.default.default == 2
assert undoc_opt.default.help == ""
assert undoc_opt.annotation is str


def test_from_docstring_implicit_runtime_behaviour() -> None:
"""Required arguments are required and option defaults are kept."""

app = typer.Typer()

@cli.from_docstring(app)
def command(value: int, scale: float = 1.5) -> None:
"""Summary."""
print(type(value).__name__, value, scale)

result = runner.invoke(app, [])
assert result.exit_code != 0

result = runner.invoke(app, ["3"])
assert result.exit_code == 0
assert result.stdout.strip() == "int 3 1.5"


def test_from_docstring_keeps_explicit_annotated_help() -> None:
"""Explicit help in Annotated metadata is not overwritten by the docstring."""

app = typer.Typer()

@cli.from_docstring(app)
def command(
explicit: Annotated[int, typer.Argument(help="Explicit help.")],
implicit: Annotated[int, typer.Option()] = 1,
) -> None:
"""Summary.

Parameters
----------
explicit : int
Docstring help.
implicit : int
Implicit help.
"""

params = inspect.signature(command).parameters
(explicit_info,) = get_args(params["explicit"].annotation)[1:]
assert explicit_info.help == "Explicit help."
(implicit_info,) = get_args(params["implicit"].annotation)[1:]
assert implicit_info.help == "Implicit help."


def test_from_docstring_command_help_short_only() -> None:
"""A docstring without a long description yields just the summary."""

app = typer.Typer()

@cli.from_docstring(app)
def command() -> None:
"""Summary only."""

assert app.registered_commands[0].help == "Summary only."


def test_from_docstring_command_help_with_long_description() -> None:
"""The long description is appended to the summary."""

app = typer.Typer()

@cli.from_docstring(app)
def command() -> None:
"""Summary.

Longer description.
"""

assert app.registered_commands[0].help == "Summary.\n\nLonger description."


def test_from_docstring_command_help_no_summary() -> None:
"""A docstring with only a parameters section gives empty command help."""

app = typer.Typer()

@cli.from_docstring(app)
def command(x: int) -> None:
"""
Parameters
----------
x : int
X help.
"""

assert app.registered_commands[0].help == ""
x = inspect.signature(command).parameters["x"]
assert isinstance(x.default, ArgumentInfo)
assert x.default.help == "X help."


def test_from_docstring_forces_numpydoc_style() -> None:
"""The docstring is parsed as numpydoc even if another style has more fields."""

app = typer.Typer()

@cli.from_docstring(app)
def command(x: int) -> None:
"""Summary.

:param a: Not a numpydoc parameter.
:param b: Not a numpydoc parameter.

Parameters
----------
x : int
X help.
"""

x = inspect.signature(command).parameters["x"]
assert isinstance(x.default, ArgumentInfo)
assert x.default.help == "X help."
Loading