Compare commits

...

10 Commits

Author SHA1 Message Date
Anna Schumaker 06771ecab6 Emmental 3.0.4
Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-23 15:51:03 -04:00
Anna Schumaker 17b2a82e20 db: Have Playlists use the child_set as the children base model
I combine this with the table's Filter object to show playlists matching
the current search query.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-23 11:37:47 -04:00
Anna Schumaker c0c516fb70 db: Have decade Playlists use the new child_set
I implement add_year(), remove_year(), and has_year() functions and make
sure we load the set of yearids during startup. Additionally, I have
Years add and remove themselves from Decades as they are created and
deleted.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-23 11:37:47 -04:00
Anna Schumaker 3cddde0986 db: Have Album playlists use the new child_set
I implement add_medium(), remove_medium(), and has_medium() functions
and make sure we load the set of mediumids during startup. Additionally,
I have Mediums add and remove themselves from Albums as they are created
and deleted.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-23 11:37:47 -04:00
Anna Schumaker 4f15bde850 db: Have Artist playlists use the new child_set
I switch over to adding and removing Albums using the generic
add_child() and remove_child() functions. I also switch from using a
KeySet filter holding albumids to a Gtk.CustomFilter that calls
Artist.has_album() to check if an Album is in the set.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-23 11:37:47 -04:00
Anna Schumaker 5ee86a9b5e db: Give the Media table a custom Filter
We want to filter out Medium playlists with empty names in the sidebar,
and the easiest way to do that is through a custom filter attached to
the Media table.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-23 11:37:47 -04:00
Anna Schumaker 85c18fb5fe db: Add a child_set TableSubset to the Playlist
It defaults to a None pointer, but calling add_children() will set one
up with an empty set. I also implement functions for adding, removing,
and verifying child playlists.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-22 11:27:40 -04:00
Anna Schumaker 67b508384c db: Create a TableSubset model
This is similar to a Gtk.FilterListModel, except we already know exactly
which rows are part of the model or not. So we can skip the entire
filtering step and show rows directly instead.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-22 11:07:32 -04:00
Anna Schumaker 929beb2a97 db: Add set features to the KeySet
This includes implementing the __contains__() magic method, and adding
signals that are emitted when rows are added, removed, or directly set.
This will allow us to build a model around the rows represented by the
set.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-19 13:42:28 -04:00
Anna Schumaker f400366210 db: Rename the Table.Filter to Table.KeySet
I'm going to expand on this and use it for more than just filtering
Gtk.FilterListModels. Renaming it to something more generic is the first
step.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
2023-06-19 09:55:11 -04:00
17 changed files with 662 additions and 186 deletions

View File

@ -21,7 +21,7 @@ from gi.repository import Adw
MAJOR_VERSION = 3
MINOR_VERSION = 0
MICRO_VERSION = 3
MICRO_VERSION = 4
VERSION_NUMBER = f"{MAJOR_VERSION}.{MINOR_VERSION}.{MICRO_VERSION}"
VERSION_STRING = f"Emmental {VERSION_NUMBER}{gsetup.DEBUG_STR}"

View File

@ -3,7 +3,6 @@
import pathlib
import sqlite3
from gi.repository import GObject
from gi.repository import Gtk
from .media import Medium
from .. import format
from . import playlist
@ -23,10 +22,11 @@ class Album(playlist.Playlist):
"""Initialize an Album object."""
super().__init__(**kwargs)
self.add_children(self.table.sql.media,
Gtk.CustomFilter.new(self.__match_medium))
self.table.get_mediumids(self))
def __match_medium(self, medium: Medium) -> bool:
return medium.albumid == self.albumid and len(medium.name) > 0
def add_medium(self, medium: Medium) -> None:
"""Add a Medium to this Album."""
self.add_child(medium)
def get_artists(self) -> list[playlist.Playlist]:
"""Get a list of artists for this album."""
@ -36,6 +36,14 @@ class Album(playlist.Playlist):
"""Get a list of media for this album."""
return self.table.get_media(self)
def has_medium(self, medium: Medium) -> bool:
"""Check if a Medium is from this Album."""
return self.has_child(medium)
def remove_medium(self, medium: Medium) -> None:
"""Remove a Medium from this Album."""
return self.remove_child(medium)
@property
def primary_key(self) -> int:
"""Get the Album primary key."""
@ -139,6 +147,11 @@ class Table(playlist.Table):
def get_media(self, album: Album) -> list[Medium]:
"""Get the list of media for this album."""
return [self.sql.media.rows.get(id)
for id in self.get_mediumids(album)]
def get_mediumids(self, album: Album) -> set[int]:
"""Get the set of mediumids for this album."""
rows = self.sql("SELECT mediumid FROM media WHERE albumid=?",
album.albumid)
return [self.sql.media.rows.get(row["mediumid"]) for row in rows]
return {row["mediumid"] for row in rows.fetchall()}

View File

@ -19,21 +19,21 @@ class Artist(playlist.Playlist):
"""Initialize an Artist object."""
super().__init__(**kwargs)
self.add_children(self.table.sql.albums,
table.Filter(self.table.get_albumids(self)))
self.table.get_albumids(self))
def add_album(self, album: Album) -> None:
"""Add an Album to this Artist."""
if self.table.add_album(self, album):
self.children.get_filter().add_row(album)
self.add_child(album)
def has_album(self, album: Album) -> bool:
"""Check if the Artist has this Album."""
return self.children.get_filter().match(album)
return self.has_child(album)
def remove_album(self, album: Album) -> None:
"""Remove an album from this Artist."""
self.children.get_filter().remove_row(album)
self.table.remove_album(self, album)
self.remove_child(album)
@property
def primary_key(self) -> int:
@ -41,7 +41,7 @@ class Artist(playlist.Playlist):
return self.artistid
class Filter(table.Filter):
class Filter(table.KeySet):
"""Custom filter to hide artists without albums."""
show_all = GObject.Property(type=bool, default=False)
@ -51,7 +51,7 @@ class Filter(table.Filter):
super().__init__(show_all=show_all)
self.connect("notify::show-all", self.__notify_show_all)
def __notify_show_all(self, filter: table.Filter, param) -> None:
def __notify_show_all(self, filter: table.KeySet, param) -> None:
self.changed(Gtk.FilterChange.LESS_STRICT if self.show_all else
Gtk.FilterChange.MORE_STRICT)
@ -66,7 +66,7 @@ class Filter(table.Filter):
"""Check if the artist matches the filter."""
res = super().do_match(artist)
if not self.show_all and res:
return artist.children.get_filter().n_keys > 0
return artist.child_set.keyset.n_keys > 0
return res

View File

@ -2,7 +2,6 @@
"""A custom Gio.ListModel for working with decades."""
import sqlite3
from gi.repository import GObject
from gi.repository import Gtk
from .years import Year
from . import playlist
from . import tracks
@ -17,15 +16,24 @@ class Decade(playlist.Playlist):
"""Initialize a Decade object."""
super().__init__(**kwargs)
self.add_children(self.table.sql.years,
Gtk.CustomFilter.new(self.__match_year))
self.table.get_yearids(self))
def __match_year(self, year: Year) -> bool:
return self.decade == year.year // 10 * 10
def add_year(self, year: Year) -> None:
"""Add a year to this decade."""
self.add_child(year)
def get_years(self) -> list[Year]:
"""Get a list of years for this decade."""
return self.table.get_years(self)
def has_year(self, year: Year) -> bool:
"""Check if the year is in this decade."""
return self.has_child(year)
def remove_year(self, year: Year) -> None:
"""Remove a year from this decade."""
self.remove_child(year)
@property
def primary_key(self) -> int:
"""Get the primary key of this Decade."""
@ -90,8 +98,12 @@ class Table(playlist.Table):
return self.sql("""SELECT trackid FROM decade_tracks_view
WHERE decade=?""", decade.decade)
def get_years(self, decade: Decade) -> list[Year]:
"""Get the list of years for this decade."""
def get_yearids(self, decade: Decade) -> set[int]:
"""Get the set of years for this decade."""
rows = self.sql("SELECT year FROM years WHERE (year / 10 * 10)=?",
decade.decade)
return [self.sql.years.rows.get(row["year"]) for row in rows]
return {row["year"] for row in rows}
def get_years(self, decade: Decade) -> list[Year]:
"""Get the list of years for this decade."""
return [self.sql.years.rows.get(yr) for yr in self.get_yearids(decade)]

View File

@ -2,8 +2,10 @@
"""A custom Gio.ListModel for managing individual media in an album."""
import sqlite3
from gi.repository import GObject
from gi.repository import Gtk
from .. import format
from . import playlist
from . import table
from . import tracks
@ -34,12 +36,26 @@ class Medium(playlist.Playlist):
return self.get_album()
class Filter(table.KeySet):
"""Custom filter to hide media with empty names."""
def do_get_strictness(self) -> Gtk.FilterMatch:
"""Get the strictness of the filter."""
if (res := super().do_get_strictness()) == Gtk.FilterMatch.ALL:
res = Gtk.FilterMatch.SOME
return res
def do_match(self, medium: Medium) -> bool:
"""Check if the Medium matches the filter."""
return len(medium.name) > 0 if super().do_match(medium) else False
class Table(playlist.Table):
"""Our Media Table."""
def __init__(self, sql: GObject.TYPE_PYOBJECT, **kwargs):
"""Initialize the Media Table."""
super().__init__(sql=sql, autodelete=True,
super().__init__(sql=sql, filter=Filter(), autodelete=True,
system_tracks=False, **kwargs)
def do_construct(self, **kwargs) -> Medium:
@ -61,6 +77,7 @@ class Table(playlist.Table):
def do_sql_delete(self, medium: Medium) -> sqlite3.Cursor:
"""Delete a medium."""
medium.get_album().remove_medium(medium)
return self.sql("DELETE FROM media WHERE mediumid=?",
medium.mediumid)
@ -100,6 +117,13 @@ class Table(playlist.Table):
return self.sql(f"UPDATE media SET {column}=? WHERE mediumid=?",
newval, medium.mediumid)
def create(self, album: playlist.Playlist,
*args, **kwargs) -> Medium | None:
"""Create a new Medium playlist."""
if (medium := super().create(album, *args, **kwargs)) is not None:
album.add_medium(medium)
return medium
def rename(self, medium: Medium, new_name: str) -> bool:
"""Rename a medium."""
if (new_name := new_name.strip()) != medium.name:

View File

@ -1,6 +1,7 @@
# Copyright 2022 (c) Anna Schumaker
"""A customized Gio.ListStore for tracking Playlist GObjects."""
import sqlite3
import typing
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import Gtk
@ -28,6 +29,7 @@ class Playlist(table.Row):
tracks_movable = GObject.Property(type=bool, default=False)
current_trackid = GObject.Property(type=int)
child_set = GObject.Property(type=table.TableSubset)
children = GObject.Property(type=Gtk.FilterListModel)
def __init__(self, table: Gio.ListModel, propertyid: int,
@ -48,20 +50,26 @@ class Playlist(table.Row):
self.table.remove_track(self, track)
return True
def add_children(self, child_table: table.Table,
child_filter: Gtk.Filter) -> None:
def add_children(self, child_table: table.Table, child_keys: set) -> None:
"""Create a FilterListModel for this playlist's children."""
self.children = Gtk.FilterListModel.new(child_table, child_filter)
self.child_set = table.TableSubset(child_table, keys=child_keys)
self.children = Gtk.FilterListModel.new(self.child_set,
child_table.get_filter())
self.children.set_incremental(True)
def do_update(self, column: str) -> bool:
"""Update a Playlist object."""
match column:
case "propertyid" | "name" | "n-tracks" | "children" | \
"user-tracks" | "tracks-loaded" | "tracks-movable": pass
case "propertyid" | "name" | "n-tracks" | "child-set" | \
"children" | "user-tracks" | "tracks-loaded" | \
"tracks-movable": pass
case _: return super().do_update(column)
return True
def add_child(self, child: typing.Self) -> None:
"""Add a child Playlist to this Playlist."""
self.child_set.add_row(child)
def add_track(self, track: Track, *, idle: bool = False) -> None:
"""Add a Track to this Playlist."""
if self.table.add_track(self, track):
@ -71,6 +79,10 @@ class Playlist(table.Row):
"""Get a dictionary mapping for trackid -> sorted position."""
return self.table.get_track_order(self)
def has_child(self, child: typing.Self) -> bool:
"""Check if this Playlist has a specific child Playlist."""
return child in self.child_set
def has_track(self, track: Track) -> bool:
"""Check if a Track is on this Playlist."""
return track in self.tracks
@ -95,6 +107,10 @@ class Playlist(table.Row):
self.tracks_loaded = False
self.table.queue.push(self.load_tracks, now=not idle)
def remove_child(self, child: typing.Self) -> None:
"""Remove a child Playlist from this Playlist."""
self.child_set.remove_row(child)
def remove_track(self, track: table.Row, *, idle: bool = False) -> None:
"""Remove a Track from this Playlist."""
self.table.queue.push(self.__remove_track, track, now=not idle)

View File

@ -1,5 +1,6 @@
# Copyright 2022 (c) Anna Schumaker
"""Base classes for database objects."""
import bisect
import sqlite3
from gi.repository import GObject
from gi.repository import Gio
@ -37,44 +38,52 @@ class Row(GObject.GObject):
raise NotImplementedError
class Filter(Gtk.Filter):
"""A Filter that can be used to search playlists."""
class KeySet(Gtk.Filter):
"""A Gtk.Filter that also acts as a Python Set."""
n_keys = GObject.Property(type=int)
def __init__(self, keys: set | None = None, **kwargs):
"""Set up our Filter."""
"""Set up our KeySet."""
super().__init__(**kwargs)
self._keys = keys
self.n_keys = len(keys) if keys is not None else -1
def __contains__(self, row: Row) -> bool:
"""Check if a Row is in the KeySet."""
return self._keys is None or row.primary_key in self._keys
def __sub__(self, rhs: Gtk.Filter) -> set[int]:
"""Subtract two Filters and return the result."""
"""Subtract two KeySets and return the result."""
match (self._keys, rhs._keys):
case (None, _): return None
case (_, None): return self._keys
case (_, _): return self._keys - rhs._keys
def __find_change(self, keys: set[any] | None) -> Gtk.FilterChange | None:
if keys == self._keys:
return None
elif keys is None:
return Gtk.FilterChange.LESS_STRICT
elif self._keys is None:
return Gtk.FilterChange.MORE_STRICT
elif keys.issuperset(self._keys):
return Gtk.FilterChange.LESS_STRICT
elif keys.issubset(self._keys):
return Gtk.FilterChange.MORE_STRICT
return Gtk.FilterChange.DIFFERENT
def __find_difference(self, new: set[any] | None) \
-> tuple[set, set, Gtk.FilterChange | None]:
if self._keys is None:
if new is None:
return (set(), set(), None)
return (set(), new, Gtk.FilterChange.MORE_STRICT)
elif new is None:
return (self._keys, set(), Gtk.FilterChange.LESS_STRICT)
removed = self._keys - new
added = new - self._keys
match len(removed), len(added):
case 0, 0: return (removed, added, None)
case _, 0: return (removed, added, Gtk.FilterChange.MORE_STRICT)
case 0, _: return (removed, added, Gtk.FilterChange.LESS_STRICT)
case _, _: return (removed, added, Gtk.FilterChange.DIFFERENT)
def changed(self, how: Gtk.FilterChange) -> None:
"""Notify that the filter has changed."""
"""Notify that the KeySet has changed."""
self.n_keys = len(self._keys) if self._keys is not None else -1
super().changed(how)
def do_get_strictness(self) -> Gtk.FilterMatch:
"""Get the strictness of the filter."""
"""Get the strictness of the Gtk.Filter."""
if self._keys is None:
return Gtk.FilterMatch.ALL
if len(self._keys) == 0:
@ -82,19 +91,21 @@ class Filter(Gtk.Filter):
return Gtk.FilterMatch.SOME
def do_match(self, row: Row) -> bool:
"""Check if the Row matches the filter."""
"""Check if the Row is in the KeySet."""
return self._keys is None or row.primary_key in self._keys
def add_row(self, row: Row) -> None:
"""Add a Row to the Filter."""
if self._keys is not None:
"""Add a Row to the KeySet."""
if row not in self:
self._keys.add(row.primary_key)
self.emit("key-added", row.primary_key)
self.changed(Gtk.FilterChange.LESS_STRICT)
def remove_row(self, row: Row) -> None:
"""Remove a Row from the Filter."""
if self._keys is not None:
"""Remove a Row from the KeySet."""
if self._keys is not None and row in self:
self._keys.discard(row.primary_key)
self.emit("key-removed", row.primary_key)
self.changed(Gtk.FilterChange.MORE_STRICT)
@property
@ -105,9 +116,23 @@ class Filter(Gtk.Filter):
@keys.setter
def keys(self, keys: set[any] | None) -> None:
"""Set the matching primary keys."""
if (how := self.__find_change(keys)) is not None:
(removed, added, change) = self.__find_difference(keys)
if change is not None:
self._keys = keys
self.changed(how)
self.emit("keys-changed", removed, added)
self.changed(change)
@GObject.Signal(arg_types=(int,))
def key_added(self, key: int) -> None:
"""Signal that a Row has been added to the KeySet."""
@GObject.Signal(arg_types=(int,))
def key_removed(self, key: int) -> None:
"""Signal that a Row has been removed from the KeySet."""
@GObject.Signal(arg_types=(GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT))
def keys_changed(self, removed: set | None, added: set | None) -> None:
"""Signal that the KeySet has been directly modified."""
class Table(Gtk.FilterListModel):
@ -121,12 +146,12 @@ class Table(Gtk.FilterListModel):
loaded = GObject.Property(type=bool, default=False)
def __init__(self, sql: GObject.TYPE_PYOBJECT,
filter: Filter | None = None,
filter: KeySet | None = None,
queue: Queue | None = None, **kwargs):
"""Set up our Table object."""
super().__init__(sql=sql, rows=dict(), incremental=True,
store=store.SortedList(self.get_sort_key),
filter=(filter if filter else Filter()),
filter=(filter if filter else KeySet()),
queue=(queue if queue else Queue()), **kwargs)
self.set_model(self.store)
@ -247,3 +272,75 @@ class Table(Gtk.FilterListModel):
def update(self, row: Row, column: str, newval) -> bool:
"""Update a Row."""
return self.do_sql_update(row, column, newval) is not None
class TableSubset(GObject.GObject, Gio.ListModel):
"""A list model containing a subset of the rows in the source Table."""
keyset = GObject.Property(type=KeySet)
table = GObject.Property(type=Table)
n_rows = GObject.Property(type=int)
def __init__(self, table: Table, *, keys: set[any] | None = None):
"""Initialize a KeySetModel."""
super().__init__(keyset=KeySet(set() if keys is None else keys),
table=table)
self._items = []
self.keyset.connect("key-added", self.__on_key_added)
self.keyset.connect("key-removed", self.__on_key_removed)
self.table.connect("notify::loaded", self.__notify_table_loaded)
def __contains__(self, row: Row) -> bool:
"""Check if the Row is in the internal KeySet."""
return row in self.keyset
def __bisect(self, key: any) -> int | None:
if self.table.loaded:
sort_key = self.table.get_sort_key(self.table.rows[key])
return bisect.bisect_left(self._items, sort_key,
key=self.table.get_sort_key)
return None
def __items_changed(self, position: int, removed: int, added: int) -> None:
self.n_rows = len(self._items)
self.items_changed(position, removed, added)
def __notify_table_loaded(self, table: Table, param) -> None:
if table.loaded and self.keyset.n_keys > 0:
self._items = sorted([table.rows[k] for k in self.keyset.keys],
key=self.table.get_sort_key)
self.__items_changed(0, 0, self.keyset.n_keys)
elif not table.loaded and self.n_rows > 0:
self._items = []
self.__items_changed(0, self.n_rows, 0)
def __on_key_added(self, keyset: KeySet, key: any) -> None:
if (pos := self.__bisect(key)) is not None:
self._items.insert(pos, self.table.rows[key])
self.__items_changed(pos, 0, 1)
def __on_key_removed(self, keyset: KeySet, key: any) -> None:
if (pos := self.__bisect(key)) is not None:
del self._items[pos]
self.__items_changed(pos, 1, 0)
def do_get_item_type(self) -> GObject.GType:
"""Get the Gio.ListModel item type."""
return Row.__gtype__
def do_get_n_items(self) -> int:
"""Get the number of Rows in the TableSubset."""
return self.n_rows
def do_get_item(self, n: int) -> int:
"""Get the nth item in the TableSubset."""
return self._items[n] if n < len(self._items) else None
def add_row(self, row: Row) -> None:
"""Add a row to the TableSubset."""
self.keyset.add_row(row)
def remove_row(self, row: Row) -> None:
"""Remove a row from the TableSubset."""
self.keyset.remove_row(row)

View File

@ -90,7 +90,7 @@ class Track(table.Row):
return self.trackid
class Filter(table.Filter):
class Filter(table.KeySet):
"""A customized Filter that never sets strictness to FilterMatch.All."""
def do_get_strictness(self) -> Gtk.FilterMatch:

View File

@ -48,6 +48,8 @@ class Table(playlist.Table):
def do_sql_delete(self, year: Year) -> sqlite3.Cursor:
"""Delete a year."""
if year.parent is not None:
year.parent.remove_year(year)
return self.sql("DELETE FROM years WHERE year=?", year.year)
def do_sql_glob(self, glob: str) -> sqlite3.Cursor:
@ -71,3 +73,10 @@ class Table(playlist.Table):
"""Load a Year's Tracks from the database."""
return self.sql("""SELECT trackid FROM year_tracks_view
WHERE year=?""", year.year)
def create(self, *args, **kwargs) -> Year | None:
"""Create a new Year playlist."""
if (year := super().create(*args, **kwargs)) is not None:
if year.parent is not None:
year.parent.add_year(year)
return year

View File

@ -41,6 +41,22 @@ class TestAlbumObject(tests.util.TestCase):
self.assertEqual(album2.mbid, "ab-cd-ef")
self.assertEqual(album2.cover, cover)
def test_add_remove_medium(self):
"""Test adding and removing a medium from the Album."""
album = self.table.create("Test Album", "Album Artist", "2023-03")
medium = self.sql.media.create(album, "Test Medium", number=1)
self.assertFalse(medium in self.album.child_set)
self.assertFalse(self.album.has_medium(medium))
self.album.add_medium(medium)
self.assertTrue(medium in self.album.child_set)
self.assertTrue(self.album.has_medium(medium))
self.album.remove_medium(medium)
self.assertFalse(medium in self.album.child_set)
self.assertFalse(self.album.has_medium(medium))
def test_get_artists(self):
"""Test getting the list of artists for this album."""
with unittest.mock.patch.object(self.table, "get_artists",
@ -56,22 +72,14 @@ class TestAlbumObject(tests.util.TestCase):
self.assertListEqual(self.album.get_media(), [1, 2, 3])
mock.assert_called_with(self.album)
def test_media_model(self):
"""Test getting a Gio.ListModel representing this Album's media."""
def test_children(self):
"""Test the Album's 'children' model is set up properly."""
self.assertIsInstance(self.album.child_set,
emmental.db.table.TableSubset)
self.assertIsInstance(self.album.children, Gtk.FilterListModel)
self.assertIsInstance(self.album.children.get_filter(),
Gtk.CustomFilter)
self.assertEqual(self.album.children.get_model(), self.sql.media)
album = self.table.create("Test Album", "Album Artist", "2023-03")
medium = self.sql.media.create(album, "Test Medium", number=1)
self.assertTrue(album.children.get_filter().match(medium))
medium.albumid = album.albumid + 1
self.assertFalse(album.children.get_filter().match(medium))
medium = self.sql.media.create(album, "", number=2)
self.assertFalse(album.children.get_filter().match(medium))
self.assertEqual(self.album.children.get_filter(),
self.sql.media.get_filter())
self.assertEqual(self.album.child_set.table, self.sql.media)
class TestAlbumTable(tests.util.TestCase):
@ -228,9 +236,11 @@ class TestAlbumTable(tests.util.TestCase):
def test_load(self):
"""Test loading the album table."""
self.table.create("Album 1", "Album Artist", "2023-03")
album = self.table.create("Album 1", "Album Artist", "2023-03")
self.table.create("Album 2", "Album Artist", "2023-03",
mbid="ab-cd-ef", cover=tests.util.COVER_JPG)
medium = self.sql.media.create(album, "Test Medium", number=1)
album.add_medium(medium)
albums2 = emmental.db.albums.Table(self.sql)
self.assertEqual(len(albums2), 0)
@ -243,6 +253,7 @@ class TestAlbumTable(tests.util.TestCase):
self.assertEqual(albums2.get_item(0).release, "2023-03")
self.assertEqual(albums2.get_item(0).mbid, "")
self.assertIsNone(albums2.get_item(0).cover)
self.assertSetEqual(albums2.get_item(0).child_set.keyset.keys, {1})
self.assertEqual(albums2.get_item(1).name, "Album 2")
self.assertEqual(albums2.get_item(1).artist, "Album Artist")
@ -250,6 +261,7 @@ class TestAlbumTable(tests.util.TestCase):
self.assertEqual(albums2.get_item(1).mbid, "ab-cd-ef")
self.assertEqual(albums2.get_item(1).cover,
tests.util.COVER_JPG)
self.assertSetEqual(albums2.get_item(1).child_set.keyset.keys, set())
def test_lookup(self):
"""Test looking up album playlists."""
@ -320,4 +332,6 @@ class TestAlbumTable(tests.util.TestCase):
medium1 = self.sql.media.create(album, "", number=1)
medium2 = self.sql.media.create(album, "", number=2)
self.assertSetEqual(self.table.get_mediumids(album),
{medium1.mediumid, medium2.mediumid})
self.assertListEqual(self.table.get_media(album), [medium1, medium2])

View File

@ -20,7 +20,6 @@ class TestArtistObject(tests.util.TestCase):
def test_init(self):
"""Test that the Artist is set up properly."""
self.assertIsInstance(self.artist, emmental.db.playlist.Playlist)
self.assertSetEqual(self.artist.children.get_filter().keys, set())
self.assertEqual(self.artist.table, self.table)
self.assertEqual(self.artist.propertyid, 456)
self.assertEqual(self.artist.artistid, 123)
@ -37,8 +36,7 @@ class TestArtistObject(tests.util.TestCase):
self.artist.add_album(album)
mock_add.assert_called_with(self.artist, album)
self.assertSetEqual(self.artist.children.get_filter().keys,
{album.albumid})
self.assertIn(album, self.artist.child_set)
self.assertTrue(self.artist.has_album(album))
with unittest.mock.patch.object(self.table, "remove_album",
@ -46,15 +44,17 @@ class TestArtistObject(tests.util.TestCase):
self.artist.remove_album(album)
mock_remove.assert_called_with(self.artist, album)
self.assertSetEqual(self.artist.children.get_filter().keys, set())
self.assertNotIn(album, self.artist.child_set)
self.assertFalse(self.artist.has_album(album))
def test_children(self):
"""Test that Albums have been added as Artist playlist children."""
self.assertIsInstance(self.artist.child_set,
emmental.db.table.TableSubset)
self.assertIsInstance(self.artist.children, Gtk.FilterListModel)
self.assertIsInstance(self.artist.children.get_filter(),
emmental.db.table.Filter)
self.assertEqual(self.artist.children.get_model(), self.sql.albums)
self.assertEqual(self.artist.children.get_filter(),
self.sql.albums.get_filter())
self.assertEqual(self.artist.child_set.table, self.sql.albums)
class TestFilter(tests.util.TestCase):
@ -68,7 +68,7 @@ class TestFilter(tests.util.TestCase):
def test_init(self):
"""Test that the filter is initialized properly."""
self.assertIsInstance(self.filter, emmental.db.table.Filter)
self.assertIsInstance(self.filter, emmental.db.table.KeySet)
self.assertFalse(self.filter.show_all)
filter2 = emmental.db.artists.Filter(show_all=True)
@ -219,13 +219,11 @@ class TestArtistTable(tests.util.TestCase):
self.assertEqual(artists2.get_item(0).name, "Artist 1")
self.assertEqual(artists2.get_item(0).mbid, "")
self.assertSetEqual(artists2.get_item(0).children.get_filter().keys,
{1})
self.assertSetEqual(artists2.get_item(0).child_set.keyset.keys, {1})
self.assertEqual(artists2.get_item(1).name, "Artist 2")
self.assertEqual(artists2.get_item(1).mbid, "ab-cd-ef")
self.assertSetEqual(artists2.get_item(1).children.get_filter().keys,
set())
self.assertSetEqual(artists2.get_item(1).child_set.keyset.keys, set())
def test_lookup(self):
"""Test looking up artist playlists."""

View File

@ -28,6 +28,21 @@ class TestDecadeObject(tests.util.TestCase):
self.assertEqual(self.decade.name, "The 2020s")
self.assertIsNone(self.decade.parent)
def test_add_remove_year(self):
"""Test adding and removing a year from the decade."""
year = self.sql.years.create(1988)
self.assertFalse(year in self.decade.child_set)
self.assertFalse(self.decade.has_year(year))
self.decade.add_year(year)
self.assertTrue(year in self.decade.child_set)
self.assertTrue(self.decade.has_year(year))
self.decade.remove_year(year)
self.assertFalse(year in self.decade.child_set)
self.assertFalse(self.decade.has_year(year))
def test_get_years(self):
"""Test getting the list of years for this decade."""
with unittest.mock.patch.object(self.table, "get_years",
@ -37,16 +52,12 @@ class TestDecadeObject(tests.util.TestCase):
def test_years_model(self):
"""Test getting a Gio.ListModel representing a Decade's years."""
self.assertIsInstance(self.decade.child_set,
emmental.db.table.TableSubset)
self.assertIsInstance(self.decade.children, Gtk.FilterListModel)
self.assertIsInstance(self.decade.children.get_filter(),
Gtk.CustomFilter)
self.assertEqual(self.decade.children.get_model(), self.sql.years)
year = self.sql.years.create(2023)
self.assertTrue(self.decade.children.get_filter().match(year))
year = self.sql.years.create(1988)
self.assertFalse(self.decade.children.get_filter().match(year))
self.assertEqual(self.decade.children.get_filter(),
self.sql.years.get_filter())
self.assertEqual(self.decade.child_set.table, self.sql.years)
class TestDecadeTable(tests.util.TestCase):
@ -164,8 +175,10 @@ class TestDecadeTable(tests.util.TestCase):
def test_load(self):
"""Load the decade table from the database."""
self.table.create(1980)
decade = self.table.create(1980)
self.table.create(1990)
year = self.sql.years.create(1988)
decade.add_year(year)
decades2 = emmental.db.decades.Table(self.sql)
self.assertEqual(len(decades2), 0)
@ -175,9 +188,11 @@ class TestDecadeTable(tests.util.TestCase):
self.assertEqual(decades2.get_item(0).decade, 1980)
self.assertEqual(decades2.get_item(0).name, "The 1980s")
self.assertSetEqual(decades2.get_item(0).child_set.keyset.keys, {1988})
self.assertEqual(decades2.get_item(1).decade, 1990)
self.assertEqual(decades2.get_item(1).name, "The 1990s")
self.assertSetEqual(decades2.get_item(1).child_set.keyset.keys, set())
def test_lookup(self):
"""Test looking up decade playlists."""
@ -214,4 +229,5 @@ class TestDecadeTable(tests.util.TestCase):
y1985 = self.sql.years.create(1985)
y1988 = self.sql.years.create(1988)
self.assertSetEqual(self.table.get_yearids(decade), {1985, 1988})
self.assertListEqual(self.table.get_years(decade), [y1985, y1988])

View File

@ -4,6 +4,7 @@ import pathlib
import unittest.mock
import emmental.db
import tests.util
from gi.repository import Gtk
class TestMediumObject(tests.util.TestCase):
@ -46,6 +47,36 @@ class TestMediumObject(tests.util.TestCase):
mock_rename.assert_called_with(self.medium, "New Name")
class TestFilter(tests.util.TestCase):
"""Test the medium filter."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.filter = emmental.db.media.Filter()
def test_init(self):
"""Test that the filter is initialized properly."""
self.assertIsInstance(self.filter, emmental.db.table.KeySet)
def test_strictness(self):
"""Test checking strictness."""
self.filter.keys = None
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
self.filter.keys = set()
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.NONE)
self.filter.keys = {1, 2, 3}
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
def test_match(self):
"""Test matching a Medium."""
album = self.sql.albums.create("Test Album", "Test Artist", "123")
medium = self.sql.media.create(album, "", number=1)
self.assertFalse(self.filter.match(medium))
medium.name = "abcde"
self.assertTrue(self.filter.match(medium))
class TestMediumsTable(tests.util.TestCase):
"""Tests our mediums table."""
@ -61,6 +92,8 @@ class TestMediumsTable(tests.util.TestCase):
def test_init(self):
"""Test that the medium model is configured corretly."""
self.assertIsInstance(self.table, emmental.db.playlist.Table)
self.assertIsInstance(self.table.get_filter(),
emmental.db.media.Filter)
self.assertEqual(len(self.table), 0)
self.assertTrue(self.table.autodelete)
self.assertFalse(self.table.system_tracks)
@ -99,6 +132,7 @@ class TestMediumsTable(tests.util.TestCase):
self.assertEqual(medium1.number, 1)
self.assertEqual(medium1.type, "")
self.assertEqual(medium1.sort_order, "mediumno, number")
self.assertTrue(self.album.has_medium(medium1))
cur = self.sql("SELECT COUNT(name) FROM media")
self.assertEqual(cur.fetchone()["COUNT(name)"], 1)
@ -123,6 +157,7 @@ class TestMediumsTable(tests.util.TestCase):
medium = self.table.create(self.album, "Medium 1", number=1)
self.assertTrue(medium.delete())
self.assertIsNone(self.table.index(medium))
self.assertFalse(self.album.has_medium(medium))
cur = self.sql("SELECT COUNT(name) FROM media")
self.assertEqual(cur.fetchone()["COUNT(name)"], 0)
@ -172,17 +207,19 @@ class TestMediumsTable(tests.util.TestCase):
self.assertEqual(len(mediums2), 0)
mediums2.load(now=True)
self.assertEqual(len(mediums2), 2)
self.assertEqual(len(mediums2.store), 2)
self.assertEqual(mediums2.get_item(0).albumid, self.album.albumid)
self.assertEqual(mediums2.get_item(0).name, "")
self.assertEqual(mediums2.get_item(0).number, 1)
self.assertEqual(mediums2.get_item(0).type, "")
self.assertEqual(mediums2.store.get_item(0).albumid,
self.album.albumid)
self.assertEqual(mediums2.store.get_item(0).name, "")
self.assertEqual(mediums2.store.get_item(0).number, 1)
self.assertEqual(mediums2.store.get_item(0).type, "")
self.assertEqual(mediums2.get_item(1).albumid, self.album.albumid)
self.assertEqual(mediums2.get_item(1).name, "Medium 2")
self.assertEqual(mediums2.get_item(1).number, 2)
self.assertEqual(mediums2.get_item(1).type, "Digital Media")
self.assertEqual(mediums2.store.get_item(1).albumid,
self.album.albumid)
self.assertEqual(mediums2.store.get_item(1).name, "Medium 2")
self.assertEqual(mediums2.store.get_item(1).number, 2)
self.assertEqual(mediums2.store.get_item(1).type, "Digital Media")
def test_lookup(self):
"""Test looking up medium playlists."""

View File

@ -65,15 +65,29 @@ class TestPlaylistRow(unittest.TestCase):
def test_children(self):
"""Test the child playlist properties."""
self.assertIsNone(self.playlist.child_set)
self.assertIsNone(self.playlist.children)
filter = Gtk.Filter()
self.playlist.add_children(self.table, filter)
table = emmental.db.table.Table(None)
self.playlist.add_children(table, set())
self.assertIsInstance(self.playlist.child_set,
emmental.db.table.TableSubset)
self.assertEqual(self.playlist.child_set.table, table)
self.assertSetEqual(self.playlist.child_set.keyset.keys, set())
self.assertIsInstance(self.playlist.children, Gtk.FilterListModel)
self.assertEqual(self.playlist.children.get_filter(), filter)
self.assertEqual(self.playlist.children.get_model(), self.table)
self.assertEqual(self.playlist.children.get_filter(),
table.get_filter())
self.assertEqual(self.playlist.children.get_model(),
self.playlist.child_set)
self.assertTrue(self.playlist.children.get_incremental())
playlist2 = emmental.db.playlist.Playlist(table=self.table,
propertyid=2, name="Plist2")
playlist2.add_children(table, {1, 2, 3})
self.assertSetEqual(playlist2.child_set.keyset.keys, {1, 2, 3})
def test_parent(self):
"""Test the parent playlist property."""
self.assertIsNone(self.playlist.parent)
@ -97,6 +111,15 @@ class TestPlaylistRow(unittest.TestCase):
self.table.update.assert_called_with(self.playlist,
prop, value)
def test_add_child(self):
"""Test adding a child playlist to the playlist."""
table = emmental.db.table.Table(None)
child = tests.util.table.MockRow(table=table, number=1)
self.playlist.add_children(table, set())
self.playlist.add_child(child)
self.assertIn(child, self.playlist.child_set)
def test_add_track(self):
"""Test adding a track to the playlist."""
self.playlist.add_track(self.track, idle=True)
@ -122,6 +145,18 @@ class TestPlaylistRow(unittest.TestCase):
{1: 3, 2: 2, 3: 1})
self.table.get_track_order.assert_called_with(self.playlist)
def test_has_child(self):
"""Test the playlist has_child() function."""
table = emmental.db.table.Table(None)
child = tests.util.table.MockRow(table=table, number=1)
self.playlist.add_children(table, set())
self.assertFalse(self.playlist.has_child(child))
self.playlist.add_child(child)
self.assertTrue(self.playlist.has_child(child))
self.playlist.remove_child(child)
self.assertFalse(self.playlist.has_child(child))
def test_has_track(self):
"""Test the playlist has_track() function."""
self.assertFalse(self.playlist.has_track(self.track))
@ -141,6 +176,16 @@ class TestPlaylistRow(unittest.TestCase):
self.assertTrue(self.playlist.move_track_up(self.track))
self.table.move_track_up.assert_called_with(self.playlist, self.track)
def test_remove_child(self):
"""Test removing a child playlist from the playlist."""
table = emmental.db.table.Table(None)
child = tests.util.table.MockRow(table=table, number=1)
self.playlist.add_children(table, set())
self.playlist.add_child(child)
self.playlist.remove_child(child)
self.assertFalse(child in self.playlist.child_set)
def test_remove_track(self):
"""Test removing a track from the playlist."""
self.playlist.tracks.trackids.add(self.track.trackid)

View File

@ -50,119 +50,166 @@ class TestRow(unittest.TestCase):
@unittest.mock.patch("gi.repository.Gtk.Filter.changed")
class TestFilter(unittest.TestCase):
"""Tests our database row Filter."""
class TestKeySet(unittest.TestCase):
"""Tests our KeySet for holding database Rows."""
def setUp(self):
"""Set up common variables."""
self.filter = emmental.db.table.Filter()
self.keyset = emmental.db.table.KeySet()
self.table = Gio.ListStore()
self.row1 = tests.util.table.MockRow(number=1, table=self.table)
self.row2 = tests.util.table.MockRow(number=2, table=self.table)
def test_init(self, mock_changed: unittest.mock.Mock):
"""Test that the filter is created correctly."""
self.assertIsInstance(self.filter, Gtk.Filter)
self.assertIsNone(self.filter._keys, None)
self.assertEqual(self.filter.n_keys, -1)
"""Test that the KeySet is created correctly."""
self.assertIsInstance(self.keyset, Gtk.Filter)
self.assertIsNone(self.keyset._keys, None)
self.assertEqual(self.keyset.n_keys, -1)
filter2 = emmental.db.table.Filter(keys={1, 2, 3})
self.assertSetEqual(filter2._keys, {1, 2, 3})
self.assertEqual(filter2.n_keys, 3)
keyset2 = emmental.db.table.KeySet(keys={1, 2, 3})
self.assertSetEqual(keyset2._keys, {1, 2, 3})
self.assertEqual(keyset2.n_keys, 3)
def test_subtract(self, mock_changed: unittest.mock.Mock):
"""Test subtracting two filters."""
filter2 = emmental.db.table.Filter(keys={2, 3})
self.assertIsNone(self.filter - self.filter)
self.assertIsNone(self.filter - filter2)
self.assertSetEqual(filter2 - self.filter, {2, 3})
"""Test subtracting two KeySets."""
keyset2 = emmental.db.table.KeySet(keys={2, 3})
self.assertIsNone(self.keyset - self.keyset)
self.assertIsNone(self.keyset - keyset2)
self.assertSetEqual(keyset2 - self.keyset, {2, 3})
self.filter.keys = {1, 2, 3, 4, 5}
self.assertSetEqual(self.filter - filter2, {1, 4, 5})
self.assertSetEqual(filter2 - self.filter, set())
self.keyset.keys = {1, 2, 3, 4, 5}
self.assertSetEqual(self.keyset - keyset2, {1, 4, 5})
self.assertSetEqual(keyset2 - self.keyset, set())
def test_strictness(self, mock_changed: unittest.mock.Mock):
"""Test checking strictness."""
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.ALL)
self.filter._keys = set()
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.NONE)
self.filter._keys = {1, 2, 3}
self.assertEqual(self.filter.get_strictness(), Gtk.FilterMatch.SOME)
self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.ALL)
self.keyset._keys = set()
self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.NONE)
self.keyset._keys = {1, 2, 3}
self.assertEqual(self.keyset.get_strictness(), Gtk.FilterMatch.SOME)
def test_add_row(self, mock_changed: unittest.mock.Mock):
"""Test adding Rows to the filter."""
self.filter.add_row(self.row1)
self.assertIsNone(self.filter.keys)
"""Test adding Rows to the KeySet."""
mock_added = unittest.mock.Mock()
self.keyset.connect("key-added", mock_added)
self.filter.keys = set()
self.filter.add_row(self.row1)
self.assertSetEqual(self.filter.keys, {1})
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.filter.n_keys, 1)
self.keyset.add_row(self.row1)
self.assertIsNone(self.keyset.keys)
mock_added.assert_not_called()
self.filter.add_row(self.row2)
self.assertSetEqual(self.filter.keys, {1, 2})
self.keyset.keys = set()
self.keyset.add_row(self.row1)
self.assertSetEqual(self.keyset.keys, {1})
self.assertEqual(self.keyset.n_keys, 1)
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.filter.n_keys, 2)
mock_added.assert_called_with(self.keyset, 1)
self.keyset.add_row(self.row2)
self.assertSetEqual(self.keyset.keys, {1, 2})
self.assertEqual(self.keyset.n_keys, 2)
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
mock_added.assert_called_with(self.keyset, 2)
mock_changed.reset_mock()
mock_added.reset_mock()
self.keyset.add_row(self.row2)
self.assertSetEqual(self.keyset.keys, {1, 2})
mock_changed.assert_not_called()
mock_added.assert_not_called()
def test_remove_row(self, mock_changed: unittest.mock.Mock):
"""Test removing Rows from the filter."""
self.filter.remove_row(self.row1)
mock_changed.assert_not_called()
"""Test removing Rows from the KeySet."""
mock_removed = unittest.mock.Mock()
self.keyset.connect("key-removed", mock_removed)
self.filter.keys = {1, 2}
self.filter.remove_row(self.row1)
self.assertSetEqual(self.filter._keys, {2})
self.keyset.remove_row(self.row1)
mock_changed.assert_not_called()
mock_removed.assert_not_called()
self.keyset.keys = {1, 2}
self.keyset.remove_row(self.row1)
self.assertSetEqual(self.keyset._keys, {2})
self.assertEqual(self.keyset.n_keys, 1)
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 1)
mock_removed.assert_called_with(self.keyset, 1)
mock_changed.reset_mock()
self.filter.remove_row(self.row2)
self.assertSetEqual(self.filter._keys, set())
mock_removed.reset_mock()
self.keyset.remove_row(self.row1)
self.assertSetEqual(self.keyset.keys, {2})
self.assertEqual(self.keyset.n_keys, 1)
mock_changed.assert_not_called()
mock_removed.assert_not_called()
self.keyset.remove_row(self.row2)
self.assertSetEqual(self.keyset._keys, set())
self.assertEqual(self.keyset.n_keys, 0)
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 0)
mock_removed.assert_called_with(self.keyset, 2)
def test_keys(self, mock_changed: unittest.mock.Mock):
"""Test setting and getting the filter keys property."""
self.assertIsNone(self.filter.keys)
"""Test getting and setting the KeySet.keys property."""
mock_keys_changed = unittest.mock.Mock()
self.keyset.connect("keys-changed", mock_keys_changed)
self.filter.keys = {1, 2, 3}
self.assertSetEqual(self.filter._keys, {1, 2, 3})
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 3)
mock_changed.reset_mock()
self.filter.keys = {1, 2}
self.assertSetEqual(self.filter.keys, {1, 2})
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
self.assertEqual(self.filter.n_keys, 2)
mock_changed.reset_mock()
self.filter.keys = {1, 2}
self.assertIsNone(self.keyset.keys)
self.keyset.keys = None
self.assertIsNone(self.keyset.keys)
mock_changed.assert_not_called()
mock_keys_changed.assert_not_called()
self.filter.keys = {1, 2, 3}
self.assertSetEqual(self.filter.keys, {1, 2, 3})
self.keyset.keys = {1, 2, 3}
self.assertSetEqual(self.keyset._keys, {1, 2, 3})
self.assertEqual(self.keyset.n_keys, 3)
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
mock_keys_changed.assert_called_with(self.keyset, set(), {1, 2, 3})
mock_changed.reset_mock()
self.keyset.keys = {1, 2}
self.assertSetEqual(self.keyset.keys, {1, 2})
self.assertEqual(self.keyset.n_keys, 2)
mock_changed.assert_called_with(Gtk.FilterChange.MORE_STRICT)
mock_keys_changed.assert_called_with(self.keyset, {3}, set())
mock_changed.reset_mock()
mock_keys_changed.reset_mock()
self.keyset.keys = {1, 2}
mock_changed.assert_not_called()
mock_keys_changed.assert_not_called()
self.keyset.keys = {1, 2, 3}
self.assertSetEqual(self.keyset.keys, {1, 2, 3})
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
mock_keys_changed.assert_called_with(self.keyset, set(), {3})
self.filter.keys = {4, 5, 6}
self.assertSetEqual(self.filter._keys, {4, 5, 6})
self.keyset.keys = {4, 5, 6}
self.assertSetEqual(self.keyset._keys, {4, 5, 6})
mock_changed.assert_called_with(Gtk.FilterChange.DIFFERENT)
mock_keys_changed.assert_called_with(self.keyset, {1, 2, 3}, {4, 5, 6})
self.filter.keys = None
self.assertIsNone(self.filter._keys)
self.keyset.keys = None
self.assertIsNone(self.keyset._keys)
self.assertEqual(self.keyset.n_keys, -1)
mock_changed.assert_called_with(Gtk.FilterChange.LESS_STRICT)
self.assertEqual(self.filter.n_keys, -1)
mock_keys_changed.assert_called_with(self.keyset, {4, 5, 6}, set())
def test_match(self, mock_changed: unittest.mock.Mock):
"""Test matching playlists."""
self.assertTrue(self.filter.match(self.row1))
self.filter.keys = {1, 2, 3}
self.assertTrue(self.filter.match(self.row1))
self.filter.keys = {4, 5, 6}
self.assertFalse(self.filter.match(self.row1))
self.filter.keys = set()
self.assertFalse(self.filter.match(self.row1))
def test_match_contains(self, mock_changed: unittest.mock.Mock):
"""Test matching Rows and the __contains__() magic method."""
self.assertTrue(self.keyset.match(self.row1))
self.assertTrue(self.row1 in self.keyset)
self.keyset.keys = {1, 2, 3}
self.assertTrue(self.keyset.match(self.row1))
self.assertTrue(self.row1 in self.keyset)
self.keyset.keys = {4, 5, 6}
self.assertFalse(self.keyset.match(self.row1))
self.assertFalse(self.row1 in self.keyset)
self.keyset.keys = set()
self.assertFalse(self.keyset.match(self.row1))
self.assertFalse(self.row1 in self.keyset)
class TestTable(tests.util.TestCase):
@ -178,7 +225,7 @@ class TestTable(tests.util.TestCase):
self.assertIsInstance(self.table, Gtk.FilterListModel)
self.assertIsInstance(self.table.queue, emmental.db.idle.Queue)
self.assertIsInstance(self.table.get_filter(),
emmental.db.table.Filter)
emmental.db.table.KeySet)
self.assertIsInstance(self.table.store, emmental.store.SortedList)
self.assertIsInstance(self.table.rows, dict)
@ -188,7 +235,7 @@ class TestTable(tests.util.TestCase):
self.assertDictEqual(self.table.rows, {})
self.assertTrue(self.table.get_incremental())
filter2 = emmental.db.table.Filter()
filter2 = emmental.db.table.KeySet()
queue2 = emmental.db.idle.Queue()
table2 = emmental.db.table.Table(self.sql, filter=filter2,
queue=queue2)
@ -393,3 +440,147 @@ class TestTableFunctions(tests.util.TestCase):
self.table.create(number=3)
self.assertFalse(self.table.update(row, "number", 3))
class TestTableSubset(tests.util.TestCase):
"""Tests the TableSubset."""
def setUp(self):
"""Set up common variables."""
super().setUp()
self.table = tests.util.table.MockTable(self.sql)
self.subset = emmental.db.table.TableSubset(self.table)
self.rows = [self.table.create(number=i) for i in range(5)]
def test_init(self):
"""Test that the TableSubset was set up properly."""
self.assertIsInstance(self.subset, Gio.ListModel)
self.assertIsInstance(self.subset, GObject.GObject)
self.assertIsInstance(self.subset.keyset, emmental.db.table.KeySet)
self.assertSetEqual(self.subset.keyset.keys, set())
self.assertEqual(self.subset.table, self.table)
subset2 = emmental.db.table.TableSubset(self.table, keys={1, 2, 3})
self.assertSetEqual(subset2.keyset.keys, {1, 2, 3})
def test_get_item_type(self):
"""Test the Gio.ListModel.get_item_type() function."""
self.assertEqual(self.subset.get_item_type(),
emmental.db.table.Row.__gtype__)
def test_get_n_items(self):
"""Test the Gio.ListModel.get_n_items() function."""
self.assertEqual(self.subset.get_n_items(), 0)
self.assertEqual(self.subset.n_rows, 0)
self.subset.add_row(self.rows[0])
self.assertEqual(self.subset.get_n_items(), 0)
self.assertEqual(self.subset.n_rows, 0)
self.table.loaded = True
self.assertEqual(self.subset.get_n_items(), 1)
self.assertEqual(self.subset.n_rows, 1)
self.table.loaded = False
self.assertEqual(self.subset.get_n_items(), 0)
self.assertEqual(self.subset.n_rows, 0)
def test_get_item(self):
"""Test the Gio.ListModel.get_item() function."""
for row in self.rows:
self.subset.add_row(row)
self.assertListEqual(self.subset._items, [])
for i, row in enumerate(self.rows):
with self.subTest(i=i, row=row.number):
self.assertIsNone(self.subset.get_item(i))
self.table.loaded = True
self.assertEqual(self.subset.get_item(i), row)
self.assertEqual(self.subset._items[i], row)
self.table.loaded = False
self.assertIsNone(self.subset.get_item(i))
def test_add_row(self):
"""Test adding a row to the TableSubset."""
expected = set()
self.table.loaded = True
self.assertListEqual(self.subset._items, [])
changed = unittest.mock.Mock()
self.subset.connect("items-changed", changed)
for n, i in enumerate([2, 0, 4, 1, 3], start=1):
row = self.rows[i]
with self.subTest(i=i, row=row.number):
expected.add(i)
self.subset.add_row(row)
self.assertSetEqual(self.subset.keyset.keys, expected)
self.assertEqual(self.subset.n_rows, n)
changed.assert_called_with(self.subset,
sorted(expected).index(i), 0, 1)
self.assertListEqual(self.subset._items, self.rows)
self.assertListEqual(list(self.subset), self.rows)
def test_remove_row(self):
"""Test removing a row from the TableSubset."""
self.table.loaded = True
[self.subset.add_row(row) for row in self.rows]
expected = {row.number for row in self.rows}
changed = unittest.mock.Mock()
self.subset.connect("items-changed", changed)
for n, i in enumerate([2, 0, 4, 1, 3], start=1):
row = self.rows[i]
rm = sorted(expected).index(i)
with self.subTest(i=i, row=row.number):
expected.discard(i)
self.subset.remove_row(row)
self.assertSetEqual(self.subset.keyset.keys, expected)
self.assertEqual(self.subset.n_rows, 5 - n)
changed.assert_called_with(self.subset, rm, 1, 0)
self.assertEqual(self.subset.n_rows, 0)
def test_contains(self):
"""Test the __contains__() magic method."""
self.table.loaded = True
self.assertFalse(self.rows[0] in self.subset)
self.subset.add_row(self.rows[0])
self.assertTrue(self.rows[0] in self.subset)
def test_table_not_loaded(self):
"""Test operations when the table hasn't been loaded."""
self.subset.add_row(self.rows[0])
self.assertListEqual(self.subset._items, [])
self.assertEqual(self.subset.n_rows, 0)
self.assertIsNone(self.subset.get_item(0))
self.subset.remove_row(self.rows[0])
self.assertListEqual(self.subset._items, [])
self.assertEqual(self.subset.n_rows, 0)
def test_table_loaded(self):
"""Test changing the value of Table.loaded."""
changed = unittest.mock.Mock()
self.subset.connect("items-changed", changed)
self.table.loaded = True
changed.assert_not_called()
self.table.loaded = False
changed.assert_not_called()
self.subset.add_row(self.rows[0])
self.subset.add_row(self.rows[1])
self.table.loaded = True
self.assertEqual(self.subset.n_rows, 2)
changed.assert_called_with(self.subset, 0, 0, 2)
self.table.loaded = False
self.assertEqual(self.subset.n_rows, 0)
changed.assert_called_with(self.subset, 0, 2, 0)

View File

@ -75,12 +75,14 @@ class TestYearTable(tests.util.TestCase):
def test_create(self):
"""Test creating a year playlist."""
decade = self.sql.decades.create(1980)
year = self.table.create(1988)
self.assertIsInstance(year, emmental.db.years.Year)
self.assertEqual(year.year, 1988)
self.assertEqual(year.name, "1988")
self.assertEqual(year.sort_order,
"release, albumartist, album, mediumno, number")
self.assertTrue(year in decade.child_set)
cur = self.sql("SELECT COUNT(year) FROM years")
self.assertEqual(cur.fetchone()["COUNT(year)"], 1)
@ -93,8 +95,10 @@ class TestYearTable(tests.util.TestCase):
def test_delete(self):
"""Test deleting a year playlist."""
decade = self.sql.decades.create(1980)
year = self.table.create(1988)
self.assertTrue(year.delete())
self.assertFalse(year in decade.child_set)
cur = self.sql("SELECT COUNT(year) FROM years")
self.assertEqual(cur.fetchone()["COUNT(year)"], 0)

View File

@ -22,9 +22,9 @@ class TestEmmental(unittest.TestCase):
"""Check that version constants have been set properly."""
self.assertEqual(emmental.MAJOR_VERSION, 3)
self.assertEqual(emmental.MINOR_VERSION, 0)
self.assertEqual(emmental.MICRO_VERSION, 3)
self.assertEqual(emmental.VERSION_NUMBER, "3.0.3")
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.3-debug")
self.assertEqual(emmental.MICRO_VERSION, 4)
self.assertEqual(emmental.VERSION_NUMBER, "3.0.4")
self.assertEqual(emmental.VERSION_STRING, "Emmental 3.0.4-debug")
def test_application(self):
"""Check that the application instance is initialized properly."""
@ -63,7 +63,7 @@ class TestEmmental(unittest.TestCase):
mock_startup.assert_called()
mock_load.assert_called()
mock_add_window.assert_called_with(self.application.win)
mock_set_useragent.assert_called_with("emmental-debug", "3.0.3")
mock_set_useragent.assert_called_with("emmental-debug", "3.0.4")
@unittest.mock.patch("sys.stdout")
@unittest.mock.patch("gi.repository.Adw.Application.add_window")