Skip to content

Add context managers for implot#473

Open
zaicruvoir1rominet wants to merge 4 commits into
pthom:mainfrom
zaicruvoir1rominet:main
Open

Add context managers for implot#473
zaicruvoir1rominet wants to merge 4 commits into
pthom:mainfrom
zaicruvoir1rominet:main

Conversation

@zaicruvoir1rominet

@zaicruvoir1rominet zaicruvoir1rominet commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Hi there !

Adding context managers, as discussed in #472.
Do tell if things are not to your liking.


About tests

I don't know how to add tests, as I have no idea how to use the Dear ImGui Test Engine with implot specifically.
I tested on a small sample project:

image
with implot_ctx.create_context():
    immapp.run(display_persons)

[...]

def display_ages_graph(persons: Persons) -> None:
    ages_structure = persons.ages_structure()

    age_count = np.array(list(ages_structure.values()), dtype=np.int8)
    ages = [str(age) for age in ages_structure]
    positions = list(range(len(ages)))

    with implot_ctx.begin_plot("Ages Structure") as plot:
        if not plot.visible:
            return

        implot.setup_axes(
            "Age", "Age Count",
            implot.AxisFlags_.auto_fit, implot.AxisFlags_.auto_fit
        )
        implot.setup_axis_ticks(
            axis=implot.ImAxis_.x1,
            values=positions,
            labels=ages,
        )
        implot.plot_bars(
            label_id="Age count",
            values=age_count,
        )

Note1: yes, I know immap has a with_implot param, this is done for the test
Note2: yes, I know I should'nt be building np.arrays in iterating code

@zaicruvoir1rominet

Copy link
Copy Markdown
Contributor Author

(I will add context managers to implot3d in a different PR, once this one is OK)

@pthom

pthom commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Hi @zaicruvoir1rominet,

I'm back from my weekend. Thanks a lot for this PR and for your involvement :-) This is a nice idea, and I really appreciate the care you put into documenting the new API.

Since this adds a user-facing API, I'd like to ask for a few changes to get it production-ready. Please bear with me, there are several of them, but most are quick, and the core of your PR is solid: the context managers themselves are well designed.

Here's the list, roughly in order:

1. Rebase on the latest main

Your branch is currently based on v1.6.3 (tag from May 2025, so over a year old), which also pins an old ImPlot. Could you rebase on the current main?

git fetch upstream      # or your remote pointing to pthom/imgui_bundle
git rebase upstream/main

I tried the rebase locally to gauge the risk, and the good news is that your context managers just forward their arguments to the underlying implot functions, and all six wrapped signatures (begin_plot, begin_subplots, push_style_color, push_style_var, push_colormap, push_plot_clip_rect) are unchanged on current main. So the rebase should be low-risk.

One thing to verify after rebasing: ImPlot v1.0 (Apr 2026) reworked the plot_* functions to use implot.Spec(...). That doesn't affect your wrappers, but a couple of your docstring examples call implot.plot_text(...) / plot_bars(...). Please run them once after the rebase to confirm the examples still match the new API.

2. Fix the mypy errors flagged by CI

See this CI run:

implot_ctx.py:282: error: Overloaded function implementation does not accept all possible parameters of signature 2  [misc]
implot_ctx.py:325: error: Overloaded function implementation does not accept all possible parameters of signature 2  [misc]

This comes from the push_colormap overloads: the second one is typed (count: int = 1), but implot.push_colormap actually takes either a colormap enum or a name (there's no count argument: that belongs to pop_colormap). The overloads add no behavior here, so the simplest fix is to drop them and use a single union-typed parameter. I verified this type-checks clean with mypy:

class _PushColormap:
    """Internal, do not call this directly."""

    def __init__(self, cmap_or_name: implot.Colormap | str) -> None:
        self.cmap_or_name = cmap_or_name

    def __enter__(self) -> _PushColormap:
        implot.push_colormap(self.cmap_or_name)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        implot.pop_colormap()

    def __repr__(self) -> str:
        return f"{self.__class__.__qualname__}()"


def push_colormap(cmap_or_name: implot.Colormap | str) -> _PushColormap:
    """Pushes a colormap onto the ImPlot stack, by enum (implot.Colormap_) or by name.
    Automatically pops it at end.

    Examples:
        >>> with implot_ctx.push_colormap(implot.Colormap_.deep):
        ...     ...
    """
    return _PushColormap(cmap_or_name)

3. Register implot_ctx in __init__.py

Right now the module is added but never exposed, so from imgui_bundle import implot_ctx won't work (unlike imgui_ctx and imgui_node_editor_ctx). In bindings/imgui_bundle/__init__.py, inside the if has_submodule("implot"): block, please follow the same pattern as imgui_node_editor_ctx:

    from imgui_bundle import implot_ctx as implot_ctx  # noqa: E402
    __all__.extend(["implot", "implot_ctx"])   # replaces the existing __all__.extend(["implot"])

4. Make the doc clearer about the boolean return (at the top of implot_ctx.py)

Your examples already show if plot:, but it's worth a short, explicit note that begin_plot / begin_subplots return a context object that must be truth-tested before drawing (the plot can be collapsed/clipped), whereas the push_* ones don't need testing. A sentence or two near the top of implot_ctx.py would help users avoid the classic begin/end mistake.

5. Trim the repeated body in the push_* docstrings

The same plot_text(..., spec=implot.Spec(flags=implot.TextFlags_.vertical)) snippet is currently repeated as the body of the examples in push_style_color, push_style_var, and push_colormap. The with lines themselves are already nicely specific to each function, so the only thing that's copy-pasted is that body, which doesn't relate to colormaps or style vars.

For these push/pop context managers the value is entirely in the with line (the body is just "whatever plotting you'd normally do"), so the simplest fix is to replace that body with an ellipsis:

    Examples:
        >>> with implot_ctx.push_colormap(implot.Colormap_.deep):
        ...     ...  # plot as usual

No need to invent a worked body for each one. Please keep the real example bodies on begin_plot and begin_subplots though, since there the body (the if plot: test plus an actual plot_bars(...) call) is the part that matters.


6. Changes I'll do myself after your changes

On my side, and after you made these changes, I'll take care of updating the demos to showcase both the classic and the context-manager style (demo_implot_markdown.py, haiku_implot_heart.py, demo_implot.py), since those need to stay in sync between the Python and C++ versions and I'd rather handle that parity myself.

Also, I'll probably add a small test under tests/ (there's a pytest setup there) that exercises the context managers? Even a minimal smoke test that enters/exits each one within a headless ImGui frame would be valuable to guard against regressions. Since I did not yet add tests for imgui_ctx, I'll do it at the same time for implot and imgui. Those tests can then serve as a model when implementing the context managers for implot3d.


Thanks again, this is a welcome addition. Let me know if anything is unclear or if you'd like me to take any of these items off your plate.

@zaicruvoir1rominet

zaicruvoir1rominet commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

This is a nice idea, and I really appreciate the care you put into documenting the new API.

❤️

Please bear with me, there are several of them, but most are quick,

Don't worry, I know what I'm getting into when I make a proposal 👍

Thanks for your very detailed review.

  • Rebase on the latest main
    How did I fork from a commit so old an not even realize it ?!?
  • Fix the mypy errors flagged by CI
    Yup, my bad, my eyes slipped and I implemented the wrong overload. Good job on your part to notice that it actually came from implot.push_colormap.
    I still believe the overload is necessary, as the 2 different implot.push_colormap overloads have 2 different parameters names: cmap & name. Meaning if you used a key-style declaration like implot.push_colormap(cmap=...) you wouldn't be able to use with implot.push_colormap(cmap=...) as a direct replacement, so if you use automated refactoring tools, you would have to make a case specific for this replacement.
    If you don't agree, I'll of course switch back to no-overloads (as I did with all other functions)
  • Register implot_ctx in __init__.py
  • Make the doc clearer about the boolean return (at the top of implot_ctx.py)
  • Trim the repeated body in the push_* docstrings

[...]

  • Provide code snippets (in this PR comments) that you could directly use within the imgui manual to save you some time ?

@pthom

pthom commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Hi @zaicruvoir1rominet,

Thanks for this new round, it addressed most of the review nicely: the mypy fix, the doc note at the top of implot_ctx.py, and the trimmed docstrings all look good. And thanks for reconsidering the push_colormap overloads yourself, I think the union-typed version is the right call here.

While preparing the follow-up work (demos, doc, tests, see below), I found one real issue and a few typos, so here is a (hopefully last!) round of changes on your side:

1. implot_ctx registration must move to the implot block of __init__.py

Right now it sits in the if has_submodule("imgui"): block, and this actually breaks the whole module at runtime:

>>> from imgui_bundle import implot_ctx
>>> implot_ctx.create_context().__enter__()
AttributeError: module 'imgui_bundle.implot' has no attribute 'create_context'

The reason is subtle: at that point of __init__.py, the native implot module is not loaded yet, so the from imgui_bundle import implot at the top of implot_ctx.py silently falls back to importing the bindings/imgui_bundle/implot/ stub directory as a namespace package, and implot_ctx keeps that dead reference forever. (It worked in your earlier testing because back then __init__.py did not import implot_ctx at all, so any manual import happened after imgui_bundle was fully initialized.)

The fix is to place it at the end of the if has_submodule("implot"): block:

if has_submodule("implot"):
    from imgui_bundle._imgui_bundle import implot as implot
    _publish("implot", implot)
    __all__.extend(["implot"])
    # ... (existing Flag types lines) ...

    from imgui_bundle import implot_ctx as implot_ctx  # noqa: E402
    __all__.extend(["implot_ctx"])

2. Small docstring typos in implot_ctx.py

  • line 4: "implot.begin...() and imgui.end...()" should be implot.end...()
  • line 8: "whether the plot is or not" is missing a word ("is visible or not")
  • lines 10 and 15: imgui_ctx.begin_plot() / imgui_ctx.push_style_color() should be implot_ctx.
  • line 200 (begin_subplots example): implot_ctx.begin_subplot("My plot") does not exist, should be begin_plot
  • lines 239 and 273: the example with blocks contain only a comment, which is not valid Python if someone pastes it. ... # plot as usual (like in push_colormap) works.

3. Please squash everything into one commit

The branch currently has 4 commits including a merge. Since a merge commit makes rebase -i fiddly, the simplest recipe:

git fetch upstream      # "upstream" or your remote name, pointing to pthom/imgui_bundle
git reset --soft $(git merge-base upstream/main HEAD)
git commit -m "Add context managers for implot (implot_ctx)"
git push --force-with-lease

And a tip for next time (e.g. the implot3d_ctx PR): create a named feature branch from upstream main (git switch -c implot3d_ctx upstream/main) instead of working on your fork's main. It keeps your main clean and makes rebasing and squashing much easier.


On my side, I have the follow-up ready on a local branch, to be merged right after your PR. Sharing it here since it can serve as a model for the implot3d_ctx PR:

Demo (in demos_immapp/demo_python_context_manager.py, which is our Python-only demo for context managers, with addons.with_implot = True and two new entries in its demos dict):

def demo_implot_begin_plot():
    # implot_ctx.begin_plot calls implot.end_plot() automatically.
    # Testing `if plot:` is required: the plot may be collapsed or clipped.
    x = np.arange(0, np.pi * 4, 0.01)
    y = np.cos(x + imgui.get_time() * 4)
    with implot_ctx.begin_plot("Wave", immapp.em_to_vec2(25, 15)) as plot:
        if plot:
            implot.setup_axes("x", "y")
            implot.plot_line("cos", x, y)


def demo_implot_push_pop():
    # The push_* context managers pop automatically: no need to test their value
    x = np.arange(0, np.pi * 4, 0.01)
    y = np.sin(x)
    with implot_ctx.push_style_color(implot.Col_.plot_bg, ImVec4(0.3, 0.6, 0.3, 1.0)):
        with implot_ctx.push_colormap(implot.Colormap_.cool):
            with implot_ctx.begin_plot("Styled plot", immapp.em_to_vec2(25, 15)) as plot:
                if plot:
                    implot.plot_line("sin", x, y)

Doc: a new subsection "Python: context managers (implot_ctx)" in docs/book/addons/plotting.md.

Tests: two smoke tests. A headless one in tests/test_implot_ctx.py (this is the one that would have caught the __init__.py issue; it runs on every wheel build in CI):

def test_implot_ctx_headless() -> None:
    # We skip windows, see note at the top of lg_imgui_bundle_test.py
    if sys.platform == "win32":
        return

    from imgui_bundle import imgui, implot, implot_ctx

    imgui_context = imgui.create_context()
    with implot_ctx.create_context():
        # push/pop pairs which do not require a running frame
        with implot_ctx.push_style_color(implot.Col_.plot_bg, (1.0, 0.0, 0.0, 1.0)):
            pass
        with implot_ctx.push_style_var(implot.StyleVar_.plot_border_size, 2.0):
            pass
        with implot_ctx.push_colormap(implot.Colormap_.deep):
            pass
        with implot_ctx.push_colormap("Paired"):  # colormap by name
            pass
    imgui.destroy_context(imgui_context)

And a GUI one in tests/tests_python_gui/test_implot_ctx_gui.py for the functions that need a running frame (begin_plot, begin_subplots, push_plot_clip_rect):

import numpy as np
from imgui_bundle import hello_imgui, imgui, implot, implot_ctx


def test_implot_ctx_gui() -> None:
    results = {}

    def gui() -> None:
        x = np.arange(0, 10, 0.1)
        y = np.sin(x)

        with implot_ctx.begin_plot("Plot") as plot:
            results["plot_visible"] = bool(plot)
            if plot:
                implot.plot_line("sin", x, y)
                with implot_ctx.push_plot_clip_rect():
                    pass

        with implot_ctx.begin_subplots("Subplots", 1, 2, hello_imgui.em_to_vec2(30, 10)) as subplots:
            results["subplots_visible"] = bool(subplots)
            if subplots:
                for _ in range(2):
                    with implot_ctx.begin_plot("##sub") as plot:
                        if plot:
                            implot.plot_line("sin", x, y)

        if imgui.get_frame_count() == 3:
            hello_imgui.get_runner_params().app_shall_exit = True

    # implot_ctx.create_context() is also exercised in real conditions
    # (hello_imgui.run does not create an implot context by itself)
    with implot_ctx.create_context():
        hello_imgui.run(gui)

    assert results["plot_visible"], "begin_plot should be visible in a fresh window"
    assert results["subplots_visible"], "begin_subplots should be visible in a fresh window"

To run them locally: pip install pytest numpy, then from the repo root simply run pytest tests/. The GUI test opens a small window for a fraction of a second (the gui function exits at frame 3); our CI wheel builds only run the headless ones.

About your kind offer to provide snippets for the manual: no need, the above covers it, thanks!

So: once the __init__.py move, the docstring typos and the squash are in, this is good to merge. Thanks again for this contribution and for your patience with the review!

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.

2 participants