Skip to content

Commit e6c1308

Browse files
authored
Merge pull request #196 from astrofrog/deterministic-option
Started implementing support for deterministic figure output
2 parents d5ed60c + 21d0653 commit e6c1308

File tree

3 files changed

+54
-90
lines changed

3 files changed

+54
-90
lines changed

Diff for: README.rst

+5-59
Original file line numberDiff line numberDiff line change
@@ -288,69 +288,15 @@ Inkscape is required for SVG comparison.
288288

289289
By default, Matplotlib does not produce deterministic output that will have a
290290
consistent hash every time it is run, or over different Matplotlib versions. In
291-
order to enforce that the output is deterministic, you will need to set metadata
292-
as described in the following subsections.
293-
294-
PNG
295-
^^^
296-
297-
For PNG files, the output can be made deterministic by setting:
298-
299-
.. code:: python
300-
301-
@pytest.mark.mpl_image_compare(savefig_kwargs={'metadata': {"Software": None}})
302-
303-
PDF
304-
^^^
305-
306-
For PDF files, the output can be made deterministic by setting:
307-
308-
.. code:: python
309-
310-
@pytest.mark.mpl_image_compare(savefig_kwargs={'format': 'pdf',
311-
'metadata': {"Creator": None,
312-
"Producer": None,
313-
"CreationDate": None}})
314-
315-
Note that deterministic PDF output can only be achieved with Matplotlib 2.1 and above
316-
317-
EPS
318-
^^^
319-
320-
For PDF files, the output can be made deterministic by setting:
321-
322-
.. code:: python
323-
324-
@pytest.mark.mpl_image_compare(savefig_kwargs={'format': 'pdf',
325-
'metadata': {"Creator": "test"})
326-
327-
and in addition you will need to set the SOURCE_DATE_EPOCH environment variable to
328-
a constant value (this is a unit timestamp):
329-
330-
.. code:: python
331-
332-
os.environ['SOURCE_DATE_EPOCH'] = '1680254601'
333-
334-
You could do this inside the test.
335-
336-
Note that deterministic PDF output can only be achieved with Matplotlib 2.1 and above
337-
338-
SVG
339-
^^^
340-
341-
For SVG files, the output can be made deterministic by setting:
342-
343-
.. code:: python
344-
345-
@pytest.mark.mpl_image_compare(savefig_kwargs={'metadata': '{"Date": None}})
346-
347-
and in addition, you should make sure the following rcParam is set to a constant string:
291+
order to enforce that the output is deterministic, you can set the ``deterministic``
292+
keyword argument in ``mpl_image_compare``:
348293

349294
.. code:: python
350295
351-
plt.rcParams['svg.hashsalt'] = 'test'
296+
@pytest.mark.mpl_image_compare(deterministic=True)
352297
353-
Note that SVG files can only be used in pytest-mpl with Matplotlib 3.3 and above.
298+
This does a number of things such as e.g., setting the creation date in the
299+
metadata to be constant, and avoids hard-coding the Matplotlib in the files.
354300

355301
Test failure example
356302
--------------------

Diff for: pytest_mpl/plugin.py

+47-13
Original file line numberDiff line numberDiff line change
@@ -431,16 +431,13 @@ def generate_baseline_image(self, item, fig):
431431
"""
432432
Generate reference figures.
433433
"""
434-
compare = get_compare(item)
435-
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
436434

437435
if not os.path.exists(self.generate_dir):
438436
os.makedirs(self.generate_dir)
439437

440438
baseline_filename = self.generate_filename(item)
441439
baseline_path = (self.generate_dir / baseline_filename).absolute()
442-
fig.savefig(str(baseline_path), **savefig_kwargs)
443-
440+
self.save_figure(item, fig, baseline_path)
444441
close_mpl_figure(fig)
445442

446443
return baseline_path
@@ -450,13 +447,9 @@ def generate_image_hash(self, item, fig):
450447
For a `matplotlib.figure.Figure`, returns the SHA256 hash as a hexadecimal
451448
string.
452449
"""
453-
compare = get_compare(item)
454-
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
455-
456-
ext = self._file_extension(item)
457450

458451
imgdata = io.BytesIO()
459-
fig.savefig(imgdata, **savefig_kwargs)
452+
self.save_figure(item, fig, imgdata)
460453
out = _hash_file(imgdata)
461454
imgdata.close()
462455

@@ -475,14 +468,13 @@ def compare_image_to_baseline(self, item, fig, result_dir, summary=None):
475468

476469
compare = get_compare(item)
477470
tolerance = compare.kwargs.get('tolerance', 2)
478-
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
479471

480472
ext = self._file_extension(item)
481473

482474
baseline_image_ref = self.obtain_baseline_image(item, result_dir)
483475

484476
test_image = (result_dir / f"result.{ext}").absolute()
485-
fig.savefig(str(test_image), **savefig_kwargs)
477+
self.save_figure(item, fig, test_image)
486478

487479
if ext in ['png', 'svg']: # Use original file
488480
summary['result_image'] = test_image.relative_to(self.results_dir).as_posix()
@@ -554,13 +546,55 @@ def load_hash_library(self, library_path):
554546
with open(str(library_path)) as fp:
555547
return json.load(fp)
556548

549+
def save_figure(self, item, fig, filename):
550+
if isinstance(filename, Path):
551+
filename = str(filename)
552+
compare = get_compare(item)
553+
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
554+
deterministic = compare.kwargs.get('deterministic', False)
555+
556+
original_source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH', None)
557+
558+
extra_rcparams = {}
559+
560+
if deterministic:
561+
562+
# Make sure we don't modify the original dictionary in case is a common
563+
# object used by different tests
564+
savefig_kwargs = savefig_kwargs.copy()
565+
566+
if 'metadata' not in savefig_kwargs:
567+
savefig_kwargs['metadata'] = {}
568+
569+
ext = self._file_extension(item)
570+
571+
if ext == 'png':
572+
extra_metadata = {"Software": None}
573+
elif ext == 'pdf':
574+
extra_metadata = {"Creator": None, "Producer": None, "CreationDate": None}
575+
elif ext == 'eps':
576+
extra_metadata = {"Creator": "test"}
577+
os.environ['SOURCE_DATE_EPOCH'] = '1680254601'
578+
elif ext == 'svg':
579+
extra_metadata = {"Date": None}
580+
extra_rcparams["svg.hashsalt"] = "test"
581+
582+
savefig_kwargs['metadata'].update(extra_metadata)
583+
584+
import matplotlib.pyplot as plt
585+
586+
with plt.rc_context(rc=extra_rcparams):
587+
fig.savefig(filename, **savefig_kwargs)
588+
589+
if original_source_date_epoch is not None:
590+
os.environ['SOURCE_DATE_EPOCH'] = original_source_date_epoch
591+
557592
def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
558593
hash_comparison_pass = False
559594
if summary is None:
560595
summary = {}
561596

562597
compare = get_compare(item)
563-
savefig_kwargs = compare.kwargs.get('savefig_kwargs', {})
564598

565599
ext = self._file_extension(item)
566600

@@ -601,7 +635,7 @@ def compare_image_to_hash_library(self, item, fig, result_dir, summary=None):
601635

602636
# Save the figure for later summary (will be removed later if not needed)
603637
test_image = (result_dir / f"result.{ext}").absolute()
604-
fig.savefig(str(test_image), **savefig_kwargs)
638+
self.save_figure(item, fig, test_image)
605639
summary['result_image'] = test_image.relative_to(self.results_dir).as_posix()
606640

607641
# Hybrid mode (hash and image comparison)

Diff for: tests/test_pytest_mpl.py

+2-18
Original file line numberDiff line numberDiff line change
@@ -704,15 +704,6 @@ def test_formats(pytester, use_hash_library, passes, file_format):
704704
else:
705705
pytest.skip('Comparing EPS and PDF files requires ghostscript to be installed')
706706

707-
if file_format == 'png':
708-
metadata = '{"Software": None}'
709-
elif file_format == 'pdf':
710-
metadata = '{"Creator": None, "Producer": None, "CreationDate": None}'
711-
elif file_format == 'eps':
712-
metadata = '{"Creator": "test"}'
713-
elif file_format == 'svg':
714-
metadata = '{"Date": None}'
715-
716707
pytester.makepyfile(
717708
f"""
718709
import os
@@ -721,16 +712,9 @@ def test_formats(pytester, use_hash_library, passes, file_format):
721712
@pytest.mark.mpl_image_compare(baseline_dir=r"{baseline_dir_abs}",
722713
{f'hash_library=r"{hash_library}",' if use_hash_library else ''}
723714
tolerance={DEFAULT_TOLERANCE},
724-
savefig_kwargs={{'format': '{file_format}',
725-
'metadata': {metadata}}})
715+
deterministic=True,
716+
savefig_kwargs={{'format': '{file_format}'}})
726717
def test_format_{file_format}():
727-
728-
# For reproducible EPS output
729-
os.environ['SOURCE_DATE_EPOCH'] = '1680254601'
730-
731-
# For reproducible SVG output
732-
plt.rcParams['svg.hashsalt'] = 'test'
733-
734718
fig = plt.figure()
735719
ax = fig.add_subplot(1, 1, 1)
736720
ax.plot([{1 if passes else 3}, 2, 3])

0 commit comments

Comments
 (0)