Skip to content

Commit

Permalink
Restructure classes
Browse files Browse the repository at this point in the history
  • Loading branch information
kernitus committed Oct 29, 2023
1 parent 74ea6af commit d88d328
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 269 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/python-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,4 @@ jobs:
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with unittest
run: |
python beetsplug/test_oldestdate.py
run: python -m unittest discover -s tests
91 changes: 91 additions & 0 deletions beetsplug/date_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import datetime
from dateutil import parser


class DateWrapper(datetime.datetime):
"""
Wrapper class for datetime objects.
Allows comparison between dates taking into
account the month and day being optional.
"""

def __new__(cls, y: int = None, m: int = None, d: int = None, iso_string: str = None):
"""
Create a new datetime object using a convenience wrapper.
Must specify at least one of either year or iso_string.
:param y: The year, as an integer
:param m: The month, as an integer (optional)
:param d: The day, as an integer (optional)
:param iso_string: A string representing the date in the format YYYYMMDD. Month and day are optional.
"""
if y is not None:
year = min(max(y, datetime.MINYEAR), datetime.MAXYEAR)
month = m if (m is not None and 0 < m <= 12) else 1
day = d if (d is not None and 0 < d <= 31) else 1
elif iso_string is not None:
parsed = parser.isoparse(iso_string)
return datetime.datetime.__new__(cls, parsed.year, parsed.month, parsed.day)
else:
raise TypeError("Must specify a value for year or a date string")

return datetime.datetime.__new__(cls, year, month, day)

@classmethod
def today(cls):
today = datetime.date.today()
return DateWrapper(today.year, today.month, today.day)

def __init__(self, y=None, m=None, d=None, iso_string=None):
if y is not None:
self.y = min(max(y, datetime.MINYEAR), datetime.MAXYEAR)
self.m = m if (m is None or 0 < m <= 12) else 1
self.d = d if (d is None or 0 < d <= 31) else 1
elif iso_string is not None:
# Remove any hyphen separators
iso_string = iso_string.replace("-", "")
length = len(iso_string)

if length < 4:
raise ValueError("Invalid value for year")

self.y = int(iso_string[:4])
self.m = None
self.d = None

# Month and day are optional
if length >= 6:
self.m = int(iso_string[4:6])
if length >= 8:
self.d = int(iso_string[6:8])
else:
raise TypeError("Must specify a value for year or a date string")

def __lt__(self, other):
if self.y != other.y:
return self.y < other.y
elif self.m is None:
return False
else:
if other.m is None:
return True
elif self.m == other.m:
if self.d is None:
return False
else:
if other.d is None:
return True
else:
return self.d < other.d
else:
return self.m < other.m

def __eq__(self, other):
if self.y != other.y:
return False
elif self.m is not None and other.m is not None:
if self.d is not None and other.d is not None:
return self.d == other.d
else:
return self.m == other.m
else:
return self.m == other.m
95 changes: 2 additions & 93 deletions beetsplug/oldestdate.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import datetime
from dateutil import parser

import mediafile
import musicbrainzngs
from beets import ui, config
from beets.autotag import hooks
from beets.importer import action
from beets.plugins import BeetsPlugin

from date_wrapper import DateWrapper

musicbrainzngs.set_useragent(
"Beets oldestdate plugin",
"1.1.4",
Expand Down Expand Up @@ -65,96 +64,6 @@ def _is_cover(recording):
return True
return False


class DateWrapper(datetime.datetime):
"""
Wrapper class for datetime objects.
Allows comparison between dates taking into
account the month and day being optional.
"""

def __new__(cls, y: int = None, m: int = None, d: int = None, iso_string: str = None):
"""
Create a new datetime object using a convenience wrapper.
Must specify at least one of either year or iso_string.
:param y: The year, as an integer
:param m: The month, as an integer (optional)
:param d: The day, as an integer (optional)
:param iso_string: A string representing the date in the format YYYYMMDD. Month and day are optional.
"""
if y is not None:
year = min(max(y, datetime.MINYEAR), datetime.MAXYEAR)
month = m if (m is not None and 0 < m <= 12) else 1
day = d if (d is not None and 0 < d <= 31) else 1
elif iso_string is not None:
parsed = parser.isoparse(iso_string)
return datetime.datetime.__new__(cls, parsed.year, parsed.month, parsed.day)
else:
raise TypeError("Must specify a value for year or a date string")

return datetime.datetime.__new__(cls, year, month, day)

@classmethod
def today(cls):
today = datetime.date.today()
return DateWrapper(today.year, today.month, today.day)

def __init__(self, y=None, m=None, d=None, iso_string=None):
if y is not None:
self.y = min(max(y, datetime.MINYEAR), datetime.MAXYEAR)
self.m = m if (m is None or 0 < m <= 12) else 1
self.d = d if (d is None or 0 < d <= 31) else 1
elif iso_string is not None:
# Remove any hyphen separators
iso_string = iso_string.replace("-", "")
length = len(iso_string)

if length < 4:
raise ValueError("Invalid value for year")

self.y = int(iso_string[:4])
self.m = None
self.d = None

# Month and day are optional
if length >= 6:
self.m = int(iso_string[4:6])
if length >= 8:
self.d = int(iso_string[6:8])
else:
raise TypeError("Must specify a value for year or a date string")

def __lt__(self, other):
if self.y != other.y:
return self.y < other.y
elif self.m is None:
return False
else:
if other.m is None:
return True
elif self.m == other.m:
if self.d is None:
return False
else:
if other.d is None:
return True
else:
return self.d < other.d
else:
return self.m < other.m

def __eq__(self, other):
if self.y != other.y:
return False
elif self.m is not None and other.m is not None:
if self.d is not None and other.d is not None:
return self.d == other.d
else:
return self.m == other.m
else:
return self.m == other.m


# Fetch work, including recording relations
def _fetch_work(work_id):
return musicbrainzngs.get_work_by_id(work_id, ['recording-rels'])['work']
Expand Down
147 changes: 147 additions & 0 deletions tests/test_date_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import datetime
import unittest
from beetsplug.date_wrapper import DateWrapper


class DateWrapperTest(unittest.TestCase):
def test_creating_date(self):
result = DateWrapper(2022, 12, 10)
self.assertEqual(2022, result.y)
self.assertEqual(12, result.m)
self.assertEqual(10, result.d)

def test_invalid_date(self):
result = DateWrapper(2022, 0, 10)
self.assertEqual(2022, result.y)
self.assertEqual(1, result.m)
self.assertEqual(10, result.d)
result = DateWrapper(2022, 10, 0)
self.assertEqual(2022, result.y)
self.assertEqual(10, result.m)
self.assertEqual(1, result.d)

# Force year to be within range 1 - 9999
def test_year_zero(self):
result = DateWrapper(0, 12, 10)
self.assertEqual(1, result.y)
self.assertEqual(12, result.m)
self.assertEqual(10, result.d)

def test_year_10000(self):
result = DateWrapper(10000, 12, 10)
self.assertEqual(9999, result.y)
self.assertEqual(12, result.m)
self.assertEqual(10, result.d)

def test_less_than_year(self):
first_date = DateWrapper(2021, 12, 10)
second_date = DateWrapper(2022, 12, 10)
self.assertTrue(first_date < second_date)

def test_less_than_month(self):
first_date = DateWrapper(2022, 11, 10)
second_date = DateWrapper(2022, 12, 10)
self.assertTrue(first_date < second_date)

def test_less_than_day(self):
first_date = DateWrapper(2022, 12, 9)
second_date = DateWrapper(2022, 12, 10)
self.assertTrue(first_date < second_date)

# If a value is None, that date should be bigger
# This means when testing for oldest (smallest) the one with values gets picked
def test_less_than_none_month(self):
first_date = DateWrapper(2022, None, 9)
second_date = DateWrapper(2022, 12, 10)
self.assertFalse(first_date < second_date)

def test_less_than_none_day(self):
first_date = DateWrapper(2022, 12, None)
second_date = DateWrapper(2022, 12, 10)
self.assertFalse(first_date < second_date)

def test_less_than_none_month_day(self):
first_date = DateWrapper(2022, None, None)
second_date = DateWrapper(2022, 1, 1)
self.assertFalse(first_date < second_date)

def test_less_than_none_month_backwards(self):
first_date = DateWrapper(2022, 12, 9)
second_date = DateWrapper(2022, None, 10)
self.assertTrue(first_date < second_date)

def test_less_than_none_day_backwards(self):
first_date = DateWrapper(2022, 12, 10)
second_date = DateWrapper(2022, 12, None)
self.assertTrue(first_date < second_date)

def test_less_than_none_month_day_backwards(self):
first_date = DateWrapper(2022, 1, 1)
second_date = DateWrapper(2022, None, None)
self.assertTrue(first_date < second_date)

def test_equal(self):
first_date = DateWrapper(2022, 12, 10)
second_date = DateWrapper(2022, 12, 10)
self.assertEqual(first_date, first_date)
self.assertEqual(first_date, second_date)

def test_equal_none_month(self):
first_date = DateWrapper(2022, None, 10)
second_date = DateWrapper(2022, 12, 10)
self.assertNotEqual(first_date, second_date)

def test_equal_none_month_backwards(self):
first_date = DateWrapper(2022, 12, 10)
second_date = DateWrapper(2022, None, 10)
self.assertNotEqual(first_date, second_date)

def test_equal_none_months(self):
first_date = DateWrapper(2022, None, 10)
second_date = DateWrapper(2022, None, 10)
self.assertTrue(first_date == second_date)

def test_equal_none_day(self):
first_date = DateWrapper(2022, 12, None)
second_date = DateWrapper(2022, 12, 10)
self.assertNotEqual(first_date, second_date)

def test_equal_none_day_backwards(self):
first_date = DateWrapper(2022, 12, 10)
second_date = DateWrapper(2022, 12, None)
self.assertNotEqual(first_date, second_date)

def test_equal_none_days(self):
first_date = DateWrapper(2022, 12, None)
second_date = DateWrapper(2022, 12, None)
self.assertTrue(first_date == second_date)

def test_isostring(self):
first_date = DateWrapper(iso_string="2022-12-10")
second_date = DateWrapper(2022, 12, 10)
self.assertTrue(first_date == second_date)

def test_isostring_year_month(self):
first_date = DateWrapper(iso_string="2022-12")
second_date = DateWrapper(2022, 12)
self.assertTrue(first_date == second_date)

def test_isostring_year(self):
first_date = DateWrapper(iso_string="2022")
second_date = DateWrapper(2022)
self.assertTrue(first_date == second_date)

def test_isostring_empty(self):
with self.assertRaises(ValueError):
DateWrapper(iso_string="")

def test_no_year_no_isostring(self):
with self.assertRaises(TypeError):
DateWrapper()

def test_today(self):
first_date = DateWrapper.today()
today = datetime.datetime.today()
second_date = DateWrapper(today.year, today.month, today.day)

self.assertEqual(first_date, second_date)
Loading

0 comments on commit d88d328

Please sign in to comment.