diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index b6150ae3a694..bb542c05eff2 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -37,7 +37,7 @@ import contextlib import tempfile import warnings -from matplotlib.cbook import iterable, is_string_like +from matplotlib.cbook import iterable, is_string_like, fspath_no_except from matplotlib.compat import subprocess from matplotlib import verbose from matplotlib import rcParams, rcParamsDefault, rc_context @@ -281,6 +281,7 @@ def _run(self): output = sys.stdout else: output = subprocess.PIPE + command = [fspath_no_except(cmd) for cmd in command] verbose.report('MovieWriter.run: running command: %s' % ' '.join(command)) self._proc = subprocess.Popen(command, shell=False, diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 587e09e9534d..80b51fade9f1 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -30,7 +30,8 @@ from matplotlib import verbose, rcParams from matplotlib.backend_bases import (RendererBase, FigureManagerBase, FigureCanvasBase) -from matplotlib.cbook import is_string_like, maxdict, restrict_dict +from matplotlib.cbook import (is_string_like, maxdict, restrict_dict, + fspath_no_except, to_filehandle) from matplotlib.figure import Figure from matplotlib.font_manager import findfont, get_font from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, @@ -553,7 +554,10 @@ def print_png(self, filename_or_obj, *args, **kwargs): close = False try: - _png.write_png(renderer._renderer, filename_or_obj, self.figure.dpi) + _png.write_png( + renderer._renderer, fspath_no_except(filename_or_obj), + self.figure.dpi + ) finally: if close: filename_or_obj.close() @@ -606,7 +610,8 @@ def print_jpg(self, filename_or_obj, *args, **kwargs): if 'quality' not in options: options['quality'] = rcParams['savefig.jpeg_quality'] - return background.save(filename_or_obj, format='jpeg', **options) + return background.save(to_filehandle(filename_or_obj), format='jpeg', + **options) print_jpeg = print_jpg # add TIFF support @@ -616,7 +621,7 @@ def print_tif(self, filename_or_obj, *args, **kwargs): return image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1) dpi = (self.figure.dpi, self.figure.dpi) - return image.save(filename_or_obj, format='tiff', + return image.save(to_filehandle(filename_or_obj), format='tiff', dpi=dpi) print_tiff = print_tif diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 21719273c423..47591f91fc96 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -34,7 +34,7 @@ FigureManagerBase, FigureCanvasBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.cbook import (Bunch, is_string_like, get_realpath_and_stat, - is_writable_file_like, maxdict) + is_writable_file_like, maxdict, fspath_no_except) from matplotlib.figure import Figure from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font from matplotlib.afm import AFM @@ -429,6 +429,7 @@ def __init__(self, filename): self.passed_in_file_object = False self.original_file_like = None self.tell_base = 0 + filename = fspath_no_except(filename) if is_string_like(filename): fh = open(filename, 'wb') elif is_writable_file_like(filename): diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 0e8c0f50e150..455736b09c67 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -756,6 +756,7 @@ def to_filehandle(fname, flag='rU', return_opened=False): files is automatic, if the filename ends in .gz. *flag* is a read/write flag for :func:`file` """ + fname = fspath_no_except(fname) if is_string_like(fname): if fname.endswith('.gz'): # get rid of 'U' in flag for gzipped files. @@ -815,6 +816,7 @@ def get_sample_data(fname, asfileobj=True): root = matplotlib.rcParams['examples.directory'] else: root = os.path.join(matplotlib._get_data_path(), 'sample_data') + fname = fspath_no_except(fname) path = os.path.join(root, fname) if asfileobj: @@ -1019,6 +1021,7 @@ def __init__(self): self._cache = {} def __call__(self, path): + path = fspath_no_except(path) result = self._cache.get(path) if result is None: realpath = os.path.realpath(path) @@ -1173,7 +1176,7 @@ def listFiles(root, patterns='*', recurse=1, return_folders=0): pattern_list = patterns.split(';') results = [] - for dirname, dirs, files in os.walk(root): + for dirname, dirs, files in os.walk(fspath_no_except(root)): # Append to results all relevant files (and perhaps folders) for name in files: fullname = os.path.normpath(os.path.join(dirname, name)) @@ -1197,10 +1200,10 @@ def get_recursive_filelist(args): files = [] for arg in args: - if os.path.isfile(arg): + if os.path.isfile(fspath_no_except(arg)): files.append(arg) continue - if os.path.isdir(arg): + if os.path.isdir(fspath_no_except(arg)): newfiles = listFiles(arg, recurse=1, return_folders=1) files.extend(newfiles) @@ -1726,6 +1729,7 @@ def simple_linear_interpolation(a, steps): def recursive_remove(path): + path = fspath_no_except(path) if os.path.isdir(path): for fname in (glob.glob(os.path.join(path, '*')) + glob.glob(os.path.join(path, '.*'))): @@ -2666,7 +2670,7 @@ class TimeoutError(RuntimeError): pass def __init__(self, path): - self.path = path + self.path = fspath_no_except(path) self.end = "-" + str(os.getpid()) self.lock_path = os.path.join(self.path, self.LOCKFN + self.end) self.pattern = os.path.join(self.path, self.LOCKFN + '-*') @@ -2701,3 +2705,44 @@ def __exit__(self, exc_type, exc_value, traceback): os.rmdir(path) except OSError: pass + + +try: + from os import fspath +except ImportError: + def fspath(path): + """ + Return the string representation of the path. + If str or bytes is passed in, it is returned unchanged. + This code comes from PEP 519, modified to support earlier versions of + python. + + This is required for python < 3.6. + """ + if isinstance(path, (six.text_type, six.binary_type)): + return path + + # Work from the object's type to match method resolution of other magic + # methods. + path_type = type(path) + try: + return path_type.__fspath__(path) + except AttributeError: + if hasattr(path_type, '__fspath__'): + raise + try: + import pathlib + except ImportError: + pass + else: + if isinstance(path, pathlib.PurePath): + return six.text_type(path) + + raise TypeError("expected str, bytes or os.PathLike object, not " + + path_type.__name__) + +def fspath_no_except(path): + try: + return fspath(path) + except TypeError: + return path diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 9e1646f9bc94..c4f2e0f924f0 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -525,7 +525,7 @@ def contains(self, mouseevent): def write_png(self, fname): """Write the image to png file with fname""" im = self.to_rgba(self._A, bytes=True, norm=False) - _png.write_png(im, fname) + _png.write_png(im, cbook.fspath_no_except(fname)) def set_data(self, A): """ diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 0b549b36d088..228ff7ff2b35 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -4,6 +4,8 @@ import inspect import warnings from contextlib import contextmanager +import shutil +import tempfile import matplotlib from matplotlib.cbook import is_string_like, iterable @@ -164,3 +166,21 @@ def setup(): rcdefaults() # Start with all defaults set_font_settings_for_testing() + + +@contextmanager +def closed_tempfile(suffix='', text=None): + """ + Context manager which yields the path to a closed temporary file with the + suffix `suffix`. The file will be deleted on exiting the context. An + additional argument `text` can be provided to have the file contain `text`. + """ + with tempfile.NamedTemporaryFile( + 'w+t', suffix=suffix, delete=False + ) as test_file: + file_name = test_file.name + if text is not None: + test_file.write(text) + test_file.flush() + yield file_name + shutil.rmtree(file_name, ignore_errors=True) diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index c6373584ce2d..c75100ba8658 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -12,7 +12,7 @@ import matplotlib as mpl from matplotlib import pyplot as plt from matplotlib import animation -from ..testing import xfail, skip +from ..testing import xfail, skip, closed_tempfile from ..testing.decorators import cleanup @@ -107,6 +107,12 @@ def test_save_animation_smoketest(): for writer, extension in six.iteritems(WRITER_OUTPUT): yield check_save_animation, writer, extension + for writer, extension in six.iteritems(WRITER_OUTPUT): + yield check_save_animation_pep_519, writer, extension + + for writer, extension in six.iteritems(WRITER_OUTPUT): + yield check_save_animation_pathlib, writer, extension + @cleanup def check_save_animation(writer, extension='mp4'): @@ -128,20 +134,72 @@ def animate(i): line.set_data(x, y) return line, - # Use NamedTemporaryFile: will be automatically deleted - F = tempfile.NamedTemporaryFile(suffix='.' + extension) - F.close() - anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) + with closed_tempfile(suffix='.' + extension) as fname: + anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) + anim.save(fname, fps=30, writer=writer, bitrate=500) + + +@cleanup +def check_save_animation_pep_519(writer, extension='mp4'): + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + if not animation.writers.is_available(writer): + skip("writer '%s' not available on this system" + % writer) + fig, ax = plt.subplots() + line, = ax.plot([], []) + + ax.set_xlim(0, 10) + ax.set_ylim(-1, 1) + + def init(): + line.set_data([], []) + return line, + + def animate(i): + x = np.linspace(0, 10, 100) + y = np.sin(x + i) + line.set_data(x, y) + return line, + + with closed_tempfile(suffix='.' + extension) as fname: + anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) + anim.save(FakeFSPathClass(fname), fps=30, writer=writer, bitrate=500) + + +@cleanup +def check_save_animation_pathlib(writer, extension='mp4'): try: - anim.save(F.name, fps=30, writer=writer, bitrate=500) - except UnicodeDecodeError: - xfail("There can be errors in the numpy import stack, " - "see issues #1891 and #2679") - finally: - try: - os.remove(F.name) - except Exception: - pass + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + if not animation.writers.is_available(writer): + skip("writer '%s' not available on this system" % writer) + fig, ax = plt.subplots() + line, = ax.plot([], []) + + ax.set_xlim(0, 10) + ax.set_ylim(-1, 1) + + def init(): + line.set_data([], []) + return line, + + def animate(i): + x = np.linspace(0, 10, 100) + y = np.sin(x + i) + line.set_data(x, y) + return line, + + with closed_tempfile(suffix='.' + extension) as fname: + anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) + anim.save(Path(fname), fps=30, writer=writer, bitrate=500) @cleanup diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 2feee6fb1238..4274493edeff 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -14,6 +14,7 @@ from matplotlib import pyplot as plt from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) +from matplotlib.testing import skip, closed_tempfile if 'TRAVIS' not in os.environ: @image_comparison(baseline_images=['pdf_use14corefonts'], @@ -56,14 +57,12 @@ def test_multipage_pagecount(): @cleanup def test_multipage_keep_empty(): from matplotlib.backends.backend_pdf import PdfPages - from tempfile import NamedTemporaryFile # test empty pdf files # test that an empty pdf is left behind with keep_empty=True (default) - with NamedTemporaryFile(delete=False) as tmp: + with closed_tempfile(".pdf") as tmp: with PdfPages(tmp) as pdf: filename = pdf._file.fh.name assert os.path.exists(filename) - os.remove(filename) # test if an empty pdf is deleting itself afterwards with keep_empty=False with PdfPages(filename, keep_empty=False) as pdf: pass @@ -73,19 +72,17 @@ def test_multipage_keep_empty(): ax = fig.add_subplot(111) ax.plot([1, 2, 3]) # test that a non-empty pdf is left behind with keep_empty=True (default) - with NamedTemporaryFile(delete=False) as tmp: + with closed_tempfile(".pdf") as tmp: with PdfPages(tmp) as pdf: filename = pdf._file.fh.name pdf.savefig() assert os.path.exists(filename) - os.remove(filename) # test that a non-empty pdf is left behind with keep_empty=False - with NamedTemporaryFile(delete=False) as tmp: + with closed_tempfile(".pdf") as tmp: with PdfPages(tmp, keep_empty=False) as pdf: filename = pdf._file.fh.name pdf.savefig() assert os.path.exists(filename) - os.remove(filename) @cleanup @@ -132,3 +129,34 @@ def test_grayscale_alpha(): ax.imshow(dd, interpolation='none', cmap='gray_r') ax.set_xticks([]) ax.set_yticks([]) + + +@cleanup +def test_pdfpages_accept_pep_519(): + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + with closed_tempfile(suffix='.pdf') as fname: + with PdfPages(FakeFSPathClass(fname)) as pdf: + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + pdf.savefig(fig) + + +@cleanup +def test_savefig_accept_pathlib(): + try: + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with closed_tempfile(suffix='.pdf') as fname: + with PdfPages(Path(fname)) as pdf: + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + pdf.savefig(fig) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 7560f5ebf861..8830bea5818c 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -13,6 +13,8 @@ assert_array_almost_equal) from nose.tools import (assert_equal, assert_not_equal, raises, assert_true, assert_raises) +from matplotlib.testing.decorators import cleanup +from matplotlib.testing import skip, closed_tempfile import matplotlib.cbook as cbook import matplotlib.colors as mcolors @@ -523,3 +525,29 @@ def test_flatiter(): assert 0 == next(it) assert 1 == next(it) + + +@cleanup +def test_to_filehandle_accept_pep_519(): + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + with closed_tempfile() as tmpfile: + pep519_path = FakeFSPathClass(tmpfile) + cbook.to_filehandle(pep519_path) + + +@cleanup +def test_to_filehandle_accept_pathlib(): + try: + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + with closed_tempfile() as tmpfile: + path = Path(tmpfile) + cbook.to_filehandle(path) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index ba45f133961d..9d8d9a74e332 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -5,8 +5,10 @@ from six.moves import xrange from nose.tools import assert_equal, assert_true + from matplotlib import rcParams from matplotlib.testing.decorators import image_comparison, cleanup +from matplotlib.testing import skip, closed_tempfile from matplotlib.axes import Axes import matplotlib.pyplot as plt import numpy as np @@ -205,6 +207,98 @@ def test_figaspect(): assert h / w == 1 +@cleanup +def test_savefig_accept_pep_519_png(): + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + + with closed_tempfile(suffix='.png') as fname: + pep519_path = FakeFSPathClass(fname) + fig.savefig(pep519_path) + + +@cleanup +def test_savefig_accept_pathlib_png(): + try: + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with closed_tempfile(suffix='.png') as fname: + path = Path(fname) + fig.savefig(path) + + +@cleanup +def test_savefig_accept_pep_519_svg(): + + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with closed_tempfile(suffix='.svg') as fname: + pep519_path = FakeFSPathClass(fname) + fig.savefig(pep519_path) + + +@cleanup +def test_savefig_accept_pathlib_svg(): + try: + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with closed_tempfile(suffix='.svg') as fname: + path = Path(fname) + fig.savefig(path) + + +@cleanup +def test_savefig_accept_pep_519_pdf(): + + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with closed_tempfile(suffix='.pdf') as fname: + pep519_path = FakeFSPathClass(fname) + fig.savefig(pep519_path) + + +@cleanup +def test_savefig_accept_pathlib_pdf(): + try: + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with closed_tempfile(suffix='.pdf') as fname: + path = Path(fname) + fig.savefig(path) + if __name__ == "__main__": import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 51e529a4a341..de81fa1cf65d 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -12,6 +12,7 @@ from matplotlib.font_manager import ( findfont, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, fontManager as fm) +from matplotlib.testing import closed_tempfile import os.path @@ -36,15 +37,9 @@ def test_font_priority(): def test_json_serialization(): # on windows, we can't open a file twice, so save the name and unlink # manually... - try: - name = None - with tempfile.NamedTemporaryFile(delete=False) as temp: - name = temp.name - json_dump(fontManager, name) - copy = json_load(name) - finally: - if name and os.path.exists(name): - os.remove(name) + with closed_tempfile(".json") as temp: + json_dump(fontManager, temp) + copy = json_load(temp) with warnings.catch_warnings(): warnings.filterwarnings('ignore', 'findfont: Font family.*not found') for prop in ({'family': 'STIXGeneral'}, diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 851f9cffb775..670da66599d7 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -9,6 +9,7 @@ import numpy as np +from matplotlib.testing import skip, closed_tempfile from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) from matplotlib.image import (BboxImage, imread, NonUniformImage, @@ -752,5 +753,33 @@ def test_imshow_endianess(): ax2.imshow(Z.astype('>f8'), **kwargs) -if __name__ == '__main__': - nose.runmodule(argv=['-s', '--with-doctest'], exit=False) +@cleanup +def test_imsave_accept_pep_519(): + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + a = np.array([[1, 2], [3, 4]]) + with closed_tempfile(suffix='.pdf') as fname: + pep519_path = FakeFSPathClass(fname) + plt.imsave(pep519_path, a) + + +@cleanup +def test_imsave_accept_pathlib(): + try: + from pathlib import Path + except ImportError: + skip("pathlib not installed") + + a = np.array([[1, 2], [3, 4]]) + with closed_tempfile(suffix='.pdf') as fname: + path = Path(fname) + plt.imsave(path, a) + + +if __name__=='__main__': + nose.runmodule(argv=['-s','--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_testing.py b/lib/matplotlib/tests/test_testing.py new file mode 100644 index 000000000000..17d37fd6ba56 --- /dev/null +++ b/lib/matplotlib/tests/test_testing.py @@ -0,0 +1,21 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import os.path + +from matplotlib.testing import closed_tempfile + + +def test_closed_tempfile(): + with closed_tempfile(".txt") as fname: + assert os.path.exists(fname) + assert fname.endswith(".txt") + name = fname + assert os.path.exists(name) + + +def test_closed_tempfile_text(): + text = "This is a test" + with closed_tempfile(".txt", text=text) as f: + with open(f, "rt") as g: + assert text == g.read()