curds: Improvements to the notifications system

- Clean up the code
- Kick off a GLib idle handler when events are added
- If we are already running in the main thread, then we don't need to
  use the idle handler.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2019-12-04 10:39:41 -05:00
parent 7433154494
commit 6db713a993
18 changed files with 127 additions and 128 deletions

View File

@ -19,7 +19,7 @@ Track = tags.track.Track
def reset():
notify.queued.clear()
notify.clear()
tags.map.clear()
playlist.reset()

View File

@ -1,36 +1,46 @@
# Copyright 2019 (c) Anna Schumaker.
import threading
from gi.repository import GLib
registered = { }
queued = [ ]
lock = threading.Lock()
events = { }
main_queue = [ ]
event_lock = threading.Lock()
def cancel(name, func):
cb = events.get(name, [])
for event in cb:
if event[0] == func:
cb.remove(event)
if len(cb) == 0:
events.pop(name)
def clear():
events.clear()
main_queue.clear()
def notify(name, *args):
for (func, queue) in registered.get(name, []):
if queue == True:
with lock:
if (func, args) not in queued:
queued.append((func, args))
notify("task-queued")
is_main = threading.current_thread() == threading.main_thread()
for (func, main) in events.get(name, []):
if main == True and is_main == False:
with event_lock:
if (func, args) not in main_queue:
main_queue.append((func, args))
if len(main_queue) == 1:
GLib.idle_add(notify_idle)
else:
func(*args)
def cancel(name, func, queue=False):
cb = registered.get(name, [])
if (func, queue) in cb:
cb.remove((func, queue))
if len(cb) == 0:
registered.pop(name)
def notify_idle():
with event_lock:
if len(main_queue) == 0:
return False
(func, args) = main_queue.pop(0)
more = len(main_queue) > 0
func(*args)
return more
def register(name, func, queue=False):
cb = registered.setdefault(name, [])
def register(name, func, main=False):
cb = events.setdefault(name, [])
if func in [ n[0] for n in cb ]:
return
cb.append((func, queue))
def run_queued():
with lock:
if len(queued) > 0:
(func, args) = queued.pop(0)
func(*args)
return len(queued) > 0
cb.append((func, main))

View File

@ -4,10 +4,12 @@ from . import playlist
from . import root
from .. import data
from .. import notify
import threading
Library = library.LibraryPlaylist
Node = node.PlaylistNode
Root = None
Lock = threading.Lock()
def current():
@ -17,8 +19,9 @@ def lookup(name):
return Root.lookup(name)
def save():
with data.DataFile("playlists.pickle", data.WRITE) as f:
f.pickle([ Root ])
with Lock:
with data.DataFile("playlists.pickle", data.WRITE) as f:
f.pickle([ Root ])
def load():
global Root
@ -39,4 +42,5 @@ def init():
global UpNext
Starred = Root.lookup("Playlists").lookup("Starred")
UpNext = Root.lookup("Up Next")
notify.register("save-playlists", save, queue=True)
notify.register("save-playlists", save, main=True)
notify.register("save-data", save)

View File

@ -22,6 +22,7 @@ class LibraryPlaylist(playlist.Playlist):
def thread_save(self):
tags.map.save()
notify.notify("save-data")
def thread_scan(self):
for dirname, subdirs, files in os.walk(self.name):

View File

@ -11,7 +11,7 @@ test_album = os.path.join("./trier/Test Library/Test Artist 01/Test Album 1")
class TestArtistPlaylist(unittest.TestCase):
def setUp(self):
notify.registered.clear()
notify.clear()
tags.map.clear()
def test_artist_node(self):
@ -46,7 +46,7 @@ class TestArtistPlaylist(unittest.TestCase):
anode.reset()
self.assertEqual(len(anode.children), 0)
self.assertIn((anode.new_track, False), notify.registered["new-track"])
self.assertIn((anode.new_track, False), notify.events["new-track"])
def test_artist_playlist_alloc(self):
anode = artist.ArtistPlaylist("Test Playlist", artist.ARTIST_ICON)

View File

@ -38,7 +38,7 @@ class TestCollectionPlaylist(unittest.TestCase):
self.assertEqual(len(plist), 0)
self.assertFalse(plist.random)
self.assertTrue(plist.loop)
self.assertIn((plist.add, False), notify.registered["new-track"])
self.assertIn((plist.add, False), notify.events["new-track"])
def test_collection_loop(self):
plist = collection.CollectionPlaylist()

View File

@ -14,7 +14,7 @@ test_album3 = os.path.join("Test Album 4", "02 - Test Track 02.ogg")
class TestDecadePlaylist(unittest.TestCase):
def setUp(self):
notify.registered.clear()
notify.clear()
tags.map.clear()
def test_decade(self):
@ -55,7 +55,7 @@ class TestDecadePlaylist(unittest.TestCase):
dnode.reset()
self.assertEqual(len(dnode.children), 0)
self.assertIn((dnode.new_track, False), notify.registered["new-track"])
self.assertIn((dnode.new_track, False), notify.events["new-track"])
def test_decade_playlist_alloc(self):
dnode = decade.DecadePlaylist("Test Decade", decade.DECADE_ICON)

View File

@ -11,7 +11,7 @@ test_library = os.path.abspath("./trier/Test Library/Test Artist 01")
class TestGenrePlaylist(unittest.TestCase):
def setUp(self):
notify.registered.clear()
notify.clear()
tags.map.clear()
def test_genre_node(self):
@ -69,4 +69,4 @@ class TestGenrePlaylist(unittest.TestCase):
gnode.reset()
self.assertEqual(gnode.n_children(), 0)
self.assertIn((gnode.new_track, False), notify.registered["new-track"])
self.assertIn((gnode.new_track, False), notify.events["new-track"])

View File

@ -17,7 +17,7 @@ class TestLibraryPlaylist(unittest.TestCase):
library.reset()
tags.map.clear()
tags.map.remove()
notify.registered.clear()
notify.clear()
def tearDownClass():
library.stop()

View File

@ -1,5 +1,6 @@
# Copyright 2019 (c) Anna Schumaker.
from .. import data
from .. import notify
import os
import threading
@ -38,5 +39,6 @@ def save():
with data.DataFile(tag_file, data.WRITE) as f:
f.pickle(tag_map)
notify.register("save-data", save)
if os.environ.get("EMMENTAL_TESTING"): remove()

View File

@ -1,19 +1,26 @@
# Copyright 2019 (c) Anna Schumaker.
from . import notify
import gi
import threading
import unittest
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class TestNotify(unittest.TestCase):
def setUp(self):
notify.registered.clear()
notify.queued.clear()
notify.clear()
self.test_count = 0
self.test_arg1 = None
self.test_arg2 = None
self.task_queued = False
def tearDown(self):
notify.queued.clear()
notify.registered.clear()
notify.clear()
def notify_thread(self, event, *args):
thread = threading.Thread(target=notify.notify, args=(event, *args))
thread.start()
thread.join()
def on_test1(self, arg1, arg2): self.test_count += 1
def on_test2(self, arg1, arg2): self.test_arg1 = arg1
@ -22,69 +29,65 @@ class TestNotify(unittest.TestCase):
def on_task_queued(self): self.task_queued = True
def on_test_queue(self, arg1): self.test_count += 1; self.test_arg1 = arg1
def test_notify(self):
self.assertEqual(notify.registered, {})
self.assertEqual(notify.queued, [])
def test_notify_init(self):
self.assertEqual(notify.events, {})
self.assertEqual(notify.main_queue, [])
self.assertFalse(notify.event_lock.locked())
def test_notify_register(self):
notify.register("on-test", self.on_test1)
self.assertEqual(notify.registered, {"on-test" : [ (self.on_test1, False) ]})
self.assertEqual(notify.events, {"on-test" : [ (self.on_test1, False) ]})
notify.register("on-test", self.on_test1, True)
notify.register("on-test", self.on_test2)
notify.register("on-test", self.on_test3, True)
self.assertEqual(notify.registered, {"on-test" : [ (self.on_test1, False),
(self.on_test2, False),
(self.on_test3, True ) ]})
self.assertEqual(notify.events, {"on-test" : [ (self.on_test1, False),
(self.on_test2, False),
(self.on_test3, True ) ]})
def test_notify_clear(self):
notify.events = { "abc" : [ (self.on_test1, False) ]}
notify.main_q = [ 1, 2, 3 ]
notify.clear()
self.assertEqual(notify.events, {})
self.assertEqual(notify.main_queue, [])
def test_notify_cancel(self):
notify.events = { "on-test" : [ (self.on_test1, False),
(self.on_test2, False),
(self.on_test3, True) ]}
notify.cancel("on-test", self.on_test2)
self.assertEqual(notify.events, { "on-test" : [ (self.on_test1, False),
(self.on_test3, True) ]})
notify.cancel("on-test", self.on_test1)
self.assertEqual(notify.events, { "on-test" : [ (self.on_test3, True) ] })
notify.cancel("on-test", self.on_test3)
self.assertEqual(notify.events, { })
def test_notify_main_thread(self):
notify.register("on-test", self.on_test1)
notify.register("on-test", self.on_test2)
notify.register("on-test", self.on_test3, True)
notify.notify("on-test", "It Worked", "CoolCoolCool")
self.assertEqual(self.test_count, 1)
self.assertEqual(self.test_arg1, "It Worked")
self.assertEqual(self.test_arg2, None)
self.assertEqual(notify.queued, [ (self.on_test3, ("It Worked", "CoolCoolCool")) ])
self.assertFalse(notify.run_queued())
self.assertEqual(self.test_arg2, "CoolCoolCool")
self.assertEqual(notify.queued, [ ])
notify.notify("no-cb", "Please", "Don't", "Crash")
self.assertFalse(notify.run_queued())
def test_notify_bg_thread(self):
notify.register("on-test", self.on_test1, True)
notify.register("on-test", self.on_test2)
notify.cancel("on-test", self.on_test2)
self.assertEqual(notify.registered, {"on-test" : [ (self.on_test1, False),
(self.on_test3, True) ]})
notify.cancel("on-test", self.on_test1)
self.assertEqual(notify.registered, {"on-test" : [ (self.on_test3, True) ]})
notify.cancel("on-test", self.on_test3, True)
self.assertEqual(notify.registered, { })
notify.cancel("on-test", self.on_test1)
self.assertEqual(notify.registered, { })
self.notify_thread("on-test", "It Worked", "CoolCoolCool")
self.assertEqual(self.test_count, 0)
self.assertEqual(self.test_arg1, "It Worked")
self.assertEqual(notify.main_queue, [ (self.on_test1, ("It Worked", "CoolCoolCool")) ])
def test_queue(self):
notify.register("task-queued", self.on_task_queued)
notify.register("on-test", self.on_test_queue, True)
self.notify_thread("on-test", "It Worked", "CoolCoolCool")
self.assertEqual(self.test_count, 0)
self.assertEqual(notify.main_queue, [ (self.on_test1, ("It Worked", "CoolCoolCool")) ])
self.assertEqual(notify.registered, {"on-test" : [ (self.on_test_queue, True) ],
"task-queued" : [ (self.on_task_queued, False) ]})
notify.notify("on-test", "Test Bundle")
self.assertTrue(self.task_queued)
self.assertEqual(notify.queued, [ (self.on_test_queue, ("Test Bundle",)) ])
self.task_queued = False
notify.notify("on-test", "Test Bundle")
self.assertFalse(self.task_queued)
self.assertEqual(notify.queued, [ (self.on_test_queue, ("Test Bundle",)) ])
notify.notify("on-test", "Test Bundle 2")
self.assertTrue(self.task_queued)
self.assertEqual(notify.queued, [ (self.on_test_queue, ("Test Bundle",)),
(self.on_test_queue, ("Test Bundle 2",)) ])
self.assertTrue(notify.run_queued())
self.assertEqual(self.test_count, 1)
self.assertEqual(self.test_arg1, "Test Bundle")
self.assertEqual(notify.queued, [ (self.on_test_queue, ("Test Bundle 2",)) ])
self.assertFalse(notify.run_queued())
self.assertEqual(self.test_count, 2)
self.assertEqual(self.test_arg1, "Test Bundle 2")
self.assertEqual(notify.queued, [ ])
while Gtk.events_pending(): Gtk.main_iteration_do(True)
self.assertEqual(self.test_count, 1)
self.assertEqual(notify.main_queue, [ ])
self.assertFalse(notify.notify_idle())

View File

@ -11,7 +11,7 @@ class TestPlaylist(unittest.TestCase):
self.orig = playlist.Root
def tearDown(self):
notify.registered.clear()
notify.clear()
playlist.node.nodes.clear()
playlist.Root = self.orig
playlist.Root.reset()
@ -52,7 +52,7 @@ class TestPlaylist(unittest.TestCase):
self.assertFalse(dfile.exists())
playlist.Starred.changed()
notify.run_queued()
notify.notify_idle()
self.assertTrue(dfile.exists())
playlist.load()

View File

@ -110,7 +110,7 @@ Entry.connect("activate", on_activate_entry)
def on_show_more(visible):
Box.set_visible(visible)
curds.notify.register("show-more", on_show_more)
curds.notify.register("show-more", on_show_more, main=True)
def state_changed(state):

View File

@ -18,7 +18,6 @@ class EmmentalApplication(Gtk.Application):
super().__init__(*args, application_id="org.gtk.emmental", **kwargs)
self.window = None
self.idle_id = None
curds.notify.register("task-queued", self.task_queued)
def do_activate(self):
if self.window == None:
@ -36,14 +35,6 @@ class EmmentalApplication(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
def on_idle(self):
if curds.notify.run_queued() == False:
self.idle_id = None
return GLib.SOURCE_CONTINUE if self.idle_id else GLib.SOURCE_REMOVE
def task_queued(self):
if self.idle_id == None:
self.idle_id = GLib.idle_add(self.on_idle)
def can_activate_accel(widget, signal):
widget.stop_emission_by_name("can-activate-accel")
@ -63,10 +54,7 @@ def main_loop(delay=0.0, iteration_delay=0.0):
Gtk.main_iteration_do(True)
def notify_loop(delay=0.0, iteration_delay=0.0):
time.sleep(delay)
while curds.notify.run_queued():
time.sleep(iteration_delay)
main_loop()
main_loop(delay, iteration_delay)
def timeout_loop(timeout, iteration_delay=0.0):
timeout = time.time() + timeout

View File

@ -114,5 +114,5 @@ class NodeTreeModel(GObject.GObject, Gtk.TreeModel):
return iter
def reset(self):
curds.notify.register("node-inserted", self.on_node_inserted)
curds.notify.register("playlist-changed", self.on_playlist_changed, queue=True)
curds.notify.register("node-inserted", self.on_node_inserted, main=True)
curds.notify.register("playlist-changed", self.on_playlist_changed, main=True)

View File

@ -80,14 +80,14 @@ def on_node_inserted(plist, index):
path = Filter.convert_child_path_to_path(child)
TreeView.expand_to_path(path)
curds.notify.register("node-inserted", on_node_inserted)
curds.notify.register("node-inserted", on_node_inserted, main=True)
def on_show_more(visible):
Entry.set_visible(visible)
Separator.set_visible(visible)
curds.notify.register("show-more", on_show_more)
curds.notify.register("show-more", on_show_more, main=True)
def reset():
@ -99,5 +99,5 @@ def reset():
Filter.clear_cache()
on_show_more(False)
TreeView.set_model(Filter)
curds.notify.register("show-more", on_show_more)
curds.notify.register("node-inserted", on_node_inserted)
curds.notify.register("show-more", on_show_more, main=True)
curds.notify.register("node-inserted", on_node_inserted, main=True)

View File

@ -122,7 +122,7 @@ def on_add_track(plist, track, index):
Model.row_inserted(Gtk.TreePath(NextPos), iter)
NextPos += 1
curds.notify.register("add-track", on_add_track, queue=True)
curds.notify.register("add-track", on_add_track, main=True)
def on_remove_track(plist, track, index):
@ -145,7 +145,7 @@ def on_playlist_changed(plist):
if plist == Model.playlist:
set_runtime(plist.runtime())
curds.notify.register("playlist-changed", on_playlist_changed, queue=True)
curds.notify.register("playlist-changed", on_playlist_changed, main=True)
def scroll_to(index):
@ -194,8 +194,8 @@ switch(curds.playlist.lookup("Collection"))
def reset():
switch(curds.playlist.lookup("Collection"))
curds.notify.register("add-track", on_add_track, queue=True)
curds.notify.register("add-track", on_add_track, main=True)
curds.notify.register("remove-track", on_remove_track)
curds.notify.register("playlist-changed", on_playlist_changed, queue=True)
curds.notify.register("playlist-changed", on_playlist_changed, main=True)
curds.notify.register("stream-start", on_stream_start)
curds.notify.register("show-more", on_show_more)

View File

@ -28,15 +28,6 @@ class TestGtk(unittest.TestCase):
gtk.main_loop(delay=0.1)
self.assertTrue(window.is_visible())
self.assertIsNone(app.idle_id)
app.task_queued()
self.assertGreater(app.idle_id, 0)
idle_id = app.idle_id
app.task_queued()
self.assertEqual(app.idle_id, idle_id)
gtk.main_loop(delay=0.1)
self.assertIsNone(app.idle_id)
app.idle_id = GLib.idle_add(self.fake_idle)
app.quit()
gtk.main_loop(delay=0.1)