Search code examples
pythonpython-importzipapp

What is the correct way to import modules for standalone app created using `python -m zipapp`?


I have a module which I'm going to distribute as standalone app. The module has the following structure:

$ tree -L 2 ./
./
├── mymodule
│   ├── __main__.py
│   ├── fun.py
└── mymodule.pyz

mymodule/__main__.py contains next lines:

#!/usr/bin/env python

import argparse
import sys
import os.path
from mymodule.fun import Fun

def main():
    sys.stdout.write('main is running')
    Fun().run()

if __name__ == '__main__':
    main()

mymodule/fun.py contains next lines:

#!/usr/bin/env python

import sys

class Fun:
    """FUN"""

    def __init__(self):
        pass

    def run(self):
        sys.stdout.write("fun")

If I run module using $ python -m mymodule the output is main is runningfun

But if I create a standalone app using $ python -m zipapp -p "/usr/bin/evn python" mymodule and run it $ python mymodule.pyz I got error

Traceback (most recent call last):
  File "/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "mymodule.pyz/__main__.py", line 6, in <module>
ModuleNotFoundError: No module named 'mymodule'

What is wrong with import?

UPD: sys.path output

$ python -m mymodule
['', '/Users/igork/.pyenv/versions/3.6.4/lib/python36.zip', '/Users/igork/.pyenv/versions/3.6.4/lib/python3.6', '/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/lib-dynload', '/Users/igork/.local/lib/python3.6/site-packages', '/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/site-packages']

$ python mymodule.pyz
['mymodule.pyz', '/Users/igork/.pyenv/versions/3.6.4/lib/python36.zip', '/Users/igork/.pyenv/versions/3.6.4/lib/python3.6', '/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/lib-dynload', '/Users/igork/.local/lib/python3.6/site-packages', '/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/site-packages']

Solution

  • First of all I run the app using wrong command, the correct is:

    $ python mymodule
    

    To create single file app and run it I need to use:

    $ python -m zipapp -p "/usr/bin/evn python" mymodule
    $ python mymodule.pyz
    

    To run unit tests I need to use:

    $ python -m unittest
    

    But unit tests were failing because of error:

    ======================================================================                                                          
    ERROR: test_fun (unittest.loader._FailedTest)                                                                                   
    ----------------------------------------------------------------------                                                          
    ImportError: Failed to import test module: test_fun                                                                             
    Traceback (most recent call last):                                                                                              
      File "/Users/igork/.pyenv/versions/3.6.4/lib/python3.6/unittest/loader.py", line 153, in loadTestsFromName                    
        module = __import__(module_name)                                                                                            
      File "/Users/igork/developer/github/python/testzipapp/tests/test_fun.py", line 2, in <module>                                 
        from mymodule.fun import Fun                                                                                                
      File "/Users/igork/developer/github/python/testzipapp/mymodule/fun.py", line 8, in <module>                                   
        from bar import Bar                                                                                                         
    ModuleNotFoundError: No module named 'bar'                                                                                      
                                                                                                                                    
                                                                                                                                    
    ----------------------------------------------------------------------                                                          
    Ran 1 test in 0.000s                                                                                                            
                                                                                                                                    
    FAILED (errors=1)                                                                                                               
    

    To fix the error I added next lines to tests/__init__.py file:

    import sys                    
    sys.path.append('mymodule')   
    

    To summarize: the structure of the project I use is:

    testzipapp $ tree -L 2             
    .                                             
    ├── create-pyz.sh                              
    ├── mymodule                                  
    │   ├── __main__.py                            
    │   ├── bar.py                                
    │   ├── fun.py                                
    ├── mymodule.pyz                              
    └── tests                                     
        ├── __init__.py                           
        ├── test_fun.py                           
       
    

    file tests/test_fun.py is:

    import unittest                                                 
    from mymodule.fun import Fun                                    
                                                                    
                                                                    
    class TestFunImports(unittest.TestCase):                        
        def test_fun_run(self):                                     
            s = Fun()                                               
            s.run()                                                 
            self.assertEqual(2+2,4)                                 
                                                                    
    

    file mymodule/__main__.py is:

    #!/usr/bin/env python                                                      
                                                                               
    import sys                                                                                                                            
    from fun import Fun                                                        
                                                                               
    def main():                                                                
        sys.stdout.write('main is running ')                                   
        Fun().run()                                                            
                                                                               
    if __name__ == '__main__':                                                 
        main()