OK I figured out the reason it's leaking: the binding automatically created inside the kv language (by the function call_fn) keeps the objects alive by including it in the idmap dictionary in its closure. Here's a simplified example to show what's going on.
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty
from kivy.uix.button import Button
from kivy.uix.popup import Popup
from kivy.uix.widget import Widget
from objgraph import find_backref_chain, by_type
from pprint import PrettyPrinter
pp = PrettyPrinter().pprint
Builder.load_string('''
<XPop>:
title: root.xxx.s
''')
class XPop(Popup):
xxx = ObjectProperty(None)
class X(Widget):
s = StringProperty('hello world')
@property
def popup(self):
pp([find_backref_chain(p, lambda y: y is self) for p in by_type('XPop')])
return XPop(xxx=self)
class XApp(App):
def build(self):
xx = X()
b = Button(text='pop')
b.bind(on_release=lambda *args: xx.popup.open())
return b
if __name__ == '__main__':
XApp().run()
If you keep clicking to open the popup, you will see that none of the previous popups were garbage collected. The print output shows the backref chain leading back to the xx object, and essentially it works like xx -> s -> observers of s -> (includes) call_fn -> (closure includes) idmap -> (contains) the popup.
Now if there's a way of accessing this call_fn I can try unbinding it, but is there?
This is particularly troublesome, since I have data models with kivy properties upon which many facets depend upon using kivy bindings through kv.