Simulating stdin Inputs from User

I recently ran into a problem where I was trying to automate unit testing for a function that paused, mid-execution, and waited for a user to input some value.

For example

def dummy_fn():
    name = input()
    return('Hello, ', name)
dummy_fn()
Nick





('Hello, ', 'Nick')

Simulating a user input wound up being a non-trivial thing to figure out, so I figured it beared writing a note involving:

  • The StringIO class
  • Temporarily overwriting sys.stdin

StringIO

io.StringIO is used to convert your typical string into something that can be read as a stream of inputs, much like reading lines in a file.

For instance, imagine we’re trying to simulate loading a .csv

from io import StringIO
import csv

Here, the iterator is considering each unique character. Because every other “line” is “a comma that splits two empty strings” we get a bunch of nonsense.

for char in csv.reader('a,b,c,d,e'):
    print(char)
['a']
['', '']
['b']
['', '']
['c']
['', '']
['d']
['', '']
['e']

Whereas using StringIO, we can tell python to read our input as one unified line.

f = csv.reader(StringIO('a,b,c,d,e'))
for char in f:
    print(char)
['a', 'b', 'c', 'd', 'e']

Temporarily Overwriting sys.stdin

sys.stdin is the default that gets called when you find yourself writing something that that uses the standard input() function. It’s got this cryptic TextIOWrapper for a repr, but it essentially takes whatever a user submits to standard in by typing and hitting Enter.

import sys
sys.stdin
<_io.TextIOWrapper name='<stdin>' mode='r' encoding='cp1252'>

But if we inspect, this TextIOWrapper inherits from the same base class as StringIO

sys.stdin.__class__.__base__
_io._TextIOBase
StringIO.__base__
_io._TextIOBase

Meaning we can leverage the same underlying functionality if we spoof a function designed to call sys.stdin. In this case, input()

with StringIO('asdf') as f:
    stdin = sys.stdin
    sys.stdin = f
    print("'" + input() + "' wasn't actually typed at the command line")
    sys.stdin = stdin
 'asdf' wasn't actually typed at the command line

(Note: Because Jupyter Notebooks use a different stdin scheme, this, ironically, is just markdown. But running it in IPython or regular ol’ Python works just fine)

Putting it All Together

with StringIO('Nick') as f:
    stdin = sys.stdin
    sys.stdin = f
    dummy_fn()
    sys.stdin = stdin
('Hello', 'Nick')