HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Read stdin like a dictator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
stdinlikedictatorread

Problem

Rags.

Introduction

All too often I find myself wanting to allow only a certain list of characters to be written to stdin, and only recently did I actually bother to implement it. In Python, of all languages!

Essentially, this module provides a few APIs that allow a very tailored approach to reading characters from a terminal. By intercepting individual keypresses at the instant they occur, we can make cool decisions about closing the input stream whenever we want -- we dictate who says what in our terminal!

The standard input stream, after being opened, can be closed after a number of characters, or, at caller's option, any combination of a number of characters and allowed inputs.

API Overview

Outward interface

-
func read_single_keypress() -> string — cross-platform function that gets exactly one keypress, and returns it after processing.

-
func thismany(count: int = -1) -> string — get exactly count characters from stdin. if count is -1, then sys.maxsize chars will be read.

-
func until(chars: string || list, count: int = -1) -> string — get characters from stdin until char is read, or until count chars have been read. if count is -1, sys.maxsize chars will be read.

-
func until_not(chars: string || list, count: int = -1) -> string — get characters from stdin until any of chars is not read. if count is -1, then sys.maxsize chars will be read.

-
func pretty_press() -> string — read a char from stdin, and send it through the same processing as the other functions here do — write it, then if it's a backspace write a backspace, etc

Inward interface

-
class _Getch: determines system platform and calls one of _GetchUnix or _GetchWindows appropriately

-
class _GetchUnix: get a raw character from stdin, on any *nx box

-
class _GetchWindows: get a raw character from stdin, on any Windows box

-
func nbsp: do stuff accordingly for certain chars of input; handles backspace, etc

-
func parsenum: retur

Solution

I really don't like that you detect which version of _GetchX to use via an ImportError - that isn't obvious to me at all. I also don't like that you keep importing things locally. I think you can solve this like so:

import platform
system = platform.system()    
import sys

if system == "Windows":
    import msvcrt

    class _Getch:
        """Gets a single character from standard input."""

        def __call__(self):
            return msvcrt.getch()

else:
    import tty, termios

    class _Getch: 
        """Gets a single character from standard input."""

        def __call__(self):
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                ch = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch


If that level of repetition is also upsetting, move the if inside of the __call__ implementation, but that seems like too much. You could try this instead:

class _Getch:
    """Gets a single character from standard input."""

if system == "Windows":
    import msvcrt

    def _call(self):
        return msvcrt.getch()

else:
    import tty, termios

    def _call(self):
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(sys.stdin.fileno())
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
           return ch

_Getch.__call__ = _call


More generally, however, I don't understand why this is a class. Just make it a function.

if system == "Windows":
    import msvcrt

    def getch():
        return msvcrt.getch()

else:
    import tty, termios

    def getch():
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(sys.stdin.fileno())
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
           return ch


Why is this a lambda?

parsenum = (lambda num:
        (sys.maxsize if 0 > num else num))


Just make a normal function, for readability if nothing else.

def parsenum(num):
    return sys.maxsize if 0 > num else num


General thoughts on the rest of your code:

Why are you explicitly calling __call__()? Just call it normally (or just use the function, as above). You also have a lot of magic numbers, which to someone who is not immediately familiar with their ASCII codes (I have literally never been able to remember any ASCII code off the top of my head) isn't helpful - prefer named constants here. Instead of ox == 27 or ox == 127 just do ox in [27, 127] (and replace that with some constant). You shouldn't need to explicitly turn chars into a list, unless you expect that it is some bizarre iterable type that doesn't implement __getitem__.

You mentioned in the comments that you assign the result of sys.stdout.write(i) because it will append the result to the output otherwise. I had no idea that happens, and it seems pretty odd, but it looks like a code smell to anyone (like myself) who doesn't know that it happens. You could put a comment everywhere you do that, but that is annoying and you might forget if you add it in the future. I'd write a simple helper method

def write_to_stdout(i):
    # Have to assign or it will append the result to the output
    _ = sys.stdout.write(i)
    sys.stdout.flush()


until and until_not are basically identical - they could be condensed to this:

def _until_condition(chars, condition, count) -> str:
    y = []
    count = parsenum(count)
    while len(y)  str:
    """get chars of stdin until any of chars is read,
    or until count chars have been read, whichever comes first"""

    return _until_condition(chars, lambda i, chars: i in chars, count)

def until_not(chars, count=-1) -> str:
    """read stdin until any of chars stop being read,
    or until count chars have been read; whichever comes first"""

    return _until_condition(chars, lambda i, chars: i not in chars, count)

Code Snippets

import platform
system = platform.system()    
import sys

if system == "Windows":
    import msvcrt

    class _Getch:
        """Gets a single character from standard input."""

        def __call__(self):
            return msvcrt.getch()

else:
    import tty, termios

    class _Getch: 
        """Gets a single character from standard input."""

        def __call__(self):
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                ch = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch
class _Getch:
    """Gets a single character from standard input."""

if system == "Windows":
    import msvcrt

    def _call(self):
        return msvcrt.getch()

else:
    import tty, termios

    def _call(self):
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(sys.stdin.fileno())
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
           return ch

_Getch.__call__ = _call
if system == "Windows":
    import msvcrt

    def getch():
        return msvcrt.getch()

else:
    import tty, termios

    def getch():
       fd = sys.stdin.fileno()
       old_settings = termios.tcgetattr(fd)
       try:
           tty.setraw(sys.stdin.fileno())
           ch = sys.stdin.read(1)
       finally:
           termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
           return ch
parsenum = (lambda num:
        (sys.maxsize if 0 > num else num))
def parsenum(num):
    return sys.maxsize if 0 > num else num

Context

StackExchange Code Review Q#118721, answer score: 8

Revisions (0)

No revisions yet.