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