19

I need to run a script foo.py, but I need to also insert some debugging lines to run before the code in foo.py. Currently I just put those lines in foo.py and I'm careful not to commit that to Git, but I don't like this solution.

What I want is a separate file bar.py that I don't commit to Git. Then I want to run:

python /somewhere/bar.py /somewhere_else/foo.py

What I want this to do is first run some lines of code in bar.py, and then run foo.py as __main__. It should be in the same process that the bar.py lines ran in, otherwise the debugging lines won't help.

Is there a way to make bar.py do this?

Someone suggested this:

import imp
import sys

# Debugging code here

fp, pathname, description = imp.find_module(sys.argv[1])
imp.load_module('__main__', fp, pathname, description)

The problem with that is that because it uses import machinery, I need to be on the same folder as foo.py to run that. I don't want that. I want to simply put in the full path to foo.py.

Also: The solution needs to work with .pyc files as well.

Ram Rachum
  • 84,019
  • 84
  • 236
  • 374
  • Can you make an alias and pipe it into your actual call? E.g. alias `pytester=python -m bar.py`, then call `pytester | python foo.py`. You would need to get foo.py to accept data on sys.stdin. *Edit:* Or even skip the alias altogether and just call `python /path/to/bar.py | python /path/to/foo.py`. – Andy Kubiak Aug 24 '15 at 14:14
  • Can't you simply chdir to the path of foo before importing it? – Gerard van Helden Aug 24 '15 at 14:14
  • @GerardvanHelden Not a clean solution. I can think of a few practical problems that would cause, since the script expects to be in a specific folder. – Ram Rachum Aug 24 '15 at 14:15
  • If this is all for debugging just debug with this: https://wiki.python.org/moin/PythonDebuggingTools – Aleksander Lidtke Aug 24 '15 at 14:15
  • @AleksanderLidtke Sorry I'm already invested in the debugger I use and I have no interest in switching to a different one. – Ram Rachum Aug 24 '15 at 14:17
  • @AndyKubiak Passing data as stdin into `foo.py` doesn't help me. I need to run actual Python lines of code there. I don't see how I could do that with your suggestion. – Ram Rachum Aug 24 '15 at 14:18
  • @RamRachum some things are better than others, you shouldn't be afraid of change. But your call. – Aleksander Lidtke Aug 24 '15 at 14:18
  • @RamRachum maybe just create a function that will copy the file with debugging lines added. Messy as it is but it's guaranteed to work. `for line in fileLines: if line in linesToDebug: newFile.write(debuggingLine)` sort of a thing. – Aleksander Lidtke Aug 24 '15 at 14:20
  • @RamRachum Maybe just add the debugging stuff at the beginning/end of the line followed/preceded by a `;`, the line numbers will still match. – Aleksander Lidtke Aug 24 '15 at 14:41
  • 1
    @AleksanderLidtke If I ever find myself needing to break out of jail using only a napkin and a bobpin, I will definitely contact you for advice :) Unfortunately your new hack won't work because I have some function definitions, which can't fit on one line. – Ram Rachum Aug 24 '15 at 14:44
  • @RamRachum :) thank you, I'd be delighted. By the way, maybe `lambda` will work? :D – Aleksander Lidtke Aug 24 '15 at 14:50
  • @AleksanderLidtke Nah, you can't put a 30-line function in a lambda. – Ram Rachum Aug 24 '15 at 14:56
  • @RamRachum actually, there is a way to fit all your stuff in one line: `a = 'def hack():\n print("this is a hell of a hack")\nhack()'; exec(a)` – Eli Korvigo Aug 26 '15 at 16:51
  • can you do `cat bar.py foo.py | python` – NicE Aug 26 '15 at 17:43
  • @EliKorvigo Might work but too hacky for me. – Ram Rachum Aug 26 '15 at 18:24
  • @NicE I think this won't work because the code might run different as stdin, but I'll give it a try later. – Ram Rachum Aug 26 '15 at 18:24
  • will you accept shell script? – Jason Hu Aug 26 '15 at 18:27
  • @HuStmpHrrr Reluctantly :) If there isn't a Python-only solution, and your shell script solution isn't too bad, then yes. – Ram Rachum Aug 26 '15 at 18:32
  • i will guarantee you it's ugly. it will just concat two pieces and run a temporary file. – Jason Hu Aug 26 '15 at 18:47
  • Would you be willing/able to add a sitecustomize or usercustomize file? – Jeremy Allen Aug 26 '15 at 19:33
  • @JeremyAllen I'm not sure how these work, so I'll be happy to hear. – Ram Rachum Aug 27 '15 at 11:31

7 Answers7

23

Python has a mechanism for running code at startup; the site module.

"This module is automatically imported during initialization."

The site module will attempt to import a module named sitecustomize before __main__ is imported. It will also attempt to import a module named usercustomize if your environment instructs it to.

For example, you could put a sitecustomize.py file in your site-packages folder that contains this:

import imp

import os

if 'MY_STARTUP_FILE' in os.environ:
    try:
        file_path = os.environ['MY_STARTUP_FILE']
        folder, file_name = os.path.split(file_path)
        module_name, _ = os.path.splitext(file_name)
        fp, pathname, description = imp.find_module(module_name, [folder])
    except Exception as e:
        # Broad exception handling since sitecustomize exceptions are ignored
        print "There was a problem finding startup file", file_path
        print repr(e)
        exit()

    try:
        imp.load_module(module_name, fp, pathname, description)
    except Exception as e:
        print "There was a problem loading startup file: ", file_path
        print repr(e)
        exit()
    finally:
        # "the caller is responsible for closing the file argument" from imp docs
        if fp:
            fp.close()

Then you could run your script like this:

MY_STARTUP_FILE=/somewhere/bar.py python /somewhere_else/foo.py
  • You could run any script before foo.py without needing to add code to reimport __main__.
  • Run export MY_STARTUP_FILE=/somewhere/bar.py and not need to reference it every time
Jeremy Allen
  • 6,434
  • 2
  • 26
  • 31
7

You can use execfile() if the file is .py and uncompyle2 if the file is .pyc.

Let's say you have your file structure like:

test|-- foo.py
    |-- bar
         |--bar.py   

foo.py

import sys

a = 1
print ('debugging...')

# run the other file
if sys.argv[1].endswith('.py'): # if .py run right away
    execfile(sys.argv[1], globals(), locals())
elif sys.argv[1].endswith('.pyc'): # if .pyc, first uncompyle, then run
    import uncompyle2
    from StringIO import StringIO
    f = StringIO()
    uncompyle2.uncompyle_file(sys.argv[1], f)
    f.seek(0)
    exec(f.read(), globals(), locals())

bar.py

print a
print 'real job'

And in test/, if you do:

$ python foo.py bar/bar.py
$ python foo.py bar/bar.pyc

Both, outputs the same:

debugging...
1
real job

Please also see this answer.

Community
  • 1
  • 1
Sait
  • 19,045
  • 18
  • 72
  • 99
  • This would have been my ideal solution, but unfortunately `execfile` doesn't work for `.pyc` files, and I need that. – Ram Rachum Aug 27 '15 at 11:17
  • @RamRachum, I updated the answer for `.pyc` files. It solves the problem if your concern is not the time but you actually do not have the `.py` file. – Sait Aug 28 '15 at 00:56
3

You probably have something along the lines of:

if __name__ == '__main__':
    # some code

Instead, write your code in a function main() in foo and then do:

if __name__ == '__main__':
    main()

Then, in bar, you can import foo and call foo.main().

Additionaly, if you need to change the working directory, you can use the os.chdir(path) method, e.g. os.chdir('path/of/bar') .

Community
  • 1
  • 1
Fernando Matsumoto
  • 2,697
  • 1
  • 18
  • 24
  • Sorry, that's too big of a change for `foo.py`. It's a big code file that dozens of people use in my company and I can't refactor it. Also, there might be other differences that we're not thinking of when launching the file like that and I don't want to take chances. – Ram Rachum Aug 24 '15 at 16:33
1

bar.py would have to behave like the Python interpreter itself and run foo.py (or foo.pyc, as you asked for that) as if it was the main script. This is surprisingly hard. My 90% solution to do that looks like so:

def run_script_as_main(cmdline):
    # It is crucial to import locally what we need as we
    # later remove everything from __main__
    import sys, imp, traceback, os
    # Patch sys.argv
    sys.argv = cmdline
    # Clear the __main__ namespace and set it up to run
    # the secondary program
    maindict = sys.modules["__main__"].__dict__
    builtins = maindict['__builtins__']
    maindict.clear()
    maindict['__file__'] = cmdline[0]
    maindict['__builtins__'] = builtins
    maindict['__name__'] = "__main__"
    maindict['__doc__'] = None
    # Python prepends a script's location to sys.path
    sys.path[0] = os.path.dirname(os.path.abspath(cmdline[0]))
    # Treat everything as a Python source file, except it
    # ends in '.pyc'
    loader = imp.load_source
    if maindict["__file__"].endswith(".pyc"):
        loader = imp.load_compiled
    with open(cmdline[0], 'rb') as f:
        try:
            loader('__main__', maindict["__file__"], f)
        except Exception:
            # In case of an exception, remove this script from the
            # stack trace; if you don't care seeing some bar.py in
            # traceback, you can leave that out.
            ex_type, ex_value, ex_traceback = sys.exc_info()
            tb = traceback.extract_tb(ex_traceback, None)[1:]
            sys.stderr.write("Traceback (most recent call last):\n")
            for line in traceback.format_list(tb):
                sys.stderr.write(line)
            for line in traceback.format_exception_only(ex_type, ex_value):
                sys.stderr.write(line)

This mimics the default behavior of the Python interpreter relatively closely. It sets system globals __name__, __file__, sys.argv, inserts the script location into sys.path, clears the global namespace etc.

You would copy that code to bar.py or import it from somewhere and use it like so:

if __name__ == "__main__":
    # You debugging setup goes here
    ...
    # Run the Python program given as argv[1]
    run_script_as_main(sys.argv[1:])

Then, given this:

$ find so-foo-bar
so-foo-bar
so-foo-bar/debugaid
so-foo-bar/debugaid/bar.py
so-foo-bar/foo.py

and foo.py looking like this:

import sys

if __name__ == "__main__":
    print "My name is", __name__
    print "My file is", __file__
    print "My command line is", sys.argv
    print "First 2 path items", sys.path[:2]
    print "Globals", globals().keys()
    raise Exception("foo")

... running foo.py via bar.py gives you that:

    $ python so-foo-bar/debugaid/bar.py so-foo-bar/foo.py
    My name is __main__
    My file is so-foo-bar/foo.py
    My command line is ['so-foo-bar/foo.py']
    First 2 path items ['~/so-foo-bar', '/usr/local/...']
    Globals ['__builtins__', '__name__', '__file__', 'sys', '__package__', '__doc__']
    Traceback (most recent call last):
      File "so-foo-bar/foo.py", line 9, in 
        raise Exception("foo")
    Exception: foo

While I'd guess that run_script_as_main is lacking in some interesting details, it's pretty close to the way Python would run foo.py.

fpbhb
  • 1,469
  • 10
  • 22
0

The foo.py need not be on the same folder as the main folder. Use

export PYTHONPATH=$HOME/dirWithFoo/:$PYTHONPATH

To add the path that foo resides to Python path. Now you can

from foo import myfunc

myfunc()

And make myfunc do whatever u want

Aswin Murugesh
  • 10,831
  • 10
  • 40
  • 69
0

Have you tried this?

bar.py

imp.load_source('__main__', '../../foo.py')

for me it runs whatever is in foo.py (before and inside main)

After, maybe what you do in bar.py might be more tricky that my basic test.

The good thing with load_source is that it imports the .pyc if it already exists or initiates a "compile" of the .py and then imports the .pyc.

DevLounge
  • 8,313
  • 3
  • 31
  • 44
0

This solution is what ended up working well for me:

#!/usr/bin/env python

import imp
import sys
import os.path

file_path = sys.argv.pop(1)
assert os.path.exists(file_path)

folder, file_name = os.path.split(file_path)

###############################################################################
#                                                                             #

# Debugging cruft here

#                                                                             #
###############################################################################


module_name, _ = os.path.splitext(file_name)
sys.path.append(folder)
imp.load_module('__main__', *imp.find_module(module_name))
Ram Rachum
  • 84,019
  • 84
  • 236
  • 374
  • You'll find a more general version of that approach in my answer, which goes to some length to avoid unwanted side-effects for the script that is run. – fpbhb Sep 15 '15 at 17:08