Skip to content

Add PEP 519 support #6772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 9 additions & 4 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
53 changes: 49 additions & 4 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -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)

Expand Down Expand Up @@ -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, '.*'))):
Expand Down Expand Up @@ -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 + '-*')
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/matplotlib/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
96 changes: 96 additions & 0 deletions lib/matplotlib/tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'):
Expand Down Expand Up @@ -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()
Expand Down
35 changes: 35 additions & 0 deletions lib/matplotlib/tests/test_backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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)
31 changes: 31 additions & 0 deletions lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Loading