Skip to content

Commit

Permalink
Add audeer.move() (#128)
Browse files Browse the repository at this point in the history
* Add audeer.move() as replacement for move_file()

* Add fixes and tests

* Extend docstring

* Add to API docs

* Try to fix Windows test

* Test empty folder under Windows

* Adjust test and docstring

* Update tests, extend docstring

* Add more tests and error cases

* Fix typo

* Change to OSError

* Rephrase docstring

* Fix expected Windows error message

* Try to fix Windows test

* Try to fix Windows test

* Make test easier to understand

* Update audeer/core/io.py

Co-authored-by: audeerington <[email protected]>

* Use move() instead of move_file() in tests

---------

Co-authored-by: audeerington <[email protected]>
  • Loading branch information
hagenw and audeerington authored Dec 6, 2023
1 parent ef3b271 commit 11b8734
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 3 deletions.
1 change: 1 addition & 0 deletions audeer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from audeer.core.io import list_file_names
from audeer.core.io import md5
from audeer.core.io import mkdir
from audeer.core.io import move
from audeer.core.io import move_file
from audeer.core.io import replace_file_extension
from audeer.core.io import rmdir
Expand Down
48 changes: 47 additions & 1 deletion audeer/core/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,48 @@ def md5_read_chunk(
yield data


def move(
src_path,
dst_path,
):
"""Move a file or folder independent of operating system.
As :func:`os.rename` works differently
under Unix and Windows
and :func:`shutil.move` can be slow,
we use :func:`os.replace`
to move the file/folder.
Args:
src_path: source file/folder path
dst_path: destination file/folder path
Raises:
OSError: if ``src_path`` is a file
and ``dst_path`` is an existing folder
OSError: if ``src_path`` is a folder
and ``dst_path`` is an existing file
(not raised under Windows)
OSError: if ``dst_path`` is a non-empty folder,
different from ``src_path``,
and ``src_path`` is also a folder
OSError: if ``dst_path`` is an empty folder,
different from ``src_path``
and ``src_path`` is also a folder
(raised only under Windows)
Examples:
>>> path = mkdir('folder')
>>> src_path = touch(path, 'file1')
>>> dst_path = os.path.join(path, 'file2')
>>> move(src_path, dst_path)
>>> list_file_names(path, basenames=True)
['file2']
"""
os.replace(src_path, dst_path)


def move_file(
src_path,
dst_path,
Expand All @@ -854,6 +896,10 @@ def move_file(
we use :func:`os.replace`
to move the file.
Warning:
:func:`audeer.move_file` is deprecated,
please use :func:`audeer.move` instead.
Args:
src_path: source file path
dst_path: destination file path
Expand All @@ -867,7 +913,7 @@ def move_file(
['file2']
"""
os.replace(src_path, dst_path)
move(src_path, dst_path)


def replace_file_extension(
Expand Down
1 change: 1 addition & 0 deletions docs/api-src/audeer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ audeer
LooseVersion
md5
mkdir
move
move_file
path
progress_bar
Expand Down
253 changes: 251 additions & 2 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,256 @@ def test_mkdir(tmpdir):
assert mode != int('775', 8)


@pytest.mark.parametrize(
'src_path, dst_path',
[
(
'path1',
'path1',
),
(
'path1',
'path2',
),
]
)
def test_move(tmpdir, src_path, dst_path):

system = platform.system()
tmp_dir = audeer.mkdir(tmpdir, 'folder')

# src: file
# dst: new file
audeer.touch(tmp_dir, src_path)
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))

# src: file
# dst: existing file
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir)
audeer.touch(tmp_dir, src_path)
audeer.touch(tmp_dir, dst_path)
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))

# src: folder
# dst: new folder
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir, src_path)
audeer.touch(tmp_dir, src_path, 'file.txt')
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt'))
assert os.path.isdir(os.path.join(tmp_dir, dst_path))

# src: non-empty folder
# dst: existing non-empty folder
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir, src_path)
audeer.touch(tmp_dir, src_path, 'file.txt')
if src_path != dst_path:
audeer.mkdir(tmp_dir, dst_path)
audeer.touch(tmp_dir, dst_path, 'file.txt')
if src_path != dst_path:
if system == 'Windows':
error_msg = 'Access is denied'
else:
error_msg = 'Directory not empty'
with pytest.raises(OSError, match=error_msg):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

# src: non-empty folder
# dst: existing empty folder
os.remove(os.path.join(tmp_dir, dst_path, 'file.txt'))
if system == 'Windows':
# Only under Windows
# we get an error
# if destination is an empty folder
with pytest.raises(OSError, match='Access is denied'):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
else:
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt'))
assert os.path.isdir(os.path.join(tmp_dir, dst_path))

# src: non-empty folder
# dst: identical to src
if src_path == dst_path:
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt'))
assert os.path.isdir(os.path.join(tmp_dir, dst_path))

# src: empty folder
# dst: existing non-empty folder
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir, src_path)
if src_path != dst_path:
audeer.mkdir(tmp_dir, dst_path)
audeer.touch(tmp_dir, dst_path, 'file.txt')
if src_path != dst_path:
if system == 'Windows':
error_msg = 'Access is denied'
else:
error_msg = 'Directory not empty'
with pytest.raises(OSError, match=error_msg):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

# src: empty folder
# dst: existing empty folder
os.remove(os.path.join(tmp_dir, dst_path, 'file.txt'))
if system == 'Windows':
# Only under Windows
# we get an error
# if destination is an empty folder
with pytest.raises(OSError, match='Access is denied'):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
else:
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert not os.path.exists(
os.path.join(tmp_dir, dst_path, 'file.txt')
)
assert os.path.isdir(os.path.join(tmp_dir, dst_path))

# src: empty folder
# dst: identical to src
if src_path == dst_path:
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

if src_path != dst_path:
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert not os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt'))
assert os.path.isdir(os.path.join(tmp_dir, dst_path))

if src_path != dst_path:

# src: file
# dst: non-empty folder
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir)
audeer.touch(tmp_dir, src_path)
audeer.mkdir(tmp_dir, dst_path)
audeer.touch(tmp_dir, dst_path, 'file.txt')
if system == 'Windows':
error_msg = 'Access is denied'
else:
error_msg = 'Is a directory'
with pytest.raises(OSError, match=error_msg):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

# src: file
# dst: empty folder
os.remove(os.path.join(tmp_dir, dst_path, 'file.txt'))
with pytest.raises(OSError, match=error_msg):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)

# src: non-empty folder
# dst: file
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir)
audeer.mkdir(tmp_dir, src_path)
audeer.touch(tmp_dir, src_path, 'file.txt')
audeer.touch(tmp_dir, dst_path)
if system != 'Windows':
error_msg = 'Not a directory'
with pytest.raises(OSError, match=error_msg):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
else:
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path, 'file.txt'))
assert os.path.isdir(os.path.join(tmp_dir, dst_path))

# src: empty folder
# dst: file
audeer.rmdir(tmp_dir)
audeer.mkdir(tmp_dir, src_path)
audeer.touch(tmp_dir, dst_path)
if system != 'Windows':
error_msg = 'Not a directory'
with pytest.raises(OSError, match=error_msg):
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
else:
audeer.move(
os.path.join(tmp_dir, src_path),
os.path.join(tmp_dir, dst_path),
)
assert not os.path.exists(os.path.join(tmp_dir, src_path))
assert os.path.exists(os.path.join(tmp_dir, dst_path))
assert not os.path.exists(
os.path.join(tmp_dir, dst_path, 'file.txt')
)
assert os.path.isdir(os.path.join(tmp_dir, dst_path))


@pytest.mark.parametrize(
'src_file, dst_file',
[
Expand All @@ -1150,8 +1400,7 @@ def test_mkdir(tmpdir):
)
def test_move_file(tmpdir, src_file, dst_file):

tmp_path = str(tmpdir.mkdir('folder'))
tmp_path = audeer.mkdir(tmp_path)
tmp_path = audeer.mkdir(tmpdir, 'folder')

src_path = audeer.touch(tmp_path, src_file)
dst_path = os.path.join(tmp_path, dst_file)
Expand Down

0 comments on commit 11b8734

Please sign in to comment.