Search code examples
pythoncross-platformsetuptoolspy2app

Python app distribution cross-platform


I want to distribute my app on OSX (using py2app) and as a Debian package.

The structure of my app is like:

app/
     debian/
            <lots of debian related stuff>
     scripts/
             app
     app/
         __init__.py
         app.py
         mod1/
              __init__.py
              a.py
         mod2/
              __init__.py
              b.py

My setup.py looks something like:

from setuptools import setup
import os
import os.path

osname = os.uname()[0]

if osname == 'Darwin':
    APP = ['app/app.py']
    DATA_FILES = []
    OPTIONS = {'argv_emulation': True}

    setup(
        app=APP,
        data_files=DATA_FILES,
        options={'py2app': OPTIONS},
        setup_requires=['py2app'],
    )
elif osname == 'Linux':
        setup(
        name = "app",
        version = "0.0.1",
        description = "foo bar",
        packages = ["app", "app.mod1", "app.mod2"],
        scripts = ["scripts/app"],
        data_files = [
            ("/usr/bin", ["scripts/app"]),
       ]
    )

Then, in b.py (this is on OSX):

from app.mod2.b import *

I get:

ImportError: No module named mod2.b

So basically, mod2 can't acccess mod1. On Linux there's no problem, because the python module 'app' is installed globally in /usr/shared/pyshared. But on OSX the app will obviously be a self-contained .app thing built by py2app. I wonder if I approached this totally wrong, are there any best practices when distributing Python apps on OSX?

Edit: I also tried a hack like this in b.py:

from ..mod2.b import *

ValueError: Attempted relative import beyond toplevel package

Edit2: Seems to be related to this How to do relative imports in Python?


Solution

  • I'm not sure if this is the 'best practice' or not (I've not put much python software into proper distribution), but I would just make sure that the top-level app package was in sys.path. Something like putting the following into the top-level __init__.py:

    try:
        import myapp
    except ImportError:
        import sys
        from os.path import abspath, dirname, split
        parent_dir = split(dirname(abspath(__file__)))[0]
        sys.path.append(parent_dir)
    

    I think that should do the right thing in a cross platform way.

    EDIT: As kaizer.se points out this might not work in the __init__.py file, depending on how the code you're invoking is getting executed. It would only work if that file is evaluated. The key is to make sure that the top-level package is in sys.path from some the code that actually is running.

    Often times, so that I an execute individual files inside of a package directly (for testing with the if __name__ eq '__main__' idiom), I'll do something like place a statement:

    import _setup
    

    At the top of the individual file in question, and then create a file _setup.py which does the path munging as necessary. So, something like:

    package/
        __init__.py
        _setup.py
        mod1/
            __init__.py
            _setup.py
            somemodule.py
    

    If you import _setup from somemodule.py, that setup file can ensure that the top level package is in sys.path before the rest of the code in somemodule.py is evaluated.