Skip to content

FIX/API: fig.canvas.draw always updates internal state #18408

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

Merged
merged 3 commits into from
Feb 17, 2021
Merged
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
22 changes: 19 additions & 3 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,12 @@ def _draw(renderer): raise Done(renderer)
figure.canvas = orig_canvas


def _no_output_draw(figure):
renderer = _get_renderer(figure)
with renderer._draw_disabled():
figure.draw(renderer)


def _is_non_interactive_terminal_ipython(ip):
"""
Return whether we are in a a terminal IPython, but non interactive.
Expand Down Expand Up @@ -1621,7 +1627,9 @@ def _check_savefig_extra_args(func=None, extra_kwargs=()):
@functools.wraps(func)
def wrapper(*args, **kwargs):
name = 'savefig' # Reasonable default guess.
public_api = re.compile(r'^savefig|print_[A-Za-z0-9]+$')
public_api = re.compile(
r'^savefig|print_[A-Za-z0-9]+|_no_output_draw$'
)
seen_print_figure = False
for frame, line in traceback.walk_stack(None):
if frame is None:
Expand All @@ -1632,8 +1640,9 @@ def wrapper(*args, **kwargs):
frame.f_globals.get('__name__', '')):
if public_api.match(frame.f_code.co_name):
name = frame.f_code.co_name
if name == 'print_figure':
if name in ('print_figure', '_no_output_draw'):
seen_print_figure = True

else:
break

Expand Down Expand Up @@ -2021,7 +2030,14 @@ def release_mouse(self, ax):
self.mouse_grabber = None

def draw(self, *args, **kwargs):
"""Render the `.Figure`."""
"""
Render the `.Figure`.

It is important that this method actually walk the artist tree
even if not output is produced because this will trigger
deferred work (like computing limits auto-limits and tick
values) that users may want access to before saving to disk.
"""

def draw_idle(self, *args, **kwargs):
"""
Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
GraphicsContextBase, RendererBase)
GraphicsContextBase, RendererBase, _no_output_draw)
from matplotlib.backends.backend_mixed import MixedModeRenderer
from matplotlib.figure import Figure
from matplotlib.font_manager import findfont, get_font
Expand Down Expand Up @@ -2730,6 +2730,10 @@ def print_pdf(self, filename, *,
else: # we opened the file above; now finish it off
file.close()

def draw(self):
_no_output_draw(self.figure)
return super().draw()


FigureManagerPdf = FigureManagerBase

Expand Down
7 changes: 6 additions & 1 deletion lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
from matplotlib import _api, cbook, font_manager as fm
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
GraphicsContextBase, RendererBase)
GraphicsContextBase, RendererBase, _no_output_draw
)
from matplotlib.backends.backend_mixed import MixedModeRenderer
from matplotlib.backends.backend_pdf import (
_create_pdf_info_dict, _datetime_to_pdf)
Expand Down Expand Up @@ -906,6 +907,10 @@ def print_png(self, fname_or_fh, *args, **kwargs):
def get_renderer(self):
return RendererPgf(self.figure, None)

def draw(self):
_no_output_draw(self.figure)
return super().draw()


FigureManagerPgf = FigureManagerBase

Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from matplotlib.afm import AFM
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
GraphicsContextBase, RendererBase)
GraphicsContextBase, RendererBase, _no_output_draw)
from matplotlib.cbook import is_writable_file_like, file_requires_unicode
from matplotlib.font_manager import get_font
from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE
Expand Down Expand Up @@ -1129,6 +1129,10 @@ def _print_figure_tex(

_move_path_to_path_or_stream(tmpfile, outfile)

def draw(self):
_no_output_draw(self.figure)
return super().draw()


def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble,
paper_width, paper_height, orientation):
Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from matplotlib import _api, cbook
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase,
RendererBase)
RendererBase, _no_output_draw)
from matplotlib.backends.backend_mixed import MixedModeRenderer
from matplotlib.colors import rgb2hex
from matplotlib.dates import UTC
Expand Down Expand Up @@ -1363,6 +1363,10 @@ def _print_svg(self, filename, fh, *, dpi=None, bbox_inches_restore=None,
def get_default_filetype(self):
return 'svg'

def draw(self):
_no_output_draw(self.figure)
return super().draw()


FigureManagerSVG = FigureManagerBase

Expand Down
9 changes: 8 additions & 1 deletion lib/matplotlib/backends/backend_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,14 @@ class methods button_press_event, button_release_event,
"""

def draw(self):
"""Draw the figure using the renderer."""
"""
Draw the figure using the renderer.

It is important that this method actually walk the artist tree
even if not output is produced because this will trigger
deferred work (like computing limits auto-limits and tick
values) that users may want access to before saving to disk.
"""
renderer = RendererTemplate(self.figure.dpi)
self.figure.draw(renderer)

Expand Down
31 changes: 31 additions & 0 deletions lib/matplotlib/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import locale
import logging
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory

import matplotlib as mpl
from matplotlib import _api
Expand Down Expand Up @@ -44,3 +47,31 @@ def setup():
# are not necessarily the default values as specified in rcsetup.py.
set_font_settings_for_testing()
set_reproducibility_for_testing()


def check_for_pgf(texsystem):
"""
Check if a given TeX system + pgf is available

Parameters
----------
texsystem : str
The executable name to check
"""
with TemporaryDirectory() as tmpdir:
tex_path = Path(tmpdir, "test.tex")
tex_path.write_text(r"""
\documentclass{minimal}
\usepackage{pgf}
\begin{document}
\typeout{pgfversion=\pgfversion}
\makeatletter
\@@end
""")
try:
subprocess.check_call(
[texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
return False
return True
39 changes: 39 additions & 0 deletions lib/matplotlib/tests/test_backend_bases.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re

from matplotlib.testing import check_for_pgf
from matplotlib.backend_bases import (
FigureCanvasBase, LocationEvent, MouseButton, MouseEvent,
NavigationToolbar2, RendererBase)
Expand All @@ -13,6 +14,9 @@
import numpy as np
import pytest

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


def test_uses_per_path():
id = transforms.Affine2D()
Expand Down Expand Up @@ -187,3 +191,38 @@ def test_toolbar_zoompan():
assert ax.get_navigate_mode() == "ZOOM"
ax.figure.canvas.manager.toolmanager.trigger_tool('pan')
assert ax.get_navigate_mode() == "PAN"


@pytest.mark.parametrize(
"backend", ['svg', 'ps', 'pdf', pytest.param('pgf', marks=needs_xelatex)]
)
def test_draw(backend):
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvas
test_backend = pytest.importorskip(
f'matplotlib.backends.backend_{backend}'
)
TestCanvas = test_backend.FigureCanvas
fig_test = Figure(constrained_layout=True)
TestCanvas(fig_test)
axes_test = fig_test.subplots(2, 2)

# defaults to FigureCanvasBase
fig_agg = Figure(constrained_layout=True)
# put a backends.backend_agg.FigureCanvas on it
FigureCanvas(fig_agg)
axes_agg = fig_agg.subplots(2, 2)

init_pos = [ax.get_position() for ax in axes_test.ravel()]

fig_test.canvas.draw()
fig_agg.canvas.draw()

layed_out_pos_test = [ax.get_position() for ax in axes_test.ravel()]
layed_out_pos_agg = [ax.get_position() for ax in axes_agg.ravel()]

for init, placed in zip(init_pos, layed_out_pos_test):
assert not np.allclose(init, placed, atol=0.005)

for ref, test in zip(layed_out_pos_agg, layed_out_pos_test):
np.testing.assert_allclose(ref, test, atol=0.005)
32 changes: 5 additions & 27 deletions lib/matplotlib/tests/test_backend_pgf.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import datetime
from io import BytesIO
import os
from pathlib import Path
import shutil
import subprocess
from tempfile import TemporaryDirectory

import numpy as np
import pytest

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.testing import check_for_pgf
from matplotlib.testing.compare import compare_images, ImageComparisonFailure
from matplotlib.backends.backend_pgf import PdfPages, common_texification
from matplotlib.testing.decorators import (_image_directories,
Expand All @@ -19,32 +18,11 @@

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


def check_for(texsystem):
with TemporaryDirectory() as tmpdir:
tex_path = Path(tmpdir, "test.tex")
tex_path.write_text(r"""
\documentclass{minimal}
\usepackage{pgf}
\begin{document}
\typeout{pgfversion=\pgfversion}
\makeatletter
\@@end
""")
try:
subprocess.check_call(
[texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
return False
return True


needs_xelatex = pytest.mark.skipif(not check_for('xelatex'),
needs_xelatex = pytest.mark.skipif(not check_for_pgf('xelatex'),
reason='xelatex + pgf is required')
needs_pdflatex = pytest.mark.skipif(not check_for('pdflatex'),
needs_pdflatex = pytest.mark.skipif(not check_for_pgf('pdflatex'),
reason='pdflatex + pgf is required')
needs_lualatex = pytest.mark.skipif(not check_for('lualatex'),
needs_lualatex = pytest.mark.skipif(not check_for_pgf('lualatex'),
reason='lualatex + pgf is required')
needs_ghostscript = pytest.mark.skipif(
"eps" not in mpl.testing.compare.converter,
Expand Down Expand Up @@ -341,7 +319,7 @@ def test_unknown_font(caplog):
@pytest.mark.parametrize("texsystem", ("pdflatex", "xelatex", "lualatex"))
@pytest.mark.backend("pgf")
def test_minus_signs_with_tex(fig_test, fig_ref, texsystem):
if not check_for(texsystem):
if not check_for_pgf(texsystem):
pytest.skip(texsystem + ' + pgf is required')
mpl.rcParams["pgf.texsystem"] = texsystem
fig_test.text(.5, .5, "$-1$")
Expand Down