Coverage for typer / _types.py: 100%

49 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1from collections.abc import Iterable, Mapping, Sequence 1abcdefg

2from enum import Enum 1abcdefg

3from typing import Any, Generic, TypeVar 1abcdefg

4 

5from . import _click 1abcdefg

6from ._click import types 1abcdefg

7from ._click.shell_completion import CompletionItem 1abcdefg

8 

9ParamTypeValue = TypeVar("ParamTypeValue") 1abcdefg

10 

11 

12class TyperChoice(types.ParamType, Generic[ParamTypeValue]): 1abcdefg

13 # Code adapted from Click 8.3.1, with Typer using enum values in normalize_choice 

14 name = "choice" 1abcdefg

15 

16 def __init__( 1abcdefg

17 self, choices: Iterable[ParamTypeValue], case_sensitive: bool = True 

18 ) -> None: 

19 self.choices: Sequence[ParamTypeValue] = tuple(choices) 1abcdefg

20 self.case_sensitive = case_sensitive 1abcdefg

21 

22 def _normalized_mapping( 1abcdefg

23 self, ctx: _click.Context | None = None 

24 ) -> Mapping[ParamTypeValue, str]: 

25 """ 

26 Returns mapping where keys are the original choices and the values are 

27 the normalized values that are accepted via the command line. 

28 """ 

29 return { 1abcdefg

30 choice: self.normalize_choice( 

31 choice=choice, 

32 ctx=ctx, 

33 ) 

34 for choice in self.choices 

35 } 

36 

37 def normalize_choice( 1abcdefg

38 self, choice: ParamTypeValue, ctx: _click.Context | None 

39 ) -> str: 

40 normed_value = str(choice.value) if isinstance(choice, Enum) else str(choice) 1abcdefg

41 

42 if ctx is not None and ctx.token_normalize_func is not None: 1abcdefg

43 normed_value = ctx.token_normalize_func(normed_value) 1abcdefg

44 

45 if not self.case_sensitive: 1abcdefg

46 normed_value = normed_value.casefold() 1abcdefg

47 

48 return normed_value 1abcdefg

49 

50 def get_metavar(self, param: _click.Parameter, ctx: _click.Context) -> str | None: 1abcdefg

51 if param.param_type_name == "option" and not param.show_choices: # type: ignore 1abcdefg

52 choice_metavars = [ 1abcdefg

53 types.convert_type(type(choice)).name for choice in self.choices 

54 ] 

55 choices_str = "|".join([*dict.fromkeys(choice_metavars)]) 1abcdefg

56 else: 

57 choices_str = "|".join( 1abcdefg

58 [str(i) for i in self._normalized_mapping(ctx=ctx).values()] 

59 ) 

60 

61 # Use square braces as it's a type. 

62 return f"<{choices_str}>" 1abcdefg

63 

64 def get_missing_message( 1abcdefg

65 self, param: _click.Parameter, ctx: _click.Context | None 

66 ) -> str: 

67 """Message shown when no choice is passed.""" 

68 choices = ",\n\t".join(self._normalized_mapping(ctx=ctx).values()) 1abcdefg

69 return f"Choose from:\n\t{choices}" 1abcdefg

70 

71 def convert( 1abcdefg

72 self, value: Any, param: _click.Parameter | None, ctx: _click.Context | None 

73 ) -> ParamTypeValue: 

74 """ 

75 For a given value from the parser, normalize it and find its 

76 matching normalized value in the list of choices. Then return the 

77 matched "original" choice. 

78 """ 

79 normed_value = self.normalize_choice(choice=value, ctx=ctx) 1abcdefg

80 normalized_mapping = self._normalized_mapping(ctx=ctx) 1abcdefg

81 

82 try: 1abcdefg

83 return next( 1abcdefg

84 original 

85 for original, normalized in normalized_mapping.items() 

86 if normalized == normed_value 

87 ) 

88 except StopIteration: 1abcdefg

89 self.fail( 1abcdefg

90 self.get_invalid_choice_message(value=value, ctx=ctx), 

91 param=param, 

92 ctx=ctx, 

93 ) 

94 

95 def get_invalid_choice_message(self, value: Any, ctx: _click.Context | None) -> str: 1abcdefg

96 """Get the error message when the given choice is invalid.""" 

97 choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values())) 1abcdefg

98 return f"{value!r} is not one of {choices_str}." 1abcdefg

99 

100 def __repr__(self) -> str: 1abcdefg

101 return f"Choice({list(self.choices)})" 1abcdefg

102 

103 def shell_complete( 1abcdefg

104 self, ctx: _click.Context, param: _click.Parameter, incomplete: str 

105 ) -> list[CompletionItem]: 

106 """Complete choices that start with the incomplete value.""" 

107 

108 str_choices = map(str, self.choices) 1abcdefg

109 

110 if self.case_sensitive: 1abcdefg

111 matched = (c for c in str_choices if c.startswith(incomplete)) 1abcdefg

112 else: 

113 incomplete = incomplete.lower() 1abcdefg

114 matched = (c for c in str_choices if c.lower().startswith(incomplete)) 1abcdefg

115 

116 return [CompletionItem(c) for c in matched] 1abcdefg