Skip to content

fix(api): apply_style fails fast on a non-Axes first positional#320

Open
dchaudhari7177 wants to merge 4 commits into
scitex-ai:mainfrom
dchaudhari7177:fix/apply-style-typeerror-fast
Open

fix(api): apply_style fails fast on a non-Axes first positional#320
dchaudhari7177 wants to merge 4 commits into
scitex-ai:mainfrom
dchaudhari7177:fix/apply-style-typeerror-fast

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Symptom

fr.apply_style("SCITEX_STYLE")   # string preset name in the ax slot
# → AttributeError: 'str' object has no attribute 'set_facecolor'

apply_style(ax, style=None) accepted any first positional, so a preset name passed where the Axes belongs sailed past the API boundary and crashed deep inside with an opaque AttributeError. Downstream try/except Exception wrappers silently swallowed it — the exact silent-fallback the issue calls out.

Fix

A TypeError at the boundary, per the issue's proposed shape:

if not isinstance(ax, matplotlib.axes.Axes):
    raise TypeError(
        f"apply_style(ax, style=...): ax must be a matplotlib Axes, "
        f"got {type(ax).__name__}. If you meant a style preset, pass it "
        f"as the style argument: apply_style(ax, style=...)."
    )

I pointed the hint at the existing style= argument rather than load_style('SCITEX'), since that auto-propagation is #159's scope and isn't in this repo yet — keeping this PR to just the apply_style half the issue explicitly separates out. Easy to reword to reference load_style when #159 lands.

Tests

test__style_manager.py gains two regression tests: the string first-positional raises TypeError matching must be a matplotlib Axes, and a real Axes still returns a positive float trace width. pytest tests/figrecipe/_api/ → 32 passed.

Fixes #160

apply_style(ax, style=None) accepted any first positional, so passing
the preset name — apply_style("SCITEX_STYLE") — sailed past the boundary
and crashed deep inside with an opaque AttributeError ('str' object has
no attribute 'set_facecolor'), which downstream try/except blocks
silently swallowed. Raise a TypeError at the boundary naming the bad
type and the correct call shape, so the wrong call fails loud with a
traceback pointing at the call site.

Fixes scitex-ai#160 (the apply_style half; the load_style auto-propagation in
scitex-ai#159 is out of scope here).

Adds regression tests for both the reject and the valid-Axes paths.
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Please sign the SciTeX CLA before your contribution can be merged.
Comment I have read and agree to the SciTeX CLA. to sign.


I have read and agree to the SciTeX CLA.


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@ywatanabe1989 ywatanabe1989 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, and a faithful implementation of exactly what #160 proposed — the guard, the message, and both tests are all right. We ran your tests against current main: 3/3 pass.

One case slipped through, and it is really on us: the issue's own snippet had the same blind spot, so you implemented precisely what we asked. figrecipe's own fr.subplots() returns a RecordingAxes composition wrapper (src/figrecipe/_wrappers/_axes.py — mixins delegating through __getattr__, not an Axes subclass). On current main, fr.apply_style(ax) with that wrapper works via delegation; with the strict isinstance guard it now raises TypeError. Our internal style path already unwraps first (src/figrecipe/_api/_subplots.py:219), and the same one-liner fixes it here — see the suggested change below.

Could you also add a third test pinning the wrapper case, so nobody re-breaks it?

def test_apply_style_accepts_figrecipe_wrapped_axes():
    import figrecipe as fr
    fig, ax = fr.subplots()
    try:
        result = fr.apply_style(ax)
        assert isinstance(result, float)
    finally:
        import matplotlib.pyplot as plt
        plt.close("all")

Everything else is merge-ready. (Separately, whenever convenient: the CLA bot's one-comment signature is the only other thing needed before merge — opening and review needed none.)

Comment on lines +155 to +158
# Fail loud at the boundary: a non-Axes first positional (commonly a preset
# name passed as `apply_style("SCITEX_STYLE")`) otherwise crashes deep
# inside with an opaque `'str' object has no attribute ...` (#160).
if not isinstance(ax, matplotlib.axes.Axes):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Fail loud at the boundary: a non-Axes first positional (commonly a preset
# name passed as `apply_style("SCITEX_STYLE")`) otherwise crashes deep
# inside with an opaque `'str' object has no attribute ...` (#160).
if not isinstance(ax, matplotlib.axes.Axes):
# figrecipe's own subplots() returns a RecordingAxes wrapper (composition,
# not an Axes subclass) — unwrap it first so `fig, ax = fr.subplots();
# fr.apply_style(ax)` keeps working, matching the unwrap in _subplots.py.
ax = getattr(ax, "_ax", ax)
# Fail loud at the boundary: a non-Axes first positional (commonly a preset
# name passed as `apply_style("SCITEX_STYLE")`) otherwise crashes deep
# inside with an opaque `'str' object has no attribute ...` (#160).
if not isinstance(ax, matplotlib.axes.Axes):

fr.subplots() returns a RecordingAxes composition wrapper (delegates via
__getattr__, not an Axes subclass), so the strict isinstance guard
rejected it. Unwrap to ._ax first, mirroring the style path in
_subplots.py. Adds a test pinning the wrapper case.
@dchaudhari7177

Copy link
Copy Markdown
Author

Good catch — fixed. apply_style now unwraps to ax._ax before the isinstance guard (the same one-liner as _subplots.py:219), so a RecordingAxes wrapper from fr.subplots() passes through while a string/other non-Axes still raises TypeError. Added your suggested test_apply_style_accepts_figrecipe_wrapped_axes — the suite is now 4/4.

On the CLA: that's a legal sign-off my collaborator needs to make on their own account, so I've flagged it to them to complete.

@ywatanabe1989 ywatanabe1989 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect — the unwrap is exactly the idiom from _subplots.py, placed before the guard so wrappers pass and a stray string still fails loud, and the new wrapper test pins it. Approving.

For the CLA: our bot checks the commit author account, and your commits are authored as dipakchaudhari12717 — so that's the account that needs to post the one-line signing comment (here or on any of your three PRs; one signature covers all SciTeX repos). Once it's in, we'll merge all three. Thanks for the quick turnaround — three solid PRs on day one is a great start.

@ywatanabe1989

Copy link
Copy Markdown
Collaborator

@dchaudhari7177 — full note on #318 (covers all three). In short: the CI red here was on our side — I updated your branch to current main to fix it — and the only remaining step is the CLA sign-off comment from dipakchaudhari12717 (the commit author) on any one of the three, which covers this one. Sorry for the runaround, and thank you for the careful work. — Yusuke

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0.28.20 regression / docs gap: apply_style(string-name) raises 'str' object has no attribute set_facecolor

4 participants