Improved overall type annotations. WIP. Fixed a logical error in _ConfigLib._set_behavior()

This commit is contained in:
Emma Nora Theuer 2024-12-30 03:40:50 +01:00
parent d1d0e7c969
commit bc5d07925f

View file

@ -1,9 +1,10 @@
from sys import exit from sys import exit
from os import chdir, getenv, system from os import chdir, getenv, system
from typing import List from typing import List, Dict, Union
import logging import logging
import tomllib import tomllib
from datetime import datetime, time from datetime import datetime, time
from apscheduler.schedulers.background import BackgroundScheduler, BlockingScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
# Setup Logging. NOTE: Declaration as a global variable is necessary to ensure correct functionality across multiple modules. # 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 pass
class _ConfigLib: class _ConfigLib:
# Initializes the most important config values. TODO: Add handling for the empty config case # Initializes the most important config values. TODO: Add handling for the empty config edge case
def __init__(self): def __init__(self) -> None:
self.config_file: dict = self._initialize_config() # Full config self.config_file: Dict[str, Dict[str, Union[int, str, bool, List[str]]]] = self._initialize_config() # Full config
# Dictionaries # Dictionaries
self.config_general: dict = self.config_file["general"] self.config_general: Dict[str, Union[int, str, bool, List[str]]] = self.config_file["general"]
self.config_changing_times: dict = self.config_file["changing_times"] self.config_changing_times: Dict[str, str] = self.config_file["changing_times"]
# Values in Dicts # Values in Dicts
self.config_wallpaper_sets_enabled: bool = self.config_general["enable_wallpaper_sets"] 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_wallpapers_per_set: int = self.config_general["wallpapers_per_set"]
self.config_total_changing_times: int = len(self.config_changing_times) self.config_total_changing_times: int = len(self.config_changing_times)
self.config_log_level: str = self.config_general.get("loglevel", "INFO").upper() self.config_log_level: str = self.config_general.get("loglevel", "INFO").upper()
@ -34,9 +35,9 @@ class _ConfigLib:
self.config_notify: bool = False self.config_notify: bool = False
logger.warning("'notify' is not set in dictionary general in the config file, defaulting to 'false'.") logger.warning("'notify' is not set in dictionary general in the config file, defaulting to 'false'.")
try: try:
self.config_systray = self.config_general["systray"] self.config_systray: bool = self.config_general["systray"]
except KeyError: 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'.") logger.warning("'systray' is not set in the dictionary general in the config file, defaulting to 'true'.")
# Setup logging # Setup logging
@ -46,10 +47,10 @@ class _ConfigLib:
self._initialize_systray() self._initialize_systray()
# Read config. TODO: Add error handling for the config not found case. # 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/") chdir(str(getenv("HOME")) + "/.config/wallman/")
with open("wallman.toml", "rb") as config_file: 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 return data
# HACK on this to avoid double importing of wallman_systray due to variable scope. Idea: Global variable or Variable that is inherited? # 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 logging
global logger global logger
chdir("/var/log/wallman/") 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) logger.setLevel(numeric_level)
logging.basicConfig(filename="wallman.log", encoding="utf-8", level=numeric_level) logging.basicConfig(filename="wallman.log", encoding="utf-8", level=numeric_level)
@ -73,13 +74,13 @@ class _ConfigLib:
try: try:
self.config_general.get("behavior") self.config_general.get("behavior")
except KeyError: except KeyError:
logger.info("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("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"] 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"] 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() 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...") 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...") print(f"ERROR: The value provided for behaviors, {behavior}, is not valid. Defaulting to --bg-fill...")
@ -102,7 +103,7 @@ class _ConfigLib:
return behavior return behavior
def _set_fallback_wallpaper(self): def _set_fallback_wallpaper(self) -> None:
if self.config_general["fallback_wallpaper"]: if self.config_general["fallback_wallpaper"]:
system(f"feh {self.config_behavior} --no-fehbg {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.") logger.info("The fallback Wallpaper has been set.")
@ -115,7 +116,7 @@ class ConfigValidity(_ConfigLib):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def _check_fallback_wallpaper(self): def _check_fallback_wallpaper(self) -> bool:
if self.config_general["fallback_wallpaper"]: if self.config_general["fallback_wallpaper"]:
logger.debug("A fallback wallpaper has been defined.") logger.debug("A fallback wallpaper has been defined.")
return True return True
@ -155,7 +156,7 @@ class ConfigValidity(_ConfigLib):
logger.debug("A valid amount of options has been provided in general") logger.debug("A valid amount of options has been provided in general")
return True return True
def _check_wallpaper_dicts(self): def _check_wallpaper_dicts(self) -> bool:
# This block checks if a dictionary for each wallpaper set exists # This block checks if a dictionary for each wallpaper set exists
for wallpaper_set in self.config_used_sets: for wallpaper_set in self.config_used_sets:
if wallpaper_set in self.config_file: if wallpaper_set in self.config_file:
@ -171,8 +172,9 @@ class ConfigValidity(_ConfigLib):
except ConfigError: except ConfigError:
logger.critical(f"No dictionary {wallpaper_set} has been found in the config exiting...") 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...") 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 # 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: for wallpaper_set in self.config_used_sets:
if len(self.config_file[wallpaper_set]) == self.config_wallpapers_per_set: if len(self.config_file[wallpaper_set]) == self.config_wallpapers_per_set:
@ -187,6 +189,7 @@ class ConfigValidity(_ConfigLib):
except ConfigError: except ConfigError:
logger.critical(f"Dictionary {wallpaper_set} does not have sufficient entries, exciting...") 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...") raise ConfigError(f"Dictionary {wallpaper_set} does not have the correct amount of entries, exciting...")
return False
def validate_config(self) -> bool: def validate_config(self) -> bool:
# NOTE: Consider changing this to exit(-1) # 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: 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. # TODO: Ensure functionality and if needed add handling for the 1 wallpaper per set case.
class WallpaperLogic(_ConfigLib): class WallpaperLogic(_ConfigLib):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
# NOTE: This looks a bit ugly. Consider pros and cons of adding this into _ConfigLib # 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. # 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 # Returns a list of a split string that contains a changing time from the config file
def _clean_times(self, desired_time) -> list: def _clean_times(self, desired_time: int) -> List[str]:
unclean_times = list(self.config_changing_times.values())[desired_time] unclean_times: str = list(self.config_changing_times.values())[desired_time]
return unclean_times.split(":") return unclean_times.split(":")
# NOTE: This could be in a different file because it's not needed in the "Only one wallpaper set" case. # 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: def _choose_wallpaper_set(self) -> None:
from random import choice as choose_from from random import choice as choose_from
self.chosen_wallpaper_set = choose_from(self.config_used_sets) 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}") logger.debug(f"Chose wallpaper set {self.chosen_wallpaper_set}")
# NOTE: Same as _clean_times() # NOTE: Same as _clean_times()
# Verify if a given time is in a given range # 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: if start <= end:
return start <= x <= end return start <= x <= end
else: else:
return start <= x or x < end 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. # 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: if code != 0:
try: try:
self._set_fallback_wallpaper() self._set_fallback_wallpaper()
@ -264,14 +268,14 @@ class WallpaperLogic(_ConfigLib):
self._choose_wallpaper_set() self._choose_wallpaper_set()
for time_range in range(self.config_total_changing_times - 1): for time_range in range(self.config_total_changing_times - 1):
self.current_time_range = time_range # Store current time for better debugging output self.current_time_range = time_range # Store current time for better debugging output
clean_time = self._clean_times(time_range) clean_time: List[str] = self._clean_times(time_range)
clean_time_two = self._clean_times(time_range + 1) 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 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. # 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. # 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()): 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]}") exitcode: int = system(f"feh {self.config_behavior} --no-fehbg --quiet {self.wallpaper_list[time_range]}")
has_wallpaper_been_set = self._check_system_exitcode(exitcode) has_wallpaper_been_set: bool = self._check_system_exitcode(exitcode)
# TODO: Add this check to _notify_user. # TODO: Add this check to _notify_user.
if self.config_notify: if self.config_notify:
self._notify_user() self._notify_user()
@ -279,15 +283,15 @@ class WallpaperLogic(_ConfigLib):
else: else:
continue continue
exitcode = system(f"feh {self.config_behavior} --no-fehbg {self.wallpaper_list[-1]}") exitcode: int = system(f"feh {self.config_behavior} --no-fehbg {self.wallpaper_list[-1]}")
has_wallpaper_been_set = self._check_system_exitcode(exitcode) has_wallpaper_been_set: bool = self._check_system_exitcode(exitcode)
if self.config_notify: if self.config_notify:
self._notify_user() self._notify_user()
return has_wallpaper_been_set return has_wallpaper_been_set
# NOTE: Consider avoiding nested functions. # NOTE: Consider avoiding nested functions.
def schedule_wallpapers(self): def schedule_wallpapers(self) -> None:
def _schedule_background_wallpapers(): def _schedule_background_wallpapers() -> BackgroundScheduler:
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
# Create a scheduled job for every changing time # Create a scheduled job for every changing time
@ -299,7 +303,7 @@ class WallpaperLogic(_ConfigLib):
logger.info("The background scheduler has been started.") logger.info("The background scheduler has been started.")
return scheduler return scheduler
def _schedule_blocking_wallpapers(): def _schedule_blocking_wallpapers() -> None:
from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler() scheduler = BlockingScheduler()
# Create a scheduled job for every changing time # 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. # NOTE: The wallman_systray impomrt should be handled differently. See the note in Config_Validity.
import wallman_systray as systray import wallman_systray as systray
from functools import partial from functools import partial
scheduler = _schedule_background_wallpapers() scheduler: BackgroundScheduler = _schedule_background_wallpapers()
menu = systray.Menu ( menu: systray.Menu = systray.Menu (
systray.item("Re-Set Wallpaper", partial(systray.set_wallpaper_again, wallpaper_setter=self.set_wallpaper_by_time)), 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("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)) 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 = systray.Icon("wallman_icon", systray.icon_image, "My Tray Icon", menu)
icon.run() icon.run()
else: else:
_schedule_blocking_wallpapers() _schedule_blocking_wallpapers()