diff --git a/nbconvert/filters/__init__.py b/nbconvert/filters/__init__.py index 0a0e7c48b..4285c8bcc 100755 --- a/nbconvert/filters/__init__.py +++ b/nbconvert/filters/__init__.py @@ -8,4 +8,4 @@ from .metadata import * from .pandoc import * -from ipython_genutils.text import indent +from nbconvert.utils.text import indent diff --git a/nbconvert/nbconvertapp.py b/nbconvert/nbconvertapp.py index 153a90466..dcb6bcaac 100755 --- a/nbconvert/nbconvertapp.py +++ b/nbconvert/nbconvertapp.py @@ -15,9 +15,9 @@ import glob import asyncio from textwrap import fill, dedent -from ipython_genutils.text import indent from jupyter_core.application import JupyterApp, base_aliases, base_flags +from nbconvert.utils.text import indent from traitlets.config import catch_config_error, Configurable from traitlets import ( Unicode, List, Instance, DottedObjectName, Type, Bool, diff --git a/nbconvert/utils/io.py b/nbconvert/utils/io.py index 42ddec478..ad2d1ace0 100644 --- a/nbconvert/utils/io.py +++ b/nbconvert/utils/io.py @@ -5,6 +5,10 @@ # Distributed under the terms of the Modified BSD License. import codecs +import errno +import os +import random +import shutil import sys def unicode_std_stream(stream='stdout'): @@ -48,3 +52,81 @@ def unicode_stdin_stream(): class FormatSafeDict(dict): def __missing__(self, key): return '{' + key + '}' + + +try: + ENOLINK = errno.ENOLINK +except AttributeError: + ENOLINK = 1998 + + +def link(src, dst): + """Hard links ``src`` to ``dst``, returning 0 or errno. + + Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't + supported by the operating system. + """ + + if not hasattr(os, "link"): + return ENOLINK + link_errno = 0 + try: + os.link(src, dst) + except OSError as e: + link_errno = e.errno + return link_errno + + +def link_or_copy(src, dst): + """Attempts to hardlink ``src`` to ``dst``, copying if the link fails. + + Attempts to maintain the semantics of ``shutil.copy``. + + Because ``os.link`` does not overwrite files, a unique temporary file + will be used if the target already exists, then that file will be moved + into place. + """ + + if os.path.isdir(dst): + dst = os.path.join(dst, os.path.basename(src)) + + link_errno = link(src, dst) + if link_errno == errno.EEXIST: + if os.stat(src).st_ino == os.stat(dst).st_ino: + # dst is already a hard link to the correct file, so we don't need + # to do anything else. If we try to link and rename the file + # anyway, we get duplicate files - see http://bugs.python.org/issue21876 + return + + new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), ) + try: + link_or_copy(src, new_dst) + except: + try: + os.remove(new_dst) + except OSError: + pass + raise + os.rename(new_dst, dst) + elif link_errno != 0: + # Either link isn't supported, or the filesystem doesn't support + # linking, or 'src' and 'dst' are on different filesystems. + shutil.copy(src, dst) + + +def ensure_dir_exists(path, mode=0o755): + """ensure that a directory exists + + If it doesn't exist, try to create it and protect against a race condition + if another process is doing the same. + + The default permissions are 755, which differ from os.makedirs default of 777. + """ + if not os.path.exists(path): + try: + os.makedirs(path, mode=mode) + except OSError as e: + if e.errno != errno.EEXIST: + raise + elif not os.path.isdir(path): + raise IOError("%r exists but is not a directory" % path) diff --git a/nbconvert/utils/text.py b/nbconvert/utils/text.py new file mode 100644 index 000000000..8ed3ea966 --- /dev/null +++ b/nbconvert/utils/text.py @@ -0,0 +1,41 @@ +import os +import re + + +def indent(instr, nspaces=4, ntabs=0, flatten=False): + """Indent a string a given number of spaces or tabstops. + + indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces. + + Parameters + ---------- + + instr : basestring + The string to be indented. + nspaces : int (default: 4) + The number of spaces to be indented. + ntabs : int (default: 0) + The number of tabs to be indented. + flatten : bool (default: False) + Whether to scrub existing indentation. If True, all lines will be + aligned to the same indentation. If False, existing indentation will + be strictly increased. + + Returns + ------- + + str|unicode : string indented by ntabs and nspaces. + + """ + if instr is None: + return + ind = '\t' * ntabs + ' ' * nspaces + if flatten: + pat = re.compile(r'^\s*', re.MULTILINE) + else: + pat = re.compile(r'^', re.MULTILINE) + outstr = re.sub(pat, ind, instr) + if outstr.endswith(os.linesep + ind): + return outstr[:-len(ind)] + else: + return outstr diff --git a/nbconvert/writers/files.py b/nbconvert/writers/files.py index 28f88eebd..bd12c73c7 100644 --- a/nbconvert/writers/files.py +++ b/nbconvert/writers/files.py @@ -8,8 +8,10 @@ import glob from pathlib import Path + +from nbconvert.utils.io import ensure_dir_exists, link_or_copy from traitlets import Unicode, observe -from ipython_genutils.path import link_or_copy, ensure_dir_exists + from .base import WriterBase