diff --git a/_tests/test_dated_entries_group.py b/_tests/test_dated_entries_group.py index bff525a..b6d475d 100644 --- a/_tests/test_dated_entries_group.py +++ b/_tests/test_dated_entries_group.py @@ -1,8 +1,8 @@ from unittest import TestCase -import dated_entries_group from dated_entries_group import DatedEntriesGroup, InvalidDateError, \ - DatedEntryMissingError, TriedCreatingDuplicateDatedEntryError + DatedEntryMissingError, TriedCreatingDuplicateDatedEntryError, \ + IncompleteDataRow class TestDate(TestCase): @@ -43,6 +43,7 @@ def test_creating_entries_from_row(self): "note": "" } ) + # This would be a duplicate from the one already created with self.assertRaises(TriedCreatingDuplicateDatedEntryError): my_date.create_dated_entry_from_row( { @@ -53,6 +54,17 @@ def test_creating_entries_from_row(self): "note": "" } ) + # This lacks the minimum required keys - time and mood - to function correctly + with self.assertRaises(IncompleteDataRow): + my_date.create_dated_entry_from_row( + { + "time": "5:00 PM", + "mood": "", + "activities": "", + "note_title": "", + "note": "" + } + ) def test_create_dated_entries_groups(self): """ diff --git a/_tests/test_mood.py b/_tests/test_mood.py new file mode 100644 index 0000000..1999cf1 --- /dev/null +++ b/_tests/test_mood.py @@ -0,0 +1,157 @@ +import logging +from unittest import TestCase + +from entry.mood import Moodverse, MoodGroup, Mood, MoodNotFoundError + + +# noinspection SpellCheckingInspection +class TestMoodverse(TestCase): + def test_default_moodverse_no_customisation(self): + my_default_moodverse = Moodverse() + self.assertTrue(isinstance(my_default_moodverse["rad"], MoodGroup)) + self.assertTrue(isinstance(my_default_moodverse["good"], MoodGroup)) + self.assertTrue(isinstance(my_default_moodverse["neutral"], MoodGroup)) + self.assertTrue(isinstance(my_default_moodverse["bad"], MoodGroup)) + self.assertTrue(isinstance(my_default_moodverse["awful"], MoodGroup)) + + self.assertEqual(my_default_moodverse["rad"], ["rad"]) + self.assertEqual(my_default_moodverse["good"], ["good"]) + self.assertEqual(my_default_moodverse["neutral"], ["neutral"]) + self.assertEqual(my_default_moodverse["bad"], ["bad"]) + self.assertEqual(my_default_moodverse["awful"], ["awful"]) + + # this is just so I can test whether my __eq__ function overload correctly skips this + self.assertNotEqual(my_default_moodverse["awful"], MoodGroup("awful")) + + # These comparisons should be falsy because the array has more moods than the default mood set initialised + self.assertNotEqual(my_default_moodverse["rad"], ["rad", "amazing"]) + self.assertNotEqual(my_default_moodverse["awful"], ["awful", "miserable"]) + + # This comparison should be falsy because it does not contain the default mood set initialised + # └── known moods of 'neutral' group + # └── neutral <-- from standard + # And we're basically saying, "In neutral group there should only be a 'meh' mood" + self.assertNotEqual(my_default_moodverse["neutral"], ["meh"]) + + def test_loading_valid_moods_into_moodverse(self): + # These moods are self-sufficient, because even if standard mood set didn't exist, they satisfy all requirements + ok_moods_loaded_from_json = { + "rad": + ["rad", "amazing"], + "good": + ["good", "nice"], + "neutral": + ["neutral", "ok", "fine"], + "bad": + ["bad"], + "awful": + ["awful", "miserable"] + } + my_moodverse = Moodverse(ok_moods_loaded_from_json) + self.assertTrue(isinstance(my_moodverse["rad"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["good"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["neutral"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["bad"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["awful"], MoodGroup)) + + self.assertEqual(my_moodverse["rad"], ["rad", "amazing"]) + self.assertEqual(my_moodverse["good"], ["good", "nice"]) + self.assertEqual(my_moodverse["neutral"], ["neutral", "ok", "fine"]) + self.assertEqual(my_moodverse["bad"], ["bad"]) + self.assertEqual(my_moodverse["awful"], ["awful", "miserable"]) + + def test_loading_semi_valid_moods_into_moodverse(self): + # This mood set isn't self-sufficient, but still valid, because it has all the required "groups". + # Standard mood set is used here to cover for missing moods + # so, when: + semi_ok_moods_loaded_from_json = { + "rad": + ["amazing"], # lacks rad + "good": + ["nice"], # lacks good + "neutral": + ["ok", "fine"], # lacks neutral + "bad": + ["bad"], # OK + "awful": + ["miserable"] # lacks awful + } + + # then I can still use my moodverse, because standard set filled the blanks like so: + # . + # ├── known moods of 'rad' group + # │ └── rad <-- from standard + # │ └── amazing + # ├── known moods of 'great' group + # │ └── great <-- from standard + # │ └── nice + # ├── known moods of 'neutral' group + # │ └── neutral <-- from standard + # │ └── ok + # │ └── fine + # ├── known moods of 'bad' group + # │ └── bad + # └── known moods of 'awful' group + # └── awful <0 from standard + # └── miserable + + my_moodverse = Moodverse(semi_ok_moods_loaded_from_json) + self.assertTrue(isinstance(my_moodverse["rad"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["good"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["neutral"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["bad"], MoodGroup)) + self.assertTrue(isinstance(my_moodverse["awful"], MoodGroup)) + + # responses should be identical to the ones in previous test, because standard mood filled the blanks + self.assertEqual(my_moodverse["rad"], ["rad", "amazing"]) + self.assertEqual(my_moodverse["good"], ["good", "nice"]) + self.assertEqual(my_moodverse["neutral"], ["neutral", "ok", "fine"]) + self.assertEqual(my_moodverse["bad"], ["bad"]) + self.assertEqual(my_moodverse["awful"], ["awful", "miserable"]) + + def test_loading_invalid_moods_into_moodverse(self): + bad_moods_loaded_from_json = { + "rad": + [""], # lacks rad + "good": + [""], # lacks good + "neutral": + [""], # lacks neutral + "bad": + [""], # lacks bad + "awful": + [""] # lacks awful + } + + with self.assertLogs(logging.getLogger(), level=logging.WARNING): + Moodverse(bad_moods_loaded_from_json) + + bad_moods_loaded_from_json = { + "rad": + ["rad"], + "good": + ["good"], + "neutral": + ["neutral"], + "bad": + ["bed"], # lacks bad + "awful": + [""] # lacks awful + } + + with self.assertLogs(logging.getLogger(), level=logging.WARNING): + Moodverse(bad_moods_loaded_from_json) + + bad_moods_loaded_from_json = { + "rad": + ["rad"], + "good": + ["good"], + "neutral": + ["neutral"] + # lacks bed + # lacks awful + } + + with self.assertLogs(logging.getLogger(), level=logging.WARNING): + Moodverse(bad_moods_loaded_from_json) diff --git a/dated_entry.py b/dated_entry.py index 3fdce94..d599c7f 100644 --- a/dated_entry.py +++ b/dated_entry.py @@ -182,19 +182,7 @@ def __init__(self, except IsNotTimeError: raise ValueError("Cannot create object without valid Time attribute") - # Mood - if len(mood) == 0 or mood is None: - raise ValueError("Cannot create object without valid Mood attribute") - else: - if known_moods is True: - mood_is_in_dictionary = False - for i, (_, this_group) in enumerate(known_moods.items()): - if mood in this_group: - mood_is_in_dictionary = True - break - if not mood_is_in_dictionary: - self.__logger.warning(ErrorMsg.print(ErrorMsg.INVALID_MOOD, mood)) - # Assign it anyway. Warning is enough. + self.__mood = mood # Processing other, optional properties diff --git a/entry/mood.py b/entry/mood.py new file mode 100644 index 0000000..94e3e5a --- /dev/null +++ b/entry/mood.py @@ -0,0 +1,163 @@ +import logging +from typing import List + +import errors +import utils + + +class ErrorMsg(errors.ErrorMsgBase): + MOOD_GROUP_NOT_FOUND = "Expected to find {} mood group, but the key is missing from the dictionary." + STANDARD_MOODS_USED = "Problem parsing custom moods file. Standard mood set - i.e. {} - will be used." + SKIPPED_INVALID_MOOD = "Skipping \'{}\' as it is not a valid mood." + + +class EmptyValue(utils.CustomException): + """Attribute cannot be set to empty.""" + + +class MoodNotFoundError(utils.CustomException): + """The mood could not be found neither in the standard mood set nor the custom one.""" + + +# noinspection SpellCheckingInspection +class Moodverse: + def __init__(self, moods_to_process: dict[str, List[str]] = None): + self.__logger = logging.getLogger(self.__class__.__name__) + # DEFAULT PART OF INIT + # -------------------- + # Build a minimal-viable mood set with these five mood groups + # . + # ├── known moods of 'rad' group + # │ └── rad + # ├── known moods of 'great' group + # │ └── great + # ├── known moods of 'neutral' group + # │ └── neutral + # ├── known moods of 'bad' group + # │ └── bad + # └── known moods of 'awful' group + # └── awful + + self.__mood_set: dict[str, MoodGroup] = {} + + for group_name in ["rad", "good", "neutral", "bad", "awful"]: + # Add the new group to the dict + self.__mood_set[group_name] = MoodGroup(group_name) + # Ask it to create its main mood (e.g. 'rad' for 'rad'-group) + self.__mood_set[group_name].create_mood() + + # We can stop here and be content with our "default" / "standard" mood-set if user did not pass a custom one + # CUSTOM PART OF INIT + # -------------------- + if moods_to_process is not None: + try: + self.__create_moods(moods_to_process) + except MoodNotFoundError: + msg = ErrorMsg.print(ErrorMsg.STANDARD_MOODS_USED, ', '.join(self.__mood_set.keys())) + self.__logger.warning(msg) + + def __create_moods(self, moods_to_process: dict[str, List[str]]): + # Then expand the minimal-viable mood set from Moodverse __init__ + # . + # ├── known moods of 'rad' group + # │ └── rad + # │ └── (add more...) + # ├── known moods of 'great' group + # │ └── great + # │ └── (add more...) + # ├── known moods of 'neutral' group + # │ └── neutral + # │ └── (add more...) + # ├── known moods of 'bad' group + # │ └── bad + # │ └── (add more...) + # └── known moods of 'awful' group + # └── awful + # └── (add more...) + + # Use keys from standard_mood_set to navigate through the passed dictionary and add their unique moods over + # The side effect is that keys in the passed dictionary which do not appear in standard mood set are skipped + + # e.g. I'm expecting a "rad" group to be in the dict + for expected_mood_group in self.__mood_set.keys(): + expected_mood_group: str + try: + # Leap of faith - there must be a "rad" group, otherwise there's no point in continuing + mood_group_to_transfer = moods_to_process[expected_mood_group] + except KeyError: + msg = ErrorMsg.print(ErrorMsg.MOOD_GROUP_NOT_FOUND, expected_mood_group) + self.__logger.warning(msg) + raise MoodNotFoundError(msg) + # Go through each mood in this mood group - e.g. rad - and transfer them in the form of Mood objects + else: + for mood in mood_group_to_transfer: + self.__mood_set[expected_mood_group].create_mood(mood) + + def __getitem__(self, item: str): + return self.__mood_set[item] + + @property + def known_mood_groups(self): + return self.__mood_set + + +class AbstractMood: + def __init__(self, value): + if not value: + raise EmptyValue + elif isinstance(value, str) is False: + raise utils.WrongValueError + else: + self.__name = value + + @property + def name(self): + return self.__name + + def __str__(self): + return self.__name + + +class MoodGroup(AbstractMood): + def __init__(self, name_of_the_mood_group: str): + self.__logger = logging.getLogger(self.__class__.__name__) + self.__known_moods: dict[str, Mood] = {} + + super().__init__(name_of_the_mood_group) + + def create_mood(self, name: str = None): + # e.g. if no argument is passed, MoodGroup "rad" will create a Mood "rad" + # it's just a shorthand so that you don't have to write MoodGroup("rad").create_mood("rad") + # └── known moods of 'rad' group + # └── rad + final_name = self.name if name is None else name + try: + ref = Mood(final_name) + except (EmptyValue, utils.WrongValueError): + self.__logger.warning(ErrorMsg.print(ErrorMsg.SKIPPED_INVALID_MOOD, final_name)) + # all went ok + else: + self.__known_moods[final_name] = ref + return ref + + # TODO: possibly could do funky stuff with multiple inheritance - this method could come from Moodverse + @property + def known_moods(self): + return self.__known_moods + + # TODO: possibly could do funky stuff with multiple inheritance - this method could come from Moodverse + def __getitem__(self, item: str): + return self.__known_moods[item] + + def __eq__(self, other): + # Used to compare instance of this class to ["mood", "mood", "mood"] and check if they contain the same moods + if isinstance(other, list) and all(isinstance(item, str) for item in other): + return [str(obj) for obj in self.known_moods] == other + else: + # Call the superclass' __eq__ for any other comparison + return super().__eq__(other) + + +class Mood(AbstractMood): + def __init__(self, value: str): + super().__init__(value) diff --git a/utils.py b/utils.py index 594f1b0..a0a71f4 100755 --- a/utils.py +++ b/utils.py @@ -35,6 +35,10 @@ def __init__(self, message=None): self.message = message +class WrongValueError(CustomException): + """Passed an unexpected value""" + + def slugify(text: str, taggify: bool): # noinspection SpellCheckingInspection """