From ae40011a8309d1697551e81cfdb5a43da7aa2b05 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 27 Jan 2026 19:42:04 +1000 Subject: [PATCH 1/2] Delegate external axes methods with guardrails Route attribute access to external axes by default while keeping UltraPlot-specific formatting and container overrides on the parent. --- ultraplot/axes/container.py | 62 +++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/ultraplot/axes/container.py b/ultraplot/axes/container.py index fdad0e01b..27c057153 100644 --- a/ultraplot/axes/container.py +++ b/ultraplot/axes/container.py @@ -61,6 +61,34 @@ class ExternalAxesContainer(CartesianAxes): ``external_padding=2`` or ``external_padding=0`` to disable padding entirely. """ + _EXTERNAL_DELEGATE_BLOCKLIST = { + # Keep container overrides that mark external axes stale. + "plot", + "scatter", + "fill", + "contour", + "contourf", + "pcolormesh", + "tripcolor", + "tricontour", + "tricontourf", + "triplot", + "imshow", + "hexbin", + # Keep UltraPlot formatting/guide behaviors on the container. + "format", + "colorbar", + "legend", + "set_title", + "set_suptitle", + "set_xlabel", + "set_ylabel", + "set_zlabel", + "set_xticks", + "set_yticks", + "set_zticks", + } + def __init__( self, *args, external_axes_class=None, external_axes_kwargs=None, **kwargs ): @@ -804,27 +832,27 @@ def __getattr__(self, name): This allows the container to act as a transparent wrapper, forwarding plotting methods and other attributes to the external axes. """ - # Avoid infinite recursion for private attributes - # But allow parent class lookups during initialization - if name.startswith("_"): - # During initialization, let parent class handle private attributes - # This prevents interfering with parent class setup - raise AttributeError( - f"'{type(self).__name__}' object has no attribute '{name}'" - ) - - # Try to get from external axes if it exists - if hasattr(self, "_external_axes") and self._external_axes is not None: - try: - return getattr(self._external_axes, name) - except AttributeError: - pass - - # Not found anywhere raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) + def __getattribute__(self, name): + """ + Prefer external axes attributes unless explicitly blocked. + """ + if name.startswith("_"): + return object.__getattribute__(self, name) + blocklist = object.__getattribute__(self, "_EXTERNAL_DELEGATE_BLOCKLIST") + if name in blocklist: + return object.__getattribute__(self, name) + try: + external = object.__getattribute__(self, "_external_axes") + except AttributeError: + external = None + if external is not None and hasattr(external, name): + return getattr(external, name) + return object.__getattribute__(self, name) + def __dir__(self): """Include external axes attributes in dir() output.""" attrs = set(super().__dir__()) From a2107806d209c3ceb9c72af0cf627da4cc67d00a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 27 Jan 2026 20:05:45 +1000 Subject: [PATCH 2/2] Simplify external axes delegation Delegate missing methods to the external axes via __getattr__ and keep a small blocklist for UltraPlot formatting/guide APIs. --- ultraplot/axes/container.py | 48 ++++++------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/ultraplot/axes/container.py b/ultraplot/axes/container.py index 27c057153..2afd07f4d 100644 --- a/ultraplot/axes/container.py +++ b/ultraplot/axes/container.py @@ -62,31 +62,11 @@ class ExternalAxesContainer(CartesianAxes): """ _EXTERNAL_DELEGATE_BLOCKLIST = { - # Keep container overrides that mark external axes stale. - "plot", - "scatter", - "fill", - "contour", - "contourf", - "pcolormesh", - "tripcolor", - "tricontour", - "tricontourf", - "triplot", - "imshow", - "hexbin", # Keep UltraPlot formatting/guide behaviors on the container. "format", "colorbar", "legend", "set_title", - "set_suptitle", - "set_xlabel", - "set_ylabel", - "set_zlabel", - "set_xticks", - "set_yticks", - "set_zticks", } def __init__( @@ -827,32 +807,18 @@ def get_tightbbox(self, renderer, *args, **kwargs): def __getattr__(self, name): """ - Delegate attribute access to the external axes when not found on container. - - This allows the container to act as a transparent wrapper, forwarding - plotting methods and other attributes to the external axes. + Delegate missing attributes to the external axes unless blocked. """ + if name in self._EXTERNAL_DELEGATE_BLOCKLIST: + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + if self._external_axes is not None: + return getattr(self._external_axes, name) raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) - def __getattribute__(self, name): - """ - Prefer external axes attributes unless explicitly blocked. - """ - if name.startswith("_"): - return object.__getattribute__(self, name) - blocklist = object.__getattribute__(self, "_EXTERNAL_DELEGATE_BLOCKLIST") - if name in blocklist: - return object.__getattribute__(self, name) - try: - external = object.__getattribute__(self, "_external_axes") - except AttributeError: - external = None - if external is not None and hasattr(external, name): - return getattr(external, name) - return object.__getattribute__(self, name) - def __dir__(self): """Include external axes attributes in dir() output.""" attrs = set(super().__dir__())