My Fucking Note

Software Engineeringのメモ

Zappaのexcludeでパス指定はできない

Zappaとは

ZappaAWS用サーバーレスPythonのパッケージだ。 ZappaはLambda用のパッケージングやAPI GatewayやIAMの設定をしてくれて、Pythonアプリケーションを簡単にデプロイすることができる。

excludeオプション

Zappaにはパッケージングから除外したいファイルを指定するexcludeというオプションがある。このオプションはテストコードやドキュメントなどの実行時に不要なファイルをパッケージから除外するためのものであると思われる。このexcludeでgitの.gitignoreで使われるようなパスとファイル名を組み合わせた指定ができないことが分かった。 例えば doc/*.txt の様な指定はできない。

なぜパス指定ができないか

Zappaのコードを読んでみると、excludeオプションは zappa.core.create_lambda_zip 関数に渡されている。

def create_lambda_zip(
    self,
    prefix="lambda_package",
    handler_file=None,
    slim_handler=False,
    minify=True,
    exclude=None,
    exclude_glob=None,
    use_precompiled_packages=True,
    include=None,
    venv=None,
    output=None,
    disable_progress=False,
    archive_format="zip",
):

create_lambda_zip 関数を読み進めていくと、Python標準ライブラリの shutil.copytreeshutil.ignore_patterns使われている

copytree(
    cwd,
    temp_project_path,
    metadata=False,
    symlinks=False,
    ignore=shutil.ignore_patterns(*excludes),
)

copytreeのドキュメントには次のように書いてある。

ignore は copytree() が走査しているディレクトリと os.listdir() が返すその内容のリストを引数として受け取ることのできる呼び出し可能オブジェクトでなければなりません。 copytree() は再帰的に呼び出されるので、 ignore はコピーされる各ディレクトリ毎に呼び出されます。 ignore の戻り値はカレントディレクトリに相対的なディレクトリ名およびファイル名のシーケンス(すなわち第二引数の項目のサブセット)でなければなりません。それらの名前はコピー中に無視されます。 ignore_patterns() を用いて glob 形式のパターンによって無視する呼び出し可能オブジェクトを作成することが出来ます。

Python 3.10の実装だと

まずos.scandirで各ディレクトリ内のファイルを列挙している。 https://github.com/python/cpython/blob/v3.10.4/Lib/shutil.py#L554~L555

with os.scandir(src) as itr:
    entries = list(itr)

次にscandir()がyieldするos.DirEntryのname属性に対してignoreを実行している。

https://github.com/python/cpython/blob/v3.10.4/Lib/shutil.py#L453

ignored_names = ignore(os.fspath(src), [x.name for x in entries])

ignore_patternsが生成するignore関数は第一引数でパスを受け取るようになっているが、中をよく見ると第一引数は全く使われていない。

https://github.com/python/cpython/blob/v3.10.4/Lib/shutil.py#L443~L447

def _ignore_patterns(path, names):
    ignored_names = []
    for pattern in patterns:
        ignored_names.extend(fnmatch.filter(names, pattern))
    return set(ignored_names)

os.DirEntryのname属性にもパス情報は含まれていない。 そのためZappaのexcludeオプションにスラッシュ区切りのパスでパターンを指定しても、比較対象のos.DirEntry.nameにはマッチせずただ無視されるだけとなる。

Python3.7くらいまでの実装も確認したが、os.scandirの代わりにos.listdirが使われていたり、copytreeの実装が若干違うだけで振る舞いは変わらなそうだった。