Skip to content

Instantly share code, notes, and snippets.

@embray
Created October 1, 2014 17:20
Show Gist options
  • Save embray/3dc1dcb0d264bce912fb to your computer and use it in GitHub Desktop.
Save embray/3dc1dcb0d264bce912fb to your computer and use it in GitHub Desktop.
That time I reimplemented/replaced the raw_input builtin so I could log stdin.
PY3K = sys.version_info[:2] >= (3, 0)
# The global_logging system replaces the raw_input builtin (input on Python 3)
# for two reasons:
#
# 1) It's the easiest way to capture the raw_input prompt and subsequent user
# input to the log.
#
# 2) On Python 2.x raw_input() does not play nicely with GUI toolkits if
# sys.stdout has been replaced by a non-file object (as global_logging
# does). The default raw_input() implementation first checks that
# sys.stdout and sys.stdin are connected to a terminal. If so it uses the
# PyOS_Readline() implementation, which allows a GUI's event loop to run
# while waiting for user input via PyOS_InputHook(). However, if
# sys.stdout is not attached to a terminal, raw_input() uses
# PyFile_GetLine(), which blocks until a line is entered on sys.stdin,
# thus preventing the GUI from updating. It doesn't matter if sys.stdin is
# still attached to the terminal even if sys.stdout isn't, nor does it
# automatically fall back on sys.__stdout__ and sys.__stdin__.
#
# This replacement raw_input() reimplements most of the built in
# raw_input(), but is aware that sys.stdout may have been replaced and
# knows how to find the real stdout if so.
#
# Note that this is a non-issue in Python 3 which has a new implementation
# in which it doesn't matter what sys.stdout points to--only that it has a
# fileno() method that returns the correct file descriptor for the
# console's stdout.
if not PY3K:
import os
import __builtin__ as builtins
from ctypes import pythonapi, py_object, c_void_p, c_char_p, c_int
# PyFile_AsFile returns a FILE * from a python file object.
# This is used later with pythonapi.PyOS_Readline to perform
# the readline.
pythonapi.PyFile_AsFile.argtypes = (py_object,)
pythonapi.PyFile_AsFile.restype = c_void_p
pythonapi.PyOS_Readline.argtypes = (c_void_p, c_void_p, c_char_p)
pythonapi.PyOS_Readline.restype = c_char_p
def global_logging_raw_input(prompt):
def get_stream(name):
if hasattr(sys, name):
stream = getattr(sys, name)
if isinstance(stream, file):
return stream
elif isinstance(stream, StreamTeeLogger):
return stream.stream
if hasattr(sys, '__%s__' % name):
stream = getattr(sys, '__%s__' % name)
if isinstance(stream, file):
return stream
return None
def check_interactive(stream, name):
try:
fd = stream.fileno()
except:
# Could be an AttributeError, an OSError, and IOError, or who
# knows what else...
return False
realfd = {'stdin': 0, 'stdout': 1, 'stderr': 2}[name]
return fd == realfd and os.isatty(fd)
stdout = get_stream('stdout')
stdin = get_stream('stdin')
stderr = get_stream('stderr')
if stdout is None:
raise RuntimeError('raw_input(): lost sys.stdout')
if stdin is None:
raise RuntimeError('raw_input(): lost sys.stdin')
if stderr is None:
raise RuntimeError('raw_input(): lost sys.stderr')
if (not check_interactive(stdin, 'stdin') or
not check_interactive(stdout, 'stdout')):
# Use the built-in raw_input(); this will repeat some of the checks
# we just did, but will save us from having to reimplement
# raw_input() in its entirety
retval = builtins._original_raw_input(prompt)
else:
stdout.flush()
infd = pythonapi.PyFile_AsFile(stdin)
outfd = pythonapi.PyFile_AsFile(stdout)
retval = pythonapi.PyOS_Readline(infd, outfd, str(prompt))
retval = retval.rstrip('\n')
if isinstance(sys.stdout, StreamTeeLogger):
sys.stdout.log_orig(str(prompt) + retval, echo=False)
return retval
else:
import builtins
def global_logging_raw_input(prompt):
retval = builtins._original_raw_input(prompt)
if isinstance(sys.stdout, StreamTeeLogger):
sys.stdout.log_orig(str(prompt) + retval, echo=False)
return retval
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment