Extend py2exe to copy files to the zipfile where pkg_resources can load them

In creating a Windows PDF generation app for a client of mine, I ran into a limitation of py2exe which prevents resource loading with pkg_resources. The module attempts to load resources from py2exe’s library.zip. As the zipfile is reserved for compiled bytecode, there’s no option to copy files into it. You can copy files using py2exe’s data_files, but not into the zipfile, and package_data isn’t honoured. You can, however, extend the py2exe class, and that’s what I did.

The best entry point I could find was copy_extensions(). You can override that method to copy files into the build directory. You’ll want to create the necessary directories if they don’t already exist and then register the copied files for copying to dist. Here’s an example with images in a foo module’s media directory:

import os
import glob
from py2exe.build_exe import py2exe as build_exe

class MediaCollector(build_exe):
    def copy_extensions(self, extensions):
        super(MediaCollector, self).copy_extensions(extensions)

        # Create the media subdir where the
        # Python files are collected.
        media = os.path.join('foo', 'media')
        full = os.path.join(self.collect_dir, media)
        if not os.path.exists(full):
            self.mkpath(full)

        # Copy the media files to the collection dir.
        # Also add the copied file to the list of compiled
        # files so it will be included in zipfile.
        for f in glob.glob('foo/media/*'):
            name = os.path.basename(f)
            self.copy_file(f, os.path.join(full, name))
            self.compiled_files.append(os.path.join(media, name))

Strictly speaking, resources are not compiled files but treating them as such was the only way I could find to force them to be included in the zipfile. To use this new collector, just specify it in your py2exe options:

py2exe_options = {
    'cmdclass': {'py2exe': MediaCollector},
    # [...] Other py2exe options here.
}

setup(
    # [...] Other setup options here.
    **py2exe_options
)
  1. Igor Tkach

    December 7, 2008 at 11:42 pm

    Thanks for the tip, exactly what I was looking for. I’m having trouble getting it to work though – looks like build_exe is classic class, so call to super fails. I tried this with py2exe 0.6.8 and 0.6.9, Python 2.5.2. How did you get around this?

  2. Igor Tkach

    December 8, 2008 at 12:04 am

    Answering myself: just use proper syntax for classic classes:
    build_exe.copy_extensions(self, extensions)
    Other than this works great, thank you!

  3. Dragos Anghel

    April 3, 2009 at 2:53 pm

    First of all, thanks for your article… I try to use it, but I’ve got some errors with “super(MediaCollector, self).copy_extensions(extensions)” line:

    File “ServiceDirs\mysetup.py”, line 76, in copy_extensions
    super(MediaCollector, self).copy_extensions(extensions)
    TypeError: super() argument 1 must be type, not classobj

    Also, I try how Igor says, but doesn’t help…
    All I want is to add to zipfile the files from my app “Gfx” subdirectory, from where pkg_resources could load them

    My “setup.py” looks like:

    ##############
    from distutils.core import setup
    import py2exe
    import sys, os
    import glob

    ############################################################################
    MainScriptName = ‘test.py’
    description = “my test app”
    version = “0.0.1”
    company_name = “No Company”
    copyright = “roDAX”
    name = “test”
    author = ‘Dragos (dax) ANGHEL’
    iconFile = “Gfx\\AppIco.ico”
    ############################################################################
    # … this creates the MainScriptName of your .exe file in the dist folder
    if MainScriptName.endswith(“.py”):
    distribution = MainScriptName[:-3]
    elif MainScriptName.endswith(“.pyw”):
    distribution = MainScriptName[:-4]

    # if run without args, build executables in quiet mode
    if len(sys.argv) == 1:
    sys.argv.append(“py2exe”)
    sys.argv.append(“-q”)

    class MyAppClass:
    def __init__(self, **kw):
    self.__dict__.update(kw)
    # for the versioninfo resources, edit to your needs
    self.version = version
    self.company_name = company_name
    self.copyright = copyright
    self.name = name
    self.author =author

    myApp = MyAppClass(
    description = description, # the versioninfo resource
    script = MainScriptName, # will be the exe MainScriptName
    icon_resources = [(1, iconFile)], # give it an iconfile; otherwise a default icon is used
    )

    # follow Ian Stevens’ part
    #——————————————————————————-
    ”’
    The best entry point I could find was copy_extensions().
    You can override that method to copy files into the build directory.
    You’ll want to create the necessary directories if they don’t already exist and then
    register the copied files for copying to dist.
    Here’s an example with images in a foo module’s media directory:
    ”’

    #import os
    #import glob
    from py2exe.build_exe import py2exe as build_exe

    class MediaCollector(build_exe):
    def copy_extensions(self, extensions):
    super(MediaCollector, self).copy_extensions(extensions) # <<<<pyc) 1 or 2=optimized (py–>pyo; 1= -O python param; 2=-OO python param)
    “ascii”: 1, # to make a smaller executable, don’t include the encodings
    “bundle_files”: 1, # 1=All inside; 2=PythonXX.dll outside; 3=All outside
    },
    },
    zipfile = distribution +’.bin’, # None=all components in exe_file; libname_string=all in ‘libname” file
    windows = [myApp],
    **py2exe_options
    )
    ###########

    If I comment the involved line from within the “MediaCollector” class, I obtain the “Gfx” in zipfile with all his content, but from builded “zipfile\\wx” directory is missing all PYDs files…

    Could anyone have some suggestions about the error thrown by the interpreter??

    Thanks in advance…
    Dragos

    p.s.: sorry for this huge question/reply

  4. Kade Blad

    August 22, 2017 at 4:10 pm

    Unfortunately, py2exe has changed the way that their module works, so the example provided above does not work anymore.

    I have been able to do this by overriding one of py2exe’s functions, and then just inserting them into the zipfile that is created by py2exe.

    Here’s an example:

    import py2exe
    import zipfile

    myFiles = [
    “C:/Users/Kade/Documents/ExampleFiles/example_1.doc”,
    “C:/Users/Kade/Documents/ExampleFiles/example_2.dll”,
    “C:/Users/Kade/Documents/ExampleFiles/example_3.obj”,
    “C:/Users/Kade/Documents/ExampleFiles/example_4.H”,
    ]

    def better_copy_files(self, destdir):
    “””Overriden so that things can be included in the library.zip.”””

    #Run function as normal
    original_copy_files(self, destdir)

    #Get the zipfile’s location
    if self.options.libname is not None:
    libpath = os.path.join(destdir, self.options.libname)

    #Re-open the zip file
    if self.options.compress:
    compression = zipfile.ZIP_DEFLATED
    else:
    compression = zipfile.ZIP_STORED
    arc = zipfile.ZipFile(libpath, “a”, compression = compression)

    #Add your items to the zipfile
    for item in myFiles:
    if self.options.verbose:
    print(“Copy File %s to %s” % (item, libpath))
    arc.write(item, os.path.basename(item))
    arc.close()

    #Connect overrides
    original_copy_files = py2exe.runtime.Runtime.copy_files
    py2exe.runtime.Runtime.copy_files = better_copy_files