Skip to content

[Bug]: possible race condition between TextBoxes with toolmanager and QtAgg #29687

Open
@derrickturk

Description

@derrickturk

Bug summary

Under the QtAgg backend, when creating multiple TextBox widgets with toolmanager in use, clicking in one TextBox and then directly in another sometimes results in ValueError('already locked') from TextBox.begin_typing.

Code for reproduction

import os
import sys

import matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import TextBox


matplotlib.use('QTAgg')
matplotlib.rcParams['toolbar'] = 'toolmanager'


def main(argv: list[str]) -> int:
    fig, ax = plt.subplots()
    plt.subplots_adjust(bottom=0.2)
    xs = list(range(25))
    ys = [x ** 3 for x in xs]
    plt.plot(xs, ys)

    tb1 = TextBox(plt.axes([0.15, 0.05, 0.3, 0.075]), 'TB1')
    tb2 = TextBox(plt.axes([0.55, 0.05, 0.3, 0.075]), 'TB2')

    plt.show()

    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))

Actual outcome

Traceback (most recent call last):
  File "/redacted/.venv/lib/python3.13/site-packages/matplotlib/cbook.py", line 361, in process
    func(*args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^
  File "/redacted/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 1499, in _click
    self.begin_typing()
    ~~~~~~~~~~~~~~~~~^^
  File "/redacted/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 1464, in begin_typing
    toolmanager.keypresslock(self)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/redacted/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 44, in __call__
    raise ValueError('already locked')
ValueError: already locked

Expected outcome

I'd expect to be able to navigate from one text box to the other without this happening.

Additional information

I believe this is related to the changes in #14343. I subclassed TextBox to get a little more information:

import os
import sys

import matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import TextBox


matplotlib.use('QTAgg')
matplotlib.rcParams['toolbar'] = 'toolmanager'
print(matplotlib.get_backend())

class SnitchBox(TextBox):
    def __init__(self, name, *rest) -> None:
        self.name = name
        super().__init__(*rest)

    def begin_typing(self) -> None:
        print(f'{self.name} BEGIN TYPING')
        super().begin_typing()

    def stop_typing(self) -> None:
        print(f'{self.name} STOP TYPING')
        super().stop_typing()


def main(argv: list[str]) -> int:
    fig, ax = plt.subplots()
    plt.subplots_adjust(bottom=0.2)
    xs = list(range(25))
    ys = [x ** 3 for x in xs]
    plt.plot(xs, ys)

    tb1 = SnitchBox('TB1', plt.axes([0.15, 0.05, 0.3, 0.075]), 'TB1')
    tb2 = SnitchBox('TB2', plt.axes([0.55, 0.05, 0.3, 0.075]), 'TB2')

    plt.show()

    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))

With this instrumentation, I sometimes see the following trace when clicking between textboxes:

TB2 BEGIN TYPING
TB1 BEGIN TYPING
Traceback (most recent call last):
  File "/home/dwt/work/vso/VSO_GBT2/.venv/lib/python3.13/site-packages/matplotlib/cbook.py", line 361, in process
    func(*args, **kwargs)
    ~~~~^^^^^^^^^^^^^^^^^
  File "/home/dwt/work/vso/VSO_GBT2/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 1499, in _click
    self.begin_typing()
    ~~~~~~~~~~~~~~~~~^^
  File "/home/dwt/work/vso/VSO_GBT2/demo.py", line 20, in begin_typing
    super().begin_typing()
    ~~~~~~~~~~~~~~~~~~~~^^
  File "/home/dwt/work/vso/VSO_GBT2/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 1464, in begin_typing
    toolmanager.keypresslock(self)
    ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/home/dwt/work/vso/VSO_GBT2/.venv/lib/python3.13/site-packages/matplotlib/widgets.py", line 44, in __call__
    raise ValueError('already locked')
ValueError: already locked
TB2 STOP TYPING

Which implies that (perhaps this is related to the QtAgg backend specifically?) the begin_typing method on the "new" textbox is sometimes fired before the stop_typing method on the "old" textbox. begin_typing tries to take the keypresslock, but the other textbox still holds it (it's released in stop_typing).

Operating system

Arch Linux (6.12.7-arch1-1)

Matplotlib Version

3.10.0

Matplotlib Backend

QtAgg

Python version

No response

Jupyter version

No response

Installation

pip

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions