Dans lequel shutil.copytree affiche sa progression
Copier récursivement un dossier contenant quelques milliers de fichiers de petite taille à l'aide de la fonction shutil.copytree de la librairie standard de Python est une opération dont l'exécution demande souvent un délai non négligeable. Évidemment, ce délai varie selon l'alignement des astres, l'humeur de Kim Jong-il et la tension la tension artérielle de Steves Jobs, mais il demeure toujours agaçant. Et puisque la fonction ne prévoit aucun mécanisme simple permettant d'informer la fonction appelante de l'avancement des travaux, l'attente n'en paraît que plus longue.
D'où l'idée d'une nouvelle implantation de la fonction shutil.copytree qui prend en argument un callback optionnel: progress.
Le callback de progression est appelé pour chaque pourcent de l'opération complété avec comme arguments le nombre de fichiers ayant été copiés et le nombre de fichiers total devant être copiés (ce qui permet, à l'aide de calculs mathématiques très poussés dont le lecteur imagine et redoute la complexité, d'obtenir un pourcentage d'avancement de l'opération en cours).
Il est donc possible d'afficher à l'utilisateur une belle barre de progression, un indicateur de pourcentage, la combinaison gagnante de la loterie du dimanche, etc.
import shutil
import os
__all__ = ['copytree']
def copytree(src, dst, symlinks=False, ignore=None, progress=None):
"""
A nonrecursive reimplementation of shutil.copytree with
a progress callback based on the number of copied files.
Works well over large folder trees containing lot of small
files.
Tested against the shutil test suite, specifically:
- test_copytree_simple
- test_copytree_with_exclude
- test_copytree_named_pipe
If the optional symlinks flag is true, symbolic links in the
source tree result in symbolic links in the destination tree; if
it is false, the contents of the files pointed to by symbolic
links are copied.
The optional ignore argument is a callable. If given, it
is called with the `src` parameter, which is the directory
being visited by copytree(), and `names` which is the list of
`src` contents, as returned by os.listdir():
callable(src, names) -> ignored_names
Even if this implementation of copytree is not recursive,
the callable will be called once for each directory that
is copied. It returns a list of names relative to the `src`
directory that should not be copied.
The optional progress argument is a callable. If given, it
will be called several times during the copytree operation
with the `over` parameter, which is the total number of files
to copy, and `done` which is the total number of files already
copied. You shouldn't take for granted that progress will be
called for every file copied, but it is assured that it will
be called when the copytree operation is completed.
callable(over, done) -> void
"""
names = [('','')] # start at the root of src
for name in names:
# name[0] -> path to name[1], relative to src
# name[1] -> file or folder currently handled
relname = os.path.join(name[0], name[1])
absname = os.path.join(src, relname)
if os.path.isdir(absname):
nnames = os.listdir(absname)
ignored_names = []
if ignore is not None:
ignored_list = ignore(absname, nnames)
for ignored in ignored_list:
ignored_names.append((relname, ignored))
for nname in nnames:
entry = (relname, nname)
if entry not in ignored_names:
names.append(entry)
todo = len(names) # total number of files to copy
done = 0 # total number of files copied
freq = round(todo * 0.01) # number of files to copy between call to progress
since = 0 # number of copied files since last call to progress
errors = []
# Before we start, do a fast checkup to make
# sure that dst doesn't already exists.
# Anyway, It's the shutil.copytree behavior.
os.makedirs(dst)
os.removedirs(dst)
for name in names:
if name in ignored_names:
continue
srcname = os.path.join(src, name[0], name[1]) # abs path to source file
dstname = os.path.join(dst, name[0], name[1]) # abs path to destination file
try:
if symlinks and os.path.islink(srcname):
linkto = os.readlink(srcname)
os.symlink(linkto, dstname)
elif os.path.isdir(srcname):
os.makedirs(dstname)
else:
# Will raise a SpecialFileError for unsupported file types
shutil.copy2(srcname, dstname)
if progress is not None:
done += 1
since += 1
if since >= freq:
since = 0
progress(todo, done)
# catch the Error so that we can continue with other files
except shutil.Error as err:
errors.extend(err.args[0])
except EnvironmentError as why:
errors.append((srcname, dstname, str(why)))
if progress is not None and since != 0:
progress(todo, done) # we're done
try:
shutil.copystat(src, dst)
except OSError as why:
if WindowsError is not None and isinstance(why, WindowsError):
# Copying file access times may fail on Windows
pass
else:
errors.extend((src, dst, str(why)))
if errors:
raise shutil.Error(errors)
La nouvelle implantation passe les tests unitaires de shutil.copytree sans erreur.
test_copytree_named_pipe (__main__.TestCopytree2) ... ok test_copytree_simple (__main__.TestCopytree2) ... ok test_copytree_with_exclude (__main__.TestCopytree2) ... ok ------------------------------------------------------------- Ran 3 tests in 0.111s OK
Évidemment, l'implantation est un peu tordue, dans la mesure où le nombre de fichiers devant être copiés doit être connu à l'avance pour pouvoir en informer le callback.
Aussi, étant donné que tous les chemins des fichiers à copier sont stockés dans une liste, les performances risques de se dégrader significativement pour des arborescences contenants plusieurs (dizaines) milliers de fichiers.
De plus, puisque l'interface de shutil.copytree est respectée, quelques contorsions sont nécessaires afin d'appeler le callback ignore adéquatement.
Finalement, si les fichiers ne sont pas tous d'une taille uniforme, la progression affichée risque d'être quelque peu aléatoire.