Revert "Delete .venv directory"

This reverts commit 5a2693bd9f.
This commit is contained in:
Untriex Programming
2021-08-31 22:26:50 +02:00
parent fd493a708d
commit 6544d16770
5105 changed files with 1440072 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
from pathlib import Path
# Check that the test directories exist.
if not (Path(__file__).parent / 'baseline_images').exists():
raise IOError(
'The baseline image directory does not exist. '
'This is most likely because the test data is not installed. '
'You may need to install matplotlib from source to get the '
'test data.')

View File

@@ -0,0 +1,4 @@
from matplotlib.testing.conftest import (mpl_test_settings,
mpl_image_comparison_parameters,
pytest_configure, pytest_unconfigure,
pd)

View File

@@ -0,0 +1,137 @@
from io import BytesIO
import pytest
import logging
from matplotlib import afm
from matplotlib import font_manager as fm
# See note in afm.py re: use of comma as decimal separator in the
# UnderlineThickness field and re: use of non-ASCII characters in the Notice
# field.
AFM_TEST_DATA = b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
FontName MyFont-Bold
EncodingScheme FontSpecific
FullName My Font Bold
FamilyName Test Fonts
Weight Bold
ItalicAngle 0.0
IsFixedPitch false
UnderlinePosition -100
UnderlineThickness 56,789
Version 001.000
Notice Copyright \xa9 2017 No one.
FontBBox 0 -321 1234 369
StartCharMetrics 3
C 0 ; WX 250 ; N space ; B 0 0 0 0 ;
C 42 ; WX 1141 ; N foo ; B 40 60 800 360 ;
C 99 ; WX 583 ; N bar ; B 40 -10 543 210 ;
EndCharMetrics
EndFontMetrics
"""
def test_nonascii_str():
# This tests that we also decode bytes as utf-8 properly.
# Else, font files with non ascii characters fail to load.
inp_str = "привет"
byte_str = inp_str.encode("utf8")
ret = afm._to_str(byte_str)
assert ret == inp_str
def test_parse_header():
fh = BytesIO(AFM_TEST_DATA)
header = afm._parse_header(fh)
assert header == {
b'StartFontMetrics': 2.0,
b'FontName': 'MyFont-Bold',
b'EncodingScheme': 'FontSpecific',
b'FullName': 'My Font Bold',
b'FamilyName': 'Test Fonts',
b'Weight': 'Bold',
b'ItalicAngle': 0.0,
b'IsFixedPitch': False,
b'UnderlinePosition': -100,
b'UnderlineThickness': 56.789,
b'Version': '001.000',
b'Notice': b'Copyright \xa9 2017 No one.',
b'FontBBox': [0, -321, 1234, 369],
b'StartCharMetrics': 3,
}
def test_parse_char_metrics():
fh = BytesIO(AFM_TEST_DATA)
afm._parse_header(fh) # position
metrics = afm._parse_char_metrics(fh)
assert metrics == (
{0: (250.0, 'space', [0, 0, 0, 0]),
42: (1141.0, 'foo', [40, 60, 800, 360]),
99: (583.0, 'bar', [40, -10, 543, 210]),
},
{'space': (250.0, 'space', [0, 0, 0, 0]),
'foo': (1141.0, 'foo', [40, 60, 800, 360]),
'bar': (583.0, 'bar', [40, -10, 543, 210]),
})
def test_get_familyname_guessed():
fh = BytesIO(AFM_TEST_DATA)
font = afm.AFM(fh)
del font._header[b'FamilyName'] # remove FamilyName, so we have to guess
assert font.get_familyname() == 'My Font'
def test_font_manager_weight_normalization():
font = afm.AFM(BytesIO(
AFM_TEST_DATA.replace(b"Weight Bold\n", b"Weight Custom\n")))
assert fm.afmFontProperty("", font).weight == "normal"
@pytest.mark.parametrize(
"afm_data",
[
b"""nope
really nope""",
b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
FontName MyFont-Bold
EncodingScheme FontSpecific""",
],
)
def test_bad_afm(afm_data):
fh = BytesIO(afm_data)
with pytest.raises(RuntimeError):
afm._parse_header(fh)
@pytest.mark.parametrize(
"afm_data",
[
b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
Aardvark bob
FontName MyFont-Bold
EncodingScheme FontSpecific
StartCharMetrics 3""",
b"""StartFontMetrics 2.0
Comment Comments are ignored.
Comment Creation Date:Mon Nov 13 12:34:11 GMT 2017
ItalicAngle zero degrees
FontName MyFont-Bold
EncodingScheme FontSpecific
StartCharMetrics 3""",
],
)
def test_malformed_header(afm_data, caplog):
fh = BytesIO(afm_data)
with caplog.at_level(logging.ERROR):
afm._parse_header(fh)
assert len(caplog.records) == 1

View File

@@ -0,0 +1,246 @@
import io
import numpy as np
from numpy.testing import assert_array_almost_equal
from PIL import Image, TiffTags
import pytest
from matplotlib import (
collections, path, pyplot as plt, transforms as mtransforms, rcParams)
from matplotlib.image import imread
from matplotlib.figure import Figure
from matplotlib.testing.decorators import image_comparison
def test_repeated_save_with_alpha():
# We want an image which has a background color of bluish green, with an
# alpha of 0.25.
fig = Figure([1, 0.4])
fig.set_facecolor((0, 1, 0.4))
fig.patch.set_alpha(0.25)
# The target color is fig.patch.get_facecolor()
buf = io.BytesIO()
fig.savefig(buf,
facecolor=fig.get_facecolor(),
edgecolor='none')
# Save the figure again to check that the
# colors don't bleed from the previous renderer.
buf.seek(0)
fig.savefig(buf,
facecolor=fig.get_facecolor(),
edgecolor='none')
# Check the first pixel has the desired color & alpha
# (approx: 0, 1.0, 0.4, 0.25)
buf.seek(0)
assert_array_almost_equal(tuple(imread(buf)[0, 0]),
(0.0, 1.0, 0.4, 0.250),
decimal=3)
def test_large_single_path_collection():
buff = io.BytesIO()
# Generates a too-large single path in a path collection that
# would cause a segfault if the draw_markers optimization is
# applied.
f, ax = plt.subplots()
collection = collections.PathCollection(
[path.Path([[-10, 5], [10, 5], [10, -5], [-10, -5], [-10, 5]])])
ax.add_artist(collection)
ax.set_xlim(10**-3, 1)
plt.savefig(buff)
def test_marker_with_nan():
# This creates a marker with nans in it, which was segfaulting the
# Agg backend (see #3722)
fig, ax = plt.subplots(1)
steps = 1000
data = np.arange(steps)
ax.semilogx(data)
ax.fill_between(data, data*0.8, data*1.2)
buf = io.BytesIO()
fig.savefig(buf, format='png')
def test_long_path():
buff = io.BytesIO()
fig, ax = plt.subplots()
np.random.seed(0)
points = np.random.rand(70000)
ax.plot(points)
fig.savefig(buff, format='png')
@image_comparison(['agg_filter.png'], remove_text=True)
def test_agg_filter():
def smooth1d(x, window_len):
# copied from http://www.scipy.org/Cookbook/SignalSmooth
s = np.r_[
2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]]
w = np.hanning(window_len)
y = np.convolve(w/w.sum(), s, mode='same')
return y[window_len-1:-window_len+1]
def smooth2d(A, sigma=3):
window_len = max(int(sigma), 3) * 2 + 1
A = np.apply_along_axis(smooth1d, 0, A, window_len)
A = np.apply_along_axis(smooth1d, 1, A, window_len)
return A
class BaseFilter:
def get_pad(self, dpi):
return 0
def process_image(self, padded_src, dpi):
raise NotImplementedError("Should be overridden by subclasses")
def __call__(self, im, dpi):
pad = self.get_pad(dpi)
padded_src = np.pad(im, [(pad, pad), (pad, pad), (0, 0)],
"constant")
tgt_image = self.process_image(padded_src, dpi)
return tgt_image, -pad, -pad
class OffsetFilter(BaseFilter):
def __init__(self, offsets=(0, 0)):
self.offsets = offsets
def get_pad(self, dpi):
return int(max(self.offsets) / 72 * dpi)
def process_image(self, padded_src, dpi):
ox, oy = self.offsets
a1 = np.roll(padded_src, int(ox / 72 * dpi), axis=1)
a2 = np.roll(a1, -int(oy / 72 * dpi), axis=0)
return a2
class GaussianFilter(BaseFilter):
"""Simple Gaussian filter."""
def __init__(self, sigma, alpha=0.5, color=(0, 0, 0)):
self.sigma = sigma
self.alpha = alpha
self.color = color
def get_pad(self, dpi):
return int(self.sigma*3 / 72 * dpi)
def process_image(self, padded_src, dpi):
tgt_image = np.empty_like(padded_src)
tgt_image[:, :, :3] = self.color
tgt_image[:, :, 3] = smooth2d(padded_src[:, :, 3] * self.alpha,
self.sigma / 72 * dpi)
return tgt_image
class DropShadowFilter(BaseFilter):
def __init__(self, sigma, alpha=0.3, color=(0, 0, 0), offsets=(0, 0)):
self.gauss_filter = GaussianFilter(sigma, alpha, color)
self.offset_filter = OffsetFilter(offsets)
def get_pad(self, dpi):
return max(self.gauss_filter.get_pad(dpi),
self.offset_filter.get_pad(dpi))
def process_image(self, padded_src, dpi):
t1 = self.gauss_filter.process_image(padded_src, dpi)
t2 = self.offset_filter.process_image(t1, dpi)
return t2
fig, ax = plt.subplots()
# draw lines
line1, = ax.plot([0.1, 0.5, 0.9], [0.1, 0.9, 0.5], "bo-",
mec="b", mfc="w", lw=5, mew=3, ms=10, label="Line 1")
line2, = ax.plot([0.1, 0.5, 0.9], [0.5, 0.2, 0.7], "ro-",
mec="r", mfc="w", lw=5, mew=3, ms=10, label="Line 1")
gauss = DropShadowFilter(4)
for line in [line1, line2]:
# draw shadows with same lines with slight offset.
xx = line.get_xdata()
yy = line.get_ydata()
shadow, = ax.plot(xx, yy)
shadow.update_from(line)
# offset transform
ot = mtransforms.offset_copy(line.get_transform(), ax.figure,
x=4.0, y=-6.0, units='points')
shadow.set_transform(ot)
# adjust zorder of the shadow lines so that it is drawn below the
# original lines
shadow.set_zorder(line.get_zorder() - 0.5)
shadow.set_agg_filter(gauss)
shadow.set_rasterized(True) # to support mixed-mode renderers
ax.set_xlim(0., 1.)
ax.set_ylim(0., 1.)
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
def test_too_large_image():
fig = plt.figure(figsize=(300, 1000))
buff = io.BytesIO()
with pytest.raises(ValueError):
fig.savefig(buff)
def test_chunksize():
x = range(200)
# Test without chunksize
fig, ax = plt.subplots()
ax.plot(x, np.sin(x))
fig.canvas.draw()
# Test with chunksize
fig, ax = plt.subplots()
rcParams['agg.path.chunksize'] = 105
ax.plot(x, np.sin(x))
fig.canvas.draw()
@pytest.mark.backend('Agg')
def test_jpeg_dpi():
# Check that dpi is set correctly in jpg files.
plt.plot([0, 1, 2], [0, 1, 0])
buf = io.BytesIO()
plt.savefig(buf, format="jpg", dpi=200)
im = Image.open(buf)
assert im.info['dpi'] == (200, 200)
def test_pil_kwargs_png():
from PIL.PngImagePlugin import PngInfo
buf = io.BytesIO()
pnginfo = PngInfo()
pnginfo.add_text("Software", "test")
plt.figure().savefig(buf, format="png", pil_kwargs={"pnginfo": pnginfo})
im = Image.open(buf)
assert im.info["Software"] == "test"
def test_pil_kwargs_tiff():
buf = io.BytesIO()
pil_kwargs = {"description": "test image"}
plt.figure().savefig(buf, format="tiff", pil_kwargs=pil_kwargs)
im = Image.open(buf)
tags = {TiffTags.TAGS_V2[k].name: v for k, v in im.tag_v2.items()}
assert tags["ImageDescription"] == "test image"

View File

@@ -0,0 +1,33 @@
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
@image_comparison(baseline_images=['agg_filter_alpha'],
extensions=['png', 'pdf'])
def test_agg_filter_alpha():
# Remove this line when this test image is regenerated.
plt.rcParams['pcolormesh.snap'] = False
ax = plt.axes()
x, y = np.mgrid[0:7, 0:8]
data = x**2 - y**2
mesh = ax.pcolormesh(data, cmap='Reds', zorder=5)
def manual_alpha(im, dpi):
im[:, :, 3] *= 0.6
print('CALLED')
return im, 0, 0
# Note: Doing alpha like this is not the same as setting alpha on
# the mesh itself. Currently meshes are drawn as independent patches,
# and we see fine borders around the blocks of color. See the SO
# question for an example: https://stackoverflow.com/questions/20678817
mesh.set_agg_filter(manual_alpha)
# Currently we must enable rasterization for this to have an effect in
# the PDF backend.
mesh.set_rasterized(True)
ax.plot([0, 4, 7], [1, 3, 8])

View File

@@ -0,0 +1,361 @@
import gc
import os
from pathlib import Path
import subprocess
import sys
import weakref
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib import animation
@pytest.fixture()
def anim(request):
"""Create a simple animation (with options)."""
fig, ax = plt.subplots()
line, = ax.plot([], [])
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
def init():
line.set_data([], [])
return line,
def animate(i):
x = np.linspace(0, 10, 100)
y = np.sin(x + i)
line.set_data(x, y)
return line,
# "klass" can be passed to determine the class returned by the fixture
kwargs = dict(getattr(request, 'param', {})) # make a copy
klass = kwargs.pop('klass', animation.FuncAnimation)
if 'frames' not in kwargs:
kwargs['frames'] = 5
return klass(fig=fig, func=animate, init_func=init, **kwargs)
class NullMovieWriter(animation.AbstractMovieWriter):
"""
A minimal MovieWriter. It doesn't actually write anything.
It just saves the arguments that were given to the setup() and
grab_frame() methods as attributes, and counts how many times
grab_frame() is called.
This class doesn't have an __init__ method with the appropriate
signature, and it doesn't define an isAvailable() method, so
it cannot be added to the 'writers' registry.
"""
def setup(self, fig, outfile, dpi, *args):
self.fig = fig
self.outfile = outfile
self.dpi = dpi
self.args = args
self._count = 0
def grab_frame(self, **savefig_kwargs):
self.savefig_kwargs = savefig_kwargs
self._count += 1
def finish(self):
pass
def test_null_movie_writer(anim):
# Test running an animation with NullMovieWriter.
filename = "unused.null"
dpi = 50
savefig_kwargs = dict(foo=0)
writer = NullMovieWriter()
anim.save(filename, dpi=dpi, writer=writer,
savefig_kwargs=savefig_kwargs)
assert writer.fig == plt.figure(1) # The figure used by anim fixture
assert writer.outfile == filename
assert writer.dpi == dpi
assert writer.args == ()
assert writer.savefig_kwargs == savefig_kwargs
assert writer._count == anim.save_count
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_animation_delete(anim):
anim = animation.FuncAnimation(**anim)
with pytest.warns(Warning, match='Animation was deleted'):
del anim
gc.collect()
def test_movie_writer_dpi_default():
class DummyMovieWriter(animation.MovieWriter):
def _run(self):
pass
# Test setting up movie writer with figure.dpi default.
fig = plt.figure()
filename = "unused.null"
fps = 5
codec = "unused"
bitrate = 1
extra_args = ["unused"]
writer = DummyMovieWriter(fps, codec, bitrate, extra_args)
writer.setup(fig, filename)
assert writer.dpi == fig.dpi
@animation.writers.register('null')
class RegisteredNullMovieWriter(NullMovieWriter):
# To be able to add NullMovieWriter to the 'writers' registry,
# we must define an __init__ method with a specific signature,
# and we must define the class method isAvailable().
# (These methods are not actually required to use an instance
# of this class as the 'writer' argument of Animation.save().)
def __init__(self, fps=None, codec=None, bitrate=None,
extra_args=None, metadata=None):
pass
@classmethod
def isAvailable(cls):
return True
WRITER_OUTPUT = [
('ffmpeg', 'movie.mp4'),
('ffmpeg_file', 'movie.mp4'),
('avconv', 'movie.mp4'),
('avconv_file', 'movie.mp4'),
('imagemagick', 'movie.gif'),
('imagemagick_file', 'movie.gif'),
('pillow', 'movie.gif'),
('html', 'movie.html'),
('null', 'movie.null')
]
def gen_writers():
for writer, output in WRITER_OUTPUT:
if not animation.writers.is_available(writer):
mark = pytest.mark.skip(
f"writer '{writer}' not available on this system")
yield pytest.param(writer, None, output, marks=[mark])
yield pytest.param(writer, None, Path(output), marks=[mark])
continue
writer_class = animation.writers[writer]
for frame_format in getattr(writer_class, 'supported_formats', [None]):
yield writer, frame_format, output
yield writer, frame_format, Path(output)
# Smoke test for saving animations. In the future, we should probably
# design more sophisticated tests which compare resulting frames a-la
# matplotlib.testing.image_comparison
@pytest.mark.parametrize('writer, frame_format, output', gen_writers())
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_save_animation_smoketest(tmpdir, writer, frame_format, output, anim):
if frame_format is not None:
plt.rcParams["animation.frame_format"] = frame_format
anim = animation.FuncAnimation(**anim)
dpi = None
codec = None
if writer == 'ffmpeg':
# Issue #8253
anim._fig.set_size_inches((10.85, 9.21))
dpi = 100.
codec = 'h264'
# Use temporary directory for the file-based writers, which produce a file
# per frame with known names.
with tmpdir.as_cwd():
anim.save(output, fps=30, writer=writer, bitrate=500, dpi=dpi,
codec=codec)
with pytest.warns(None):
del anim
@pytest.mark.parametrize('writer', [
pytest.param(
'ffmpeg', marks=pytest.mark.skipif(
not animation.FFMpegWriter.isAvailable(),
reason='Requires FFMpeg')),
pytest.param(
'imagemagick', marks=pytest.mark.skipif(
not animation.ImageMagickWriter.isAvailable(),
reason='Requires ImageMagick')),
])
@pytest.mark.parametrize('html, want', [
('none', None),
('html5', '<video width'),
('jshtml', '<script ')
])
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
def test_animation_repr_html(writer, html, want, anim):
if (writer == 'imagemagick' and html == 'html5'
# ImageMagick delegates to ffmpeg for this format.
and not animation.FFMpegWriter.isAvailable()):
pytest.skip('Requires FFMpeg')
# create here rather than in the fixture otherwise we get __del__ warnings
# about producing no output
anim = animation.FuncAnimation(**anim)
with plt.rc_context({'animation.writer': writer,
'animation.html': html}):
html = anim._repr_html_()
if want is None:
assert html is None
with pytest.warns(UserWarning):
del anim # Animtion was never run, so will warn on cleanup.
else:
assert want in html
@pytest.mark.parametrize('anim', [dict(frames=iter(range(5)))],
indirect=['anim'])
def test_no_length_frames(anim):
anim.save('unused.null', writer=NullMovieWriter())
def test_movie_writer_registry():
assert len(animation.writers._registered) > 0
mpl.rcParams['animation.ffmpeg_path'] = "not_available_ever_xxxx"
assert not animation.writers.is_available("ffmpeg")
# something guaranteed to be available in path and exits immediately
bin = "true" if sys.platform != 'win32' else "where"
mpl.rcParams['animation.ffmpeg_path'] = bin
assert animation.writers.is_available("ffmpeg")
@pytest.mark.parametrize(
"method_name",
[pytest.param("to_html5_video", marks=pytest.mark.skipif(
not animation.writers.is_available(mpl.rcParams["animation.writer"]),
reason="animation writer not installed")),
"to_jshtml"])
@pytest.mark.parametrize('anim', [dict(frames=1)], indirect=['anim'])
def test_embed_limit(method_name, caplog, tmpdir, anim):
caplog.set_level("WARNING")
with tmpdir.as_cwd():
with mpl.rc_context({"animation.embed_limit": 1e-6}): # ~1 byte.
getattr(anim, method_name)()
assert len(caplog.records) == 1
record, = caplog.records
assert (record.name == "matplotlib.animation"
and record.levelname == "WARNING")
@pytest.mark.parametrize(
"method_name",
[pytest.param("to_html5_video", marks=pytest.mark.skipif(
not animation.writers.is_available(mpl.rcParams["animation.writer"]),
reason="animation writer not installed")),
"to_jshtml"])
@pytest.mark.parametrize('anim', [dict(frames=1)], indirect=['anim'])
def test_cleanup_temporaries(method_name, tmpdir, anim):
with tmpdir.as_cwd():
getattr(anim, method_name)()
assert list(Path(str(tmpdir)).iterdir()) == []
@pytest.mark.skipif(os.name != "posix", reason="requires a POSIX OS")
def test_failing_ffmpeg(tmpdir, monkeypatch, anim):
"""
Test that we correctly raise a CalledProcessError when ffmpeg fails.
To do so, mock ffmpeg using a simple executable shell script that
succeeds when called with no arguments (so that it gets registered by
`isAvailable`), but fails otherwise, and add it to the $PATH.
"""
with tmpdir.as_cwd():
monkeypatch.setenv("PATH", ".:" + os.environ["PATH"])
exe_path = Path(str(tmpdir), "ffmpeg")
exe_path.write_text("#!/bin/sh\n"
"[[ $@ -eq 0 ]]\n")
os.chmod(str(exe_path), 0o755)
with pytest.raises(subprocess.CalledProcessError):
anim.save("test.mpeg")
@pytest.mark.parametrize("cache_frame_data", [False, True])
def test_funcanimation_cache_frame_data(cache_frame_data):
fig, ax = plt.subplots()
line, = ax.plot([], [])
class Frame(dict):
# this subclassing enables to use weakref.ref()
pass
def init():
line.set_data([], [])
return line,
def animate(frame):
line.set_data(frame['x'], frame['y'])
return line,
frames_generated = []
def frames_generator():
for _ in range(5):
x = np.linspace(0, 10, 100)
y = np.random.rand(100)
frame = Frame(x=x, y=y)
# collect weak references to frames
# to validate their references later
frames_generated.append(weakref.ref(frame))
yield frame
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=frames_generator,
cache_frame_data=cache_frame_data)
writer = NullMovieWriter()
anim.save('unused.null', writer=writer)
assert len(frames_generated) == 5
for f in frames_generated:
# If cache_frame_data is True, then the weakref should be alive;
# if cache_frame_data is False, then the weakref should be dead (None).
assert (f() is None) != cache_frame_data
@pytest.mark.parametrize('return_value', [
# User forgot to return (returns None).
None,
# User returned a string.
'string',
# User returned an int.
1,
# User returns a sequence of other objects, e.g., string instead of Artist.
('string', ),
# User forgot to return a sequence (handled in `animate` below.)
'artist',
])
def test_draw_frame(return_value):
# test _draw_frame method
fig, ax = plt.subplots()
line, = ax.plot([])
def animate(i):
# general update func
line.set_data([0, 1], [0, i])
if return_value == 'artist':
# *not* a sequence
return line
else:
return return_value
with pytest.raises(RuntimeError):
animation.FuncAnimation(fig, animate, blit=True)

View File

@@ -0,0 +1,69 @@
import re
import numpy as np
import pytest
from matplotlib import _api
@pytest.mark.parametrize('target,test_shape',
[((None, ), (1, 3)),
((None, 3), (1,)),
((None, 3), (1, 2)),
((1, 5), (1, 9)),
((None, 2, None), (1, 3, 1))
])
def test_check_shape(target, test_shape):
error_pattern = (f"^'aardvark' must be {len(target)}D.*" +
re.escape(f'has shape {test_shape}'))
data = np.zeros(test_shape)
with pytest.raises(ValueError, match=error_pattern):
_api.check_shape(target, aardvark=data)
def test_classproperty_deprecation():
class A:
@_api.deprecated("0.0.0")
@_api.classproperty
def f(cls):
pass
with pytest.warns(_api.MatplotlibDeprecationWarning):
A.f
with pytest.warns(_api.MatplotlibDeprecationWarning):
a = A()
a.f
def test_delete_parameter():
@_api.delete_parameter("3.0", "foo")
def func1(foo=None):
pass
@_api.delete_parameter("3.0", "foo")
def func2(**kwargs):
pass
for func in [func1, func2]:
func() # No warning.
with pytest.warns(_api.MatplotlibDeprecationWarning):
func(foo="bar")
def pyplot_wrapper(foo=_api.deprecation._deprecated_parameter):
func1(foo)
pyplot_wrapper() # No warning.
with pytest.warns(_api.MatplotlibDeprecationWarning):
func(foo="bar")
def test_make_keyword_only():
@_api.make_keyword_only("3.0", "arg")
def func(pre, arg, post=None):
pass
func(1, arg=2) # Check that no warning is emitted.
with pytest.warns(_api.MatplotlibDeprecationWarning):
func(1, 2)
with pytest.warns(_api.MatplotlibDeprecationWarning):
func(1, 2, 3)

View File

@@ -0,0 +1,176 @@
import pytest
import platform
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
import matplotlib.patches as mpatches
def draw_arrow(ax, t, r):
ax.annotate('', xy=(0.5, 0.5 + r), xytext=(0.5, 0.5), size=30,
arrowprops=dict(arrowstyle=t,
fc="b", ec='k'))
@image_comparison(['fancyarrow_test_image'])
def test_fancyarrow():
# Added 0 to test division by zero error described in issue 3930
r = [0.4, 0.3, 0.2, 0.1, 0]
t = ["fancy", "simple", mpatches.ArrowStyle.Fancy()]
fig, axs = plt.subplots(len(t), len(r), squeeze=False,
figsize=(8, 4.5), subplot_kw=dict(aspect=1))
for i_r, r1 in enumerate(r):
for i_t, t1 in enumerate(t):
ax = axs[i_t, i_r]
draw_arrow(ax, t1, r1)
ax.tick_params(labelleft=False, labelbottom=False)
@image_comparison(['boxarrow_test_image.png'])
def test_boxarrow():
styles = mpatches.BoxStyle.get_styles()
n = len(styles)
spacing = 1.2
figheight = (n * spacing + .5)
fig = plt.figure(figsize=(4 / 1.5, figheight / 1.5))
fontsize = 0.3 * 72
for i, stylename in enumerate(sorted(styles)):
fig.text(0.5, ((n - i) * spacing - 0.5)/figheight, stylename,
ha="center",
size=fontsize,
transform=fig.transFigure,
bbox=dict(boxstyle=stylename, fc="w", ec="k"))
def __prepare_fancyarrow_dpi_cor_test():
"""
Convenience function that prepares and returns a FancyArrowPatch. It aims
at being used to test that the size of the arrow head does not depend on
the DPI value of the exported picture.
NB: this function *is not* a test in itself!
"""
fig2 = plt.figure("fancyarrow_dpi_cor_test", figsize=(4, 3), dpi=50)
ax = fig2.add_subplot()
ax.set_xlim([0, 1])
ax.set_ylim([0, 1])
ax.add_patch(mpatches.FancyArrowPatch(posA=(0.3, 0.4), posB=(0.8, 0.6),
lw=3, arrowstyle='->',
mutation_scale=100))
return fig2
@image_comparison(['fancyarrow_dpi_cor_100dpi.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.02,
savefig_kwarg=dict(dpi=100))
def test_fancyarrow_dpi_cor_100dpi():
"""
Check the export of a FancyArrowPatch @ 100 DPI. FancyArrowPatch is
instantiated through a dedicated function because another similar test
checks a similar export but with a different DPI value.
Remark: test only a rasterized format.
"""
__prepare_fancyarrow_dpi_cor_test()
@image_comparison(['fancyarrow_dpi_cor_200dpi.png'], remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.02,
savefig_kwarg=dict(dpi=200))
def test_fancyarrow_dpi_cor_200dpi():
"""
As test_fancyarrow_dpi_cor_100dpi, but exports @ 200 DPI. The relative size
of the arrow head should be the same.
"""
__prepare_fancyarrow_dpi_cor_test()
@image_comparison(['fancyarrow_dash.png'], remove_text=True, style='default')
def test_fancyarrow_dash():
fig, ax = plt.subplots()
e = mpatches.FancyArrowPatch((0, 0), (0.5, 0.5),
arrowstyle='-|>',
connectionstyle='angle3,angleA=0,angleB=90',
mutation_scale=10.0,
linewidth=2,
linestyle='dashed',
color='k')
e2 = mpatches.FancyArrowPatch((0, 0), (0.5, 0.5),
arrowstyle='-|>',
connectionstyle='angle3',
mutation_scale=10.0,
linewidth=2,
linestyle='dotted',
color='k')
ax.add_patch(e)
ax.add_patch(e2)
@image_comparison(['arrow_styles.png'], style='mpl20', remove_text=True,
tol=0 if platform.machine() == 'x86_64' else 0.005)
def test_arrow_styles():
styles = mpatches.ArrowStyle.get_styles()
n = len(styles)
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_xlim(0, 1)
ax.set_ylim(-1, n)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
for i, stylename in enumerate(sorted(styles)):
patch = mpatches.FancyArrowPatch((0.1, i), (0.45, i),
arrowstyle=stylename,
mutation_scale=25)
ax.add_patch(patch)
for i, stylename in enumerate([']-[', ']-', '-[', '|-|']):
style = stylename
if stylename[0] != '-':
style += ',angleA=ANGLE'
if stylename[-1] != '-':
style += ',angleB=ANGLE'
for j, angle in enumerate([-30, 60]):
arrowstyle = style.replace('ANGLE', str(angle))
patch = mpatches.FancyArrowPatch((0.55, 2*i + j), (0.9, 2*i + j),
arrowstyle=arrowstyle,
mutation_scale=25)
ax.add_patch(patch)
@image_comparison(['connection_styles.png'], style='mpl20', remove_text=True)
def test_connection_styles():
styles = mpatches.ConnectionStyle.get_styles()
n = len(styles)
fig, ax = plt.subplots(figsize=(6, 10))
ax.set_xlim(0, 1)
ax.set_ylim(-1, n)
for i, stylename in enumerate(sorted(styles)):
patch = mpatches.FancyArrowPatch((0.1, i), (0.8, i + 0.5),
arrowstyle="->",
connectionstyle=stylename,
mutation_scale=25)
ax.add_patch(patch)
def test_invalid_intersection():
conn_style_1 = mpatches.ConnectionStyle.Angle3(angleA=20, angleB=200)
p1 = mpatches.FancyArrowPatch((.2, .2), (.5, .5),
connectionstyle=conn_style_1)
with pytest.raises(ValueError):
plt.gca().add_patch(p1)
conn_style_2 = mpatches.ConnectionStyle.Angle3(angleA=20, angleB=199.9)
p2 = mpatches.FancyArrowPatch((.2, .2), (.5, .5),
connectionstyle=conn_style_2)
plt.gca().add_patch(p2)

View File

@@ -0,0 +1,342 @@
import io
from itertools import chain
import numpy as np
import pytest
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
import matplotlib.path as mpath
import matplotlib.transforms as mtransforms
import matplotlib.collections as mcollections
import matplotlib.artist as martist
from matplotlib.testing.decorators import check_figures_equal, image_comparison
def test_patch_transform_of_none():
# tests the behaviour of patches added to an Axes with various transform
# specifications
ax = plt.axes()
ax.set_xlim([1, 3])
ax.set_ylim([1, 3])
# Draw an ellipse over data coord (2, 2) by specifying device coords.
xy_data = (2, 2)
xy_pix = ax.transData.transform(xy_data)
# Not providing a transform of None puts the ellipse in data coordinates .
e = mpatches.Ellipse(xy_data, width=1, height=1, fc='yellow', alpha=0.5)
ax.add_patch(e)
assert e._transform == ax.transData
# Providing a transform of None puts the ellipse in device coordinates.
e = mpatches.Ellipse(xy_pix, width=120, height=120, fc='coral',
transform=None, alpha=0.5)
assert e.is_transform_set()
ax.add_patch(e)
assert isinstance(e._transform, mtransforms.IdentityTransform)
# Providing an IdentityTransform puts the ellipse in device coordinates.
e = mpatches.Ellipse(xy_pix, width=100, height=100,
transform=mtransforms.IdentityTransform(), alpha=0.5)
ax.add_patch(e)
assert isinstance(e._transform, mtransforms.IdentityTransform)
# Not providing a transform, and then subsequently "get_transform" should
# not mean that "is_transform_set".
e = mpatches.Ellipse(xy_pix, width=120, height=120, fc='coral',
alpha=0.5)
intermediate_transform = e.get_transform()
assert not e.is_transform_set()
ax.add_patch(e)
assert e.get_transform() != intermediate_transform
assert e.is_transform_set()
assert e._transform == ax.transData
def test_collection_transform_of_none():
# tests the behaviour of collections added to an Axes with various
# transform specifications
ax = plt.axes()
ax.set_xlim([1, 3])
ax.set_ylim([1, 3])
# draw an ellipse over data coord (2, 2) by specifying device coords
xy_data = (2, 2)
xy_pix = ax.transData.transform(xy_data)
# not providing a transform of None puts the ellipse in data coordinates
e = mpatches.Ellipse(xy_data, width=1, height=1)
c = mcollections.PatchCollection([e], facecolor='yellow', alpha=0.5)
ax.add_collection(c)
# the collection should be in data coordinates
assert c.get_offset_transform() + c.get_transform() == ax.transData
# providing a transform of None puts the ellipse in device coordinates
e = mpatches.Ellipse(xy_pix, width=120, height=120)
c = mcollections.PatchCollection([e], facecolor='coral',
alpha=0.5)
c.set_transform(None)
ax.add_collection(c)
assert isinstance(c.get_transform(), mtransforms.IdentityTransform)
# providing an IdentityTransform puts the ellipse in device coordinates
e = mpatches.Ellipse(xy_pix, width=100, height=100)
c = mcollections.PatchCollection([e],
transform=mtransforms.IdentityTransform(),
alpha=0.5)
ax.add_collection(c)
assert isinstance(c._transOffset, mtransforms.IdentityTransform)
@image_comparison(["clip_path_clipping"], remove_text=True)
def test_clipping():
exterior = mpath.Path.unit_rectangle().deepcopy()
exterior.vertices *= 4
exterior.vertices -= 2
interior = mpath.Path.unit_circle().deepcopy()
interior.vertices = interior.vertices[::-1]
clip_path = mpath.Path.make_compound_path(exterior, interior)
star = mpath.Path.unit_regular_star(6).deepcopy()
star.vertices *= 2.6
fig, (ax1, ax2) = plt.subplots(1, 2, sharex=True, sharey=True)
col = mcollections.PathCollection([star], lw=5, edgecolor='blue',
facecolor='red', alpha=0.7, hatch='*')
col.set_clip_path(clip_path, ax1.transData)
ax1.add_collection(col)
patch = mpatches.PathPatch(star, lw=5, edgecolor='blue', facecolor='red',
alpha=0.7, hatch='*')
patch.set_clip_path(clip_path, ax2.transData)
ax2.add_patch(patch)
ax1.set_xlim([-3, 3])
ax1.set_ylim([-3, 3])
@check_figures_equal(extensions=['png'])
def test_clipping_zoom(fig_test, fig_ref):
# This test places the Axes and sets its limits such that the clip path is
# outside the figure entirely. This should not break the clip path.
ax_test = fig_test.add_axes([0, 0, 1, 1])
l, = ax_test.plot([-3, 3], [-3, 3])
# Explicit Path instead of a Rectangle uses clip path processing, instead
# of a clip box optimization.
p = mpath.Path([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
p = mpatches.PathPatch(p, transform=ax_test.transData)
l.set_clip_path(p)
ax_ref = fig_ref.add_axes([0, 0, 1, 1])
ax_ref.plot([-3, 3], [-3, 3])
ax_ref.set(xlim=(0.5, 0.75), ylim=(0.5, 0.75))
ax_test.set(xlim=(0.5, 0.75), ylim=(0.5, 0.75))
def test_cull_markers():
x = np.random.random(20000)
y = np.random.random(20000)
fig, ax = plt.subplots()
ax.plot(x, y, 'k.')
ax.set_xlim(2, 3)
pdf = io.BytesIO()
fig.savefig(pdf, format="pdf")
assert len(pdf.getvalue()) < 8000
svg = io.BytesIO()
fig.savefig(svg, format="svg")
assert len(svg.getvalue()) < 20000
@image_comparison(['hatching'], remove_text=True, style='default')
def test_hatching():
fig, ax = plt.subplots(1, 1)
# Default hatch color.
rect1 = mpatches.Rectangle((0, 0), 3, 4, hatch='/')
ax.add_patch(rect1)
rect2 = mcollections.RegularPolyCollection(4, sizes=[16000],
offsets=[(1.5, 6.5)],
transOffset=ax.transData,
hatch='/')
ax.add_collection(rect2)
# Ensure edge color is not applied to hatching.
rect3 = mpatches.Rectangle((4, 0), 3, 4, hatch='/', edgecolor='C1')
ax.add_patch(rect3)
rect4 = mcollections.RegularPolyCollection(4, sizes=[16000],
offsets=[(5.5, 6.5)],
transOffset=ax.transData,
hatch='/', edgecolor='C1')
ax.add_collection(rect4)
ax.set_xlim(0, 7)
ax.set_ylim(0, 9)
def test_remove():
fig, ax = plt.subplots()
im = ax.imshow(np.arange(36).reshape(6, 6))
ln, = ax.plot(range(5))
assert fig.stale
assert ax.stale
fig.canvas.draw()
assert not fig.stale
assert not ax.stale
assert not ln.stale
assert im in ax._mouseover_set
assert ln not in ax._mouseover_set
assert im.axes is ax
im.remove()
ln.remove()
for art in [im, ln]:
assert art.axes is None
assert art.figure is None
assert im not in ax._mouseover_set
assert fig.stale
assert ax.stale
@image_comparison(["default_edges.png"], remove_text=True, style='default')
def test_default_edges():
# Remove this line when this test image is regenerated.
plt.rcParams['text.kerning_factor'] = 6
fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2)
ax1.plot(np.arange(10), np.arange(10), 'x',
np.arange(10) + 1, np.arange(10), 'o')
ax2.bar(np.arange(10), np.arange(10), align='edge')
ax3.text(0, 0, "BOX", size=24, bbox=dict(boxstyle='sawtooth'))
ax3.set_xlim((-1, 1))
ax3.set_ylim((-1, 1))
pp1 = mpatches.PathPatch(
mpath.Path([(0, 0), (1, 0), (1, 1), (0, 0)],
[mpath.Path.MOVETO, mpath.Path.CURVE3,
mpath.Path.CURVE3, mpath.Path.CLOSEPOLY]),
fc="none", transform=ax4.transData)
ax4.add_patch(pp1)
def test_properties():
ln = mlines.Line2D([], [])
ln.properties() # Check that no warning is emitted.
def test_setp():
# Check empty list
plt.setp([])
plt.setp([[]])
# Check arbitrary iterables
fig, ax = plt.subplots()
lines1 = ax.plot(range(3))
lines2 = ax.plot(range(3))
martist.setp(chain(lines1, lines2), 'lw', 5)
plt.setp(ax.spines.values(), color='green')
# Check *file* argument
sio = io.StringIO()
plt.setp(lines1, 'zorder', file=sio)
assert sio.getvalue() == ' zorder: float\n'
def test_None_zorder():
fig, ax = plt.subplots()
ln, = ax.plot(range(5), zorder=None)
assert ln.get_zorder() == mlines.Line2D.zorder
ln.set_zorder(123456)
assert ln.get_zorder() == 123456
ln.set_zorder(None)
assert ln.get_zorder() == mlines.Line2D.zorder
@pytest.mark.parametrize('accept_clause, expected', [
('', 'unknown'),
("ACCEPTS: [ '-' | '--' | '-.' ]", "[ '-' | '--' | '-.' ]"),
('ACCEPTS: Some description.', 'Some description.'),
('.. ACCEPTS: Some description.', 'Some description.'),
('arg : int', 'int'),
('*arg : int', 'int'),
('arg : int\nACCEPTS: Something else.', 'Something else. '),
])
def test_artist_inspector_get_valid_values(accept_clause, expected):
class TestArtist(martist.Artist):
def set_f(self, arg):
pass
TestArtist.set_f.__doc__ = """
Some text.
%s
""" % accept_clause
valid_values = martist.ArtistInspector(TestArtist).get_valid_values('f')
assert valid_values == expected
def test_artist_inspector_get_aliases():
# test the correct format and type of get_aliases method
ai = martist.ArtistInspector(mlines.Line2D)
aliases = ai.get_aliases()
assert aliases["linewidth"] == {"lw"}
def test_set_alpha():
art = martist.Artist()
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
art.set_alpha('string')
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
art.set_alpha([1, 2, 3])
with pytest.raises(ValueError, match="outside 0-1 range"):
art.set_alpha(1.1)
with pytest.raises(ValueError, match="outside 0-1 range"):
art.set_alpha(np.nan)
def test_set_alpha_for_array():
art = martist.Artist()
with pytest.raises(TypeError, match='^alpha must be numeric or None'):
art._set_alpha_for_array('string')
with pytest.raises(ValueError, match="outside 0-1 range"):
art._set_alpha_for_array(1.1)
with pytest.raises(ValueError, match="outside 0-1 range"):
art._set_alpha_for_array(np.nan)
with pytest.raises(ValueError, match="alpha must be between 0 and 1"):
art._set_alpha_for_array([0.5, 1.1])
with pytest.raises(ValueError, match="alpha must be between 0 and 1"):
art._set_alpha_for_array([0.5, np.nan])
def test_callbacks():
def func(artist):
func.counter += 1
func.counter = 0
art = martist.Artist()
oid = art.add_callback(func)
assert func.counter == 0
art.pchanged() # must call the callback
assert func.counter == 1
art.set_zorder(10) # setting a property must also call the callback
assert func.counter == 2
art.remove_callback(oid)
art.pchanged() # must not call the callback anymore
assert func.counter == 2

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
import re
from matplotlib.testing import _check_for_pgf
from matplotlib.backend_bases import (
FigureCanvasBase, LocationEvent, MouseButton, MouseEvent,
NavigationToolbar2, RendererBase)
from matplotlib.backend_tools import (ToolZoom, ToolPan, RubberbandBase,
ToolViewsPositions, _views_positions)
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
import matplotlib.path as path
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()
paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
tforms_matrices = [id.rotate(i).get_matrix().copy() for i in range(1, 5)]
offsets = np.arange(20).reshape((10, 2))
facecolors = ['red', 'green']
edgecolors = ['red', 'green']
def check(master_transform, paths, all_transforms,
offsets, facecolors, edgecolors):
rb = RendererBase()
raw_paths = list(rb._iter_collection_raw_paths(
master_transform, paths, all_transforms))
gc = rb.new_gc()
ids = [path_id for xo, yo, path_id, gc0, rgbFace in
rb._iter_collection(
gc, master_transform, all_transforms,
range(len(raw_paths)), offsets,
transforms.AffineDeltaTransform(master_transform),
facecolors, edgecolors, [], [], [False],
[], 'screen')]
uses = rb._iter_collection_uses_per_path(
paths, all_transforms, offsets, facecolors, edgecolors)
if raw_paths:
seen = np.bincount(ids, minlength=len(raw_paths))
assert set(seen).issubset([uses - 1, uses])
check(id, paths, tforms_matrices, offsets, facecolors, edgecolors)
check(id, paths[0:1], tforms_matrices, offsets, facecolors, edgecolors)
check(id, [], tforms_matrices, offsets, facecolors, edgecolors)
check(id, paths, tforms_matrices[0:1], offsets, facecolors, edgecolors)
check(id, paths, [], offsets, facecolors, edgecolors)
for n in range(0, offsets.shape[0]):
check(id, paths, tforms_matrices, offsets[0:n, :],
facecolors, edgecolors)
check(id, paths, tforms_matrices, offsets, [], edgecolors)
check(id, paths, tforms_matrices, offsets, facecolors, [])
check(id, paths, tforms_matrices, offsets, [], [])
check(id, paths, tforms_matrices, offsets, facecolors[0:1], edgecolors)
def test_canvas_ctor():
assert isinstance(FigureCanvasBase().figure, Figure)
def test_get_default_filename():
assert plt.figure().canvas.get_default_filename() == 'image.png'
def test_canvas_change():
fig = plt.figure()
# Replaces fig.canvas
canvas = FigureCanvasBase(fig)
# Should still work.
plt.close(fig)
assert not plt.fignum_exists(fig.number)
@pytest.mark.backend('pdf')
def test_non_gui_warning(monkeypatch):
plt.subplots()
monkeypatch.setenv("DISPLAY", ":999")
with pytest.warns(UserWarning) as rec:
plt.show()
assert len(rec) == 1
assert ('Matplotlib is currently using pdf, which is a non-GUI backend'
in str(rec[0].message))
with pytest.warns(UserWarning) as rec:
plt.gcf().show()
assert len(rec) == 1
assert ('Matplotlib is currently using pdf, which is a non-GUI backend'
in str(rec[0].message))
@pytest.mark.parametrize(
"x, y", [(42, 24), (None, 42), (None, None), (200, 100.01), (205.75, 2.0)])
def test_location_event_position(x, y):
# LocationEvent should cast its x and y arguments to int unless it is None.
fig, ax = plt.subplots()
canvas = FigureCanvasBase(fig)
event = LocationEvent("test_event", canvas, x, y)
if x is None:
assert event.x is None
else:
assert event.x == int(x)
assert isinstance(event.x, int)
if y is None:
assert event.y is None
else:
assert event.y == int(y)
assert isinstance(event.y, int)
if x is not None and y is not None:
assert re.match(
"x={} +y={}".format(ax.format_xdata(x), ax.format_ydata(y)),
ax.format_coord(x, y))
ax.fmt_xdata = ax.fmt_ydata = lambda x: "foo"
assert re.match("x=foo +y=foo", ax.format_coord(x, y))
def test_pick():
fig = plt.figure()
fig.text(.5, .5, "hello", ha="center", va="center", picker=True)
fig.canvas.draw()
picks = []
fig.canvas.mpl_connect("pick_event", lambda event: picks.append(event))
start_event = MouseEvent(
"button_press_event", fig.canvas, *fig.transFigure.transform((.5, .5)),
MouseButton.LEFT)
fig.canvas.callbacks.process(start_event.name, start_event)
assert len(picks) == 1
def test_interactive_zoom():
fig, ax = plt.subplots()
ax.set(xscale="logit")
assert ax.get_navigate_mode() is None
tb = NavigationToolbar2(fig.canvas)
tb.zoom()
assert ax.get_navigate_mode() == 'ZOOM'
xlim0 = ax.get_xlim()
ylim0 = ax.get_ylim()
# Zoom from x=1e-6, y=0.1 to x=1-1e-5, 0.8 (data coordinates, "d").
d0 = (1e-6, 0.1)
d1 = (1-1e-5, 0.8)
# Convert to screen coordinates ("s"). Events are defined only with pixel
# precision, so round the pixel values, and below, check against the
# corresponding xdata/ydata, which are close but not equal to d0/d1.
s0 = ax.transData.transform(d0).astype(int)
s1 = ax.transData.transform(d1).astype(int)
# Zoom in.
start_event = MouseEvent(
"button_press_event", fig.canvas, *s0, MouseButton.LEFT)
fig.canvas.callbacks.process(start_event.name, start_event)
stop_event = MouseEvent(
"button_release_event", fig.canvas, *s1, MouseButton.LEFT)
fig.canvas.callbacks.process(stop_event.name, stop_event)
assert ax.get_xlim() == (start_event.xdata, stop_event.xdata)
assert ax.get_ylim() == (start_event.ydata, stop_event.ydata)
# Zoom out.
start_event = MouseEvent(
"button_press_event", fig.canvas, *s1, MouseButton.RIGHT)
fig.canvas.callbacks.process(start_event.name, start_event)
stop_event = MouseEvent(
"button_release_event", fig.canvas, *s0, MouseButton.RIGHT)
fig.canvas.callbacks.process(stop_event.name, stop_event)
# Absolute tolerance much less than original xmin (1e-7).
assert ax.get_xlim() == pytest.approx(xlim0, rel=0, abs=1e-10)
assert ax.get_ylim() == pytest.approx(ylim0, rel=0, abs=1e-10)
tb.zoom()
assert ax.get_navigate_mode() is None
assert not ax.get_autoscalex_on() and not ax.get_autoscaley_on()
def test_toolbar_zoompan():
expected_warning_regex = (
r"Treat the new Tool classes introduced in "
r"v[0-9]*.[0-9]* as experimental for now; "
"the API and rcParam may change in future versions.")
with pytest.warns(UserWarning, match=expected_warning_regex):
plt.rcParams['toolbar'] = 'toolmanager'
ax = plt.gca()
assert ax.get_navigate_mode() is None
ax.figure.canvas.manager.toolmanager.add_tool(name="zoom",
tool=ToolZoom)
ax.figure.canvas.manager.toolmanager.add_tool(name="pan",
tool=ToolPan)
ax.figure.canvas.manager.toolmanager.add_tool(name=_views_positions,
tool=ToolViewsPositions)
ax.figure.canvas.manager.toolmanager.add_tool(name='rubberband',
tool=RubberbandBase)
ax.figure.canvas.manager.toolmanager.trigger_tool('zoom')
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)

View File

@@ -0,0 +1,48 @@
import numpy as np
import pytest
from matplotlib.testing.decorators import check_figures_equal
from matplotlib import (
collections as mcollections, patches as mpatches, path as mpath)
@pytest.mark.backend('cairo')
@check_figures_equal(extensions=["png"])
def test_patch_alpha_coloring(fig_test, fig_ref):
"""
Test checks that the patch and collection are rendered with the specified
alpha values in their facecolor and edgecolor.
"""
star = mpath.Path.unit_regular_star(6)
circle = mpath.Path.unit_circle()
# concatenate the star with an internal cutout of the circle
verts = np.concatenate([circle.vertices, star.vertices[::-1]])
codes = np.concatenate([circle.codes, star.codes])
cut_star1 = mpath.Path(verts, codes)
cut_star2 = mpath.Path(verts + 1, codes)
# Reference: two separate patches
ax = fig_ref.subplots()
ax.set_xlim([-1, 2])
ax.set_ylim([-1, 2])
patch = mpatches.PathPatch(cut_star1,
linewidth=5, linestyle='dashdot',
facecolor=(1, 0, 0, 0.5),
edgecolor=(0, 0, 1, 0.75))
ax.add_patch(patch)
patch = mpatches.PathPatch(cut_star2,
linewidth=5, linestyle='dashdot',
facecolor=(1, 0, 0, 0.5),
edgecolor=(0, 0, 1, 0.75))
ax.add_patch(patch)
# Test: path collection
ax = fig_test.subplots()
ax.set_xlim([-1, 2])
ax.set_ylim([-1, 2])
col = mcollections.PathCollection([cut_star1, cut_star2],
linewidth=5, linestyles='dashdot',
facecolor=(1, 0, 0, 0.5),
edgecolor=(0, 0, 1, 0.75))
ax.add_collection(col)

View File

@@ -0,0 +1,51 @@
from matplotlib import pyplot as plt
import pytest
pytest.importorskip("matplotlib.backends.backend_gtk3agg")
@pytest.mark.backend("gtk3agg")
def test_correct_key():
pytest.xfail("test_widget_send_event is not triggering key_press_event")
from gi.repository import Gdk, Gtk
fig = plt.figure()
buf = []
def send(event):
for key, mod in [
(Gdk.KEY_a, Gdk.ModifierType.SHIFT_MASK),
(Gdk.KEY_a, 0),
(Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK),
(Gdk.KEY_agrave, 0),
(Gdk.KEY_Control_L, Gdk.ModifierType.MOD1_MASK),
(Gdk.KEY_Alt_L, Gdk.ModifierType.CONTROL_MASK),
(Gdk.KEY_agrave,
Gdk.ModifierType.CONTROL_MASK
| Gdk.ModifierType.MOD1_MASK
| Gdk.ModifierType.MOD4_MASK),
(0xfd16, 0), # KEY_3270_Play.
(Gdk.KEY_BackSpace, 0),
(Gdk.KEY_BackSpace, Gdk.ModifierType.CONTROL_MASK),
]:
# This is not actually really the right API: it depends on the
# actual keymap (e.g. on Azerty, shift+agrave -> 0).
Gtk.test_widget_send_key(fig.canvas, key, mod)
def receive(event):
buf.append(event.key)
if buf == [
"A", "a", "ctrl+a",
"\N{LATIN SMALL LETTER A WITH GRAVE}",
"alt+control", "ctrl+alt",
"ctrl+alt+super+\N{LATIN SMALL LETTER A WITH GRAVE}",
# (No entry for KEY_3270_Play.)
"backspace", "ctrl+backspace",
]:
plt.close(fig)
fig.canvas.mpl_connect("draw_event", send)
fig.canvas.mpl_connect("key_press_event", receive)
plt.show()

View File

@@ -0,0 +1,28 @@
import os
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory
import pytest
nbformat = pytest.importorskip('nbformat')
# From https://blog.thedataincubator.com/2016/06/testing-jupyter-notebooks/
def test_ipynb():
nb_path = Path(__file__).parent / 'test_nbagg_01.ipynb'
with TemporaryDirectory() as tmpdir:
out_path = Path(tmpdir, "out.ipynb")
subprocess.check_call(
["jupyter", "nbconvert", "--to", "notebook",
"--execute", "--ExecutePreprocessor.timeout=500",
"--output", str(out_path), str(nb_path)],
env={**os.environ, "IPYTHONDIR": tmpdir})
with out_path.open() as out:
nb = nbformat.read(out, nbformat.current_nbformat)
errors = [output for cell in nb.cells for output in cell.get("outputs", [])
if output.output_type == "error"]
assert not errors

View File

@@ -0,0 +1,341 @@
import datetime
import decimal
import io
import os
from pathlib import Path
from tempfile import NamedTemporaryFile
import numpy as np
import pytest
import matplotlib as mpl
from matplotlib import dviread, pyplot as plt, checkdep_usetex, rcParams
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.testing.decorators import check_figures_equal, image_comparison
needs_usetex = pytest.mark.skipif(
not checkdep_usetex(True),
reason="This test needs a TeX installation")
@image_comparison(['pdf_use14corefonts.pdf'])
def test_use14corefonts():
rcParams['pdf.use14corefonts'] = True
rcParams['font.family'] = 'sans-serif'
rcParams['font.size'] = 8
rcParams['font.sans-serif'] = ['Helvetica']
rcParams['pdf.compression'] = 0
text = '''A three-line text positioned just above a blue line
and containing some French characters and the euro symbol:
"Merci pépé pour les 10 €"'''
fig, ax = plt.subplots()
ax.set_title('Test PDF backend with option use14corefonts=True')
ax.text(0.5, 0.5, text, horizontalalignment='center',
verticalalignment='bottom',
fontsize=14)
ax.axhline(0.5, linewidth=0.5)
def test_type42():
rcParams['pdf.fonttype'] = 42
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
fig.savefig(io.BytesIO())
def test_multipage_pagecount():
with PdfPages(io.BytesIO()) as pdf:
assert pdf.get_pagecount() == 0
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
fig.savefig(pdf, format="pdf")
assert pdf.get_pagecount() == 1
pdf.savefig()
assert pdf.get_pagecount() == 2
def test_multipage_properfinalize():
pdfio = io.BytesIO()
with PdfPages(pdfio) as pdf:
for i in range(10):
fig, ax = plt.subplots()
ax.set_title('This is a long title')
fig.savefig(pdf, format="pdf")
s = pdfio.getvalue()
assert s.count(b'startxref') == 1
assert len(s) < 40000
def test_multipage_keep_empty():
# test empty pdf files
# test that an empty pdf is left behind with keep_empty=True (default)
with NamedTemporaryFile(delete=False) as tmp:
with PdfPages(tmp) as pdf:
filename = pdf._file.fh.name
assert os.path.exists(filename)
os.remove(filename)
# test if an empty pdf is deleting itself afterwards with keep_empty=False
with PdfPages(filename, keep_empty=False) as pdf:
pass
assert not os.path.exists(filename)
# test pdf files with content, they should never be deleted
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
# test that a non-empty pdf is left behind with keep_empty=True (default)
with NamedTemporaryFile(delete=False) as tmp:
with PdfPages(tmp) as pdf:
filename = pdf._file.fh.name
pdf.savefig()
assert os.path.exists(filename)
os.remove(filename)
# test that a non-empty pdf is left behind with keep_empty=False
with NamedTemporaryFile(delete=False) as tmp:
with PdfPages(tmp, keep_empty=False) as pdf:
filename = pdf._file.fh.name
pdf.savefig()
assert os.path.exists(filename)
os.remove(filename)
def test_composite_image():
# Test that figures can be saved with and without combining multiple images
# (on a single set of axes) into a single composite image.
X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1))
Z = np.sin(Y ** 2)
fig, ax = plt.subplots()
ax.set_xlim(0, 3)
ax.imshow(Z, extent=[0, 1, 0, 1])
ax.imshow(Z[::-1], extent=[2, 3, 0, 1])
plt.rcParams['image.composite_image'] = True
with PdfPages(io.BytesIO()) as pdf:
fig.savefig(pdf, format="pdf")
assert len(pdf._file._images) == 1
plt.rcParams['image.composite_image'] = False
with PdfPages(io.BytesIO()) as pdf:
fig.savefig(pdf, format="pdf")
assert len(pdf._file._images) == 2
def test_savefig_metadata(monkeypatch):
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
buf = io.BytesIO()
fig.savefig(buf, metadata=md, format='pdf')
with pikepdf.Pdf.open(buf) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Producer': f'Matplotlib pdf backend v{mpl.__version__}',
'/Subject': 'Test page',
'/Title': 'Multipage PDF',
'/Trapped': '/True',
}
def test_invalid_metadata():
fig, ax = plt.subplots()
with pytest.warns(UserWarning,
match="Unknown infodict keyword: 'foobar'."):
fig.savefig(io.BytesIO(), format='pdf', metadata={'foobar': 'invalid'})
with pytest.warns(UserWarning,
match='not an instance of datetime.datetime.'):
fig.savefig(io.BytesIO(), format='pdf',
metadata={'ModDate': '1968-08-01'})
with pytest.warns(UserWarning,
match='not one of {"True", "False", "Unknown"}'):
fig.savefig(io.BytesIO(), format='pdf', metadata={'Trapped': 'foo'})
with pytest.warns(UserWarning, match='not an instance of str.'):
fig.savefig(io.BytesIO(), format='pdf', metadata={'Title': 1234})
def test_multipage_metadata(monkeypatch):
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
buf = io.BytesIO()
with PdfPages(buf, metadata=md) as pdf:
pdf.savefig(fig)
pdf.savefig(fig)
with pikepdf.Pdf.open(buf) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Producer': f'Matplotlib pdf backend v{mpl.__version__}',
'/Subject': 'Test page',
'/Title': 'Multipage PDF',
'/Trapped': '/True',
}
def test_text_urls():
pikepdf = pytest.importorskip('pikepdf')
test_url = 'https://test_text_urls.matplotlib.org/'
fig = plt.figure(figsize=(2, 1))
fig.text(0.1, 0.1, 'test plain 123', url=f'{test_url}plain')
fig.text(0.1, 0.4, 'test mathtext $123$', url=f'{test_url}mathtext')
with io.BytesIO() as fd:
fig.savefig(fd, format='pdf')
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots
# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
for y, fragment in [('0.1', 'plain'), ('0.4', 'mathtext')]:
annot = next(
(a for a in annots if a.A.URI == f'{test_url}{fragment}'),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect[1] == decimal.Decimal(y) * 72
@needs_usetex
def test_text_urls_tex():
pikepdf = pytest.importorskip('pikepdf')
test_url = 'https://test_text_urls.matplotlib.org/'
fig = plt.figure(figsize=(2, 1))
fig.text(0.1, 0.7, 'test tex $123$', usetex=True, url=f'{test_url}tex')
with io.BytesIO() as fd:
fig.savefig(fd, format='pdf')
with pikepdf.Pdf.open(fd) as pdf:
annots = pdf.pages[0].Annots
# Iteration over Annots must occur within the context manager,
# otherwise it may fail depending on the pdf structure.
annot = next(
(a for a in annots if a.A.URI == f'{test_url}tex'),
None)
assert annot is not None
# Positions in points (72 per inch.)
assert annot.Rect[1] == decimal.Decimal('0.7') * 72
def test_pdfpages_fspath():
with PdfPages(Path(os.devnull)) as pdf:
pdf.savefig(plt.figure())
@image_comparison(['hatching_legend.pdf'])
def test_hatching_legend():
"""Test for correct hatching on patches in legend"""
fig = plt.figure(figsize=(1, 2))
a = plt.Rectangle([0, 0], 0, 0, facecolor="green", hatch="XXXX")
b = plt.Rectangle([0, 0], 0, 0, facecolor="blue", hatch="XXXX")
fig.legend([a, b, a, b], ["", "", "", ""])
@image_comparison(['grayscale_alpha.pdf'])
def test_grayscale_alpha():
"""Masking images with NaN did not work for grayscale images"""
x, y = np.ogrid[-2:2:.1, -2:2:.1]
dd = np.exp(-(x**2 + y**2))
dd[dd < .1] = np.nan
fig, ax = plt.subplots()
ax.imshow(dd, interpolation='none', cmap='gray_r')
ax.set_xticks([])
ax.set_yticks([])
# This tests tends to hit a TeX cache lock on AppVeyor.
@pytest.mark.flaky(reruns=3)
@needs_usetex
def test_missing_psfont(monkeypatch):
"""An error is raised if a TeX font lacks a Type-1 equivalent"""
def psfont(*args, **kwargs):
return dviread.PsFont(texname='texfont', psname='Some Font',
effects=None, encoding=None, filename=None)
monkeypatch.setattr(dviread.PsfontsMap, '__getitem__', psfont)
rcParams['text.usetex'] = True
fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'hello')
with NamedTemporaryFile() as tmpfile, pytest.raises(ValueError):
fig.savefig(tmpfile, format='pdf')
@pytest.mark.style('default')
@check_figures_equal(extensions=["pdf", "eps"])
def test_pdf_eps_savefig_when_color_is_none(fig_test, fig_ref):
ax_test = fig_test.add_subplot()
ax_test.set_axis_off()
ax_test.plot(np.sin(np.linspace(-5, 5, 100)), "v", c="none")
ax_ref = fig_ref.add_subplot()
ax_ref.set_axis_off()
@needs_usetex
def test_failing_latex():
"""Test failing latex subprocess call"""
plt.xlabel("$22_2_2$", usetex=True) # This fails with "Double subscript"
with pytest.raises(RuntimeError):
plt.savefig(io.BytesIO(), format="pdf")
def test_empty_rasterized():
# Check that empty figures that are rasterised save to pdf files fine
fig, ax = plt.subplots()
ax.plot([], [], rasterized=True)
fig.savefig(io.BytesIO(), format="pdf")
@image_comparison(['kerning.pdf'])
def test_kerning():
fig = plt.figure()
s = "AVAVAVAVAVAVAVAV€AAVV"
fig.text(0, .25, s, size=5)
fig.text(0, .75, s, size=20)

View File

@@ -0,0 +1,322 @@
import datetime
from io import BytesIO
import os
import shutil
import numpy as np
import pytest
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.testing import _has_tex_package, _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,
check_figures_equal,
image_comparison)
baseline_dir, result_dir = _image_directories(lambda: 'dummy func')
needs_xelatex = pytest.mark.skipif(not _check_for_pgf('xelatex'),
reason='xelatex + pgf is required')
needs_pdflatex = pytest.mark.skipif(not _check_for_pgf('pdflatex'),
reason='pdflatex + pgf is required')
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,
reason="This test needs a ghostscript installation")
def compare_figure(fname, savefig_kwargs={}, tol=0):
actual = os.path.join(result_dir, fname)
plt.savefig(actual, **savefig_kwargs)
expected = os.path.join(result_dir, "expected_%s" % fname)
shutil.copyfile(os.path.join(baseline_dir, fname), expected)
err = compare_images(expected, actual, tol=tol)
if err:
raise ImageComparisonFailure(err)
def create_figure():
plt.figure()
x = np.linspace(0, 1, 15)
# line plot
plt.plot(x, x ** 2, "b-")
# marker
plt.plot(x, 1 - x**2, "g>")
# filled paths and patterns
plt.fill_between([0., .4], [.4, 0.], hatch='//', facecolor="lightgray",
edgecolor="red")
plt.fill([3, 3, .8, .8, 3], [2, -2, -2, 0, 2], "b")
# text and typesetting
plt.plot([0.9], [0.5], "ro", markersize=3)
plt.text(0.9, 0.5, 'unicode (ü, °, µ) and math ($\\mu_i = x_i^2$)',
ha='right', fontsize=20)
plt.ylabel('sans-serif, blue, $\\frac{\\sqrt{x}}{y^2}$..',
family='sans-serif', color='blue')
plt.xlim(0, 1)
plt.ylim(0, 1)
@pytest.mark.parametrize('plain_text, escaped_text', [
(r'quad_sum: $\sum x_i^2$', r'quad\_sum: \(\displaystyle \sum x_i^2\)'),
(r'no \$splits \$ here', r'no \$splits \$ here'),
('with_underscores', r'with\_underscores'),
('% not a comment', r'\% not a comment'),
('^not', r'\^not'),
])
def test_common_texification(plain_text, escaped_text):
assert common_texification(plain_text) == escaped_text
# test compiling a figure to pdf with xelatex
@needs_xelatex
@pytest.mark.backend('pgf')
@image_comparison(['pgf_xelatex.pdf'], style='default')
def test_xelatex():
rc_xelatex = {'font.family': 'serif',
'pgf.rcfonts': False}
mpl.rcParams.update(rc_xelatex)
create_figure()
# test compiling a figure to pdf with pdflatex
@needs_pdflatex
@pytest.mark.skipif(not _has_tex_package('ucs'), reason='needs ucs.sty')
@pytest.mark.backend('pgf')
@image_comparison(['pgf_pdflatex.pdf'], style='default')
def test_pdflatex():
if os.environ.get('APPVEYOR'):
pytest.xfail("pdflatex test does not work on appveyor due to missing "
"LaTeX fonts")
rc_pdflatex = {'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
'pgf.preamble': ('\\usepackage[utf8x]{inputenc}'
'\\usepackage[T1]{fontenc}')}
mpl.rcParams.update(rc_pdflatex)
create_figure()
# test updating the rc parameters for each figure
@needs_xelatex
@needs_pdflatex
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
def test_rcupdate():
rc_sets = [{'font.family': 'sans-serif',
'font.size': 30,
'figure.subplot.left': .2,
'lines.markersize': 10,
'pgf.rcfonts': False,
'pgf.texsystem': 'xelatex'},
{'font.family': 'monospace',
'font.size': 10,
'figure.subplot.left': .1,
'lines.markersize': 20,
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
'pgf.preamble': ('\\usepackage[utf8x]{inputenc}'
'\\usepackage[T1]{fontenc}'
'\\usepackage{sfmath}')}]
tol = [6, 0]
for i, rc_set in enumerate(rc_sets):
with mpl.rc_context(rc_set):
for substring, pkg in [('sfmath', 'sfmath'), ('utf8x', 'ucs')]:
if (substring in mpl.rcParams['pgf.preamble']
and not _has_tex_package(pkg)):
pytest.skip(f'needs {pkg}.sty')
create_figure()
compare_figure('pgf_rcupdate%d.pdf' % (i + 1), tol=tol[i])
# test backend-side clipping, since large numbers are not supported by TeX
@needs_xelatex
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
def test_pathclip():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
plt.plot([0., 1e100], [0., 1e100])
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.savefig(BytesIO(), format="pdf") # No image comparison.
# test mixed mode rendering
@needs_xelatex
@pytest.mark.backend('pgf')
@image_comparison(['pgf_mixedmode.pdf'], style='default')
def test_mixedmode():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
Y, X = np.ogrid[-1:1:40j, -1:1:40j]
plt.pcolor(X**2 + Y**2).set_rasterized(True)
# test bbox_inches clipping
@needs_xelatex
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
def test_bbox_inches():
mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False})
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.plot(range(5))
ax2.plot(range(5))
plt.tight_layout()
bbox = ax1.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
compare_figure('pgf_bbox_inches.pdf', savefig_kwargs={'bbox_inches': bbox},
tol=0)
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
@pytest.mark.parametrize('system', [
pytest.param('lualatex', marks=[needs_lualatex]),
pytest.param('pdflatex', marks=[needs_pdflatex]),
pytest.param('xelatex', marks=[needs_xelatex]),
])
def test_pdf_pages(system):
rc_pdflatex = {
'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': system,
}
mpl.rcParams.update(rc_pdflatex)
fig1, ax1 = plt.subplots()
ax1.plot(range(5))
fig1.tight_layout()
fig2, ax2 = plt.subplots(figsize=(3, 2))
ax2.plot(range(5))
fig2.tight_layout()
path = os.path.join(result_dir, f'pdfpages_{system}.pdf')
md = {
'Author': 'me',
'Title': 'Multipage PDF with pgf',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'Unknown'
}
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig1)
pdf.savefig(fig2)
pdf.savefig(fig1)
assert pdf.get_pagecount() == 3
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
@pytest.mark.parametrize('system', [
pytest.param('lualatex', marks=[needs_lualatex]),
pytest.param('pdflatex', marks=[needs_pdflatex]),
pytest.param('xelatex', marks=[needs_xelatex]),
])
def test_pdf_pages_metadata_check(monkeypatch, system):
# Basically the same as test_pdf_pages, but we keep it separate to leave
# pikepdf as an optional dependency.
pikepdf = pytest.importorskip('pikepdf')
monkeypatch.setenv('SOURCE_DATE_EPOCH', '0')
mpl.rcParams.update({'pgf.texsystem': system})
fig, ax = plt.subplots()
ax.plot(range(5))
md = {
'Author': 'me',
'Title': 'Multipage PDF with pgf',
'Subject': 'Test page',
'Keywords': 'test,pdf,multipage',
'ModDate': datetime.datetime(
1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))),
'Trapped': 'True'
}
path = os.path.join(result_dir, f'pdfpages_meta_check_{system}.pdf')
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig)
with pikepdf.Pdf.open(path) as pdf:
info = {k: str(v) for k, v in pdf.docinfo.items()}
# Not set by us, so don't bother checking.
if '/PTEX.FullBanner' in info:
del info['/PTEX.FullBanner']
if '/PTEX.Fullbanner' in info:
del info['/PTEX.Fullbanner']
# Some LaTeX engines ignore this setting, and state themselves as producer.
producer = info.pop('/Producer')
assert producer == f'Matplotlib pgf backend v{mpl.__version__}' or (
system == 'lualatex' and 'LuaTeX' in producer)
assert info == {
'/Author': 'me',
'/CreationDate': 'D:19700101000000Z',
'/Creator': f'Matplotlib v{mpl.__version__}, https://matplotlib.org',
'/Keywords': 'test,pdf,multipage',
'/ModDate': 'D:19680801000000Z',
'/Subject': 'Test page',
'/Title': 'Multipage PDF with pgf',
'/Trapped': '/True',
}
@needs_xelatex
def test_tex_restart_after_error():
fig = plt.figure()
fig.suptitle(r"\oops")
with pytest.raises(ValueError):
fig.savefig(BytesIO(), format="pgf")
fig = plt.figure() # start from scratch
fig.suptitle(r"this is ok")
fig.savefig(BytesIO(), format="pgf")
@needs_xelatex
def test_bbox_inches_tight():
fig, ax = plt.subplots()
ax.imshow([[0, 1], [2, 3]])
fig.savefig(BytesIO(), format="pdf", backend="pgf", bbox_inches="tight")
@needs_xelatex
@needs_ghostscript
def test_png():
# Just a smoketest.
fig, ax = plt.subplots()
fig.savefig(BytesIO(), format="png", backend="pgf")
@needs_xelatex
def test_unknown_font(caplog):
with caplog.at_level("WARNING"):
mpl.rcParams["font.family"] = "this-font-does-not-exist"
plt.figtext(.5, .5, "hello, world")
plt.savefig(BytesIO(), format="pgf")
assert "Ignoring unknown font: this-font-does-not-exist" in [
r.getMessage() for r in caplog.records]
@check_figures_equal(extensions=["pdf"])
@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_pgf(texsystem):
pytest.skip(texsystem + ' + pgf is required')
mpl.rcParams["pgf.texsystem"] = texsystem
fig_test.text(.5, .5, "$-1$")
fig_ref.text(.5, .5, "$\N{MINUS SIGN}1$")

View File

@@ -0,0 +1,195 @@
import io
from pathlib import Path
import re
import tempfile
import pytest
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib import cbook, patheffects
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.cbook import MatplotlibDeprecationWarning
needs_ghostscript = pytest.mark.skipif(
"eps" not in mpl.testing.compare.converter,
reason="This test needs a ghostscript installation")
needs_usetex = pytest.mark.skipif(
not mpl.checkdep_usetex(True),
reason="This test needs a TeX installation")
# This tests tends to hit a TeX cache lock on AppVeyor.
@pytest.mark.flaky(reruns=3)
@pytest.mark.parametrize('orientation', ['portrait', 'landscape'])
@pytest.mark.parametrize('format, use_log, rcParams', [
('ps', False, {}),
('ps', False, {'ps.usedistiller': 'ghostscript'}),
('ps', False, {'ps.usedistiller': 'xpdf'}),
('ps', False, {'text.usetex': True}),
('eps', False, {}),
('eps', True, {'ps.useafm': True}),
('eps', False, {'text.usetex': True}),
], ids=[
'ps',
'ps with distiller=ghostscript',
'ps with distiller=xpdf',
'ps with usetex',
'eps',
'eps afm',
'eps with usetex'
])
def test_savefig_to_stringio(format, use_log, rcParams, orientation):
mpl.rcParams.update(rcParams)
fig, ax = plt.subplots()
with io.StringIO() as s_buf, io.BytesIO() as b_buf:
if use_log:
ax.set_yscale('log')
ax.plot([1, 2], [1, 2])
title = "Déjà vu"
if not mpl.rcParams["text.usetex"]:
title += " \N{MINUS SIGN}\N{EURO SIGN}"
ax.set_title(title)
allowable_exceptions = []
if rcParams.get("ps.usedistiller"):
allowable_exceptions.append(mpl.ExecutableNotFoundError)
if rcParams.get("text.usetex"):
allowable_exceptions.append(RuntimeError)
if rcParams.get("ps.useafm"):
allowable_exceptions.append(MatplotlibDeprecationWarning)
try:
fig.savefig(s_buf, format=format, orientation=orientation)
fig.savefig(b_buf, format=format, orientation=orientation)
except tuple(allowable_exceptions) as exc:
pytest.skip(str(exc))
s_val = s_buf.getvalue().encode('ascii')
b_val = b_buf.getvalue()
# Strip out CreationDate: ghostscript and cairo don't obey
# SOURCE_DATE_EPOCH, and that environment variable is already tested in
# test_determinism.
s_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", s_val)
b_val = re.sub(b"(?<=\n%%CreationDate: ).*", b"", b_val)
assert s_val == b_val.replace(b'\r\n', b'\n')
def test_patheffects():
mpl.rcParams['path.effects'] = [
patheffects.withStroke(linewidth=4, foreground='w')]
fig, ax = plt.subplots()
ax.plot([1, 2, 3])
with io.BytesIO() as ps:
fig.savefig(ps, format='ps')
@needs_usetex
@needs_ghostscript
def test_tilde_in_tempfilename(tmpdir):
# Tilde ~ in the tempdir path (e.g. TMPDIR, TMP or TEMP on windows
# when the username is very long and windows uses a short name) breaks
# latex before https://github.com/matplotlib/matplotlib/pull/5928
base_tempdir = Path(tmpdir, "short-1")
base_tempdir.mkdir()
# Change the path for new tempdirs, which is used internally by the ps
# backend to write a file.
with cbook._setattr_cm(tempfile, tempdir=str(base_tempdir)):
# usetex results in the latex call, which does not like the ~
mpl.rcParams['text.usetex'] = True
plt.plot([1, 2, 3, 4])
plt.xlabel(r'\textbf{time} (s)')
# use the PS backend to write the file...
plt.savefig(base_tempdir / 'tex_demo.eps', format="ps")
@image_comparison(["empty.eps"])
def test_transparency():
fig, ax = plt.subplots()
ax.set_axis_off()
ax.plot([0, 1], color="r", alpha=0)
ax.text(.5, .5, "foo", color="r", alpha=0)
def test_bbox():
fig, ax = plt.subplots()
with io.BytesIO() as buf:
fig.savefig(buf, format='eps')
buf = buf.getvalue()
bb = re.search(b'^%%BoundingBox: (.+) (.+) (.+) (.+)$', buf, re.MULTILINE)
assert bb
hibb = re.search(b'^%%HiResBoundingBox: (.+) (.+) (.+) (.+)$', buf,
re.MULTILINE)
assert hibb
for i in range(1, 5):
# BoundingBox must use integers, and be ceil/floor of the hi res.
assert b'.' not in bb.group(i)
assert int(bb.group(i)) == pytest.approx(float(hibb.group(i)), 1)
@needs_usetex
def test_failing_latex():
"""Test failing latex subprocess call"""
mpl.rcParams['text.usetex'] = True
# This fails with "Double subscript"
plt.xlabel("$22_2_2$")
with pytest.raises(RuntimeError):
plt.savefig(io.BytesIO(), format="ps")
@needs_usetex
def test_partial_usetex(caplog):
caplog.set_level("WARNING")
plt.figtext(.5, .5, "foo", usetex=True)
plt.savefig(io.BytesIO(), format="ps")
assert caplog.records and all("as if usetex=False" in record.getMessage()
for record in caplog.records)
@image_comparison(["useafm.eps"])
def test_useafm():
mpl.rcParams["ps.useafm"] = True
fig, ax = plt.subplots()
ax.set_axis_off()
ax.axhline(.5)
ax.text(.5, .5, "qk")
@image_comparison(["type3.eps"])
def test_type3_font():
plt.figtext(.5, .5, "I/J")
@check_figures_equal(extensions=["eps"])
def test_text_clip(fig_test, fig_ref):
ax = fig_test.add_subplot()
# Fully clipped-out text should not appear.
ax.text(0, 0, "hello", transform=fig_test.transFigure, clip_on=True)
fig_ref.add_subplot()
@needs_ghostscript
def test_d_glyph(tmp_path):
# Ensure that we don't have a procedure defined as /d, which would be
# overwritten by the glyph definition for "d".
fig = plt.figure()
fig.text(.5, .5, "def")
out = tmp_path / "test.eps"
fig.savefig(out)
mpl.testing.compare.convert(out, cache=False) # Should not raise.
@image_comparison(["type42_without_prep.eps"], style='mpl20')
def test_type42_font_without_prep():
# Test whether Type 42 fonts without prep table are properly embedded
mpl.rcParams["ps.fonttype"] = 42
mpl.rcParams["mathtext.fontset"] = "stix"
plt.figtext(0.5, 0.5, "Mass $m$")

View File

@@ -0,0 +1,297 @@
import copy
import signal
from unittest import mock
import matplotlib
from matplotlib import pyplot as plt
from matplotlib._pylab_helpers import Gcf
import pytest
try:
from matplotlib.backends.qt_compat import QtGui
except ImportError:
pytestmark = pytest.mark.skip('No usable Qt5 bindings')
@pytest.fixture
def qt_core(request):
backend, = request.node.get_closest_marker('backend').args
qt_compat = pytest.importorskip('matplotlib.backends.qt_compat')
QtCore = qt_compat.QtCore
if backend == 'Qt4Agg':
try:
py_qt_ver = int(QtCore.PYQT_VERSION_STR.split('.')[0])
except AttributeError:
py_qt_ver = QtCore.__version_info__[0]
if py_qt_ver != 4:
pytest.skip('Qt4 is not available')
return QtCore
@pytest.mark.parametrize('backend', [
# Note: the value is irrelevant; the important part is the marker.
pytest.param(
'Qt4Agg',
marks=pytest.mark.backend('Qt4Agg', skip_on_importerror=True)),
pytest.param(
'Qt5Agg',
marks=pytest.mark.backend('Qt5Agg', skip_on_importerror=True)),
])
def test_fig_close(backend):
# save the state of Gcf.figs
init_figs = copy.copy(Gcf.figs)
# make a figure using pyplot interface
fig = plt.figure()
# simulate user clicking the close button by reaching in
# and calling close on the underlying Qt object
fig.canvas.manager.window.close()
# assert that we have removed the reference to the FigureManager
# that got added by plt.figure()
assert init_figs == Gcf.figs
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_fig_signals(qt_core):
# Create a figure
plt.figure()
# Access signals
event_loop_signal = None
# Callback to fire during event loop: save SIGINT handler, then exit
def fire_signal_and_quit():
# Save event loop signal
nonlocal event_loop_signal
event_loop_signal = signal.getsignal(signal.SIGINT)
# Request event loop exit
qt_core.QCoreApplication.exit()
# Timer to exit event loop
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
# Save original SIGINT handler
original_signal = signal.getsignal(signal.SIGINT)
# Use our own SIGINT handler to be 100% sure this is working
def CustomHandler(signum, frame):
pass
signal.signal(signal.SIGINT, CustomHandler)
# mainloop() sets SIGINT, starts Qt event loop (which triggers timer and
# exits) and then mainloop() resets SIGINT
matplotlib.backends.backend_qt5._BackendQT5.mainloop()
# Assert: signal handler during loop execution is signal.SIG_DFL
assert event_loop_signal == signal.SIG_DFL
# Assert: current signal handler is the same as the one we set before
assert CustomHandler == signal.getsignal(signal.SIGINT)
# Reset SIGINT handler to what it was before the test
signal.signal(signal.SIGINT, original_signal)
@pytest.mark.parametrize(
'qt_key, qt_mods, answer',
[
('Key_A', ['ShiftModifier'], 'A'),
('Key_A', [], 'a'),
('Key_A', ['ControlModifier'], 'ctrl+a'),
('Key_Aacute', ['ShiftModifier'],
'\N{LATIN CAPITAL LETTER A WITH ACUTE}'),
('Key_Aacute', [],
'\N{LATIN SMALL LETTER A WITH ACUTE}'),
('Key_Control', ['AltModifier'], 'alt+control'),
('Key_Alt', ['ControlModifier'], 'ctrl+alt'),
('Key_Aacute', ['ControlModifier', 'AltModifier', 'MetaModifier'],
'ctrl+alt+super+\N{LATIN SMALL LETTER A WITH ACUTE}'),
('Key_Play', [], None),
('Key_Backspace', [], 'backspace'),
('Key_Backspace', ['ControlModifier'], 'ctrl+backspace'),
],
ids=[
'shift',
'lower',
'control',
'unicode_upper',
'unicode_lower',
'alt_control',
'control_alt',
'modifier_order',
'non_unicode_key',
'backspace',
'backspace_mod',
]
)
@pytest.mark.parametrize('backend', [
# Note: the value is irrelevant; the important part is the marker.
pytest.param(
'Qt4Agg',
marks=pytest.mark.backend('Qt4Agg', skip_on_importerror=True)),
pytest.param(
'Qt5Agg',
marks=pytest.mark.backend('Qt5Agg', skip_on_importerror=True)),
])
def test_correct_key(backend, qt_core, qt_key, qt_mods, answer):
"""
Make a figure.
Send a key_press_event event (using non-public, qtX backend specific api).
Catch the event.
Assert sent and caught keys are the same.
"""
qt_mod = qt_core.Qt.NoModifier
for mod in qt_mods:
qt_mod |= getattr(qt_core.Qt, mod)
class _Event:
def isAutoRepeat(self): return False
def key(self): return getattr(qt_core.Qt, qt_key)
def modifiers(self): return qt_mod
def on_key_press(event):
assert event.key == answer
qt_canvas = plt.figure().canvas
qt_canvas.mpl_connect('key_press_event', on_key_press)
qt_canvas.keyPressEvent(_Event())
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_pixel_ratio_change():
"""
Make sure that if the pixel ratio changes, the figure dpi changes but the
widget remains the same physical size.
"""
prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT.devicePixelRatioF'
with mock.patch(prop) as p:
p.return_value = 3
fig = plt.figure(figsize=(5, 2), dpi=120)
qt_canvas = fig.canvas
qt_canvas.show()
def set_pixel_ratio(ratio):
p.return_value = ratio
# Make sure the mocking worked
assert qt_canvas._dpi_ratio == ratio
# The value here doesn't matter, as we can't mock the C++ QScreen
# object, but can override the functional wrapper around it.
# Emitting this event is simply to trigger the DPI change handler
# in Matplotlib in the same manner that it would occur normally.
screen.logicalDotsPerInchChanged.emit(96)
qt_canvas.draw()
qt_canvas.flush_events()
qt_canvas.manager.show()
size = qt_canvas.size()
screen = qt_canvas.window().windowHandle().screen()
set_pixel_ratio(3)
# The DPI and the renderer width/height change
assert fig.dpi == 360
assert qt_canvas.renderer.width == 1800
assert qt_canvas.renderer.height == 720
# The actual widget size and figure physical size don't change
assert size.width() == 600
assert size.height() == 240
assert qt_canvas.get_width_height() == (600, 240)
assert (fig.get_size_inches() == (5, 2)).all()
set_pixel_ratio(2)
# The DPI and the renderer width/height change
assert fig.dpi == 240
assert qt_canvas.renderer.width == 1200
assert qt_canvas.renderer.height == 480
# The actual widget size and figure physical size don't change
assert size.width() == 600
assert size.height() == 240
assert qt_canvas.get_width_height() == (600, 240)
assert (fig.get_size_inches() == (5, 2)).all()
set_pixel_ratio(1.5)
# The DPI and the renderer width/height change
assert fig.dpi == 180
assert qt_canvas.renderer.width == 900
assert qt_canvas.renderer.height == 360
# The actual widget size and figure physical size don't change
assert size.width() == 600
assert size.height() == 240
assert qt_canvas.get_width_height() == (600, 240)
assert (fig.get_size_inches() == (5, 2)).all()
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_subplottool():
fig, ax = plt.subplots()
with mock.patch(
"matplotlib.backends.backend_qt5.SubplotToolQt.exec_",
lambda self: None):
fig.canvas.manager.toolbar.configure_subplots()
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_figureoptions():
fig, ax = plt.subplots()
ax.plot([1, 2])
ax.imshow([[1]])
ax.scatter(range(3), range(3), c=range(3))
with mock.patch(
"matplotlib.backends.qt_editor._formlayout.FormDialog.exec_",
lambda self: None):
fig.canvas.manager.toolbar.edit_parameters()
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_double_resize():
# Check that resizing a figure twice keeps the same window size
fig, ax = plt.subplots()
fig.canvas.draw()
window = fig.canvas.manager.window
w, h = 3, 2
fig.set_size_inches(w, h)
assert fig.canvas.width() == w * matplotlib.rcParams['figure.dpi']
assert fig.canvas.height() == h * matplotlib.rcParams['figure.dpi']
old_width = window.width()
old_height = window.height()
fig.set_size_inches(w, h)
assert window.width() == old_width
assert window.height() == old_height
@pytest.mark.backend('Qt5Agg', skip_on_importerror=True)
def test_canvas_reinit():
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
called = False
def crashing_callback(fig, stale):
nonlocal called
fig.canvas.draw_idle()
called = True
fig, ax = plt.subplots()
fig.stale_callback = crashing_callback
# this should not raise
canvas = FigureCanvasQTAgg(fig)
fig.stale = True
assert called

Some files were not shown because too many files have changed in this diff Show More