diff --git a/IPython/html/services/contents/fileio.py b/IPython/html/services/contents/fileio.py index 0f7ff5c..7fa09c2 100644 --- a/IPython/html/services/contents/fileio.py +++ b/IPython/html/services/contents/fileio.py @@ -11,6 +11,7 @@ import errno import io import os import shutil +import tempfile from tornado.web import HTTPError @@ -19,10 +20,91 @@ from IPython.html.utils import ( to_os_path, ) from IPython import nbformat -from IPython.utils.io import atomic_writing from IPython.utils.py3compat import str_to_unicode +def _copy_metadata(src, dst): + """Copy the set of metadata we want for atomic_writing. + + Permission bits and flags. We'd like to copy file ownership as well, but we + can't do that. + """ + shutil.copymode(src, dst) + st = os.stat(src) + if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): + os.chflags(dst, st.st_flags) + +@contextmanager +def atomic_writing(path, text=True, encoding='utf-8', **kwargs): + """Context manager to write to a file only if the entire write is successful. + + This works by creating a temporary file in the same directory, and renaming + it over the old file if the context is exited without an error. If other + file names are hard linked to the target file, this relationship will not be + preserved. + + On Windows, there is a small chink in the atomicity: the target file is + deleted before renaming the temporary file over it. This appears to be + unavoidable. + + Parameters + ---------- + path : str + The target file to write to. + + text : bool, optional + Whether to open the file in text mode (i.e. to write unicode). Default is + True. + + encoding : str, optional + The encoding to use for files opened in text mode. Default is UTF-8. + + **kwargs + Passed to :func:`io.open`. + """ + # realpath doesn't work on Windows: http://bugs.python.org/issue9949 + # Luckily, we only need to resolve the file itself being a symlink, not + # any of its directories, so this will suffice: + if os.path.islink(path): + path = os.path.join(os.path.dirname(path), os.readlink(path)) + + dirname, basename = os.path.split(path) + tmp_dir = tempfile.mkdtemp(prefix=basename, dir=dirname) + tmp_path = os.path.join(tmp_dir, basename) + if text: + fileobj = io.open(tmp_path, 'w', encoding=encoding, **kwargs) + else: + fileobj = io.open(tmp_path, 'wb', **kwargs) + + try: + yield fileobj + except: + fileobj.close() + shutil.rmtree(tmp_dir) + raise + + # Flush to disk + fileobj.flush() + os.fsync(fileobj.fileno()) + + # Written successfully, now rename it + fileobj.close() + + # Copy permission bits, access time, etc. + try: + _copy_metadata(path, tmp_path) + except OSError: + # e.g. the file didn't already exist. Ignore any failure to copy metadata + pass + + if os.name == 'nt' and os.path.exists(path): + # Rename over existing file doesn't work on Windows + os.remove(path) + + os.rename(tmp_path, path) + shutil.rmtree(tmp_dir) + + class FileManagerMixin(object): """ Mixin for ContentsAPI classes that interact with the filesystem. diff --git a/IPython/html/services/contents/tests/test_fileio.py b/IPython/html/services/contents/tests/test_fileio.py new file mode 100644 index 0000000..b517280 --- /dev/null +++ b/IPython/html/services/contents/tests/test_fileio.py @@ -0,0 +1,131 @@ +# encoding: utf-8 +"""Tests for file IO""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import io as stdlib_io +import os.path +import stat + +import nose.tools as nt + +from IPython.testing.decorators import skip_win32 +from ..fileio import atomic_writing + +from IPython.utils.tempdir import TemporaryDirectory + +umask = 0 + +def test_atomic_writing(): + class CustomExc(Exception): pass + + with TemporaryDirectory() as td: + f1 = os.path.join(td, 'penguin') + with stdlib_io.open(f1, 'w') as f: + f.write(u'Before') + + if os.name != 'nt': + os.chmod(f1, 0o701) + orig_mode = stat.S_IMODE(os.stat(f1).st_mode) + + f2 = os.path.join(td, 'flamingo') + try: + os.symlink(f1, f2) + have_symlink = True + except (AttributeError, NotImplementedError, OSError): + # AttributeError: Python doesn't support it + # NotImplementedError: The system doesn't support it + # OSError: The user lacks the privilege (Windows) + have_symlink = False + + with nt.assert_raises(CustomExc): + with atomic_writing(f1) as f: + f.write(u'Failing write') + raise CustomExc + + # Because of the exception, the file should not have been modified + with stdlib_io.open(f1, 'r') as f: + nt.assert_equal(f.read(), u'Before') + + with atomic_writing(f1) as f: + f.write(u'Overwritten') + + with stdlib_io.open(f1, 'r') as f: + nt.assert_equal(f.read(), u'Overwritten') + + if os.name != 'nt': + mode = stat.S_IMODE(os.stat(f1).st_mode) + nt.assert_equal(mode, orig_mode) + + if have_symlink: + # Check that writing over a file preserves a symlink + with atomic_writing(f2) as f: + f.write(u'written from symlink') + + with stdlib_io.open(f1, 'r') as f: + nt.assert_equal(f.read(), u'written from symlink') + +def _save_umask(): + global umask + umask = os.umask(0) + os.umask(umask) + +def _restore_umask(): + os.umask(umask) + +@skip_win32 +@nt.with_setup(_save_umask, _restore_umask) +def test_atomic_writing_umask(): + with TemporaryDirectory() as td: + os.umask(0o022) + f1 = os.path.join(td, '1') + with atomic_writing(f1) as f: + f.write(u'1') + mode = stat.S_IMODE(os.stat(f1).st_mode) + nt.assert_equal(mode, 0o644, '{:o} != 644'.format(mode)) + + os.umask(0o057) + f2 = os.path.join(td, '2') + with atomic_writing(f2) as f: + f.write(u'2') + mode = stat.S_IMODE(os.stat(f2).st_mode) + nt.assert_equal(mode, 0o620, '{:o} != 620'.format(mode)) + + +def test_atomic_writing_newlines(): + with TemporaryDirectory() as td: + path = os.path.join(td, 'testfile') + + lf = u'a\nb\nc\n' + plat = lf.replace(u'\n', os.linesep) + crlf = lf.replace(u'\n', u'\r\n') + + # test default + with stdlib_io.open(path, 'w') as f: + f.write(lf) + with stdlib_io.open(path, 'r', newline='') as f: + read = f.read() + nt.assert_equal(read, plat) + + # test newline=LF + with stdlib_io.open(path, 'w', newline='\n') as f: + f.write(lf) + with stdlib_io.open(path, 'r', newline='') as f: + read = f.read() + nt.assert_equal(read, lf) + + # test newline=CRLF + with atomic_writing(path, newline='\r\n') as f: + f.write(lf) + with stdlib_io.open(path, 'r', newline='') as f: + read = f.read() + nt.assert_equal(read, crlf) + + # test newline=no convert + text = u'crlf\r\ncr\rlf\n' + with atomic_writing(path, newline='') as f: + f.write(text) + with stdlib_io.open(path, 'r', newline='') as f: + read = f.read() + nt.assert_equal(read, text) diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 3d236eb..6bed42e 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -3,33 +3,24 @@ IO related utilities. """ -#----------------------------------------------------------------------------- -# Copyright (C) 2008-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function from __future__ import absolute_import -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- + import codecs from contextlib import contextmanager import io import os import shutil -import stat import sys import tempfile +import warnings from .capture import CapturedIO, capture_output from .py3compat import string_types, input, PY3 -#----------------------------------------------------------------------------- -# Code -#----------------------------------------------------------------------------- - class IOStream: @@ -221,87 +212,11 @@ def temp_pyfile(src, ext='.py'): f.flush() return fname, f -def _copy_metadata(src, dst): - """Copy the set of metadata we want for atomic_writing. - - Permission bits and flags. We'd like to copy file ownership as well, but we - can't do that. - """ - shutil.copymode(src, dst) - st = os.stat(src) - if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): - os.chflags(dst, st.st_flags) - -@contextmanager -def atomic_writing(path, text=True, encoding='utf-8', **kwargs): - """Context manager to write to a file only if the entire write is successful. - - This works by creating a temporary file in the same directory, and renaming - it over the old file if the context is exited without an error. If other - file names are hard linked to the target file, this relationship will not be - preserved. - - On Windows, there is a small chink in the atomicity: the target file is - deleted before renaming the temporary file over it. This appears to be - unavoidable. - - Parameters - ---------- - path : str - The target file to write to. - - text : bool, optional - Whether to open the file in text mode (i.e. to write unicode). Default is - True. - - encoding : str, optional - The encoding to use for files opened in text mode. Default is UTF-8. - - **kwargs - Passed to :func:`io.open`. - """ - # realpath doesn't work on Windows: http://bugs.python.org/issue9949 - # Luckily, we only need to resolve the file itself being a symlink, not - # any of its directories, so this will suffice: - if os.path.islink(path): - path = os.path.join(os.path.dirname(path), os.readlink(path)) - - dirname, basename = os.path.split(path) - tmp_dir = tempfile.mkdtemp(prefix=basename, dir=dirname) - tmp_path = os.path.join(tmp_dir, basename) - if text: - fileobj = io.open(tmp_path, 'w', encoding=encoding, **kwargs) - else: - fileobj = io.open(tmp_path, 'wb', **kwargs) - - try: - yield fileobj - except: - fileobj.close() - shutil.rmtree(tmp_dir) - raise - - # Flush to disk - fileobj.flush() - os.fsync(fileobj.fileno()) - - # Written successfully, now rename it - fileobj.close() - - # Copy permission bits, access time, etc. - try: - _copy_metadata(path, tmp_path) - except OSError: - # e.g. the file didn't already exist. Ignore any failure to copy metadata - pass - - if os.name == 'nt' and os.path.exists(path): - # Rename over existing file doesn't work on Windows - os.remove(path) - - os.rename(tmp_path, path) - shutil.rmtree(tmp_dir) - +def atomic_writing(*args, **kwargs): + """DEPRECATED: moved to IPython.html.services.contents.fileio""" + warn("IPython.utils.io.atomic_writing has moved to IPython.html.services.contents.fileio") + from IPython.html.services.contents.fileio import atomic_writing + return atomic_writing(*args, **kwargs) def raw_print(*args, **kw): """Raw print to sys.__stdout__, otherwise identical interface to print().""" @@ -323,25 +238,9 @@ def raw_print_err(*args, **kw): rprint = raw_print rprinte = raw_print_err -def unicode_std_stream(stream='stdout'): - u"""Get a wrapper to write unicode to stdout/stderr as UTF-8. - - This ignores environment variables and default encodings, to reliably write - unicode to stdout or stderr. - :: - - unicode_std_stream().write(u'ł@e¶ŧ←') - """ - assert stream in ('stdout', 'stderr') - stream = getattr(sys, stream) - if PY3: - try: - stream_b = stream.buffer - except AttributeError: - # sys.stdout has been replaced - use it directly - return stream - else: - stream_b = stream - - return codecs.getwriter('utf-8')(stream_b) +def unicode_std_stream(stream='stdout'): + """DEPRECATED, moved to jupyter_nbconvert.utils.io""" + warn("IPython.utils.io.unicode_std_stream has moved to jupyter_nbconvert.utils.io") + from jupyter_nbconvert.utils.io import unicode_std_stream + return unicode_std_stream(stream) diff --git a/IPython/utils/tests/test_io.py b/IPython/utils/tests/test_io.py index aa00a88..04c4e9e 100644 --- a/IPython/utils/tests/test_io.py +++ b/IPython/utils/tests/test_io.py @@ -18,9 +18,7 @@ import unittest import nose.tools as nt from IPython.testing.decorators import skipif, skip_win32 -from IPython.utils.io import (Tee, capture_output, unicode_std_stream, - atomic_writing, - ) +from IPython.utils.io import Tee, capture_output from IPython.utils.py3compat import doctest_refactor_print, PY3 from IPython.utils.tempdir import TemporaryDirectory @@ -86,146 +84,4 @@ def test_capture_output(): nt.assert_equal(io.stdout, 'hi, stdout\n') nt.assert_equal(io.stderr, 'hi, stderr\n') -def test_UnicodeStdStream(): - # Test wrapping a bytes-level stdout - if PY3: - stdoutb = stdlib_io.BytesIO() - stdout = stdlib_io.TextIOWrapper(stdoutb, encoding='ascii') - else: - stdout = stdoutb = stdlib_io.BytesIO() - - orig_stdout = sys.stdout - sys.stdout = stdout - try: - sample = u"@łe¶ŧ←" - unicode_std_stream().write(sample) - - output = stdoutb.getvalue().decode('utf-8') - nt.assert_equal(output, sample) - assert not stdout.closed - finally: - sys.stdout = orig_stdout - -@skipif(not PY3, "Not applicable on Python 2") -def test_UnicodeStdStream_nowrap(): - # If we replace stdout with a StringIO, it shouldn't get wrapped. - orig_stdout = sys.stdout - sys.stdout = StringIO() - try: - nt.assert_is(unicode_std_stream(), sys.stdout) - assert not sys.stdout.closed - finally: - sys.stdout = orig_stdout - -def test_atomic_writing(): - class CustomExc(Exception): pass - - with TemporaryDirectory() as td: - f1 = os.path.join(td, 'penguin') - with stdlib_io.open(f1, 'w') as f: - f.write(u'Before') - - if os.name != 'nt': - os.chmod(f1, 0o701) - orig_mode = stat.S_IMODE(os.stat(f1).st_mode) - - f2 = os.path.join(td, 'flamingo') - try: - os.symlink(f1, f2) - have_symlink = True - except (AttributeError, NotImplementedError, OSError): - # AttributeError: Python doesn't support it - # NotImplementedError: The system doesn't support it - # OSError: The user lacks the privilege (Windows) - have_symlink = False - - with nt.assert_raises(CustomExc): - with atomic_writing(f1) as f: - f.write(u'Failing write') - raise CustomExc - - # Because of the exception, the file should not have been modified - with stdlib_io.open(f1, 'r') as f: - nt.assert_equal(f.read(), u'Before') - - with atomic_writing(f1) as f: - f.write(u'Overwritten') - - with stdlib_io.open(f1, 'r') as f: - nt.assert_equal(f.read(), u'Overwritten') - - if os.name != 'nt': - mode = stat.S_IMODE(os.stat(f1).st_mode) - nt.assert_equal(mode, orig_mode) - - if have_symlink: - # Check that writing over a file preserves a symlink - with atomic_writing(f2) as f: - f.write(u'written from symlink') - - with stdlib_io.open(f1, 'r') as f: - nt.assert_equal(f.read(), u'written from symlink') - -def _save_umask(): - global umask - umask = os.umask(0) - os.umask(umask) - -def _restore_umask(): - os.umask(umask) - -@skip_win32 -@nt.with_setup(_save_umask, _restore_umask) -def test_atomic_writing_umask(): - with TemporaryDirectory() as td: - os.umask(0o022) - f1 = os.path.join(td, '1') - with atomic_writing(f1) as f: - f.write(u'1') - mode = stat.S_IMODE(os.stat(f1).st_mode) - nt.assert_equal(mode, 0o644, '{:o} != 644'.format(mode)) - - os.umask(0o057) - f2 = os.path.join(td, '2') - with atomic_writing(f2) as f: - f.write(u'2') - mode = stat.S_IMODE(os.stat(f2).st_mode) - nt.assert_equal(mode, 0o620, '{:o} != 620'.format(mode)) - - -def test_atomic_writing_newlines(): - with TemporaryDirectory() as td: - path = os.path.join(td, 'testfile') - - lf = u'a\nb\nc\n' - plat = lf.replace(u'\n', os.linesep) - crlf = lf.replace(u'\n', u'\r\n') - - # test default - with stdlib_io.open(path, 'w') as f: - f.write(lf) - with stdlib_io.open(path, 'r', newline='') as f: - read = f.read() - nt.assert_equal(read, plat) - - # test newline=LF - with stdlib_io.open(path, 'w', newline='\n') as f: - f.write(lf) - with stdlib_io.open(path, 'r', newline='') as f: - read = f.read() - nt.assert_equal(read, lf) - - # test newline=CRLF - with atomic_writing(path, newline='\r\n') as f: - f.write(lf) - with stdlib_io.open(path, 'r', newline='') as f: - read = f.read() - nt.assert_equal(read, crlf) - - # test newline=no convert - text = u'crlf\r\ncr\rlf\n' - with atomic_writing(path, newline='') as f: - f.write(text) - with stdlib_io.open(path, 'r', newline='') as f: - read = f.read() - nt.assert_equal(read, text) + diff --git a/jupyter_client/session.py b/jupyter_client/session.py index 5f0307f..4ff1f58 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -49,7 +49,6 @@ from zmq.eventloop.zmqstream import ZMQStream from IPython.core.release import kernel_protocol_version from IPython.config.configurable import Configurable, LoggingConfigurable -from IPython.utils import io from IPython.utils.importstring import import_item from jupyter_client.jsonutil import extract_dates, squash_dates, date_default from IPython.utils.py3compat import (str_to_bytes, str_to_unicode, unicode_type, @@ -59,6 +58,8 @@ from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set, TraitError, ) from jupyter_client.adapter import adapt +from traitlets.log import get_logger + #----------------------------------------------------------------------------- # utility functions @@ -653,8 +654,9 @@ class Session(Configurable): msg = self.msg(msg_or_type, content=content, parent=parent, header=header, metadata=metadata) if not os.getpid() == self.pid: - io.rprint("WARNING: attempted to send message from fork") - io.rprint(msg) + get_logger().warn("WARNING: attempted to send message from fork\n%s", + msg + ) return buffers = [] if buffers is None else buffers if self.adapt_version: diff --git a/jupyter_nbconvert/utils/io.py b/jupyter_nbconvert/utils/io.py new file mode 100644 index 0000000..c03ec7b --- /dev/null +++ b/jupyter_nbconvert/utils/io.py @@ -0,0 +1,33 @@ +# coding: utf-8 +"""io-related utilities""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import codecs +import sys +from IPython.utils.py3compat import PY3 + + +def unicode_std_stream(stream='stdout'): + u"""Get a wrapper to write unicode to stdout/stderr as UTF-8. + + This ignores environment variables and default encodings, to reliably write + unicode to stdout or stderr. + + :: + + unicode_std_stream().write(u'ł@e¶ŧ←') + """ + assert stream in ('stdout', 'stderr') + stream = getattr(sys, stream) + if PY3: + try: + stream_b = stream.buffer + except AttributeError: + # sys.stdout has been replaced - use it directly + return stream + else: + stream_b = stream + + return codecs.getwriter('utf-8')(stream_b) diff --git a/jupyter_nbconvert/utils/pandoc.py b/jupyter_nbconvert/utils/pandoc.py index fa320de..5832ff0 100644 --- a/jupyter_nbconvert/utils/pandoc.py +++ b/jupyter_nbconvert/utils/pandoc.py @@ -1,33 +1,20 @@ """Utility for calling pandoc""" -#----------------------------------------------------------------------------- -# Copyright (c) 2014 the IPython Development Team. -# +# Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -# -# The full license is in the file COPYING.txt, distributed with this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- -from __future__ import print_function +from __future__ import print_function, absolute_import -# Stdlib imports import subprocess import warnings import re from io import TextIOWrapper, BytesIO -# IPython imports from IPython.utils.py3compat import cast_bytes from IPython.utils.version import check_version from IPython.utils.process import is_cmd_found, FindCmdError from .exceptions import ConversionException -#----------------------------------------------------------------------------- -# Classes and functions -#----------------------------------------------------------------------------- _minimal_version = "1.12.1" def pandoc(source, fmt, to, extra_args=None, encoding='utf-8'): diff --git a/jupyter_nbconvert/utils/tests/test_io.py b/jupyter_nbconvert/utils/tests/test_io.py new file mode 100644 index 0000000..6c4af00 --- /dev/null +++ b/jupyter_nbconvert/utils/tests/test_io.py @@ -0,0 +1,50 @@ +# encoding: utf-8 +"""Tests for utils.io""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import io as stdlib_io +import sys + +import nose.tools as nt + +from IPython.testing.decorators import skipif +from ..io import unicode_std_stream +from IPython.utils.py3compat import PY3 + +if PY3: + from io import StringIO +else: + from StringIO import StringIO + +def test_UnicodeStdStream(): + # Test wrapping a bytes-level stdout + if PY3: + stdoutb = stdlib_io.BytesIO() + stdout = stdlib_io.TextIOWrapper(stdoutb, encoding='ascii') + else: + stdout = stdoutb = stdlib_io.BytesIO() + + orig_stdout = sys.stdout + sys.stdout = stdout + try: + sample = u"@łe¶ŧ←" + unicode_std_stream().write(sample) + + output = stdoutb.getvalue().decode('utf-8') + nt.assert_equal(output, sample) + assert not stdout.closed + finally: + sys.stdout = orig_stdout + +@skipif(not PY3, "Not applicable on Python 2") +def test_UnicodeStdStream_nowrap(): + # If we replace stdout with a StringIO, it shouldn't get wrapped. + orig_stdout = sys.stdout + sys.stdout = StringIO() + try: + nt.assert_is(unicode_std_stream(), sys.stdout) + assert not sys.stdout.closed + finally: + sys.stdout = orig_stdout diff --git a/jupyter_nbconvert/writers/stdout.py b/jupyter_nbconvert/writers/stdout.py index b816425..6970219 100644 --- a/jupyter_nbconvert/writers/stdout.py +++ b/jupyter_nbconvert/writers/stdout.py @@ -1,24 +1,13 @@ """ Contains Stdout writer """ -#----------------------------------------------------------------------------- -#Copyright (c) 2013, the IPython Development Team. -# -#Distributed under the terms of the Modified BSD License. -# -#The full license is in the file COPYING.txt, distributed with this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. -from IPython.utils import io +from jupyter_nbconvert.utils import io from .base import WriterBase -#----------------------------------------------------------------------------- -# Classes -#----------------------------------------------------------------------------- class StdoutWriter(WriterBase): """Consumes output from nbconvert export...() methods and writes to the