Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better interactive shell #57

Merged
merged 10 commits into from
May 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .moban.d/CUSTOM_README.rst.jj2
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ License
================================================================================

NEW BSD License


It embeds MIT licensed `cutie <>`_ from Hans Schülein. Please refer to LICENSE
file for more details
{% endblock %}
17 changes: 15 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright (c) 2015-2017 by Onni Software Ltd. and its contributors
Copyright (c) 2015-2020 by Onni Software Ltd. and its contributors
All rights reserved.

Redistribution and use in source and binary forms of the software as well
Expand Down Expand Up @@ -27,4 +27,17 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
DAMAGE.


Please note 'cutie' package under under the following license:

The MIT License (MIT)

Copyright © 2018 Hans Schülein

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,7 @@ License
================================================================================

NEW BSD License


It embeds MIT licensed `cutie <>`_ from Hans Schülein. Please refer to LICENSE
file for more details
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ ruamel.yaml>=0.15.5;python_version != '3.4' and python_version < '3.7'
ruamel.yaml>=0.15.98;python_version == '3.8'
Jinja2
moban>=0.6.0
crayons
colorful
rich
readchar
colorama
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@
INSTALL_REQUIRES = [
"Jinja2",
"moban>=0.6.0",
"crayons",
"colorful",
"rich",
"readchar",
"colorama",
]
SETUP_COMMANDS = {}

Expand Down
65 changes: 65 additions & 0 deletions tests/cutie_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from yehua.thirdparty import cutie

import readchar


def PrintCall(states):
def func(msg=None, state="selectable"):
if msg:
return ((states[state] + msg,),)
else:
return ((states[state],),)

return func


def yield_input(*data, raise_on_empty=False):
"""
Closure that returns predefined data.

If the data is exhausted raise a MockException or reraise the IndexError
"""
data = list(data)

def func(*a, **kw):
try:
return data.pop(0)
except IndexError as e:
if raise_on_empty:
raise MockException()
else:
raise e

return func


class InputContext:
"""
Context manager to simulate keyboard input returned by `readchar.readkey`,
by replacing it in `cutie` with `yield_input`

When the supplied keystrokes are exhausted a `MockException` will be
raised. This can be used to terminate the execution at any desired point,
rather than relying on internal control mechanisms.

Usage:
with InputContext(" ", "\r"):
cutie.select(["foo", "bar"])
This will issue a space and enter keypress, selecting the first item and
confirming.
"""

def __init__(self, *data, raise_on_empty=True):
cutie.readchar.readkey = yield_input(
*data, raise_on_empty=raise_on_empty
)

def __enter__(self):
pass

def __exit__(self, *a):
cutie.readchar.readkey = readchar.readkey


class MockException(Exception):
pass
112 changes: 112 additions & 0 deletions tests/cutie_tests/test_get_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import unittest
from unittest import mock

from yehua.thirdparty import cutie

from . import MockException


class TestCutieGetNumber(unittest.TestCase):
@mock.patch("yehua.thirdparty.cutie.print", side_effect=MockException)
def test_invalid_number(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="foo"):
with self.assertRaises(MockException):
cutie.get_number("bar")
mock_print.assert_called_once_with(
"Not a valid number.\033[K\033[1A\r\033[K", end=""
)

@mock.patch("yehua.thirdparty.cutie.print", side_effect=MockException)
def test_not_allow_float(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.2"):
with self.assertRaises(MockException):
cutie.get_number("foo", allow_float=False)
mock_print.assert_called_once_with(
"Has to be an integer.\033[K\033[1A\r\033[K", end=""
)

def test_allow_float_returns_float(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.2"):
val = cutie.get_number("foo")
self.assertIsInstance(val, float)
self.assertEqual(val, 1.2)

def test_not_allow_float_returns_int(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1"):
val = cutie.get_number("foo", allow_float=False)
self.assertIsInstance(val, int)
self.assertEqual(val, 1)

@mock.patch("yehua.thirdparty.cutie.print", side_effect=MockException)
def test_min_value_float_too_low(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.2"):
with self.assertRaises(MockException):
cutie.get_number("foo", min_value=1.3)
mock_print.assert_called_once_with(
"Has to be at least 1.3.\033[K\033[1A\r\033[K", end=""
)

def test_min_value_float_equal(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.2"):
self.assertEqual(cutie.get_number("foo", min_value=1.2), 1.2)

def test_min_value_float_greater(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.3"):
self.assertEqual(cutie.get_number("foo", min_value=1.2), 1.3)

@mock.patch("yehua.thirdparty.cutie.print", side_effect=MockException)
def test_min_value_int_too_low(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1"):
with self.assertRaises(MockException):
cutie.get_number("foo", min_value=2)
mock_print.assert_called_once_with(
"Has to be at least 2.\033[K\033[1A\r\033[K", end=""
)

def test_min_value_int_equal(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1"):
self.assertEqual(cutie.get_number("foo", min_value=1), 1)

def test_min_value_int_greater(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="2"):
self.assertEqual(cutie.get_number("foo", min_value=1), 2)

@mock.patch("yehua.thirdparty.cutie.print", side_effect=MockException)
def test_max_value_float_too_high(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.2"):
with self.assertRaises(MockException):
cutie.get_number("foo", max_value=1.1)
mock_print.assert_called_once_with(
"Has to be at most 1.1.\033[1A\r\033[K", end=""
)

def test_max_value_float_equal(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.1"):
self.assertEqual(cutie.get_number("foo", max_value=1.1), 1.1)

def test_max_value_float_smaller(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1.1"):
self.assertEqual(cutie.get_number("foo", max_value=1.2), 1.1)

@mock.patch("yehua.thirdparty.cutie.print", side_effect=MockException)
def test_max_value_int_too_high(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="2"):
with self.assertRaises(MockException):
cutie.get_number("foo", max_value=1)
mock_print.assert_called_once_with(
"Has to be at most 1.\033[1A\r\033[K", end=""
)

def test_max_value_int_equal(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1"):
self.assertEqual(cutie.get_number("foo", max_value=1), 1)

def test_max_value_int_smaller(self):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1"):
self.assertEqual(cutie.get_number("foo", max_value=2), 1)

@mock.patch("yehua.thirdparty.cutie.print")
def test_print_finalize(self, mock_print):
with mock.patch("yehua.thirdparty.cutie.input", return_value="1"):
cutie.get_number("foo")
mock_print.assert_called_once_with("\033[K", end="")
Loading