Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recovering original argv

When a script is invoked explicitly with python, the argv is mucked with so that argv[0] is the path to the script being run. This is the case if invoked as python foo/bar.py or even as python -m foo.bar.

I need a way to recover the original argv (ie. the one received by python). Unfortunately, it's not as easy as prepending sys.executable to sys.argv because python foo/bar.py is different than python -m foo.bar (the implicit PYTHONPATH differs, which can be crucial depending on your module structure).

More specifically in the cases of python foo/bar.py some other args and python -m foo.bar some other args, I'm looking to recover ['python', 'foo/bar.py', 'some', 'other', 'args'] and ['python', '-m', 'foo.bar', 'some', 'other', 'args'], respectively.

I am aware of prior questions about this:

  • how to get the ORIGINAL command line in python? with spaces, tabs, etc
  • Full command line as it was typed

But these seem to have a misunderstanding of how shells work and the answers reflect this. I am not interested in undoing the work of the shell (eg. evaluated shell vars and functions are fine), I just want to get at the original argv given to python.

The only solution I've found is to use /proc/<PID>/cmdline:

import os
with open("/proc/{}/cmdline".format(os.getpid()), 'rb') as f:
  original_argv = f.read().split('\0')[:-1]

This does work, but it is Linux-only (no OSX, and Windows support seems to require installing the wmi package). Fortunately for my current use case this restriction is fine. But, it would be nice to have a cleaner, cross platform approach.

The fact that that /proc/<PID>/cmdline approach works gives me hope that python isn't execing before it runs the script (at least not the syscall exec, but maybe the exec builtin). I remember reading somewhere that all of this argument handling (ex. -m) is done in pure python, not C (this is confirmed by the fact that python -m this.does.not.exist will produce an exception that looks like it came from the runtime). So, I'd venture a guess that somewhere in pure python the original argv is available (perhaps this requires some spelunking through the runtime initialization?).

tl;dr Is there a cross platform (builtin, preferably) way to get at the original argv passed to python (before it remove the python executable and transforms -m blah into blah.py)?

edit From spelunking, I discovered Py_GetArgcArgv, which can be accessed via ctypes (found it here, links to several SO posts that mention this approach):

import ctypes

_argv = ctypes.POINTER(ctypes.c_wchar_p)()
_argc = ctypes.c_int()

ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(_argc),
                                ctypes.byref(_argv))

argv = _argv[:_argc.value]
print(argv)

Now this is OS-portable, but not python implementation portable (only works on cpython and ctypes is yucky if you don't need it). Also, peculiarly, I don't get the right output on Ubunutu 16.04 (python -m foo.bar gives me ['python', '-m', '-m']), but I may just be making a silly mistake (I get the same behavior on OSX). It would be great to have a fully portable solution (that doesn't dig into ctypes).

like image 352
Bailey Parker Avatar asked Mar 22 '18 13:03

Bailey Parker


1 Answers

This seems XY problem and you are getting into the weeds in order to accommodate some existing complicated test setup (I've found the question behind the question in your comment). Further efforts would be better spent writing a sane test setup.

  1. Use a better test runner, not unittest.
  2. Create any initial state within the test setup, not in the external environment before entering the Python runtime.
  3. Use a plugin for the randomization and seed stuff, personally I use this one but there are others.

For example if you decide to go with pytest runner, all the test setup can be configured within a [tool:pytest] section of the setup.cfg file and/or in fixture setup (conftest.py). Overriding the default test configuration can be done with environment variables and/or command line arguments, and neither of these approaches will get mucked around by the shell or during Python interpreter startup.

The manner in which to execute the test suite can and should be as simple as executing a single command:

pytest

And then your perceived problem of needing to recover the original sys.argv will go away.

like image 152
wim Avatar answered Oct 15 '22 10:10

wim