Browse Source

First public version.

Rob Austein 6 years ago
commit
fa9a04b06a
9 changed files with 120 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 21 0
      Makefile
  3. 27 0
      README.md
  4. 4 0
      demo1/__init__.py
  5. 7 0
      demo1/bar.py
  6. 8 0
      demo1/main.py
  7. 6 0
      demo1/stuff.json
  8. 1 0
      demo2/main.py
  9. 44 0
      pyzipper

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+*.pyc
+demo[12].zip

+ 21 - 0
Makefile

@@ -0,0 +1,21 @@
+SRC := $(wildcard demo[0-9])
+BIN := $(addsuffix .zip,${SRC})
+
+all: ${BIN}
+
+%.zip: pyzipper Makefile
+	${PYTHON} ./pyzipper $(if $(PYTHON),-e $(PYTHON)) -o $@ $*
+	./$@
+
+clean:
+	git clean -dfx
+
+.PHONY: all clean
+
+GIT_LS_TREE := $(shell git ls-tree --name-only -r HEAD)
+
+define DEPENDENCIES
+$(1).zip : $$(filter $(1)/%,$${GIT_LS_TREE})
+endef
+
+$(foreach S,${SRC},$(eval $(call DEPENDENCIES,$(S))))

+ 27 - 0
README.md

@@ -0,0 +1,27 @@
+pyzipper
+========
+
+A small tool to package up a Python program and associated data files
+as a single executable zip file.
+
+All the facilities for this are already built into Python, this script
+just ties it all together with a bow.
+
+`demo1` and `demo2` are two trivial sample packages.
+
+Tested with both Python 2 and Python 3, should work with either.
+
+How it works
+------------
+
+This relies on two somewhat obscure Python features:
+
+* The `zipfile.PyZipFile` class's ability to construct an executable
+  ZIP file and populate it with Python modules, packages, and data;
+  and
+  
+* Python's ability to treat a zip file as equivalent to a directory
+  tree on `import`.
+  
+`demo1` takes advantage of a third obscure feature: the `pkgutil`
+library's ability to load data files via the `import` system.

+ 4 - 0
demo1/__init__.py

@@ -0,0 +1,4 @@
+import pkgutil
+
+def get_resource(name):
+    return pkgutil.get_data(__name__, name)

+ 7 - 0
demo1/bar.py

@@ -0,0 +1,7 @@
+from . import get_resource
+import json
+
+def run():
+    j = json.loads(get_resource("stuff.json"))
+    print(repr(j))
+    print(j["foo"])

+ 8 - 0
demo1/main.py

@@ -0,0 +1,8 @@
+from . import bar
+
+def main():
+    print("Hello, pyzipper")
+    bar.run()
+
+if __name__ == "__main__":
+    main()

+ 6 - 0
demo1/stuff.json

@@ -0,0 +1,6 @@
+{
+    "foo": "FOO",
+    "bar": "BAR",
+    "wibble": 3.14159,
+    "wobble": [0,1,2,3,4,5,6]
+}

+ 1 - 0
demo2/main.py

@@ -0,0 +1 @@
+print("Hello, world")

+ 44 - 0
pyzipper

@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+"""
+Generate an executable Python zip package.
+"""
+
+import os, zipfile, argparse
+
+ap = argparse.ArgumentParser(description = __doc__,
+                             formatter_class = argparse.ArgumentDefaultsHelpFormatter)
+ap.add_argument("-o", "--output",  required = True, type = argparse.FileType("wb+"))
+ap.add_argument("-m", "--module",  default = "main", help = "module within package to run")
+ap.add_argument("-e", "--executable", default = "python", help = "python executable to run")
+ap.add_argument("source")
+args = ap.parse_args()
+
+# PyZipFile.writepy()'s behavior changes when it sees __init__.py
+if os.path.exists(os.path.join(args.source, "__init__.py")):
+    module = "{0.source}.{0.module}".format(args)
+else:
+    module = args.module
+
+# Write executable shim
+args.output.write('''\
+#/bin/sh -
+PYTHONPATH="$0"${{PYTHONPATH+":$PYTHONPATH"}} exec {e} -m {m} ${{1+"$@"}}
+'''.format(m = module, e = args.executable).encode("ascii"))
+
+# Make output executable
+os.fchmod(args.output.fileno(), 0o755)
+
+# Create the zip file and populate it with the code
+z = zipfile.PyZipFile(args.output, "a", zipfile.ZIP_DEFLATED)
+z.writepy(args.source)
+
+# Add data files, if any
+if os.path.isdir(args.source):
+    for root, dirs, files in os.walk(args.source):
+        for fn in files:
+            if not any(fn.endswith(fn2) for fn2 in (".py", ".pyc", ".pyo")):
+                z.write(os.path.join(root, fn))
+
+# Test the zip file we just generated out of paranoia
+z.testzip()