From 2d099ad9c657d2cab59de91cdb8bfed2da236ef6 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 27 May 2020 11:17:43 -0700 Subject: [PATCH] Backport of Python 3.8 shutil.copytree, refs #744 (#769) --- datasette/utils/__init__.py | 5 +- datasette/utils/shutil_backport.py | 101 +++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 datasette/utils/shutil_backport.py diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 04bf41af..cdb1bbc9 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -15,6 +15,7 @@ import shutil import urllib import numbers import yaml +from .shutil_backport import copytree try: import pysqlite3 as sqlite3 @@ -602,9 +603,9 @@ def link_or_copy(src, dst): def link_or_copy_directory(src, dst): try: - shutil.copytree(src, dst, copy_function=os.link, dirs_exist_ok=True) + copytree(src, dst, copy_function=os.link, dirs_exist_ok=True) except OSError: - shutil.copytree(src, dst, dirs_exist_ok=True) + copytree(src, dst, dirs_exist_ok=True) def module_from_path(path, name): diff --git a/datasette/utils/shutil_backport.py b/datasette/utils/shutil_backport.py new file mode 100644 index 00000000..dbe22404 --- /dev/null +++ b/datasette/utils/shutil_backport.py @@ -0,0 +1,101 @@ +""" +Backported from Python 3.8. + +This code is licensed under the Python License: +https://github.com/python/cpython/blob/v3.8.3/LICENSE +""" +import os +from shutil import copy, copy2, copystat, Error + + +def _copytree( + entries, + src, + dst, + symlinks, + ignore, + copy_function, + ignore_dangling_symlinks, + dirs_exist_ok=False, +): + if ignore is not None: + ignored_names = ignore(src, set(os.listdir(src))) + else: + ignored_names = set() + + os.makedirs(dst, exist_ok=dirs_exist_ok) + errors = [] + use_srcentry = copy_function is copy2 or copy_function is copy + + for srcentry in entries: + if srcentry.name in ignored_names: + continue + srcname = os.path.join(src, srcentry.name) + dstname = os.path.join(dst, srcentry.name) + srcobj = srcentry if use_srcentry else srcname + try: + if srcentry.is_symlink(): + linkto = os.readlink(srcname) + if symlinks: + os.symlink(linkto, dstname) + copystat(srcobj, dstname, follow_symlinks=not symlinks) + else: + if not os.path.exists(linkto) and ignore_dangling_symlinks: + continue + if srcentry.is_dir(): + copytree( + srcobj, + dstname, + symlinks, + ignore, + copy_function, + dirs_exist_ok=dirs_exist_ok, + ) + else: + copy_function(srcobj, dstname) + elif srcentry.is_dir(): + copytree( + srcobj, + dstname, + symlinks, + ignore, + copy_function, + dirs_exist_ok=dirs_exist_ok, + ) + else: + copy_function(srcentry, dstname) + except Error as err: + errors.extend(err.args[0]) + except OSError as why: + errors.append((srcname, dstname, str(why))) + try: + copystat(src, dst) + except OSError as why: + # Copying file access times may fail on Windows + if getattr(why, "winerror", None) is None: + errors.append((src, dst, str(why))) + if errors: + raise Error(errors) + return dst + + +def copytree( + src, + dst, + symlinks=False, + ignore=None, + copy_function=copy2, + ignore_dangling_symlinks=False, + dirs_exist_ok=False, +): + with os.scandir(src) as entries: + return _copytree( + entries=entries, + src=src, + dst=dst, + symlinks=symlinks, + ignore=ignore, + copy_function=copy_function, + ignore_dangling_symlinks=ignore_dangling_symlinks, + dirs_exist_ok=dirs_exist_ok, + )