From 94e8e543c362acc689c171ad2cc103f18f005044 Mon Sep 17 00:00:00 2001 From: James Tocknell Date: Mon, 13 Jun 2016 22:53:16 +1000 Subject: [PATCH 1/2] Added tests for PEP 519/pathlib, these should fail --- lib/matplotlib/tests/test_animation.py | 96 +++++++++++++++++++++++ lib/matplotlib/tests/test_backend_pdf.py | 35 +++++++++ lib/matplotlib/tests/test_cbook.py | 31 ++++++++ lib/matplotlib/tests/test_figure.py | 99 ++++++++++++++++++++++++ lib/matplotlib/tests/test_image.py | 32 ++++++++ 5 files changed, 293 insertions(+) diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 4118ab7b797e..7aec0fb9e3e2 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -7,6 +7,8 @@ import tempfile import numpy as np from nose import with_setup +from nose.plugins.skip import SkipTest +import matplotlib as mpl from matplotlib import pyplot as plt from matplotlib import animation from matplotlib.testing.noseclasses import KnownFailureTest @@ -27,6 +29,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'): @@ -66,6 +74,94 @@ def animate(i): pass +@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): + raise KnownFailureTest("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, + + # Use NamedTemporaryFile: will be automatically deleted + F = tempfile.NamedTemporaryFile(suffix='.' + extension) + F.close() + anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) + try: + anim.save(FakeFSPathClass(F.name), fps=30, writer=writer, bitrate=500) + except UnicodeDecodeError: + raise KnownFailureTest("There can be errors in the numpy " + + "import stack, " + + "see issues #1891 and #2679") + finally: + try: + os.remove(F.name) + except Exception: + pass + + +@cleanup +def check_save_animation_pathlib(writer, extension='mp4'): + try: + from pathlib import Path + except ImportError: + raise SkipTest("pathlib not installed") + + if not animation.writers.is_available(writer): + raise KnownFailureTest("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, + + # Use NamedTemporaryFile: will be automatically deleted + F = tempfile.NamedTemporaryFile(suffix='.' + extension) + F.close() + anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5) + try: + anim.save(Path(F.name), fps=30, writer=writer, bitrate=500) + except UnicodeDecodeError: + raise KnownFailureTest("There can be errors in the numpy " + + "import stack, " + + "see issues #1891 and #2679") + finally: + try: + os.remove(F.name) + except Exception: + pass + + @cleanup def test_no_length_frames(): fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index a52a95464491..e1ea0c0c96d6 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 nose.plugins.skip import SkipTest if 'TRAVIS' not in os.environ: @image_comparison(baseline_images=['pdf_use14corefonts'], @@ -132,3 +133,37 @@ 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(): + from tempfile import NamedTemporaryFile + + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + with NamedTemporaryFile(suffix='.pdf') as tmpfile: + with PdfPages(FakeFSPathClass(tmpfile.name)) 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: + raise SkipTest("pathlib not installed") + from tempfile import NamedTemporaryFile + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with NamedTemporaryFile(suffix='.pdf') as tmpfile: + with PdfPages(Path(tmpfile.name)) 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 fe0377a72eba..3b17618239aa 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 nose.plugins.skip import SkipTest +from matplotlib.testing.decorators import cleanup import matplotlib.cbook as cbook import matplotlib.colors as mcolors @@ -499,3 +501,32 @@ class dummy(): base_set = mapping[ref(objs[0])] for o in objs[1:]: assert mapping[ref(o)] is base_set + + +@cleanup +def test_to_filehandle_accept_pep_519(): + from tempfile import NamedTemporaryFile + + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + with NamedTemporaryFile() as tmpfile: + pep519_path = FakeFSPathClass(tmpfile.name) + cbook.to_filehandle(pep519_path) + + +@cleanup +def test_to_filehandle_accept_pathlib(): + try: + from pathlib import Path + except ImportError: + raise SkipTest("pathlib not installed") + from tempfile import NamedTemporaryFile + + with NamedTemporaryFile() as tmpfile: + path = Path(tmpfile.name) + cbook.to_filehandle(path) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 631474c23287..5a4b4a011bd2 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -5,6 +5,7 @@ from matplotlib.externals.six.moves import xrange from nose.tools import assert_equal, assert_true +from nose.plugins.skip import SkipTest from matplotlib.testing.decorators import image_comparison, cleanup from matplotlib.axes import Axes import matplotlib.pyplot as plt @@ -203,6 +204,104 @@ def test_figaspect(): assert h / w == 1 +@cleanup +def test_savefig_accept_pep_519_png(): + from tempfile import NamedTemporaryFile + + 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 NamedTemporaryFile(suffix='.png') as tmpfile: + pep519_path = FakeFSPathClass(tmpfile.name) + fig.savefig(pep519_path) + + +@cleanup +def test_savefig_accept_pathlib_png(): + try: + from pathlib import Path + except ImportError: + raise SkipTest("pathlib not installed") + from tempfile import NamedTemporaryFile + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with NamedTemporaryFile(suffix='.png') as tmpfile: + path = Path(tmpfile.name) + fig.savefig(path) + + +@cleanup +def test_savefig_accept_pep_519_svg(): + from tempfile import NamedTemporaryFile + + 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 NamedTemporaryFile(suffix='.svg') as tmpfile: + pep519_path = FakeFSPathClass(tmpfile.name) + fig.savefig(pep519_path) + + +@cleanup +def test_savefig_accept_pathlib_svg(): + try: + from pathlib import Path + except ImportError: + raise SkipTest("pathlib not installed") + from tempfile import NamedTemporaryFile + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with NamedTemporaryFile(suffix='.svg') as tmpfile: + path = Path(tmpfile.name) + fig.savefig(path) + + +@cleanup +def test_savefig_accept_pep_519_pdf(): + from tempfile import NamedTemporaryFile + + 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 NamedTemporaryFile(suffix='.pdf') as tmpfile: + pep519_path = FakeFSPathClass(tmpfile.name) + fig.savefig(pep519_path) + + +@cleanup +def test_savefig_accept_pathlib_pdf(): + try: + from pathlib import Path + except ImportError: + raise SkipTest("pathlib not installed") + from tempfile import NamedTemporaryFile + + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4]) + with NamedTemporaryFile(suffix='.pdf') as tmpfile: + path = Path(tmpfile.name) + fig.savefig(path) + if __name__ == "__main__": import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 81d996354d5e..764082efba68 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -7,6 +7,7 @@ import os from nose.plugins.attrib import attr +from nose.plugins.skip import SkipTest import numpy as np @@ -674,6 +675,37 @@ def test_mask_image(): ax2.imshow(A, interpolation='nearest') +@cleanup +def test_imsave_accept_pep_519(): + from tempfile import NamedTemporaryFile + + class FakeFSPathClass(object): + def __init__(self, path): + self._path = path + + def __fspath__(self): + return self._path + + a = np.array([[1, 2], [3, 4]]) + with NamedTemporaryFile(suffix='.pdf') as tmpfile: + pep519_path = FakeFSPathClass(tmpfile.name) + plt.imsave(pep519_path, a) + + +@cleanup +def test_imsave_accept_pathlib(): + try: + from pathlib import Path + except ImportError: + raise SkipTest("pathlib not installed") + from tempfile import NamedTemporaryFile + + a = np.array([[1, 2], [3, 4]]) + with NamedTemporaryFile(suffix='.pdf') as tmpfile: + path = Path(tmpfile.name) + plt.imsave(path, a) + + if __name__=='__main__': import nose nose.runmodule(argv=['-s','--with-doctest'], exit=False) From d9c62be32d89384662d52887266a4d8c43ad528c Mon Sep 17 00:00:00 2001 From: James Tocknell Date: Sun, 17 Jul 2016 14:54:32 +1000 Subject: [PATCH 2/2] Add PEP 519 support --- lib/matplotlib/animation.py | 3 +- lib/matplotlib/backends/backend_agg.py | 13 +++++-- lib/matplotlib/backends/backend_pdf.py | 3 +- lib/matplotlib/cbook.py | 53 ++++++++++++++++++++++++-- lib/matplotlib/image.py | 2 +- 5 files changed, 63 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 7997e856d48e..aa2a904c93d0 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -36,7 +36,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 @@ -210,6 +210,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 cbc339060a0a..8c6bc5ec89c0 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) from matplotlib.figure import Figure from matplotlib.font_manager import findfont, get_font from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, @@ -524,7 +525,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() @@ -572,7 +576,8 @@ def print_jpg(self, filename_or_obj, *args, **kwargs): if 'quality' not in options: options['quality'] = rcParams['savefig.jpeg_quality'] - return image.save(filename_or_obj, format='jpeg', **options) + return image.save(to_filehandle(filename_or_obj), format='jpeg', + **options) print_jpeg = print_jpg # add TIFF support @@ -582,7 +587,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 6e4144075c4d..3fd7eb51e67b 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -33,7 +33,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 @@ -418,6 +418,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 7763424135f7..950724a14f17 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -765,6 +765,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. @@ -823,6 +824,7 @@ def get_sample_data(fname, asfileobj=True): else: root = os.path.join(os.path.dirname(__file__), "mpl-data", "sample_data") + fname = fspath_no_except(fname) path = os.path.join(root, fname) if asfileobj: @@ -1027,6 +1029,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) @@ -1181,7 +1184,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)) @@ -1205,10 +1208,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) @@ -1784,6 +1787,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, '.*'))): @@ -2703,7 +2707,7 @@ class Locked(object): LOCKFN = '.matplotlib_lock' 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 + '-*') @@ -2739,3 +2743,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 0d86e2b74081..f2dd7f5b48b4 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -501,7 +501,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): """