Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7993257
animation: skip auto_layout on draw_idle
cvanelteren Jan 24, 2026
93f9672
layout: skip auto_layout unless layout is dirty
cvanelteren Jan 24, 2026
ecf6010
layout: avoid dirtying layout on backend size updates
cvanelteren Jan 24, 2026
eb70a98
ci: append coverage data with xdist
cvanelteren Jan 24, 2026
0d3bf5b
ci: force pytest-cov plugin with xdist
cvanelteren Jan 24, 2026
ab165b5
ci: fall back to full tests when selection is empty
cvanelteren Jan 24, 2026
008ddb1
ci: handle empty selected tests under bash -e
cvanelteren Jan 24, 2026
4d6c0a2
ci: fall back to full baseline generation on empty selection
cvanelteren Jan 25, 2026
b2ea1a3
ci: treat missing nodeids as empty selection
cvanelteren Jan 25, 2026
a959cd6
ci: run coverage without xdist to avoid worker gaps
cvanelteren Jan 25, 2026
c9a0f0f
ci: quiet pytest output
cvanelteren Jan 25, 2026
e3ba31a
ci: suppress pytest warnings output
cvanelteren Jan 25, 2026
9de5e83
ci: stabilize pytest exit handling
cvanelteren Jan 25, 2026
bcffb19
ci: retry pytest without xdist on nonzero exit
cvanelteren Jan 25, 2026
2591a82
ci: run main test step without xdist
cvanelteren Jan 25, 2026
f0b0622
ci: filter missing nodeids before pytest
cvanelteren Jan 25, 2026
9736984
ci: bump cache keys for test map and baselines
cvanelteren Jan 25, 2026
ed69c48
ci: rely on coverage step for test gating
cvanelteren Jan 25, 2026
e119fbb
ci: drop coverage step from build workflow
cvanelteren Jan 25, 2026
0d1564f
Merge branch 'main' into fix-idle-draw-animation
cvanelteren Jan 25, 2026
4942b5b
Remove workflow changes from branch
cvanelteren Jan 25, 2026
9e66b83
run test single thread
cvanelteren Jan 25, 2026
5f4e25e
Prevent None from interfering with tickers
cvanelteren Jan 25, 2026
1492c35
Remove git install
cvanelteren Jan 25, 2026
23f0466
Harden workflow
cvanelteren Jan 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 35 additions & 94 deletions .github/workflows/build-ultraplot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ jobs:
with:
fetch-depth: 0

- name: Install CA certificates (act)
if: ${{ env.ACT }}
run: |
sudo apt-get update
sudo apt-get install -y ca-certificates

- uses: mamba-org/setup-micromamba@v2.0.7
with:
environment-file: ./environment.yml
Expand All @@ -54,34 +60,27 @@ jobs:

- name: Test Ultraplot
run: |
status=0
filter_nodeids() {
local filtered=""
for nodeid in ${TEST_NODEIDS}; do
local path="${nodeid%%::*}"
if [ -f "$path" ]; then
filtered="${filtered} ${nodeid}"
if [ "${TEST_MODE}" = "selected" ] && [ -n "${TEST_NODEIDS}" ]; then
nodeids=()
for token in ${TEST_NODEIDS}; do
if [[ "${token}" == *"::"* || "${token}" == *.py ]]; then
nodeids+=("${token}")
fi
done
echo "${filtered}"
}
if [ "${TEST_MODE}" = "selected" ] && [ -n "${TEST_NODEIDS}" ]; then
FILTERED_NODEIDS="$(filter_nodeids)"
if [ -z "${FILTERED_NODEIDS}" ]; then
echo "No valid nodeids found; running full suite."
pytest -q --tb=short --disable-warnings -n 0 ultraplot || status=$?
if [ "${#nodeids[@]}" -gt 0 ]; then
pytest --cov=ultraplot --cov-branch --cov-report term-missing --cov-report=xml "${nodeids[@]}"
else
pytest -q --tb=short --disable-warnings -n 0 ${FILTERED_NODEIDS} || status=$?
if [ "$status" -eq 4 ] || [ "$status" -eq 5 ]; then
echo "No tests collected from selected nodeids; running full suite."
status=0
pytest -q --tb=short --disable-warnings -n 0 ultraplot || status=$?
fi
pytest --cov=ultraplot --cov-branch --cov-report term-missing --cov-report=xml ultraplot
fi
else
pytest -q --tb=short --disable-warnings -n 0 ultraplot || status=$?
pytest --cov=ultraplot --cov-branch --cov-report term-missing --cov-report=xml ultraplot
fi
exit "$status"

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: Ultraplot/ultraplot

compare-baseline:
name: Compare baseline Python ${{ inputs.python-version }} with MPL ${{ inputs.matplotlib-version }}
Expand All @@ -96,6 +95,12 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Install CA certificates (act)
if: ${{ env.ACT }}
run: |
sudo apt-get update
sudo apt-get install -y ca-certificates

- uses: mamba-org/setup-micromamba@v2.0.7
with:
environment-file: ./environment.yml
Expand All @@ -115,9 +120,9 @@ jobs:
with:
path: ./ultraplot/tests/baseline # The directory to cache
# Key is based on OS, Python/Matplotlib versions, and the base commit SHA
key: ${{ runner.os }}-baseline-base-v2-${{ github.event.pull_request.base.sha }}-${{ inputs.python-version }}-${{ inputs.matplotlib-version }}
key: ${{ runner.os }}-baseline-base-${{ github.event.pull_request.base.sha }}-${{ inputs.python-version }}-${{ inputs.matplotlib-version }}
restore-keys: |
${{ runner.os }}-baseline-base-v2-${{ github.event.pull_request.base.sha }}-${{ inputs.python-version }}-${{ inputs.matplotlib-version }}-
${{ runner.os }}-baseline-base-${{ github.event.pull_request.base.sha }}-${{ inputs.python-version }}-${{ inputs.matplotlib-version }}-

# Conditional Baseline Generation (Only runs on cache miss)
- name: Generate baseline from main
Expand All @@ -137,41 +142,12 @@ jobs:
# Generate the baseline images and hash library
python -c "import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')"
if [ "${TEST_MODE}" = "selected" ] && [ -n "${TEST_NODEIDS}" ]; then
status=0
filter_nodeids() {
local filtered=""
for nodeid in ${TEST_NODEIDS}; do
local path="${nodeid%%::*}"
if [ -f "$path" ]; then
filtered="${filtered} ${nodeid}"
fi
done
echo "${filtered}"
}
FILTERED_NODEIDS="$(filter_nodeids)"
if [ -z "${FILTERED_NODEIDS}" ]; then
echo "No valid nodeids found; running full suite."
pytest -q --tb=short --disable-warnings -W ignore \
--mpl-generate-path=./ultraplot/tests/baseline/ \
--mpl-default-style="./ultraplot.yml" \
ultraplot/tests || status=$?
else
pytest -q --tb=short --disable-warnings -W ignore \
pytest -W ignore \
--mpl-generate-path=./ultraplot/tests/baseline/ \
--mpl-default-style="./ultraplot.yml" \
${FILTERED_NODEIDS} || status=$?
if [ "$status" -eq 4 ] || [ "$status" -eq 5 ]; then
echo "No tests collected from selected nodeids on base; running full suite."
status=0
pytest -q --tb=short --disable-warnings -W ignore \
--mpl-generate-path=./ultraplot/tests/baseline/ \
--mpl-default-style="./ultraplot.yml" \
ultraplot/tests || status=$?
fi
fi
exit "$status"
${TEST_NODEIDS}
else
pytest -q --tb=short --disable-warnings -W ignore \
pytest -W ignore \
--mpl-generate-path=./ultraplot/tests/baseline/ \
--mpl-default-style="./ultraplot.yml" \
ultraplot/tests
Expand All @@ -191,50 +167,15 @@ jobs:
mkdir -p results
python -c "import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')"
if [ "${TEST_MODE}" = "selected" ] && [ -n "${TEST_NODEIDS}" ]; then
status=0
filter_nodeids() {
local filtered=""
for nodeid in ${TEST_NODEIDS}; do
local path="${nodeid%%::*}"
if [ -f "$path" ]; then
filtered="${filtered} ${nodeid}"
fi
done
echo "${filtered}"
}
FILTERED_NODEIDS="$(filter_nodeids)"
if [ -z "${FILTERED_NODEIDS}" ]; then
echo "No valid nodeids found; running full suite."
pytest -q --tb=short --disable-warnings -W ignore \
--mpl \
--mpl-baseline-path=./ultraplot/tests/baseline \
--mpl-results-path=./results/ \
--mpl-generate-summary=html \
--mpl-default-style="./ultraplot.yml" \
ultraplot/tests || status=$?
else
pytest -q --tb=short --disable-warnings -W ignore \
pytest -W ignore \
--mpl \
--mpl-baseline-path=./ultraplot/tests/baseline \
--mpl-results-path=./results/ \
--mpl-generate-summary=html \
--mpl-default-style="./ultraplot.yml" \
${FILTERED_NODEIDS} || status=$?
if [ "$status" -eq 4 ] || [ "$status" -eq 5 ]; then
echo "No tests collected from selected nodeids; running full suite."
status=0
pytest -q --tb=short --disable-warnings -W ignore \
--mpl \
--mpl-baseline-path=./ultraplot/tests/baseline \
--mpl-results-path=./results/ \
--mpl-generate-summary=html \
--mpl-default-style="./ultraplot.yml" \
ultraplot/tests || status=$?
fi
fi
exit "$status"
${TEST_NODEIDS}
else
pytest -q --tb=short --disable-warnings -W ignore \
pytest -W ignore \
--mpl \
--mpl-baseline-path=./ultraplot/tests/baseline \
--mpl-results-path=./results/ \
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ jobs:
with:
fetch-depth: 0

- name: Install git (act)
if: ${{ env.ACT }}
run: |
sudo apt-get update
sudo apt-get install -y git

- name: Prepare workspace
run: mkdir -p .ci

Expand All @@ -40,9 +46,9 @@ jobs:
uses: actions/cache/restore@v4
with:
path: .ci/test-map.json
key: test-map-v2-${{ github.event.pull_request.base.sha }}
key: test-map-${{ github.event.pull_request.base.sha }}
restore-keys: |
test-map-v2-
test-map-

- name: Select impacted tests
id: select
Expand Down Expand Up @@ -86,6 +92,12 @@ jobs:
with:
fetch-depth: 0

- name: Install git (act)
if: ${{ env.ACT }}
run: |
sudo apt-get update
sudo apt-get install -y git

- uses: actions/setup-python@v6
with:
python-version: "3.11"
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/test-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ jobs:
with:
fetch-depth: 0

- name: Install CA certificates (act)
if: ${{ env.ACT }}
run: |
sudo apt-get update
sudo apt-get install -y ca-certificates

- uses: mamba-org/setup-micromamba@v2.0.7
with:
environment-file: ./environment.yml
Expand All @@ -36,7 +42,7 @@ jobs:
- name: Generate test coverage map
run: |
mkdir -p .ci
pytest -q --tb=short --disable-warnings -n 0 -p pytest_cov --cov=ultraplot --cov-branch --cov-context=test --cov-report= ultraplot
pytest -n auto --cov=ultraplot --cov-branch --cov-context=test --cov-report= ultraplot
python tools/ci/build_test_map.py --coverage-file .coverage --output .ci/test-map.json --root .

- name: Cache test map
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ ignore = ["I001", "I002", "I003", "I004"]

[tool.basedpyright]
exclude = ["**/*.ipynb"]

[tool.pytest.ini_options]
filterwarnings = [
"ignore:'resetCache' deprecated - use 'reset_cache':DeprecationWarning:matplotlib._fontconfig_pattern",
]
2 changes: 2 additions & 0 deletions ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3371,6 +3371,8 @@ def format(
ultraplot.gridspec.SubplotGrid.format
ultraplot.config.Configurator.context
"""
if self.figure is not None:
self.figure._layout_dirty = True
skip_figure = kwargs.pop("skip_figure", False) # internal keyword arg
params = _pop_params(kwargs, self.figure._format_signature)

Expand Down
35 changes: 34 additions & 1 deletion ultraplot/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,14 +476,28 @@ def _canvas_preprocess(self, *args, **kwargs):
else:
return

skip_autolayout = getattr(fig, "_skip_autolayout", False)
layout_dirty = getattr(fig, "_layout_dirty", False)
if (
skip_autolayout
and getattr(fig, "_layout_initialized", False)
and not layout_dirty
):
fig._skip_autolayout = False
return func(self, *args, **kwargs)
fig._skip_autolayout = False

# Adjust layout
# NOTE: The authorized_context is needed because some backends disable
# constrained layout or tight layout before printing the figure.
ctx1 = fig._context_adjusting(cache=cache)
ctx2 = fig._context_authorized() # skip backend set_constrained_layout()
ctx3 = rc.context(fig._render_context) # draw with figure-specific setting
with ctx1, ctx2, ctx3:
fig.auto_layout()
if not fig._layout_initialized or layout_dirty:
fig.auto_layout()
fig._layout_initialized = True
fig._layout_dirty = False
return func(self, *args, **kwargs)

# Add preprocessor
Expand Down Expand Up @@ -797,6 +811,9 @@ def __init__(
self._subplot_counter = 0 # avoid add_subplot() returning an existing subplot
self._is_adjusting = False
self._is_authorized = False
self._layout_initialized = False
self._layout_dirty = True
self._skip_autolayout = False
self._includepanels = None
self._render_context = {}
rc_kw, rc_mode = _pop_rc(kwargs)
Expand Down Expand Up @@ -1546,6 +1563,7 @@ def _add_figure_panel(
"""
Add a figure panel.
"""
self._layout_dirty = True
# Interpret args and enforce sensible keyword args
side = _translate_loc(side, "panel", default="right")
if side in ("left", "right"):
Expand Down Expand Up @@ -1579,6 +1597,7 @@ def _add_subplot(self, *args, **kwargs):
"""
The driver function for adding single subplots.
"""
self._layout_dirty = True
# Parse arguments
kwargs = self._parse_proj(**kwargs)

Expand Down Expand Up @@ -2549,6 +2568,7 @@ def format(
ultraplot.gridspec.SubplotGrid.format
ultraplot.config.Configurator.context
"""
self._layout_dirty = True
# Initiate context block
axs = axs or self._subplot_dict.values()
skip_axes = kwargs.pop("skip_axes", False) # internal keyword arg
Expand Down Expand Up @@ -3134,6 +3154,17 @@ def set_canvas(self, canvas):
# method = '_draw' if callable(getattr(canvas, '_draw', None)) else 'draw'
_add_canvas_preprocessor(canvas, "print_figure", cache=False) # saves, inlines
_add_canvas_preprocessor(canvas, method, cache=True) # renderer displays

orig_draw_idle = getattr(type(canvas), "draw_idle", None)
if orig_draw_idle is not None:

def _draw_idle(self, *args, **kwargs):
fig = self.figure
if fig is not None:
fig._skip_autolayout = True
return orig_draw_idle(self, *args, **kwargs)

canvas.draw_idle = _draw_idle.__get__(canvas)
super().set_canvas(canvas)

def _is_same_size(self, figsize, eps=None):
Expand Down Expand Up @@ -3200,6 +3231,8 @@ def set_size_inches(self, w, h=None, *, forward=True, internal=False, eps=None):
super().set_size_inches(figsize, forward=forward)
if not samesize: # gridspec positions will resolve differently
self.gridspec.update()
if not backend and not internal:
self._layout_dirty = True

def _iter_axes(self, hidden=False, children=False, panels=True):
"""
Expand Down
Loading
Loading