Skip to content
Open
Show file tree
Hide file tree
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
13 changes: 11 additions & 2 deletions lean/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,22 @@ class CaseInsensitiveChoice(Choice):
def __init__(self, choices, **kwargs):
super().__init__(choices, case_sensitive=False, **kwargs)

def get_metavar(self, param, ctx=None) -> str:
def _get_display_choices(self) -> List[str]:
import enum
choices_str = "|".join(c.value if isinstance(c, enum.Enum) else str(c) for c in self.choices)
return [c.value if isinstance(c, enum.Enum) else str(c) for c in self.choices]

def get_metavar(self, param, ctx=None) -> str:
choices_str = "|".join(self._get_display_choices())
if param is not None and param.required and param.param_type_name == "argument":
return f"{{{choices_str}}}"
return f"[{choices_str}]"

def get_invalid_choice_message(self, value, ctx=None) -> str:
choices_str = ", ".join(map(repr, self._get_display_choices()))
if len(self.choices) == 1:
return f"{value!r} is not {choices_str}."
return f"{value!r} is not one of {choices_str}."


class PathParameter(ParamType):
"""A limited version of click.Path which uses pathlib.Path."""
Expand Down
43 changes: 42 additions & 1 deletion tests/test_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import pytest
from click.testing import CliRunner

from lean.click import DateParameter, LeanCommand, PathParameter, RegexParameter
from lean.click import CaseInsensitiveChoice, DateParameter, LeanCommand, PathParameter, RegexParameter
from lean.container import container
from tests.test_helpers import create_fake_lean_cli_directory

Expand Down Expand Up @@ -257,3 +257,44 @@ def command(arg: str) -> None:

assert result.exit_code != 0
assert f"must match the '{TIME_REGEX}' format" in result.output


def test_case_insensitive_choice_shows_original_casing_in_help() -> None:
@click.command()
@click.option("--brokerage", type=CaseInsensitiveChoice(["Paper Trading", "Interactive Brokers"]))
def command(brokerage: str) -> None:
pass

result = CliRunner().invoke(command, ["--help"])

assert result.exit_code == 0
assert "[Paper Trading|Interactive Brokers]" in result.output


@pytest.mark.parametrize("input", ["Interactive Brokers", "interactive brokers", "INTERACTIVE BROKERS"])
def test_case_insensitive_choice_returns_original_choice_regardless_of_input_casing(input: str) -> None:
given_brokerage: Optional[str] = None

@click.command()
@click.option("--brokerage", type=CaseInsensitiveChoice(["Paper Trading", "Interactive Brokers"]))
def command(brokerage: str) -> None:
nonlocal given_brokerage
given_brokerage = brokerage

result = CliRunner().invoke(command, ["--brokerage", input])

assert result.exit_code == 0

assert given_brokerage == "Interactive Brokers"


def test_case_insensitive_choice_shows_original_casing_when_input_not_valid() -> None:
@click.command()
@click.option("--brokerage", type=CaseInsensitiveChoice(["Paper Trading", "Interactive Brokers"]))
def command(brokerage: str) -> None:
pass

result = CliRunner().invoke(command, ["--brokerage", "invalid"])

assert result.exit_code != 0
assert "'invalid' is not one of 'Paper Trading', 'Interactive Brokers'." in result.output