8000 Deprecate PdfPages(keep_empty=True). · matplotlib/matplotlib@b50c724 · GitHub
[go: up one dir, main page]

Skip to content

Commit b50c724

Browse files
committed
Deprecate PdfPages(keep_empty=True).
... because 0-page pdfs are not valid pdf files. Explicitly passing keep_empty=False remains supported for now to help transitioning to the new behavior. I also delayed file creation in backend_pdf.PdfPages as that seems simpler than re-deleting the file at closure if needed. I further dropped `__slots__` as the micro-optimization of these classes seem pointless (backend_pdf.PdfPages is wrapping a PdfFile object which is much larger anyways).
1 parent e4905bf commit b50c724

File tree

5 files changed

+138
-56
lines changed

5 files changed

+138
-56
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
``PdfPages(keep_empty=True)``
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
A zero-page pdf is not valid, thus passing ``keep_empty=True`` to
4+
`.backend_pdf.PdfPages` and `.backend_pgf.PdfPages`, and the ``keep_empty``
5+
attribute of these classes, are deprecated. Currently, these classes default
6+
to keeping empty outputs, but that behavior is deprecated too. Explicitly
7+
passing ``keep_empty=False`` remains supported for now to help transition to
8+
the new behavior.
9+
10+
Furthermore, `.backend_pdf.PdfPages` no longer immediately creates the target
11+
file upon instantiation, but only when the first figure is saved. To fully
12+
control file creation, directly pass an opened file object as argument
13+
(``with open(path, "wb") as file, PdfPages(file) as pdf: ...``).

lib/matplotlib/backends/backend_pdf.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2669,18 +2669,19 @@ class PdfPages:
26692669
In reality `PdfPages` is a thin wrapper around `PdfFile`, in order to avoid
26702670
confusion when using `~.pyplot.savefig` and forgetting the format argument.
26712671
"""
2672-
__slots__ = ('_file', 'keep_empty')
26732672

2674-
def __init__(self, filename, keep_empty=True, metadata=None 8000 ):
2673+
_UNSET = object()
2674+
2675+
def __init__(self, filename, keep_empty=_UNSET, metadata=None):
26752676
"""
26762677
Create a new PdfPages object.
26772678
26782679
Parameters
26792680
----------
26802681
filename : str or path-like or file-like
2681-
Plots using `PdfPages.savefig` will be written to a file at this
2682-
location. The file is opened at once and any older file with the
2683-
same name is overwritten.
2682+
Plots using `PdfPages.savefig` will be written to a file at this location.
2683+
The file is opened when a figure is saved for the first time (overwriting
2684+
any older file with the same name).
26842685
26852686
keep_empty : bool, optional
26862687
If set to False, then empty pdf files will be deleted automatically
@@ -2696,34 +2697,50 @@ def __init__(self, filename, keep_empty=True, metadata=None):
26962697
'Trapped'. Values have been predefined for 'Creator', 'Producer'
26972698
and 'CreationDate'. They can be removed by setting them to `None`.
26982699
"""
2699-
self._file = PdfFile(filename, metadata=metadata)
2700-
self.keep_empty = keep_empty
2700+
self._filename = filename
2701+
self._metadata = metadata
2702+
self._file = None
2703+
if keep_empty and keep_empty is not self._UNSET:
2704+
_api.warn_deprecated("3.8", message=(
2705+
"Keeping empty pdf files is deprecated since %(since)s and support "
2706+
"will be removed %(removal)s."))
2707+
self._keep_empty = keep_empty
2708+
2709+
keep_empty = _api.deprecate_privatize_attribute("3.8")
27012710

27022711
def __enter__(self):
27032712
return self
27042713

27052714
def __exit__(self, exc_type, exc_val, exc_tb):
27062715
self.close()
27072716

2717+
def _ensure_file(self):
2718+
if self._file is None:
2719+
self._file = PdfFile(self._filename, metadata=self._metadata) # init.
2720+
return self._file
2721+
27082722
def close(self):
27092723
"""
27102724
Finalize this object, making the underlying file a complete
27112725
PDF file.
27122726
"""
2713-
self._file.finalize()
2714-
self._file.close()
2715-
if (self.get_pagecount() == 0 and not self.keep_empty and
2716-
not self._file.passed_in_file_object):
2717-
os.remove(self._file.fh.name)
2718-
self._file = None
2727+
if self._file is not None:
2728+
self._file.finalize()
2729+
self._file.close()
2730+
self._file = None
2731+
elif self._keep_empty: # True *or* UNSET.
2732+
_api.warn_deprecated("3.8", message=(
2733+
"Keeping empty pdf files is deprecated since %(since)s and support "
2734+
"will be removed %(removal)s."))
2735+
PdfFile(self._filename, metadata=self._metadata) # touch the file.
27192736

27202737
def infodict(self):
27212738
"""
27222739
Return a modifiable information dictionary object
27232740
(see PDF reference section 10.2.1 'Document Information
27242741
Dictionary').
27252742
"""
2726-
return self._file.infoDict
2743+
return self._ensure_file().infoDict
27272744

27282745
def savefig(self, figure=None, **kwargs):
27292746
"""
@@ -2750,7 +2767,7 @@ def savefig(self, figure=None, **kwargs):
27502767

27512768
def get_pagecount(self):
27522769
"""Return the current number of pages in the multipage pdf file."""
2753-
return len(self._file.pageList)
2770+
return len(self._ensure_file().pageList)
27542771

27552772
def attach_note(self, text, positionRect=[-100, -100, 0, 0]):
27562773
"""
@@ -2759,7 +2776,7 @@ def attach_note(self, text, positionRect=[-100, -100, 0, 0]):
27592776
page. It is outside the page per default to make sure it is
27602777
invisible on printouts.
27612778
"""
2762-
self._file.newTextnote(text, positionRect)
2779+
self._ensure_file().newTextnote(text, positionRect)
27632780

27642781

27652782
class FigureCanvasPdf(FigureCanvasBase):
@@ -2778,7 +2795,7 @@ def print_pdf(self, filename, *,
27782795
self.figure.dpi = 72 # there are 72 pdf points to an inch
27792796
width, height = self.figure.get_size_inches()
27802797
if isinstance(filename, PdfPages):
2781-
file = filename._file
2798+
file = filename._ensure_file()
27822799
else:
27832800
file = PdfFile(filename, metadata=metadata)
27842801
try:

lib/matplotlib/backends/backend_pgf.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from PIL import Image
1515

1616
import matplotlib as mpl
17-
from matplotlib import cbook, font_manager as fm
17+
from matplotlib import _api, cbook, font_manager as fm
1818
from matplotlib.backend_bases import (
1919
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase
2020
)
@@ -874,16 +874,10 @@ class PdfPages:
874874
... # When no figure is specified the current figure is saved
875875
... pdf.savefig()
876876
"""
877-
__slots__ = (
878-
'_output_name',
879-
'keep_empty',
880-
'_n_figures',
881-
'_file',
882-
'_info_dict',
883-
'_metadata',
884-
)
885877

886-
def __init__(self, filename, *, keep_empty=True, metadata=None):
878+
_UNSET = object()
879+
880+
def __init__(self, filename, *, keep_empty=_UNSET, metadata=None):
887881
"""
888882
Create a new PdfPages object.
889883
@@ -912,11 +906,17 @@ def __init__(self, filename, *, keep_empty=True, metadata=None):
912906
"""
913907
self._output_name = filename
914908
self._n_figures = 0
915-
self.keep_empty = keep_empty
909+
if keep_empty and keep_empty is not self._UNSET:
910+
_api.warn_deprecated("3.8", message=(
911+
"Keeping empty pdf files is deprecated since %(since)s and support "
912+
"will be removed %(removal)s."))
913+
self._keep_empty = keep_empty
916914
self._metadata = (metadata or {}).copy()
917915
self._info_dict = _create_pdf_info_dict('pgf', self._metadata)
918916
self._file = BytesIO()
919917

918+
keep_empty = _api.deprecate_privatize_attribute("3.8")
919+
920920
def _write_header(self, width_inches, height_inches):
921921
pdfinfo = ','.join(
922922
_metadata_to_str(k, v) for k, v in self._info_dict.items())
@@ -946,7 +946,10 @@ def close(self):
946946
self._file.write(rb'\end{document}\n')
947947
if self._n_figures > 0:
948948
self._run_latex()
949-
elif self.keep_empty:
949+
elif self._keep_empty:
950+
_api.warn_deprecated("3.8", message=(
951+
"Keeping empty pdf files is deprecated since %(since)s and support "
952+
"will be removed %(removal)s."))
950953
open(self._output_name, 'wb').close()
951954
self._file.close()
952955

lib/matplotlib/tests/test_backend_pdf.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import io
44
import os
55
from pathlib import Path
6-
from tempfile import NamedTemporaryFile
76

87
import numpy as np
98
import pytest
@@ -81,35 +80,44 @@ def test_multipage_properfinalize():
8180
assert len(s) < 40000
8281

8382

84-
def test_multipage_keep_empty():
83+
def test_multipage_keep_empty(tmp_path):
84+
os.chdir(tmp_path)
85+
8586
# test empty pdf files
86-
# test that an empty pdf is left behind with keep_empty=True (default)
87-
with NamedTemporaryFile(delete=False) as tmp:
88-
with PdfPages(tmp) as pdf:
89-
filename = pdf._file.fh.name
90-
assert os.path.exists(filename)
91-
os.remove(filename)
92-
# test if an empty pdf is deleting itself afterwards with keep_empty=False
93-
with PdfPages(filename, keep_empty=False) as pdf:
87+
88+
# an empty pdf is left behind with keep_empty unset
89+
with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages("a.pdf") as pdf:
90+
pass
91+
assert os.path.exists("a.pdf")
92+
93+
# an empty pdf is left behind with keep_empty=True
94+
with pytest.warns(mpl.MatplotlibDeprecationWarning), \
95+
PdfPages("b.pdf", keep_empty=True) as pdf:
9496
pass
95-
assert not os.path.exists(filename)
97+
assert os.path.exists("b.pdf")
98+
99+
# an empty pdf deletes itself afterwards with keep_empty=False
100+
with PdfPages("c.pdf", keep_empty=False) as pdf:
101+
pass
102+
assert not os.path.exists("c.pdf")
103+
96104
# test pdf files with content, they should never be deleted
97-
fig, ax = plt.subplots()
98-
ax.plot([1, 2, 3])
99-
# test that a non-empty pdf is left behind with keep_empty=True (default)
100-
with NamedTemporaryFile(delete=False) as tmp:
101-
with PdfPages(tmp) as pdf:
102-
filename = pdf._file.fh.name
103-
pdf.savefig()
104-
assert os.path.exists(filename)
105-
os.remove(filename)
106-
# test that a non-empty pdf is left behind with keep_empty=False
107-
with NamedTemporaryFile(delete=False) as tmp:
108-
with PdfPages(tmp, keep_empty=False) as pdf:
109-
filename = pdf._file.fh.name
110-
pdf.savefig()
111-
assert os.path.exists(filename)
112-
os.remove(filename)
105+
106+
# a non-empty pdf is left behind with keep_empty unset
107+
with PdfPages("d.pdf") as pdf:
108+
pdf.savefig(plt.figure())
109+
assert os.path.exists("d.pdf")
110+
111+
# a non-empty pdf is left behind with keep_empty=True
112+
with pytest.warns(mpl.MatplotlibDeprecationWarning), \
113+
PdfPages("e.pdf", keep_empty=True) as pdf:
114+
pdf.savefig(plt.figure())
115+
assert os.path.exists("e.pdf")
116+
117+
# a non-empty pdf is left behind with keep_empty=False
118+
with PdfPages("f.pdf", keep_empty=False) as pdf:
119+
pdf.savefig(plt.figure())
120+
assert os.path.exists("f.pdf")
113121

114122

115123
def test_composite_image():

lib/matplotlib/tests/test_backend_pgf.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,47 @@ def test_pdf_pages_metadata_check(monkeypatch, system):
286286
}
287287

288288

289+
@needs_pgf_xelatex
290+
def test_multipage_keep_empty(tmp_path):
291+
os.chdir(tmp_path)
292+
293+
# test empty pdf files
294+
295+
# an empty pdf is left behind with keep_empty unset
296+
with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages("a.pdf") as pdf:
297+
pass
298+
assert os.path.exists("a.pdf")
299+
300+
# an empty pdf is left behind with keep_empty=True
301+
with pytest.warns(mpl.MatplotlibDeprecationWarning), \
302+
PdfPages("b.pdf", keep_empty=True) as pdf:
303+
pass
304+
assert os.path.exists("b.pdf")
305+
306+
# an empty pdf deletes itself afterwards with keep_empty=False
307+
with PdfPages("c.pdf", keep_empty=False) as pdf:
308+
pass
309+
assert not os.path.exists("c.pdf")
310+
311+
# test pdf files with content, they should never be deleted
312+
313+
# a non-empty pdf is left behind with keep_empty unset
314+
with PdfPages("d.pdf") as pdf:
315+
pdf.savefig(plt.figure())
316+
assert os.path.exists("d.pdf")
317+
318+
# a non-empty pdf is left behind with keep_empty=True
319+
with pytest.warns(mpl.MatplotlibDeprecationWarning), \
320+
PdfPages("e.pdf", keep_empty=True) as pdf:
321+
pdf.savefig(plt.figure())
322+
assert os.path.exists("e.pdf")
323+
324+
# a non-empty pdf is left behind with keep_empty=False
325+
with PdfPages("f.pdf", keep_empty=False) as pdf:
326+
pdf.savefig(plt.figure())
327+
assert os.path.exists("f.pdf")
328+
329+
289330
@needs_pgf_xelatex
290331
def test_tex_restart_after_error():
291332
fig = plt.figure()

0 commit comments

Comments
 (0)
0