1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
|
From: Christian Bayle <bayle@debian.org>
Date: Sat, 20 Sep 2025 19:40:29 +0200
Subject: Make matplotlib reproducible
---
sphinx_needs/directives/needbar.py | 12 ++++++++++--
sphinx_needs/utils.py | 18 +++++++++++++++++-
2 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/sphinx_needs/directives/needbar.py b/sphinx_needs/directives/needbar.py
index 833d1a0..838f360 100644
--- a/sphinx_needs/directives/needbar.py
+++ b/sphinx_needs/directives/needbar.py
@@ -472,7 +472,9 @@ def process_needbar(
# 9. final storage
# We need to calculate an unique bar-image file name
- hash_value = hashlib.sha256(node.attributes["ids"][0].encode()).hexdigest()[:5]
+ #hash_value = hashlib.sha256(node.attributes["ids"][0].encode()).hexdigest()[:5]
+ from sphinx_needs.utils import stable_hash
+ hash_value = stable_hash(node.attributes["ids"][0], length=8)
image_node = save_matplotlib_figure(
app, figure, f"need_bar_{hash_value}", fromdocname
)
@@ -488,7 +490,13 @@ def process_needbar(
# 10. cleanup matplotlib
# Reset the style configuration:
- matplotlib.rcParams = style_previous_to_script_execution
+ #matplotlib.rcParams = style_previous_to_script_execution
+ matplotlib.rcParams.update(style_previous_to_script_execution)
+
+ # Ensure SVG outputs reproducible IDs
+ if hasattr(matplotlib, "rcParams"):
+ matplotlib.rcParams["svg.id"] = "needbar"
+ matplotlib.rcParams["svg.hashsalt"] = "needbar"
# close the figure, to free consumed memory. Otherwise we will get:
# RuntimeWarning from matplotlib: More than 20 figures have been opened.
diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py
index a9398bc..52e159f 100644
--- a/sphinx_needs/utils.py
+++ b/sphinx_needs/utils.py
@@ -5,6 +5,7 @@ import importlib
import operator
import os
import re
+import hashlib
from dataclasses import dataclass
from functools import lru_cache, reduce, wraps
from typing import TYPE_CHECKING, Any, Callable, Protocol, TypeVar
@@ -24,6 +25,9 @@ from sphinx_needs.views import NeedsAndPartsListView, NeedsView
if TYPE_CHECKING:
import matplotlib
+ if hasattr(matplotlib, "rcParams"):
+ matplotlib.rcParams["svg.hashsalt"] = "sphinx-needs"
+ matplotlib.rcParams["svg.id"] = "sphinx-needs"
from matplotlib.figure import FigureBase
@@ -45,6 +49,11 @@ MONTH_NAMES = [
"December",
]
+def stable_hash(value: str, length: int = 8) -> str:
+ """
+ Deterministic replacement for Python's built-in hash().
+ """
+ return hashlib.sha1(str(value).encode("utf-8")).hexdigest()[:length]
def split_need_id(need_id_full: str) -> tuple[str, str | None]:
"""A need id can be a combination of a main id and a part id,
@@ -386,6 +395,7 @@ def import_matplotlib() -> matplotlib | None:
return None
if not os.environ.get("DISPLAY"):
matplotlib.use("Agg")
+ matplotlib.pyplot.rcParams['svg.hashsalt'] = 'reproducible-salt'
return matplotlib
@@ -398,6 +408,12 @@ def save_matplotlib_figure(
image_folder = os.path.join(builder.outdir, builder.imagedir)
os.makedirs(image_folder, exist_ok=True)
+ # Ensure reproducible SVG output (only matters for savefig)
+ import matplotlib
+ if hasattr(matplotlib, "rcParams"):
+ matplotlib.rcParams["svg.hashsalt"] = "sphinx-needs"
+ matplotlib.rcParams["svg.id"] = "sphinx-needs"
+
# Determine a common mimetype between matplotlib and the builder.
matplotlib_types = {
"image/svg+xml": "svg",
@@ -418,7 +434,7 @@ def save_matplotlib_figure(
abs_file_path = os.path.join(image_folder, f"{basename}.{ext}")
if abs_file_path not in env.images:
- figure.savefig(os.path.join(env.app.srcdir, abs_file_path))
+ figure.savefig(os.path.join(env.app.srcdir, abs_file_path), metadata={'Date': None})
env.images.add_file(fromdocname, abs_file_path)
image_node = nodes.image()
|