This commit is contained in:
2020-01-13 23:34:26 -07:00
commit 51e97c74a4
3096 changed files with 887666 additions and 0 deletions

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
from io import BytesIO
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"

View File

@@ -0,0 +1,263 @@
import io
import numpy as np
from numpy.testing import assert_array_almost_equal
import pytest
from matplotlib import (
collections, path, pyplot as plt, transforms as mtransforms, rcParams)
from matplotlib.image import imread
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
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])
canvas = FigureCanvas(fig)
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(baseline_images=['agg_filter'],
extensions=['png'], remove_text=True)
def test_agg_filter():
def smooth1d(x, window_len):
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
A1 = np.array([smooth1d(x, window_len) for x in np.asarray(A)])
A2 = np.transpose(A1)
A3 = np.array([smooth1d(x, window_len) for x in A2])
A4 = np.transpose(A3)
return A4
class BaseFilter(object):
def prepare_image(self, src_image, dpi, pad):
ny, nx, depth = src_image.shape
padded_src = np.zeros([pad*2 + ny, pad*2 + nx, depth], dtype="d")
padded_src[pad:-pad, pad:-pad, :] = src_image[:, :, :]
return padded_src # , tgt_image
def get_pad(self, dpi):
return 0
def __call__(self, im, dpi):
pad = self.get_pad(dpi)
padded_src = self.prepare_image(im, dpi, pad)
tgt_image = self.process_image(padded_src, dpi)
return tgt_image, -pad, -pad
class OffsetFilter(BaseFilter):
def __init__(self, offsets=None):
if offsets is None:
self.offsets = (0, 0)
else:
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 gauss filter"
def __init__(self, sigma, alpha=0.5, color=None):
self.sigma = sigma
self.alpha = alpha
if color is None:
self.color = (0, 0, 0)
else:
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.zeros_like(padded_src)
aa = smooth2d(padded_src[:, :, -1]*self.alpha,
self.sigma/72.*dpi)
tgt_image[:, :, -1] = aa
tgt_image[:, :, :-1] = self.color
return tgt_image
class DropShadowFilter(BaseFilter):
def __init__(self, sigma, alpha=0.3, color=None, offsets=None):
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 = plt.figure()
ax = fig.add_subplot(111)
# draw lines
l1, = 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")
l2, = 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 l in [l1, l2]:
# draw shadows with same lines with slight offset.
xx = l.get_xdata()
yy = l.get_ydata()
shadow, = ax.plot(xx, yy)
shadow.update_from(l)
# offset transform
ot = mtransforms.offset_copy(l.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(l.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():
Image = pytest.importorskip("PIL.Image")
# 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():
Image = pytest.importorskip("PIL.Image")
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():
Image = pytest.importorskip("PIL.Image")
from PIL.TiffTags import TAGS_V2 as TAGS
buf = io.BytesIO()
pil_kwargs = {"description": "test image"}
plt.figure().savefig(buf, format="tiff", pil_kwargs=pil_kwargs)
im = Image.open(buf)
tags = {TAGS[k].name: v for k, v in im.tag_v2.items()}
assert tags["ImageDescription"] == "test image"

View File

@@ -0,0 +1,30 @@
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():
ax = plt.axes()
x, y = np.mgrid[0:7, 0:8]
data = x**2 - y**2
mesh = ax.pcolormesh(data, cmap='Reds', zorder=5)
def manual_alpha(im, dpi):
im[:, :, 3] *= 0.6
print('CALLED')
return im, 0, 0
# Note: Doing alpha like this is not the same as setting alpha on
# the mesh itself. Currently meshes are drawn as independent patches,
# and we see fine borders around the blocks of color. See the SO
# question for an example: https://stackoverflow.com/questions/20678817
mesh.set_agg_filter(manual_alpha)
# Currently we must enable rasterization for this to have an effect in
# the PDF backend.
mesh.set_rasterized(True)
ax.plot([0, 4, 7], [1, 3, 8])

View File

@@ -0,0 +1,309 @@
import os
from pathlib import Path
import subprocess
import sys
import weakref
import numpy as np
from pathlib import Path
import pytest
import matplotlib as mpl
from matplotlib import pyplot as plt
from matplotlib import animation
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 make_animation(**kwargs):
fig, ax = plt.subplots()
line, = ax.plot([])
def init():
pass
def animate(i):
line.set_data([0, 1], [0, i])
return line,
return animation.FuncAnimation(fig, animate, **kwargs)
def test_null_movie_writer():
# Test running an animation with NullMovieWriter.
num_frames = 5
anim = make_animation(frames=num_frames)
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 make_animation.
assert writer.outfile == filename
assert writer.dpi == dpi
assert writer.args == ()
assert writer.savefig_kwargs == savefig_kwargs
assert writer._count == num_frames
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')
]
WRITER_OUTPUT += [
(writer, Path(output)) for writer, output in WRITER_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, output', WRITER_OUTPUT)
def test_save_animation_smoketest(tmpdir, writer, output):
if writer == 'pillow':
pytest.importorskip("PIL")
try:
# for ImageMagick the rcparams must be patched to account for
# 'convert' being a built in MS tool, not the imagemagick
# tool.
writer._init_from_registry()
except AttributeError:
pass
if not animation.writers.is_available(writer):
pytest.skip("writer '%s' not available on this system" % writer)
fig, ax = plt.subplots()
line, = ax.plot([], [])
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)
dpi = None
codec = None
if writer == 'ffmpeg':
# Issue #8253
fig.set_size_inches((10.85, 9.21))
dpi = 100.
codec = 'h264'
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,
# Use temporary directory for the file-based writers, which produce a file
# per frame with known names.
with tmpdir.as_cwd():
anim = animation.FuncAnimation(fig, animate, init_func=init, frames=5)
try:
anim.save(output, fps=30, writer=writer, bitrate=500, dpi=dpi,
codec=codec)
except UnicodeDecodeError:
pytest.xfail("There can be errors in the numpy import stack, "
"see issues #1891 and #2679")
def test_no_length_frames():
(make_animation(frames=iter(range(5)))
.save('unused.null', writer=NullMovieWriter()))
def test_movie_writer_registry():
ffmpeg_path = mpl.rcParams['animation.ffmpeg_path']
# Not sure about the first state as there could be some writer
# which set rcparams
# assert not animation.writers._dirty
assert len(animation.writers._registered) > 0
animation.writers.list() # resets dirty state
assert not animation.writers._dirty
mpl.rcParams['animation.ffmpeg_path'] = "not_available_ever_xxxx"
assert animation.writers._dirty
animation.writers.list() # resets
assert not animation.writers._dirty
assert not animation.writers.is_available("ffmpeg")
# something which is 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._dirty
animation.writers.list() # resets
assert not animation.writers._dirty
assert animation.writers.is_available("ffmpeg")
mpl.rcParams['animation.ffmpeg_path'] = ffmpeg_path
@pytest.mark.skipif(
not animation.writers.is_available(mpl.rcParams["animation.writer"]),
reason="animation writer not installed")
@pytest.mark.parametrize("method_name", ["to_html5_video", "to_jshtml"])
def test_embed_limit(method_name, caplog, tmpdir):
caplog.set_level("WARNING")
with tmpdir.as_cwd():
with mpl.rc_context({"animation.embed_limit": 1e-6}): # ~1 byte.
getattr(make_animation(frames=1), method_name)()
assert len(caplog.records) == 1
record, = caplog.records
assert (record.name == "matplotlib.animation"
and record.levelname == "WARNING")
@pytest.mark.skipif(
not animation.writers.is_available(mpl.rcParams["animation.writer"]),
reason="animation writer not installed")
@pytest.mark.parametrize(
"method_name",
["to_html5_video",
pytest.param("to_jshtml",
marks=pytest.mark.xfail)])
def test_cleanup_temporaries(method_name, tmpdir):
with tmpdir.as_cwd():
getattr(make_animation(frames=1), 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):
"""
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.
"""
try:
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)
animation.writers.reset_available_writers()
with pytest.raises(subprocess.CalledProcessError):
make_animation().save("test.mpeg")
finally:
animation.writers.reset_available_writers()
@pytest.mark.parametrize("cache_frame_data, weakref_assertion_fn", [
pytest.param(
False, lambda ref: ref is None, id='cache_frame_data_is_disabled'),
pytest.param(
True, lambda ref: ref is not None, id='cache_frame_data_is_enabled'),
])
def test_funcanimation_holding_frames(cache_frame_data, weakref_assertion_fn):
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:
assert weakref_assertion_fn(f())

View File

@@ -0,0 +1,170 @@
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(baseline_images=['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, axes = plt.subplots(len(t), len(r), squeeze=False,
subplot_kw=dict(aspect=True),
figsize=(8, 4.5))
for i_r, r1 in enumerate(r):
for i_t, t1 in enumerate(t):
ax = axes[i_t, i_r]
draw_arrow(ax, t1, r1)
ax.tick_params(labelleft=False, labelbottom=False)
@image_comparison(baseline_images=['boxarrow_test_image'], extensions=['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(111)
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(baseline_images=['fancyarrow_dpi_cor_100dpi'],
remove_text=True, extensions=['png'],
tol={'aarch64': 0.02}.get(platform.machine(), 0.0),
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(baseline_images=['fancyarrow_dpi_cor_200dpi'],
remove_text=True, extensions=['png'],
tol={'aarch64': 0.02}.get(platform.machine(), 0.0),
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(baseline_images=['fancyarrow_dash'],
remove_text=True, extensions=['png'],
style='default')
def test_fancyarrow_dash():
from matplotlib.patches import FancyArrowPatch
fig, ax = plt.subplots()
e = 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 = 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(baseline_images=['arrow_styles'], extensions=['png'],
style='mpl20', remove_text=True)
def test_arrow_styles():
styles = mpatches.ArrowStyle.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),
arrowstyle=stylename,
mutation_scale=25)
ax.add_patch(patch)
@image_comparison(baseline_images=['connection_styles'], extensions=['png'],
style='mpl20', remove_text=True)
def test_connection_styles():
styles = mpatches.ConnectionStyle.get_styles()
n = len(styles)
fig, ax = plt.subplots(figsize=(6, 10))
ax.set_xlim(0, 1)
ax.set_ylim(-1, n)
for i, stylename in enumerate(sorted(styles)):
patch = mpatches.FancyArrowPatch((0.1, i), (0.8, i + 0.5),
arrowstyle="->",
connectionstyle=stylename,
mutation_scale=25)
ax.add_patch(patch)
def test_invalid_intersection():
conn_style_1 = mpatches.ConnectionStyle.Angle3(angleA=20, angleB=200)
p1 = mpatches.FancyArrowPatch((.2, .2), (.5, .5),
connectionstyle=conn_style_1)
with pytest.raises(ValueError):
plt.gca().add_patch(p1)
conn_style_2 = mpatches.ConnectionStyle.Angle3(angleA=20, angleB=199.9)
p2 = mpatches.FancyArrowPatch((.2, .2), (.5, .5),
connectionstyle=conn_style_2)
plt.gca().add_patch(p2)

View File

@@ -0,0 +1,286 @@
import io
from itertools import chain
import warnings
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 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_point(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_point(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(baseline_images=["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(vertices=np.concatenate([exterior.vertices,
interior.vertices]),
codes=np.concatenate([exterior.codes,
interior.codes]))
star = mpath.Path.unit_regular_star(6).deepcopy()
star.vertices *= 2.6
ax1 = plt.subplot(121)
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)
ax2 = plt.subplot(122, sharex=ax1, sharey=ax1)
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])
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(baseline_images=['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(baseline_images=["default_edges"], remove_text=True,
extensions=['png'], style='default')
def test_default_edges():
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([], [])
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
ln.properties()
assert len(w) == 0
def test_setp():
# Check empty list
plt.setp([])
plt.setp([[]])
# Check arbitrary iterables
fig, axes = plt.subplots()
lines1 = axes.plot(range(3))
lines2 = axes.plot(range(3))
martist.setp(chain(lines1, lines2), 'lw', 5)
plt.setp(axes.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"}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
import re
from matplotlib.backend_bases import (
FigureCanvasBase, LocationEvent, RendererBase)
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
import matplotlib.path as path
import os
import numpy as np
import pytest
def test_uses_per_path():
id = transforms.Affine2D()
paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)]
tforms = [id.rotate(i) 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.IdentityTransform(),
facecolors, edgecolors, [], [], [False],
[], 'data')]
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, offsets, facecolors, edgecolors)
check(id, paths[0:1], tforms, offsets, facecolors, edgecolors)
check(id, [], tforms, offsets, facecolors, edgecolors)
check(id, paths, tforms[0:1], offsets, facecolors, edgecolors)
check(id, paths, [], offsets, facecolors, edgecolors)
for n in range(0, offsets.shape[0]):
check(id, paths, tforms, offsets[0:n, :], facecolors, edgecolors)
check(id, paths, tforms, offsets, [], edgecolors)
check(id, paths, tforms, offsets, facecolors, [])
check(id, paths, tforms, offsets, [], [])
check(id, paths, tforms, offsets, facecolors[0:1], edgecolors)
def test_get_default_filename(tmpdir):
plt.rcParams['savefig.directory'] = str(tmpdir)
fig = plt.figure()
canvas = FigureCanvasBase(fig)
filename = canvas.get_default_filename()
assert filename == 'image.png'
@pytest.mark.backend('pdf')
def test_non_gui_warning(monkeypatch):
plt.subplots()
monkeypatch.setitem(os.environ, "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))

View File

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

View File

@@ -0,0 +1,33 @@
from pathlib import Path
import subprocess
import tempfile
import pytest
nbformat = pytest.importorskip('nbformat')
# From https://blog.thedataincubator.com/2016/06/testing-jupyter-notebooks/
def _notebook_run(nb_file):
"""Execute a notebook via nbconvert and collect output.
:returns (parsed nb object, execution errors)
"""
with tempfile.NamedTemporaryFile(suffix=".ipynb", mode='w+t') as fout:
subprocess.check_call([
"jupyter", "nbconvert", "--to", "notebook",
"--execute", "--ExecutePreprocessor.timeout=500",
"--output", fout.name, nb_file,
])
fout.seek(0)
nb = nbformat.read(fout, nbformat.current_nbformat)
errors = [output for cell in nb.cells if "outputs" in cell
for output in cell["outputs"]
if output.output_type == "error"]
return nb, errors
def test_ipynb():
nb, errors = _notebook_run(Path(__file__).parent / 'test_nbagg_01.ipynb')
assert errors == []

View File

@@ -0,0 +1,243 @@
import io
import os
from pathlib import Path
import sys
import tempfile
import warnings
import numpy as np
import pytest
from matplotlib import dviread, pyplot as plt, checkdep_usetex, rcParams
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.testing.compare import compare_images
from matplotlib.testing.decorators import image_comparison
from matplotlib.testing.determinism import (_determinism_source_date_epoch,
_determinism_check)
with warnings.catch_warnings():
warnings.simplefilter('ignore')
needs_usetex = pytest.mark.skipif(
not checkdep_usetex(True),
reason="This test needs a TeX installation")
@image_comparison(baseline_images=['pdf_use14corefonts'],
extensions=['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 = plt.figure()
ax = fig.add_subplot(1, 1, 1)
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 = plt.figure()
ax = fig.add_subplot(111)
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 = plt.figure()
ax = fig.add_subplot(111)
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 = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('This is a long title')
fig.savefig(pdf, format="pdf")
pdfio.seek(0)
assert sum(b'startxref' in line for line in pdfio) == 1
assert sys.getsizeof(pdfio) < 40000
def test_multipage_keep_empty():
from matplotlib.backends.backend_pdf import PdfPages
from tempfile import NamedTemporaryFile
# 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 = plt.figure()
ax = fig.add_subplot(111)
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 = plt.figure()
ax = fig.add_subplot(1, 1, 1)
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_pdfpages_fspath():
with PdfPages(Path(os.devnull)) as pdf:
pdf.savefig(plt.figure())
def test_source_date_epoch():
"""Test SOURCE_DATE_EPOCH support for PDF output"""
_determinism_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)")
def test_determinism_plain():
"""Test for reproducible PDF output: simple figure"""
_determinism_check('', format="pdf")
def test_determinism_images():
"""Test for reproducible PDF output: figure with different images"""
_determinism_check('i', format="pdf")
def test_determinism_hatches():
"""Test for reproducible PDF output: figure with different hatches"""
_determinism_check('h', format="pdf")
def test_determinism_markers():
"""Test for reproducible PDF output: figure with different markers"""
_determinism_check('m', format="pdf")
def test_determinism_all():
"""Test for reproducible PDF output"""
_determinism_check(format="pdf")
@image_comparison(baseline_images=['hatching_legend'],
extensions=['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(baseline_images=['grayscale_alpha'],
extensions=['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 tempfile.TemporaryFile() as tmpfile, pytest.raises(ValueError):
fig.savefig(tmpfile, format='pdf')
@pytest.mark.style('default')
def test_pdf_savefig_when_color_is_none(tmpdir):
fig, ax = plt.subplots()
plt.axis('off')
ax.plot(np.sin(np.linspace(-5, 5, 100)), 'v', c='none')
actual_image = tmpdir.join('figure.pdf')
expected_image = tmpdir.join('figure.eps')
fig.savefig(str(actual_image), format='pdf')
fig.savefig(str(expected_image), format='eps')
result = compare_images(str(actual_image), str(expected_image), 0)
assert result is None
@needs_usetex
def test_failing_latex(tmpdir):
"""Test failing latex subprocess call"""
path = str(tmpdir.join("tmpoutput.pdf"))
rcParams['text.usetex'] = True
# This fails with "Double subscript"
plt.xlabel("$22_2_2$")
with pytest.raises(RuntimeError):
plt.savefig(path)
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")

View File

@@ -0,0 +1,272 @@
import os
from pathlib import Path
import shutil
import subprocess
from tempfile import TemporaryDirectory
import numpy as np
import pytest
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.testing.compare import compare_images, ImageComparisonFailure
from matplotlib.testing.decorators import image_comparison, _image_directories
from matplotlib.backends.backend_pgf import PdfPages
baseline_dir, result_dir = _image_directories(lambda: 'dummy func')
def check_for(texsystem):
with TemporaryDirectory() as tmpdir:
tex_path = Path(tmpdir, "test.tex")
tex_path.write_text(r"""
\documentclass{minimal}
\usepackage{pgf}
\begin{document}
\typeout{pgfversion=\pgfversion}
\makeatletter
\@@end
""")
try:
subprocess.check_call(
[texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
return False
return True
needs_xelatex = pytest.mark.skipif(not check_for('xelatex'),
reason='xelatex + pgf is required')
needs_pdflatex = pytest.mark.skipif(not check_for('pdflatex'),
reason='pdflatex + pgf is required')
needs_lualatex = pytest.mark.skipif(not check_for('lualatex'),
reason='lualatex + pgf is required')
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)
# test compiling a figure to pdf with xelatex
@needs_xelatex
@pytest.mark.backend('pgf')
@image_comparison(baseline_images=['pgf_xelatex'], extensions=['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.backend('pgf')
@image_comparison(baseline_images=['pgf_pdflatex'], extensions=['pdf'],
style='default')
def test_pdflatex():
if os.environ.get('APPVEYOR', False):
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):
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():
rc_xelatex = {'font.family': 'serif',
'pgf.rcfonts': False}
mpl.rcParams.update(rc_xelatex)
plt.figure()
plt.plot([0., 1e100], [0., 1e100])
plt.xlim(0, 1)
plt.ylim(0, 1)
# this test passes if compiling/saving to pdf works (no image comparison)
plt.savefig(os.path.join(result_dir, "pgf_pathclip.pdf"))
# test mixed mode rendering
@needs_xelatex
@pytest.mark.backend('pgf')
@image_comparison(baseline_images=['pgf_mixedmode'], extensions=['pdf'],
style='default')
def test_mixedmode():
rc_xelatex = {'font.family': 'serif',
'pgf.rcfonts': False}
mpl.rcParams.update(rc_xelatex)
Y, X = np.ogrid[-1:1:40j, -1:1:40j]
plt.figure()
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():
rc_xelatex = {'font.family': 'serif',
'pgf.rcfonts': False}
mpl.rcParams.update(rc_xelatex)
Y, X = np.ogrid[-1:1:40j, -1:1:40j]
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.plot(range(5))
ax2 = fig.add_subplot(122)
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)
@needs_pdflatex
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
def test_pdf_pages():
rc_pdflatex = {
'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': 'pdflatex',
}
mpl.rcParams.update(rc_pdflatex)
fig1 = plt.figure()
ax1 = fig1.add_subplot(1, 1, 1)
ax1.plot(range(5))
fig1.tight_layout()
fig2 = plt.figure(figsize=(3, 2))
ax2 = fig2.add_subplot(1, 1, 1)
ax2.plot(range(5))
fig2.tight_layout()
with PdfPages(os.path.join(result_dir, 'pdfpages.pdf')) as pdf:
pdf.savefig(fig1)
pdf.savefig(fig2)
@needs_xelatex
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
def test_pdf_pages_metadata():
rc_pdflatex = {
'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': 'xelatex',
}
mpl.rcParams.update(rc_pdflatex)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(range(5))
fig.tight_layout()
md = {'author': 'me', 'title': 'Multipage PDF with pgf'}
path = os.path.join(result_dir, 'pdfpages_meta.pdf')
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig)
pdf.savefig(fig)
pdf.savefig(fig)
assert pdf.get_pagecount() == 3
@needs_lualatex
@pytest.mark.style('default')
@pytest.mark.backend('pgf')
def test_pdf_pages_lualatex():
rc_pdflatex = {
'font.family': 'serif',
'pgf.rcfonts': False,
'pgf.texsystem': 'lualatex'
}
mpl.rcParams.update(rc_pdflatex)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(range(5))
fig.tight_layout()
md = {'author': 'me', 'title': 'Multipage PDF with pgf'}
path = os.path.join(result_dir, 'pdfpages_lua.pdf')
with PdfPages(path, metadata=md) as pdf:
pdf.savefig(fig)
pdf.savefig(fig)
assert pdf.get_pagecount() == 2

View File

@@ -0,0 +1,143 @@
import io
import os
from pathlib import Path
import re
import tempfile
import warnings
import pytest
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib import cbook, patheffects
from matplotlib.testing.decorators import image_comparison
from matplotlib.testing.determinism import (_determinism_source_date_epoch,
_determinism_check)
with warnings.catch_warnings():
warnings.simplefilter('ignore')
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('format, use_log, rcParams', [
('ps', False, {}),
pytest.param('ps', False, {'ps.usedistiller': 'ghostscript'},
marks=needs_ghostscript),
pytest.param('ps', False, {'text.usetex': True},
marks=[needs_ghostscript, needs_usetex]),
('eps', False, {}),
('eps', True, {'ps.useafm': True}),
pytest.param('eps', False, {'text.usetex': True},
marks=[needs_ghostscript, needs_usetex]),
], ids=[
'ps',
'ps with distiller',
'ps with usetex',
'eps',
'eps afm',
'eps with usetex'
])
def test_savefig_to_stringio(format, use_log, rcParams):
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])
ax.set_title("Déjà vu")
fig.savefig(s_buf, format=format)
fig.savefig(b_buf, format=format)
s_val = s_buf.getvalue().encode('ascii')
b_val = b_buf.getvalue()
# Remove comments from the output. This includes things that could
# change from run to run, such as the time.
s_val, b_val = [re.sub(b'%%.*?\n', b'', x) for x in [s_val, b_val]]
assert s_val == b_val.replace(b'\r\n', b'\n')
def test_patheffects():
with mpl.rc_context():
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(str(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 ~
plt.rc('text', usetex=True)
plt.plot([1, 2, 3, 4])
plt.xlabel(r'\textbf{time} (s)')
output_eps = os.path.join(str(base_tempdir), 'tex_demo.eps')
# use the PS backend to write the file...
plt.savefig(output_eps, format="ps")
def test_source_date_epoch():
"""Test SOURCE_DATE_EPOCH support for PS output"""
# SOURCE_DATE_EPOCH support is not tested with text.usetex,
# because the produced timestamp comes from ghostscript:
# %%CreationDate: D:20000101000000Z00\'00\', and this could change
# with another ghostscript version.
_determinism_source_date_epoch(
"ps", b"%%CreationDate: Sat Jan 01 00:00:00 2000")
def test_determinism_all():
"""Test for reproducible PS output"""
_determinism_check(format="ps")
@needs_usetex
@needs_ghostscript
def test_determinism_all_tex():
"""Test for reproducible PS/tex output"""
_determinism_check(format="ps", usetex=True)
@image_comparison(baseline_images=["empty"], extensions=["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)
@needs_usetex
def test_failing_latex(tmpdir):
"""Test failing latex subprocess call"""
path = str(tmpdir.join("tmpoutput.ps"))
mpl.rcParams['text.usetex'] = True
# This fails with "Double subscript"
plt.xlabel("$22_2_2$")
with pytest.raises(RuntimeError):
plt.savefig(path)

View File

@@ -0,0 +1,329 @@
import copy
import sys
from unittest import mock
import matplotlib
from matplotlib import pyplot as plt
from matplotlib._pylab_helpers import Gcf
import pytest
@pytest.fixture(autouse=True)
def mpl_test_settings(qt_module, mpl_test_settings):
"""
Ensure qt_module fixture is *first* fixture.
We override the `mpl_test_settings` fixture and depend on the `qt_module`
fixture first. It is very important that it is first, because it skips
tests when Qt is not available, and if not, then the main
`mpl_test_settings` fixture will try to switch backends before the skip can
be triggered.
"""
pass
@pytest.fixture
def qt_module(request):
backend, = request.node.get_closest_marker('backend').args
if backend == 'Qt4Agg':
if any(k in sys.modules for k in ('PyQt5', 'PySide2')):
pytest.skip('Qt5 binding already imported')
try:
import PyQt4
# RuntimeError if PyQt5 already imported.
except (ImportError, RuntimeError):
try:
import PySide
except ImportError:
pytest.skip("Failed to import a Qt4 binding.")
elif backend == 'Qt5Agg':
if any(k in sys.modules for k in ('PyQt4', 'PySide')):
pytest.skip('Qt4 binding already imported')
try:
import PyQt5
# RuntimeError if PyQt4 already imported.
except (ImportError, RuntimeError):
try:
import PySide2
except ImportError:
pytest.skip("Failed to import a Qt5 binding.")
else:
raise ValueError('Backend marker has unknown value: ' + backend)
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')
from matplotlib.backends.backend_qt4 import (
MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT)
elif backend == 'Qt5Agg':
from matplotlib.backends.backend_qt5 import (
MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT)
mods = {}
keys = {}
for name, index in zip(['Alt', 'Control', 'Shift', 'Super'],
[ALT, CTRL, SHIFT, SUPER]):
_, mod, key = MODIFIER_KEYS[index]
mods[name + 'Modifier'] = mod
keys[name + 'Key'] = key
return QtCore, mods, keys
@pytest.fixture
def qt_key(request):
QtCore, _, keys = request.getfixturevalue('qt_module')
if request.param.startswith('Key'):
return getattr(QtCore.Qt, request.param)
else:
return keys[request.param]
@pytest.fixture
def qt_mods(request):
QtCore, mods, _ = request.getfixturevalue('qt_module')
result = QtCore.Qt.NoModifier
for mod in request.param:
result |= mods[mod]
return result
@pytest.mark.parametrize('backend', [
# Note: the value is irrelevant; the important part is the marker.
pytest.param('Qt4Agg', marks=pytest.mark.backend('Qt4Agg')),
pytest.param('Qt5Agg', marks=pytest.mark.backend('Qt5Agg')),
])
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')
def test_fig_signals(qt_module):
# Create a figure
fig = plt.figure()
# Access QtCore
QtCore = qt_module[0]
# Access signals
import signal
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
QtCore.QCoreApplication.exit()
# Timer to exit event loop
QtCore.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}'),
('ControlKey', ['AltModifier'], 'alt+control'),
('AltKey', ['ControlModifier'], 'ctrl+alt'),
('Key_Aacute', ['ControlModifier', 'AltModifier', 'SuperModifier'],
'ctrl+alt+super+\N{LATIN SMALL LETTER A WITH ACUTE}'),
('Key_Backspace', [], 'backspace'),
('Key_Backspace', ['ControlModifier'], 'ctrl+backspace'),
('Key_Play', [], None),
],
indirect=['qt_key', 'qt_mods'],
ids=[
'shift',
'lower',
'control',
'unicode_upper',
'unicode_lower',
'alt_control',
'control_alt',
'modifier_order',
'backspace',
'backspace_mod',
'non_unicode_key',
]
)
@pytest.mark.parametrize('backend', [
# Note: the value is irrelevant; the important part is the marker.
pytest.param('Qt4Agg', marks=pytest.mark.backend('Qt4Agg')),
pytest.param('Qt5Agg', marks=pytest.mark.backend('Qt5Agg')),
])
def test_correct_key(backend, 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_canvas = plt.figure().canvas
event = mock.Mock()
event.isAutoRepeat.return_value = False
event.key.return_value = qt_key
event.modifiers.return_value = qt_mods
def receive(event):
assert event.key == answer
qt_canvas.mpl_connect('key_press_event', receive)
qt_canvas.keyPressEvent(event)
@pytest.mark.backend('Qt5Agg')
def test_dpi_ratio_change():
"""
Make sure that if _dpi_ratio changes, the figure dpi changes but the
widget remains the same physical size.
"""
prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT._dpi_ratio'
with mock.patch(prop, new_callable=mock.PropertyMock) as p:
p.return_value = 3
fig = plt.figure(figsize=(5, 2), dpi=120)
qt_canvas = fig.canvas
qt_canvas.show()
from matplotlib.backends.backend_qt5 import qApp
# Make sure the mocking worked
assert qt_canvas._dpi_ratio == 3
size = qt_canvas.size()
qt_canvas.manager.show()
qt_canvas.draw()
qApp.processEvents()
# 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()
p.return_value = 2
assert qt_canvas._dpi_ratio == 2
qt_canvas.draw()
qApp.processEvents()
# this second processEvents is required to fully run the draw.
# On `update` we notice the DPI has changed and trigger a
# resize event to refresh, the second processEvents is
# required to process that and fully update the window sizes.
qApp.processEvents()
# 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()
@pytest.mark.backend('Qt5Agg')
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')
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")
def test_canvas_reinit():
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from functools import partial
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)
assert called

View File

@@ -0,0 +1,191 @@
import numpy as np
from io import BytesIO
import os
import re
import tempfile
import warnings
import xml.parsers.expat
import pytest
import matplotlib as mpl
from matplotlib import dviread
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from matplotlib.testing.decorators import image_comparison
with warnings.catch_warnings():
warnings.simplefilter('ignore')
needs_usetex = pytest.mark.skipif(
not mpl.checkdep_usetex(True),
reason="This test needs a TeX installation")
def test_visibility():
fig, ax = plt.subplots()
x = np.linspace(0, 4 * np.pi, 50)
y = np.sin(x)
yerr = np.ones_like(y)
a, b, c = ax.errorbar(x, y, yerr=yerr, fmt='ko')
for artist in b:
artist.set_visible(False)
fd = BytesIO()
fig.savefig(fd, format='svg')
fd.seek(0)
buf = fd.read()
fd.close()
parser = xml.parsers.expat.ParserCreate()
parser.Parse(buf) # this will raise ExpatError if the svg is invalid
@image_comparison(baseline_images=['fill_black_with_alpha'], remove_text=True,
extensions=['svg'])
def test_fill_black_with_alpha():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(x=[0, 0.1, 1], y=[0, 0, 0], c='k', alpha=0.1, s=10000)
@image_comparison(baseline_images=['noscale'], remove_text=True)
def test_noscale():
X, Y = np.meshgrid(np.arange(-5, 5, 1), np.arange(-5, 5, 1))
Z = np.sin(Y ** 2)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.imshow(Z, cmap='gray', interpolation='none')
def test_text_urls():
fig = plt.figure()
test_url = "http://test_text_urls.matplotlib.org"
fig.suptitle("test_text_urls", url=test_url)
fd = BytesIO()
fig.savefig(fd, format='svg')
fd.seek(0)
buf = fd.read().decode()
fd.close()
expected = '<a xlink:href="{0}">'.format(test_url)
assert expected in buf
@image_comparison(baseline_images=['bold_font_output'], extensions=['svg'])
def test_bold_font_output():
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(np.arange(10), np.arange(10))
ax.set_xlabel('nonbold-xlabel')
ax.set_ylabel('bold-ylabel', fontweight='bold')
ax.set_title('bold-title', fontweight='bold')
@image_comparison(baseline_images=['bold_font_output_with_none_fonttype'],
extensions=['svg'])
def test_bold_font_output_with_none_fonttype():
plt.rcParams['svg.fonttype'] = 'none'
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(np.arange(10), np.arange(10))
ax.set_xlabel('nonbold-xlabel')
ax.set_ylabel('bold-ylabel', fontweight='bold')
ax.set_title('bold-title', fontweight='bold')
def _test_determinism_save(filename, usetex):
# This function is mostly copy&paste from "def test_visibility"
mpl.rc('svg', hashsalt='asdf')
mpl.rc('text', usetex=usetex)
fig = Figure() # Require no GUI.
ax = fig.add_subplot(111)
x = np.linspace(0, 4 * np.pi, 50)
y = np.sin(x)
yerr = np.ones_like(y)
a, b, c = ax.errorbar(x, y, yerr=yerr, fmt='ko')
for artist in b:
artist.set_visible(False)
ax.set_title('A string $1+2+\\sigma$')
ax.set_xlabel('A string $1+2+\\sigma$')
ax.set_ylabel('A string $1+2+\\sigma$')
fig.savefig(filename, format="svg")
@pytest.mark.parametrize(
"filename, usetex",
# unique filenames to allow for parallel testing
[("determinism_notex.svg", False),
pytest.param("determinism_tex.svg", True, marks=needs_usetex)])
def test_determinism(filename, usetex):
import sys
from subprocess import check_output, STDOUT, CalledProcessError
plots = []
for i in range(3):
# Using check_output and setting stderr to STDOUT will capture the real
# problem in the output property of the exception
try:
check_output(
[sys.executable, '-R', '-c',
'import matplotlib; '
'matplotlib._called_from_pytest = True; '
'matplotlib.use("svg", force=True); '
'from matplotlib.tests.test_backend_svg '
'import _test_determinism_save;'
'_test_determinism_save(%r, %r)' % (filename, usetex)],
stderr=STDOUT)
except CalledProcessError as e:
# it's easier to use utf8 and ask for forgiveness than try
# to figure out what the current console has as an
# encoding :-/
print(e.output.decode(encoding="utf-8", errors="ignore"))
raise e
else:
with open(filename, 'rb') as fd:
plots.append(fd.read())
finally:
os.unlink(filename)
for p in plots[1:]:
assert p == plots[0]
@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)
mpl.rc('text', usetex=True)
fig, ax = plt.subplots()
ax.text(0.5, 0.5, 'hello')
with tempfile.TemporaryFile() as tmpfile, pytest.raises(ValueError):
fig.savefig(tmpfile, format='svg')
# Use Computer Modern Sans Serif, not Helvetica (which has no \textwon).
@pytest.mark.style('default')
@needs_usetex
def test_unicode_won():
fig = Figure()
fig.text(.5, .5, r'\textwon', usetex=True)
with BytesIO() as fd:
fig.savefig(fd, format='svg')
buf = fd.getvalue().decode('ascii')
won_id = 'Computer_Modern_Sans_Serif-142'
assert re.search(r'<path d=(.|\s)*?id="{0}"/>'.format(won_id), buf)
assert re.search(r'<use[^/>]*? xlink:href="#{0}"/>'.format(won_id), buf)

View File

@@ -0,0 +1,20 @@
import pytest
from matplotlib.backend_tools import ToolHelpBase
@pytest.mark.parametrize('rc_shortcut,expected', [
('home', 'Home'),
('backspace', 'Backspace'),
('f1', 'F1'),
('ctrl+a', 'Ctrl+A'),
('ctrl+A', 'Ctrl+Shift+A'),
('a', 'a'),
('A', 'A'),
('ctrl+shift+f1', 'Ctrl+Shift+F1'),
('1', '1'),
('cmd+p', 'Cmd+P'),
('cmd+1', 'Cmd+1'),
])
def test_format_shortcut(rc_shortcut, expected):
assert ToolHelpBase.format_shortcut(rc_shortcut) == expected

View File

@@ -0,0 +1,28 @@
import subprocess
import os
import sys
import pytest
@pytest.mark.parametrize("backend", ["webagg", "nbagg"])
def test_webagg_fallback(backend):
if backend == "nbagg":
pytest.importorskip("IPython")
env = {}
if os.name == "nt":
env = dict(os.environ)
else:
env = {"DISPLAY": ""}
env["MPLBACKEND"] = backend
test_code = (
"import os;"
+ f"assert os.environ['MPLBACKEND'] == '{backend}';"
+ "import matplotlib.pyplot as plt; "
+ "print(plt.get_backend());"
f"assert '{backend}' == plt.get_backend().lower();"
)
ret = subprocess.call([sys.executable, "-c", test_code], env=env)
assert ret == 0

View File

@@ -0,0 +1,145 @@
import importlib
import importlib.util
import os
import signal
import subprocess
import sys
import time
import urllib.request
import pytest
import matplotlib as mpl
# Minimal smoke-testing of the backends for which the dependencies are
# PyPI-installable on Travis. They are not available for all tested Python
# versions so we don't fail on missing backends.
def _get_testable_interactive_backends():
backends = []
for deps, backend in [
(["cairo", "gi"], "gtk3agg"),
(["cairo", "gi"], "gtk3cairo"),
(["PyQt5"], "qt5agg"),
(["PyQt5", "cairocffi"], "qt5cairo"),
(["tkinter"], "tkagg"),
(["wx"], "wx"),
(["wx"], "wxagg"),
]:
reason = None
if not os.environ.get("DISPLAY"):
reason = "No $DISPLAY"
elif any(importlib.util.find_spec(dep) is None for dep in deps):
reason = "Missing dependency"
if reason:
backend = pytest.param(
backend, marks=pytest.mark.skip(reason=reason))
backends.append(backend)
return backends
# Using a timer not only allows testing of timers (on other backends), but is
# also necessary on gtk3 and wx, where a direct call to key_press_event("q")
# from draw_event causes breakage due to the canvas widget being deleted too
# early. Also, gtk3 redefines key_press_event with a different signature, so
# we directly invoke it from the superclass instead.
_test_script = """\
import importlib
import importlib.util
import sys
from unittest import TestCase
import matplotlib as mpl
from matplotlib import pyplot as plt, rcParams
from matplotlib.backend_bases import FigureCanvasBase
rcParams.update({
"webagg.open_in_browser": False,
"webagg.port_retries": 1,
})
backend = plt.rcParams["backend"].lower()
assert_equal = TestCase().assertEqual
assert_raises = TestCase().assertRaises
if backend.endswith("agg") and not backend.startswith(("gtk3", "web")):
# Force interactive framework setup.
plt.figure()
# Check that we cannot switch to a backend using another interactive
# framework, but can switch to a backend using cairo instead of agg, or a
# non-interactive backend. In the first case, we use tkagg as the "other"
# interactive backend as it is (essentially) guaranteed to be present.
# Moreover, don't test switching away from gtk3 (as Gtk.main_level() is
# not set up at this point yet) and webagg (which uses no interactive
# framework).
if backend != "tkagg":
with assert_raises(ImportError):
mpl.use("tkagg", force=True)
def check_alt_backend(alt_backend):
mpl.use(alt_backend, force=True)
fig = plt.figure()
assert_equal(
type(fig.canvas).__module__,
"matplotlib.backends.backend_{}".format(alt_backend))
if importlib.util.find_spec("cairocffi"):
check_alt_backend(backend[:-3] + "cairo")
check_alt_backend("svg")
mpl.use(backend, force=True)
fig, ax = plt.subplots()
assert_equal(
type(fig.canvas).__module__,
"matplotlib.backends.backend_{}".format(backend))
ax.plot([0, 1], [2, 3])
timer = fig.canvas.new_timer(1)
timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q")
# Trigger quitting upon draw.
fig.canvas.mpl_connect("draw_event", lambda event: timer.start())
plt.show()
"""
_test_timeout = 10 # Empirically, 1s is not enough on Travis.
@pytest.mark.parametrize("backend", _get_testable_interactive_backends())
@pytest.mark.flaky(reruns=3)
def test_interactive_backend(backend):
proc = subprocess.run([sys.executable, "-c", _test_script],
env={**os.environ, "MPLBACKEND": backend},
timeout=_test_timeout)
if proc.returncode:
pytest.fail("The subprocess returned with non-zero exit status "
f"{proc.returncode}.")
@pytest.mark.skipif('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI' in os.environ,
reason="this test fails an azure for unknown reasons")
@pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.")
def test_webagg():
pytest.importorskip("tornado")
proc = subprocess.Popen([sys.executable, "-c", _test_script],
env={**os.environ, "MPLBACKEND": "webagg"})
url = "http://{}:{}".format(
mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"])
timeout = time.perf_counter() + _test_timeout
while True:
try:
retcode = proc.poll()
# check that the subprocess for the server is not dead
assert retcode is None
conn = urllib.request.urlopen(url)
break
except urllib.error.URLError:
if time.perf_counter() > timeout:
pytest.fail("Failed to connect to the webagg server.")
else:
continue
conn.close()
proc.send_signal(signal.SIGINT)
assert proc.wait(timeout=_test_timeout) == 0

View File

@@ -0,0 +1,57 @@
import builtins
import subprocess
import sys
import matplotlib
from matplotlib.cbook import dedent
def test_simple():
assert 1 + 1 == 2
def test_override_builtins():
import pylab
ok_to_override = {
'__name__',
'__doc__',
'__package__',
'__loader__',
'__spec__',
'any',
'all',
'sum',
'divmod'
}
overridden = False
for key in dir(pylab):
if key in dir(builtins):
if (getattr(pylab, key) != getattr(builtins, key) and
key not in ok_to_override):
print("'%s' was overridden in globals()." % key)
overridden = True
assert not overridden
def test_lazy_imports():
source = dedent("""
import sys
import matplotlib.figure
import matplotlib.backend_bases
import matplotlib.pyplot
assert 'matplotlib._png' not in sys.modules
assert 'matplotlib._tri' not in sys.modules
assert 'matplotlib._qhull' not in sys.modules
assert 'matplotlib._contour' not in sys.modules
assert 'urllib.request' not in sys.modules
""")
subprocess.check_call([
sys.executable,
'-c',
source
])

View File

@@ -0,0 +1,110 @@
import numpy as np
from io import BytesIO
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import matplotlib.patches as mpatches
from matplotlib.ticker import FuncFormatter
@image_comparison(baseline_images=['bbox_inches_tight'], remove_text=True,
savefig_kwarg=dict(bbox_inches='tight'))
def test_bbox_inches_tight():
#: Test that a figure saved using bbox_inches='tight' is clipped correctly
data = [[66386, 174296, 75131, 577908, 32015],
[58230, 381139, 78045, 99308, 160454],
[89135, 80552, 152558, 497981, 603535],
[78415, 81858, 150656, 193263, 69638],
[139361, 331509, 343164, 781380, 52269]]
colLabels = rowLabels = [''] * 5
rows = len(data)
ind = np.arange(len(colLabels)) + 0.3 # the x locations for the groups
cellText = []
width = 0.4 # the width of the bars
yoff = np.zeros(len(colLabels))
# the bottom values for stacked bar chart
fig, ax = plt.subplots(1, 1)
for row in range(rows):
ax.bar(ind, data[row], width, bottom=yoff, align='edge', color='b')
yoff = yoff + data[row]
cellText.append([''])
plt.xticks([])
plt.legend([''] * 5, loc=(1.2, 0.2))
# Add a table at the bottom of the axes
cellText.reverse()
the_table = plt.table(cellText=cellText,
rowLabels=rowLabels,
colLabels=colLabels, loc='bottom')
@image_comparison(baseline_images=['bbox_inches_tight_suptile_legend'],
remove_text=False, savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_tight_suptile_legend():
plt.plot(np.arange(10), label='a straight line')
plt.legend(bbox_to_anchor=(0.9, 1), loc='upper left')
plt.title('Axis title')
plt.suptitle('Figure title')
# put an extra long y tick on to see that the bbox is accounted for
def y_formatter(y, pos):
if int(y) == 4:
return 'The number 4'
else:
return str(y)
plt.gca().yaxis.set_major_formatter(FuncFormatter(y_formatter))
plt.xlabel('X axis')
@image_comparison(baseline_images=['bbox_inches_tight_clipping'],
remove_text=True, savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_tight_clipping():
# tests bbox clipping on scatter points, and path clipping on a patch
# to generate an appropriately tight bbox
plt.scatter(np.arange(10), np.arange(10))
ax = plt.gca()
ax.set_xlim([0, 5])
ax.set_ylim([0, 5])
# make a massive rectangle and clip it with a path
patch = mpatches.Rectangle([-50, -50], 100, 100,
transform=ax.transData,
facecolor='blue', alpha=0.5)
path = mpath.Path.unit_regular_star(5).deepcopy()
path.vertices *= 0.25
patch.set_clip_path(path, transform=ax.transAxes)
plt.gcf().artists.append(patch)
@image_comparison(baseline_images=['bbox_inches_tight_raster'],
remove_text=True, savefig_kwarg={'bbox_inches': 'tight'})
def test_bbox_inches_tight_raster():
"""Test rasterization with tight_layout"""
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1.0, 2.0], rasterized=True)
def test_only_on_non_finite_bbox():
fig, ax = plt.subplots()
ax.annotate("", xy=(0, float('nan')))
ax.set_axis_off()
# we only need to test that it does not error out on save
fig.savefig(BytesIO(), bbox_inches='tight', format='png')
def test_tight_pcolorfast():
fig, ax = plt.subplots()
ax.pcolorfast(np.arange(4).reshape((2, 2)))
ax.set(ylim=(0, .1))
buf = BytesIO()
fig.savefig(buf, bbox_inches="tight")
buf.seek(0)
height, width, _ = plt.imread(buf).shape
# Previously, the bbox would include the area of the image clipped out by
# the axes, resulting in a very tall image given the y limits of (0, 0.1).
assert width > height

View File

@@ -0,0 +1,275 @@
"""Catch all for categorical functions"""
import pytest
import numpy as np
from matplotlib.axes import Axes
import matplotlib.pyplot as plt
import matplotlib.category as cat
# Python2/3 text handling
_to_str = cat.StrCategoryFormatter._text
class TestUnitData(object):
test_cases = [('single', (["hello world"], [0])),
('unicode', (["Здравствуйте мир"], [0])),
('mixed', (['A', "np.nan", 'B', "3.14", "мир"],
[0, 1, 2, 3, 4]))]
ids, data = zip(*test_cases)
@pytest.mark.parametrize("data, locs", data, ids=ids)
def test_unit(self, data, locs):
unit = cat.UnitData(data)
assert list(unit._mapping.keys()) == data
assert list(unit._mapping.values()) == locs
def test_update(self):
data = ['a', 'd']
locs = [0, 1]
data_update = ['b', 'd', 'e']
unique_data = ['a', 'd', 'b', 'e']
updated_locs = [0, 1, 2, 3]
unit = cat.UnitData(data)
assert list(unit._mapping.keys()) == data
assert list(unit._mapping.values()) == locs
unit.update(data_update)
assert list(unit._mapping.keys()) == unique_data
assert list(unit._mapping.values()) == updated_locs
failing_test_cases = [("number", 3.14), ("nan", np.nan),
("list", [3.14, 12]), ("mixed type", ["A", 2])]
fids, fdata = zip(*test_cases)
@pytest.mark.parametrize("fdata", fdata, ids=fids)
def test_non_string_fails(self, fdata):
with pytest.raises(TypeError):
cat.UnitData(fdata)
@pytest.mark.parametrize("fdata", fdata, ids=fids)
def test_non_string_update_fails(self, fdata):
unitdata = cat.UnitData()
with pytest.raises(TypeError):
unitdata.update(fdata)
class FakeAxis(object):
def __init__(self, units):
self.units = units
class TestStrCategoryConverter(object):
"""Based on the pandas conversion and factorization tests:
ref: /pandas/tseries/tests/test_converter.py
/pandas/tests/test_algos.py:TestFactorize
"""
test_cases = [("unicode", ["Здравствуйте мир"]),
("ascii", ["hello world"]),
("single", ['a', 'b', 'c']),
("integer string", ["1", "2"]),
("single + values>10", ["A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N",
"O", "P", "Q", "R", "S", "T", "U",
"V", "W", "X", "Y", "Z"])]
ids, values = zip(*test_cases)
failing_test_cases = [("mixed", [3.14, 'A', np.inf]),
("string integer", ['42', 42])]
fids, fvalues = zip(*failing_test_cases)
@pytest.fixture(autouse=True)
def mock_axis(self, request):
self.cc = cat.StrCategoryConverter()
# self.unit should be probably be replaced with real mock unit
self.unit = cat.UnitData()
self.ax = FakeAxis(self.unit)
@pytest.mark.parametrize("vals", values, ids=ids)
def test_convert(self, vals):
np.testing.assert_allclose(self.cc.convert(vals, self.ax.units,
self.ax),
range(len(vals)))
@pytest.mark.parametrize("value", ["hi", "мир"], ids=["ascii", "unicode"])
def test_convert_one_string(self, value):
assert self.cc.convert(value, self.unit, self.ax) == 0
def test_convert_one_number(self):
actual = self.cc.convert(0.0, self.unit, self.ax)
np.testing.assert_allclose(actual, np.array([0.]))
def test_convert_float_array(self):
data = np.array([1, 2, 3], dtype=float)
actual = self.cc.convert(data, self.unit, self.ax)
np.testing.assert_allclose(actual, np.array([1., 2., 3.]))
@pytest.mark.parametrize("fvals", fvalues, ids=fids)
def test_convert_fail(self, fvals):
with pytest.raises(TypeError):
self.cc.convert(fvals, self.unit, self.ax)
def test_axisinfo(self):
axis = self.cc.axisinfo(self.unit, self.ax)
assert isinstance(axis.majloc, cat.StrCategoryLocator)
assert isinstance(axis.majfmt, cat.StrCategoryFormatter)
def test_default_units(self):
assert isinstance(self.cc.default_units(["a"], self.ax), cat.UnitData)
@pytest.fixture
def ax():
return plt.figure().subplots()
PLOT_LIST = [Axes.scatter, Axes.plot, Axes.bar]
PLOT_IDS = ["scatter", "plot", "bar"]
class TestStrCategoryLocator(object):
def test_StrCategoryLocator(self):
locs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
unit = cat.UnitData([str(j) for j in locs])
ticks = cat.StrCategoryLocator(unit._mapping)
np.testing.assert_array_equal(ticks.tick_values(None, None), locs)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_StrCategoryLocatorPlot(self, ax, plotter):
ax.plot(["a", "b", "c"])
np.testing.assert_array_equal(ax.yaxis.major.locator(), range(3))
class TestStrCategoryFormatter(object):
test_cases = [("ascii", ["hello", "world", "hi"]),
("unicode", ["Здравствуйте", "привет"])]
ids, cases = zip(*test_cases)
@pytest.mark.parametrize("ydata", cases, ids=ids)
def test_StrCategoryFormatter(self, ax, ydata):
unit = cat.UnitData(ydata)
labels = cat.StrCategoryFormatter(unit._mapping)
for i, d in enumerate(ydata):
assert labels(i, i) == _to_str(d)
@pytest.mark.parametrize("ydata", cases, ids=ids)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_StrCategoryFormatterPlot(self, ax, ydata, plotter):
plotter(ax, range(len(ydata)), ydata)
for i, d in enumerate(ydata):
assert ax.yaxis.major.formatter(i, i) == _to_str(d)
assert ax.yaxis.major.formatter(i+1, i+1) == ""
assert ax.yaxis.major.formatter(0, None) == ""
def axis_test(axis, labels):
ticks = list(range(len(labels)))
np.testing.assert_array_equal(axis.get_majorticklocs(), ticks)
graph_labels = [axis.major.formatter(i, i) for i in ticks]
assert graph_labels == [_to_str(l) for l in labels]
assert list(axis.units._mapping.keys()) == [l for l in labels]
assert list(axis.units._mapping.values()) == ticks
class TestPlotBytes(object):
bytes_cases = [('string list', ['a', 'b', 'c']),
('bytes list', [b'a', b'b', b'c']),
('bytes ndarray', np.array([b'a', b'b', b'c']))]
bytes_ids, bytes_data = zip(*bytes_cases)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
@pytest.mark.parametrize("bdata", bytes_data, ids=bytes_ids)
def test_plot_bytes(self, ax, plotter, bdata):
counts = np.array([4, 6, 5])
plotter(ax, bdata, counts)
axis_test(ax.xaxis, bdata)
class TestPlotNumlike(object):
numlike_cases = [('string list', ['1', '11', '3']),
('string ndarray', np.array(['1', '11', '3'])),
('bytes list', [b'1', b'11', b'3']),
('bytes ndarray', np.array([b'1', b'11', b'3']))]
numlike_ids, numlike_data = zip(*numlike_cases)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
@pytest.mark.parametrize("ndata", numlike_data, ids=numlike_ids)
def test_plot_numlike(self, ax, plotter, ndata):
counts = np.array([4, 6, 5])
plotter(ax, ndata, counts)
axis_test(ax.xaxis, ndata)
class TestPlotTypes(object):
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_unicode(self, ax, plotter):
words = ['Здравствуйте', 'привет']
plotter(ax, words, [0, 1])
axis_test(ax.xaxis, words)
@pytest.fixture
def test_data(self):
self.x = ["hello", "happy", "world"]
self.xy = [2, 6, 3]
self.y = ["Python", "is", "fun"]
self.yx = [3, 4, 5]
@pytest.mark.usefixtures("test_data")
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_xaxis(self, ax, test_data, plotter):
plotter(ax, self.x, self.xy)
axis_test(ax.xaxis, self.x)
@pytest.mark.usefixtures("test_data")
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_yaxis(self, ax, test_data, plotter):
plotter(ax, self.yx, self.y)
axis_test(ax.yaxis, self.y)
@pytest.mark.usefixtures("test_data")
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_plot_xyaxis(self, ax, test_data, plotter):
plotter(ax, self.x, self.y)
axis_test(ax.xaxis, self.x)
axis_test(ax.yaxis, self.y)
@pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS)
def test_update_plot(self, ax, plotter):
plotter(ax, ['a', 'b'], ['e', 'g'])
plotter(ax, ['a', 'b', 'd'], ['f', 'a', 'b'])
plotter(ax, ['b', 'c', 'd'], ['g', 'e', 'd'])
axis_test(ax.xaxis, ['a', 'b', 'd', 'c'])
axis_test(ax.yaxis, ['e', 'g', 'f', 'a', 'b', 'd'])
failing_test_cases = [("mixed", ['A', 3.14]),
("number integer", ['1', 1]),
("string integer", ['42', 42]),
("missing", ['12', np.nan])]
fids, fvalues = zip(*failing_test_cases)
PLOT_BROKEN_LIST = [Axes.scatter,
pytest.param(Axes.plot, marks=pytest.mark.xfail),
pytest.param(Axes.bar, marks=pytest.mark.xfail)]
PLOT_BROKEN_IDS = ["scatter", "plot", "bar"]
@pytest.mark.parametrize("plotter", PLOT_BROKEN_LIST, ids=PLOT_BROKEN_IDS)
@pytest.mark.parametrize("xdata", fvalues, ids=fids)
def test_mixed_type_exception(self, ax, plotter, xdata):
with pytest.raises(TypeError):
plotter(ax, xdata, [1, 2])
@pytest.mark.parametrize("plotter", PLOT_BROKEN_LIST, ids=PLOT_BROKEN_IDS)
@pytest.mark.parametrize("xdata", fvalues, ids=fids)
def test_mixed_type_update_exception(self, ax, plotter, xdata):
with pytest.raises(TypeError):
plotter(ax, [0, 3], [1, 3])
plotter(ax, xdata, [1, 2])

View File

@@ -0,0 +1,599 @@
import itertools
import pickle
from weakref import ref
import warnings
from unittest.mock import patch, Mock
from datetime import datetime
import numpy as np
from numpy.testing import (assert_array_equal, assert_approx_equal,
assert_array_almost_equal)
import pytest
import matplotlib.cbook as cbook
import matplotlib.colors as mcolors
from matplotlib.cbook import (
MatplotlibDeprecationWarning, delete_masked_points as dmp)
def test_is_hashable():
s = 'string'
assert cbook.is_hashable(s)
lst = ['list', 'of', 'stings']
assert not cbook.is_hashable(lst)
class Test_delete_masked_points(object):
def setup_method(self):
self.mask1 = [False, False, True, True, False, False]
self.arr0 = np.arange(1.0, 7.0)
self.arr1 = [1, 2, 3, np.nan, np.nan, 6]
self.arr2 = np.array(self.arr1)
self.arr3 = np.ma.array(self.arr2, mask=self.mask1)
self.arr_s = ['a', 'b', 'c', 'd', 'e', 'f']
self.arr_s2 = np.array(self.arr_s)
self.arr_dt = [datetime(2008, 1, 1), datetime(2008, 1, 2),
datetime(2008, 1, 3), datetime(2008, 1, 4),
datetime(2008, 1, 5), datetime(2008, 1, 6)]
self.arr_dt2 = np.array(self.arr_dt)
self.arr_colors = ['r', 'g', 'b', 'c', 'm', 'y']
self.arr_rgba = mcolors.to_rgba_array(self.arr_colors)
def test_bad_first_arg(self):
with pytest.raises(ValueError):
dmp('a string', self.arr0)
def test_string_seq(self):
actual = dmp(self.arr_s, self.arr1)
ind = [0, 1, 2, 5]
expected = (self.arr_s2[ind], self.arr2[ind])
assert_array_equal(actual[0], expected[0])
assert_array_equal(actual[1], expected[1])
def test_datetime(self):
actual = dmp(self.arr_dt, self.arr3)
ind = [0, 1, 5]
expected = (self.arr_dt2[ind], self.arr3[ind].compressed())
assert_array_equal(actual[0], expected[0])
assert_array_equal(actual[1], expected[1])
def test_rgba(self):
actual = dmp(self.arr3, self.arr_rgba)
ind = [0, 1, 5]
expected = (self.arr3[ind].compressed(), self.arr_rgba[ind])
assert_array_equal(actual[0], expected[0])
assert_array_equal(actual[1], expected[1])
class Test_boxplot_stats(object):
def setup(self):
np.random.seed(937)
self.nrows = 37
self.ncols = 4
self.data = np.random.lognormal(size=(self.nrows, self.ncols),
mean=1.5, sigma=1.75)
self.known_keys = sorted([
'mean', 'med', 'q1', 'q3', 'iqr',
'cilo', 'cihi', 'whislo', 'whishi',
'fliers', 'label'
])
self.std_results = cbook.boxplot_stats(self.data)
self.known_nonbootstrapped_res = {
'cihi': 6.8161283264444847,
'cilo': -0.1489815330368689,
'iqr': 13.492709959447094,
'mean': 13.00447442387868,
'med': 3.3335733967038079,
'fliers': np.array([
92.55467075, 87.03819018, 42.23204914, 39.29390996
]),
'q1': 1.3597529879465153,
'q3': 14.85246294739361,
'whishi': 27.899688243699629,
'whislo': 0.042143774965502923
}
self.known_bootstrapped_ci = {
'cihi': 8.939577523357828,
'cilo': 1.8692703958676578,
}
self.known_whis3_res = {
'whishi': 42.232049135969874,
'whislo': 0.042143774965502923,
'fliers': np.array([92.55467075, 87.03819018]),
}
self.known_res_percentiles = {
'whislo': 0.1933685896907924,
'whishi': 42.232049135969874
}
self.known_res_range = {
'whislo': 0.042143774965502923,
'whishi': 92.554670752188699
}
def test_form_main_list(self):
assert isinstance(self.std_results, list)
def test_form_each_dict(self):
for res in self.std_results:
assert isinstance(res, dict)
def test_form_dict_keys(self):
for res in self.std_results:
assert set(res) <= set(self.known_keys)
def test_results_baseline(self):
res = self.std_results[0]
for key, value in self.known_nonbootstrapped_res.items():
assert_array_almost_equal(res[key], value)
def test_results_bootstrapped(self):
results = cbook.boxplot_stats(self.data, bootstrap=10000)
res = results[0]
for key, value in self.known_bootstrapped_ci.items():
assert_approx_equal(res[key], value)
def test_results_whiskers_float(self):
results = cbook.boxplot_stats(self.data, whis=3)
res = results[0]
for key, value in self.known_whis3_res.items():
assert_array_almost_equal(res[key], value)
def test_results_whiskers_range(self):
results = cbook.boxplot_stats(self.data, whis='range')
res = results[0]
for key, value in self.known_res_range.items():
assert_array_almost_equal(res[key], value)
def test_results_whiskers_percentiles(self):
results = cbook.boxplot_stats(self.data, whis=[5, 95])
res = results[0]
for key, value in self.known_res_percentiles.items():
assert_array_almost_equal(res[key], value)
def test_results_withlabels(self):
labels = ['Test1', 2, 'ardvark', 4]
results = cbook.boxplot_stats(self.data, labels=labels)
res = results[0]
for lab, res in zip(labels, results):
assert res['label'] == lab
results = cbook.boxplot_stats(self.data)
for res in results:
assert 'label' not in res
def test_label_error(self):
labels = [1, 2]
with pytest.raises(ValueError):
results = cbook.boxplot_stats(self.data, labels=labels)
def test_bad_dims(self):
data = np.random.normal(size=(34, 34, 34))
with pytest.raises(ValueError):
results = cbook.boxplot_stats(data)
def test_boxplot_stats_autorange_false(self):
x = np.zeros(shape=140)
x = np.hstack([-25, x, 25])
bstats_false = cbook.boxplot_stats(x, autorange=False)
bstats_true = cbook.boxplot_stats(x, autorange=True)
assert bstats_false[0]['whislo'] == 0
assert bstats_false[0]['whishi'] == 0
assert_array_almost_equal(bstats_false[0]['fliers'], [-25, 25])
assert bstats_true[0]['whislo'] == -25
assert bstats_true[0]['whishi'] == 25
assert_array_almost_equal(bstats_true[0]['fliers'], [])
class Test_callback_registry(object):
def setup(self):
self.signal = 'test'
self.callbacks = cbook.CallbackRegistry()
def connect(self, s, func):
return self.callbacks.connect(s, func)
def is_empty(self):
assert self.callbacks._func_cid_map == {}
assert self.callbacks.callbacks == {}
def is_not_empty(self):
assert self.callbacks._func_cid_map != {}
assert self.callbacks.callbacks != {}
def test_callback_complete(self):
# ensure we start with an empty registry
self.is_empty()
# create a class for testing
mini_me = Test_callback_registry()
# test that we can add a callback
cid1 = self.connect(self.signal, mini_me.dummy)
assert type(cid1) == int
self.is_not_empty()
# test that we don't add a second callback
cid2 = self.connect(self.signal, mini_me.dummy)
assert cid1 == cid2
self.is_not_empty()
assert len(self.callbacks._func_cid_map) == 1
assert len(self.callbacks.callbacks) == 1
del mini_me
# check we now have no callbacks registered
self.is_empty()
def dummy(self):
pass
def test_pickling(self):
assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())),
"callbacks")
def raising_cb_reg(func):
class TestException(Exception):
pass
def raising_function():
raise RuntimeError
def transformer(excp):
if isinstance(excp, RuntimeError):
raise TestException
raise excp
# default behavior
cb = cbook.CallbackRegistry()
cb.connect('foo', raising_function)
# old default
cb_old = cbook.CallbackRegistry(exception_handler=None)
cb_old.connect('foo', raising_function)
# filter
cb_filt = cbook.CallbackRegistry(exception_handler=transformer)
cb_filt.connect('foo', raising_function)
return pytest.mark.parametrize('cb, excp',
[[cb, None],
[cb_old, RuntimeError],
[cb_filt, TestException]])(func)
@raising_cb_reg
def test_callbackregistry_process_exception(cb, excp):
if excp is not None:
with pytest.raises(excp):
cb.process('foo')
else:
cb.process('foo')
def test_sanitize_sequence():
d = {'a': 1, 'b': 2, 'c': 3}
k = ['a', 'b', 'c']
v = [1, 2, 3]
i = [('a', 1), ('b', 2), ('c', 3)]
assert k == sorted(cbook.sanitize_sequence(d.keys()))
assert v == sorted(cbook.sanitize_sequence(d.values()))
assert i == sorted(cbook.sanitize_sequence(d.items()))
assert i == cbook.sanitize_sequence(i)
assert k == cbook.sanitize_sequence(k)
fail_mapping = (
({'a': 1}, {'forbidden': ('a')}),
({'a': 1}, {'required': ('b')}),
({'a': 1, 'b': 2}, {'required': ('a'), 'allowed': ()})
)
warn_passing_mapping = (
({'a': 1, 'b': 2}, {'a': 1}, {'alias_mapping': {'a': ['b']}}, 1),
({'a': 1, 'b': 2}, {'a': 1},
{'alias_mapping': {'a': ['b']}, 'allowed': ('a',)}, 1),
({'a': 1, 'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}, 1),
({'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'c': 3},
{'alias_mapping': {'a': ['b']}, 'required': ('a', )}, 1),
)
pass_mapping = (
({'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}),
({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
({'b': 2}, {'a': 2},
{'alias_mapping': {'a': ['b']}, 'forbidden': ('b', )}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3},
{'required': ('a', ), 'allowed': ('c', )}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3},
{'required': ('a', 'c'), 'allowed': ('c', )}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3},
{'required': ('a', 'c'), 'allowed': ('a', 'c')}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3},
{'required': ('a', 'c'), 'allowed': ()}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c')}),
({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'allowed': ('a', 'c')}),
)
@pytest.mark.parametrize('inp, kwargs_to_norm', fail_mapping)
def test_normalize_kwargs_fail(inp, kwargs_to_norm):
with pytest.raises(TypeError):
cbook.normalize_kwargs(inp, **kwargs_to_norm)
@pytest.mark.parametrize('inp, expected, kwargs_to_norm, warn_count',
warn_passing_mapping)
def test_normalize_kwargs_warn(inp, expected, kwargs_to_norm, warn_count):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm)
assert len(w) == warn_count
@pytest.mark.parametrize('inp, expected, kwargs_to_norm',
pass_mapping)
def test_normalize_kwargs_pass(inp, expected, kwargs_to_norm):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm)
assert len(w) == 0
def test_warn_external_frame_embedded_python():
with patch.object(cbook, "sys") as mock_sys:
mock_sys._getframe = Mock(return_value=None)
with warnings.catch_warnings(record=True) as w:
cbook._warn_external("dummy")
assert len(w) == 1
assert str(w[0].message) == "dummy"
def test_to_prestep():
x = np.arange(4)
y1 = np.arange(4)
y2 = np.arange(4)[::-1]
xs, y1s, y2s = cbook.pts_to_prestep(x, y1, y2)
x_target = np.asarray([0, 0, 1, 1, 2, 2, 3], dtype='float')
y1_target = np.asarray([0, 1, 1, 2, 2, 3, 3], dtype='float')
y2_target = np.asarray([3, 2, 2, 1, 1, 0, 0], dtype='float')
assert_array_equal(x_target, xs)
assert_array_equal(y1_target, y1s)
assert_array_equal(y2_target, y2s)
xs, y1s = cbook.pts_to_prestep(x, y1)
assert_array_equal(x_target, xs)
assert_array_equal(y1_target, y1s)
def test_to_prestep_empty():
steps = cbook.pts_to_prestep([], [])
assert steps.shape == (2, 0)
def test_to_poststep():
x = np.arange(4)
y1 = np.arange(4)
y2 = np.arange(4)[::-1]
xs, y1s, y2s = cbook.pts_to_poststep(x, y1, y2)
x_target = np.asarray([0, 1, 1, 2, 2, 3, 3], dtype='float')
y1_target = np.asarray([0, 0, 1, 1, 2, 2, 3], dtype='float')
y2_target = np.asarray([3, 3, 2, 2, 1, 1, 0], dtype='float')
assert_array_equal(x_target, xs)
assert_array_equal(y1_target, y1s)
assert_array_equal(y2_target, y2s)
xs, y1s = cbook.pts_to_poststep(x, y1)
assert_array_equal(x_target, xs)
assert_array_equal(y1_target, y1s)
def test_to_poststep_empty():
steps = cbook.pts_to_poststep([], [])
assert steps.shape == (2, 0)
def test_to_midstep():
x = np.arange(4)
y1 = np.arange(4)
y2 = np.arange(4)[::-1]
xs, y1s, y2s = cbook.pts_to_midstep(x, y1, y2)
x_target = np.asarray([0, .5, .5, 1.5, 1.5, 2.5, 2.5, 3], dtype='float')
y1_target = np.asarray([0, 0, 1, 1, 2, 2, 3, 3], dtype='float')
y2_target = np.asarray([3, 3, 2, 2, 1, 1, 0, 0], dtype='float')
assert_array_equal(x_target, xs)
assert_array_equal(y1_target, y1s)
assert_array_equal(y2_target, y2s)
xs, y1s = cbook.pts_to_midstep(x, y1)
assert_array_equal(x_target, xs)
assert_array_equal(y1_target, y1s)
def test_to_midstep_empty():
steps = cbook.pts_to_midstep([], [])
assert steps.shape == (2, 0)
@pytest.mark.parametrize(
"args",
[(np.arange(12).reshape(3, 4), 'a'),
(np.arange(12), 'a'),
(np.arange(12), np.arange(3))])
def test_step_fails(args):
with pytest.raises(ValueError):
cbook.pts_to_prestep(*args)
def test_grouper():
class dummy():
pass
a, b, c, d, e = objs = [dummy() for j in range(5)]
g = cbook.Grouper()
g.join(*objs)
assert set(list(g)[0]) == set(objs)
assert set(g.get_siblings(a)) == set(objs)
for other in objs[1:]:
assert g.joined(a, other)
g.remove(a)
for other in objs[1:]:
assert not g.joined(a, other)
for A, B in itertools.product(objs[1:], objs[1:]):
assert g.joined(A, B)
def test_grouper_private():
class dummy():
pass
objs = [dummy() for j in range(5)]
g = cbook.Grouper()
g.join(*objs)
# reach in and touch the internals !
mapping = g._mapping
for o in objs:
assert ref(o) in mapping
base_set = mapping[ref(objs[0])]
for o in objs[1:]:
assert mapping[ref(o)] is base_set
def test_flatiter():
x = np.arange(5)
it = x.flat
assert 0 == next(it)
assert 1 == next(it)
ret = cbook.safe_first_element(it)
assert ret == 0
assert 0 == next(it)
assert 1 == next(it)
def test_reshape2d():
class dummy():
pass
xnew = cbook._reshape_2D([], 'x')
assert np.shape(xnew) == (1, 0)
x = [dummy() for j in range(5)]
xnew = cbook._reshape_2D(x, 'x')
assert np.shape(xnew) == (1, 5)
x = np.arange(5)
xnew = cbook._reshape_2D(x, 'x')
assert np.shape(xnew) == (1, 5)
x = [[dummy() for j in range(5)] for i in range(3)]
xnew = cbook._reshape_2D(x, 'x')
assert np.shape(xnew) == (3, 5)
# this is strange behaviour, but...
x = np.random.rand(3, 5)
xnew = cbook._reshape_2D(x, 'x')
assert np.shape(xnew) == (5, 3)
# Now test with a list of lists with different lengths, which means the
# array will internally be converted to a 1D object array of lists
x = [[1, 2, 3], [3, 4], [2]]
xnew = cbook._reshape_2D(x, 'x')
assert isinstance(xnew, list)
assert isinstance(xnew[0], np.ndarray) and xnew[0].shape == (3,)
assert isinstance(xnew[1], np.ndarray) and xnew[1].shape == (2,)
assert isinstance(xnew[2], np.ndarray) and xnew[2].shape == (1,)
# We now need to make sure that this works correctly for Numpy subclasses
# where iterating over items can return subclasses too, which may be
# iterable even if they are scalars. To emulate this, we make a Numpy
# array subclass that returns Numpy 'scalars' when iterating or accessing
# values, and these are technically iterable if checking for example
# isinstance(x, collections.abc.Iterable).
class ArraySubclass(np.ndarray):
def __iter__(self):
for value in super().__iter__():
yield np.array(value)
def __getitem__(self, item):
return np.array(super().__getitem__(item))
v = np.arange(10, dtype=float)
x = ArraySubclass((10,), dtype=float, buffer=v.data)
xnew = cbook._reshape_2D(x, 'x')
# We check here that the array wasn't split up into many individual
# ArraySubclass, which is what used to happen due to a bug in _reshape_2D
assert len(xnew) == 1
assert isinstance(xnew[0], ArraySubclass)
def test_contiguous_regions():
a, b, c = 3, 4, 5
# Starts and ends with True
mask = [True]*a + [False]*b + [True]*c
expected = [(0, a), (a+b, a+b+c)]
assert cbook.contiguous_regions(mask) == expected
d, e = 6, 7
# Starts with True ends with False
mask = mask + [False]*e
assert cbook.contiguous_regions(mask) == expected
# Starts with False ends with True
mask = [False]*d + mask[:-e]
expected = [(d, d+a), (d+a+b, d+a+b+c)]
assert cbook.contiguous_regions(mask) == expected
# Starts and ends with False
mask = mask + [False]*e
assert cbook.contiguous_regions(mask) == expected
# No True in mask
assert cbook.contiguous_regions([False]*5) == []
# Empty mask
assert cbook.contiguous_regions([]) == []
def test_safe_first_element_pandas_series(pd):
# deliberately create a pandas series with index not starting from 0
s = pd.Series(range(5), index=range(10, 15))
actual = cbook.safe_first_element(s)
assert actual == 0
def test_make_keyword_only(recwarn):
@cbook._make_keyword_only("3.0", "arg")
def func(pre, arg, post=None):
pass
func(1, arg=2)
assert len(recwarn) == 0
with pytest.warns(MatplotlibDeprecationWarning):
func(1, 2)
with pytest.warns(MatplotlibDeprecationWarning):
func(1, 2, 3)

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