Skip to content

Commit 8d6ebe0

Browse files
authored
Merge branch 'main' into fix/static-parsing
2 parents 61b355a + 215ddad commit 8d6ebe0

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,16 @@ docs = [
8080
"lxml-html-clean",
8181
"markdown",
8282
"mpltern",
83-
"nbsphinx",
83+
# Floors below are the versions that work with Sphinx 9, which removed
84+
# sphinx.ext.autosummary.get_documenter. Without them pip is free to pair a
85+
# current Sphinx with an extension that cannot import it, and the build
86+
# dies with "Extension error (sphinx_automodapi.automodsumm)".
87+
"nbsphinx>=0.9.8", # 0.9.7 declares sphinx<8.2
8488
"sphinx",
8589
"sphinx-autoapi",
86-
"sphinx-automodapi",
90+
"sphinx-automodapi>=0.22", # 0.19 imports the removed get_documenter
8791
"sphinx-copybutton",
88-
"sphinx-design",
92+
"sphinx-design>=0.7", # 0.6.1 declares sphinx<9
8993
"sphinx-gallery",
9094
"shibuya",
9195
"sphinx-sitemap",

ultraplot/axes/plot_types/circlize.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,34 @@
1515
from ... import constructor
1616
from ...config import rc
1717

18+
#: Settings ``pycirclize.config`` writes into the global rcParams the moment it
19+
#: is imported. Importing a library must not change how unrelated figures are
20+
#: saved, so `_import_pycirclize` puts these back.
21+
_PYCIRCLIZE_RC_LEAKS = ("savefig.bbox", "savefig.pad_inches", "svg.fonttype")
22+
1823

1924
def _import_pycirclize():
25+
"""
26+
Import pycirclize without letting it restyle the session.
27+
28+
``pycirclize.config`` runs ``mpl.rcParams.update(...)`` at import time,
29+
setting ``savefig.bbox='tight'`` and ``savefig.pad_inches=0.5``. Since the
30+
import is lazy, the first chord, radar, phylogeny or circos plot in a
31+
session would otherwise silently change the size and padding of every
32+
figure saved afterwards.
33+
"""
34+
import matplotlib as mpl
35+
36+
restore = {key: mpl.rcParams[key] for key in _PYCIRCLIZE_RC_LEAKS}
37+
try:
38+
return _import_pycirclize_unguarded()
39+
finally:
40+
for key, value in restore.items():
41+
if mpl.rcParams[key] != value:
42+
mpl.rcParams[key] = value
43+
44+
45+
def _import_pycirclize_unguarded():
2046
try:
2147
import pycirclize
2248
except ImportError as exc:

ultraplot/tests/test_circlize_integration.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,51 @@ def test_alias_methods(fake_pycirclize, tmp_path):
211211
circos = ax.bed(bed_path, plot=False)
212212
assert hasattr(circos, "sectors")
213213
uplt.close(fig)
214+
215+
216+
def test_import_pycirclize_restores_savefig_settings():
217+
"""
218+
Importing pycirclize must not change how unrelated figures are saved.
219+
220+
``pycirclize.config`` runs ``mpl.rcParams.update(...)`` at import time,
221+
setting ``savefig.bbox='tight'`` and ``savefig.pad_inches=0.5``. UltraPlot
222+
imports it lazily, so without a guard the first chord or radar plot in a
223+
session would silently pad and resize every figure saved afterwards.
224+
"""
225+
import matplotlib as mpl
226+
227+
pytest.importorskip("pycirclize")
228+
watched = ("savefig.bbox", "savefig.pad_inches", "svg.fonttype")
229+
before = {key: mpl.rcParams[key] for key in watched}
230+
try:
231+
circlize_mod._import_pycirclize()
232+
assert {key: mpl.rcParams[key] for key in watched} == before
233+
finally:
234+
for key, value in before.items():
235+
mpl.rcParams[key] = value
236+
237+
238+
def test_chord_diagram_does_not_resize_later_figures():
239+
"""
240+
A figure made after a chord diagram keeps the size it was asked for.
241+
"""
242+
import matplotlib as mpl
243+
244+
pytest.importorskip("pycirclize")
245+
pandas = pytest.importorskip("pandas")
246+
numpy = pytest.importorskip("numpy")
247+
248+
watched = ("savefig.bbox", "savefig.pad_inches")
249+
before = {key: mpl.rcParams[key] for key in watched}
250+
try:
251+
names = list("ABCD")
252+
matrix = pandas.DataFrame(
253+
numpy.arange(16).reshape(4, 4) + 1, index=names, columns=names
254+
)
255+
fig, ax = uplt.subplots(figwidth="26mm", figheight="26mm", proj="polar")
256+
ax.chord_diagram(matrix, ticks_interval=None)
257+
uplt.close(fig)
258+
assert {key: mpl.rcParams[key] for key in watched} == before
259+
finally:
260+
for key, value in before.items():
261+
mpl.rcParams[key] = value

0 commit comments

Comments
 (0)