Skip to content

Commit 580a818

Browse files
committed
Add new cue: Password cue
A new cue is available to use: the Password cue. This is especially useful when you need to retrieve some sensitive information from the user.
1 parent cdeaeac commit 580a818

File tree

6 files changed

+265
-2
lines changed

6 files changed

+265
-2
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,36 @@ This produces the following output:
207207

208208
<img src="https://raw.githubusercontent.com/GBS3/cues/main/media/checkbox.gif" width="800">
209209

210+
<h3 align="center"><i><b>Password</b></i></h3>
211+
212+
You can use the `Password` class when you need input from the user but would prefer their input be hidden while they type as it may contain sensitive information.
213+
214+
If you would like to to see an example of this in action, you can run the following command in your terminal:
215+
216+
```
217+
python -m cues.password
218+
```
219+
220+
```python
221+
from cues import Password
222+
223+
224+
name = 'password'
225+
message = 'Password:'
226+
227+
cue = Password(name, message)
228+
answer = cue.send()
229+
print(answer)
230+
```
231+
232+
This produces the following output:
233+
234+
<img src="https://raw.githubusercontent.com/GBS3/cues/main/media/password.gif" width="800">
235+
210236
## To Do
211237

212-
- [x] Bring support to macOS
213-
- [x] Bring support to Linux
214238
- [ ] JSON prompt
215239
- [x] Checkbox prompt
240+
- [x] Password prompt
216241

217242
...*amongst other things!*

cues/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
from .checkbox import Checkbox
3131
from .confirm import Confirm
3232
from .form import Form
33+
from .password import Password
3334
from .select import Select
3435
from .survey import Survey

cues/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
FORM_MARKER_UNC = '○'
1414
FORM_MARKER_COM = '●'
1515

16+
PASSWORD_MARKER = '*'
17+
1618
SURVEY_PT = '○'
1719
SURVEY_PT_FILL = '●'
1820
SURVEY_LINE = '─'

cues/listen/ansi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
# Erase functions:
5858

5959
CLEAR_LINE = '\x1b[K' # Clears the current line
60+
CLEAR_ENTIRE_LINE = '\x1b[2K' # Clears the entire line
6061

6162

6263
# Set mode:

cues/password.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
cues.password
5+
=============
6+
7+
This module contains the Password class.
8+
"""
9+
10+
from . import cursor
11+
from .cue import Cue
12+
from .listen import ansi
13+
14+
15+
class Password(Cue):
16+
"""Construct a Password object to retrieve hidden input from the user.
17+
18+
Password objects are useful when you require input from the user but
19+
would like their input to be hidden as they type as it may contain
20+
sensitive information.
21+
22+
Attributes
23+
----------
24+
_password_fmt : str
25+
The format for the password prompt.
26+
"""
27+
28+
__name__ = 'password'
29+
__module__ = 'cues'
30+
31+
def __init__(self, name: str, message: str):
32+
"""
33+
34+
Parameters
35+
----------
36+
name
37+
The name of the Form instance.
38+
message
39+
Instructions or useful information regarding the prompt for the user.
40+
"""
41+
42+
super().__init__(name, message)
43+
44+
if message.strip()[-1].isalnum():
45+
self._password_fmt = '[pink][?][/pink] {message} [grey]∙[/grey] {input}'
46+
self._password_fmt_len = 7
47+
else:
48+
self._password_fmt = '[pink][?][/pink] {message} {input}'
49+
self._password_fmt_len = 5
50+
51+
def send(self) -> dict:
52+
"""Returns a dict object containing user's response to the prompt.
53+
54+
Returns
55+
-------
56+
dict
57+
Contains the user's response to the prompt.
58+
"""
59+
60+
self._draw()
61+
return self.answer
62+
63+
def _draw(self):
64+
"""Assembles and prints the Password cue to the console.
65+
"""
66+
67+
up = self.keys.get('up')
68+
down = self.keys.get('down')
69+
right = self.keys.get('right')
70+
left = self.keys.get('left')
71+
backspace = self.keys.get('backspace')
72+
enter = self.keys.get('enter')
73+
74+
padding = self._password_fmt_len + len(self._message)
75+
input = ''
76+
password = ''
77+
buffer = ''
78+
79+
while True:
80+
cursor.write(buffer + self._password_fmt.format(
81+
message=self._message, input=password), color=True)
82+
83+
div, mod = divmod(padding + len(input), self.max_columns)
84+
if not mod:
85+
div -= 1
86+
87+
buffer = ''
88+
for _ in range(div + 1):
89+
buffer = ((ansi.CLEAR_ENTIRE_LINE + ansi.UP_ONE) * div) + \
90+
ansi.CLEAR_ENTIRE_LINE
91+
92+
key = self.listen_for_key()
93+
94+
if key == backspace:
95+
input = input[:-1]
96+
password = password[:-1]
97+
98+
elif key == enter:
99+
cursor.move(x=-self.max_columns)
100+
cursor.write(buffer)
101+
break
102+
103+
elif (key == up) or (key == down) or (key == right) or (key == left):
104+
pass
105+
106+
else:
107+
input += chr(key)
108+
password += '*'
109+
110+
cursor.move(x=-self.max_columns)
111+
112+
self.answer = {self._name: input}
113+
114+
@classmethod
115+
def from_dict(cls, prompt: dict):
116+
"""Creates and instantiates a Password object from a dict object.
117+
118+
Parameters
119+
----------
120+
prompt
121+
A dict that contains a name key and a message key.
122+
123+
Returns
124+
-------
125+
cues.Password
126+
A Password object to retrieve a single response from a user.
127+
"""
128+
129+
name = prompt['name']
130+
message = prompt['message']
131+
return cls(name, message)
132+
133+
134+
def main():
135+
name = 'password'
136+
message = 'Password:'
137+
138+
prompt = Password(name, message)
139+
answer = prompt.send()
140+
print(answer)
141+
142+
143+
if __name__ == '__main__':
144+
main()

tests/test_password.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
tests.test_password
5+
===================
6+
7+
A testing module for `cues.password`.
8+
"""
9+
10+
import pytest
11+
12+
from cues import cursor, password
13+
from cues.password import Password
14+
15+
16+
class TestPassword:
17+
def setup(self):
18+
self.name = 'password'
19+
self.message = 'Password:'
20+
self.message_endswith_alnum = 'Password'
21+
22+
def test_init(self):
23+
cue = Password(self.name, self.message)
24+
25+
assert cue._name == self.name
26+
assert cue._message == self.message
27+
28+
def test_init_with_message_endswith_alnum(self):
29+
cue = Password(self.name, self.message_endswith_alnum)
30+
31+
assert cue._name == self.name
32+
assert cue._message == self.message_endswith_alnum
33+
34+
def test_send(self, monkeypatch):
35+
cue = Password(self.name, self.message)
36+
37+
monkeypatch.setattr(cue, '_draw', lambda: None)
38+
assert cue.answer is None
39+
40+
def test_draw(self, monkeypatch):
41+
cue = Password(self.name, self.message)
42+
up = cue.keys.get('up')
43+
down = cue.keys.get('down')
44+
right = cue.keys.get('right')
45+
left = cue.keys.get('left')
46+
enter = cue.keys.get('enter')
47+
backspace = cue.keys.get('backspace')
48+
49+
def generic_return_none(*args, **kwargs):
50+
return None
51+
52+
moves = [backspace, 49, 50, 51, 52, up, down, right, left, enter]
53+
54+
def mock_listen_for_key():
55+
if moves:
56+
return moves.pop(0)
57+
58+
monkeypatch.setattr(cursor, 'write', generic_return_none)
59+
monkeypatch.setattr(cue, 'listen_for_key', mock_listen_for_key)
60+
monkeypatch.setattr(cursor, 'move', generic_return_none)
61+
62+
assert cue._draw() is None
63+
assert cue.answer == {self.name: '1234'}
64+
65+
def test_from_dict(self):
66+
cue = Password(self.name, self.message)
67+
68+
assert cue._name == self.name
69+
assert cue._message == self.message
70+
71+
# For dev use only (do NOT use with CI):
72+
73+
# def test__draw(self):
74+
# print()
75+
# cue = Password(self.name, self.message)
76+
77+
# answer = cue.send()
78+
# print(answer)
79+
80+
# def test__draw_message_endswith_alnum(self):
81+
# print()
82+
# cue = Password(self.name, self.message_endswith_alnum)
83+
84+
# answer = cue.send()
85+
# print(answer)
86+
87+
88+
def test_main(monkeypatch):
89+
monkeypatch.setattr(Password, 'send', lambda _: None)
90+
assert password.main() is None

0 commit comments

Comments
 (0)