Improved overall type annotations. WIP. Fixed a logical error in _ConfigLib._set_behavior()
This commit is contained in:
		
							parent
							
								
									d1d0e7c969
								
							
						
					
					
						commit
						bc5d07925f
					
				
					 1 changed files with 42 additions and 37 deletions
				
			
		| 
						 | 
				
			
			@ -1,9 +1,10 @@
 | 
			
		|||
from sys import exit
 | 
			
		||||
from os import chdir, getenv, system
 | 
			
		||||
from typing import List
 | 
			
		||||
from typing import List, Dict, Union
 | 
			
		||||
import logging
 | 
			
		||||
import tomllib
 | 
			
		||||
from datetime import datetime, time
 | 
			
		||||
from apscheduler.schedulers.background import BackgroundScheduler, BlockingScheduler
 | 
			
		||||
from apscheduler.triggers.cron import CronTrigger
 | 
			
		||||
 | 
			
		||||
# Setup Logging. NOTE: Declaration as a global variable is necessary to ensure correct functionality across multiple modules.
 | 
			
		||||
| 
						 | 
				
			
			@ -14,15 +15,15 @@ class ConfigError(Exception):
 | 
			
		|||
    pass
 | 
			
		||||
 | 
			
		||||
class _ConfigLib:
 | 
			
		||||
    # Initializes the most important config values. TODO: Add handling for the empty config case
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.config_file: dict = self._initialize_config() # Full config
 | 
			
		||||
    # Initializes the most important config values. TODO: Add handling for the empty config edge case
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        self.config_file: Dict[str, Dict[str, Union[int, str, bool, List[str]]]] = self._initialize_config() # Full config
 | 
			
		||||
        # Dictionaries
 | 
			
		||||
        self.config_general: dict = self.config_file["general"]
 | 
			
		||||
        self.config_changing_times: dict = self.config_file["changing_times"]
 | 
			
		||||
        self.config_general: Dict[str, Union[int, str, bool, List[str]]] = self.config_file["general"]
 | 
			
		||||
        self.config_changing_times: Dict[str, str] = self.config_file["changing_times"]
 | 
			
		||||
        # Values in Dicts
 | 
			
		||||
        self.config_wallpaper_sets_enabled: bool = self.config_general["enable_wallpaper_sets"]
 | 
			
		||||
        self.config_used_sets: list = self.config_general["used_sets"]
 | 
			
		||||
        self.config_used_sets: List[str] = self.config_general["used_sets"]
 | 
			
		||||
        self.config_wallpapers_per_set: int = self.config_general["wallpapers_per_set"]
 | 
			
		||||
        self.config_total_changing_times: int = len(self.config_changing_times)
 | 
			
		||||
        self.config_log_level: str = self.config_general.get("loglevel", "INFO").upper()
 | 
			
		||||
| 
						 | 
				
			
			@ -34,9 +35,9 @@ class _ConfigLib:
 | 
			
		|||
            self.config_notify: bool = False
 | 
			
		||||
            logger.warning("'notify' is not set in dictionary general in the config file, defaulting to 'false'.")
 | 
			
		||||
        try:
 | 
			
		||||
            self.config_systray = self.config_general["systray"]
 | 
			
		||||
            self.config_systray: bool = self.config_general["systray"]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            self.config_systray = True
 | 
			
		||||
            self.config_systray: bool = True
 | 
			
		||||
            logger.warning("'systray' is not set in the dictionary general in the config file, defaulting to 'true'.")
 | 
			
		||||
 | 
			
		||||
        # Setup logging
 | 
			
		||||
| 
						 | 
				
			
			@ -46,10 +47,10 @@ class _ConfigLib:
 | 
			
		|||
            self._initialize_systray()
 | 
			
		||||
 | 
			
		||||
    # Read config. TODO: Add error handling for the config not found case.
 | 
			
		||||
    def _initialize_config(self) -> dict:
 | 
			
		||||
    def _initialize_config(self) -> Dict[str, Dict[str, Union[int, str, bool, List[str]]]]:
 | 
			
		||||
        chdir(str(getenv("HOME")) + "/.config/wallman/")
 | 
			
		||||
        with open("wallman.toml", "rb") as config_file:
 | 
			
		||||
            data = tomllib.load(config_file)
 | 
			
		||||
            data: Dict[str, Dict[str, Union[int, str, bool, List[str]]]] = tomllib.load(config_file)
 | 
			
		||||
            return data
 | 
			
		||||
 | 
			
		||||
    # HACK on this to avoid double importing of wallman_systray due to variable scope. Idea: Global variable or Variable that is inherited?
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +64,7 @@ class _ConfigLib:
 | 
			
		|||
        global logging
 | 
			
		||||
        global logger
 | 
			
		||||
        chdir("/var/log/wallman/")
 | 
			
		||||
        numeric_level = getattr(logging, self.config_log_level, logging.INFO)
 | 
			
		||||
        numeric_level: int = getattr(logging, self.config_log_level, logging.INFO)
 | 
			
		||||
        logger.setLevel(numeric_level)
 | 
			
		||||
        logging.basicConfig(filename="wallman.log", encoding="utf-8", level=numeric_level)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,13 +74,13 @@ class _ConfigLib:
 | 
			
		|||
        try:
 | 
			
		||||
            self.config_general.get("behavior")
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            logger.info("There is no wallpaper behavior specified in general, defaulting to fill...")
 | 
			
		||||
            print("There is no wallpaper behavior specified in general, defaulting to fill...")
 | 
			
		||||
            logger.error("There is no wallpaper behavior specified in general, defaulting to fill...")
 | 
			
		||||
            print("ERROR: There is no wallpaper behavior specified in general, defaulting to fill...")
 | 
			
		||||
 | 
			
		||||
        human_behaviors: List[str] = ["pure", "tile", "center", "fill", "max", "scale"]
 | 
			
		||||
        machine_behaviors: List[str] = ["--bg", "--bg-tile", "--bg-center", "--bg-fill", "--bg-max", "--bg-scale"]
 | 
			
		||||
        behavior: str = self.config_general.get("behavior", "--bg-fill").lower()
 | 
			
		||||
        if behavior not in human_behaviors or behavior not in machine_behaviors:
 | 
			
		||||
        if behavior not in human_behaviors and behavior not in machine_behaviors:
 | 
			
		||||
            logging.error(f"The value provided for behaviors, {behavior}, is not valid. Defaulting to fill...")
 | 
			
		||||
            print(f"ERROR: The value provided for behaviors, {behavior}, is not valid. Defaulting to --bg-fill...")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +103,7 @@ class _ConfigLib:
 | 
			
		|||
        return behavior
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def _set_fallback_wallpaper(self):
 | 
			
		||||
    def _set_fallback_wallpaper(self) -> None:
 | 
			
		||||
        if self.config_general["fallback_wallpaper"]:
 | 
			
		||||
            system(f"feh {self.config_behavior} --no-fehbg {self.config_general['fallback_wallpaper']}")
 | 
			
		||||
            logger.info("The fallback Wallpaper has been set.")
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +116,7 @@ class ConfigValidity(_ConfigLib):
 | 
			
		|||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
 | 
			
		||||
    def _check_fallback_wallpaper(self):
 | 
			
		||||
    def _check_fallback_wallpaper(self) -> bool:
 | 
			
		||||
        if self.config_general["fallback_wallpaper"]:
 | 
			
		||||
            logger.debug("A fallback wallpaper has been defined.")
 | 
			
		||||
            return True
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +156,7 @@ class ConfigValidity(_ConfigLib):
 | 
			
		|||
            logger.debug("A valid amount of options has been provided in general")
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
    def _check_wallpaper_dicts(self):
 | 
			
		||||
    def _check_wallpaper_dicts(self) -> bool:
 | 
			
		||||
        # This block checks if a dictionary for each wallpaper set exists
 | 
			
		||||
        for wallpaper_set in self.config_used_sets:
 | 
			
		||||
            if wallpaper_set in self.config_file:
 | 
			
		||||
| 
						 | 
				
			
			@ -171,8 +172,9 @@ class ConfigValidity(_ConfigLib):
 | 
			
		|||
                except ConfigError:
 | 
			
		||||
                    logger.critical(f"No dictionary {wallpaper_set} has been found in the config exiting...")
 | 
			
		||||
                    raise ConfigError(f"The dictionary {wallpaper_set} has not been found in the config, exiting...")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def _check_wallpaper_amount(self):
 | 
			
		||||
    def _check_wallpaper_amount(self) -> bool:
 | 
			
		||||
        # This block checks if if each wallpaper set dictionary provides enough wallpapers to satisfy wallpapers_per_set
 | 
			
		||||
        for wallpaper_set in self.config_used_sets:
 | 
			
		||||
            if len(self.config_file[wallpaper_set]) == self.config_wallpapers_per_set:
 | 
			
		||||
| 
						 | 
				
			
			@ -187,6 +189,7 @@ class ConfigValidity(_ConfigLib):
 | 
			
		|||
                except ConfigError:
 | 
			
		||||
                    logger.critical(f"Dictionary {wallpaper_set} does not have sufficient entries, exciting...")
 | 
			
		||||
                    raise ConfigError(f"Dictionary {wallpaper_set} does not have the correct amount of entries, exciting...")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def validate_config(self) -> bool:
 | 
			
		||||
        # NOTE: Consider changing this to exit(-1)
 | 
			
		||||
| 
						 | 
				
			
			@ -207,34 +210,35 @@ class ConfigValidity(_ConfigLib):
 | 
			
		|||
# TODO: Improve modularity. See notes inside the class for more details.
 | 
			
		||||
# TODO: Ensure functionality and if needed add handling for the 1 wallpaper per set case.
 | 
			
		||||
class WallpaperLogic(_ConfigLib):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
    def __init__(self) -> None:
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        # NOTE: This looks a bit ugly. Consider pros and cons of adding this into _ConfigLib
 | 
			
		||||
        self.chosen_wallpaper_set = False
 | 
			
		||||
        self.chosen_wallpaper_set: Union[bool, List[str]] = False
 | 
			
		||||
 | 
			
		||||
    # NOTE: This function could be in a different file because it's not needed in the case only 1 wallpaper per set is needed.
 | 
			
		||||
    # Returns a list of a split string that contains a changing time from the config file
 | 
			
		||||
    def _clean_times(self, desired_time) -> list:
 | 
			
		||||
        unclean_times = list(self.config_changing_times.values())[desired_time]
 | 
			
		||||
    def _clean_times(self, desired_time: int) -> List[str]:
 | 
			
		||||
        unclean_times: str = list(self.config_changing_times.values())[desired_time]
 | 
			
		||||
        return unclean_times.split(":")
 | 
			
		||||
 | 
			
		||||
    # NOTE: This could be in a different file because it's not needed in the "Only one wallpaper set" case.
 | 
			
		||||
    # FIXME: Use a TypedDict here
 | 
			
		||||
    def _choose_wallpaper_set(self) -> None:
 | 
			
		||||
        from random import choice as choose_from
 | 
			
		||||
        self.chosen_wallpaper_set = choose_from(self.config_used_sets)
 | 
			
		||||
        self.wallpaper_list = list(self.config_file[self.chosen_wallpaper_set].values())
 | 
			
		||||
        self.wallpaper_list: List[str] = list(self.config_file[self.chosen_wallpaper_set].values())
 | 
			
		||||
        logger.debug(f"Chose wallpaper set {self.chosen_wallpaper_set}")
 | 
			
		||||
 | 
			
		||||
    # NOTE: Same as _clean_times()
 | 
			
		||||
    # Verify if a given time is in a given range
 | 
			
		||||
    def _time_in_range(self, start, end, x) -> bool:
 | 
			
		||||
    def _time_in_range(self, start: time, end: time, x: time) -> bool:
 | 
			
		||||
        if start <= end:
 | 
			
		||||
            return start <= x <= end
 | 
			
		||||
        else:
 | 
			
		||||
            return start <= x or x < end
 | 
			
		||||
 | 
			
		||||
    # NOTE: Potentially add handling for this to be also usable for notify_user and add logging if notify_user fails. Consider adding an argument that is where it's called from and handle accordingly.
 | 
			
		||||
    def _check_system_exitcode(self, code) -> bool:
 | 
			
		||||
    def _check_system_exitcode(self, code: int) -> bool:
 | 
			
		||||
        if code != 0:
 | 
			
		||||
            try:
 | 
			
		||||
                self._set_fallback_wallpaper()
 | 
			
		||||
| 
						 | 
				
			
			@ -264,14 +268,14 @@ class WallpaperLogic(_ConfigLib):
 | 
			
		|||
            self._choose_wallpaper_set()
 | 
			
		||||
        for time_range in range(self.config_total_changing_times - 1):
 | 
			
		||||
            self.current_time_range = time_range # Store current time for better debugging output
 | 
			
		||||
            clean_time = self._clean_times(time_range)
 | 
			
		||||
            clean_time_two = self._clean_times(time_range + 1)
 | 
			
		||||
            clean_time: List[str] = self._clean_times(time_range)
 | 
			
		||||
            clean_time_two: List[str] = self._clean_times(time_range + 1)
 | 
			
		||||
            # HACK on this to make it more readable. This function call is way too long. Consider storing these in a bunch of temporary variables, though keep function length in mind.
 | 
			
		||||
            # HACK on this to see if this logic can be simplified. It's very ugly to check it that way.
 | 
			
		||||
            # Check if the current time is between a given and the following changing time and if so, set that wallpaper. If not, keep trying.
 | 
			
		||||
            if self._time_in_range(time(int(clean_time[0]), int(clean_time[1]), int(clean_time[2])), time(int(clean_time_two[0]), int(clean_time_two[1]), int(clean_time_two[2])), datetime.now().time()):
 | 
			
		||||
                exitcode = system(f"feh {self.config_behavior} --no-fehbg --quiet {self.wallpaper_list[time_range]}")
 | 
			
		||||
                has_wallpaper_been_set = self._check_system_exitcode(exitcode)
 | 
			
		||||
                exitcode: int = system(f"feh {self.config_behavior} --no-fehbg --quiet {self.wallpaper_list[time_range]}")
 | 
			
		||||
                has_wallpaper_been_set: bool = self._check_system_exitcode(exitcode)
 | 
			
		||||
                # TODO: Add this check to _notify_user.
 | 
			
		||||
                if self.config_notify:
 | 
			
		||||
                    self._notify_user()
 | 
			
		||||
| 
						 | 
				
			
			@ -279,15 +283,15 @@ class WallpaperLogic(_ConfigLib):
 | 
			
		|||
            else:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
        exitcode = system(f"feh {self.config_behavior} --no-fehbg {self.wallpaper_list[-1]}")
 | 
			
		||||
        has_wallpaper_been_set = self._check_system_exitcode(exitcode)
 | 
			
		||||
        exitcode: int = system(f"feh {self.config_behavior} --no-fehbg {self.wallpaper_list[-1]}")
 | 
			
		||||
        has_wallpaper_been_set: bool = self._check_system_exitcode(exitcode)
 | 
			
		||||
        if self.config_notify:
 | 
			
		||||
            self._notify_user()
 | 
			
		||||
        return has_wallpaper_been_set
 | 
			
		||||
 | 
			
		||||
    # NOTE: Consider avoiding nested functions.
 | 
			
		||||
    def schedule_wallpapers(self):
 | 
			
		||||
        def _schedule_background_wallpapers():
 | 
			
		||||
    def schedule_wallpapers(self) -> None:
 | 
			
		||||
        def _schedule_background_wallpapers() -> BackgroundScheduler:
 | 
			
		||||
            from apscheduler.schedulers.background import BackgroundScheduler
 | 
			
		||||
            scheduler = BackgroundScheduler()
 | 
			
		||||
            # Create a scheduled job for every changing time
 | 
			
		||||
| 
						 | 
				
			
			@ -299,7 +303,7 @@ class WallpaperLogic(_ConfigLib):
 | 
			
		|||
            logger.info("The background scheduler has been started.")
 | 
			
		||||
            return scheduler
 | 
			
		||||
 | 
			
		||||
        def _schedule_blocking_wallpapers():
 | 
			
		||||
        def _schedule_blocking_wallpapers() -> None:
 | 
			
		||||
            from apscheduler.schedulers.blocking import BlockingScheduler
 | 
			
		||||
            scheduler = BlockingScheduler()
 | 
			
		||||
            # Create a scheduled job for every changing time
 | 
			
		||||
| 
						 | 
				
			
			@ -314,8 +318,8 @@ class WallpaperLogic(_ConfigLib):
 | 
			
		|||
            # NOTE: The wallman_systray impomrt should be handled differently. See the note in Config_Validity.
 | 
			
		||||
            import wallman_systray as systray
 | 
			
		||||
            from functools import partial
 | 
			
		||||
            scheduler = _schedule_background_wallpapers()
 | 
			
		||||
            menu = systray.Menu (
 | 
			
		||||
            scheduler: BackgroundScheduler = _schedule_background_wallpapers()
 | 
			
		||||
            menu: systray.Menu = systray.Menu (
 | 
			
		||||
                systray.item("Re-Set Wallpaper", partial(systray.set_wallpaper_again, wallpaper_setter=self.set_wallpaper_by_time)),
 | 
			
		||||
                systray.item("Reroll Wallpapers", partial(systray.reroll_wallpapers, wallpaper_chooser=self._choose_wallpaper_set, wallpaper_setter=self.set_wallpaper_by_time)),
 | 
			
		||||
                systray.item("Quit", partial(systray.on_quit, shutdown_scheduler=scheduler.shutdown))
 | 
			
		||||
| 
						 | 
				
			
			@ -323,4 +327,5 @@ class WallpaperLogic(_ConfigLib):
 | 
			
		|||
            icon = systray.Icon("wallman_icon", systray.icon_image, "My Tray Icon", menu)
 | 
			
		||||
            icon.run()
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
            _schedule_blocking_wallpapers()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue