複数のファイルを利用したpythonスクリプトを1つの実行可能なzipファイルにまとめる方法のメモ
pythonのscriptを1つのファイルにまとめたい。そのような場合にはzipappの機能が使えるかもしれない。
zipファイルとして1つにまとめたファイルはpythonインタプリタで直接実行できる。
このモジュールは Python コードを含む zip ファイルの作成を行うツールを提供します。 zip ファイルは Python インタープリタで直接実行することが出来ます。
ただし、後述するが、外部パッケージを利用するものに関してはzipappではない方が良いかもしれない。
外部パッケージを利用しない場合
外部パッケージを利用しない場合、つまり標準ライブラリだけのスクリプトの場合にはzipappだけで済む。
例えば以下の様なファイル構成でのmain.pyを1つのファイルとして実行可能にしたい。
../myzipapp ├── Makefile ├── foo │ ├── __init__.py │ └── hello.py └── main.py 1 directory, 4 files
main.pyはportの8000番でテキトーなwsgi appを動かすようなコード。
python main.py Serving on port 8000... 127.0.0.1 - - [03/Feb/2021 22:54:18] "GET / HTTP/1.1" 200 26
hello worldというrequestが返ってくる。
$ http :8000 HTTP/1.0 200 OK Content-Length: 26 Content-type: application/json; charset=utf-8 Date: Wed, 03 Feb 2021 13:54:18 GMT Server: WSGIServer/0.2 CPython/3.8.5 { "message": "hello world" }
このような挙動のmain.pyからapp.pyzを作る。作られたapp.pyzは直接実行可能になる。
$ python -m zipapp -c -o app.pyz ../myzipapp -m "main:main" # 異なる場所に置く $ mv app.pyz /tmp $ python /tmp/app.pyz Serving on port 8000... 127.0.0.1 - - [03/Feb/2021 23:16:05] "GET / HTTP/1.1" 200 26
ちなみに、このzipファイルは以下の様な内容になっている。
$ zipinfo /tmp/app.pyz Archive: /tmp/app.pyz Zip file size: 1354 bytes, number of entries: 7 -rw-r--r-- 2.0 unx 0 b- defN 21-Feb-03 23:14 app.pyz drwxr-xr-x 2.0 unx 0 b- stor 21-Feb-03 23:11 foo/ -rw-r--r-- 2.0 unx 188 b- defN 21-Feb-03 22:58 main.py -rw-r--r-- 2.0 unx 192 b- defN 21-Feb-03 23:12 Makefile -rw-r--r-- 2.0 unx 533 b- defN 21-Feb-03 22:50 foo/__init__.py -rw-r--r-- 2.0 unx 56 b- defN 21-Feb-03 22:46 foo/hello.py ?rw------- 2.0 unx 48 b- defN 21-Feb-03 23:14 __main__.py 7 files, 1017 bytes uncompressed, 672 bytes compressed: 33.9%
つまるところ、指定したディレクトリを1つのモジュールとして扱い、__main__.py
が追加されるという形。生成された __main__.py
は -m
で指定されたエントリーポイントを呼び出すだけのファイル。
__main__.py
# -*- coding: utf-8 -*- import mian main.main()
ちなみに他のファイルは以下の様な内容だった。このコード自体はあまり重要ではない。ただのJSONを返すhttpサーバー。
main.py
import foo from foo.hello import hello def main(): app = foo.make_app(lambda environ: {"message": hello("world")}) foo.run_app(app, 8000) if __name__ == "__main__": main()
foo/__init__.py
import sys import json from wsgiref.simple_server import make_server def make_app(handler): def app(environ, start_response): status = "200 OK" headers = [("Content-type", "application/json; charset=utf-8")] start_response(status, headers) return [json.dumps(handler(environ)).encode("utf-8")] return app def run_app(app, port): httpd = make_server("", port, app) print(f"Serving on port {port}...", file=sys.stderr) # Serve until process is killed httpd.serve_forever()
foo/hello.py
def hello(name: str) -> str: return f"hello {name}"
外部パッケージを利用する場合
さて、先程のzipappは外部パッケージを利用しない場合のもの。zipapp自体は外部パッケージの依存をいい感じに管理はしてくれない。依存したものをzipファイル化したい場合には、いろいろ自分で調整する必要がある
そんな面倒なことをしたくないという場合にはshivが使える。 github.com
ただしsetup.pyを書いてあげる必要がある。パッケージの依存を見たいので。
$ tree . ├── Makefile ├── main.py └── setup.py 0 directories, 3 files
今度はテキトーなライブラリを使ったコードにする。とりあえずfastapiでも使うことにする。
$ python main.py INFO: Started server process [74075] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) INFO: 127.0.0.1:64986 - "GET / HTTP/1.1" 200 OK
以下のようにshivを実行する。console_scriptsを指定して実行されるスクリプトを決めてあげる。
# shiv -o <file> -c <console script> $ shiv -o myapp.pyz -c myapp2 .
myapp2になっている理由は、setup.pyのconsole_scriptsの設定がそうだったため。
setup.py
from distutils.core import setup setup( name="myapp2", version="0.0.0", data_files=["main.py"], # 真面目にモジュールを分けた場合にはpackages=find_packages()を使うかもしれない install_requires=["fastapi", "uvicorn"], entry_points=""" [console_scripts] myapp2 = main:main """, )
main.py
from fastapi import FastAPI app = FastAPI() @app.get("/") def hello(): return {"message": "Hello World"} def main(): import os import uvicorn port = int(os.environ.get("PORT") or "8000") uvicorn.run(app, port=port) if __name__ == "__main__": main()
もちろん別の場所で普通に動く。
$ mv myapp.pyz /tmp # 空の仮想環境で実行 $ python -m venv xxx $ . xxx/bin/activate (xxx) $ pip freeze (xxx) $ python /tmp/myapp.pyz INFO: Started server process [74409] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
zipファイルに含まれた内容はこんな感じ(長いのでgistへのリンク)。
see also
ちなみに提供の形態を把握するにはこのページが便利。
pythonのインタプリタ自体も同梱したい場合にはPyOxidizerあたりが良いかもしれない。