8000 Merge pull request #2512 from mdboom/ps_StringIO · matplotlib/matplotlib@a3d2992 · GitHub
[go: up one dir, main page]

Skip to content

Commit a3d2992

Browse files
committed
Merge pull request #2512 from mdboom/ps_StringIO
Fix saving to in-memory file-like objects in Postscript backend
2 parents 65ce30e + 63f13c8 commit a3d2992

File tree

6 files changed

+155
-37
lines changed

6 files changed

+155
-37
lines changed

lib/matplotlib/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,16 @@
115115
def compare_versions(a, b):
116116
"return True if a is greater than or equal to b"
117117
if a:
118+
if six.PY3:
119+
if isinstance(a, bytes):
120+
a = a.decode('ascii')
121+
if isinstance(b, bytes):
122+
b = b.decode('ascii')
118123
a = distutils.version.LooseVersion(a)
119124
b = distutils.version.LooseVersion(b)
120-
if a>=b: return True
121-
else: return False
122-
else: return False
125+
return a >= b
126+
else:
127+
return False
123128

124129
try:
125130
import pyparsing
@@ -1222,6 +1227,7 @@ def tk_window_focus():
12221227
'matplotlib.tests.test_axes',
12231228
'matplotlib.tests.test_backend_pdf',
12241229
'matplotlib.tests.test_backend_pgf',
1230+
'matplotlib.tests.test_backend_ps',
12251231
'matplotlib.tests.test_backend_qt4',
12261232
'matplotlib.tests.test_backend_svg',
12271233
'matplotlib.tests.test_basic',

lib/matplotlib/backends/backend_ps.py

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
unicode_literals)
77

88
import six
9+
from six.moves import StringIO
910

1011
import glob, math, os, shutil, sys, time
1112
def _fn_name(): return sys._getframe(1).f_code.co_name
@@ -24,7 +25,7 @@ def _fn_name(): return sys._getframe(1).f_code.co_name
2425
FigureManagerBase, FigureCanvasBase
2526

2627
from matplotlib.cbook import is_string_like, get_realpath_and_stat, \
27-
is_writable_file_like, maxdict
28+
is_writable_file_like, maxdict, file_requires_unicode
2829
from matplotlib.mlab import quad2cubic
2930
from matplotlib.figure import Figure
3031

@@ -1085,7 +1086,7 @@ def write(self, *kl, **kwargs):
10851086

10861087
self._pswriter = NullWriter()
10871088
else:
1088-
self._pswriter = six.moves.cStringIO()
1089+
self._pswriter = io.StringIO()
10891090

10901091

10911092
# mixed mode rendering
@@ -1105,8 +1106,6 @@ def write(self, *kl, **kwargs):
11051106
self.figure.set_edgecolor(origedgecolor)
11061107

11071108
def print_figure_impl():
1108-
fh = io.TextIOWrapper(raw_fh, encoding="ascii")
1109-
11101109
# write the PostScript headers
11111110
if isEPSF: print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
11121111
else: print("%!PS-Adobe-3.0", file=fh)
@@ -1157,7 +1156,7 @@ def print_figure_impl():
11571156
fh.flush()
11581157
convert_ttf_to_ps(
11591158
font_filename.encode(sys.getfilesystemencoding()),
1160-
raw_fh, fonttype, glyph_ids)
1159+
fh, fonttype, glyph_ids)
11611160
print("end", file=fh)
11621161
print("%%EndProlog", file=fh)
11631162

@@ -1181,22 +1180,31 @@ def print_figure_impl():
11811180
if not isEPSF: print("%%EOF", file=fh)
11821181
fh.flush()
11831182

1184-
if six.PY3:
1185-
fh.detach()
1186-
11871183
if rcParams['ps.usedistiller']:
11881184
# We are going to use an external program to process the output.
11891185
# Write to a temporary file.
11901186
fd, tmpfile = mkstemp()
1191-
with io.open(fd, 'wb') as raw_fh:
1187+
with io.open(fd, 'w', encoding='ascii') as fh:
11921188
print_figure_impl()
11931189
else:
11941190
# Write directly to outfile.
11951191
if passed_in_file_object:
1196-
raw_fh = outfile
1192+
requires_unicode = file_requires_unicode(outfile)
1193+
1194+
if (not requires_unicode and
1195+
(six.PY3 or not isinstance(outfile, StringIO))):
1196+
fh = io.TextIOWrapper(outfile, encoding="ascii")
1197+
# Prevent the io.TextIOWrapper from closing the
1198+
# underlying file
1199+
def do_nothing():
1200+
pass
1201+
fh.close = do_nothing
1202+
else:
1203+
fh = outfile
1204+
11971205
print_figure_impl()
11981206
else:
1199-
with io.open(outfile, 'wb') as raw_fh:
1207+
with io.open(outfile, 'w', encoding='ascii') as fh:
12001208
print_figure_impl()
12011209

12021210
if rcParams['ps.usedistiller']:
@@ -1206,8 +1214,12 @@ def print_figure_impl():
12061214
xpdf_distill(tmpfile, isEPSF, ptype=papertype, bbox=bbox)
12071215

12081216
if passed_in_file_object:
1209-
with io.open(tmpfile, 'rb') as fh:
1210-
outfile.write(fh.read())
1217+
if file_requires_unicode(outfile):
1218+
with io.open(tmpfile, 'rb') as fh:
1219+
outfile.write(fh.read().decode('ascii'))
1220+
else:
1221+
with io.open(tmpfile, 'rb') as fh:
1222+
outfile.write(fh.read())
12111223
else:
12121224
with io.open(outfile, 'w') as fh:
12131225
pass
@@ -1224,7 +1236,12 @@ def _print_figure_tex(self, outfile, format, dpi, facecolor, edgecolor,
12241236
package. These files are processed to yield the final ps or eps file.
12251237
"""
12261238
isEPSF = format == 'eps'
1227-
title = outfile
1239+
if is_string_like(outfile):
1240+
title = outfile
1241+
elif is_writable_file_like(outfile):
1242+
title = None
1243+
else:
1244+
raise ValueError("outfile must be a path or a file-like object")
12281245

12291246
self.figure.dpi = 72 # ignore the dpi kwarg
12301247
width, height = self.figure.get_size_inches()
@@ -1252,7 +1269,7 @@ def write(self, *kl, **kwargs):
12521269

12531270
self._pswriter = NullWriter()
12541271
else:
1255-
self._pswriter = six.moves.cStringIO()
1272+
self._pswriter = io.StringIO()
12561273

12571274

12581275
# mixed mode rendering
@@ -1273,11 +1290,7 @@ def write(self, *kl, **kwargs):
12731290

12741291
# write to a temp file, we'll move it to outfile when done
12751292
fd, tmpfile = mkstemp()
1276-
if six.PY3:
1277-
fh = io.open(fd, 'w', encoding='ascii')
1278-
else:
1279-
fh = io.open(fd, 'wb')
1280-
with fh:
1293+
with io.open(fd, 'w', encoding='ascii') as fh:
12811294
# write the Encapsulated PostScript headers
12821295
print("%!PS-Adobe-3.0 EPSF-3.0", file=fh)
12831296
if title: print("%%Title: "+title, file=fh)
@@ -1357,17 +1370,13 @@ def write(self, *kl, **kwargs):
13571370
else: gs_distill(tmpfile, isEPSF, ptype=papertype, bbox=bbox,
13581371
rotated=psfrag_rotated)
13591372

1360-
is_file = False
1361-
if six.PY3:
1362-
if isinstance(outfile, io.IOBase):
1363-
is_file = True
1364-
else:
1365-
if isinstance(outfile, file):
1366-
is_file = True
1367-
1368-
if is_file:
1369-
with io.open(tmpfile, 'rb') as fh:
1370-
outfile.write(fh.read())
1373+
if is_writable_file_like(outfile):
1374+
if file_requires_unicode(outfile):
1375+
with io.open(tmpfile, 'rb') as fh:
1376+
outfile.write(fh.read().decode('ascii'))
1377+
else:
1378+
with io.open(tmpfile, 'rb') as fh:
1379+
outfile.write(fh.read())
13711380
else:
13721381
with io.open(outfile, 'wb') as fh:
13731382
pass

lib/matplotlib/cbook.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,19 @@ def is_writable_file_like(obj):
693693
return hasattr(obj, 'write') and six.callable(obj.write)
694694

695695

696+
def file_requires_unicode(x):
697+
"""
698+
Returns `True` if the given writable file-like object requires Unicode
699+
to be written to it.
700+
"""
701+
try:
702+
x.write(b'')
703+
except TypeError:
704+
return True
705+
else:
706+
return False
707+
708+
696709
def is_scalar(obj):
697710
'return true if *obj* is not string like and is not iterable'
698711
return not is_string_like(obj) and not iterable(obj)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from __future__ import (absolute_import, division, print_function,
4+
unicode_literals)
5+
6+
import io
7+
import re
8+
9+
import six
10+
from nose.plugins.skip import SkipTest
11+
12+
import matplotlib
13+
import matplotlib.pyplot as plt
14+
from matplotlib.testing.decorators import cleanup
15+
16+
17+
def _test_savefig_to_stringio(format='ps'):
18+
buffers = [
19+
six.moves.StringIO(),
20+
io.StringIO(),
21+
io.BytesIO()]
22+
23+
plt.figure()
24+
plt.plot([0, 1], [0, 1])
25+
plt.title("Déjà vu")
26+
for buffer in buffers:
27+
plt.savefig(buffer, format=format)
28+
29+
values = [x.getvalue() for x in buffers]
30+
31+
if six.PY3:
32+
values = [
33+
values[0].encode('ascii'),
34+
values[1].encode('ascii'),
35+
values[2]]
36+
37+
# Remove comments from the output. This includes things that
38+
# could change from run to run, such as the time.
39+
values = [re.sub(b'%%.*?\n', b'', x) for x in values]
40+
41+
assert values[0] == values[1]
42+
assert values[1] == values[2]
43+
44+
45+
@cleanup
46+
def test_savefig_to_stringio():
47+
_test_savefig_to_stringio()
48+
49+
10000
50+
@cleanup
51+
def test_savefig_to_stringio_with_distiller():
52+
matplotlib.rcParams['ps.usedistiller'] = 'ghostscript'
53+
_test_savefig_to_stringio()
54+
55+
56+
@cleanup
57+
def test_savefig_to_stringio_with_usetex():
58+
if not matplotlib.checkdep_tex():
59+
raise SkipTest("This test requires a TeX installation")
60+
61+
matplotlib.rcParams['text.latex.unicode'] = True
62+
matplotlib.rcParams['text.usetex'] = True
63+
_test_savefig_to_stringio()
64+
65+
66+
@cleanup
67+
def test_savefig_to_stringio_eps():
68+
_test_savefig_to_stringio(format='eps')
69+
70+
71+
@cleanup
72+
def test_savefig_to_stringio_with_usetex_eps():
73+
if not matplotlib.checkdep_tex():
74+
raise SkipTest("This test requires a TeX installation")
75+
76+
matplotlib.rcParams['text.latex.unicode'] = True
77+
matplotlib.rcParams['text.usetex'] = True
78+
_test_savefig_to_stringio(format='eps')
79+
80+
81+
if __name__ == '__main__':
82+
import nose
83+
nose.runmodule(argv=['-s', '--with-doctest'], exit=False)

lib/matplotlib/texmanager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ def make_dvi(self, tex, fontsize):
413413
raise RuntimeError(
414414
('LaTeX was not able to process the following '
415415
'string:\n%s\nHere is the full report generated by '
416-
'LaTeX: \n\n' % repr(tex)) + report)
416+
'LaTeX: \n\n' % repr(tex.encode('unicode_escape')) +
417+
report))
417418
else:
418419
mpl.verbose.report(report, 'debug')
419420
for fname in glob.glob(basefile + '*'):

src/_ttconv.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,15 @@ class PythonFileWriter : public TTStreamWriter
4949
if (_write_method)
5050
{
5151
#if PY3K
52-
result = PyObject_CallFunction(_write_method, (char *)"y", a);
53-
#else
5452
result = PyObject_CallFunction(_write_method, (char *)"s", a);
53+
#else
54+
PyObject* decoded = NULL;
55+
decoded = PyUnicode_FromString(a);
56+
if (decoded == NULL) {
57+
throw PythonExceptionOccurred();
58+
}
59+
result = PyObject_CallFunction(_write_method, "O", decoded);
60+
Py_DECREF(decoded);
5561
#endif
5662
if (! result)
5763
{

0 commit comments

Comments
 (0)
0