Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b50c724

Browse filesBrowse 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
Copy full SHA for b50c724

File tree

Expand file treeCollapse file tree

5 files changed

+138
-56
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+138
-56
lines changed
+13Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_pdf.py
+34-17Lines 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):
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

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_pgf.py
+15-12Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_backend_pdf.py
+35-27Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_backend_pgf.py
+41Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.