この記事の結論
Python の pathlib.Path.is_file() によるファイル存在判定は、OS ごとに「存在しない長いファイルパス」への挙動が以下のように異なる (「個別に短いパス」の意味は後述)。
なお、Windows では長いパスを有効にしているものとする。
| Windows | Linux | macOS | |
|---|---|---|---|
| ファイルパスが ~ 約 1024 バイト 上:左から存在しない「個別に短いパス」が取れる 下:左から存在しない「〃」が取れない |
False (1) False (2) |
False (1) 例外 (4) |
False (1) 例外 (4) |
| ファイルパスが 約 1024 ~ 4096 バイト 上:左から存在しない「個別に短いパス」が取れる 下:左から存在しない「〃」が取れない |
False (1) False (2) |
False (1) 例外 (4) |
例外 (5) |
| ファイルパスが 約 4096 ~ 32767 バイト 上:左から存在しない「個別に短いパス」が取れる 下:左から存在しない「〃」が取れない |
False (1) False (2) |
例外 (5) | 例外 (5) |
| ファイルパスが 約 32767 バイト ~ | False (3) | 例外 (5) | 例外 (5) |
上記の表のセル内の各番号で発生する事象は以下である。
- (1)
os.stat()がENOENT(No such file or directory) となるが、捕捉される。 - (2)
os.stat()が_WINERROR_INVALID_NAME(ファイル名不正) となるが、捕捉される。 - (3)
os.stat()がValueError: stat: path too long for Windowsだが、捕捉される。 - (4)
os.stat()がENAMETOOLONG(ファイル/ディレクトリ長超過) で、捕捉されない。 - (5)
os.stat()がENAMETOOLONG(フルパス最大長超過) で、捕捉されない。
これらの差は、pathlib が ENAMETOOLONG (名前が長すぎる) は捕捉しない一方で、Windows の _WINERROR_INVALID_NAME や ValueError: stat: path too long for Windows は捕捉すること、OS ごとに許容されるパスの最大長が異なる (macOS 1024 バイト、Linux 4096 バイト、Windows は長いパスを有効にすれば 32767 バイト) ことに起因する。
…なので、クロスプラットフォームで動作させるプロジェクト (特にファイル存在判定に長い文字列が渡されうるプロジェクト) ではファイル存在確認に注意する。例えばファイル名の形式を定めておき pathlib.Path.is_file() の前にチェックするか、OSError を捕捉する。
import pathlib
def is_file(path):
try:
return pathlib.Path(path).is_file()
except OSError:
pass
return False
ret = is_file('a' * 1100)
print(ret)
なお、ここで「個別に短いパス」とは、個別のディレクトリ/ファイル名は約 255 バイト以内になっているパスのことをいう (下記)。要は、左から短い名前を解決している間に不存在が発見されれば、ENAMETOOLONG より先に ENOENT (No such file or directory) が出てくれる (そして pathlib が捕捉してくれる)。
- 左から存在しない「個別に短いパス」が取れる例 (実際に存在、不存在):
- {英数250字}/{英数250字}/{英数250字}/{英数500字}
- {英数250字}/{英数250字}/{英数250字}/{英数500字}
- {英数250字}/{英数250字}/{英数250字}/{英数500字}
- 左から存在しない「個別に短いパス」が取れない例 (実際に存在、不存在):
- {英数250字}/{英数250字}/{英数250字}/{英数500字}
参考文献
-
https://github.com/python/cpython/blob/3.12/Lib/pathlib.py#L886-L901 -
pathlib.Path.is_file()の実装。OSErrorは_ignore_error()のみ捕捉する。ValueErrorは必ず捕捉する。 -
https://github.com/python/cpython/blob/3.12/Lib/pathlib.py#L52-L54 -
_ignore_error()の実装。ENOENT(No such file or directory) 及び_WINERROR_INVALID_NAMEは捕捉されるが、ENAMETOOLONGは捕捉されない。
OS ごとの os.stat() の実行結果
pathlib.Path.is_file() は os.stat() で情報取得するので、以下の結果を確認する。
python -c "import os; os.stat('a' * 1100 + '.txt')"
python -c "import os; os.stat('a/' + 'a' * 1100 + '.txt')"
結果、どの OS でも存在しないファイルの情報取得は例外になるが、例外の内容が異なる。macOS では 1100 文字で既に許容されるパス最大長を超過しているために、親ディレクトリが不存在でも File name too long になっている。
| Windows | Linux | macOS | |
|---|---|---|---|
os.stat('a' * 1100 + '.txt') (存在しない名前が長いファイル) |
OSError: [WinError 123] ファイル名、ディレクトリ名、またはボリューム ラベルの構文が間違っています。 | OSError: [Errno 36] File name too long | OSError: [Errno 63] File name too long |
os.stat('a/' + 'a' * 1100 + '.txt') (存在しない名前の短いサブディレクトリ以下にある名前が長いファイル) |
FileNotFoundError: [WinError 3] 指定されたパスが見つかりません。 | FileNotFoundError: [Errno 2] No such file or directory | OSError: [Errno 63] File name too long |
ちなみに、'a' * 5000 にすると Linux も親ディレクトリによらず File name too long になる。さらに 'a' * 50000 にすると Windows は親ディレクトリによらず ValueError: stat: path too long for Windows となる (ValueError になるのが他 OS と異なる)。
OS ごとの pathlib.Path.is_file() の実行結果
次に、肝心の pathlib.Path.is_file() の結果を確認する。
python -c "import pathlib; print(pathlib.Path('a' * 1100 + '.txt').is_file())"
python -c "import pathlib; print(pathlib.Path('a/' + 'a' * 1100 + '.txt').is_file())"
pathlib.Path.is_file() はファイル名が長すぎる例外を捕捉しないため、この例外は貫通している。が、Windows でファイル名が長すぎる場合に送出される _WINERROR_INVALID_NAME は捕捉するので Windows においてはファイル名が長くても例外とならない。
| Windows | Linux | macOS | |
|---|---|---|---|
pathlib.Path('a' * 1100 + '.txt').is_file() (存在しない名前が長いファイル) |
False | OSError: [Errno 36] File name too long | OSError: [Errno 63] File name too long |
pathlib.Path('a/' + 'a' * 1100 + '.txt').is_file() (存在しない名前の短いサブディレクトリ以下にある名前が長いファイル) |
False | False | OSError: [Errno 63] File name too long |
この記事の背景
- 私の実験プロジェクトでは、実験設定を TOML ファイルパスでも TOML 解釈できる文字列でも渡せるようにしていた (先にファイルパスとみなして存在確認し、存在しなければ TOML パースを試みる)。これは Windows 機と Linux 機で問題なく動作していた。が、最近 Mac 機でも実験しようとしたら一部のテストがエラーになった。
- 原因は TOML 文字列をファイルパスとみなして存在確認するとき、Mac では期待通り
Falseが返らずに例外が送出されるためである。実験設定 TOML 文字列は英数字 2000 文字程度のため、Mac においてはパス最大長を超過してただちに File name too long となり、これは pathlib に捕捉されない。 - Linux でエラーとなっていなかった原因は、 Linux ではパス最大長には到達していなかった & 実験設定 TOML 文字列にはスラッシュが含まれるため、最初のスラッシュまでが有効バイト数の親ディレクトリとみなされ FileNotFoundError となって捕捉されていたためである。
- Windows でエラーとなっていなかった原因は、実験設定 TOML 文字列は Windows ではファイル名に使用できない文字 (ダブルクオート等) を含んでおり
_WINERROR_INVALID_NAMEとなっていたが、これも捕捉されていたためである。
- 原因は TOML 文字列をファイルパスとみなして存在確認するとき、Mac では期待通り
- 暫定対応として、ファイル存在確認より TOML パースを先に試みるようにした。