# vim:sw=4
"""
Demonstrate problem with PyGObject and the Python GC.

The problem can be described as:
- we have a GObject 'win' and a GObject 'l', and their Python wrappers.
- 'win' and 'l' are connected to each other on a GObject level
  (such as gtk_container_add() for example).
- Now, we create a cyclic reference which includes 'l' (the GC is used to
  unwrap cyclic references). In this example this is done with the simple
  Python instance 'a' of class A.
- Now we can remove our references to 'l' and 'a'. Since 'l' references 'a'
  and visa versa, there is no need to need to add an extra reference on 'l'..
- The garbage collector examines our objects and sees 'l' and 'a' being
  connected to each other and cyclic. No relation is found with class 'win'..
- 'l', 'a' and their dictionaries (l.__dict__) are set for garbage collection.
  Their 'tp_clear' functions are invoked:
  - 'l' clears its connected signals
  - 'a' has nothing to do
  - the dictionaries of both 'a' and 'l' are cleared (causing 'a' to be
    destroyed)

Case 1 (l refs a, a refs l):
                     +------------+  (our cycle)
Python:              V            |
  ,-----.         ,-----.      ,-----.
  | win |         |  l  |----->|  a  |
  '-----'         '-----'      '-----'
-----|---------------|------------------
GTK: V               V
  ,-----.         ,-----.
  | win |-------->|  l  | (GtkLabel)
  '-----'         '-----'

Case 2 (l refs itself):
                     +------+  (our cycle)
Python:              V      |
  ,-----.         ,-----.   |
  | win |         |  l  |---+
  '-----'         '-----'
-----|---------------|------------------
GTK: V               V
  ,-----.         ,-----.
  | win |-------->|  l  | (GtkLabel)
  '-----'         '-----'

Case 3 (l is ref'ed by the GObject and itself, this is not a problem):
                     +------+  (our cycle)
Python:              V      |
  ,-----.         ,-----.   |
  | win |         |  l  |---+
  '-----'         '-----'
-----|---------------^------------------
GTK: V               |
  ,-----.         ,-----.
  | win |-------->|  l  | (GtkLabel)
  '-----'         '-----'

Possible solutions:
1. Create a special dict class that does not clear if the GObject has more than
   one reference.
2. Change the tp_traverse function
3. Change the tp_clear function (implies 1.)
"""
import pygtk
pygtk.require('2.0')

import gtk, weakref, gc, sys

class A:
    pass

# Case 1:
print 'Case 1'

win = gtk.Window()

a = A()
l = gtk.Label('Hello')
assert sys.getrefcount(l) == 2
assert l.__grefcount__ == 1
win.add(l)
assert sys.getrefcount(l) == 2
assert l.__grefcount__ == 2

# Make the reference cyclic:
l.a = a
a.l = l
assert sys.getrefcount(a) == 3
assert sys.getrefcount(l) == 3
assert l.__grefcount__ == 2

weak_l = weakref.ref(l)
weak_a = weakref.ref(a)

assert len(l.__dict__.values()) > 0
del l, a

assert sys.getrefcount(weak_a()) == 2
assert sys.getrefcount(weak_l()) == 2
assert weak_l().__grefcount__ == 2

gc.collect()

assert weak_a() == None
# Gobject.tp_dealloc saves our 'l',
assert weak_l() != None
assert sys.getrefcount(weak_l()) == 2
assert weak_l().__grefcount__ == 1
# ...the __dict__ is cleared though. (shouldn't happen)
assert len(weak_l().__dict__.values()) == 0


# Case 2:
print 'Case 2'

win = gtk.Window()
l = gtk.Label('Hello')
win.add(l)

# create a cyclic reference:
l.l = l

assert sys.getrefcount(l) == 3
assert l.__grefcount__ == 2

weak_l = weakref.ref(l)
assert len(l.__dict__.values()) > 0
del l

gc.collect()

# Gobject.tp_dealloc saves our 'l',
assert weak_l() != None
assert sys.getrefcount(weak_l()) == 2
assert weak_l().__grefcount__ == 1
# ...the __dict__ is cleared though. (shouldn't happen!)
assert len(weak_l().__dict__.values()) == 0


# Case 3:
print 'Case 3'

win = gtk.Window()
l = gtk.Label('Hello')
win.add(l)

assert sys.getrefcount(l) == 2
assert l.__grefcount__ == 2

weak_l = weakref.ref(l)
assert len(l.__dict__.values()) == 0
del l

assert sys.getrefcount(weak_l()) == 2
assert weak_l().__grefcount__ == 1

# create a cyclic reference:
weak_l().l = weak_l()

gc.collect()

# Gobject.tp_dealloc saves our 'l',
assert weak_l() != None
assert sys.getrefcount(weak_l()) == 3
assert weak_l().__grefcount__ == 1
# ...the __dict__ is not cleared.
assert len(weak_l().__dict__.values()) > 0

print 'done'
