diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4703203ba..5441940a4 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -48,4 +48,6 @@ f36bc497c8c8f89004f3f6879908d3f0b25123e1 # Fix formatting c490ac5810b70f3cf5fd8649669838e8fdb19f4d # Importer restructure -9147577b2b19f43ca827e9650261a86fb0450cef \ No newline at end of file +9147577b2b19f43ca827e9650261a86fb0450cef +# Copy paste query, types from library to dbcore +1a045c91668c771686f4c871c84f1680af2e944b diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 9812a7528..3243445cb 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -40,6 +40,8 @@ else: # To use the SQLite "blob" type, it doesn't suffice to provide a byte # string; SQLite treats that as encoded text. Wrapping it in a # `memoryview` tells it that we actually mean non-text data. +# needs to be defined in here due to circular import. +# TODO: remove it from this module and define it in dbcore/types.py instead BLOB_TYPE = memoryview diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index 27cd04b92..be28f6891 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -22,6 +22,7 @@ import typing from abc import ABC from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast +import beets from beets import util from . import query @@ -345,7 +346,7 @@ class DateType(Float): return self.null -class PathType(Type[bytes, bytes]): +class BasePathType(Type[bytes, N]): """A dbcore type for filesystem paths. These are represented as `bytes` objects, in keeping with @@ -356,27 +357,10 @@ class PathType(Type[bytes, bytes]): query = query.PathQuery model_type = bytes - def __init__(self, nullable=False): - """Create a path type object. + def parse(self, string: str) -> bytes: + return util.normpath(string) - `nullable` controls whether the type may be missing, i.e., None. - """ - self.nullable = nullable - - @property - def null(self): - if self.nullable: - return None - else: - return b"" - - def format(self, value): - return util.displayable_path(value) - - def parse(self, string): - return util.normpath(util.bytestring_path(string)) - - def normalize(self, value): + def normalize(self, value: Any) -> bytes | N: if isinstance(value, str): # Paths stored internally as encoded bytes. return util.bytestring_path(value) @@ -391,12 +375,30 @@ class PathType(Type[bytes, bytes]): def from_sql(self, sql_value): return self.normalize(sql_value) - def to_sql(self, value): + def to_sql(self, value: bytes) -> BLOB_TYPE: if isinstance(value, bytes): value = BLOB_TYPE(value) return value +class NullPathType(BasePathType[None]): + @property + def null(self) -> None: + return None + + def format(self, value: bytes | None) -> str: + return util.displayable_path(value or b"") + + +class PathType(BasePathType[bytes]): + @property + def null(self) -> bytes: + return b"" + + def format(self, value: bytes) -> str: + return util.displayable_path(value or b"") + + class MusicalKey(String): """String representing the musical key of a song. diff --git a/beets/library.py b/beets/library.py index 5a692ef1c..9223b3209 100644 --- a/beets/library.py +++ b/beets/library.py @@ -910,7 +910,7 @@ class Album(LibModel): _always_dirty = True _fields = { "id": types.PRIMARY_ID, - "artpath": types.PathType(True), + "artpath": types.NullPathType(), "added": types.DATE, "albumartist": types.STRING, "albumartist_sort": types.STRING, diff --git a/test/test_types.py b/test/test_types.py index 8a6acd0dc..6727917d8 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -1,59 +1,58 @@ import time -import unittest import beets from beets.dbcore import types from beets.util import normpath -class LibraryFieldTypesTest(unittest.TestCase): - """Test format() and parse() for library-specific field types""" +def test_datetype(): + t = types.DATE - def test_datetype(self): - t = types.DATE + # format + time_format = beets.config["time_format"].as_str() + time_local = time.strftime(time_format, time.localtime(123456789)) + assert time_local == t.format(123456789) + # parse + assert 123456789.0 == t.parse(time_local) + assert 123456789.0 == t.parse("123456789.0") + assert t.null == t.parse("not123456789.0") + assert t.null == t.parse("1973-11-29") - # format - time_format = beets.config["time_format"].as_str() - time_local = time.strftime(time_format, time.localtime(123456789)) - assert time_local == t.format(123456789) - # parse - assert 123456789.0 == t.parse(time_local) - assert 123456789.0 == t.parse("123456789.0") - assert t.null == t.parse("not123456789.0") - assert t.null == t.parse("1973-11-29") - def test_pathtype(self): - t = types.PathType() +def test_pathtype(): + t = types.PathType() - # format - assert "/tmp" == t.format("/tmp") - assert "/tmp/\xe4lbum" == t.format("/tmp/\u00e4lbum") - # parse - assert normpath(b"/tmp") == t.parse("/tmp") - assert normpath(b"/tmp/\xc3\xa4lbum") == t.parse("/tmp/\u00e4lbum/") + # format + assert "/tmp" == t.format("/tmp") + assert "/tmp/\xe4lbum" == t.format("/tmp/\u00e4lbum") + # parse + assert normpath(b"/tmp") == t.parse("/tmp") + assert normpath(b"/tmp/\xc3\xa4lbum") == t.parse("/tmp/\u00e4lbum/") - def test_musicalkey(self): - t = types.MusicalKey() - # parse - assert "C#m" == t.parse("c#m") - assert "Gm" == t.parse("g minor") - assert "Not c#m" == t.parse("not C#m") +def test_musicalkey(): + t = types.MusicalKey() - def test_durationtype(self): - t = types.DurationType() + # parse + assert "C#m" == t.parse("c#m") + assert "Gm" == t.parse("g minor") + assert "Not c#m" == t.parse("not C#m") - # format - assert "1:01" == t.format(61.23) - assert "60:01" == t.format(3601.23) - assert "0:00" == t.format(None) - # parse - assert 61.0 == t.parse("1:01") - assert 61.23 == t.parse("61.23") - assert 3601.0 == t.parse("60:01") - assert t.null == t.parse("1:00:01") - assert t.null == t.parse("not61.23") - # config format_raw_length - beets.config["format_raw_length"] = True - assert 61.23 == t.format(61.23) - assert 3601.23 == t.format(3601.23) + +def test_durationtype(): + t = types.DurationType() + + # format + assert "1:01" == t.format(61.23) + assert "60:01" == t.format(3601.23) + assert "0:00" == t.format(None) + # parse + assert 61.0 == t.parse("1:01") + assert 61.23 == t.parse("61.23") + assert 3601.0 == t.parse("60:01") + assert t.null == t.parse("1:00:01") + assert t.null == t.parse("not61.23") + # config format_raw_length + beets.config["format_raw_length"] = True + assert 61.23 == t.format(61.23) + assert 3601.23 == t.format(3601.23)