• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Biblioteka do projektowania interaktywnych menu

0 głosów
88 wizyt
pytanie zadane 12 października w Nasze projekty przez whiteman808 Mądrala (5,430 p.)

Hejka, co myślicie o moim nowym programie testującym bibliotekę do projektowania interaktywnych menu w konsoli?

calc.py

#!/usr/bin/env python3
import gettext
import os
from operator import add as op_add
from operator import mul, sub, truediv
from typing import Any, Callable

from ui.menu import Menu, MenuItem
from ui.user_input import read_number

# mypy and pylint don't work well with gettext.install
LOCALE = os.getenv('LANG', 'en')
_ = gettext.translation('python_misc',
                        localedir='locale',
                        languages=[LOCALE]).gettext


def make_binop(fn: Callable[[Any, Any], Any], name: str,
               nonzero_second=False) -> Callable[[], None]:
    def _op():
        a = read_number(_('Enter the first operand: '))
        b = read_number(_('Enter the second operand: '),
                        nonzero=nonzero_second)
        print(_('The result is {0}.').format(fn(a, b)))
    _op.__name__ = name
    return _op


if __name__ == '__main__':
    items = [MenuItem(_('Add'), make_binop(op_add, 'add'), _('a')),
             MenuItem(_('Subtract'), make_binop(sub, 'subtract'), _('s')),
             MenuItem(_('Multiply'), make_binop(mul, 'multiply'), _('m')),
             MenuItem(_('Divide'), make_binop(truediv, 'divide'), _('d'))]
    menu = Menu(_('Calculator'), items)
    menu.loop()
    print(_('Good bye!'))

ui/user_input.py

import gettext
import os
import sys
from contextlib import contextmanager
from numbers import Number
from typing import IO, Any, Iterator, Optional, TextIO, overload

# mypy and pylint don't work well with gettext.install
LOCALE = os.getenv('LANG', 'en')
_ = gettext.translation('python_misc',
                        localedir='locale',
                        languages=[LOCALE]).gettext


@contextmanager
def redirect_stdio(stdin: IO[Any],
                   stdout: IO[Any],
                   stderr: IO[Any] = sys.stderr) -> Iterator[None]:
    old_stdin = sys.stdin
    old_stdout = sys.stdout
    old_stderr = sys.stderr
    sys.stdin = stdin
    sys.stdout = stdout
    sys.stderr = stderr

    yield

    sys.stdin = old_stdin
    sys.stdout = old_stdout
    sys.stderr = old_stderr


def read_str(prompt: str = '', *,
             stdin: TextIO = sys.stdin,
             stdout: TextIO = sys.stdout) -> str:
    stdout.write(prompt)
    stdout.flush()
    user_input = stdin.readline()
    if not user_input:
        raise EOFError
    return user_input


def wait_for_enter(*,
                   stdin: TextIO = sys.stdin,
                   stdout: TextIO = sys.stdout) -> None:
    read_str(_('\nPress ENTER to continue...\n'), stdin=stdin, stdout=stdout)


def print_input_error(message: str) -> None:
    print(_('Error: {0}. Try again...').format(message))


def _get_default_prompt(lower: Optional[Number], upper: Optional[Number],
                        nonzero: bool):
    prompt = _('Enter the number')
    if lower is not None or upper is not None or nonzero:
        prompt += ' ('
    if lower is not None:
        prompt += _('starting from {0}').format(lower)
    if upper is not None:
        prompt += _('up to {0}').format(upper)
    if nonzero:
        if lower is not None or upper is not None:
            prompt += ', '
        prompt += _('cannot be zero')
    if lower is not None or upper is not None or nonzero:
        prompt += ')'
    prompt += ': '
    return prompt


@overload
def read_number[T: Number](prompt: str = ..., *,
                           stdin: TextIO = ...,
                           stdout: TextIO = ...,
                           lower: Optional[T] = ...,
                           upper: Optional[T] = ...,
                           nonzero: bool = ...,
                           _type: type[T]) -> T: ...


@overload
def read_number[T: Number](prompt: str = ..., *,
                           stdin: TextIO = ...,
                           stdout: TextIO = ...,
                           lower: Optional[T] = ...,
                           upper: Optional[T] = ...,
                           nonzero: bool = ...) -> float: ...


def read_number[T: Number](prompt: str = '', *,
                           stdin: TextIO = sys.stdin,
                           stdout: TextIO = sys.stdout,
                           lower: Optional[T] = None,
                           upper: Optional[T] = None,
                           nonzero: bool = False,
                           _type: type = float):
    if not prompt:
        prompt = _get_default_prompt(lower, upper, nonzero)
    while True:
        try:
            number: Any = _type(read_str(prompt,
                                         stdin=stdin,
                                         stdout=stdout))
            if nonzero and number == 0:
                print_input_error(_('number is equal to zero'))
            if lower is not None and number < lower:
                print_input_error(_('number is too small'))
            if upper is not None and number > upper:
                print_input_error(_('number is too big'))
            return number
        except ValueError:
            print_input_error(_('not a number'))
        except (EOFError, KeyboardInterrupt):
            print(_('\nAborted by user.'))
            raise

ui/menu.py

from __future__ import annotations

import gettext
import os
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
LOCALE = os.getenv('LANG', 'en')
_ = gettext.translation('python_misc',
                        localedir='locale',
                        languages=[LOCALE]).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], *,
                 fin: TextIO = sys.stdin,
                 fout: TextIO = sys.stdout):
        self.title = title
        self.items = list(items)
        if Menu.nesting_level > 0:
            quit_item_title = _('Back')
        else:
            quit_item_title = _('Quit')
        quit_item = MenuItem(quit_item_title, self._quit, _('q'))
        self._check_for_duplicates(quit_item)
        self.items.append(quit_item)
        self.active = False
        self.stdin = fin
        self.stdout = fout

    @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.append(item)

    def loop(self) -> None:
        with redirect_stdio(self.stdin, self.stdout):
            Menu.nesting_level += 1
            self.active = True
            while self.active:
                user_choice = self._read_choice()
                if user_choice.action.__name__ == self._quit.__name__:
                    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.name == item.name
                or candidate.action == item.action
                    or candidate.key == item.key):
                raise ValueError('duplicate menu item')

    def _print_title(self) -> None:
        line = '-' * (len(self.title) + 4)
        print(line, file=self.stdout)
        print(f'| {self.title} |', file=self.stdout)
        print(line, file=self.stdout)

    def _read_choice(self) -> MenuItem:
        with redirect_stdio(self.stdin, self.stdout):
            while True:
                self._print_title()
                for idx, item in enumerate(self.visible_items, start=1):
                    print(f'{idx}) {item}')
                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

locale/en/LC_MESSAGES/python_misc.po

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR ORGANIZATION
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2025-10-12 16:57+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n"


#: calc.py:19
msgid "Enter the first operand: "
msgstr ""

#: calc.py:20
msgid "Enter the second operand: "
msgstr ""

#: calc.py:22
msgid "The result is {0}."
msgstr ""

#: calc.py:28
msgid "Add"
msgstr ""

#: calc.py:28
msgid "a"
msgstr ""

#: calc.py:29
msgid "Subtract"
msgstr ""

#: calc.py:29
msgid "s"
msgstr ""

#: calc.py:30
msgid "Multiply"
msgstr ""

#: calc.py:30
msgid "m"
msgstr ""

#: calc.py:31
msgid "Divide"
msgstr ""

#: calc.py:31
msgid "d"
msgstr ""

#: calc.py:32
msgid "Calculator"
msgstr ""

#: calc.py:34
msgid "Good bye!"
msgstr ""

#: ui/menu.py:39
msgid "Back"
msgstr ""

#: ui/menu.py:41
msgid "Quit"
msgstr ""

#: ui/menu.py:42
msgid "q"
msgstr ""

#: ui/menu.py:73
msgid "Your choice: "
msgstr ""

#: ui/menu.py:75
msgid ""
"\n"
"Exiting..."
msgstr ""

#: ui/menu.py:92
msgid "invalid operation"
msgstr ""

#: ui/menu.py:97
msgid "invalid choice"
msgstr ""

#: ui/user_input.py:47
msgid ""
"\n"
"Press ENTER to continue...\n"
msgstr ""

#: ui/user_input.py:52
msgid "Enter the number"
msgstr ""

#: ui/user_input.py:56
msgid "starting from {0}"
msgstr ""

#: ui/user_input.py:58
msgid "up to {0}"
msgstr ""

#: ui/user_input.py:62
msgid "cannot be zero"
msgstr ""

#: ui/user_input.py:70
msgid "Error: {0}. Try again..."
msgstr ""

#: ui/user_input.py:107
msgid "number is equal to zero"
msgstr ""

#: ui/user_input.py:109
msgid "number is too small"
msgstr ""

#: ui/user_input.py:111
msgid "number is too big"
msgstr ""

#: ui/user_input.py:114
msgid "not a number"
msgstr ""

#: ui/user_input.py:116
msgid ""
"\n"
"Aborted by user."
msgstr ""

Zaloguj lub zarejestruj się, aby odpowiedzieć na to pytanie.

Podobne pytania

0 głosów
0 odpowiedzi 92 wizyt
pytanie zadane 11 października w Nasze projekty przez whiteman808 Mądrala (5,430 p.)
+2 głosów
2 odpowiedzi 649 wizyt

93,605 zapytań

142,529 odpowiedzi

322,999 komentarzy

63,096 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj

Kursy INF.02 i INF.03
...