views:

397

answers:

2

Here's some code from Richard Jones' Blog:

with gui.vertical:
    text = gui.label('hello!')
    items = gui.selection(['one', 'two', 'three'])
    with gui.button('click me!'):
        def on_click():
            text.value = items.value
            text.foreground = red

My question is: how the heck did he do this? How can the context manager access the scope inside the with block? Here's a basic template for trying to figure this out:

from __future__ import with_statement

class button(object):
  def __enter__(self):
    #do some setup
    pass

  def __exit__(self, exc_type, exc_value, traceback):
    #XXX: how can we find the testing() function?
    pass

with button():
  def testing():
    pass
+10  A: 

Here's one way:

from __future__ import with_statement
import inspect

class button(object):
  def __enter__(self):
    # keep track of all that's already defined BEFORE the `with`
    f = inspect.currentframe(1)
    self.mustignore = dict(f.f_locals)

  def __exit__(self, exc_type, exc_value, traceback):
    f = inspect.currentframe(1)
    # see what's been bound anew in the body of the `with`
    interesting = dict()
    for n in f.f_locals:
      newf = f.f_locals[n]
      if n not in self.mustignore:
        interesting[n] = newf
        continue
      anf = self.mustignore[n]
      if id(newf) != id(anf):
        interesting[n] = newf
    if interesting:
      print 'interesting new things: %s' % ', '.join(sorted(interesting))
      for n, v in interesting.items():
        if isinstance(v, type(lambda:None)):
          print 'function %r' % n
          print v()
    else:
      print 'nothing interesting'

def main():
  for i in (1, 2):
    def ignorebefore():
      pass
    with button():
      def testing(i=i):
        return i
    def ignoreafter():
      pass

main()

Edit: stretched code a bit more, added some explanation...:

Catching caller's locals at __exit__ is easy -- trickier is avoiding those locals that were already defined before the with block, which is why I added to main two local functions that the with should ignore. I'm not 100% happy with this solution, which looks a bit complicated, but I couldn't get equality testing correct with either == or is, so I resorted to this rather complicated approach.

I've also added a loop (to make more strongly sure the defs before / within / after are being properly handled) and a type-check and function-call to make sure the right incarnation of testing is the one that's identified (everything seems to work fine) -- of course the code as written only works if the def inside the with is for a function callable without arguments, it's not hard to get the signature with inspect to ward against that (but since I'm doing the call only for the purpose of checking that the right function objects are identified, I didn't bother about this last refinement;-).

Alex Martelli
Lovely, thank you very much.
llimllib
You're welcome! was a fun problem to tackle, so tx for posing it;-).
Alex Martelli
I posted a blog entry on using the code you gave me, in case you're interested: http://billmill.org/multi_line_lambdas.html
llimllib
Neat indeed, thanks!
Alex Martelli
A: 

To answer your question, yes, it's frame introspection.

But the syntax I would create to do the same thing is

with gui.vertical:
    text = gui.label('hello!')
    items = gui.selection(['one', 'two', 'three'])
    @gui.button('click me!')
    class button:
        def on_click():
            text.value = items.value
            text.foreground = red

Here I would implement gui.button as a decorator that returns button instance given some parameters and events (though it appears to me now that button = gui.button('click me!', mybutton_onclick is fine as well).

I would also leave gui.vertical as it is since it can be implemented without introspection. I'm not sure about its implementation, but it may involve setting gui.direction = gui.VERTICAL so that gui.label() and others use it in computing their coordinates.

Now when I look at this, I think I'd try the syntax:

    with gui.vertical:
        text = gui.label('hello!')
        items = gui.selection(['one', 'two', 'three'])

        @gui.button('click me!')
        def button():
            text.value = items.value
            foreground = red

(the idea being that similarly to how label is made out of text, a button is made out of text and function)

ilya n.
but then why use "with gui.vertical"? It would need to do the same stack introspection to get access to the text, items, and button within it. I'm sure you do something like: class MyLayout(gui.Vertical): text = gui.label('hello!') #etcright?Anyway, I'm well aware that this is a seriously non-standard abuse of the with block. I just wanted to know how he did it.I hope you at least see that it's a cool abuse of the with block :)
llimllib
I read `with gui.vertical` as "create no elements, but make sure all elements created in this context compute their coordinates vertically from the current point". No introspection.
ilya n.