New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
tempfile.TemporaryDirectory() removes entire directory tree even if it's a mount-point #80603
Comments
|
The behavior of However, in case a filesystem has been mounted on the temporary While I agree that it would be responsibility of the programmer to @contextmanager
def mount_sshfs(localdir, remote):
subprocess.run(f"sshfs {remote} {localdir}")
yield
subprocess.run(f"fusermount -u {localdir}", check=True)
with TemporaryDirectory() as tmpdir:
with mount_sshfs(tmpdir, remote):
# ... do stuff ...Now, even if the Is there a way this pattern can be prevented or at least mitigated?
I have seen this happen with Py 3.6 but it's likely there in the entire 3.x series since Thanks, |
|
Allowing delete_on_error=False is kind of defeating the purpose here; the whole point of the with statement is guaranteed, consistent cleanup in both normal and error cases. If you knew enough to know you needed to pass delete_on_error, you'd also know enough to know you should be handling errors properly in the first place, e.g. by changing your mount_sshfs manager to: @contextmanager
def mount_sshfs(localdir, remote):
subprocess.run(f"sshfs {remote} {localdir}")
try:
yield
finally:
subprocess.run(f"fusermount -u {localdir}", check=True)so it actually performed the guaranteed cleanup you expected from it. I don't see anything wrong with adding a delete=True argument to TemporaryDirectory, though I'm not seeing it as being as useful as it is with TemporaryFile (since the "rewrite file to tempfile, atomic replace old file with new file" pattern for updating a file safely doesn't transfer directly to directories, where atomic renames aren't an option). It just seems like your fundamental problem is code that doesn't properly handle failure, and I don't think the solution is to make TemporaryDirectory handle failure badly as well. |
This would fix the case where errors occur in the "yield" part of the I would contend that, in general, |
|
Only deleting from the local filesystem seems reasonable to me. In the context of a temporary directory tree, I don't see a semantic difference between "unmounting a mount point" and "removing a subdirectory entry" -- i.e. they both remove the offending subdirectory tree from the temporary directory tree. Internally, tempfile.TemporaryDirectory() calls shutil.rmtree() with a custom error handler. The handler tries to reset the permissions on the offending entry and calls os.unlink(). If this throws an error, it recursively calls shutil.rmtree() on the offending entry (ignoring the temp directory entry itself). This seems like where a mounted directory tree would get deleted. shutil.rmtree() follows the "rm -rf" semantics and refuses to delete a mount point. It seems reasonable to me to add special handling for mount points to the error handler in tempfile.TemporaryDirectory(). The high-level logic would be something like: if os.path.ismount(path):
try:
unmount_func(path)
_shutil.rmtree(path)
except FileNotFoundError:
passThe tricky part is implementing unmount_func() in a portable manner since there is no such functionality currently implemented in the Python library. Also, what would be the appropriate behavior when unmount_func() fails (e.g. for a permission error)? Right now, any exceptions other than IsADirectoryError, IsADirectoryError, and FileNotFoundError are passed on to the caller. |
|
(correcting typos) What would be the appropriate behavior when unmount_func() fails (e.g. for a permission error)? Right now, any exceptions other than IsADirectoryError, PermissionError, and FileNotFoundError are passed on to the caller. |
|
Since tempfile.TemporaryDirectory() already returns some exceptions to the caller when it can't figure out how to delete a directory item, I don't see a problem with throwing PermissionError when encountering a mount point. This would preserve the 'rm -rf' semantics of the underlying shutil.rmtree() calls. |
|
Since having tempfile.TemporaryDirectory() automatically unmount mount points is difficult to implement in a portable way (Windows, BSD/macOS, and Linux are all different), a shorter-term solution could be to add an 'onerror' callback function as a class initialization parameter. This would allow the caller to inspect and try to handle any directory entries that the cleanup() function can't handle (like unmounting a mount point). |
|
Another data point: On macOS 10.14.4, unlink() returns EBUSY when it tries to delete the mount point. tempfile.TemporaryDirectory() doesn't handle EBUSY and ends up skipping over the mount point and propagates the OSError exception up to the caller. It leaves the mounted directory tree untouched. |
|
tempfile.TemporaryDirectory() relies upon shutil.rmtree() to do the actual cleanup. Up through 3.7, it simply calls shutil.rmtree(). 3.8 adds some more logic using the onerror callback parameter of shutil.rmtree() to try changing the permissions on otherwise undeletable directory entries. In order to make tempfile.TemporaryDirectory() handle mount points, shutil.rmtree() requires some enhancements so that mount points can be detected before their contents are deleted. I am working on some generic enhancements to tempfile.TemporaryDirectory() and shutil.rmtree() that add (hopefully) useful functionality that covers mount points as well as other corner cases:
I debated implementing "ondelete" instead of "onitem". "ondelete" would be called just prior to deleting an item, while "onitem" is called when the item is encountered. The difference in semantics is that a subdirectory entry triggers a call to "onitem" immediately, while "ondelete" would get called after the subdirectory tree had been traversed and all of its items deleted. "onitem" seems more useful because it allows the caller to implement special handling per item. I will have a pull request ready soon. In the mean time, I am open to any other suggestions for handling non-portable corner cases. |
|
I implemented an onerror callback for tempfile.TemporaryDirectory() and confirmed that it cannot be used to unmount a mount point. Attempting to unmount the mount point from within the callback results in a resource busy error. It may be related to shutil.rmtree() holding an open file descriptor to the parent directory of the mount point. I uploaded the program I used for testing on macOS (test-mounted-image.py). The subprocess.run() calls can be modified to run it on other platforms. |
-1 That is expected behavior and the use case looks pretty unusual. Such a new parameter wouldn't even be supported by other "batteries" since there's no portable/standard way to either mount or unmount a directory (in fact you had to use a subprocess in your unit-tests). A
In summary, I think this would over-complicate the API for no good reason. |


Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
The text was updated successfully, but these errors were encountered: