Skip to content

Commit 73b5101

Browse files
authored
Merge pull request #18408 from tacaswell/fix_pgf_draw
FIX/API: `fig.canvas.draw` always updates internal state
2 parents c152045 + 22e84f6 commit 73b5101

File tree

9 files changed

+123
-35
lines changed

9 files changed

+123
-35
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,12 @@ def _draw(renderer): raise Done(renderer)
15841584
figure.canvas = orig_canvas
15851585

15861586

1587+
def _no_output_draw(figure):
1588+
renderer = _get_renderer(figure)
1589+
with renderer._draw_disabled():
1590+
figure.draw(renderer)
1591+
1592+
15871593
def _is_non_interactive_terminal_ipython(ip):
15881594
"""
15891595
Return whether we are in a a terminal IPython, but non interactive.
@@ -1621,7 +1627,9 @@ def _check_savefig_extra_args(func=None, extra_kwargs=()):
16211627
@functools.wraps(func)
16221628
def wrapper(*args, **kwargs):
16231629
name = 'savefig' # Reasonable default guess.
1624-
public_api = re.compile(r'^savefig|print_[A-Za-z0-9]+$')
1630+
public_api = re.compile(
1631+
r'^savefig|print_[A-Za-z0-9]+|_no_output_draw$'
1632+
)
16251633
seen_print_figure = False
16261634
for frame, line in traceback.walk_stack(None):
16271635
if frame is None:
@@ -1632,8 +1640,9 @@ def wrapper(*args, **kwargs):
16321640
frame.f_globals.get('__name__', '')):
16331641
if public_api.match(frame.f_code.co_name):
16341642
name = frame.f_code.co_name
1635-
if name == 'print_figure':
1643+
if name in ('print_figure', '_no_output_draw'):
16361644
seen_print_figure = True
1645+
16371646
else:
16381647
break
16391648

@@ -2021,7 +2030,14 @@ def release_mouse(self, ax):
20212030
self.mouse_grabber = None
20222031

20232032
def draw(self, *args, **kwargs):
2024-
"""Render the `.Figure`."""
2033+
"""
2034+
Render the `.Figure`.
2035+
2036+
It is important that this method actually walk the artist tree
2037+
even if not output is produced because this will trigger
2038+
deferred work (like computing limits auto-limits and tick
2039+
values) that users may want access to before saving to disk.
2040+
"""
20252041

20262042
def draw_idle(self, *args, **kwargs):
20272043
"""

lib/matplotlib/backends/backend_pdf.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from matplotlib._pylab_helpers import Gcf
2929
from matplotlib.backend_bases import (
3030
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
31-
GraphicsContextBase, RendererBase)
31+
GraphicsContextBase, RendererBase, _no_output_draw)
3232
from matplotlib.backends.backend_mixed import MixedModeRenderer
3333
from matplotlib.figure import Figure
3434
from matplotlib.font_manager import findfont, get_font
@@ -2730,6 +2730,10 @@ def print_pdf(self, filename, *,
27302730
else: # we opened the file above; now finish it off
27312731
file.close()
27322732

2733+
def draw(self):
2734+
_no_output_draw(self.figure)
2735+
return super().draw()
2736+
27332737

27342738
FigureManagerPdf = FigureManagerBase
27352739

lib/matplotlib/backends/backend_pgf.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
from matplotlib import _api, cbook, font_manager as fm
2020
from matplotlib.backend_bases import (
2121
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
22-
GraphicsContextBase, RendererBase)
22+
GraphicsContextBase, RendererBase, _no_output_draw
23+
)
2324
from matplotlib.backends.backend_mixed import MixedModeRenderer
2425
from matplotlib.backends.backend_pdf import (
2526
_create_pdf_info_dict, _datetime_to_pdf)
@@ -906,6 +907,10 @@ def print_png(self, fname_or_fh, *args, **kwargs):
906907
def get_renderer(self):
907908
return RendererPgf(self.figure, None)
908909

910+
def draw(self):
911+
_no_output_draw(self.figure)
912+
return super().draw()
913+
909914

910915
FigureManagerPgf = FigureManagerBase
911916

lib/matplotlib/backends/backend_ps.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from matplotlib.afm import AFM
2424
from matplotlib.backend_bases import (
2525
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
26-
GraphicsContextBase, RendererBase)
26+
GraphicsContextBase, RendererBase, _no_output_draw)
2727
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
2828
from matplotlib.font_manager import get_font
2929
from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE
@@ -1129,6 +1129,10 @@ def _print_figure_tex(
11291129

11301130
_move_path_to_path_or_stream(tmpfile, outfile)
11311131

1132+
def draw(self):
1133+
_no_output_draw(self.figure)
1134+
return super().draw()
1135+
11321136

11331137
def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble,
11341138
paper_width, paper_height, orientation):

lib/matplotlib/backends/backend_svg.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from matplotlib import _api, cbook
1818
from matplotlib.backend_bases import (
1919
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
20-
RendererBase)
20+
RendererBase, _no_output_draw)
2121
from matplotlib.backends.backend_mixed import MixedModeRenderer
2222
from matplotlib.colors import rgb2hex
2323
from matplotlib.dates import UTC
@@ -1363,6 +1363,10 @@ def _print_svg(self, filename, fh, *, dpi=None, bbox_inches_restore=None,
13631363
def get_default_filetype(self):
13641364
return 'svg'
13651365

1366+
def draw(self):
1367+
_no_output_draw(self.figure)
1368+
return super().draw()
1369+
13661370

13671371
FigureManagerSVG = FigureManagerBase
13681372

lib/matplotlib/backends/backend_template.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,14 @@ class methods button_press_event, button_release_event,
191191
"""
192192

193193
def draw(self):
194-
"""Draw the figure using the renderer."""
194+
"""
195+
Draw the figure using the renderer.
196+
197+
It is important that this method actually walk the artist tree
198+
even if not output is produced because this will trigger
199+
deferred work (like computing limits auto-limits and tick
200+
values) that users may want access to before saving to disk.
201+
"""
195202
renderer = RendererTemplate(self.figure.dpi)
196203
self.figure.draw(renderer)
197204

lib/matplotlib/testing/__init__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
import locale
66
import logging
7+
import subprocess
8+
from pathlib import Path
9+
from tempfile import TemporaryDirectory
710

811
import matplotlib as mpl
912
from matplotlib import _api
@@ -44,3 +47,31 @@ def setup():
4447
# are not necessarily the default values as specified in rcsetup.py.
4548
set_font_settings_for_testing()
4649
set_reproducibility_for_testing()
50+
51+
52+
def check_for_pgf(texsystem):
53+
"""
54+
Check if a given TeX system + pgf is available
55+
56+
Parameters
57+
----------
58+
texsystem : str
59+
The executable name to check
60+
"""
61+
with TemporaryDirectory() as tmpdir:
62+
tex_path = Path(tmpdir, "test.tex")
63+
tex_path.write_text(r"""
64+
\documentclass{minimal}
65+
\usepackage{pgf}
66+
\begin{document}
67+
\typeout{pgfversion=\pgfversion}
68+
\makeatletter
69+
\@@end
70+
""")
71+
try:
72+
subprocess.check_call(
73+
[texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir,
74+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
75+
except (OSError, subprocess.CalledProcessError):
76+
return False
77+
return True

lib/matplotlib/tests/test_backend_bases.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import re
22

3+
from matplotlib.testing import check_for_pgf
34
from matplotlib.backend_bases import (
45
FigureCanvasBase, LocationEvent, MouseButton, MouseEvent,
56
NavigationToolbar2, RendererBase)
@@ -13,6 +14,9 @@
1314
import numpy as np
1415
import pytest
1516

17+
needs_xelatex = pytest.mark.skipif(not check_for_pgf('xelatex'),
18+
reason='xelatex + pgf is required')
19+
1620

1721
def test_uses_per_path():
1822
id = transforms.Affine2D()
@@ -183,3 +187,38 @@ def test_toolbar_zoompan():
183187
assert ax.get_navigate_mode() == "ZOOM"
184188
ax.figure.canvas.manager.toolmanager.trigger_tool('pan')
185189
assert ax.get_navigate_mode() == "PAN"
190+
191+
192+
@pytest.mark.parametrize(
193+
"backend", ['svg', 'ps', 'pdf', pytest.param('pgf', marks=needs_xelatex)]
194+
)
195+
def test_draw(backend):
196+
from matplotlib.figure import Figure
197+
from matplotlib.backends.backend_agg import FigureCanvas
198+
test_backend = pytest.importorskip(
199+
f'matplotlib.backends.backend_{backend}'
200+
)
201+
TestCanvas = test_backend.FigureCanvas
202+
fig_test = Figure(constrained_layout=True)
203+
TestCanvas(fig_test)
204+
axes_test = fig_test.subplots(2, 2)
205+
206+
# defaults to FigureCanvasBase
207+
fig_agg = Figure(constrained_layout=True)
208+
# put a backends.backend_agg.FigureCanvas on it
209+
FigureCanvas(fig_agg)
210+
axes_agg = fig_agg.subplots(2, 2)
211+
212+
init_pos = [ax.get_position() for ax in axes_test.ravel()]
213+
214+
fig_test.canvas.draw()
215+
fig_agg.canvas.draw()
216+
217+
layed_out_pos_test = [ax.get_position() for ax in axes_test.ravel()]
218+
layed_out_pos_agg = [ax.get_position() for ax in axes_agg.ravel()]
219+
220+
for init, placed in zip(init_pos, layed_out_pos_test):
221+
assert not np.allclose(init, placed, atol=0.005)
222+
223+
for ref, test in zip(layed_out_pos_agg, layed_out_pos_test):
224+
np.testing.assert_allclose(ref, test, atol=0.005)

lib/matplotlib/tests/test_backend_pgf.py

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import datetime
22
from io import BytesIO
33
import os
4-
from pathlib import Path
54
import shutil
65
import subprocess
7-
from tempfile import TemporaryDirectory
86

97
import numpy as np
108
import pytest
119

1210
import matplotlib as mpl
1311
import matplotlib.pyplot as plt
12+
from matplotlib.testing import check_for_pgf
1413
from matplotlib.testing.compare import compare_images, ImageComparisonFailure
1514
from matplotlib.backends.backend_pgf import PdfPages, common_texification
1615
from matplotlib.testing.decorators import (_image_directories,
@@ -19,32 +18,11 @@
1918

2019
baseline_dir, result_dir = _image_directories(lambda: 'dummy func')
2120

22-
23-
def check_for(texsystem):
24-
with TemporaryDirectory() as tmpdir:
25-
tex_path = Path(tmpdir, "test.tex")
26-
tex_path.write_text(r"""
27-
\documentclass{minimal}
28-
\usepackage{pgf}
29-
\begin{document}
30-
\typeout{pgfversion=\pgfversion}
31-
\makeatletter
32-
\@@end
33-
""")
34-
try:
35-
subprocess.check_call(
36-
[texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir,
37-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
38-
except (OSError, subprocess.CalledProcessError):
39-
return False
40-
return True
41-
42-
43-
needs_xelatex = pytest.mark.skipif(not check_for('xelatex'),
21+
needs_xelatex = pytest.mark.skipif(not check_for_pgf('xelatex'),
4422
reason='xelatex + pgf is required')
45-
needs_pdflatex = pytest.mark.skipif(not check_for('pdflatex'),
23+
needs_pdflatex = pytest.mark.skipif(not check_for_pgf('pdflatex'),
4624
reason='pdflatex + pgf is required')
47-
needs_lualatex = pytest.mark.skipif(not check_for('lualatex'),
25+
needs_lualatex = pytest.mark.skipif(not check_for_pgf('lualatex'),
4826
reason='lualatex + pgf is required')
4927
needs_ghostscript = pytest.mark.skipif(
5028
"eps" not in mpl.testing.compare.converter,
@@ -338,7 +316,7 @@ def test_unknown_font(caplog):
338316
@pytest.mark.parametrize("texsystem", ("pdflatex", "xelatex", "lualatex"))
339317
@pytest.mark.backend("pgf")
340318
def test_minus_signs_with_tex(fig_test, fig_ref, texsystem):
341-
if not check_for(texsystem):
319+
if not check_for_pgf(texsystem):
342320
pytest.skip(texsystem + ' + pgf is required')
343321
mpl.rcParams["pgf.texsystem"] = texsystem
344322
fig_test.text(.5, .5, "$-1$")

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy