mirror of
https://github.com/beetbox/beets.git
synced 2025-12-25 10:05:13 +01:00
dbcore: types translate null values on assignment
In preparation for #660, where we will allow MediaFile to expose None values when tags are missing (and consume None to remove tags). This makes it possible to hide nullness in the rest of beets by translating None to a suitable zero-ish value on field assignment. Types can of course opt out of this to preserve a distinct null value. We do this now for the album_id field, which needs to be null to indicate singletons. Type.normalize() also enables more sophisticated translations (e.g., an integer field could round off float values assigned into it) in the future.
This commit is contained in:
parent
1b434a7dae
commit
c09bac603f
4 changed files with 53 additions and 12 deletions
|
|
@ -152,8 +152,15 @@ class Model(object):
|
|||
def __setitem__(self, key, value):
|
||||
"""Assign the value for a field.
|
||||
"""
|
||||
source = self._values_fixed if key in self._fields \
|
||||
else self._values_flex
|
||||
# Choose where to place the value. If the corresponding field
|
||||
# has a type, filter the value.
|
||||
if key in self._fields:
|
||||
source = self._values_fixed
|
||||
value = self._fields[key].normalize(value)
|
||||
else:
|
||||
source = self._values_flex
|
||||
|
||||
# Assign value and possibly mark as dirty.
|
||||
old_value = source.get(key)
|
||||
source[key] = value
|
||||
if old_value != value:
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ from beets.util import str2bool
|
|||
|
||||
class Type(object):
|
||||
"""An object encapsulating the type of a model field. Includes
|
||||
information about how to store the value in the database, query,
|
||||
format, and parse a given field.
|
||||
information about how to store, query, format, and parse a given
|
||||
field.
|
||||
"""
|
||||
|
||||
sql = None
|
||||
|
|
@ -36,6 +36,10 @@ class Type(object):
|
|||
"""The `Query` subclass to be used when querying the field.
|
||||
"""
|
||||
|
||||
null = None
|
||||
"""The value to be exposed when the underlying value is None.
|
||||
"""
|
||||
|
||||
def format(self, value):
|
||||
"""Given a value of this type, produce a Unicode string
|
||||
representing the value. This is used in template evaluation.
|
||||
|
|
@ -48,6 +52,16 @@ class Type(object):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def normalize(self, value):
|
||||
"""Given a value that will be assigned into a field of this
|
||||
type, normalize the value to have the appropriate type. This
|
||||
base implementation only reinterprets `None`.
|
||||
"""
|
||||
if value is None:
|
||||
return self.null
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
|
||||
# Reusable types.
|
||||
|
|
@ -58,6 +72,7 @@ class Integer(Type):
|
|||
"""
|
||||
sql = u'INTEGER'
|
||||
query = query.NumericQuery
|
||||
null = 0
|
||||
|
||||
def format(self, value):
|
||||
return unicode(value or 0)
|
||||
|
|
@ -93,9 +108,14 @@ class ScaledInt(Integer):
|
|||
|
||||
|
||||
class Id(Integer):
|
||||
"""An integer used as the row key for a SQLite table.
|
||||
"""An integer used as the row id or a foreign key in a SQLite table.
|
||||
This type is nullable: None values are not translated to zero.
|
||||
"""
|
||||
sql = u'INTEGER PRIMARY KEY'
|
||||
null = None
|
||||
|
||||
def __init__(self, primary=True):
|
||||
if primary:
|
||||
self.sql = u'INTEGER PRIMARY KEY'
|
||||
|
||||
|
||||
class Float(Type):
|
||||
|
|
@ -103,6 +123,7 @@ class Float(Type):
|
|||
"""
|
||||
sql = u'REAL'
|
||||
query = query.NumericQuery
|
||||
null = 0.0
|
||||
|
||||
def format(self, value):
|
||||
return u'{0:.1f}'.format(value or 0.0)
|
||||
|
|
@ -119,6 +140,7 @@ class String(Type):
|
|||
"""
|
||||
sql = u'TEXT'
|
||||
query = query.SubstringQuery
|
||||
null = u''
|
||||
|
||||
def format(self, value):
|
||||
return unicode(value) if value else u''
|
||||
|
|
@ -132,6 +154,7 @@ class Boolean(Type):
|
|||
"""
|
||||
sql = u'INTEGER'
|
||||
query = query.BooleanQuery
|
||||
null = False
|
||||
|
||||
def format(self, value):
|
||||
return unicode(bool(value))
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class SingletonQuery(dbcore.Query):
|
|||
class DateType(types.Type):
|
||||
sql = u'REAL'
|
||||
query = dbcore.query.DateQuery
|
||||
null = 0.0
|
||||
|
||||
def format(self, value):
|
||||
return time.strftime(beets.config['time_format'].get(unicode),
|
||||
|
|
@ -95,7 +96,7 @@ class DateType(types.Type):
|
|||
try:
|
||||
return float(string)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
return self.null
|
||||
|
||||
|
||||
class PathType(types.Type):
|
||||
|
|
@ -122,9 +123,9 @@ class PathType(types.Type):
|
|||
# - Is the field writable?
|
||||
# - Does the field reflect an attribute of a MediaFile?
|
||||
ITEM_FIELDS = [
|
||||
('id', types.Id(), False, False),
|
||||
('id', types.Id(True), False, False),
|
||||
('path', PathType(), False, False),
|
||||
('album_id', types.Integer(), False, False),
|
||||
('album_id', types.Id(False), False, False),
|
||||
|
||||
('title', types.String(), True, True),
|
||||
('artist', types.String(), True, True),
|
||||
|
|
@ -192,9 +193,9 @@ ITEM_KEYS = [f[0] for f in ITEM_FIELDS]
|
|||
# The third entry in each tuple indicates whether the field reflects an
|
||||
# identically-named field in the items table.
|
||||
ALBUM_FIELDS = [
|
||||
('id', types.Id(), False),
|
||||
('artpath', PathType(), False),
|
||||
('added', DateType(), True),
|
||||
('id', types.Id(True), False),
|
||||
('artpath', PathType(), False),
|
||||
('added', DateType(), True),
|
||||
|
||||
('albumartist', types.String(), True),
|
||||
('albumartist_sort', types.String(), True),
|
||||
|
|
|
|||
|
|
@ -219,6 +219,16 @@ class ModelTest(_common.TestCase):
|
|||
with self.assertRaises(KeyError):
|
||||
del model['field_one']
|
||||
|
||||
def test_null_value_normalization_by_type(self):
|
||||
model = TestModel1()
|
||||
model.field_one = None
|
||||
self.assertEqual(model.field_one, 0)
|
||||
|
||||
def test_null_value_stays_none_for_untyped_field(self):
|
||||
model = TestModel1()
|
||||
model.foo = None
|
||||
self.assertEqual(model.foo, None)
|
||||
|
||||
|
||||
class FormatTest(_common.TestCase):
|
||||
def test_format_fixed_field(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue