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

Typed dialog_wrapper #55

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
100 changes: 56 additions & 44 deletions libinithooks/dialog_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import traceback
from io import StringIO
from os import environ
from pathlib import Path
import logging
from typing import Optional

EMAIL_RE = re.compile(r"(?:^|\s).*\S@\S+(?:\s|$)", re.IGNORECASE)

Expand All @@ -19,26 +19,26 @@
filename='/var/log/dialog.log',
encoding='utf-8',
level=LOG_LEVEL
)
)


class Error(Exception):
pass


def password_complexity(password):
def password_complexity(password: str) -> int:
"""return password complexity score from 0 (invalid) to 4 (strong)"""

lowercase = re.search('[a-z]', password) is not None
uppercase = re.search('[A-Z]', password) is not None
number = re.search('\d', password) is not None
nonalpha = re.search('\W', password) is not None
number = re.search(r'\d', password) is not None
nonalpha = re.search(r'\W', password) is not None

return sum([lowercase, uppercase, number, nonalpha])


class Dialog:
def __init__(self, title, width=60, height=20):
def __init__(self, title: str, width: int = 60, height: int = 20) -> None:
self.width = width
self.height = height

Expand All @@ -47,41 +47,45 @@ def __init__(self, title, width=60, height=20):
self.console.add_persistent_args(["--backtitle", title])
self.console.add_persistent_args(["--no-mouse"])

def _handle_exitcode(self, retcode):
def _handle_exitcode(self, retcode: int) -> bool:
logging.debug(f"_handle_exitcode(retcode={retcode!r})")
if retcode == self.console.ESC: # ESC, ALT+?
text = "Do you really want to quit?"
if self.console.yesno(text) == self.console.OK:
sys.exit(0)
return False

logging.debug("_handle_exitcode(): [no conditions met, returning True]")
logging.debug("_handle_exitcode():"
" [no conditions met, returning True]")
return True

def _calc_height(self, text):
def _calc_height(self, text: str) -> int:
height = 6
for line in text.splitlines():
height += (len(line) // self.width) + 1

return height

def wrapper(self, dialog_name, text, *args, **kws):
def wrapper(self, dialog_name: str, text: str, *args, **kws
) -> tuple[int, str]:
retcode = 0
logging.debug(
f"wrapper(dialog_name={dialog_name!r}, text=<redacted>,"
+f" *{args!r}, **{kws!r})")
f" *{args!r}, **{kws!r})")
try:
method = getattr(self.console, dialog_name)
except AttributeError as e:
logging.error(
f"wrapper(dialog_name={dialog_name!r}, ...) raised exception",
exc_info=e)
f"wrapper(dialog_name={dialog_name!r}, ...) raised exception",
exc_info=e)
raise Error("dialog not supported: " + dialog_name)

while 1:
try:
retcode = method("\n" + text, *args, **kws)
logging.debug(
f"wrapper(dialog_name={dialog_name!r}, ...) -> {retcode!r}")
f"wrapper(dialog_name={dialog_name!r}, ...)"
f" -> {retcode!r}")

if self._handle_exitcode(retcode):
break
Expand All @@ -94,38 +98,40 @@ def wrapper(self, dialog_name, text, *args, **kws):
exc_info=e)
self.msgbox("Caught exception", sio.getvalue())

return retcode
return retcode, ''

def error(self, text):
def error(self, text: str) -> tuple[int, str]:
height = self._calc_height(text)
return self.wrapper("msgbox", text, height, self.width, title="Error")

def msgbox(self, title, text):
def msgbox(self, title: str, text: str) -> tuple[int, str]:
height = self._calc_height(text)
logging.debug(f"msgbox(title={title!r}, text=<redacted>)")
return self.wrapper("msgbox", text, height, self.width, title=title)

def infobox(self, text):
def infobox(self, text: str) -> tuple[int, str]:
height = self._calc_height(text)
logging.debug(f"infobox(text={text!r}")
return self.wrapper("infobox", text, height, self.width)

def inputbox(self, title, text, init='', ok_label="OK",
cancel_label="Cancel"):
def inputbox(self, title: str, text: str, init: str = '',
ok_label: str = "OK", cancel_label: str = "Cancel"
) -> tuple[int, str]:
logging.debug(
f"inputbox(title={title!r}, text=<redacted>,"
+f" init={init!r}, ok_label={ok_label!r},"
+f" cancel_label={cancel_label!r})")
f" init={init!r}, ok_label={ok_label!r},"
f" cancel_label={cancel_label!r})")

height = self._calc_height(text) + 3
no_cancel = True if cancel_label == "" else False
logging.debug(
f"inputbox(...) [calculated height={height}, no_cancel={no_cancel}]")
logging.debug(f"inputbox(...) [calculated height={height},"
f" no_cancel={no_cancel}]")
return self.wrapper("inputbox", text, height, self.width, title=title,
init=init, ok_label=ok_label,
cancel_label=cancel_label, no_cancel=no_cancel)

def yesno(self, title, text, yes_label="Yes", no_label="No"):
def yesno(self, title: str, text: str, yes_label: str = "Yes",
no_label: str = "No") -> bool:
height = self._calc_height(text)
retcode = self.wrapper("yesno", text, height, self.width, title=title,
yes_label=yes_label, no_label=no_label)
Expand All @@ -135,7 +141,7 @@ def yesno(self, title, text, yes_label="Yes", no_label="No"):
f" -> {retcode}")
return True if retcode == 'ok' else False

def menu(self, title, text, choices):
def menu(self, title: str, text: str, choices: str) -> str:
"""choices: array of tuples
[ (opt1, opt1_text), (opt2, opt2_text) ]
"""
Expand All @@ -145,21 +151,24 @@ def menu(self, title, text, choices):
no_cancel=True)
return choice

def get_password(self, title, text, pass_req=8,
min_complexity=3, blacklist=[]):
def get_password(self, title: str, text: str, pass_req: int = 8,
min_complexity: int = 3,
blacklist: Optional[list[str]] = None) -> Optional[str]:
if not blacklist:
blacklist = []
req_string = (
f'\n\nPassword Requirements\n - must be at least {pass_req}'
+' characters long\n - must contain characters from at'
+f' least {min_complexity} of the following categories: uppercase,'
+' lowercase, numbers, symbols'
' characters long\n - must contain characters from at'
f' least {min_complexity} of the following categories: uppercase,'
' lowercase, numbers, symbols'
)
if blacklist:
req_string = (
f'{req_string}. Also must NOT contain these characters:'
f' {" ".join(blacklist)}')
height = self._calc_height(text+req_string) + 3

def ask(title, text):
def ask(title, text: str) -> str:
return self.wrapper('passwordbox', text+req_string, height,
self.width, title=title, ok_label='OK',
no_cancel='True', insecure=True)[1]
Expand All @@ -172,25 +181,26 @@ def ask(title, text):

if isinstance(pass_req, int):
if len(password) < pass_req:
self.error(f"Password must be at least {pass_req} characters.")
self.error(f"Password must be at least"
f" {pass_req} characters.")
continue
else:
if not re.match(pass_req, password):
self.error("Password does not match complexity"
+" requirements.")
" requirements.")
continue

if password_complexity(password) < min_complexity:
if min_complexity <= 3:
self.error("Insecure password! Mix uppercase, lowercase,"
+" and at least one number. Multiple words and"
+" punctuation are highly recommended but not"
+" strictly required.")
" and at least one number. Multiple words and"
" punctuation are highly recommended but not"
" strictly required.")
elif min_complexity == 4:
self.error("Insecure password! Mix uppercase, lowercase,"
+" numbers and at least one special/punctuation"
+" character. Multiple words are highly"
+" recommended but not strictly required.")
" numbers and at least one special/punctuation"
" character. Multiple words are highly"
" recommended but not strictly required.")
continue

found_items = []
Expand All @@ -199,8 +209,8 @@ def ask(title, text):
found_items.append(item)
if found_items:
self.error(
f'Password can NOT include these characters: {blacklist}.'
+f' Found {found_items}')
f'Password can NOT include these characters: {blacklist}.'
f' Found {found_items}')
continue

if password == ask(title, 'Confirm password'):
Expand All @@ -209,7 +219,8 @@ def ask(title, text):
self.error('Password mismatch, please try again.')

def get_email(self, title, text, init=''):
logging.debug(f'get_email(title={title!r}, text=<redacted>, init={init!r})')
logging.debug(
f'get_email(title={title!r}, text=<redacted>, init={init!r})')
while 1:
email = self.inputbox(title, text, init, "Apply", "")[1]
logging.debug(f'get_email(...) email={email!r}')
Expand All @@ -223,7 +234,8 @@ def get_email(self, title, text, init=''):

return email

def get_input(self, title, text, init=''):
def get_input(self, title: str, text: str, init: str = ''
) -> Optional[str]:
while 1:
s = self.inputbox(title, text, init, "Apply", "")[1]
if not s:
Expand Down