Wie ktoś jak pozbyć zmiennej statycznej Menu.nesting_level i przekazywać zaktualizowane informacje dotyczące poziomu zagnieżdżenia Menu dopiero w momencie wywoływania metody menu.add_item np menu.add_item('Podmenu', submenu.loop) lub menu.add_submenu? Nie wszystkie obiekty Menu mają być podmenu powiązanymi z innymi menu w programie.
from __future__ import annotations
import gettext
import locale
import sys
from dataclasses import dataclass
from typing import Callable, Optional, TextIO
from .user_input import print_input_error, redirect_stdio, wait_for_enter
# mypy and pylint don't work well with gettext.install
lang, _enc = locale.getlocale()
lang = lang or 'en'
_ = gettext.translation('python_misc',
localedir='locale',
languages=[lang, lang.split('_')[0], 'en'],
fallback=True).gettext
@dataclass(frozen=True)
class MenuItem:
name: str
action: Callable[[], None]
key: Optional[str] = None
visible: bool = True
enabled: bool = True
def __str__(self):
return self.name
class Menu:
nesting_level = -1
def __init__(self,
title: str,
items: list[MenuItem], *,
quit_key: Optional[str] = 'q',
stdin: TextIO = sys.stdin,
stdout: TextIO = sys.stdout):
self.title = title
self.items = list(items)
quit_item = MenuItem('Quit', self._quit, quit_key)
self._check_for_duplicates(quit_item)
self.items.append(quit_item)
self.active = False
self.quit_key = quit_key
self.stdin = stdin
self.stdout = stdout
@property
def visible_items(self):
return [item for item in self.items if item.visible]
def add_item(self,
name: str,
action: Callable[[], None],
key: Optional[str] = None) -> None:
item = MenuItem(name, action, key)
self._check_for_duplicates(item)
self.items.insert(-1, item)
def remove_item(self, name: str) -> bool:
for idx, item in enumerate(self.items):
if item.name == name:
del self.items[idx]
return True
return False
def add_submenu(self,
name: str,
menu: Menu,
key: Optional[str] = None) -> None:
item = MenuItem(name, menu.loop, key)
self._check_for_duplicates(item)
self.items.insert(-1, item)
def loop(self) -> None:
with redirect_stdio(self.stdin, self.stdout):
Menu.nesting_level += 1
if Menu.nesting_level > 0:
quit_item_title = _('Back')
else:
quit_item_title = _('Quit')
quit_item = MenuItem(quit_item_title, self._quit, self.quit_key)
self.items[-1] = quit_item
self.active = True
while self.active:
user_choice = self._read_choice()
if user_choice.action == self._quit: # pylint: disable=W0143
self._quit()
else:
try:
user_choice.action()
wait_for_enter()
except (EOFError, KeyboardInterrupt):
pass
def _check_for_duplicates(self, candidate: MenuItem) -> None:
for item in self.items:
if (candidate.key is not None and item.key is not None
and candidate.key.lower() == item.key.lower()):
raise ValueError('duplicate key')
def _print_title(self) -> None:
longest_item = max(len(item.name) for item in self.visible_items)
has_key = any(item.key is not None for item in self.visible_items)
if has_key:
longest_key = max(len(item.key) for item in self.visible_items
if item.key is not None)
else:
longest_key = 0
field_width = max(len(self.title), longest_item) + 2
if longest_key > 0:
field_width += longest_key
line = '-' * (field_width + 2)
print(line, file=self.stdout)
print(f'|{self.title:^{field_width}}|', file=self.stdout)
print(line, file=self.stdout)
def _display_menu(self) -> None:
self._print_title()
longest = max(len(item.name) for item in self.visible_items)
for idx, item in enumerate(self.visible_items, start=1):
label = f'{idx}) {item.name:<{longest}}'
label += f' [{item.key}]' if item.key else ''
print(label)
def _read_choice(self) -> MenuItem:
with redirect_stdio(self.stdin, self.stdout):
while True:
self._display_menu()
try:
user_input = input(_('Your choice: ')).strip().lower()
except (EOFError, KeyboardInterrupt):
print(_('\nExiting...'))
return self.items[-1]
try:
if user_input.isdigit():
idx = int(user_input)
if not 1 <= idx <= len(self.visible_items):
raise ValueError
candidate = self.visible_items[idx - 1]
else:
for idx, item in enumerate(self.visible_items):
if user_input == item.key:
candidate = self.visible_items[idx]
break
else:
raise ValueError
if not candidate.enabled:
print_input_error(_('invalid operation'))
wait_for_enter()
else:
return candidate
except ValueError:
print_input_error(_('invalid choice'))
def _quit(self) -> None:
self.active = False
Menu.nesting_level -= 1