This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: shutil.move raises AttributeError if first argument is a pathlib.Path object and destination is a directory
Type: behavior Stage: resolved
Components: Library (Lib) Versions: Python 3.9
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: bodom, craigh, emilyemorehouse, gvanrossum, miss-islington, yan12125
Priority: normal Keywords: patch

Created on 2018-01-28 00:01 by craigh, last changed 2022-04-11 14:58 by admin. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 5393 closed emilyemorehouse, 2018-01-28 19:13
PR 15326 merged python-dev, 2019-08-18 13:13
Messages (6)
msg310900 - (view) Author: Craig Holmquist (craigh) Date: 2018-01-28 00:01
>>> import os, pathlib, shutil
>>> os.mkdir('test1')
>>> os.mkdir('test2')
>>> path = pathlib.Path('test1')
>>> shutil.move(path, 'test2')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.6/shutil.py", line 540, in move
    real_dst = os.path.join(dst, _basename(src))
  File "/usr/lib/python3.6/shutil.py", line 504, in _basename
    return os.path.basename(path.rstrip(sep))
AttributeError: 'PosixPath' object has no attribute 'rstrip'
msg310983 - (view) Author: Emily Morehouse (emilyemorehouse) * (Python committer) Date: 2018-01-28 19:13
Thanks for the bug report!

shutil.move should certainly accept a path object, as shutil.copy does, though it should be noted that in your example, 'path' could become out of date as it does not refresh the path information. For example, with shutil.move fixed:

    >>> import os, pathlib, shutil
    >>> os.mkdir('test1')
    >>>
    >>> os.mkdir('test2')
    >>> path = pathlib.Path('test1')
    >>> path.absolute()
    PosixPath('/Users/e/Development/OSS/cpython/test1')
    >>> shutil.move(path, 'test2')
    'test2/test1'
    >>> path.absolute()
    PosixPath('/Users/e/Development/OSS/cpython/test2')

test1 is now actually at '/Users/e/Development/OSS/cpython/test2/test1'


For the fix:
I did a bit of digging and the error comes from a helper method _basename that uses rstrip to remove a trailing separator, hence the error as rstrip doesn't exist for a path object (and I don't think it makes sense that it should, though that was one solution). Removing the trailing separator is, however, very important in determining the full destination path.

After trying a few different approaches, I think the simplest way is to cast the src to a string before finding its appropriate basename. I also added some comments to make it more clear why _basename is used over os.path.basename to hopefully save someone else time in the future.

A more robust option would be to explicitly handle Path objects or to handle exceptions for any dst that cannot be cast to a string. However, the current patch fixes the issue without introducing new problems.
msg310986 - (view) Author: Craig Holmquist (craigh) Date: 2018-01-28 19:43
In my test, the second call to path.absolute() is just returning the same result as the first call, which is what I would expect (as you say, the path object doesn't update automatically).

However, your output shows it returning '/Users/e/Development/OSS/cpython/test2' instead of the (now broken) path from the first call.  Maybe I'm missing something?
msg311373 - (view) Author: Emily Morehouse (emilyemorehouse) * (Python committer) Date: 2018-01-31 21:04
Ah, you're right. That was a typo when I was redacting my full path. The path object remains unchanged even though the directory has moved. 

Should have been:

    >>> import os, pathlib, shutil
    >>> os.mkdir('test1')
    >>> os.mkdir('test2')
    >>> path = pathlib.Path('test1')
    >>> path.absolute()
    PosixPath('/Users/e/Development/OSS/cpython/test1')
    >>> shutil.move(path, 'test2')
    'test2/test1'
    >>> path.absolute()
    PosixPath('/Users/e/Development/OSS/cpython/test1')

test1 is now actually at '/Users/e/Development/OSS/cpython/test2/test1'
msg353628 - (view) Author: miss-islington (miss-islington) Date: 2019-10-01 02:41
New changeset cf57cabef82c4689ce9796bb1fcdb125fa05efcb by Miss Islington (bot) (Maxwell A McKinnon) in branch 'master':
bpo-32689: Updates shutil.move to allow for Path objects to be used as source arg (GH-15326)
https://github.com/python/cpython/commit/cf57cabef82c4689ce9796bb1fcdb125fa05efcb
msg353630 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2019-10-01 02:46
3.9 only!
History
Date User Action Args
2022-04-11 14:58:57adminsetgithub: 76870
2019-12-27 03:13:08xtreaklinkissue39140 superseder
2019-10-01 02:46:32gvanrossumsetstatus: open -> closed

versions: + Python 3.9, - Python 3.6, Python 3.7, Python 3.8
nosy: + gvanrossum

messages: + msg353630
resolution: fixed
stage: patch review -> resolved
2019-10-01 02:41:28miss-islingtonsetnosy: + miss-islington
messages: + msg353628
2019-09-05 10:00:24bodomsetnosy: + bodom
2019-08-18 13:13:41python-devsetpull_requests: + pull_request15044
2019-01-14 10:51:06Tiger-222setversions: + Python 3.7, Python 3.8
2018-07-08 12:12:32berker.peksaglinkissue34069 superseder
2018-01-31 21:04:15emilyemorehousesetmessages: + msg311373
2018-01-31 17:32:55yan12125setnosy: + yan12125
2018-01-28 19:43:45craighsetmessages: + msg310986
2018-01-28 19:13:57emilyemorehousesetnosy: + emilyemorehouse
messages: + msg310983
2018-01-28 19:13:07emilyemorehousesetkeywords: + patch
stage: patch review
pull_requests: + pull_request5228
2018-01-28 00:01:24craighcreate