Skip to content

Commit 3557917

Browse files
authored
formatting and coverage for copy_if_newer (#34)
* formatting and coverage for copy_if_newer * simplify copy_file * doc fix
1 parent 36c2af0 commit 3557917

3 files changed

Lines changed: 110 additions & 92 deletions

File tree

docs/source/info.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,5 +189,5 @@ Because of this requirement, times are stored as
189189
`epoch times <https://en.wikipedia.org/wiki/Unix_time>`_. The Info object
190190
will convert these to datetime objects from the standard library.
191191
Additionally, the Info object will convert permissions from a list of
192-
strings in to a `class`:fs.permissions.Permissions` objects.
192+
strings in to a :class:`~fs.permissions.Permissions` objects.
193193

fs/copy.py

Lines changed: 82 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,22 @@ def copy_fs(src_fs, dst_fs, walker=None, on_copy=None):
3030
in ``src_fs``. Set this if you only want to consider a sub-set
3131
of the resources in ``src_fs``.
3232
:type walker: :class:`~fs.walk.Walker`
33-
:param on_copy: A function callback called after a single file copy is executed.
34-
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs, dst_path)``.
33+
:param on_copy: A function callback called after a single file copy
34+
is executed.
35+
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs,
36+
dst_path)``.
3537
"""
36-
return copy_dir(src_fs, '/', dst_fs, '/', walker=walker, on_copy=on_copy)
38+
return copy_dir(src_fs, '/', dst_fs, '/',
39+
walker=walker, on_copy=on_copy)
3740

3841

3942
def copy_fs_if_newer(src_fs, dst_fs, walker=None, on_copy=None):
4043
"""
41-
Copy the contents of one filesystem to another. If both source and destination files exist,
42-
the copy is executed only if the source file is newer than the destination file.
43-
In case modification times of source or destination files are not available,
44-
copy file is always executed.
44+
Copy the contents of one filesystem to another. If both source and
45+
destination files exist, the copy is executed only if the source
46+
file is newer than the destination file. In case modification times
47+
of source or destination files are not available, copy file is
48+
always executed.
4549
4650
:param src_fs: Source filesystem.
4751
:type src_fs: :type src_fs: FS URL or instance
@@ -53,10 +57,13 @@ def copy_fs_if_newer(src_fs, dst_fs, walker=None, on_copy=None):
5357
in ``src_fs``. Set this if you only want to consider a sub-set
5458
of the resources in ``src_fs``.
5559
:type walker: :class:`~fs.walk.Walker`
56-
:param on_copy: A function callback called after a single file copy is executed.
57-
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs, dst_path)``.
60+
:param on_copy: A function callback called after a single file copy
61+
is executed.
62+
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs,
63+
dst_path)``.
5864
"""
59-
return copy_dir_if_newer(src_fs, '/', dst_fs, '/', walker=walker, on_copy=on_copy)
65+
return copy_dir_if_newer(src_fs, '/', dst_fs, '/',
66+
walker=walker, on_copy=on_copy)
6067

6168

6269
def _source_is_newer(src_fs, src_path, dst_fs, dst_path):
@@ -73,29 +80,21 @@ def _source_is_newer(src_fs, src_path, dst_fs, dst_path):
7380
file modification time cannot be determined. False otherwise.
7481
"""
7582
try:
76-
if not dst_fs.exists(dst_path):
77-
return True
78-
else:
83+
if dst_fs.exists(dst_path):
7984
namespace = ('details', 'modified')
80-
8185
src_modified = src_fs.getinfo(src_path, namespace).modified
82-
if src_modified is None:
83-
return True
84-
85-
dst_modified = dst_fs.getinfo(dst_path, namespace).modified
86-
if dst_modified is None:
87-
return True
88-
89-
return src_modified > dst_modified
90-
except FSError:
86+
if src_modified is not None:
87+
dst_modified = dst_fs.getinfo(dst_path, namespace).modified
88+
return dst_modified is None or src_modified > dst_modified
89+
return True
90+
except FSError: # pragma: nocover
9191
#todo: should log something here
92-
#log.error("cannot determine if source file " + src_path + " is newer than destination file " + dst_path + ", thus safely copy the file')
9392
return True
9493

9594
def copy_file(src_fs, src_path, dst_fs, dst_path):
9695
"""
97-
Copy a file from one filesystem to another.
98-
If the destination exists, and is a file, it will be first truncated.
96+
Copy a file from one filesystem to another. If the destination
97+
exists, and is a file, it will be first truncated.
9998
10099
:param src_fs: Source filesystem.
101100
:type src_fs: FS URL or instance
@@ -105,33 +104,31 @@ def copy_file(src_fs, src_path, dst_fs, dst_path):
105104
:type dst_fs: FS URL or instance
106105
:param dst_path: Path to a file on ``dst_fs``.
107106
:type dst_path: str
108-
:returns: True if the file copy was executed, False otherwise.
109107
110108
"""
111109
with manage_fs(src_fs, writeable=False) as src_fs:
112110
with manage_fs(dst_fs, create=True) as dst_fs:
113111
if src_fs is dst_fs:
114-
# Same filesystem, so we can do a potentially optimized copy
112+
# Same filesystem, so we can do a potentially optimized
113+
# copy
115114
src_fs.copy(src_path, dst_path, overwrite=True)
116-
return True
117115
else:
118116
# Standard copy
119117
with src_fs.lock(), dst_fs.lock():
120118
with src_fs.open(src_path, 'rb') as read_file:
121-
# There may be an optimized copy available on dst_fs
119+
# There may be an optimized copy available on
120+
# dst_fs
122121
dst_fs.setbinfile(dst_path, read_file)
123-
return True
124-
125122

126123

127124
def copy_file_if_newer(src_fs, src_path, dst_fs, dst_path):
128125
"""
129-
Copy a file from one filesystem to another.
130-
If the destination exists, and is a file, it will be first truncated.
131-
If both source and destination files exist,
132-
the copy is executed only if the source file is newer than the destination file.
133-
In case modification times of source or destination files are not available,
134-
copy is always executed.
126+
Copy a file from one filesystem to another. If the destination
127+
exists, and is a file, it will be first truncated. If both source
128+
and destination files exist, the copy is executed only if the source
129+
file is newer than the destination file. In case modification times
130+
of source or destination files are not available, copy is always
131+
executed.
135132
136133
:param src_fs: Source filesystem.
137134
:type src_fs: FS URL or instance
@@ -147,7 +144,8 @@ def copy_file_if_newer(src_fs, src_path, dst_fs, dst_path):
147144
with manage_fs(src_fs, writeable=False) as src_fs:
148145
with manage_fs(dst_fs, create=True) as dst_fs:
149146
if src_fs is dst_fs:
150-
# Same filesystem, so we can do a potentially optimized copy
147+
# Same filesystem, so we can do a potentially optimized
148+
# copy
151149
if _source_is_newer(src_fs, src_path, dst_fs, dst_path):
152150
src_fs.copy(src_path, dst_path, overwrite=True)
153151
return True
@@ -156,9 +154,11 @@ def copy_file_if_newer(src_fs, src_path, dst_fs, dst_path):
156154
else:
157155
# Standard copy
158156
with src_fs.lock(), dst_fs.lock():
159-
if _source_is_newer(src_fs, src_path, dst_fs, dst_path):
157+
if _source_is_newer(src_fs, src_path,
158+
dst_fs, dst_path):
160159
with src_fs.open(src_path, 'rb') as read_file:
161-
# There may be an optimized copy available on dst_fs
160+
# There may be an optimized copy available
161+
# on dst_fs
162162
dst_fs.setbinfile(dst_path, read_file)
163163
return True
164164
else:
@@ -188,7 +188,8 @@ def copy_structure(src_fs, dst_fs, walker=None):
188188
dst_fs.makedir(dir_path, recreate=True)
189189

190190

191-
def copy_dir(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=None):
191+
def copy_dir(src_fs, src_path, dst_fs, dst_path,
192+
walker=None, on_copy=None):
192193
"""
193194
Copy a directory from one filesystem to another.
194195
@@ -203,11 +204,13 @@ def copy_dir(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=None):
203204
in ``src_fs``. Set this if you only want to consider a sub-set
204205
of the resources in ``src_fs``.
205206
:type walker: :class:`~fs.walk.Walker`
206-
:param on_copy: A function callback called after a single file copy is executed.
207-
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs, dst_path)``.
207+
:param on_copy: A function callback called after a single file copy
208+
is executed.
209+
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs,
210+
dst_path)``.
208211
209212
"""
210-
213+
on_copy = on_copy or (lambda *args: None)
211214
walker = walker or Walker()
212215
_src_path = abspath(normpath(src_path))
213216
_dst_path = abspath(normpath(dst_path))
@@ -226,23 +229,25 @@ def copy_dir(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=None):
226229
recreate=True
227230
)
228231
for info in files:
229-
file_copied = copy_file(
232+
src_path = info.make_path(dir_path)
233+
dst_path = info.make_path(copy_path)
234+
copy_file(
230235
src_fs,
231-
info.make_path(dir_path),
236+
src_path,
232237
dst_fs,
233-
info.make_path(copy_path)
238+
dst_path
234239
)
235-
if file_copied:
236-
if on_copy is not None:
237-
on_copy(src_fs, dir_path, dst_fs, copy_path)
240+
on_copy(src_fs, src_path, dst_fs, dst_path)
238241

239242

240-
def copy_dir_if_newer(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=None):
243+
def copy_dir_if_newer(src_fs, src_path, dst_fs, dst_path,
244+
walker=None, on_copy=None):
241245
"""
242-
Copy a directory from one filesystem to another. If both source and destination files exist,
243-
the copy is executed only if the source file is newer than the destination file.
244-
In case modification times of source or destination files are not available,
245-
copy is always executed.
246+
Copy a directory from one filesystem to another. If both source and
247+
destination files exist, the copy is executed only if the source
248+
file is newer than the destination file. In case modification times
249+
of source or destination files are not available, copy is always
250+
executed.
246251
247252
:param src_fs: Source filesystem.
248253
:type src_fs: FS URL or instance
@@ -255,10 +260,13 @@ def copy_dir_if_newer(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=N
255260
in ``src_fs``. Set this if you only want to consider a sub-set
256261
of the resources in ``src_fs``.
257262
:type walker: :class:`~fs.walk.Walker`
258-
:param on_copy: A function callback called after a single file copy is executed.
259-
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs, dst_path)``.
263+
:param on_copy: A function callback called after a single file copy
264+
is executed.
265+
:type on_copy: Function, with signature ``(src_fs, src_path, dst_fs,
266+
dst_path)``.
260267
261268
"""
269+
on_copy = on_copy or (lambda *args: None)
262270
walker = walker or Walker()
263271
_src_path = abspath(normpath(src_path))
264272
_dst_path = abspath(normpath(dst_path))
@@ -268,12 +276,13 @@ def copy_dir_if_newer(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=N
268276
dst_fs.makedir(_dst_path, recreate=True)
269277
namespace = ('details', 'modified')
270278
dst_state = {
271-
filepath: info
272-
for filepath, info in walker.info(dst_fs, _dst_path, namespace)
279+
path: info
280+
for path, info in walker.info(dst_fs, _dst_path, namespace)
273281
if info.is_file
274282
}
275283
src_state = [
276-
(filepath, info) for filepath, info in walker.info(src_fs, _src_path, namespace)
284+
(path, info)
285+
for path, info in walker.info(src_fs, _src_path, namespace)
277286
]
278287
for dir_path, copy_info in src_state:
279288
copy_path = combine(
@@ -282,34 +291,17 @@ def copy_dir_if_newer(src_fs, src_path, dst_fs, dst_path, walker=None, on_copy=N
282291
)
283292
if copy_info.is_dir:
284293
dst_fs.makedir(copy_path, recreate=True)
285-
if copy_info.is_file:
286-
do_copy = False
287-
if dir_path not in dst_state:
288-
#dst file is not present
289-
do_copy = True
290-
else:
291-
#dst file is present, try to figure out if copy is necessary
292-
src_modified = copy_info.modified
293-
if src_modified is None:
294-
#cannot retrieve src modified timestamp => file will be copied
295-
do_copy = True
296-
dst_modified = dst_state[dir_path].modified
297-
if dst_modified is None:
298-
#cannot retrieve dst modified timestamp => file will be copied
299-
do_copy = True
300-
if src_modified > dst_modified:
301-
#src file is newer than dst file => file will be copied
302-
do_copy = True
294+
elif copy_info.is_file:
295+
# dst file is present, try to figure out if copy
296+
# is necessary
297+
src_modified = copy_info.modified
298+
do_copy = (
299+
dir_path not in dst_state or
300+
src_modified is None or
301+
dst_state[dir_path].modified is None or
302+
src_modified > dst_state[dir_path].modified
303+
)
303304
if do_copy:
304-
file_copied = copy_file(
305-
src_fs,
306-
dir_path,
307-
dst_fs,
308-
copy_path
309-
)
310-
if file_copied:
311-
if on_copy is not None:
312-
on_copy(src_fs, dir_path, dst_fs, copy_path)
313-
314-
305+
copy_file(src_fs, dir_path, dst_fs, copy_path)
306+
on_copy(src_fs, dir_path, dst_fs, copy_path)
315307

tests/test_copy.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import unicode_literals
22

33
import errno
4+
import datetime
45
import os
5-
import time
66
import unittest
77
import tempfile
88
import shutil
9+
import datetime
910
from six import PY2
1011

1112
import fs.copy
@@ -42,6 +43,22 @@ def test_copy_dir(self):
4243
self.assertTrue(dst_fs.isdir('empty'))
4344
self.assertTrue(dst_fs.isfile('bar/baz.txt'))
4445

46+
def test_copy_dir_on_copy(self):
47+
src_fs = open_fs('mem://')
48+
src_fs.touch('baz.txt')
49+
50+
on_copy_calls = []
51+
def on_copy(*args):
52+
on_copy_calls.append(args)
53+
54+
dst_fs = open_fs('mem://')
55+
fs.copy.copy_dir(src_fs, '/', dst_fs, '/', on_copy=on_copy)
56+
print(on_copy_calls)
57+
self.assertEqual(
58+
on_copy_calls,
59+
[(src_fs, '/baz.txt', dst_fs, '/baz.txt')]
60+
)
61+
4562
def mkdirp(self, path):
4663
#os.makedirs(path, exist_ok=True) only for python3.?
4764
try:
@@ -84,6 +101,15 @@ def _delay_file_utime(self, filepath, delta_sec=None):
84101
times = (file_access_mod_time, file_access_mod_time)
85102
os.utime(filepath, times)
86103

104+
def test_copy_file_if_newer_same_fs(self):
105+
src_fs = open_fs('mem://')
106+
src_fs.makedir('foo2').touch('exists')
107+
src_fs.makedir('foo1').touch('test1.txt')
108+
src_fs.settimes('foo2/exists', datetime.datetime.utcnow() + datetime.timedelta(hours=1))
109+
self.assertTrue(fs.copy.copy_file_if_newer(src_fs, 'foo1/test1.txt', src_fs, 'foo2/test1.txt.copy'))
110+
self.assertFalse(fs.copy.copy_file_if_newer(src_fs, 'foo1/test1.txt', src_fs, 'foo2/exists'))
111+
self.assertTrue(src_fs.exists('foo2/test1.txt.copy'))
112+
87113
def test_copy_file_if_newer_dst_older(self):
88114
try:
89115
#create first dst ==> dst is older the src ==> file should be copied

0 commit comments

Comments
 (0)