先日「could not create 'ファイルパス': No such file or directory」
なるエラーの原因がよくわからず数時間嵌っておりましたが、原因は無意識にdistutils.dir_utilとshutilまぜこぜにしてファイル操作していたためでした。
追記:stack overflowで同事象について語られていました。
https://stackoverflow.com/questions/9160227/dir-util-copy-tree-fails-after-shutil-rmtree
概要は後述・・・
環境情報
OS: Windows 10
Python: 3.7.7
概要
以下で発生します。
-
「パスA」をツリー構成維持したまま「パスB」としてcopy_tree
<distutils.dir_util.copy_tree('パスA', 'パスB')> -
「パスB」を「パスC」としてmove
<shutil.move('パスB', 'パスC')> -
改めて「パスA」をツリー構成維持したまま「パスB」としてcopy_tree
<distutils.dir_util.copy_tree('パスA', 'パスB')>
⇒「could not create 'ファイルパス': No such file or directory」発生
ちなみに「パスA」がファイルを含まない、ディレクトリだけのツリーであればエラーは発生しませんが、copy_treeをしているにもかかわらずディレクトリが一切作成されないという状況になります。
サンプルコードと解説
サンプルコードは以下の通りです。
from os import path
from shutil import move
from distutils.dir_util import remove_tree, mkpath, copy_tree
from pathlib import Path
try:
remove_tree('c:/tmp01/') # <---rmしているので実行する際はお気を付けください。
except:
pass
try:
mkpath('c:/tmp01/test_org/test/test')
Path('c:/tmp01/test_org/test/test/test.txt').touch()
except:
pass
'''copy_treeしてるのにディレクトリとか作成されない'''
# distutils.dir_utilでcopy_tree
copy_tree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
# shutilでディレクトリ移動(rmtreeでもOK)
move('c:/tmp01/test01', 'c:/tmp01/test11')
# 改めてdistutils.dir_utilでcopy_tree
try:
copy_tree('c:/tmp01/test_org', 'c:/tmp01/test01')
except Exception as e:
# エラー発生
# ディレクトリのないところにファイル作ろうとしてエラーになる。
# could not create 'c:/tmp01/test01\test\test\test.txt': No such file or directory
print(e)
print(path.exists('c:/tmp01/test01/test'))
# -> False 作成されていない!
エラー発生個所のcopy_treeをデバッグで追ってみたところ、copy_treeから呼び出しているdistutils.dir_util.mkpathで問題が発生していました。
distutils.dir_util.mkpathはパスが既に存在しているか否かを判定して、存在しない場合にのみパスを作成しようとするようですが、shutilでmove(もしくはrmtree)して対象パスが実際に消えていても、distutils.dir_utilが自ら作成したパスについてはshutilでパスが消されたということを認識できずに、まだ存在するものと判断してしまい、パスを作成しません。
mkpathでのサンプルコードが以下になります。
from os import path
from shutil import move
from distutils.dir_util import remove_tree, mkpath
try:
remove_tree('c:/tmp01/') # <---rmしているので実行する際はお気を付けください。
except:
pass
'''mkpathしてるのにパスが作成されない'''
# distutils.dir_utilでディレクトリ作成
mkpath('c:/tmp01/test01/test')
print(path.exists('c:/tmp01/test01/test'))
# -> True
# shutilでディレクトリ移動(rmtreeでもOK)
move('c:/tmp01/test01', 'c:/tmp01/test11')
# 改めてdistutils.dir_utilでディレクトリ作成
mkpath('c:/tmp01/test01/test')
print(path.exists('c:/tmp01/test01/test'))
# -> False 作成されていない!
解消法
shutilかdistutils.dir_utilどちらかに統一すれば問題にはなりません。
from os import path
from shutil import move, copytree
from distutils.dir_util import remove_tree, mkpath
try:
remove_tree('c:/tmp01/') # <---rmしているので実行する際はお気を付けください。
except:
pass
try:
mkpath('c:/tmp01/test_org/test/test')
except:
pass
'''shutilで統一。copytree'''
# shutilでcopytree
copytree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
# shutilでディレクトリ移動(rmtreeでもOK)
move('c:/tmp01/test01', 'c:/tmp01/test11')
# 改めてshutilでcopytree
copytree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
from os import path
from distutils.dir_util import remove_tree, mkpath, copy_tree
try:
remove_tree('c:/tmp01/') # <---rmしているので実行する際はお気を付けください。
except:
pass
try:
mkpath('c:/tmp01/test_org/test/test')
except:
pass
'''distutils.dir_utilで統一。copy_tree'''
# distutils.dir_utilでcopy_tree
copy_tree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
# distutils.dir_utilでコピー+削除
copy_tree('c:/tmp01/test01', 'c:/tmp01/test11')
remove_tree('c:/tmp01/test01')
# 改めてdistutils.dir_utilでcopy_tree
copy_tree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
distutils.dir_utilはmoveに相当するものが無いようで、コピーして削除という流れにしていますので、若干微妙です。
今回の処理ならshutilで統一して解消させようかと思います。
追記:stack overflowで同事象について語られていました。
https://stackoverflow.com/questions/9160227/dir-util-copy-tree-fails-after-shutil-rmtree
distutils.dir_utilは自身で作成したディレクトリの情報をdistutils.dir_util._path_createdに格納しているので、それをクリアすればOKということみたいでした。
from os import path
from shutil import move
from distutils.dir_util import remove_tree, mkpath, copy_tree
from pathlib import Path
import distutils.dir_util
try:
remove_tree('c:/tmp01/') # <---rmしているので実行する際はお気を付けください。
except:
pass
try:
mkpath('c:/tmp01/test_org/test/test')
Path('c:/tmp01/test_org/test/test/test.txt').touch()
except:
pass
'''copy_treeする前にキャッシュ削除'''
# distutils.dir_utilでcopy_tree
copy_tree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
# shutilでディレクトリ移動(rmtreeでもOK)
move('c:/tmp01/test01', 'c:/tmp01/test11')
# キャッシュdistutils.dir_util._path_createdをクリア
distutils.dir_util._path_created = {}
# 改めてdistutils.dir_utilでcopy_tree
copy_tree('c:/tmp01/test_org', 'c:/tmp01/test01')
print(path.exists('c:/tmp01/test01/test'))
# -> True
所感
嵌ってた原因をまとめてみると、こんなことで嵌ってるやつが私以外にいるのだろうかと思い始めてきましたが・・・だってそもそも混ぜないだろ普通・・・
from ~ importつかってたからcopy_treeとcopytreeが見分けつかなくなってたとか、無駄に関数分けて見通しが悪くなっていたとか、エラーの内容的に引数がおかしくなったんじゃないかとか疑ったりして解析に時間かかったって言い訳をしつつこの件は忘れよう。
追記:同じことで嵌ってる人がStack Overflowにいた!よかった・・・