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
StringIOclass - 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 csvHere, 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')