Skip to content

Add PEP 519 support #6788

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 4 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 @@ -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
Expand Down Expand Up @@ -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,
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, 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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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

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 @@ -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
Expand Down Expand Up @@ -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):
Expand Down
53 changes: 49 additions & 4 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the standard library be able to handle this on it's own?

# Append to results all relevant files (and perhaps folders)
for name in files:
fullname = os.path.normpath(os.path.join(dirname, name))
Expand All @@ -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)

Expand Down Expand Up @@ -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, '.*'))):
Expand Down Expand Up @@ -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 + '-*')
Expand Down Expand Up @@ -2701,3 +2705,44 @@ def __exit__(self, exc_type, exc_value, traceback):
os.rmdir(path)
except OSError:
pass


try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider making cbook a python module with different submodule, and have all the backward compatibility fixes in a file where we cleary can keep track of it.
That would require more work (creating the cbook folder, __init__.py, a fixes.py module with those, updating the setup.py), but would allow us to split this huge file into more manageable size files.

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm not sure what can actually be moved from matplotlib.cbook to matplotlib.cbook.fixes, as the fspath stuff is the only compatibility things I can see, and there's already matplotlib.compat...

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__)
Copy link
Member

@NelleV NelleV Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is not pep8 compliant. Please break the line after the +.
I would also prefer using string formatting instead of concatenation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8 does not prescribe either way, but has a slight preference for the code as it's written.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So change or leave?


def fspath_no_except(path):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a private function.
If it is not a private function, it should be documented.

I also don't really like this name, but I am not coming up with anything better for now…

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I seem to recall something (Sphinx? NumPyDoc?) using a safe_ prefix; so something like safe_fspath (or even more explicitly safe_fspath_to_str)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'd be a much better name.

Copy link
Contributor Author

@aragilar aragilar Oct 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of safe_fspath, as it's not really safe, since the reason it exists to to pass through anything that's not a PEP 519 path, and validating anything else doesn't happen. Also, there's no guarantee that it'll return a string, as __fspath__ can return either bytes or a string (I've special-cased pathlib here as there exist versions of pathlib without PEP 519 support), so safe_fspath_to_str is even worse. How about fspath_passthrough?

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 @@ -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):
"""
Expand Down
20 changes: 20 additions & 0 deletions lib/matplotlib/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Member

@NelleV NelleV Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please rewrite this docstring with the numpydoc format? I find the docstring relatively unclear on what exactly the function does. If I understand correctly, it creates, closes and returns a path to a file and deletes it on exiting the context. It is also unclear where the file is created. A note on how it relates and differs from tempfile.NamedTemporaryFile would help clarify what the function does.

Also, this is publicly available - do we want this publicly available? (if not, then the docstring comment can be ignored.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does publicly available mean should be used outside of matplotlib, or is the recommended option for other parts of matplotlib? I don't see much use outside matplotlib, unless you were already using matplotlib's testing utils.

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:
Copy link
Member

@NelleV NelleV Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That parenthesis here is weird. I am surprised pep8 allows this. Is there anyway to break the line in another way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    with tempfile.NamedTemporaryFile('w+t', suffix=suffix,
                                     delete=False) as test_file:

fits within 79 chars, and looks reasonable, I think.

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)
86 changes: 72 additions & 14 deletions lib/matplotlib/tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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'):
Expand All @@ -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'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid referencing to peps in function name. It is unclear what it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about check_save_animation_PathLike?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_save_animation_path_like?

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am, again, surprised pep8 allows that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't for me; I don't know why this is passing either.

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'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test and the previous one looks like they could be refactoring to loop other Path and fspath compatible object to minimize the code duplication.

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
Expand Down
Loading