Django desktop app

Q. Why build a desktop application as a prototype if ultimately you’re aiming for a web service?

A. If you need to get a couple of people in a corporate environment using your prototype ASAP. Thus you’ve got no time to play corporate politics, get a server from somewhere, get software licences, further budget to get IT support for the application…. all this just to try out a concept? Nightmare.

So we’re back to building a desktop application. Right…?

But I couldn’t face building a nice layout, program flow and functionality into a desktop application and then throwing it all away – or worse having to maintain two unrelated versions down the track. Instead I wanted to try out django, so I’d need a database, web server, python and the django libraries all rolled up into a single executable. Make it runnable on a locked-down corporate windows xp piece of shit and for extra difficulty without admin rights or other software dependencies.

Searching…

Searching…

Led me to Siddharta’s site he was doing what I wanted but without the detailed instructions a simpleton (me) needs.

Eventually I also found Joseph Jude’s much more comprehensive version (inspired by Siddharta’s original post) – including a code repository of his application. This plus hours of messing around (ah, sweet learning) and I’ve got to a solution I’m reasonably happy with.

We get a single executable – plus some supporting files that can be distributed within a single zip file. Unzip and run – hell it’ll even start up your browser (or add a tab in an existing browser window).

  • No installation required.
  • No external software dependencies required (based on windows xp sp2).
  • No administration account.
  • No hardware from the IT department.
  • No budget approvals, politics.
  • The euphoria of success (past and future failures forgotten…. momentarily).

Software Dependencies

Required

  • Python 2.5.2
  • Django (svn version at least 2008-06-05)
  • CherryPy (web server)
  • Py2Exe

Optional

  • Docutils (only necessary for documentation link)
  • PIL (Python Imaging Library)
  • subversion

Configuration

Structure

  • PYTHONPATH/Lib/site-packages/django/
  • demosite/ (django project)
    • db/
    • templates/
    • fields/ (any django application)
    • transformations/ (any django application)

Environment

We’ve got a dependency on the PYTHONPATH environment variable pointing to your python bin directory.

Files

build.bat

Runs setup.py through Py2Exe, then waits for a key-press (pause) so you can see the multitude of error messages ;). Py2Exe will create a build directory to hold compiled files and do it’s processing, the files for distribution will (in our case) be in the setup directory.

Code
python setup.py py2exe
pause

demosite.py

This is the file that runs when the executable (created with Py2Exe) starts. A console window is shown with the exit message. CherryPy (the python web server) is configured to put log messages in the site.log file. CherryPy gets the django media directory ‘grafted’ allowing it to serve the static content – CSS, JS and images. Start CherryPy and open a browser. Finally the program will close if the console window is close or by pressing CTRL-C or CTRL-BREAK (KeyboardInterrupt).

Note that the following settings can be adjusted (after deployment) in the settings.yaml file – media directory, web server port and the url opened in your browser. Note that these can be hard-coded instead and avoid any modifications to the django settings.py file.

Code
import os
import sys

os.environ[ 'DJANGO_SETTINGS_MODULE' ] = "settings"

import settings
from cherrypy import wsgiserver
import cherrypy
import webbrowser
from django.core.handlers.wsgi import WSGIHandler
from django.core.servers.basehttp import AdminMediaHandler

if __name__ == "__main__":
  print 'To exit DemoSite close this window.'

  # Set up site-wide config first so we get a log if errors occur.
  cherrypy.config.update({
    'environment': 'production',
    'log.error_file': 'site.log',
    'log.screen': False,
  })

  # run CherryPy and open browser
  try:
    full_media_path = os.path.dirname( os.path.abspath( sys.argv[ 0 ])) + settings.ADMIN_MEDIA_PREFIX

    cherrypy.tree.graft(
      AdminMediaHandler(
        WSGIHandler(),
        media_dir=full_media_path
      ),
      '/'
    )
    cherrypy.server.socket_port = settings.WEB_SERVER_PORT

    cherrypy.server.quickstart()
    cherrypy.engine.start_with_callback( webbrowser.open, ( settings.START_URL, ), )

  except KeyboardInterrupt:
    cherrypy.server.stop()

setup.py

This is the big one, we’ve got a function add_path_tree that will add all the files and directories under a specified path – this is used to add the django admin templates and media contents and the local application templates and db.

Next up are the Py2Exe options. The massive include list is all the python modules (used in the application) that Py2Exe can’t auto-magically decide to include. Py2Exe won’t pick up on these because django dynamically decides which libraries to load at run-time. Finally the setup function pulls together the options, data files and defines that our executable will kick off demosite.py in console mode.

Code
from distutils.core import setup
import py2exe
import os

def add_path_tree( base_path, path, skip_dirs=[ '.svn', '.git' ]):
  path = os.path.join( base_path, path )
  partial_data_files = []
  for root, dirs, files in os.walk( os.path.join( path )):
    sample_list = []
    for skip_dir in skip_dirs:
      if skip_dir in dirs:
        dirs.remove( skip_dir )
    if files:
      for filename in files:
        sample_list.append( os.path.join( root, filename ))
    if sample_list:
      partial_data_files.append((
        root.replace(
          base_path + os.sep if base_path else '',
          '',
          1
        ),
        sample_list
      ))
  return partial_data_files

py2exe_options = {
  'py2exe': {
    'compressed': 1,
    'optimize': 2,
    'ascii': 1,
    'bundle_files': 1,
    'dist_dir': 'setup',
    'packages': [ 'encodings' ],
    'excludes' : [
      'pywin',
      'pywin.debugger',
      'pywin.debugger.dbgcon',
      'pywin.dialogs',
      'pywin.dialogs.list',
      'Tkconstants',
      'Tkinter',
      'tcl',
    ],
    'dll_excludes': [ 'w9xpopen.exe', 'MSVCR71.dll' ],
    'includes': [
      ########
      # demosite imports
      'fields.models',
      'fields.views',
      #fields.urls
      'transformations.models',
      'transformations.views',
      #transformations.urls
      'audit',
      'urls',
      'manage',
      'settings',
      ########
      # mass django import
      'django.views.generic.list_detail',
      'django.template.loaders.filesystem',
      'django.template.loaders.app_directories',
      'django.middleware.common',
      'django.contrib.sessions.middleware',
      'django.contrib.auth.middleware',
      'django.middleware.doc',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.sessions.backends.db',
      'django.contrib.sites',
      'django.contrib.admin',
      'django.core.cache.backends',
      'django.db.backends.sqlite3.base',
      'django.db.backends.sqlite3.introspection',
      'django.db.backends.sqlite3.creation',
      'django.db.backends.sqlite3.client',
      'django.template.defaulttags',
      'django.template.defaultfilters',
      'django.template.loader_tags',
      'django.contrib.admin.urls',
      'django.conf.urls.defaults',
      'django.contrib.admin.views.main',
      'django.core.context_processors',
      'django.contrib.auth.views',
      'django.contrib.auth.backends',
      'django.views.static',
      'django.contrib.admin.templatetags.adminmedia',
      'django.contrib.admin.templatetags.adminapplist',
      'django.contrib.admin.templatetags.admin_list',
      'django.contrib.admin.templatetags.admin_modify',
      'django.contrib.admin.templatetags.log',
      'django.contrib.admin.views.auth',
      'django.contrib.admin.views.doc',
      'django.contrib.admin.views.template',
      'django.conf.urls.shortcut',
      'django.views.defaults',
      'django.core.cache.backends.locmem',
      'django.templatetags.i18n',
      'django.views.i18n',
      ########
      # also used by django?
      'email.mime.audio',
      'email.mime.base',
      'email.mime.image',
      'email.mime.message',
      'email.mime.multipart',
      'email.mime.nonmultipart',
      'email.mime.text',
      'email.charset',
      'email.encoders',
      'email.errors',
      'email.feedparser',
      'email.generator',
      'email.header',
      'email.iterators',
      'email.message',
      'email.parser',
      'email.utils',
      'email.base64mime',
      'email.quoprimime',
    ],
  }
}

# Take the first value from the environment variable PYTHON_PATH
python_path = os.environ[ 'PYTHONPATH' ].split( ';' )[ 0 ]

django_admin_path = os.path.normpath( python_path + '/lib/site-packages/django/contrib/admin' )
py2exe_data_files = []

# django admin files
py2exe_data_files += add_path_tree( django_admin_path, 'templates' )
py2exe_data_files += add_path_tree( django_admin_path, 'media' )
# project files
py2exe_data_files += add_path_tree( '', 'db' )
py2exe_data_files += add_path_tree( '', 'templates' )

setup(
  options=py2exe_options,
  data_files=py2exe_data_files,
  zipfile = None,
  console=[ 'demosite.py' ],
)

Post-Deployment Settings

So bundled it all up and sent it off to my co-developer – naturally it doesn’t work.

Foul language interlude.

Turns out that there was already a web server running on port 80 on the local host. Options reconfigure the settings to some weird port. Or allow custom settings – hmm, this might also allow my mythical user base to point to an arbitrary database.

Files

settings.py

This is the normal django settings.py horrible modified (but for a good cause) to allow settings to be modified after Py2Exe has done it’s work. Try to get them from db/settings.yaml if there’s a problem revert to a default value.

Why not just get people to edit settings.py?

  1. It’s a little more complicated than the small yaml file;
  2. You can’t edit the file because it’s been bundled up as part of the Py2Exe build.
Code
import os
import yaml

# get settings from db/settings.yaml (if any missing all will default)
# this allows customising the database and web server details after deployment
config = {}
try:
  config_file = os.path.join( 'db', 'settings.yaml' )
  if os.path.exists( config_file ):
    config = yaml.load( file( config_file, 'r' ))
except:
  pass

def get_config( config_yaml, index, default='' ):
  try:
    return config_yaml[ index ]
  except:
    return default

DEBUG = get_config( config, 'debug', False )
TEMPLATE_DEBUG = get_config( config, 'template_debug', DEBUG )
DATABASE_ENGINE = get_config( config, 'database_engine', 'sqlite3' )
DATABASE_NAME = get_config( config, 'database_name', 'db/demosite.db' )
DATABASE_USER = get_config( config, 'database_user', '' )
DATABASE_PASSWORD = get_config( config, 'database_password', '' )
DATABASE_HOST = get_config( config, 'database_host', '' )
DATABASE_PORT = get_config( config, 'database_port', '' )
TIME_ZONE = get_config( config, 'time_zone', 'Australia/Melbourne' )
LANGUAGE_CODE = get_config( config, 'language_code', 'en-us' )

START_URL = get_config( config, 'start_url', 'http://localhost/admin/' )
WEB_SERVER_PORT = get_config( config, 'web_server_port', 80 )

ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)

MANAGERS = ADMINS

SITE_ID = 1

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''

# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/'

# Make this unique, and don't share it with anybody.
SECRET_KEY = 'aaaaaasn%1aaaaaaaaaaaaeaagd%1aaaaaa6*(ef()aag$&f$%'

# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.load_template_source',
    'django.template.loaders.app_directories.load_template_source',
#     'django.template.loaders.eggs.load_template_source',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.middleware.doc.XViewMiddleware',
)

ROOT_URLCONF = 'urls'

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
  'templates',
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.admin',
    'fields',
    'transformations',
)

settings.yaml

A yaml configuration file that allows settings such as database and web sever ports to be changed after the Py2Exe bundling (deployment).

Code
# Debug
debug: True
template_debug: DEBUG

# Database
# postgresql_psycopg2, postgresql, mysql, sqlite3 or oracle.
database_engine: sqlite3
database_name: db/demosite.db
database_user:
database_password:
# set database_host to empty string for localhost, not used with sqlite3.
database_host:
database_port:

# local time zone for this installation. choices can be found here:
# http://en.wikipedia.org/wiki/list_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# if running in a windows environment this must be set to the same as your
# system time zone.
time_zone: Australia/Melbourne

# language code for this installation. all choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
language_code: en-us

# Web Server
start_url: http://localhost/admin/
web_server_port: 80

Works On

  • ? Windows 2k
  • Windows XP SP2 (? SP3)
  • ? Vista

11 Responses

  1. Thanks for this post. I didn’t read it all, but I am planning on packaging up a Django desktop application sometime in the near future. I’m sure I will come back to this.

  2. Nice! I really like the tons of cool 3rd party apps that Python has. py2exe / cherry_py is one example.

  3. Hey Waldo,
    Have you tried to build desktop app from the latest svn codebase? It fails and haven’t been able to figure out. Let me know if you find a way out.

    Thank you,
    Joseph

  4. @Joseph: Sorry I haven’t actually used the s-o-l desktop application so I’ve got no idea what the problem might be.

  5. Waldo: I upgraded to the new-forms. When I build the desktop application, it builds without any errors. However the auth modules (group & user) doesn’t appear in the admin site. I do have autodiscover(). My efforts to debug has not been fruitful. My question is, have you built your desktop application against newforms? Do you face this issue? Any clues why this might happen and how I can debug?

    (BTW: the method that you describe here is cool; now I can change the parameters even after building the application. I’m thinking by this way we can even distribute the application in binary form; what do you think?)

  6. @Joseph: Thanks for clarifying your question. I haven’t yet made the transition to new-forms so I’m yet to run into the debugging nightmare you’ve described.

    Regarding the parameters file, yes using this method you can now change them after distribution in binary form :)

  7. @joseph: the admin issue you see is due to py2exe’s modified behavior when accessing __path__ and when doing ‘imp’ – two operations the autodiscover() relies on. Your best bet is to manually import the right admin.py modules – for your apps as well as the contrib apps (such as auth) you are using.

  8. Hi, I was wondering if you had a up-to-date list of django includes for setup.py?
    Also I cannot seem to get the css or js scripts to work in the media directly when I launch the website. Also styling in the admin is missing too…

    By the way this website was sooo easy to follow. Thank you very much!

    Any ideas,
    Matt

    • Hi Matt,

      Unfortunately I haven’t looked at this in ages so I don’t have the current list of includes.

      Waldo

  9. Hello, PyInstaller features automatic support for django projects, albeit experimental:
    http://www.pyinstaller.org/wiki/DjangoApplication

Leave a reply to waldo Cancel reply