Update cli

main
mat ess 2023-08-13 14:54:11 -04:00
parent f192f1f1f3
commit 18224b1ea7
6 changed files with 156 additions and 34 deletions

View File

@ -26,10 +26,9 @@
pre-commit.settings.hooks = { pre-commit.settings.hooks = {
autoflake.enable = true; autoflake.enable = true;
black.enable = true; black.enable = true;
mypy.enable = true; # mypy.enable = true;
ruff.enable = true; ruff.enable = true;
}; };
pre-commit.settings.settings.mypy.binPath = "${pkgs.python311Packages.mypy}/bin/mypy";
packages.default = package; packages.default = package;
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
shellHook = '' shellHook = ''

View File

@ -58,7 +58,7 @@ python = ["3.11"]
[tool.hatch.envs.typing] [tool.hatch.envs.typing]
extra-dependencies = ["mypy"] extra-dependencies = ["mypy"]
[tool.hatch.envs.typing.scripts] [tool.hatch.envs.typing.scripts]
check = "mypy --install-types {args:roll tests}" check = "mypy --install-types --non-interactive {args:roll tests}"
[tool.hatch.envs.lint] [tool.hatch.envs.lint]
detached = true detached = true

View File

@ -1,50 +1,92 @@
from typing import Literal
import click import click
from roll.__about__ import __version__ from roll.__about__ import __version__
from roll.cli.roll_param import ROLL from roll.cli.roll_param import ROLL
from roll.roll import Roll from roll.roll import Roll
from roll.throw import Throw
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
@click.group( @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
context_settings={"help_option_names": ["-h", "--help"]},
invoke_without_command=True,
)
@click.version_option(version=__version__, prog_name="roll") @click.version_option(version=__version__, prog_name="roll")
@click.argument("rolls", nargs=-1, type=ROLL) @click.argument("roll", type=ROLL, default="1d20")
def roll(rolls: list[Roll]): def cli(roll: Roll | Literal["advantage"] | Literal["disadvantage"]) -> None:
"""Throw each roll specified in ROLLS and print the results. """throw a ROLL and print the results
Rolls are specified as ROLL is specified as
DdS[(+|-)M] DdS[(+|-)M]
where D = # of dice, S = sides per die, and M = optional modifier. where D = # of dice, S = sides per die, and M = optional modifier
Example usage: if no ROLL is provided, a 1d20 is thrown
instead of a ROLL specifier, you can use a substring of "advantage"
or "disadvantage" to throw 2d20 and take the appropriate result
example usage:
\b \b
$ roll 2d20+3 3d6-1 $ roll 2d20+3
rolling 2d20+3: rolling 2d20+3:
1: | 2 1: | 2
2: | 13 2: | 13
mod: +3 mod: +3
total: 18 total: 18
\b \b
rolling 3d6-1: $ roll
1: | 3 rolling 1d20:
2: | 3 1: | 11
3: | 1 total: 11
mod: -1 \b
total: 6 $ roll advantage
rolling 2d20 with advantage:
1: | 13
1: | 2
total: 13
\b
$ roll dis
rolling 2d20 with disadvantage:
1: | 1
2: | 19
total: 1
critical miss!
""" """
for roll in rolls: if isinstance(roll, str):
click.echo() throw = _advantage(roll)
click.echo(f"rolling {roll.to_str()}:") else:
throw = _throw(roll)
click.echo(f"total:\t{throw.total: >5}")
if throw.is_critical_hit: # pragma: no cover (random)
click.echo("critical hit!")
elif throw.is_critical_miss: # pragma: no cover (random)
click.echo("critical miss!")
def _advantage(advantage: str) -> Throw:
"""print the results of an advantage or disadvantage roll"""
click.echo(f"throwing 2d20 with {advantage}:")
roll = Roll()
fst = roll.throw()
snd = roll.throw()
click.echo(f"1:\t|{fst.total: >4}")
click.echo(f"2:\t|{snd.total: >4}")
if advantage == "advantage":
throw = fst if fst.total > snd.total else snd
else:
throw = fst if fst.total < snd.total else snd
return throw
def _throw(roll: Roll) -> Throw:
"""print the results of a roll"""
click.echo(f"throwing {roll.to_str()}:")
throw = roll.throw() throw = roll.throw()
for i, result in enumerate(throw.results): for i, result in enumerate(throw.results, start=1):
click.echo(f"{i + 1}:\t| {result: >3}") click.echo(f"{i}:\t|{result: >4}")
if roll.modifier: if roll.modifier is not None:
mod = roll.modifier_str() click.echo(f"mod:\t{roll.modifier_str(): >5}")
click.echo(f"mod:\t {mod: >4}") return throw
click.echo(f"total:\t {throw.total: >3}")

View File

@ -1,3 +1,5 @@
from typing import Literal
import click import click
from roll.roll import Roll from roll.roll import Roll
@ -6,14 +8,20 @@ from roll.roll import Roll
class RollParam(click.ParamType): class RollParam(click.ParamType):
name = "roll" name = "roll"
def convert(self, value: str | Roll, param: click.Parameter | None, ctx: click.Context | None) -> Roll: def convert(
"""Parse a Roll from a command line string.""" self, value: str | Roll, param: click.Parameter | None, ctx: click.Context | None
) -> Roll | Literal["advantage"] | Literal["disadvantage"]:
"""parse a Roll from a command line string"""
if isinstance(value, Roll): if isinstance(value, Roll):
return value return value
elif "advantage".startswith(value):
return "advantage"
elif "disadvantage".startswith(value):
return "disadvantage"
try: try:
return Roll.from_str(value) return Roll.from_str(value)
except Exception as e: except Exception as e:
self.fail(f"invalid roll: {value!r}, caused by {e}", param, ctx) self.fail(f"{e}", param, ctx)
ROLL = RollParam() ROLL = RollParam()

35
tests/cli/cli_test.py Normal file
View File

@ -0,0 +1,35 @@
import pytest
from click.testing import CliRunner
from roll.cli import cli
from roll.roll import Roll
@pytest.fixture
def runner() -> CliRunner:
return CliRunner()
def test_cli_smoke(runner: CliRunner):
result = runner.invoke(cli, [])
assert result.exit_code == 0
assert result.output.startswith("throwing 1d20:\n1:\t|")
assert "total" in result.output
@pytest.mark.parametrize("value", ["2d20", "4d6-2", "9d100+3"])
def test_cli_roll(runner: CliRunner, value: str):
result = runner.invoke(cli, [value])
roll = Roll.from_str(value)
assert result.exit_code == 0
assert result.output.startswith(f"throwing {value}:\n1:\t|")
assert len(result.output.splitlines()) >= roll.dice_count + 2
assert "total" in result.output
@pytest.mark.parametrize("advantage", ["advantage", "disadvantage"])
def test_cli_advantage(runner: CliRunner, advantage: str):
result = runner.invoke(cli, [advantage])
assert result.exit_code == 0
assert result.output.startswith(f"throwing 2d20 with {advantage}:\n1:\t|")
assert "total" in result.output

View File

@ -0,0 +1,38 @@
import click
import pytest
from roll.cli.roll_param import RollParam
from roll.roll import Roll
def test_roll_param_passthrough():
roll = Roll()
result = RollParam().convert(roll, None, None)
assert result == roll
@pytest.mark.parametrize("value", ["1d20", "2d20+3", "4d6-3"])
def test_roll_param_valid(value):
result = RollParam().convert(value, None, None)
assert isinstance(result, Roll)
assert result.to_str() == value
@pytest.mark.parametrize("value", ["advantage", "disadvantage"])
def test_roll_param_advantage(value):
result = RollParam().convert(value, None, None)
assert result == value
@pytest.mark.parametrize("value", ["d90", "0d", "d-1", "aba", "000", "1d1d1d"])
def test_roll_param_invalid(value):
with pytest.raises(click.exceptions.BadParameter) as e:
RollParam().convert(value, None, None)
assert value in e.value.message
@pytest.mark.parametrize("value", ["0d0", "0d20", "1d0", "1d1"])
def test_roll_param_invalid_properties(value):
with pytest.raises(click.exceptions.BadParameter) as e:
RollParam().convert(value, None, None)
assert "sides" in e.value.message or "dice" in e.value.message