From c6a46604e30f8d26ceef66cc1c816cc694488737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Thu, 16 Jun 2016 23:38:06 +0200 Subject: [PATCH 01/30] To allow reproducible output: * honour SOURCE_DATE_EPOCH for timestamps in PS and PDF files. See https://reproducible-builds.org/specs/source-date-epoch/ * get keys sorted so that hatchPatterns, images and markers are included with a reproducible order in the PDF file. See https://reproducible-builds.org/ --- lib/matplotlib/backends/backend_pdf.py | 45 +++++++++++++++++++++----- lib/matplotlib/backends/backend_ps.py | 9 +++++- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 21719273c423..a166c9a8f1aa 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -17,6 +17,7 @@ import time import warnings import zlib +import collections from io import BytesIO from functools import total_ordering @@ -24,7 +25,7 @@ from six import unichr -from datetime import datetime +from datetime import datetime, tzinfo, timedelta from math import ceil, cos, floor, pi, sin import matplotlib @@ -135,6 +136,20 @@ def _string_escape(match): assert False +# tzinfo class for UTC +class UTCtimezone(tzinfo): + """UTC timezone""" + + def utcoffset(self, dt): + return timedelta(0) + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return timedelta(0) + + def pdfRepr(obj): """Map Python objects to PDF syntax.""" @@ -202,10 +217,14 @@ def pdfRepr(obj): # A date. elif isinstance(obj, datetime): r = obj.strftime('D:%Y%m%d%H%M%S') - if time.daylight: - z = time.altzone + z = obj.utcoffset() + if z is not None: + z = z.seconds else: - z = time.timezone + if time.daylight: + z = time.altzone + else: + z = time.timezone if z == 0: r += 'Z' elif z < 0: @@ -468,10 +487,19 @@ def __init__(self, filename): self.writeObject(self.rootObject, root) revision = '' + # get source date from SOURCE_DATE_EPOCH, if set + # See https://reproducible-builds.org/specs/source-date-epoch/ + source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") + if source_date_epoch: + source_date = datetime.utcfromtimestamp(int(source_date_epoch)) + source_date = source_date.replace(tzinfo=UTCtimezone()) + else: + source_date = datetime.today() + self.infoDict = { 'Creator': 'matplotlib %s, http://matplotlib.org' % __version__, 'Producer': 'matplotlib pdf backend%s' % revision, - 'CreationDate': datetime.today() + 'CreationDate': source_date } self.fontNames = {} # maps filenames to internal font names @@ -483,14 +511,15 @@ def __init__(self, filename): self.alphaStates = {} # maps alpha values to graphics state objects self.nextAlphaState = 1 - self.hatchPatterns = {} + # reproducible writeHatches needs an ordered dict: + self.hatchPatterns = collections.OrderedDict() self.nextHatch = 1 self.gouraudTriangles = [] - self._images = {} + self._images = collections.OrderedDict() # reproducible writeImages self.nextImage = 1 - self.markers = {} + self.markers = collections.OrderedDict() # reproducible writeMarkers self.multi_byte_charprocs = {} self.paths = [] diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index b5d284cfffbd..76b43fcb47a4 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1087,7 +1087,14 @@ def print_figure_impl(): if title: print("%%Title: "+title, file=fh) print(("%%Creator: matplotlib version " +__version__+", http://matplotlib.org/"), file=fh) - print("%%CreationDate: "+time.ctime(time.time()), file=fh) + # get source date from SOURCE_DATE_EPOCH, if set + # See https://reproducible-builds.org/specs/source-date-epoch/ + source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") + if source_date_epoch: + source_date = time.asctime(time.gmtime(int(source_date_epoch))) + else: + source_date = time.ctime() + print("%%CreationDate: "+source_date, file=fh) print("%%Orientation: " + orientation, file=fh) if not isEPSF: print("%%DocumentPaperSizes: "+papertype, file=fh) print("%%%%BoundingBox: %d %d %d %d" % bbox, file=fh) From 4e477dc381172f145f1d28f322ee78d7a33f3624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Sat, 18 Jun 2016 17:42:21 +0200 Subject: [PATCH 02/30] Reproducible PDF output: sort TTF characters --- lib/matplotlib/backends/backend_pdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a166c9a8f1aa..c6fe7c24a1f5 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -949,7 +949,8 @@ def get_char_width(charcode): rawcharprocs = ttconv.get_pdf_charprocs( filename.encode(sys.getfilesystemencoding()), glyph_ids) charprocs = {} - for charname, stream in six.iteritems(rawcharprocs): + for charname in sorted(rawcharprocs): + stream = rawcharprocs[charname] charprocDict = {'Length': len(stream)} # The 2-byte characters are used as XObjects, so they # need extra info in their dictionary From 946ae4541ae5eaba015cf2a50ce82c511e524313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Sat, 18 Jun 2016 17:58:24 +0200 Subject: [PATCH 03/30] Reproducible PDF output: sort fonts --- lib/matplotlib/backends/backend_pdf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index c6fe7c24a1f5..a15e13c3a612 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -669,7 +669,8 @@ def fontName(self, fontprop): def writeFonts(self): fonts = {} - for filename, Fx in six.iteritems(self.fontNames): + for filename in sorted(self.fontNames): + Fx = self.fontNames[filename] matplotlib.verbose.report('Embedding font %s' % filename, 'debug') if filename.endswith('.afm'): # from pdf.use14corefonts From 22f71a28271b6e849634f093564fef9dc22df706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Sat, 18 Jun 2016 18:07:21 +0200 Subject: [PATCH 04/30] Tests for determinist PDF output: - SOURCE_DATE_EPOCH support - reproducible output --- lib/matplotlib/tests/test_backend_pdf.py | 131 +++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 2feee6fb1238..e0a9a53546ad 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -109,6 +109,137 @@ def test_composite_image(): assert len(pdf._file._images.keys()) == 2 +@cleanup +def test_source_date_epoch(): + # Test SOURCE_DATE_EPOCH support + try: + # save current value of SOURCE_DATE_EPOCH + sde = os.environ.pop('SOURCE_DATE_EPOCH',None) + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + x = [1, 2, 3, 4, 5] + ax.plot(x, x) + os.environ['SOURCE_DATE_EPOCH'] = "946684800" + with io.BytesIO() as pdf: + fig.savefig(pdf, format="pdf") + pdf.seek(0) + buff = pdf.read() + assert b"/CreationDate (D:20000101000000Z)" in buff + os.environ.pop('SOURCE_DATE_EPOCH',None) + with io.BytesIO() as pdf: + fig.savefig(pdf, format="pdf") + pdf.seek(0) + buff = pdf.read() + assert not b"/CreationDate (D:20000101000000Z)" in buff + finally: + # Restores SOURCE_DATE_EPOCH + if sde == None: + os.environ.pop('SOURCE_DATE_EPOCH',None) + else: + os.environ['SOURCE_DATE_EPOCH'] = sde + + +def _test_determinism_save(filename, objects=''): + # save current value of SOURCE_DATE_EPOCH and set it + # to a constant value, so that time difference is not + # taken into account + sde = os.environ.pop('SOURCE_DATE_EPOCH',None) + os.environ['SOURCE_DATE_EPOCH'] = "946684800" + + fig = plt.figure() + + if 'm' in objects: + # use different markers, to be recorded in the PdfFile object + ax1 = fig.add_subplot(1, 6, 1) + x = range(10) + ax1.plot(x, [1] * 10, marker=u'D') + ax1.plot(x, [2] * 10, marker=u'x') + ax1.plot(x, [3] * 10, marker=u'^') + ax1.plot(x, [4] * 10, marker=u'H') + ax1.plot(x, [5] * 10, marker=u'v') + + if 'h' in objects: + # also use different hatch patterns + ax2 = fig.add_subplot(1, 6, 2) + bars = ax2.bar(range(1, 5), range(1, 5)) + \ + ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5)) + ax2.set_xticks([1.5, 2.5, 3.5, 4.5]) + + patterns = ('-', '+', 'x', '\\', '*', 'o', 'O', '.') + for bar, pattern in zip(bars, patterns): + bar.set_hatch(pattern) + + if 'i' in objects: + # also use different images + A = [[1, 2, 3], [2, 3, 1], [3, 1, 2]] + fig.add_subplot(1, 6, 3).imshow(A, interpolation='nearest') + A = [[1, 3, 2], [1, 2, 3], [3, 1, 2]] + fig.add_subplot(1, 6, 4).imshow(A, interpolation='bilinear') + A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]] + fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic') + + x=range(5) + fig.add_subplot(1, 6, 6).plot(x,x) + + fig.savefig(filename, format="pdf") + + # Restores SOURCE_DATE_EPOCH + if sde == None: + os.environ.pop('SOURCE_DATE_EPOCH',None) + else: + os.environ['SOURCE_DATE_EPOCH'] = sde + + +def _test_determinism(objects=''): + import sys + from subprocess import check_call + from nose.tools import assert_equal + filename = 'determinism_O%s.pdf' % objects + plots = [] + for i in range(3): + check_call([sys.executable, '-R', '-c', + 'import matplotlib; ' + 'matplotlib.use("pdf"); ' + 'from matplotlib.tests.test_backend_pdf ' + 'import _test_determinism_save;' + '_test_determinism_save(%r,%r)' % (filename,objects)]) + with open(filename, 'rb') as fd: + plots.append(fd.read()) + os.unlink(filename) + for p in plots[1:]: + assert_equal(p,plots[0]) + + +@cleanup +def test_determinism_plain(): + """Test for reproducible PDF output: simple figure""" + _test_determinism() + + +@cleanup +def test_determinism_images(): + """Test for reproducible PDF output: figure with different images""" + _test_determinism('i') + + +@cleanup +def test_determinism_hatches(): + """Test for reproducible PDF output: figure with different hatches""" + _test_determinism('h') + + +@cleanup +def test_determinism_markers(): + """Test for reproducible PDF output: figure with different markers""" + _test_determinism('m') + + +@cleanup +def test_determinism_all(): + """Test for reproducible PDF output""" + _test_determinism('mhi') + + @image_comparison(baseline_images=['hatching_legend'], extensions=['pdf']) def test_hatching_legend(): From ad660d721f9adba1f716506de8b28e9695565583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Mon, 27 Jun 2016 20:35:29 +0200 Subject: [PATCH 05/30] Share determinism test code for PS and PDF output. --- lib/matplotlib/testing/determinism.py | 133 +++++++++++++++++++++++ lib/matplotlib/tests/test_backend_pdf.py | 111 ++----------------- lib/matplotlib/tests/test_backend_ps.py | 12 ++ 3 files changed, 153 insertions(+), 103 deletions(-) create mode 100644 lib/matplotlib/testing/determinism.py diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py new file mode 100644 index 000000000000..f32286d7803c --- /dev/null +++ b/lib/matplotlib/testing/determinism.py @@ -0,0 +1,133 @@ +""" +Provides utilities to test output reproducibility. +""" + +import io +import os + +from matplotlib import pyplot as plt + + +def _test_determinism_save(filename, objects='mhi', format="pdf"): + # save current value of SOURCE_DATE_EPOCH and set it + # to a constant value, so that time difference is not + # taken into account + sde = os.environ.pop('SOURCE_DATE_EPOCH', None) + os.environ['SOURCE_DATE_EPOCH'] = "946684800" + + fig = plt.figure() + + if 'm' in objects: + # use different markers... + ax1 = fig.add_subplot(1, 6, 1) + x = range(10) + ax1.plot(x, [1] * 10, marker=u'D') + ax1.plot(x, [2] * 10, marker=u'x') + ax1.plot(x, [3] * 10, marker=u'^') + ax1.plot(x, [4] * 10, marker=u'H') + ax1.plot(x, [5] * 10, marker=u'v') + + if 'h' in objects: + # also use different hatch patterns + ax2 = fig.add_subplot(1, 6, 2) + bars = ax2.bar(range(1, 5), range(1, 5)) + \ + ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5)) + ax2.set_xticks([1.5, 2.5, 3.5, 4.5]) + + patterns = ('-', '+', 'x', '\\', '*', 'o', 'O', '.') + for bar, pattern in zip(bars, patterns): + bar.set_hatch(pattern) + + if 'i' in objects: + # also use different images + A = [[1, 2, 3], [2, 3, 1], [3, 1, 2]] + fig.add_subplot(1, 6, 3).imshow(A, interpolation='nearest') + A = [[1, 3, 2], [1, 2, 3], [3, 1, 2]] + fig.add_subplot(1, 6, 4).imshow(A, interpolation='bilinear') + A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]] + fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic') + + x = range(5) + fig.add_subplot(1, 6, 6).plot(x, x) + + fig.savefig(filename, format=format) + + # Restores SOURCE_DATE_EPOCH + if sde is None: + os.environ.pop('SOURCE_DATE_EPOCH', None) + else: + os.environ['SOURCE_DATE_EPOCH'] = sde + + +def _test_determinism(objects='mhi', format="pdf"): + """ + Output three times the same graphs and checks that the outputs are exactly + the same. + + Parameters + ---------- + objects : str + contains characters corresponding to objects to be included in the test + document: 'm' for markers, 'h' for hatch patterns, 'i' for images. The + default value is "mhi", so that the test includes all these objects. + format : str + format string. The default value is "pdf". + """ + import sys + from subprocess import check_call + from nose.tools import assert_equal + filename = 'determinism_O%s.%s' % (objects, format) + plots = [] + for i in range(3): + check_call([sys.executable, '-R', '-c', + 'import matplotlib; ' + 'matplotlib.use(%r); ' + 'from matplotlib.testing.determinism ' + 'import _test_determinism_save;' + '_test_determinism_save(%r,%r,%r)' + % (format, filename, objects, format)]) + with open(filename, 'rb') as fd: + plots.append(fd.read()) + os.unlink(filename) + for p in plots[1:]: + assert_equal(p, plots[0]) + +def _test_source_date_epoch(format, string): + """ + Test SOURCE_DATE_EPOCH support. Output a document with the envionment + variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the + document contains the timestamp that corresponds to this date (given as an + argument). + + Parameters + ---------- + format : str + format string, such as "pdf". + string : str + timestamp string for 2000-01-01 00:00 UTC. + """ + try: + # save current value of SOURCE_DATE_EPOCH + sde = os.environ.pop('SOURCE_DATE_EPOCH', None) + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + x = [1, 2, 3, 4, 5] + ax.plot(x, x) + os.environ['SOURCE_DATE_EPOCH'] = "946684800" + with io.BytesIO() as output: + fig.savefig(output, format=format) + output.seek(0) + buff = output.read() + assert string in buff + os.environ.pop('SOURCE_DATE_EPOCH', None) + with io.BytesIO() as output: + fig.savefig(output, format=format) + output.seek(0) + buff = output.read() + assert string not in buff + finally: + # Restores SOURCE_DATE_EPOCH + if sde is None: + os.environ.pop('SOURCE_DATE_EPOCH', None) + else: + os.environ['SOURCE_DATE_EPOCH'] = sde diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index e0a9a53546ad..4b038a3e685f 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -12,6 +12,7 @@ from matplotlib import cm, rcParams from matplotlib.backends.backend_pdf import PdfPages from matplotlib import pyplot as plt +from matplotlib.testing.determinism import _test_source_date_epoch, _test_determinism from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) @@ -111,133 +112,37 @@ def test_composite_image(): @cleanup def test_source_date_epoch(): - # Test SOURCE_DATE_EPOCH support - try: - # save current value of SOURCE_DATE_EPOCH - sde = os.environ.pop('SOURCE_DATE_EPOCH',None) - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - x = [1, 2, 3, 4, 5] - ax.plot(x, x) - os.environ['SOURCE_DATE_EPOCH'] = "946684800" - with io.BytesIO() as pdf: - fig.savefig(pdf, format="pdf") - pdf.seek(0) - buff = pdf.read() - assert b"/CreationDate (D:20000101000000Z)" in buff - os.environ.pop('SOURCE_DATE_EPOCH',None) - with io.BytesIO() as pdf: - fig.savefig(pdf, format="pdf") - pdf.seek(0) - buff = pdf.read() - assert not b"/CreationDate (D:20000101000000Z)" in buff - finally: - # Restores SOURCE_DATE_EPOCH - if sde == None: - os.environ.pop('SOURCE_DATE_EPOCH',None) - else: - os.environ['SOURCE_DATE_EPOCH'] = sde - - -def _test_determinism_save(filename, objects=''): - # save current value of SOURCE_DATE_EPOCH and set it - # to a constant value, so that time difference is not - # taken into account - sde = os.environ.pop('SOURCE_DATE_EPOCH',None) - os.environ['SOURCE_DATE_EPOCH'] = "946684800" - - fig = plt.figure() - - if 'm' in objects: - # use different markers, to be recorded in the PdfFile object - ax1 = fig.add_subplot(1, 6, 1) - x = range(10) - ax1.plot(x, [1] * 10, marker=u'D') - ax1.plot(x, [2] * 10, marker=u'x') - ax1.plot(x, [3] * 10, marker=u'^') - ax1.plot(x, [4] * 10, marker=u'H') - ax1.plot(x, [5] * 10, marker=u'v') - - if 'h' in objects: - # also use different hatch patterns - ax2 = fig.add_subplot(1, 6, 2) - bars = ax2.bar(range(1, 5), range(1, 5)) + \ - ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5)) - ax2.set_xticks([1.5, 2.5, 3.5, 4.5]) - - patterns = ('-', '+', 'x', '\\', '*', 'o', 'O', '.') - for bar, pattern in zip(bars, patterns): - bar.set_hatch(pattern) - - if 'i' in objects: - # also use different images - A = [[1, 2, 3], [2, 3, 1], [3, 1, 2]] - fig.add_subplot(1, 6, 3).imshow(A, interpolation='nearest') - A = [[1, 3, 2], [1, 2, 3], [3, 1, 2]] - fig.add_subplot(1, 6, 4).imshow(A, interpolation='bilinear') - A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]] - fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic') - - x=range(5) - fig.add_subplot(1, 6, 6).plot(x,x) - - fig.savefig(filename, format="pdf") - - # Restores SOURCE_DATE_EPOCH - if sde == None: - os.environ.pop('SOURCE_DATE_EPOCH',None) - else: - os.environ['SOURCE_DATE_EPOCH'] = sde - - -def _test_determinism(objects=''): - import sys - from subprocess import check_call - from nose.tools import assert_equal - filename = 'determinism_O%s.pdf' % objects - plots = [] - for i in range(3): - check_call([sys.executable, '-R', '-c', - 'import matplotlib; ' - 'matplotlib.use("pdf"); ' - 'from matplotlib.tests.test_backend_pdf ' - 'import _test_determinism_save;' - '_test_determinism_save(%r,%r)' % (filename,objects)]) - with open(filename, 'rb') as fd: - plots.append(fd.read()) - os.unlink(filename) - for p in plots[1:]: - assert_equal(p,plots[0]) - + """Test SOURCE_DATE_EPOCH support for PDF output""" + _test_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)") @cleanup def test_determinism_plain(): """Test for reproducible PDF output: simple figure""" - _test_determinism() + _test_determinism('', format="pdf") @cleanup def test_determinism_images(): """Test for reproducible PDF output: figure with different images""" - _test_determinism('i') + _test_determinism('i', format="pdf") @cleanup def test_determinism_hatches(): """Test for reproducible PDF output: figure with different hatches""" - _test_determinism('h') + _test_determinism('h', format="pdf") @cleanup def test_determinism_markers(): """Test for reproducible PDF output: figure with different markers""" - _test_determinism('m') + _test_determinism('m', format="pdf") @cleanup def test_determinism_all(): """Test for reproducible PDF output""" - _test_determinism('mhi') + _test_determinism(format="pdf") @image_comparison(baseline_images=['hatching_legend'], diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index f017164ff7bb..f93f0bcce7e3 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -11,6 +11,7 @@ import matplotlib import matplotlib.pyplot as plt from matplotlib import patheffects +from matplotlib.testing.determinism import _test_source_date_epoch, _test_determinism from matplotlib.testing.decorators import cleanup, knownfailureif @@ -173,6 +174,17 @@ def test_tilde_in_tempfilename(): # do not break if this is not removeable... print(e) +@cleanup +def test_source_date_epoch(): + """Test SOURCE_DATE_EPOCH support for PS output""" + _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") + +@cleanup +def test_determinism_all(): + """Test for reproducible PS output""" + _test_determinism(format="ps") + + if __name__ == '__main__': import nose From 37c28b4088992e9c2f8841cb64e921331386ce90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Mon, 27 Jun 2016 20:59:42 +0200 Subject: [PATCH 06/30] Reproducible PS/tex output. --- lib/matplotlib/backends/backend_ps.py | 9 ++++++++- lib/matplotlib/tests/test_backend_ps.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 76b43fcb47a4..1659cf41a70e 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1277,7 +1277,14 @@ def write(self, *kl, **kwargs): if title: print("%%Title: "+title, file=fh) print(("%%Creator: matplotlib version " +__version__+", http://matplotlib.org/"), file=fh) - print("%%CreationDate: "+time.ctime(time.time()), file=fh) + # get source date from SOURCE_DATE_EPOCH, if set + # See https://reproducible-builds.org/specs/source-date-epoch/ + source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") + if source_date_epoch: + source_date = time.asctime(time.gmtime(int(source_date_epoch))) + else: + source_date = time.ctime() + print("%%CreationDate: "+source_date, file=fh) print("%%%%BoundingBox: %d %d %d %d" % bbox, file=fh) print("%%EndComments", file=fh) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index f93f0bcce7e3..ea763a68d067 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -179,11 +179,27 @@ def test_source_date_epoch(): """Test SOURCE_DATE_EPOCH support for PS output""" _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") +@cleanup +@needs_tex +@needs_ghostscript +def test_source_date_epoch_tex(): + """Test SOURCE_DATE_EPOCH support for PS/tex output""" + matplotlib.rcParams['text.usetex'] = True + _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") + @cleanup def test_determinism_all(): """Test for reproducible PS output""" _test_determinism(format="ps") +@cleanup +@needs_tex +@needs_ghostscript +def test_determinism_all_tex(): + """Test for reproducible PS/tex output""" + matplotlib.rcParams['text.usetex'] = True + _test_determinism(format="ps") + if __name__ == '__main__': From 2e1c773a818845dbd8646c4c5917665d738cd969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Mon, 27 Jun 2016 21:30:55 +0200 Subject: [PATCH 07/30] Removes test_source_date_epoch_tex test, since this is ghostscript dependent. --- lib/matplotlib/tests/test_backend_ps.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index ea763a68d067..408f12ee301f 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -179,13 +179,9 @@ def test_source_date_epoch(): """Test SOURCE_DATE_EPOCH support for PS output""" _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") -@cleanup -@needs_tex -@needs_ghostscript -def test_source_date_epoch_tex(): - """Test SOURCE_DATE_EPOCH support for PS/tex output""" - matplotlib.rcParams['text.usetex'] = True - _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") +# 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. @cleanup def test_determinism_all(): From 541f97e7f4dff414f39214830c3544c34e940b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Mon, 27 Jun 2016 21:34:54 +0200 Subject: [PATCH 08/30] PEP8 --- lib/matplotlib/testing/determinism.py | 1 + lib/matplotlib/tests/test_backend_pdf.py | 8 +++++--- lib/matplotlib/tests/test_backend_ps.py | 15 +++++++++------ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index f32286d7803c..bd0f623f154d 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -92,6 +92,7 @@ def _test_determinism(objects='mhi', format="pdf"): for p in plots[1:]: assert_equal(p, plots[0]) + def _test_source_date_epoch(format, string): """ Test SOURCE_DATE_EPOCH support. Output a document with the envionment diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 4b038a3e685f..43260e0a92f9 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -12,7 +12,8 @@ from matplotlib import cm, rcParams from matplotlib.backends.backend_pdf import PdfPages from matplotlib import pyplot as plt -from matplotlib.testing.determinism import _test_source_date_epoch, _test_determinism +from matplotlib.testing.determinism import (_test_source_date_epoch, + _test_determinism) from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) @@ -91,8 +92,8 @@ def test_multipage_keep_empty(): @cleanup 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. + # 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() @@ -115,6 +116,7 @@ def test_source_date_epoch(): """Test SOURCE_DATE_EPOCH support for PDF output""" _test_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)") + @cleanup def test_determinism_plain(): """Test for reproducible PDF output: simple figure""" diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 408f12ee301f..5fa190ae4478 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -11,7 +11,8 @@ import matplotlib import matplotlib.pyplot as plt from matplotlib import patheffects -from matplotlib.testing.determinism import _test_source_date_epoch, _test_determinism +from matplotlib.testing.determinism import (_test_source_date_epoch, + _test_determinism) from matplotlib.testing.decorators import cleanup, knownfailureif @@ -161,7 +162,7 @@ def test_tilde_in_tempfilename(): plt.rc('text', usetex=True) plt.plot([1, 2, 3, 4]) plt.xlabel(r'\textbf{time} (s)') - #matplotlib.verbose.set_level("debug") + # matplotlib.verbose.set_level("debug") output_eps = os.path.join(base_tempdir, 'tex_demo.eps') # use the PS backend to write the file... plt.savefig(output_eps, format="ps") @@ -174,20 +175,23 @@ def test_tilde_in_tempfilename(): # do not break if this is not removeable... print(e) + @cleanup 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. _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") -# 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. @cleanup def test_determinism_all(): """Test for reproducible PS output""" _test_determinism(format="ps") + @cleanup @needs_tex @needs_ghostscript @@ -197,7 +201,6 @@ def test_determinism_all_tex(): _test_determinism(format="ps") - if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) From 2a6ebc8092234d8a25dd3bd853221260b4a50e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Thu, 7 Jul 2016 16:26:02 +0200 Subject: [PATCH 09/30] Add what's new section --- doc/users/whats_new/reproducible_ps_pdf.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/users/whats_new/reproducible_ps_pdf.rst diff --git a/doc/users/whats_new/reproducible_ps_pdf.rst b/doc/users/whats_new/reproducible_ps_pdf.rst new file mode 100644 index 000000000000..fe937b2863a8 --- /dev/null +++ b/doc/users/whats_new/reproducible_ps_pdf.rst @@ -0,0 +1,6 @@ +Reproducible PS and PDF output +------------------------------ + +The ``SOURCE_DATE_EPOCH`` environment variable can now be used to set +the timestamps value in the PS and PDF outputs, which are then +reproducible. From 6ee896710b395552349cc698683949b6137e6b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Fri, 8 Jul 2016 11:20:27 +0200 Subject: [PATCH 10/30] Add some insight when test_source_date_epoch fails. --- lib/matplotlib/testing/determinism.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index bd0f623f154d..4ec8f8b7ab0c 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -4,6 +4,7 @@ import io import os +import re from matplotlib import pyplot as plt @@ -93,7 +94,7 @@ def _test_determinism(objects='mhi', format="pdf"): assert_equal(p, plots[0]) -def _test_source_date_epoch(format, string): +def _test_source_date_epoch(format, string, keyword=b"CreationDate"): """ Test SOURCE_DATE_EPOCH support. Output a document with the envionment variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the @@ -106,6 +107,9 @@ def _test_source_date_epoch(format, string): format string, such as "pdf". string : str timestamp string for 2000-01-01 00:00 UTC. + keyword : str + a string to look at when searching for the timestamp in the document + (used in case the test fails). """ try: # save current value of SOURCE_DATE_EPOCH @@ -115,10 +119,16 @@ def _test_source_date_epoch(format, string): x = [1, 2, 3, 4, 5] ax.plot(x, x) os.environ['SOURCE_DATE_EPOCH'] = "946684800" + find_keyword = re.compile(b".*" + keyword + b".*") with io.BytesIO() as output: fig.savefig(output, format=format) output.seek(0) buff = output.read() + key = find_keyword.search(buff) + if key: + print(key.group()) + else: + print("Timestamp keyword (%s) not found!" % keyword) assert string in buff os.environ.pop('SOURCE_DATE_EPOCH', None) with io.BytesIO() as output: From a3185e62cb92ffd65d020b78f183fe848c9f1da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Fri, 8 Jul 2016 12:16:00 +0200 Subject: [PATCH 11/30] Allow parallel execution of test_backend_ps:test_determinism_all_tex and test_determinism_all --- lib/matplotlib/testing/determinism.py | 7 +++++-- lib/matplotlib/tests/test_backend_ps.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 4ec8f8b7ab0c..840c20fa4bf3 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -60,7 +60,7 @@ def _test_determinism_save(filename, objects='mhi', format="pdf"): os.environ['SOURCE_DATE_EPOCH'] = sde -def _test_determinism(objects='mhi', format="pdf"): +def _test_determinism(objects='mhi', format="pdf", uid=""): """ Output three times the same graphs and checks that the outputs are exactly the same. @@ -73,11 +73,14 @@ def _test_determinism(objects='mhi', format="pdf"): default value is "mhi", so that the test includes all these objects. format : str format string. The default value is "pdf". + uid : str + some string to add to the filename used to store the output. Use it to + allow parallel execution of two tests with the same objects parameter. """ import sys from subprocess import check_call from nose.tools import assert_equal - filename = 'determinism_O%s.%s' % (objects, format) + filename = 'determinism_O%s%s.%s' % (objects, uid, format) plots = [] for i in range(3): check_call([sys.executable, '-R', '-c', diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 5fa190ae4478..3ebe3d0f711a 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -198,7 +198,7 @@ def test_determinism_all(): def test_determinism_all_tex(): """Test for reproducible PS/tex output""" matplotlib.rcParams['text.usetex'] = True - _test_determinism(format="ps") + _test_determinism(format="ps",uid="_tex") if __name__ == '__main__': From 8f095f613b51aa5ec563bfe28e8e97cf9187be0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Fri, 8 Jul 2016 12:43:05 +0200 Subject: [PATCH 12/30] Use subprocess for _test_source_date_epoch, to allow parallel calls (environment is shared between threads). --- lib/matplotlib/testing/determinism.py | 49 +++++++++++---------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 840c20fa4bf3..30e40d22fc53 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -114,34 +114,23 @@ def _test_source_date_epoch(format, string, keyword=b"CreationDate"): a string to look at when searching for the timestamp in the document (used in case the test fails). """ - try: - # save current value of SOURCE_DATE_EPOCH - sde = os.environ.pop('SOURCE_DATE_EPOCH', None) - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - x = [1, 2, 3, 4, 5] - ax.plot(x, x) - os.environ['SOURCE_DATE_EPOCH'] = "946684800" - find_keyword = re.compile(b".*" + keyword + b".*") - with io.BytesIO() as output: - fig.savefig(output, format=format) - output.seek(0) - buff = output.read() - key = find_keyword.search(buff) - if key: - print(key.group()) - else: - print("Timestamp keyword (%s) not found!" % keyword) - assert string in buff - os.environ.pop('SOURCE_DATE_EPOCH', None) - with io.BytesIO() as output: - fig.savefig(output, format=format) - output.seek(0) - buff = output.read() - assert string not in buff - finally: - # Restores SOURCE_DATE_EPOCH - if sde is None: - os.environ.pop('SOURCE_DATE_EPOCH', None) + import sys + from subprocess import check_call + filename = 'test_SDE_on.%s' % format + check_call([sys.executable, '-R', '-c', + 'import matplotlib; ' + 'matplotlib.use(%r); ' + 'from matplotlib.testing.determinism ' + 'import _test_determinism_save;' + '_test_determinism_save(%r,%r,%r)' + % (format, filename, "", format)]) + find_keyword = re.compile(b".*" + keyword + b".*") + with open(filename, 'rb') as fd: + buff = fd.read() + key = find_keyword.search(buff) + if key: + print(key.group()) else: - os.environ['SOURCE_DATE_EPOCH'] = sde + print("Timestamp keyword (%s) not found!" % keyword) + assert string in buff + os.unlink(filename) From c007f4998a3e4ab411c8dd58ab186ced4c179ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Mon, 11 Jul 2016 09:51:52 +0200 Subject: [PATCH 13/30] Change SOURCE_DATE_EPOCH test date, to use two-digits numbers for month, day of month and times. This way we don't mind if timestamps are written with leading 0 or space. --- lib/matplotlib/testing/determinism.py | 6 +++--- lib/matplotlib/tests/test_backend_pdf.py | 2 +- lib/matplotlib/tests/test_backend_ps.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 30e40d22fc53..82fc5d7a640c 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -14,7 +14,7 @@ def _test_determinism_save(filename, objects='mhi', format="pdf"): # to a constant value, so that time difference is not # taken into account sde = os.environ.pop('SOURCE_DATE_EPOCH', None) - os.environ['SOURCE_DATE_EPOCH'] = "946684800" + os.environ['SOURCE_DATE_EPOCH'] = "976875010" fig = plt.figure() @@ -100,7 +100,7 @@ def _test_determinism(objects='mhi', format="pdf", uid=""): def _test_source_date_epoch(format, string, keyword=b"CreationDate"): """ Test SOURCE_DATE_EPOCH support. Output a document with the envionment - variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the + variable SOURCE_DATE_EPOCH set to 2000-12-15 10:10:10 UTC and check that the document contains the timestamp that corresponds to this date (given as an argument). @@ -109,7 +109,7 @@ def _test_source_date_epoch(format, string, keyword=b"CreationDate"): format : str format string, such as "pdf". string : str - timestamp string for 2000-01-01 00:00 UTC. + timestamp string for 2000-12-15 10:10:10 UTC. keyword : str a string to look at when searching for the timestamp in the document (used in case the test fails). diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 43260e0a92f9..7586a3b205e3 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -114,7 +114,7 @@ def test_composite_image(): @cleanup def test_source_date_epoch(): """Test SOURCE_DATE_EPOCH support for PDF output""" - _test_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)") + _test_source_date_epoch("pdf", b"/CreationDate (D:20001215101010Z)") @cleanup diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 3ebe3d0f711a..7fcdf0d3dbfa 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -181,9 +181,9 @@ 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 + # %%CreationDate: D:20001215101010Z00\'00\', and this could change # with another ghostscript version. - _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") + _test_source_date_epoch("ps", b"%%CreationDate: Fri Dec 15 10:10:10 2000") @cleanup From ecbdd551c74f20ecac9b98ccc3f20915b95705ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Mon, 11 Jul 2016 09:59:29 +0200 Subject: [PATCH 14/30] PEP8 --- lib/matplotlib/testing/determinism.py | 6 +++--- lib/matplotlib/tests/test_backend_ps.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 82fc5d7a640c..94cc0f14dd6b 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -100,9 +100,9 @@ def _test_determinism(objects='mhi', format="pdf", uid=""): def _test_source_date_epoch(format, string, keyword=b"CreationDate"): """ Test SOURCE_DATE_EPOCH support. Output a document with the envionment - variable SOURCE_DATE_EPOCH set to 2000-12-15 10:10:10 UTC and check that the - document contains the timestamp that corresponds to this date (given as an - argument). + variable SOURCE_DATE_EPOCH set to 2000-12-15 10:10:10 UTC and check that + the document contains the timestamp that corresponds to this date (given as + an argument). Parameters ---------- diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 7fcdf0d3dbfa..bac1abaed288 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -198,7 +198,7 @@ def test_determinism_all(): def test_determinism_all_tex(): """Test for reproducible PS/tex output""" matplotlib.rcParams['text.usetex'] = True - _test_determinism(format="ps",uid="_tex") + _test_determinism(format="ps", uid="_tex") if __name__ == '__main__': From 65ec88e265da535edd819e3c2923feb546b3c2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 12 Jul 2016 09:16:29 +0200 Subject: [PATCH 15/30] Warnings about possible unreproducibility issues --- doc/users/whats_new/reproducible_ps_pdf.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/users/whats_new/reproducible_ps_pdf.rst b/doc/users/whats_new/reproducible_ps_pdf.rst index fe937b2863a8..94c39f766ac8 100644 --- a/doc/users/whats_new/reproducible_ps_pdf.rst +++ b/doc/users/whats_new/reproducible_ps_pdf.rst @@ -2,5 +2,12 @@ Reproducible PS and PDF output ------------------------------ The ``SOURCE_DATE_EPOCH`` environment variable can now be used to set -the timestamps value in the PS and PDF outputs, which are then -reproducible. +the timestamps value in the PS and PDF outputs. See +https://reproducible-builds.org/specs/source-date-epoch/ + +Matplotlib does its best to make PS and PDF outputs reproducible, but +be aware that some unreproducibility issues can arise if you use +different versions of Matplotlib and the tools it relies on. Although +standard plots has been checked to be reproducible, external tools can +also be a source of nondeterminism (``mathtext``, ``ps.usedistiller``, +``ps.fonttype``, ``pdf.fonttype``...). From 5b405cc935f70590c2c7e0f667454527940ca123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 12 Jul 2016 23:30:19 +0200 Subject: [PATCH 16/30] Doc rephrasing, thanks to jkseppan. --- doc/users/whats_new/reproducible_ps_pdf.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/users/whats_new/reproducible_ps_pdf.rst b/doc/users/whats_new/reproducible_ps_pdf.rst index 94c39f766ac8..97585a674b50 100644 --- a/doc/users/whats_new/reproducible_ps_pdf.rst +++ b/doc/users/whats_new/reproducible_ps_pdf.rst @@ -5,9 +5,10 @@ The ``SOURCE_DATE_EPOCH`` environment variable can now be used to set the timestamps value in the PS and PDF outputs. See https://reproducible-builds.org/specs/source-date-epoch/ -Matplotlib does its best to make PS and PDF outputs reproducible, but -be aware that some unreproducibility issues can arise if you use -different versions of Matplotlib and the tools it relies on. Although -standard plots has been checked to be reproducible, external tools can -also be a source of nondeterminism (``mathtext``, ``ps.usedistiller``, -``ps.fonttype``, ``pdf.fonttype``...). +The reproducibility of the output from the PS and PDF backends has so +far been tested using various plot elements but only default values of +options such as ``{ps,pdf}.fonttype`` that can affect the output at a +low level, and not with the mathtext or usetex features. When +matplotlib calls external tools (such as PS distillers or LaTeX) their +versions need to be kept constant for reproducibility, and they may +add sources of nondeterminism outside the control of matplotlib. From d10a21ece008d91a46d93a92e69542bed39bb3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 12 Jul 2016 23:53:07 +0200 Subject: [PATCH 17/30] Use explicit date formatting for PS backend timestamp, instead of asctime --- lib/matplotlib/backends/backend_ps.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 1659cf41a70e..496e1f53f58c 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -8,7 +8,7 @@ import six from six.moves import StringIO -import glob, math, os, shutil, sys, time +import glob, math, os, shutil, sys, time, datetime def _fn_name(): return sys._getframe(1).f_code.co_name import io @@ -1091,7 +1091,8 @@ def print_figure_impl(): # See https://reproducible-builds.org/specs/source-date-epoch/ source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") if source_date_epoch: - source_date = time.asctime(time.gmtime(int(source_date_epoch))) + source_date = datetime.datetime.utcfromtimestamp( + int(source_date_epoch) ).strftime("%a %b %e %T %Y") else: source_date = time.ctime() print("%%CreationDate: "+source_date, file=fh) @@ -1281,7 +1282,8 @@ def write(self, *kl, **kwargs): # See https://reproducible-builds.org/specs/source-date-epoch/ source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") if source_date_epoch: - source_date = time.asctime(time.gmtime(int(source_date_epoch))) + source_date = datetime.datetime.utcfromtimestamp( + int(source_date_epoch) ).strftime("%a %b %e %T %Y") else: source_date = time.ctime() print("%%CreationDate: "+source_date, file=fh) From da55bb66592964f5a15ffc370d035c2a4691ea88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 12 Jul 2016 23:56:40 +0200 Subject: [PATCH 18/30] Revert to 2000-01-01 for the SOURCE_DATE_EPOCH test date. This reverts commit c007f4998a3e4ab411c8dd58ab186ced4c179ca7. --- lib/matplotlib/testing/determinism.py | 10 +++++----- lib/matplotlib/tests/test_backend_pdf.py | 2 +- lib/matplotlib/tests/test_backend_ps.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 94cc0f14dd6b..30e40d22fc53 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -14,7 +14,7 @@ def _test_determinism_save(filename, objects='mhi', format="pdf"): # to a constant value, so that time difference is not # taken into account sde = os.environ.pop('SOURCE_DATE_EPOCH', None) - os.environ['SOURCE_DATE_EPOCH'] = "976875010" + os.environ['SOURCE_DATE_EPOCH'] = "946684800" fig = plt.figure() @@ -100,16 +100,16 @@ def _test_determinism(objects='mhi', format="pdf", uid=""): def _test_source_date_epoch(format, string, keyword=b"CreationDate"): """ Test SOURCE_DATE_EPOCH support. Output a document with the envionment - variable SOURCE_DATE_EPOCH set to 2000-12-15 10:10:10 UTC and check that - the document contains the timestamp that corresponds to this date (given as - an argument). + variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the + document contains the timestamp that corresponds to this date (given as an + argument). Parameters ---------- format : str format string, such as "pdf". string : str - timestamp string for 2000-12-15 10:10:10 UTC. + timestamp string for 2000-01-01 00:00 UTC. keyword : str a string to look at when searching for the timestamp in the document (used in case the test fails). diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 7586a3b205e3..43260e0a92f9 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -114,7 +114,7 @@ def test_composite_image(): @cleanup def test_source_date_epoch(): """Test SOURCE_DATE_EPOCH support for PDF output""" - _test_source_date_epoch("pdf", b"/CreationDate (D:20001215101010Z)") + _test_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)") @cleanup diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index bac1abaed288..30977c03db44 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -181,9 +181,9 @@ 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:20001215101010Z00\'00\', and this could change + # %%CreationDate: D:20000101000000Z00\'00\', and this could change # with another ghostscript version. - _test_source_date_epoch("ps", b"%%CreationDate: Fri Dec 15 10:10:10 2000") + _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") @cleanup From c56dae7c52af50ceaca33ba1471758c16ff86077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Wed, 13 Jul 2016 18:04:07 +0200 Subject: [PATCH 19/30] Use standard date format for PS timestamp --- lib/matplotlib/backends/backend_ps.py | 2 +- lib/matplotlib/tests/test_backend_ps.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 496e1f53f58c..fa4c7b285418 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1092,7 +1092,7 @@ def print_figure_impl(): source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") if source_date_epoch: source_date = datetime.datetime.utcfromtimestamp( - int(source_date_epoch) ).strftime("%a %b %e %T %Y") + int(source_date_epoch) ).strftime("%a %b %d %H:%M:%S %Y") else: source_date = time.ctime() print("%%CreationDate: "+source_date, file=fh) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 30977c03db44..ebc636cb4da2 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -183,7 +183,7 @@ def test_source_date_epoch(): # because the produced timestamp comes from ghostscript: # %%CreationDate: D:20000101000000Z00\'00\', and this could change # with another ghostscript version. - _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 1 00:00:00 2000") + _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 01 00:00:00 2000") @cleanup From 995173d6ef9a1dd0b1012c2729e177b89b9e9d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 4 Oct 2016 11:15:09 +0200 Subject: [PATCH 20/30] Rename functions in determinism.py to remove `test' keyword, since these are not main testing functions. --- lib/matplotlib/testing/determinism.py | 14 +++++++------- lib/matplotlib/tests/test_backend_pdf.py | 16 ++++++++-------- lib/matplotlib/tests/test_backend_ps.py | 11 ++++++----- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 30e40d22fc53..d5508a76d974 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -9,7 +9,7 @@ from matplotlib import pyplot as plt -def _test_determinism_save(filename, objects='mhi', format="pdf"): +def _determinism_save(filename, objects='mhi', format="pdf"): # save current value of SOURCE_DATE_EPOCH and set it # to a constant value, so that time difference is not # taken into account @@ -60,7 +60,7 @@ def _test_determinism_save(filename, objects='mhi', format="pdf"): os.environ['SOURCE_DATE_EPOCH'] = sde -def _test_determinism(objects='mhi', format="pdf", uid=""): +def _determinism_check(objects='mhi', format="pdf", uid=""): """ Output three times the same graphs and checks that the outputs are exactly the same. @@ -87,8 +87,8 @@ def _test_determinism(objects='mhi', format="pdf", uid=""): 'import matplotlib; ' 'matplotlib.use(%r); ' 'from matplotlib.testing.determinism ' - 'import _test_determinism_save;' - '_test_determinism_save(%r,%r,%r)' + 'import _determinism_save;' + '_determinism_save(%r,%r,%r)' % (format, filename, objects, format)]) with open(filename, 'rb') as fd: plots.append(fd.read()) @@ -97,7 +97,7 @@ def _test_determinism(objects='mhi', format="pdf", uid=""): assert_equal(p, plots[0]) -def _test_source_date_epoch(format, string, keyword=b"CreationDate"): +def _determinism_source_date_epoch(format, string, keyword=b"CreationDate"): """ Test SOURCE_DATE_EPOCH support. Output a document with the envionment variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the @@ -121,8 +121,8 @@ def _test_source_date_epoch(format, string, keyword=b"CreationDate"): 'import matplotlib; ' 'matplotlib.use(%r); ' 'from matplotlib.testing.determinism ' - 'import _test_determinism_save;' - '_test_determinism_save(%r,%r,%r)' + 'import _determinism_save;' + '_determinism_save(%r,%r,%r)' % (format, filename, "", format)]) find_keyword = re.compile(b".*" + keyword + b".*") with open(filename, 'rb') as fd: diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 43260e0a92f9..dd4dc95aca1a 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -12,8 +12,8 @@ from matplotlib import cm, rcParams from matplotlib.backends.backend_pdf import PdfPages from matplotlib import pyplot as plt -from matplotlib.testing.determinism import (_test_source_date_epoch, - _test_determinism) +from matplotlib.testing.determinism import (_determinism_source_date_epoch, + _determinism_check) from matplotlib.testing.decorators import (image_comparison, knownfailureif, cleanup) @@ -114,37 +114,37 @@ def test_composite_image(): @cleanup def test_source_date_epoch(): """Test SOURCE_DATE_EPOCH support for PDF output""" - _test_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)") + _determinism_source_date_epoch("pdf", b"/CreationDate (D:20000101000000Z)") @cleanup def test_determinism_plain(): """Test for reproducible PDF output: simple figure""" - _test_determinism('', format="pdf") + _determinism_check('', format="pdf") @cleanup def test_determinism_images(): """Test for reproducible PDF output: figure with different images""" - _test_determinism('i', format="pdf") + _determinism_check('i', format="pdf") @cleanup def test_determinism_hatches(): """Test for reproducible PDF output: figure with different hatches""" - _test_determinism('h', format="pdf") + _determinism_check('h', format="pdf") @cleanup def test_determinism_markers(): """Test for reproducible PDF output: figure with different markers""" - _test_determinism('m', format="pdf") + _determinism_check('m', format="pdf") @cleanup def test_determinism_all(): """Test for reproducible PDF output""" - _test_determinism(format="pdf") + _determinism_check(format="pdf") @image_comparison(baseline_images=['hatching_legend'], diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index ebc636cb4da2..4e61f324a326 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -11,8 +11,8 @@ import matplotlib import matplotlib.pyplot as plt from matplotlib import patheffects -from matplotlib.testing.determinism import (_test_source_date_epoch, - _test_determinism) +from matplotlib.testing.determinism import (_determinism_source_date_epoch, + _determinism_check) from matplotlib.testing.decorators import cleanup, knownfailureif @@ -183,13 +183,14 @@ def test_source_date_epoch(): # because the produced timestamp comes from ghostscript: # %%CreationDate: D:20000101000000Z00\'00\', and this could change # with another ghostscript version. - _test_source_date_epoch("ps", b"%%CreationDate: Sat Jan 01 00:00:00 2000") + _determinism_source_date_epoch( + "ps", b"%%CreationDate: Sat Jan 01 00:00:00 2000") @cleanup def test_determinism_all(): """Test for reproducible PS output""" - _test_determinism(format="ps") + _determinism_check(format="ps") @cleanup @@ -198,7 +199,7 @@ def test_determinism_all(): def test_determinism_all_tex(): """Test for reproducible PS/tex output""" matplotlib.rcParams['text.usetex'] = True - _test_determinism(format="ps", uid="_tex") + _determinism_check(format="ps", uid="_tex") if __name__ == '__main__': From eef6b12b40426187a886ea83df027fd7400b4151 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 6 Oct 2016 20:33:56 -0400 Subject: [PATCH 21/30] TST: Use standard I/O for determinism tests. --- lib/matplotlib/testing/determinism.py | 58 ++++++++++++--------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index d5508a76d974..24c706ccf1be 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -9,10 +9,11 @@ from matplotlib import pyplot as plt -def _determinism_save(filename, objects='mhi', format="pdf"): +def _determinism_save(objects='mhi', format="pdf"): # save current value of SOURCE_DATE_EPOCH and set it # to a constant value, so that time difference is not # taken into account + import sys sde = os.environ.pop('SOURCE_DATE_EPOCH', None) os.environ['SOURCE_DATE_EPOCH'] = "946684800" @@ -51,7 +52,7 @@ def _determinism_save(filename, objects='mhi', format="pdf"): x = range(5) fig.add_subplot(1, 6, 6).plot(x, x) - fig.savefig(filename, format=format) + fig.savefig(sys.stdout.buffer, format=format) # Restores SOURCE_DATE_EPOCH if sde is None: @@ -78,21 +79,18 @@ def _determinism_check(objects='mhi', format="pdf", uid=""): allow parallel execution of two tests with the same objects parameter. """ import sys - from subprocess import check_call + from subprocess import check_output from nose.tools import assert_equal - filename = 'determinism_O%s%s.%s' % (objects, uid, format) plots = [] for i in range(3): - check_call([sys.executable, '-R', '-c', - 'import matplotlib; ' - 'matplotlib.use(%r); ' - 'from matplotlib.testing.determinism ' - 'import _determinism_save;' - '_determinism_save(%r,%r,%r)' - % (format, filename, objects, format)]) - with open(filename, 'rb') as fd: - plots.append(fd.read()) - os.unlink(filename) + result = check_output([sys.executable, '-R', '-c', + 'import matplotlib; ' + 'matplotlib.use(%r); ' + 'from matplotlib.testing.determinism ' + 'import _determinism_save;' + '_determinism_save(%r,%r)' + % (format, objects, format)]) + plots.append(result) for p in plots[1:]: assert_equal(p, plots[0]) @@ -115,22 +113,18 @@ def _determinism_source_date_epoch(format, string, keyword=b"CreationDate"): (used in case the test fails). """ import sys - from subprocess import check_call - filename = 'test_SDE_on.%s' % format - check_call([sys.executable, '-R', '-c', - 'import matplotlib; ' - 'matplotlib.use(%r); ' - 'from matplotlib.testing.determinism ' - 'import _determinism_save;' - '_determinism_save(%r,%r,%r)' - % (format, filename, "", format)]) + from subprocess import check_output + buff = check_output([sys.executable, '-R', '-c', + 'import matplotlib; ' + 'matplotlib.use(%r); ' + 'from matplotlib.testing.determinism ' + 'import _determinism_save;' + '_determinism_save(%r,%r)' + % (format, "", format)]) find_keyword = re.compile(b".*" + keyword + b".*") - with open(filename, 'rb') as fd: - buff = fd.read() - key = find_keyword.search(buff) - if key: - print(key.group()) - else: - print("Timestamp keyword (%s) not found!" % keyword) - assert string in buff - os.unlink(filename) + key = find_keyword.search(buff) + if key: + print(key.group()) + else: + print("Timestamp keyword (%s) not found!" % keyword) + assert string in buff From fb529da81aee18cd646c5500758d480a0611a67f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 6 Oct 2016 20:39:59 -0400 Subject: [PATCH 22/30] TST: Remove multiple nested imports. --- lib/matplotlib/testing/determinism.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 24c706ccf1be..b4e5bb9f0835 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -5,6 +5,8 @@ import io import os import re +import sys +from subprocess import check_output from matplotlib import pyplot as plt @@ -13,7 +15,6 @@ def _determinism_save(objects='mhi', format="pdf"): # save current value of SOURCE_DATE_EPOCH and set it # to a constant value, so that time difference is not # taken into account - import sys sde = os.environ.pop('SOURCE_DATE_EPOCH', None) os.environ['SOURCE_DATE_EPOCH'] = "946684800" @@ -78,8 +79,6 @@ def _determinism_check(objects='mhi', format="pdf", uid=""): some string to add to the filename used to store the output. Use it to allow parallel execution of two tests with the same objects parameter. """ - import sys - from subprocess import check_output from nose.tools import assert_equal plots = [] for i in range(3): @@ -112,8 +111,6 @@ def _determinism_source_date_epoch(format, string, keyword=b"CreationDate"): a string to look at when searching for the timestamp in the document (used in case the test fails). """ - import sys - from subprocess import check_output buff = check_output([sys.executable, '-R', '-c', 'import matplotlib; ' 'matplotlib.use(%r); ' From ebff832c6087cb2f646e94e6f3eb23afdcfc8dd0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 8 Oct 2016 00:26:34 -0400 Subject: [PATCH 23/30] TST: Fix compatibility with Python 2. --- lib/matplotlib/testing/determinism.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index b4e5bb9f0835..2a378ed6a094 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -2,6 +2,8 @@ Provides utilities to test output reproducibility. """ +import six + import io import os import re @@ -53,7 +55,13 @@ def _determinism_save(objects='mhi', format="pdf"): x = range(5) fig.add_subplot(1, 6, 6).plot(x, x) - fig.savefig(sys.stdout.buffer, format=format) + if six.PY2 and format == 'ps': + stdout = io.StringIO() + else: + stdout = getattr(sys.stdout, 'buffer', sys.stdout) + fig.savefig(stdout, format=format) + if six.PY2 and format == 'ps': + sys.stdout.write(stdout.getvalue()) # Restores SOURCE_DATE_EPOCH if sde is None: From 1786555aa7c77ae56b2d1b52b737eef5457d5fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Sun, 9 Oct 2016 21:40:00 +0200 Subject: [PATCH 24/30] Adds __future__ imports to testing/determinism.py --- lib/matplotlib/testing/determinism.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 2a378ed6a094..396891771c94 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -2,6 +2,9 @@ Provides utilities to test output reproducibility. """ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + import six import io From af4213ed07f1adaa77c9aaa8160904437ad8feeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 11 Oct 2016 09:16:01 +0200 Subject: [PATCH 25/30] Pass usetex setting to _determinism_save --- lib/matplotlib/testing/determinism.py | 11 +++++++---- lib/matplotlib/tests/test_backend_ps.py | 3 +-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 396891771c94..a74ff85e8d1a 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -13,16 +13,19 @@ import sys from subprocess import check_output +import matplotlib from matplotlib import pyplot as plt -def _determinism_save(objects='mhi', format="pdf"): +def _determinism_save(objects='mhi', format="pdf", usetex=False): # save current value of SOURCE_DATE_EPOCH and set it # to a constant value, so that time difference is not # taken into account sde = os.environ.pop('SOURCE_DATE_EPOCH', None) os.environ['SOURCE_DATE_EPOCH'] = "946684800" + matplotlib.rcParams['text.usetex'] = usetex + fig = plt.figure() if 'm' in objects: @@ -73,7 +76,7 @@ def _determinism_save(objects='mhi', format="pdf"): os.environ['SOURCE_DATE_EPOCH'] = sde -def _determinism_check(objects='mhi', format="pdf", uid=""): +def _determinism_check(objects='mhi', format="pdf", uid="", usetex=False): """ Output three times the same graphs and checks that the outputs are exactly the same. @@ -98,8 +101,8 @@ def _determinism_check(objects='mhi', format="pdf", uid=""): 'matplotlib.use(%r); ' 'from matplotlib.testing.determinism ' 'import _determinism_save;' - '_determinism_save(%r,%r)' - % (format, objects, format)]) + '_determinism_save(%r,%r,%r)' + % (format, objects, format, usetex)]) plots.append(result) for p in plots[1:]: assert_equal(p, plots[0]) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 4e61f324a326..c2457ca3471d 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -198,8 +198,7 @@ def test_determinism_all(): @needs_ghostscript def test_determinism_all_tex(): """Test for reproducible PS/tex output""" - matplotlib.rcParams['text.usetex'] = True - _determinism_check(format="ps", uid="_tex") + _determinism_check(format="ps", uid="_tex", usetex=True) if __name__ == '__main__': From bf7387e95fb26615e0a7bfeb80f7fdc0c9404318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 11 Oct 2016 12:14:41 +0200 Subject: [PATCH 26/30] Skip test using ghostscript, since failing may be due to ghostscript timestamps --- lib/matplotlib/testing/determinism.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index a74ff85e8d1a..49b0eac936a9 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -16,6 +16,8 @@ import matplotlib from matplotlib import pyplot as plt +from nose.plugins.skip import SkipTest + def _determinism_save(objects='mhi', format="pdf", usetex=False): # save current value of SOURCE_DATE_EPOCH and set it @@ -105,7 +107,11 @@ def _determinism_check(objects='mhi', format="pdf", uid="", usetex=False): % (format, objects, format, usetex)]) plots.append(result) for p in plots[1:]: - assert_equal(p, plots[0]) + if usetex: + if p != plots[0]: + raise SkipTest("failed, maybe due to ghostscript timestamps") + else: + assert_equal(p, plots[0]) def _determinism_source_date_epoch(format, string, keyword=b"CreationDate"): From 2cdc57741bb0643c70e0d0268c502cf6d53269b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Tue, 11 Oct 2016 16:40:17 +0200 Subject: [PATCH 27/30] Forgot to change one timestamp format in c56dae7c52af50ceaca33ba1471758c16ff86077 --- lib/matplotlib/backends/backend_ps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index fa4c7b285418..6769e86659f5 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1283,7 +1283,7 @@ def write(self, *kl, **kwargs): source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") if source_date_epoch: source_date = datetime.datetime.utcfromtimestamp( - int(source_date_epoch) ).strftime("%a %b %e %T %Y") + int(source_date_epoch) ).strftime("%a %b %d %H:%M:%S %Y") else: source_date = time.ctime() print("%%CreationDate: "+source_date, file=fh) From 76bec02fc2129f322d1e3cb052c671852d83aec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Wed, 2 Nov 2016 15:20:30 +0100 Subject: [PATCH 28/30] Reuse UTC timezone from dates.py --- lib/matplotlib/backends/backend_pdf.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a15e13c3a612..cedfe29e8b2a 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -46,6 +46,7 @@ from matplotlib.mathtext import MathTextParser from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path +from matplotlib.dates import UTC from matplotlib import _path from matplotlib import _png from matplotlib import ttconv @@ -136,20 +137,6 @@ def _string_escape(match): assert False -# tzinfo class for UTC -class UTCtimezone(tzinfo): - """UTC timezone""" - - def utcoffset(self, dt): - return timedelta(0) - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return timedelta(0) - - def pdfRepr(obj): """Map Python objects to PDF syntax.""" @@ -492,7 +479,7 @@ def __init__(self, filename): source_date_epoch = os.getenv("SOURCE_DATE_EPOCH") if source_date_epoch: source_date = datetime.utcfromtimestamp(int(source_date_epoch)) - source_date = source_date.replace(tzinfo=UTCtimezone()) + source_date = source_date.replace(tzinfo=UTC) else: source_date = datetime.today() From bbab0c578a278a612c9f6ffc44961d3a6201a6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Thu, 3 Nov 2016 21:46:03 +0100 Subject: [PATCH 29/30] Removes now useless option uid for _determinism_check --- lib/matplotlib/testing/determinism.py | 7 ++----- lib/matplotlib/tests/test_backend_ps.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/testing/determinism.py b/lib/matplotlib/testing/determinism.py index 49b0eac936a9..07b5e831451f 100644 --- a/lib/matplotlib/testing/determinism.py +++ b/lib/matplotlib/testing/determinism.py @@ -78,7 +78,7 @@ def _determinism_save(objects='mhi', format="pdf", usetex=False): os.environ['SOURCE_DATE_EPOCH'] = sde -def _determinism_check(objects='mhi', format="pdf", uid="", usetex=False): +def _determinism_check(objects='mhi', format="pdf", usetex=False): """ Output three times the same graphs and checks that the outputs are exactly the same. @@ -91,9 +91,6 @@ def _determinism_check(objects='mhi', format="pdf", uid="", usetex=False): default value is "mhi", so that the test includes all these objects. format : str format string. The default value is "pdf". - uid : str - some string to add to the filename used to store the output. Use it to - allow parallel execution of two tests with the same objects parameter. """ from nose.tools import assert_equal plots = [] @@ -127,7 +124,7 @@ def _determinism_source_date_epoch(format, string, keyword=b"CreationDate"): format string, such as "pdf". string : str timestamp string for 2000-01-01 00:00 UTC. - keyword : str + keyword : bytes a string to look at when searching for the timestamp in the document (used in case the test fails). """ diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index c2457ca3471d..7797deb01669 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -198,7 +198,7 @@ def test_determinism_all(): @needs_ghostscript def test_determinism_all_tex(): """Test for reproducible PS/tex output""" - _determinism_check(format="ps", uid="_tex", usetex=True) + _determinism_check(format="ps", usetex=True) if __name__ == '__main__': From 1a5ada6baf3d7572fa2c0b64f5d9e2dae20db08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Bienven=C3=BCe?= Date: Thu, 3 Nov 2016 21:47:01 +0100 Subject: [PATCH 30/30] Typo --- doc/users/whats_new/reproducible_ps_pdf.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/users/whats_new/reproducible_ps_pdf.rst b/doc/users/whats_new/reproducible_ps_pdf.rst index 97585a674b50..2e8294f1e414 100644 --- a/doc/users/whats_new/reproducible_ps_pdf.rst +++ b/doc/users/whats_new/reproducible_ps_pdf.rst @@ -2,7 +2,7 @@ Reproducible PS and PDF output ------------------------------ The ``SOURCE_DATE_EPOCH`` environment variable can now be used to set -the timestamps value in the PS and PDF outputs. See +the timestamp value in the PS and PDF outputs. See https://reproducible-builds.org/specs/source-date-epoch/ The reproducibility of the output from the PS and PDF backends has so