Repository: matplotlib/matplotlib

Issue:
[Bug]: close_group is not called when using patheffects
### Bug summary

I am in the process of developing a custom `Renderer` for matplotlib. My understanding is that every `Renderer.open_group` call will have a corresponding `Renderer.close_group` call. However, when patheffects are used (at least), the `close_group` never gets called.

### Code for reproduction

```Python
from matplotlib import patheffects
from matplotlib.backend_bases import FigureCanvasBase, GraphicsContextBase, RendererBase
from matplotlib.figure import Figure
from matplotlib.path import Path
from matplotlib.transforms import Transform
from matplotlib.typing import ColorType


class Renderer(RendererBase):
    def __init__(self) -> None:
        super().__init__()
        self.indent = ''

    def open_group(self, s: str, gid: str | None = None) -> None:
        print(self.indent + 'open ' + s)
        self.indent += '  '
        return super().open_group(s, gid)

    def close_group(self, s: str) -> None:
        self.indent = self.indent[:-2]
        print(self.indent + 'close ' + s)
        return super().close_group(s)

    def draw_path(
        self, gc: GraphicsContextBase, path: Path, transform: Transform, rgbFace: ColorType | None = None
    ) -> None:
        pass


class Canvas(FigureCanvasBase):
    def draw(self):
        self.figure.draw(Renderer())


figure = Figure()
canvas = Canvas(figure)
ax = figure.gca()
ax.set_axis_off()

cg1 = ax.contour([[0, 1], [2, 3]])
cg1.set(path_effects=[patheffects.withTickedStroke(angle=135)])

canvas.draw()
```


### Actual outcome

    open figure
      open patch
      close patch
      open axes
        open QuadContourSet
        close axes
      close figure

### Expected outcome

`QuadContourSet` is closed.

    open figure
      open patch
      close patch
      open axes
        open QuadContourSet
        close QuadContourSet
      close axes
    close figure

### Additional information

After some investigation, it appears that in `Collection.draw`, [if any path effects are present](https://github.com/matplotlib/matplotlib/blob/76eaa9669471b86a2569885adf760dc388585783/lib/matplotlib/collections.py#L367), the renderer is updated to `PathEffectRenderer`. Subsequently, `close_group` is called on this updated renderer, but this call does not reach the original renderer.

### Operating system

Ubuntu

### Matplotlib Version

3.8.3

### Matplotlib Backend

Custom

### Python version

Python 3.10.12

### Jupyter version

_No response_

### Installation

pip


Discussion:
sherlockdoyle is correct. The issue can be traced down to:

```
# collection.py, Collection::draw()
370     if self.get_path_effects():  # true, as path effects are present
371         from matplotlib.patheffects import PathEffectRenderer
372         renderer = PathEffectRenderer(self.get_path_effects(), renderer)
```

where the renderer gets changed to a `PathEffectRenderer` object.

Seems that the renderer's behavior is meant to be kept unaltered besides the added path effects, but methods including `open_group` and `close_group` are not overridden in the class definition. In fact, the `PathEffectRenderer` docstring provides a note on this.

```
class PathEffectRenderer(RendererBase):
    """
    Implements a Renderer which contains another renderer.

    This proxy then intercepts draw calls, calling the appropriate
    :class:`AbstractPathEffect` draw method.

    .. note::
        Not all methods have been overridden on this RendererBase subclass.
        It may be necessary to add further methods to extend the PathEffects
        capabilities further.
    """
```

I opened a pull request overriding the two methods.



Patch:
diff --git a/lib/matplotlib/patheffects.py b/lib/matplotlib/patheffects.py
index 110868717936..cf159ddc4023 100644
--- a/lib/matplotlib/patheffects.py
+++ b/lib/matplotlib/patheffects.py
@@ -152,6 +152,12 @@ def __getattribute__(self, name):
         else:
             return object.__getattribute__(self, name)
 
+    def open_group(self, s, gid=None):
+        return self._renderer.open_group(s, gid)
+
+    def close_group(self, s):
+        return self._renderer.close_group(s)
+
 
 class Normal(AbstractPathEffect):
     """


Test patch:
diff --git a/lib/matplotlib/tests/test_patheffects.py b/lib/matplotlib/tests/test_patheffects.py
index 32cce14178a9..b86cca548854 100644
--- a/lib/matplotlib/tests/test_patheffects.py
+++ b/lib/matplotlib/tests/test_patheffects.py
@@ -5,6 +5,8 @@
 import matplotlib.patheffects as path_effects
 from matplotlib.path import Path
 import matplotlib.patches as patches
+from matplotlib.backend_bases import RendererBase
+from matplotlib.patheffects import PathEffectRenderer
 
 
 @image_comparison(['patheffect1'], remove_text=True)
@@ -192,3 +194,20 @@ def test_patheffects_spaces_and_newlines():
                     bbox={'color': 'thistle'})
     text1.set_path_effects([path_effects.Normal()])
     text2.set_path_effects([path_effects.Normal()])
+
+
+def test_patheffects_overridden_methods_open_close_group():
+    class CustomRenderer(RendererBase):
+        def __init__(self):
+            super().__init__()
+
+        def open_group(self, s, gid=None):
+            return "open_group overridden"
+
+        def close_group(self, s):
+            return "close_group overridden"
+
+    renderer = PathEffectRenderer([path_effects.Normal()], CustomRenderer())
+
+    assert renderer.open_group('s') == "open_group overridden"
+    assert renderer.close_group('s') == "close_group overridden"