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')