mirror of
https://github.com/beetbox/beets.git
synced 2026-02-09 00:41:57 +01:00
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:
parent
83f981762c
commit
f29fbe47da
3 changed files with 197 additions and 124 deletions
|
|
@ -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)
|
||||
|
|
|
|||
177
beets/library.py
177
beets/library.py
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue