Search code examples
pythonprogram-entry-pointpython-module

In Python, how to invoke subroutine inside [if __name__ == '__main__'] of another py file?


Assuming that I am in a python console and want to launch another python program in the same process. The python program looks like this:

import sys

if __name__ == '__main__':
      args = sys.argv
      for key in args:
          print(key)

and I'm not allowed to modify it.

What is the proper way of doing this? Can I call it's 'main function' with a different list of arguments like C++ or Java?

I'm not against method are are considered hacking. Python is a highly extendable interpreted language and I'll do whatever necessary to bypass this restriction


Solution

  • You can't "call its main function", because it doesn't have one.

    In fact, the usual way to handle this is to move all those code into a function, then make the __main__ guard code just call that:

    def main(args):
        for key in args:
            print(key)
    
    if __name__ == '__main__':
        main(sys.argv)
    

    Then you can call its main function, passing it any args you want.

    But if you can't fix the file, that won't work.


    So, what can you do? It all depends on how exactly you need to follow the usual python another.py semantics.


    The quick&dirty way is to do what blhsing's answer suggests: read the file, and exec it with the globals from your own script:

    with open('another.py') as f:
        exec(f.read(), globals())
    

    Notice that this is going to leave sys.argv untouched. You can, of course, manually set it to something else first:

    _argv = sys.argv
    sys.argv = ['args', 'i', 'want', 'it', 'to', 'see']
    try:
        with open('another.py') as f:
            exec(f.read(), globals())
    finally:
        sys.argv = _argv
    

    If you're doing this from another module, rather than from your top-level script, that won't work, but with a slight modification, it will:

    with open('another.py') as f:
        exec(f.read(), sys.modules['__main__'].__dict__)
    

    But you may be better off creating a clean global namespace. The script may not expect to have your globals sitting around—and it may change things you don't want it to change. This is just as easy:

    with open('another.py') as f:
        exec(f.read(), {})
    

    (Note that this will still automatically inherit the current builtins, which you usually want. If you don't want that for some reason, see the exec docs.)


    If you want to guarantee that you're executing it exactly the same way as running a script normally, use the runpy module.

    When you run python -m another, what it ultimately does is:

    runpy.run_module(sys.argv[0], run_name="__main__", alter_sys=True)
    

    So, you can do the same thing manually:

    runpy.run_module('another', run_name='__main__')
    

    Whether you want to add alter_sys=True depends on what you're after. Without it, if the script tries to access sys.argv (as your script does) or sys.modules['__main__'], it will see your values, instead of its own.


    If you want to emulate python another.py, you'd want to use importlib to create a full __spec__ and related objects, then calls runpy.run_path.