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
)
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?
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!
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
August 12, 2010 at 5:57 pm
[…] For example, py2exe doesn’t respect the distutils package_data option. There are a few workarounds for problems of that nature, and you can get more details about py2exe from the project’s […]
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