1# -*- coding: utf-8 -*- 2# This file is part of beets. 3# Copyright 2016, Adrian Sampson. 4# 5# Permission is hereby granted, free of charge, to any person obtaining 6# a copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, 9# distribute, sublicense, and/or sell copies of the Software, and to 10# permit persons to whom the Software is furnished to do so, subject to 11# the following conditions: 12# 13# The above copyright notice and this permission notice shall be 14# included in all copies or substantial portions of the Software. 15 16"""Representation of type information for DBCore model fields. 17""" 18from __future__ import division, absolute_import, print_function 19 20from . import query 21from beets.util import str2bool 22import six 23 24if not six.PY2: 25 buffer = memoryview # sqlite won't accept memoryview in python 2 26 27 28# Abstract base. 29 30class Type(object): 31 """An object encapsulating the type of a model field. Includes 32 information about how to store, query, format, and parse a given 33 field. 34 """ 35 36 sql = u'TEXT' 37 """The SQLite column type for the value. 38 """ 39 40 query = query.SubstringQuery 41 """The `Query` subclass to be used when querying the field. 42 """ 43 44 model_type = six.text_type 45 """The Python type that is used to represent the value in the model. 46 47 The model is guaranteed to return a value of this type if the field 48 is accessed. To this end, the constructor is used by the `normalize` 49 and `from_sql` methods and the `default` property. 50 """ 51 52 @property 53 def null(self): 54 """The value to be exposed when the underlying value is None. 55 """ 56 return self.model_type() 57 58 def format(self, value): 59 """Given a value of this type, produce a Unicode string 60 representing the value. This is used in template evaluation. 61 """ 62 if value is None: 63 value = self.null 64 # `self.null` might be `None` 65 if value is None: 66 value = u'' 67 if isinstance(value, bytes): 68 value = value.decode('utf-8', 'ignore') 69 70 return six.text_type(value) 71 72 def parse(self, string): 73 """Parse a (possibly human-written) string and return the 74 indicated value of this type. 75 """ 76 try: 77 return self.model_type(string) 78 except ValueError: 79 return self.null 80 81 def normalize(self, value): 82 """Given a value that will be assigned into a field of this 83 type, normalize the value to have the appropriate type. This 84 base implementation only reinterprets `None`. 85 """ 86 if value is None: 87 return self.null 88 else: 89 # TODO This should eventually be replaced by 90 # `self.model_type(value)` 91 return value 92 93 def from_sql(self, sql_value): 94 """Receives the value stored in the SQL backend and return the 95 value to be stored in the model. 96 97 For fixed fields the type of `value` is determined by the column 98 type affinity given in the `sql` property and the SQL to Python 99 mapping of the database adapter. For more information see: 100 http://www.sqlite.org/datatype3.html 101 https://docs.python.org/2/library/sqlite3.html#sqlite-and-python-types 102 103 Flexible fields have the type affinity `TEXT`. This means the 104 `sql_value` is either a `buffer`/`memoryview` or a `unicode` object` 105 and the method must handle these in addition. 106 """ 107 if isinstance(sql_value, buffer): 108 sql_value = bytes(sql_value).decode('utf-8', 'ignore') 109 if isinstance(sql_value, six.text_type): 110 return self.parse(sql_value) 111 else: 112 return self.normalize(sql_value) 113 114 def to_sql(self, model_value): 115 """Convert a value as stored in the model object to a value used 116 by the database adapter. 117 """ 118 return model_value 119 120 121# Reusable types. 122 123class Default(Type): 124 null = None 125 126 127class Integer(Type): 128 """A basic integer type. 129 """ 130 sql = u'INTEGER' 131 query = query.NumericQuery 132 model_type = int 133 134 135class PaddedInt(Integer): 136 """An integer field that is formatted with a given number of digits, 137 padded with zeroes. 138 """ 139 def __init__(self, digits): 140 self.digits = digits 141 142 def format(self, value): 143 return u'{0:0{1}d}'.format(value or 0, self.digits) 144 145 146class NullPaddedInt(PaddedInt): 147 """Same as `PaddedInt`, but does not normalize `None` to `0.0`. 148 """ 149 null = None 150 151 152class ScaledInt(Integer): 153 """An integer whose formatting operation scales the number by a 154 constant and adds a suffix. Good for units with large magnitudes. 155 """ 156 def __init__(self, unit, suffix=u''): 157 self.unit = unit 158 self.suffix = suffix 159 160 def format(self, value): 161 return u'{0}{1}'.format((value or 0) // self.unit, self.suffix) 162 163 164class Id(Integer): 165 """An integer used as the row id or a foreign key in a SQLite table. 166 This type is nullable: None values are not translated to zero. 167 """ 168 null = None 169 170 def __init__(self, primary=True): 171 if primary: 172 self.sql = u'INTEGER PRIMARY KEY' 173 174 175class Float(Type): 176 """A basic floating-point type. The `digits` parameter specifies how 177 many decimal places to use in the human-readable representation. 178 """ 179 sql = u'REAL' 180 query = query.NumericQuery 181 model_type = float 182 183 def __init__(self, digits=1): 184 self.digits = digits 185 186 def format(self, value): 187 return u'{0:.{1}f}'.format(value or 0, self.digits) 188 189 190class NullFloat(Float): 191 """Same as `Float`, but does not normalize `None` to `0.0`. 192 """ 193 null = None 194 195 196class String(Type): 197 """A Unicode string type. 198 """ 199 sql = u'TEXT' 200 query = query.SubstringQuery 201 202 203class Boolean(Type): 204 """A boolean type. 205 """ 206 sql = u'INTEGER' 207 query = query.BooleanQuery 208 model_type = bool 209 210 def format(self, value): 211 return six.text_type(bool(value)) 212 213 def parse(self, string): 214 return str2bool(string) 215 216 217# Shared instances of common types. 218DEFAULT = Default() 219INTEGER = Integer() 220PRIMARY_ID = Id(True) 221FOREIGN_ID = Id(False) 222FLOAT = Float() 223NULL_FLOAT = NullFloat() 224STRING = String() 225BOOLEAN = Boolean() 226