DirectButtons extraArgs relies on a __name__ argument

Bug #800861 reported by Jean-André Santoni
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
Panda3D
Fix Released
Undecided
David Rose

Bug Description

I was first trying to use this code:

            bubu[key] = DirectButton(
                text = (key, "Join", "Join", "Full"),
                command = lambda: self.commandAndDestroy( lambda: self.command(key) ),
                #extraArgs = [ functools.partial(self.command, key) ],
                #command = self.commandAndDestroy,
                #extraArgs = [ funcs[key] ],
                scale = scale,
                text_font = font,
                text_fg = (.1875,.15625,.125,1),
                text_shadow = (.5,.46484375,.40625,1),
                text_align = TextNode.ALeft,
                rolloverSound = hover_snd,
                clickSound = clicked_snd,
                pressEffect = 0,
                parent = self.frame
            )

But for an unknown reason, using lambda was the source of a very strange behaviour: whatver button I was clicking, the command was invoked with the last 'key' argument. Like if I had clicked on the last button!

sjoerd_ on IRC told me to use functools.partial instead of lambda. Wich caused this exception:

Traceback (most recent call last):
  File "main.py", line 1038, in <module>
    run()
  File "/usr/share/panda3d/direct/showbase/ShowBase.py", line 2531, in run
    self.taskMgr.run()
  File "/usr/share/panda3d/direct/task/Task.py", line 496, in run
    self.step()
  File "/usr/share/panda3d/direct/task/Task.py", line 454, in step
    self.mgr.poll()
  File "/usr/share/panda3d/direct/showbase/EventManager.py", line 61, in eventLoopTask
    self.doEvents()
  File "/usr/share/panda3d/direct/showbase/EventManager.py", line 55, in doEvents
    processFunc(self.eventQueue.dequeueEvent())
  File "/usr/share/panda3d/direct/showbase/EventManager.py", line 122, in processEvent
    messenger.send(eventName, paramList)
  File "/usr/share/panda3d/direct/showbase/Messenger.py", line 325, in send
    self.__dispatch(acceptorDict, event, sentArgs, foundWatch)
  File "/usr/share/panda3d/direct/showbase/Messenger.py", line 410, in __dispatch
    method (*(extraArgs + sentArgs))
  File "/usr/share/panda3d/direct/gui/DirectButton.py", line 103, in commandFunc
    apply(self['command'], self['extraArgs'])
  File "/home/kivutar/code/tethical/client/GUI.py", line 201, in commandAndDestroy
    Func(command),
  File "/usr/share/panda3d/direct/interval/FunctionInterval.py", line 308, in __init__
    FunctionInterval.__init__(self, function, **kw)
  File "/usr/share/panda3d/direct/interval/FunctionInterval.py", line 62, in __init__
    name = self.makeUniqueName(function)
  File "/usr/share/panda3d/direct/interval/FunctionInterval.py", line 80, in makeUniqueName
    name = 'Func-%s-%d' % (func.__name__, FunctionInterval.functionIntervalNum)
AttributeError: 'functools.partial' object has no attribute '__name__'

So I added a __name__ argument to my functools.partial:

            funcs[key] = functools.partial(self.command, key)
            funcs[key].__name__ = str(key)
            bubu[key] = DirectButton(
                text = (key, "Join", "Join", "Full"),
                #command = lambda: self.commandAndDestroy( lambda: self.command(key) ),
                #extraArgs = [ functools.partial(self.command, key) ],
                command = self.commandAndDestroy,
                extraArgs = [ funcs[key] ],
                scale = scale,
                text_font = font,
                text_fg = (.1875,.15625,.125,1),
                text_shadow = (.5,.46484375,.40625,1),
                text_align = TextNode.ALeft,
                rolloverSound = hover_snd,
                clickSound = clicked_snd,
                pressEffect = 0,
                parent = self.frame
            )

And it fixed my strange bug.
The ideal would be to not rely on this __name__ argument to fix functools compatibility, and find why lambda caused this strange bug.

Revision history for this message
David Rose (droklaunchpad) wrote :

Unlike some other languages, Python's lambda syntax doesn't perform a full closure on its parameters automatically. This means that when you use a syntax like "lambda: func(key)", the variable reference "key" does not retain the value of the variable at the time you bound it, but rather looks up the variable "key" from the *calling* namespace. This is almost never what you intended. To force Python to bind in a variable reference, you pass it in as a default parameter, like this: "lambda key = key: func(key)". This is a bit of a hairy kludge, but it's the Python way.

You can also use use Panda's Functor class to do this for you. "from direct.showbase.PythonUtil import Functor", then pass "Functor(func, key)", instead of a lambda object, to the command parameter.

I've never seen functools.partial before, but it apparently is meant to do the same thing as our Functor class. Unfortunately, it doesn't define a __name__ member for some reason (unlike almost every other kind of Python object), so it causes a crash in the FunctionInterval code which assumes the function object you pass does have a __name__ member. I've just committed a workaround to FunctionInterval to allow it to work without this.

Changed in panda3d:
status: New → Fix Committed
assignee: nobody → David Rose (droklaunchpad)
rdb (rdb)
Changed in panda3d:
status: Fix Committed → Fix Released
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.