pyinstaller icon indicating copy to clipboard operation
pyinstaller copied to clipboard

Handle relative imports in a package's __main__.py

Open JonathonReinhart opened this issue 8 years ago • 16 comments
trafficstars

I have a runnable package with __main__.py:

foo/
    __init__.py
    __main__.py

I can execute this package by running python -m foo. However you cannot directly create a PyInstaller application from this package:

  • If you try pyinstaller -F foo, you'll get IOError: [Errno 21] Is a directory.
  • If you try pyinstaller -F foo/__main__.py it will build, but then you'll get ValueError: Attempted relative import in non-package when you try to run it, presumably because it took __main__.py out of the context of its package.

The work around is to create a small "stub" application that lives outside of the package and calls into it:

from foo import main
main()

...and then point PyInstaller at the stub.

It seems that PyInstaller should be able to create an application by being pointed at the package directory.

I'm sorry if this has already been addressed, but I've been unable to find anything in the documentation or anywhere else:

JonathonReinhart avatar Apr 13 '17 14:04 JonathonReinhart

See subzero.

ghost avatar Apr 13 '17 16:04 ghost

We'd appreciate a pull-request implementing both entry-points and support for __main__.py

htgoebel avatar Apr 15 '17 17:04 htgoebel

@JonathonReinhart What does the resulting .spec file look like after running with foo/__main__.py? I have a project that I've been building fine for years that points to a __main__.py module.

djhoese avatar Apr 21 '17 18:04 djhoese

@davidh-ssec I suspect you're not using relative imports in __main__.py, and thus it's okay that __main__.py is being "removed" from the context of the package. It's as if you moved __main__.py outside of the package and ran it (as my workaround prescribes).

I put this example together, for reference: https://github.com/JonathonReinhart/pyinstaller-package-example

When I build it (using pyinstaller 3.2) this spec file is generated:

# -*- mode: python -*-

block_cipher = None


a = Analysis(['foo/__main__.py'],
             pathex=['/home/jreinhart/projects/pyinstall-package-example'],
             binaries=None,
             datas=None,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='foo_installed',
          debug=False,
          strip=False,
          upx=True,
          console=True )

And when I go to build + run it:

$ ./try_to_build_main_dot_py.sh 
16 INFO: PyInstaller: 3.2
16 INFO: Python: 2.7.13
16 INFO: Platform: Linux-4.10.6-200.fc25.x86_64-x86_64-with-fedora-25-Twenty_Five
17 INFO: wrote /home/jreinhart/projects/pyinstall-package-example/foo_installed.spec
21 INFO: UPX is available.
22 INFO: Extending PYTHONPATH with paths
['/home/jreinhart/projects/pyinstall-package-example',
 '/home/jreinhart/projects/pyinstall-package-example']
22 INFO: checking Analysis
27 INFO: checking PYZ
29 INFO: checking PKG
30 INFO: Bootloader /usr/lib/python2.7/site-packages/PyInstaller/bootloader/Linux-64bit/run
30 INFO: checking EXE

Traceback (most recent call last):
  File "foo/__main__.py", line 1, in <module>
    from . import main
ValueError: Attempted relative import in non-package
Failed to execute script __main__

JonathonReinhart avatar Apr 22 '17 18:04 JonathonReinhart

Did you ever solve this? I have the same problem.

dhowland avatar Apr 02 '18 02:04 dhowland

@dhowland The "workaround" as far as I'm concerned is to not use relative imports in your __main__.py module. I think I was doing non-relative imports in all of my packages anyway because I was under the impression that they weren't preferred so I do from mypkg.subpkg import x everywhere.

djhoese avatar Apr 02 '18 12:04 djhoese

so I do from mypkg.subpkg import x everywhere

Note that's it's not necessary to use absolute imports everywhere. It's sufficient to use absolute imports in the script you're supplying to PyInstaller, only.

bittner avatar Oct 02 '18 09:10 bittner

So far, this is the workaround:

foo/
    __init__.py
    __main__.py
    component.py
    launcher.py
# __main__.py
from .component import *
...
def main():
    ...
if __name == '__name__':
    main()
# launcher.py
# Just copy __main__.py code to here and replace .component into component
from component import *
...
def main():
    ...
if __name == '__name__':
    main()
pyinstaller -F foo/launcher.py

HaujetZhao avatar Feb 11 '21 07:02 HaujetZhao

hey, it would be cool if I could pass a setup.py to pyinstaller and it would figure it all out. Specifically also picking up the data directories.

ericoporto avatar Jan 24 '22 19:01 ericoporto

Freezing a __main__.py will work as long as there are no relative imports (from . import). Freezing setuptools entry-points directly is never going to happen because setuptools entry-points should generally also be exposed as python -m equivalents so that user's who don't add their scripts directory to PATH can still use them. And even if they're not, it should be a simple case of putting from your_project import main; main() in a Python script and PyInstaller-ing that. I'd rather not introduce a whole new syntax just to save two lines of user code.

bwoodsend avatar Jan 24 '22 19:01 bwoodsend

Using only absolute imports in __main.py__ and executing command pyinstaller --onefile "C:\some-path\__main__.py" worked for me.

Project structure:

─mypackage
│   __init__.py
│   __main__.py
│
├───app
│   │   app.py
│   │   database.py
│   │   receiver.py
│   │   util.py
│   │   __init__.py
│   │
│   ├───components
│   │   │   main_window.py
│   │   │   mq_properties.py
│   │   │   __init__.py
│   │   │

Contents of __main__.py:

from logging.config import dictConfig
import coloredlogs # Needed for pyinstaller
import yaml

from mypackage.app import runApp     # Absolute import

if __name__ == "__main__":

    # Configure logging
    with open("logging.yaml") as f:
        dictConfig(yaml.safe_load(f))

    # Run application
    runApp()

Function runApp:

import sys
from PyQt5.QtWidgets import QApplication
from .components.main_window import MyWindow

def runApp():

    app = QApplication(sys.argv)

    myWindow = MyWindow()
    myWindow.show()

    sys.exit(app.exec())

poul1x avatar Feb 12 '22 12:02 poul1x