Context
Raised from serapeum-org/pyramids#1077. pyramids'
Dataset.plot() forwards fig / ax straight into ArrayGlyph, and while documenting that passthrough we
exercised the partial forms. ax= alone works; fig= alone crashes inside cleopatra.
Passing a figure without an axes to ArrayGlyph leaves self.ax as None, and plot() then dereferences it —
raising an AttributeError that names an internal attribute and gives the caller no clue what they did wrong.
Problem / Current Behaviour
ArrayGlyph(array, fig=fig) (no ax) raises on .plot():
AttributeError: 'NoneType' object has no attribute '_cleo_projection_frame' and no __dict__ for setting new attributes
The two partial forms are asymmetric — verified on cleopatra 0.33.0:
| Call |
Result |
ArrayGlyph(arr, ax=ax).plot() |
works (axes adopted) |
ArrayGlyph(arr, fig=fig).plot() |
AttributeError (above) |
ArrayGlyph(arr, fig=fig, ax=ax).plot() |
works |
ArrayGlyph(arr).plot() |
works (creates its own) |
ax= alone has worked since #130; fig= alone appears never to have been handled.
Root cause
self.ax is never derived from the supplied fig, and _clear_projection_frame assigns the marker attribute
without a None guard:
# cleopatra/glyphs/base/glyph.py:264 (_clear_projection_frame)
frame = getattr(ax, "_cleo_projection_frame", None)
ax._cleo_projection_frame = None # <-- ax is None when only `fig` was supplied
reached via:
cleopatra/glyphs/gridded/array_glyph.py:3604 plot()
self._sync_projection_frame(projection_draws_frame(projection))
cleopatra/glyphs/gridded/array_glyph.py:2519 _sync_projection_frame()
had_frame = _clear_projection_frame(self.ax)
cleopatra/glyphs/base/glyph.py:264 _clear_projection_frame()
ax._cleo_projection_frame = None
Note the getattr(ax, ..., None) on the line above already tolerates a None ax; only the assignment does not.
Affected locations
| File |
Symbol |
Notes |
cleopatra/glyphs/base/glyph.py:264 |
_clear_projection_frame |
Assigns ax._cleo_projection_frame with no None guard |
cleopatra/glyphs/gridded/array_glyph.py:2519 |
_sync_projection_frame |
Passes self.ax, which can be None |
cleopatra/glyphs/gridded/array_glyph.py |
ArrayGlyph.__init__ |
Accepts fig= but never derives an axes from it |
Steps to Reproduce
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from cleopatra.glyphs.gridded.array_glyph import ArrayGlyph
fig, _ = plt.subplots()
ArrayGlyph(np.arange(9, dtype="float32").reshape(3, 3), fig=fig).plot()
# AttributeError: 'NoneType' object has no attribute '_cleo_projection_frame'
Swapping fig=fig for ax=ax succeeds, which is what makes the failure surprising.
Proposed Solution
Preferred — honour fig on its own, since "draw into my figure" is a legitimate request and is the natural
counterpart to the already-supported ax:
- In
ArrayGlyph.__init__ (or wherever fig / ax are normalised), when fig is supplied and ax is not, derive
the axes from the figure — e.g. ax = fig.gca() (or fig.add_subplot(111) when the figure has no axes yet) —
so the glyph draws into the caller's figure instead of leaving self.ax is None.
- Defensively guard
_clear_projection_frame so a None axes is a no-op rather than an AttributeError:
def _clear_projection_frame(ax):
if ax is None:
return False
...
If honouring fig alone is not desired, then at minimum raise a clear, actionable error at construction time
(e.g. ValueError("fig= requires ax=; pass both, or pass ax= alone")) instead of the current internal
AttributeError.
Out of Scope
- The
ax=-only and fig=+ax= paths, which already work.
- Any change to how the projection frame itself is drawn.
Effort Estimate
Size: XS
Rationale: derive the axes in one place plus a two-line None guard, with a regression test per partial form.
Definition of Done
Environment
- cleopatra 0.33.0
- matplotlib 3.11.1
- Python 3.14.7
Context
Passing a figure without an axes to
ArrayGlyphleavesself.axasNone, andplot()then dereferences it —raising an
AttributeErrorthat names an internal attribute and gives the caller no clue what they did wrong.Problem / Current Behaviour
ArrayGlyph(array, fig=fig)(noax) raises on.plot():The two partial forms are asymmetric — verified on cleopatra 0.33.0:
ArrayGlyph(arr, ax=ax).plot()ArrayGlyph(arr, fig=fig).plot()ArrayGlyph(arr, fig=fig, ax=ax).plot()ArrayGlyph(arr).plot()ax=alone has worked since #130;fig=alone appears never to have been handled.Root cause
self.axis never derived from the suppliedfig, and_clear_projection_frameassigns the marker attributewithout a
Noneguard:reached via:
Note the
getattr(ax, ..., None)on the line above already tolerates aNoneax; only the assignment does not.Affected locations
cleopatra/glyphs/base/glyph.py:264_clear_projection_frameax._cleo_projection_framewith noNoneguardcleopatra/glyphs/gridded/array_glyph.py:2519_sync_projection_frameself.ax, which can beNonecleopatra/glyphs/gridded/array_glyph.pyArrayGlyph.__init__fig=but never derives an axes from itSteps to Reproduce
Swapping
fig=figforax=axsucceeds, which is what makes the failure surprising.Proposed Solution
Preferred — honour
figon its own, since "draw into my figure" is a legitimate request and is the naturalcounterpart to the already-supported
ax:ArrayGlyph.__init__(or whereverfig/axare normalised), whenfigis supplied andaxis not, derivethe axes from the figure — e.g.
ax = fig.gca()(orfig.add_subplot(111)when the figure has no axes yet) —so the glyph draws into the caller's figure instead of leaving
self.ax is None._clear_projection_frameso aNoneaxes is a no-op rather than anAttributeError:If honouring
figalone is not desired, then at minimum raise a clear, actionable error at construction time(e.g.
ValueError("fig= requires ax=; pass both, or pass ax= alone")) instead of the current internalAttributeError.Out of Scope
ax=-only andfig=+ax=paths, which already work.Effort Estimate
Size:
XSRationale: derive the axes in one place plus a two-line
Noneguard, with a regression test per partial form.Definition of Done
ArrayGlyph(arr, fig=fig).plot()draws into the supplied figure instead of raising_clear_projection_frame(None)is a no-op rather than anAttributeErrorneither,axonly,figonly,both)ax=/fig=+ax=behaviour is unchangedEnvironment