diff --git a/userland/down.py b/userland/down.py index 68402bc..c09fa3d 100644 --- a/userland/down.py +++ b/userland/down.py @@ -1,7 +1,7 @@ "xthulu gosub example" async def main(cx, arg1, arg2): - cx.echo('gosub example {} {}\r\n'.format(arg1, arg2)) + cx.echo(f'gosub example {arg1} {arg2}\r\n') with cx.lock('testing') as l: if not l: diff --git a/userland/top.py b/userland/top.py index ddc22ad..1259024 100644 --- a/userland/top.py +++ b/userland/top.py @@ -20,7 +20,7 @@ async def main(cx): value=['testing this thing']) for k in cx.env.keys(): - cx.echo('{} = {}\r\n'.format(k, cx.env[k])) + cx.echo(f'{k} = {cx.env[k]}\r\n') dirty = True @@ -35,7 +35,7 @@ async def main(cx): ev = await cx.events.poll('resize') if ev: - cx.echo('\r\n{}\r\n'.format(ev)) + cx.echo(f'\r\n{ev}\r\n') await cx.events.flush('resize') ks = await cx.term.inkey(1) @@ -58,7 +58,7 @@ async def main(cx): elif ks.code == cx.term.KEY_ENTER: dirty = True - cx.echo('\r\n{}\r\n'.format(led.value[0])) + cx.echo(f'\r\n{led.value[0]}\r\n') else: cx.echo(led.process_keystroke(ks) + cx.term.move_x(0) + diff --git a/xthulu/__main__.py b/xthulu/__main__.py index 496f733..d834707 100644 --- a/xthulu/__main__.py +++ b/xthulu/__main__.py @@ -15,18 +15,18 @@ @click.group() def cli(): - pass + "xthulu terminal server command line utility" @cli.command() def start(): - 'Start SSH server process' + "Start SSH server process" try: log.info('Starting SSH server') loop.run_until_complete(start_server()) except (OSError, asyncssh.Error) as exc: - sys.exit('Error: {}'.format(exc)) + sys.exit(f'Error: {exc}') try: log.info('SSH server is listening') @@ -37,7 +37,7 @@ def start(): @cli.command() def db_create(): - 'Create database tables' + "Create database tables" from . import models @@ -50,7 +50,7 @@ async def f(): @cli.command() def db_init(): - 'Initialize database with starter data' + "Initialize database with starter data" from .models.user import User, hash_password diff --git a/xthulu/context.py b/xthulu/context.py index 50249ac..33cdf2b 100644 --- a/xthulu/context.py +++ b/xthulu/context.py @@ -65,7 +65,11 @@ async def _init(self): self.log.debug(repr(self.user)) def echo(self, text): - "Echo text to the terminal" + """ + Echo text to the terminal + + :param str text: The text to echo + """ if text is None: return @@ -73,20 +77,38 @@ def echo(self, text): self.proc.stdout.write(text.encode(self.encoding)) async def gosub(self, script, *args, **kwargs): - "Execute script and return result" + """ + Execute script and return result + + :param :class:`xthulu.structs.Script` script: The userland script to + execute + :returns: The return value from the script (if any) + :rtype: mixed + """ script = Script(script, args, kwargs) return await self.runscript(script) def goto(self, script, *args, **kwargs): - "Switch to script and clear stack" + """ + Switch to script and clear stack + + :param :class:`xthulu.structs.Script` script: The userland script to + execute + """ raise Goto(script, *args, **kwargs) @contextmanager def lock(self, name): - "Session lock context manager" + """ + Session lock context manager + + :param str name: The name of the lock to attempt + :returns: Whether or not the lock was granted + :rtype: bool + """ try: yield locks.get(self.sid, name) @@ -94,17 +116,35 @@ def lock(self, name): locks.release(self.sid, name) def get_lock(self, name): - "Acquire lock on behalf of session user" + """ + Acquire lock on behalf of session user + + :param str name: The name of the lock to attempt + :returns: Whether or not the lock was granted + :rtype: bool + """ return locks.get(self.sid, name) def release_lock(self, name): - "Release lock on behalf of session user" + """ + Release lock on behalf of session user + + :param str name: The name of the lock to attempt + :returns: Whether or not the lock was granted + :rtype: bool + """ return locks.release(self.sid, name) async def redirect(self, proc): - "Redirect context IO to other process" + """ + Redirect context IO to other process; convenience method which wraps + AsyncSSH's redirection routine + + :param mixed proc: The process to redirect to; can be tuple, list, + str, or :class:`multiprocessing.Popen` + """ @singledispatch async def f(proc): @@ -130,9 +170,15 @@ async def _(proc): return await f(proc) async def runscript(self, script): - "Run script and return result; used by :meth:`goto` and :meth:`gosub`" + """ + Run script and return result; used by :meth:`goto` and :meth:`gosub` + + :param `xthulu.structs.Script` script: The userland script to run + :returns: The return value of the script (if any) + :rtype: mixed + """ - self.log.info('Running {}'.format(script)) + self.log.info(f'Running {script}') split = script.name.split('.') found = None mod = None @@ -152,7 +198,7 @@ async def runscript(self, script): except Exception as exc: self.log.exception(exc) self.echo(self.term.bold_red_on_black( - '\r\nException in {}\r\n'.format(script.name))) + '\r\nException in {script.name}\r\n')) await aio.sleep(3) diff --git a/xthulu/events.py b/xthulu/events.py index b5b626f..eaa0d9b 100644 --- a/xthulu/events.py +++ b/xthulu/events.py @@ -47,7 +47,7 @@ async def flush(self, event_name=None): """ Flush the event queue - :param str event_name: (Optional) The event name to filter + :param str event_name: The event name to filter (if any) """ popped = [] diff --git a/xthulu/locks.py b/xthulu/locks.py index 4043591..d9c725e 100644 --- a/xthulu/locks.py +++ b/xthulu/locks.py @@ -7,17 +7,27 @@ class Locks(object): + + "Lock storage singleton" + locks = set([]) owned = {} def get(owner, name): - "Acquire and hold lock on behalf of user/system" + """ + Acquire and hold lock on behalf of user/system + + :param str owner: The name of the owner + :param str name: The name of the lock + :returns: Whether or not the lock was granted + :rtype: bool + """ - log.debug('{} getting lock {}'.format(owner, name)) + log.debug(f'{owner} getting lock {name}') if name in Locks.locks: - log.debug('{} lock already exists'.format(name)) + log.debug(f'{name} lock already exists') return False @@ -34,17 +44,24 @@ def get(owner, name): def release(owner, name): - "Release a lock owned by user/system" + """ + Release a lock owned by user/system - log.debug('{} releasing lock {}'.format(owner, name)) + :param str owner: The name of the owner + :param str name: The name of the lock + :returns: Whether or not the lock was valid to begin with + :rtype: bool + """ + + log.debug(f'{owner} releasing lock {name}') if name not in Locks.locks: - log.debug('{} lock does not exist'.format(name)) + log.debug(f'{name} lock does not exist') return False if owner not in Locks.owned or name not in Locks.owned[owner]: - log.debug('{} does not own lock {}'.format(owner, name)) + log.debug(f'{owner} does not own lock {name}') return False @@ -58,7 +75,14 @@ def release(owner, name): @contextmanager def hold(owner, name): - "Session-agnostic lock context manager" + """ + Session-agnostic lock context manager + + :param str owner: The name of the owner + :param str name: The name of the lock + :returns: Whether or not the lock was granted + :rtype: bool + """ try: yield get(owner, name) @@ -67,9 +91,13 @@ def hold(owner, name): def expire(owner): - "Remove all locks owned by user" + """ + Remove all locks owned by user + + :param str owner: The name of the owner + """ - log.debug('Releasing locks owned by {}'.format(owner)) + log.debug(f'Releasing locks owned by {owner}') locks = 0 owned = None @@ -78,7 +106,7 @@ def expire(owner): locks = len(owned) if locks == 0: - log.debug('No locks for {}'.format(owner)) + log.debug(f'No locks for {owner}') else: for l in owned: release(owner, l) diff --git a/xthulu/models/user.py b/xthulu/models/user.py index 1ff29ae..7db87fd 100644 --- a/xthulu/models/user.py +++ b/xthulu/models/user.py @@ -21,7 +21,7 @@ class User(db.Model): def __repr__(self): 'Represent as str' - return 'User({}#{})'.format(self.name, self.id) + return f'User({self.name}#{self.id})' idx_name_lower = db.Index('idx_user_name_lower', func.lower(User.name)) diff --git a/xthulu/ssh.py b/xthulu/ssh.py index 6173e87..e8cabf4 100644 --- a/xthulu/ssh.py +++ b/xthulu/ssh.py @@ -32,7 +32,7 @@ def connection_made(self, conn): self._peername = conn.get_extra_info('peername') self._sid = '{}:{}'.format(*self._peername) EventQueues.q[self._sid] = aio.Queue() - log.info('{} connecting'.format(self._peername[0])) + log.info(f'{self._peername[0]} connecting') def connection_lost(self, exc): "Connection closed" @@ -41,10 +41,9 @@ def connection_lost(self, exc): locks.expire(self._sid) if exc: - log.error('Error: {}'.format(exc)) + log.error(f'Error: {exc}') - log.info('{}@{} disconnected'.format(self._username, - self._peername[0])) + log.info(f'{self._username}@{self._peername[0]} disconnected') def begin_auth(self, username): "Check for auth bypass" @@ -54,10 +53,10 @@ def begin_auth(self, username): if ('no_password' in config['ssh']['auth'] and username in config['ssh']['auth']['no_password']): - log.info('No password required for {}'.format(username)) + log.info(f'No password required for {username}') pwd_required = False - log.info('{}@{} connected'.format(username, self._peername[0])) + log.info(f'{username}@{self._peername[0]} connected') return pwd_required @@ -73,19 +72,18 @@ async def validate_password(self, username, password): .gino.first()) if u is None: - log.warn('User {} does not exist'.format(username)) + log.warn(f'User {username} does not exist') return False expected, _ = hash_password(password, u.salt) if expected != u.password: - log.warn('Invalid credentials received for {}' - .format(username)) + log.warn(f'Invalid credentials received for {username}') return False - log.info('Valid credentials received for {}'.format(username)) + log.info(f'Valid credentials received for {username}') return True @@ -140,18 +138,18 @@ def terminal_process(): return if debug_term: - log.debug('proxy received: {}'.format(inp)) + log.debug(f'proxy received: {inp}') attr = getattr(term, inp[0]) if callable(attr) or len(inp[1]) or len(inp[2]): if debug_term: - log.debug('{} callable'.format(inp[0])) + log.debug(f'{inp[0]} callable') term_pipe.send(attr(*inp[1], **inp[2])) else: if debug_term: - log.debug('{} not callable'.format(inp[0])) + log.debug('{inp[0]} not callable') term_pipe.send(attr) diff --git a/xthulu/structs.py b/xthulu/structs.py index 5f2b5a4..66a1436 100644 --- a/xthulu/structs.py +++ b/xthulu/structs.py @@ -3,5 +3,7 @@ # stdlib from collections import namedtuple +#: Represents an event and its accompanying data EventData = namedtuple('EventData', ('name', 'data',)) +#: Represents a userland Python script Script = namedtuple('Script', ('name', 'args', 'kwargs',)) diff --git a/xthulu/terminal.py b/xthulu/terminal.py index bbd0402..5b31373 100644 --- a/xthulu/terminal.py +++ b/xthulu/terminal.py @@ -16,6 +16,8 @@ class Terminal(BlessedTerminal): + "Custom-tailored :class:`blessed.Terminal` implementation" + def __init__(self, kind, stream): super().__init__(kind, stream, force_styling=True) self._keyboard_fd = 'defunc' @@ -24,19 +26,33 @@ def __init__(self, kind, stream): @contextmanager def raw(self): + "Dummy method for compatibility" + yield @contextmanager def cbreak(self): + "Dummy method for compatibility" + yield @property def is_a_tty(self): + "Dummy method for compatibility" + return True class TerminalProxy(object): + """ + Asynchronous terminal proxy object; provides an asyncio interface to + :class:`blessed.Terminal`; also shuttles calls to the + :class:`xthulu.terminal.Terminal` object via IPC to avoid a bug in + Python's curses implementation that only allows one terminal type per + process to be registered + """ + _kbdbuf = [] # Terminal attributes that do not accept paramters must be treated # specially, or else they have to be called like term.clear() everywhere @@ -57,7 +73,7 @@ def wrap(*args, **kwargs): return self._wrap(attr, *args, **kwargs) if debug_term: - log.debug('wrapping {} for proxy'.format(attr)) + log.debug(f'wrapping {attr} for proxy') isfunc = True @@ -72,20 +88,26 @@ def wrap(*args, **kwargs): return wrap def _wrap(self, attr, *args, **kwargs): + "Convenience method for wrapping specific calls" + self._proxy.send((attr, args, kwargs)) out = self._proxy.recv() if debug_term: - log.debug('{} => {}'.format(attr, out)) + log.debug(f'{attr} => {out}') return out @property def height(self): + "Use private height variable instead of WINSZ" + return self._height @property def width(self): + "Use private width variable instead of WINSZ" + return self._width async def inkey(self, timeout=None, esc_delay=0.35): @@ -143,3 +165,5 @@ async def inkey(self, timeout=None, esc_delay=0.35): self._kbdbuf.append(c) return ks + + inkey.__doc__ = f'(asyncio) {BlessedTerminal.inkey.__doc__}' diff --git a/xthulu/ui/editors.py b/xthulu/ui/editors.py index 6ca1e0e..d4e044a 100644 --- a/xthulu/ui/editors.py +++ b/xthulu/ui/editors.py @@ -41,7 +41,11 @@ def __init__(self, term, rows, width, **kwargs): @property def color(self): - "Color property; setting it also sets the internal Terminal callable" + """ + Color property; setting it also sets the internal Terminal callable + + :rtype: callable + """ return getattr(self.term, self._color_str) @@ -55,6 +59,7 @@ def redraw(self, redraw_cursor=True): Output sequence to redraw editor :param bool redraw_cursor: Redraw cursor position as well + :rtype: str """ out = '' @@ -91,6 +96,8 @@ def redraw_cursor(self): """ Output sequence to restore cursor position; assumes cursor is already located at top-left of editor + + :rtype: str """ out = '' @@ -104,7 +111,13 @@ def redraw_cursor(self): return out def process_keystroke(self, ks): - "Process keystroke and produce output (if any)" + """ + Process keystroke and produce output (if any) + + :param blessed.keyboard.Keystroke ks: Keystroke object + (e.g. from :meth:`inkey`) + :rtype: str + """ row = self.value[self.pos[0]] before = row[:self.pos[1]] @@ -171,7 +184,12 @@ def __init__(self, term, width, *args, **kwargs): @property def rows(self): - "Always 1 row" + """ + A line editor only has a single row + + :rtype: int + """ + return 1 @rows.setter