0

For a small module, I want to have a __main__.py file that is executed via Pythons -m argument. As per basic docs, this file looks like

import argparse

from . import MyClass

def getparser():
    parser = argparse.ArgumentParser()
    # […]
    return parser


if __name__ == "__main__":
    parser = getparser()
    args = parser.parse_args()
    c = MyClass()
    # […]

I want to test this with runpy:

import runpy

def test_main():
    runpy.run_module("mymodule")
    # […]

which does not work because the __name__ in this case is not __main__. In principle one could omit the if __name__ == "__main__" condition to get this working.

But, I also want to document the routine with sphinxarg.ext. This requires to have the getparser() function available from sphinx. Removing the if __name__ == "__main__" condition then also runs the module within sphinxdoc, which is not what is wanted.

.. argparse::
   :module: mymodule.__main__
   :func: getparser
   :prog: myprog

How can I structure this so that all use cases work well and the code is well-readable (i.e. the ``getparser()` function and the main code should not be distributed over different files)?

olebole
  • 521
  • 4
  • 17

1 Answers1

0

One method is to keep your __main__.py very thin and import what you need and run it.

So, you might extract your logic into another module, say cli.py, and write a main function that functions the same as your code block you have under if __name__ == '__main__':.

# cli.py
def get_parser():
   """docstring"""
   ...


def main() -> int:
    parser = get_parser()
    ...
    return exit_code

Then in __main__.py you can simply have something like this, which will work with runpy (although this is maybe unnecessary now!) and doesn't need sphinx or test modules to import it:

from .cli import main
raise SystemExit(main()) # optionally use `if __name__ == ...`

This way, you're still able to import everything from cli.py (or wherever you choose), test it, auto-document it, or whatever. With this approach you can also test your main function and not have the limitations that arise with trying to test using runpy.

from mypackage.cli import main
def test_main():
    assert main() == 0

Extracting to a module like cli.py is technically optional. You could also keep all of this in __main__.py, but the main lesson is to minimize the amount of code under if __name__ == '__main__':. If I have this, it almost always looks something like this:

if __name__ == '__main__':
    main() # or raise SystemExit(main())

You can also simply set __name__ with the run_name keyword argument for runpy.run_module:

def test_main():
    runny.run_module('mymodule', run_name='__main__')

But I would suggest that extracting a testable main function is a better testing approach.

sytech
  • 29,298
  • 3
  • 45
  • 86
  • The problem here is that calling `main()` ignores `sys.argv`. – olebole May 26 '23 at 18:04
  • @olebole if you mean for testing, you can patch sys.argv in your test to add arguments https://stackoverflow.com/a/70871265/5747944 – sytech May 26 '23 at 18:32