Initial commit
This commit is contained in:
@@ -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.')
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
from matplotlib.testing.conftest import (mpl_test_settings,
|
||||
mpl_image_comparison_parameters,
|
||||
pytest_configure, pytest_unconfigure,
|
||||
pd)
|
137
.venv/lib/python3.9/site-packages/matplotlib/tests/test_afm.py
Normal file
137
.venv/lib/python3.9/site-packages/matplotlib/tests/test_afm.py
Normal 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
|
246
.venv/lib/python3.9/site-packages/matplotlib/tests/test_agg.py
Normal file
246
.venv/lib/python3.9/site-packages/matplotlib/tests/test_agg.py
Normal 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"
|
@@ -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])
|
@@ -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)
|
@@ -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)
|
@@ -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)
|
@@ -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
|
7089
.venv/lib/python3.9/site-packages/matplotlib/tests/test_axes.py
Normal file
7089
.venv/lib/python3.9/site-packages/matplotlib/tests/test_axes.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
@@ -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)
|
@@ -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()
|
@@ -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
|
@@ -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)
|
@@ -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$")
|
@@ -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$")
|
@@ -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
Reference in New Issue
Block a user