argparse and the Argument Parser

Mocking the argeparse ArgumentParser might not seem like a necessary thing, since you can pass in a list of strings to fake the command-line arguments, but I ran into trouble trying to figure out how to test it embedded in one of my classes so I thought I would explore it anyway, out of curiosity if nothing else. I am primarily interested in mocking the calls to sys.argv to see how it works.

sys.argv Calls

Using the mock_calls list from mock can be useful in figuring out how an object is being used.


parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true')
parser.add_argument('-d')

sys = MagicMock()
with patch('sys.argv', sys):
args = parser.parse_args()
for item in sys.mock_calls:
print item
print args

call.__getitem__(slice(1, None, None))
call.__getitem__().__iter__()
call.__getitem__().__getitem__(slice(0, None, None))
call.__getitem__().__getitem__().__iter__()
call.__getitem__().__getitem__().__len__()
Namespace(d=None, debug=False)

The getitem and slice

The first thing to note is the __getitem__ calls. According to the documentation it is:

Called to implement evaluation of self[key]. For sequence types, the accepted keys should be integers and slice objects.

So it looks like it is first using the built-in slice function to get a particular argument. According to the documentation the arguments are the same as for the range function (start, stop, step).

So it looks like it is doing the equivalent of [1:] in the first slice:


test = [0,1,2]

# what does it do?
print test.__getitem__(slice(1, None, None))

# are they the same?
print test[1:] == test.__getitem__(slice(1, None, None))

[1, 2]
True

One thing to note is that slice(1) is not the same thing as slice(1, None, None):


print slice(1)
print slice(1, None, None)

slice(None, 1, None)
slice(1, None, None)

Trying a lambda

So, if I give __getitem__ a function to return the arguments I want, will this work?


sys.__getitem__ = lambda x,y: ['--debug']
with patch('sys.argv', sys):
args = parser.parse_args()
for item in sys.mock_calls:
print item

print args

call.__getitem__(slice(1, None, None))
call.__getitem__().__iter__()
call.__getitem__().__getitem__(slice(0, None, None))
call.__getitem__().__getitem__().__iter__()
call.__getitem__().__getitem__().__len__()
Namespace(d=None, debug=True)

It looks like it does, but would it be better to just make argv a list?

argv as a list


args = ['--debug']

def getitem(index):
return args[index]

# make a new mock since I set __getitem__ to a lambda function
sys = MagicMock()
sys.__getitem__.side_effect = getitem

with patch('sys.argv', sys):
parsed_args = parser.parse_args()

for item in sys.mock_calls:
print item
print parsed_args

call.__getitem__(slice(1, None, None))
Namespace(d=None, debug=False)

It now does not make the other calls and it also does not set the debug to True, so it did not work.

But I seem to have forgotten my earlier slice check -- it is starting at the second item. I think that normally the name of the program is the first thing passed in so maybe there needs to be an extra (first) entry to simulate the command name.

Adding a Command Name


args = 'commandname --debug'.split()

def getitem(index):
return args[index]

sys.__getitem__.side_effect = getitem

with patch('sys.argv', sys):
parsed_args = parser.parse_args()

for item in sys.mock_calls:
print item
print parsed_args

call.__getitem__(slice(1, None, None))
call.__getitem__(slice(1, None, None))
Namespace(d=None, debug=True)

It looks like it worked, and all but the first two calls went away, so it perhaps they were a result of me using the mock, not a normal part of the way parse_args works.

The Whole Thing

Okay, but what about the option -d?


args = 'commandname -d cow --debug'.split()

def getitem(index):
return args[index]

sys.__getitem__.side_effect = getitem

with patch('sys.argv', sys):
try:
parsed_args = parser.parse_args()
except Exception as error:
print error

for item in sys.mock_calls:
print item
print parsed_args

call.__getitem__(slice(1, None, None))
call.__getitem__(slice(1, None, None))
call.__getitem__(slice(1, None, None))
Namespace(d='cow', debug=True)

Well, that was kind of painful. On the one hand, I got it to work, on the other hand, I do not really know what the slice is doing since it seems to slice the same items over and over. I think, looking at the first set of calls, after the initial slice it manipulates the sliced copy and since I am passing a real list instead of a mock, the calls are now hidden.

Looking at the Code

I downloaded the python 2.7 code and looked in argparse.py and found this:


def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)

There is more to that function, but since it is calling parse_known_args I jumped to it:


def parse_known_args(self, args=None, namespace=None):
if args is None:
# args default to the system args
args = _sys.argv[1:]

Once again there is more code after that, but this explains the slice that is seen in the calls.

Later on it calls:


namespace, args = self._parse_known_args(args, namespace)

So jumping to _parse_known_args:


arg_strings_iter = iter(arg_strings)
for i, arg_string in enumerate(arg_strings_iter):

which I think explains the __iter__ call in the first set of calls. I tried stepping through the code with pudb but could only find one slice, I am not sure what the other calls were for. I suppose it would have been smarter to look at the source code first, but this is about figuring out how to use mock so I think it was helpful to try it empirically first. No fair peeking in the back of the book until you have tried at least once.