Skip to content

Commit 528c02e

Browse files
committed
feat(appearance): add automatic choices display in help layouts
1 parent fe8234e commit 528c02e

1 file changed

Lines changed: 97 additions & 12 deletions

File tree

interfacy/appearance/layouts.py

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class InterfacyLayout(HelpLayout):
2525

2626
column_gap: str = " "
2727
format_option: str | None = "{flag_col}{column_gap}{description}{extra}"
28-
format_positional: str | None = "{flag_col}{column_gap}{description}"
28+
format_positional: str | None = "{flag_col}{column_gap}{description}{extra}"
2929
include_metavar_in_flag_display: bool = False
3030
layout_mode: Literal["auto", "adaptive", "template"] = "template"
3131
required_indicator: str = "(" + colored("*", color="red") + ")"
@@ -704,6 +704,19 @@ def _description_mentions_same_default(cls, description: str, default_text: str)
704704

705705
return f"[default: {normalized_default}]" in normalized_description
706706

707+
@classmethod
708+
def _description_mentions_same_choices(
709+
cls,
710+
description: str,
711+
choices_text: str,
712+
) -> bool:
713+
if not description or not choices_text:
714+
return False
715+
716+
normalized_description = cls._normalize_whitespace(description).lower()
717+
normalized_choices = cls._normalize_whitespace(choices_text).lower()
718+
return f"[choices: {normalized_choices}]" in normalized_description
719+
707720
@classmethod
708721
def _with_default_sentence(cls, description: str, has_default: bool, default: object) -> str:
709722
if not has_default:
@@ -721,6 +734,24 @@ def _with_default_sentence(cls, description: str, has_default: bool, default: ob
721734
return default_block
722735
return f"{description.rstrip()} {default_block}"
723736

737+
@classmethod
738+
def _with_choices_block(
739+
cls,
740+
description: str,
741+
choices: Sequence[object] | None,
742+
) -> str:
743+
if not choices:
744+
return description
745+
746+
choices_text = ", ".join(str(choice) for choice in choices)
747+
if cls._description_mentions_same_choices(description, choices_text):
748+
return description
749+
750+
choices_block = f"[choices: {choices_text}]"
751+
if not description:
752+
return choices_block
753+
return f"{description.rstrip()} {choices_block}"
754+
724755
def get_help_for_parameter(
725756
self,
726757
param: Parameter,
@@ -747,7 +778,9 @@ def get_help_for_parameter(
747778
long_flag=primary_flag,
748779
)
749780
)
750-
return self._with_default_sentence(description, has_default, default_value)
781+
description = self._with_default_sentence(description, has_default, default_value)
782+
choices = get_param_choices(param, for_display=True) if param.is_typed else None
783+
return self._with_choices_block(description, choices)
751784

752785
def format_argument(
753786
self,
@@ -769,7 +802,13 @@ def format_argument(
769802
long_flag=self._get_primary_boolean_flag_from_argument(arg),
770803
)
771804
)
772-
return self._with_default_sentence(description, has_default, default_value)
805+
description = self._with_default_sentence(description, has_default, default_value)
806+
choices = (
807+
[self._format_argument_choice_for_help(arg, choice) for choice in arg.choices]
808+
if arg.choices
809+
else None
810+
)
811+
return self._with_choices_block(description, choices)
773812

774813

775814
@dataclass(kw_only=True)
@@ -818,25 +857,63 @@ def _description_mentions_same_default(cls, description: str, default_text: str)
818857
)
819858

820859
@classmethod
821-
def _with_default_sentence(cls, description: str, has_default: bool, default: object) -> str:
822-
if not has_default:
860+
def _description_mentions_same_choices(
861+
cls,
862+
description: str,
863+
choices_text: str,
864+
) -> bool:
865+
if not description or not choices_text:
866+
return False
867+
868+
normalized_description = cls._normalize_whitespace(description).lower()
869+
normalized_choices = cls._normalize_whitespace(choices_text).lower()
870+
return (
871+
f"choices: {normalized_choices}" in normalized_description
872+
or f"possible values: {normalized_choices}" in normalized_description
873+
)
874+
875+
@classmethod
876+
def _append_sentence(cls, description: str, sentence: str) -> str:
877+
if not sentence:
823878
return description
824879

825880
description = cls._collapse_duplicate_terminal_period(description)
826881

882+
separator = ""
883+
if description:
884+
separator = " " if description.rstrip().endswith((".", "?", "!", ":", ";")) else ". "
885+
886+
terminal = "" if sentence.endswith((".", "?", "!", ":", ";")) else "."
887+
return f"{description}{separator}{sentence}{terminal}"
888+
889+
@classmethod
890+
def _with_default_sentence(cls, description: str, has_default: bool, default: object) -> str:
891+
if not has_default:
892+
return description
893+
827894
default_text = format_default_for_help(default)
828895
if default_text in {"", '""'}:
829896
default_text = "''"
830897

831898
if cls._description_mentions_same_default(description, default_text):
832899
return description
833900

834-
separator = ""
835-
if description:
836-
separator = " " if description.rstrip().endswith((".", "?", "!", ":", ";")) else ". "
901+
return cls._append_sentence(description, f"Defaults to {default_text}")
837902

838-
terminal = "" if default_text.endswith((".", "?", "!", ":", ";")) else "."
839-
return f"{description}{separator}Defaults to {default_text}{terminal}"
903+
@classmethod
904+
def _with_choices_sentence(
905+
cls,
906+
description: str,
907+
choices: Sequence[object] | None,
908+
) -> str:
909+
if not choices:
910+
return description
911+
912+
choices_text = ", ".join(str(choice) for choice in choices)
913+
if cls._description_mentions_same_choices(description, choices_text):
914+
return description
915+
916+
return cls._append_sentence(description, f"Choices: {choices_text}")
840917

841918
def get_help_for_parameter(
842919
self,
@@ -864,7 +941,9 @@ def get_help_for_parameter(
864941
long_flag=primary_flag,
865942
)
866943
)
867-
return self._with_default_sentence(description, has_default, default_value)
944+
description = self._with_default_sentence(description, has_default, default_value)
945+
choices = get_param_choices(param, for_display=True) if param.is_typed else None
946+
return self._with_choices_sentence(description, choices)
868947

869948
def format_argument(
870949
self,
@@ -886,7 +965,13 @@ def format_argument(
886965
long_flag=self._get_primary_boolean_flag_from_argument(arg),
887966
)
888967
)
889-
return self._with_default_sentence(description, has_default, default_value)
968+
description = self._with_default_sentence(description, has_default, default_value)
969+
choices = (
970+
[self._format_argument_choice_for_help(arg, choice) for choice in arg.choices]
971+
if arg.choices
972+
else None
973+
)
974+
return self._with_choices_sentence(description, choices)
890975

891976

892977
# Backward compatibility alias.

0 commit comments

Comments
 (0)