From 1f7bb1d54c25761cc39d1af044e58cd626a311b1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 21 Sep 2021 20:13:39 +0200 Subject: [PATCH 1/8] Rework cli prompt() again --- debian/control | 3 ++- moulinette/interfaces/cli.py | 43 +++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/debian/control b/debian/control index b399c78d..8713affa 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,8 @@ Depends: ${misc:Depends}, ${python3:Depends}, python3-gevent-websocket, python3-toml, python3-psutil, - python3-tz + python3-tz, + python3-prompt-toolkit Breaks: yunohost (<< 4.1) Description: prototype interfaces with ease in Python Quickly and easily prototype interfaces for your application. diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index 6e1eb8cd..faab2294 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -533,6 +533,8 @@ class Interface: color="blue", prefill="", is_multiline=False, + autocomplete=[], + help=None, ): """Prompt for a value @@ -547,16 +549,37 @@ class Interface: def _prompt(message): - if is_password: - return getpass.getpass(colorize(m18n.g("colon", message), color)) - elif not is_multiline: - print(colorize(m18n.g("colon", message), color), end="") - set_startup_hook(lambda: insert_text(prefill)) - try: - value = input() - finally: - set_startup_hook() - return value + if not is_multiline: + + import prompt_toolkit + from prompt_toolkit.contrib.completers import WordCompleter + from pygments.token import Token + + autocomplete_ = WordCompleter(autocomplete) + style = prompt_toolkit.styles.style_from_dict({ + Token.Message: f'#ansi{color} bold', + }) + + def get_bottom_toolbar_tokens(cli): + if help: + return [(Token, help)] + else: + return [] + + def get_tokens(cli): + return [ + (Token.Message, message), + (Token, ': '), + ] + + return prompt_toolkit.prompt(get_prompt_tokens=get_tokens, + get_bottom_toolbar_tokens=get_bottom_toolbar_tokens, + style=style, + default=prefill, + true_color=True, + completer=autocomplete_, + is_password=is_password) + else: while True: value = input( From c4b559cf6bb9c3d874847cef095fd24e131ad501 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 00:47:11 +0200 Subject: [PATCH 2/8] Fix test + missing dependency to prompt-toolkit in setup.py --- moulinette/interfaces/cli.py | 2 -- setup.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index faab2294..3e704294 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -2,12 +2,10 @@ import os import sys -import getpass import locale import logging import argparse import tempfile -from readline import insert_text, set_startup_hook from collections import OrderedDict from datetime import date, datetime from subprocess import call diff --git a/setup.py b/setup.py index a774d61f..b6f4a5df 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ install_deps = [ "toml", "gevent-websocket", "bottle", + "prompt-toolkit", ] test_deps = [ From 088ef05e0772dd9f4b62d1a840387a9a42c33409 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 00:57:22 +0200 Subject: [PATCH 3/8] Force version for prompt-toolkit in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b6f4a5df..63a1f8b6 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ install_deps = [ "toml", "gevent-websocket", "bottle", - "prompt-toolkit", + "prompt-toolkit==1.0.15", ] test_deps = [ From 2b62f3d2e6a4fbd4e328e8b4e6df40a7bc0a3436 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 01:01:01 +0200 Subject: [PATCH 4/8] Also depend on pygments --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 63a1f8b6..c0e858d4 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,8 @@ install_deps = [ "toml", "gevent-websocket", "bottle", - "prompt-toolkit==1.0.15", + "prompt-toolkit==1.0.15", # To be bumped to debian version once we're on bullseye (+ need tweaks in cli.py) + "pygments", ] test_deps = [ From 2f59023eee886d69ed7413db7ac4991b4caf8477 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 20:55:35 +0200 Subject: [PATCH 5/8] read_file: support for custom file mode --- moulinette/utils/filesystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moulinette/utils/filesystem.py b/moulinette/utils/filesystem.py index d075b83d..e14c9059 100644 --- a/moulinette/utils/filesystem.py +++ b/moulinette/utils/filesystem.py @@ -15,7 +15,7 @@ from moulinette.core import MoulinetteError # Files & directories -------------------------------------------------- -def read_file(file_path): +def read_file(file_path, file_mode="r"): """ Read a regular text file @@ -35,7 +35,7 @@ def read_file(file_path): # Open file and read content try: - with open(file_path, "r") as f: + with open(file_path, file_mode) as f: file_content = f.read() except IOError as e: raise MoulinetteError("cannot_open_file", file=file_path, error=str(e)) From 2ce920e2ff6ee99624836d4a7c7ed8adbf3261cc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:23:39 +0200 Subject: [PATCH 6/8] Attempt to fix tests --- test/test_auth.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/test_auth.py b/test/test_auth.py index ffb6feb7..a245cc58 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -188,7 +188,7 @@ class TestAuthAPI: class TestAuthCLI: def test_login(self, moulinette_cli, capsys, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") moulinette_cli.run(["testauth", "default"], output_as="plain") message = capsys.readouterr() @@ -201,25 +201,25 @@ class TestAuthCLI: def test_login_bad_password(self, moulinette_cli, capsys, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="Bad Password") + mocker.patch("prompt_toolkit.prompt", return_value="Bad Password") with pytest.raises(MoulinetteError): moulinette_cli.run(["testauth", "default"], output_as="plain") mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="Bad Password") + mocker.patch("prompt_toolkit.prompt", return_value="Bad Password") with pytest.raises(MoulinetteError): moulinette_cli.run(["testauth", "default"], output_as="plain") def test_login_wrong_profile(self, moulinette_cli, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") with pytest.raises(MoulinetteError) as exception: moulinette_cli.run(["testauth", "other-profile"], output_as="none") assert "invalid_password" in str(exception) mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="yoloswag") + mocker.patch("prompt_toolkit.prompt", return_value="yoloswag") with pytest.raises(MoulinetteError) as exception: moulinette_cli.run(["testauth", "default"], output_as="none") @@ -239,7 +239,7 @@ class TestAuthCLI: def test_request_only_cli(self, capsys, moulinette_cli, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") moulinette_cli.run(["testauth", "only-cli"], output_as="plain") message = capsys.readouterr() @@ -248,7 +248,7 @@ class TestAuthCLI: def test_request_not_logged_only_cli(self, capsys, moulinette_cli, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass") + mocker.patch("prompt_toolkit.prompt") with pytest.raises(MoulinetteError) as exception: moulinette_cli.run(["testauth", "only-cli"], output_as="plain") @@ -259,7 +259,7 @@ class TestAuthCLI: def test_request_with_callback(self, moulinette_cli, capsys, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") moulinette_cli.run(["--version"], output_as="plain") message = capsys.readouterr() @@ -278,7 +278,7 @@ class TestAuthCLI: def test_request_with_arg(self, moulinette_cli, capsys, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") moulinette_cli.run(["testauth", "with_arg", "yoloswag"], output_as="plain") message = capsys.readouterr() @@ -286,7 +286,7 @@ class TestAuthCLI: def test_request_arg_with_extra(self, moulinette_cli, capsys, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") moulinette_cli.run( ["testauth", "with_extra_str_only", "YoLoSwAg"], output_as="plain" ) @@ -306,7 +306,7 @@ class TestAuthCLI: def test_request_arg_with_type(self, moulinette_cli, capsys, mocker): mocker.patch("os.isatty", return_value=True) - mocker.patch("getpass.getpass", return_value="dummy") + mocker.patch("prompt_toolkit.prompt", return_value="dummy") moulinette_cli.run(["testauth", "with_type_int", "12345"], output_as="plain") message = capsys.readouterr() From 3c21a5cfbf890b9745e12c08916f9697926d02a7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:25:45 +0200 Subject: [PATCH 7/8] Stale i18n string --- locales/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 358f326f..ca0ff36d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,7 +1,6 @@ { "argument_required": "Argument '{argument}' is required", "authentication_required": "Authentication required", - "colon": "{}: ", "confirm": "Confirm {prompt}", "deprecated_command": "'{prog} {command}' is deprecated and will be removed in the future", "deprecated_command_alias": "'{prog} {old}' is deprecated and will be removed in the future, use '{prog} {new}' instead", From 433f84323f2d47b162d6b1ddac9bb4f6b6780984 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Sep 2021 21:41:19 +0200 Subject: [PATCH 8/8] debian: Depend on python3-pygments also, because it's only a recommend of python3-prompt-toolkit --- debian/control | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 8713affa..4b11237e 100644 --- a/debian/control +++ b/debian/control @@ -15,7 +15,8 @@ Depends: ${misc:Depends}, ${python3:Depends}, python3-toml, python3-psutil, python3-tz, - python3-prompt-toolkit + python3-prompt-toolkit, + python3-pygments Breaks: yunohost (<< 4.1) Description: prototype interfaces with ease in Python Quickly and easily prototype interfaces for your application.