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.

copytree2.zip