-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
bpo-45234: Fix FileNotFound exception raised instead of IsADirectoryError in shutil.copyfile() #28421
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
bpo-45234: Fix FileNotFound exception raised instead of IsADirectoryError in shutil.copyfile() #28421
Conversation
Lib/shutil.py
Outdated
@@ -279,7 +279,7 @@ def copyfile(src, dst, *, follow_symlinks=True): | |||
|
|||
# Issue 43219, raise a less confusing exception | |||
except IsADirectoryError as e: | |||
if os.path.exists(dst): | |||
if os.path.isdir(src) or os.path.isdir(dst): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to move the open for the destination file to a separate with
statement, and only handle the error for that case. The problem with a trailing slash is only with dst
. It's opened in write mode, which is disallowed for a directory, so the EISDIR
error occurs before the system even checks whether the file or directory exists. If src
doesn't exist, then a trailing slash doesn't matter. For example:
>>> open('spam/', 'rb')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'spam/'
>>> open('spam/', 'wb')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IsADirectoryError: [Errno 21] Is a directory: 'spam/'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't completely sure if on some OSes open() may return IsADirectory error in 'rb' mode, but I think it's not very likely and we already confirmed that's not the case for Linux, MacOS and Windows, so I agree with adding a separate with statement.
With 'isdir()' check, the logic is straightforward: if the error is 'IsADir' and the path is in fact a dir, it's ok to raise the error. But this relies on the subtle difference between open() and isdir() handling of non-existent "directory".
On the other hand it may be clearer to use exists() logic (pseudocode):
...
except IsADirectoryError:
if not pathlib.exists(path): # if it doesn't exist, it's more accurate to raise the following
raise FileNotFoundError()
So I'm leaning to use the latter, does that sound good?
@eryksun as the test failures show, on windows a permission error may also be raised, where it may be more accurate and possible to report an IsADirectory error. Do you think this can be fixed in this PR as well? Or open a separate issue, or leave it as is? It happens if you do copyfile(dir, ...), which opens it in 'rb' mode; I'm not sure if it happens in 'wb' mode on windows, do you happen to remember? |
Opening an existing directory in Windows requires calling At the Python level, opening an existing directory in Windows with
There's an undocumented
In the case where the last component doesn't exist, and the name ends with a trailing slash or backslash, the error will depend on the open/create disposition. If it's an open disposition, the error is
There are a couple of ways to create a directory with |
@eryksun Thanks, that's a very detailed and useful explanation :) For open() it's of course fine, but with a more high level Unfortunately I don't see a way to do this easily that wouldn't lead to a code that's too complicated for what we get out of it. The outer with statement would have to be wrapped in another level of I searched the BPO site and didn't find any complaints about this so I think it's not worth fixing (but maybe documenting?); -- unless you see a way to do it in a simpler way than what I was suggesting above. I will fix the unit test failures a bit later tonight.. |
I've seen novices waste time trying to figure out why they're getting a If Python called |
@eryksun makes sense. |
Lib/test/test_shutil.py
Outdated
self.assertRaises(err, shutil.copyfile, src_file, src_dir) | ||
self.assertRaises(err, shutil.copyfile, dir2, src_dir) | ||
self.assertRaises(err, shutil.copy, dir2, src_dir) | ||
self.assertRaises(err, shutil.copy2, dir2, src_dir) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this just test copyfile()
?
Note that for Windows there's been some discussion about changing copy2()
, and maybe copy()
, to call the system CopyFileExW()
function, which also copies alternate data streams, extended attributes, and security resource attributes. In that case the high-level copy functions will no longer use copyfile()
, copymode()
, and copystat()
in Windows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth testing copy and copy2 for consistency of handling a dir arg, because they already do some dir-related logic (for dst arg), but this may be done here or in separate tests. Looking at other tests, to follow the same naming pattern, we can do test_copy_dirs()
and test_copy2_dirs()
.
But it makes sense to do this testing in the same function for all three because we're ensuring consistency and also it's convenient because the set up of dirs and expected errors is the same.
I can remove these lines though if you think it's not worth testing them here at this point in view of the other work you mentioned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently what copy()
and copy2()
do that's related to src
being a directory is implemented in copyfile()
, so only the latter really needs to be tested. I see the point about checking consistency, but would separate tests maybe be more obvious for someone who's modifying copy()
or copy2()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I will extract them into separate tests a bit later today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the tests and they're passing, please take a look..
…rror in shutil.copyfile() (pythonGH-28421) This was a regression from fixing BPO-43219. (cherry picked from commit b7eac52) Co-authored-by: andrei kulakov <andrei.avk@gmail.com>
GH-28507 is a backport of this pull request to the 3.9 branch. |
…rror in shutil.copyfile() (pythonGH-28421) This was a regression from fixing BPO-43219. (cherry picked from commit b7eac52) Co-authored-by: andrei kulakov <andrei.avk@gmail.com>
GH-28508 is a backport of this pull request to the 3.10 branch. |
Note that os.path.isdir() correctly handles non-existent 'foo/' paths, returning False, unlike open() which on some platforms raises IsADirectoryError for such path.
Also note that I chose to raise IsADirectory error in cases when src is a dir, and dst is a 'does_not_exist/' path, when in theory either error could be raised, but IsADirectory is more backwards-compatible. (and a bit more logical because it's the first error encountered).
https://bugs.python.org/issue45234