python / cpython

The Python programming language
https://www.python.org
Other
63.27k stars 30.29k forks source link

`tkinter.Event` is not subscriptable at runtime but generic in stub file #123341

Closed yowoda closed 1 month ago

yowoda commented 2 months ago

Bug report

Bug description:

import tkinter
tkinter.Event[tkinter.Canvas] # TypeError: 'type' object is not subscriptable

This has been briefly discussed at https://github.com/python/typeshed/issues/12591

It would be great if this problem (a little more detailed version here) could be fixed in the entire standard library. As of right now, the inconsistency of class subscription support is just way too confusing for developers.

CPython versions tested on:

3.8, 3.9, 3.10, 3.11, 3.12, 3.13, CPython main branch

Operating systems tested on:

Linux, Windows

Linked PRs

serhiy-storchaka commented 2 months ago

What is the meaning of tkinter.Event[tkinter.Canvas]? Why the Event class is considered generic?

yowoda commented 2 months ago

https://github.com/python/typeshed/issues/12591#issuecomment-2308961044

In this case, it implies the event was caused by an interaction with the canvas

serhiy-storchaka commented 2 months ago

How do canvas events differ from others?

TeamSpen210 commented 2 months ago

It means that some_event.widget is specifically a tkinter.Canvas object. Even if you don't directly use that attribute, being able to annotate your event handler functions with the specific widget would allow checkers to catch passing the wrong event handler to bind().

Akuli commented 2 months ago

You can also do e.g. event.widget.create_rectangle(...) with an event that came from a Canvas.

To me, this is a very stable part of tkinter stubs. The genericness of Event will not change any time soon, and Event has been generic since 2020.

(I am the typeshed maintainer who basically takes care of everything tkinter related.)

Akuli commented 2 months ago

Older discussion about this: https://github.com/python/typeshed/issues/4431

serhiy-storchaka commented 2 months ago

So this is for the widget attribute? This is not correct. widget is not always the same as the widget for which bind() was called.

Example:

import tkinter
root = tkinter.Tk()
f = tkinter.Frame(root, name='frame', width=150, height=100)
f.pack()
root.wait_visibility()
root.update_idletasks()

def handler(e: 'tkinter.Event[tkinter.Tk]'):
    # assert isinstance(e.widget, tkinter.Tk)
    print(e.widget)

event = '<Enter>'
assert isinstance(root, tkinter.Tk)
root.bind(event, lambda e: print(e.widget))
f.event_generate(event)

The output is .frame, not .. And the commented out assert in the handler would fail.

yowoda commented 2 months ago

Well but the event does come from tkinter.Frame so imo the correct way to type hint this is tkinter.Event[tkinter.Frame] since hovering over the frame is what causes the function to be called

Akuli commented 2 months ago

You are using Tk.bind(), which is already typed with tkinter.Event[tkinter.Misc] (that is, an event where the widget may be of any type). The same goes for Toplevel.bind. For other widgets, event.widget is always* the same widget whose .bind() was called, and the stubs define it in that way.

* Technically not correct. You could use a Frame as a toplevel with the wm manage command and then receive events from arbitrary widgets, as with Tk and Toplevel, but most tkinter users don't do that and tkinter wasn't written to support it very well (Frame doesn't inherit from Wm).

serhiy-storchaka commented 2 months ago

Well, you're probably right. I was not able to find example with non-toplevel widgets.