Repository: beetbox/mediafile

Issue:
List fields don't return None if the tag doesn't exist
If a list tag does not exist, it returns an empty list rather than `None`, but if a non-list tag does not exist, `None` is returned.

I believe non-existent list tags should also return `None` so that this behavior is consistent.

Comments:

Patch:
diff --git a/.gitignore b/.gitignore
index 14035f2..29a33a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ target/
 # virtualenv
 venv/
 ENV/
+.venv/
 
 # Spyder project settings
 .spyderproject
diff --git a/mediafile.py b/mediafile.py
index ca70c94..c834346 100644
--- a/mediafile.py
+++ b/mediafile.py
@@ -600,8 +600,8 @@ class ListStorageStyle(StorageStyle):
     object to each.
 
     Subclasses may overwrite ``fetch`` and ``store``.  ``fetch`` must
-    return a (possibly empty) list and ``store`` receives a serialized
-    list of values as the second argument.
+    return a (possibly empty) list or `None` if the tag does not exist.
+    ``store`` receives a serialized list of values as the second argument.
 
     The `serialize` and `deserialize` methods (from the base
     `StorageStyle`) are still called with individual values. This class
@@ -610,15 +610,23 @@ class ListStorageStyle(StorageStyle):
     def get(self, mutagen_file):
         """Get the first value in the field's value list.
         """
+        values = self.get_list(mutagen_file)
+        if values is None:
+            return None
+
         try:
-            return self.get_list(mutagen_file)[0]
+            return values[0]
         except IndexError:
             return None
 
     def get_list(self, mutagen_file):
         """Get a list of all values for the field using this style.
         """
-        return [self.deserialize(item) for item in self.fetch(mutagen_file)]
+        raw_values = self.fetch(mutagen_file)
+        if raw_values is None:
+            return None
+
+        return [self.deserialize(item) for item in raw_values]
 
     def fetch(self, mutagen_file):
         """Get the list of raw (serialized) values.
@@ -626,19 +634,27 @@ class ListStorageStyle(StorageStyle):
         try:
             return mutagen_file[self.key]
         except KeyError:
-            return []
+            return None
 
     def set(self, mutagen_file, value):
         """Set an individual value as the only value for the field using
         this style.
         """
-        self.set_list(mutagen_file, [value])
+        if value is None:
+            self.store(mutagen_file, None)
+        else:
+            self.set_list(mutagen_file, [value])
 
     def set_list(self, mutagen_file, values):
         """Set all values for the field using this style. `values`
         should be an iterable.
         """
-        self.store(mutagen_file, [self.serialize(value) for value in values])
+        if values is None:
+            self.delete(mutagen_file)
+        else:
+            self.store(
+                mutagen_file, [self.serialize(value) for value in values]
+            )
 
     def store(self, mutagen_file, values):
         """Set the list of all raw (serialized) values for this field.
@@ -1317,7 +1333,7 @@ class ListMediaField(MediaField):
             values = style.get_list(mediafile.mgfile)
             if values:
                 return [_safe_cast(self.out_type, value) for value in values]
-        return []
+        return None
 
     def __set__(self, mediafile, values):
         for style in self.styles(mediafile.mgfile):


Test patch:
diff --git a/test/test_mediafile.py b/test/test_mediafile.py
index 10bbf80..fa474bd 100644
--- a/test/test_mediafile.py
+++ b/test/test_mediafile.py
@@ -160,7 +160,7 @@ class ImageStructureTestMixin(ArtTestMixin):
         mediafile.save()
 
         mediafile = MediaFile(mediafile.filename)
-        self.assertEqual(len(mediafile.images), 0)
+        self.assertIsNone(mediafile.images)
 
     def test_guess_cover(self):
         mediafile = self._mediafile_fixture('image')