DBCore types: no functional-style Type constructor

This was getting more and more awkward. Also added a `parse` method (in
progress).
This commit is contained in:
Adrian Sampson 2014-01-26 21:12:48 -08:00
parent 83f981762c
commit f29fbe47da
3 changed files with 197 additions and 124 deletions

View file

@ -15,6 +15,11 @@
"""Representation of type information for DBCore model fields.
"""
from . import query
from beets.util import str2bool
# Abstract base.
class Type(object):
@ -22,62 +27,116 @@ class Type(object):
information about how to store the value in the database, query,
format, and parse a given field.
"""
def __init__(self, sql, query, format_func=None):
def __init__(self, sql, query):
"""Create a type. `sql` is the SQLite column type for the value.
`query` is the `Query` subclass to be used when querying the
field. `format_func` is a function that transforms values of
this type to a human-readable Unicode string. If `format_func`
is not provided, the subclass must override `format` to provide
the functionality.
field.
"""
self.sql = sql
self.query = query
self.format_func = format_func
def format(self, value):
"""Given a value of this type, produce a Unicode string
representing the value. This is used in template evaluation.
"""
return self.format_func(value)
raise NotImplementedError()
# Common singleton types.
ID_TYPE = Type('INTEGER PRIMARY KEY', query.NumericQuery, unicode)
INT_TYPE = Type('INTEGER', query.NumericQuery,
lambda n: unicode(n or 0))
FLOAT_TYPE = Type('REAL', query.NumericQuery,
lambda n: u'{0:.1f}'.format(n or 0.0))
STRING_TYPE = Type('TEXT', query.SubstringQuery,
lambda s: unicode(s) if s else u'')
BOOL_TYPE = Type('INTEGER', query.BooleanQuery,
lambda b: unicode(bool(b)))
def parse(self, string):
"""Parse a (possibly human-written) string and return the
indicated value of this type.
"""
raise NotImplementedError()
# Parameterized types.
# Reusable types.
class PaddedInt(Type):
class Integer(Type):
"""A basic integer type.
"""
def __init__(self, sql='INTEGER', query=query.NumericQuery):
super(Integer, self).__init__(sql, query)
def format(self, value):
return unicode(value or 0)
def parse(self, string):
try:
return int(string)
except ValueError:
return 0
class PaddedInt(Integer):
"""An integer field that is formatted with a given number of digits,
padded with zeroes.
"""
def __init__(self, digits):
super(PaddedInt, self).__init__()
self.digits = digits
super(PaddedInt, self).__init__('INTEGER', query.NumericQuery)
def format(self, value):
return u'{0:0{1}d}'.format(value or 0, self.digits)
class ScaledInt(Type):
class ScaledInt(Integer):
"""An integer whose formatting operation scales the number by a
constant and adds a suffix. Good for units with large magnitudes.
"""
def __init__(self, unit, suffix=u''):
super(ScaledInt, self).__init__()
self.unit = unit
self.suffix = suffix
super(ScaledInt, self).__init__('INTEGER', query.NumericQuery)
def format(self, value):
return u'{0}{1}'.format((value or 0) // self.unit, self.suffix)
class Id(Integer):
"""An integer used as the row key for a SQLite table.
"""
def __init__(self):
super(Id, self).__init__('INTEGER PRIMARY KEY')
class Float(Type):
"""A basic floating-point type.
"""
def __init__(self, sql='REAL', query=query.NumericQuery):
super(Float, self).__init__(sql, query)
def format(self, value):
return u'{0:.1f}'.format(value or 0.0)
def parse(self, string):
try:
return float(string)
except ValueError:
return 0.0
class String(Type):
"""A Unicode string type.
"""
def __init__(self, sql='TEXT', query=query.SubstringQuery):
super(String, self).__init__(sql, query)
def format(self, value):
return unicode(value) if value else u''
def parse(self, string):
return string
class Boolean(Type):
"""A boolean type.
"""
def __init__(self, sql='INTEGER', query=query.BooleanQuery):
return super(Boolean, self).__init__(sql, query)
def format(self, value):
return unicode(bool(value))
def parse(self, string):
return str2bool(string)

View file

@ -74,16 +74,35 @@ class SingletonQuery(dbcore.Query):
# Library-specific field types.
class DateType(types.Type):
def __init__(self):
super(DateType, self).__init__('REAL', dbcore.query.NumericQuery)
def format(self, value):
return time.strftime(beets.config['time_format'].get(unicode),
time.localtime(value or 0))
def parse(self, string):
raise NotImplementedError() # FIXME
class PathType(types.Type):
def __init__(self):
super(PathType, self).__init__('BLOB', PathQuery)
def format(self, value):
return util.displayable_path(value)
def parse(self, string):
return normpath(bytestring_path(string))
# Model field lists.
# Common types used in field definitions.
DATE_TYPE = types.Type(
'REAL',
dbcore.query.NumericQuery,
lambda n: time.strftime(beets.config['time_format'].get(unicode),
time.localtime(n or 0))
)
PATH_TYPE = types.Type('BLOB', PathQuery, util.displayable_path)
# Fields in the "items" database table; all the metadata available for
# items in the library. These are used directly in SQL; they are
@ -94,21 +113,21 @@ PATH_TYPE = types.Type('BLOB', PathQuery, util.displayable_path)
# - Is the field writable?
# - Does the field reflect an attribute of a MediaFile?
ITEM_FIELDS = [
('id', types.ID_TYPE, False, False),
('path', PATH_TYPE, False, False),
('album_id', types.INT_TYPE, False, False),
('id', types.Id(), False, False),
('path', PathType(), False, False),
('album_id', types.Integer(), False, False),
('title', types.STRING_TYPE, True, True),
('artist', types.STRING_TYPE, True, True),
('artist_sort', types.STRING_TYPE, True, True),
('artist_credit', types.STRING_TYPE, True, True),
('album', types.STRING_TYPE, True, True),
('albumartist', types.STRING_TYPE, True, True),
('albumartist_sort', types.STRING_TYPE, True, True),
('albumartist_credit', types.STRING_TYPE, True, True),
('genre', types.STRING_TYPE, True, True),
('composer', types.STRING_TYPE, True, True),
('grouping', types.STRING_TYPE, True, True),
('title', types.String(), True, True),
('artist', types.String(), True, True),
('artist_sort', types.String(), True, True),
('artist_credit', types.String(), True, True),
('album', types.String(), True, True),
('albumartist', types.String(), True, True),
('albumartist_sort', types.String(), True, True),
('albumartist_credit', types.String(), True, True),
('genre', types.String(), True, True),
('composer', types.String(), True, True),
('grouping', types.String(), True, True),
('year', types.PaddedInt(4), True, True),
('month', types.PaddedInt(2), True, True),
('day', types.PaddedInt(2), True, True),
@ -116,45 +135,45 @@ ITEM_FIELDS = [
('tracktotal', types.PaddedInt(2), True, True),
('disc', types.PaddedInt(2), True, True),
('disctotal', types.PaddedInt(2), True, True),
('lyrics', types.STRING_TYPE, True, True),
('comments', types.STRING_TYPE, True, True),
('bpm', types.INT_TYPE, True, True),
('comp', types.BOOL_TYPE, True, True),
('mb_trackid', types.STRING_TYPE, True, True),
('mb_albumid', types.STRING_TYPE, True, True),
('mb_artistid', types.STRING_TYPE, True, True),
('mb_albumartistid', types.STRING_TYPE, True, True),
('albumtype', types.STRING_TYPE, True, True),
('label', types.STRING_TYPE, True, True),
('acoustid_fingerprint', types.STRING_TYPE, True, True),
('acoustid_id', types.STRING_TYPE, True, True),
('mb_releasegroupid', types.STRING_TYPE, True, True),
('asin', types.STRING_TYPE, True, True),
('catalognum', types.STRING_TYPE, True, True),
('script', types.STRING_TYPE, True, True),
('language', types.STRING_TYPE, True, True),
('country', types.STRING_TYPE, True, True),
('albumstatus', types.STRING_TYPE, True, True),
('media', types.STRING_TYPE, True, True),
('albumdisambig', types.STRING_TYPE, True, True),
('disctitle', types.STRING_TYPE, True, True),
('encoder', types.STRING_TYPE, True, True),
('rg_track_gain', types.FLOAT_TYPE, True, True),
('rg_track_peak', types.FLOAT_TYPE, True, True),
('rg_album_gain', types.FLOAT_TYPE, True, True),
('rg_album_peak', types.FLOAT_TYPE, True, True),
('lyrics', types.String(), True, True),
('comments', types.String(), True, True),
('bpm', types.Integer(), True, True),
('comp', types.Boolean(), True, True),
('mb_trackid', types.String(), True, True),
('mb_albumid', types.String(), True, True),
('mb_artistid', types.String(), True, True),
('mb_albumartistid', types.String(), True, True),
('albumtype', types.String(), True, True),
('label', types.String(), True, True),
('acoustid_fingerprint', types.String(), True, True),
('acoustid_id', types.String(), True, True),
('mb_releasegroupid', types.String(), True, True),
('asin', types.String(), True, True),
('catalognum', types.String(), True, True),
('script', types.String(), True, True),
('language', types.String(), True, True),
('country', types.String(), True, True),
('albumstatus', types.String(), True, True),
('media', types.String(), True, True),
('albumdisambig', types.String(), True, True),
('disctitle', types.String(), True, True),
('encoder', types.String(), True, True),
('rg_track_gain', types.Float(), True, True),
('rg_track_peak', types.Float(), True, True),
('rg_album_gain', types.Float(), True, True),
('rg_album_peak', types.Float(), True, True),
('original_year', types.PaddedInt(4), True, True),
('original_month', types.PaddedInt(2), True, True),
('original_day', types.PaddedInt(2), True, True),
('length', types.FLOAT_TYPE, False, True),
('length', types.Float(), False, True),
('bitrate', types.ScaledInt(1000, u'kbps'), False, True),
('format', types.STRING_TYPE, False, True),
('format', types.String(), False, True),
('samplerate', types.ScaledInt(1000, u'kHz'), False, True),
('bitdepth', types.INT_TYPE, False, True),
('channels', types.INT_TYPE, False, True),
('mtime', DATE_TYPE, False, False),
('added', DATE_TYPE, False, False),
('bitdepth', types.Integer(), False, True),
('channels', types.Integer(), False, True),
('mtime', DateType(), False, False),
('added', DateType(), False, False),
]
ITEM_KEYS_WRITABLE = [f[0] for f in ITEM_FIELDS if f[3] and f[2]]
ITEM_KEYS_META = [f[0] for f in ITEM_FIELDS if f[3]]
@ -164,36 +183,36 @@ 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_TYPE, False),
('artpath', PATH_TYPE, False),
('added', DATE_TYPE, True),
('id', types.Id(), False),
('artpath', PathType(), False),
('added', DateType(), True),
('albumartist', types.STRING_TYPE, True),
('albumartist_sort', types.STRING_TYPE, True),
('albumartist_credit', types.STRING_TYPE, True),
('album', types.STRING_TYPE, True),
('genre', types.STRING_TYPE, True),
('albumartist', types.String(), True),
('albumartist_sort', types.String(), True),
('albumartist_credit', types.String(), True),
('album', types.String(), True),
('genre', types.String(), True),
('year', types.PaddedInt(4), True),
('month', types.PaddedInt(2), True),
('day', types.PaddedInt(2), True),
('tracktotal', types.PaddedInt(2), True),
('disctotal', types.PaddedInt(2), True),
('comp', types.BOOL_TYPE, True),
('mb_albumid', types.STRING_TYPE, True),
('mb_albumartistid', types.STRING_TYPE, True),
('albumtype', types.STRING_TYPE, True),
('label', types.STRING_TYPE, True),
('mb_releasegroupid', types.STRING_TYPE, True),
('asin', types.STRING_TYPE, True),
('catalognum', types.STRING_TYPE, True),
('script', types.STRING_TYPE, True),
('language', types.STRING_TYPE, True),
('country', types.STRING_TYPE, True),
('albumstatus', types.STRING_TYPE, True),
('media', types.STRING_TYPE, True),
('albumdisambig', types.STRING_TYPE, True),
('rg_album_gain', types.FLOAT_TYPE, True),
('rg_album_peak', types.FLOAT_TYPE, True),
('comp', types.Boolean(), True),
('mb_albumid', types.String(), True),
('mb_albumartistid', types.String(), True),
('albumtype', types.String(), True),
('label', types.String(), True),
('mb_releasegroupid', types.String(), True),
('asin', types.String(), True),
('catalognum', types.String(), True),
('script', types.String(), True),
('language', types.String(), True),
('country', types.String(), True),
('albumstatus', types.String(), True),
('media', types.String(), True),
('albumdisambig', types.String(), True),
('rg_album_gain', types.Float(), True),
('rg_album_peak', types.Float(), True),
('original_year', types.PaddedInt(4), True),
('original_month', types.PaddedInt(2), True),
('original_day', types.PaddedInt(2), True),

View file

@ -25,17 +25,12 @@ from beets import dbcore
# Fixture: concrete database and model classes. For migration tests, we
# have multiple models with different numbers of fields.
ID_TYPE = dbcore.Type('INTEGER PRIMARY KEY', dbcore.query.NumericQuery,
unicode)
INT_TYPE = dbcore.Type('INTEGER', dbcore.query.NumericQuery,
unicode)
class TestModel1(dbcore.Model):
_table = 'test'
_flex_table = 'testflex'
_fields = {
'id': ID_TYPE,
'field_one': INT_TYPE,
'id': dbcore.types.Id(),
'field_one': dbcore.types.Integer(),
}
@classmethod
@ -51,9 +46,9 @@ class TestDatabase1(dbcore.Database):
class TestModel2(TestModel1):
_fields = {
'id': ID_TYPE,
'field_one': INT_TYPE,
'field_two': INT_TYPE,
'id': dbcore.types.Id(),
'field_one': dbcore.types.Integer(),
'field_two': dbcore.types.Integer(),
}
class TestDatabase2(dbcore.Database):
@ -62,10 +57,10 @@ class TestDatabase2(dbcore.Database):
class TestModel3(TestModel1):
_fields = {
'id': ID_TYPE,
'field_one': INT_TYPE,
'field_two': INT_TYPE,
'field_three': INT_TYPE,
'id': dbcore.types.Id(),
'field_one': dbcore.types.Integer(),
'field_two': dbcore.types.Integer(),
'field_three': dbcore.types.Integer(),
}
class TestDatabase3(dbcore.Database):
@ -74,11 +69,11 @@ class TestDatabase3(dbcore.Database):
class TestModel4(TestModel1):
_fields = {
'id': ID_TYPE,
'field_one': INT_TYPE,
'field_two': INT_TYPE,
'field_three': INT_TYPE,
'field_four': INT_TYPE,
'id': dbcore.types.Id(),
'field_one': dbcore.types.Integer(),
'field_two': dbcore.types.Integer(),
'field_three': dbcore.types.Integer(),
'field_four': dbcore.types.Integer(),
}
class TestDatabase4(dbcore.Database):
@ -89,8 +84,8 @@ class AnotherTestModel(TestModel1):
_table = 'another'
_flex_table = 'anotherflex'
_fields = {
'id': ID_TYPE,
'foo': INT_TYPE,
'id': dbcore.types.Id(),
'foo': dbcore.types.Integer(),
}
class TestDatabaseTwoModels(dbcore.Database):