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 = {
autoflake.enable = true;
black.enable = true;
mypy.enable = true;
# mypy.enable = true;
ruff.enable = true;
};
pre-commit.settings.settings.mypy.binPath = "${pkgs.python311Packages.mypy}/bin/mypy";
packages.default = package;
devShells.default = pkgs.mkShell {
shellHook = ''

View File

@ -58,7 +58,7 @@ python = ["3.11"]
[tool.hatch.envs.typing]
extra-dependencies = ["mypy"]
[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]
detached = true

View File

@ -1,50 +1,92 @@
from typing import Literal
import click
from roll.__about__ import __version__
from roll.cli.roll_param import ROLL
from roll.roll import Roll
from roll.throw import Throw
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
@click.group(
context_settings={"help_option_names": ["-h", "--help"]},
invoke_without_command=True,
)
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
@click.version_option(version=__version__, prog_name="roll")
@click.argument("rolls", nargs=-1, type=ROLL)
def roll(rolls: list[Roll]):
"""Throw each roll specified in ROLLS and print the results.
@click.argument("roll", type=ROLL, default="1d20")
def cli(roll: Roll | Literal["advantage"] | Literal["disadvantage"]) -> None:
"""throw a ROLL and print the results
Rolls are specified as
ROLL is specified as
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
$ roll 2d20+3 3d6-1
$ roll 2d20+3
rolling 2d20+3:
1: | 2
2: | 13
mod: +3
total: 18
\b
rolling 3d6-1:
1: | 3
2: | 3
3: | 1
mod: -1
total: 6
$ roll
rolling 1d20:
1: | 11
total: 11
\b
$ 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:
click.echo()
click.echo(f"rolling {roll.to_str()}:")
throw = roll.throw()
for i, result in enumerate(throw.results):
click.echo(f"{i + 1}:\t| {result: >3}")
if roll.modifier:
mod = roll.modifier_str()
click.echo(f"mod:\t {mod: >4}")
click.echo(f"total:\t {throw.total: >3}")
if isinstance(roll, str):
throw = _advantage(roll)
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()
for i, result in enumerate(throw.results, start=1):
click.echo(f"{i}:\t|{result: >4}")
if roll.modifier is not None:
click.echo(f"mod:\t{roll.modifier_str(): >5}")
return throw

View File

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