diff --git a/src/epub_tts/__init__.py b/src/epub_tts/__init__.py index 5f822fc..0544c51 100644 --- a/src/epub_tts/__init__.py +++ b/src/epub_tts/__init__.py @@ -18,6 +18,6 @@ from pathlib import Path PathLike = Union[str, Path] -from .logger import logger +from .logger import logger, loglevel_map from .utils import DataDict from .app import App diff --git a/src/epub_tts/app.py b/src/epub_tts/app.py index ec6feb0..47bf139 100644 --- a/src/epub_tts/app.py +++ b/src/epub_tts/app.py @@ -27,7 +27,7 @@ from dotenv import load_dotenv # Local imports from . import tts_aedocw, tts_generic, tts_kokoro -from . import logger, PathLike +from . import logger, PathLike, loglevel_map from . import DataDict class App(DataDict): @@ -36,7 +36,7 @@ class App(DataDict): ENV_FILE_OVERRIDE=False WHICH = ["ffmpeg"] # Needed in $PATH DEBUG = False - LOGLEVEL = logging.INFO + LOGLEVEL = "WARNING" BACKENDS = { # keys are the backend names, values are the corresponding TTS classes "default": tts_generic.GenericTTSBackend, @@ -55,77 +55,31 @@ class App(DataDict): def __init__(self, *args, **kwargs): super().__init__(*args,**kwargs) self._argv = sys.argv - self._args_list = args if args is not None else self._argv - - # Setup logger - self.logger = logger - self.logger.setLevel(App.LOGLEVEL) - if App.DEBUG or "--debug" in self._args_list: - self.debug = True - self.loglevel = logging.DEBUG - logger.info("Loglevel = DEBUG") - if "--loglevel" in self._args_list: - logger.warning("Ignoring '--loglevel' flag because DEBUG/'--debug' was also set.") - if "--verbose" in self._args_list: - logger.warning("Ignoring '--verbose' flag because '--debug' was also set.") - - for i, a in enumerate(self._args_list): - if a.startswith("--"): - match a: - case "--env_file": - if self.get("env_file", None): - self.env_file = self._args_list[i+1] - case "--loglevel": - self.loglevel = self._args_list[i+1] - case "--verbose": - self.loglevel = logging.INFO - case "--logfile": - self.logfile = self._args_list[i+1] - - for var_name in ["env_file","loglevel","logfile"]: - flag = f"--{var_name}=" - if a.startswith(flag): - self[var_name] = a.replace(flag, "") + self.setup_logger() if "env_file" not in self._data: self._data["env_file"] = App.DEFAULT_ENV_FILE + self.logger.debug(self._data["env_file"]) + # for path_flag in ["env_file"]: # if not Path(self[path_flag]).exists(): # self.logger.warning(f"Could not read %s. Ignoring '%s' and using '%s'", path_flag, self[path_flag], App.DEFAULT_ENV_FILE) # self[path_flag] = App["DEFAULT_" + path_flag.upper()] - # Set loglevel from laste --loglevel flag found in args/sys.argv - if "loglevel" not in self._data: - self.loglevel = self.LOGLEVEL - self.logger.setLevel(self.loglevel) - + + # load .env/env_file load_dotenv(self.env_file, override=os.environ.get("ENV_FILE_OVERRIDE",App.ENV_FILE_OVERRIDE)) - # Now we can call argparser with correct defaults and logger/loglevel # Get ArgumentParser and pased *args or read from sys.argv self._parsed_args = self.get_parser().parse_args(args or sys.argv[1:]) for k,v in self._parsed_args.__dict__.items(): self[k] = v - if self._parsed_args.loglevel: - self.loglevel = self._parsed_args.loglevel - self.logger.setLevel(self.loglevel) - elif self._parsed_args.verbose: - self.loglevel = logging.INFO - self.logger.setLevel(self.loglevel) - - if getattr(self, "logfile", None) or getattr(self._parsed_args, "logfile", None): - logfile = getattr(self, "logfile", None) or self._parsed_args.logfile - fh = logging.FileHandler(logfile) - fh.setLevel(self.loglevel) - fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) - self.logger.addHandler(fh) - # Validate and sanitize output self.output_path = App.path(self.output) if len(self.input) > 1 and not self.output_path.is_dir(): @@ -136,28 +90,28 @@ class App(DataDict): # Validate and sanitize input self.input_files = {} self.input_dirs = [] - self.info("Paths provided as input: %d", len(self.input)) + self.logger.info("Paths provided as input: %d", len(self.input)) for i, input_str in enumerate(self.input): - self.info("[Input %d] %s", i+1, input_str) + self.logger.info("[Input %d] %s", i+1, input_str) input_path = App.path(input_str) if not input_path.exists(): - self.warning("Skipping path (do not exist): '%s'.", input_path) + self.logger.warning("Skipping path (do not exist): '%s'.", input_path) continue if input_path.is_dir(): self.input_dir.append(input_path) - self.info("Added directory to queue: '%s'", input_path) + self.logger.info("Added directory to queue: '%s'", input_path) else: self.input_files[input_str] = {"path":input_path} self.input_files[input_str]["filename"] = self.input_files[input_str]["path"].name self.input_files[input_str]["stem"] = self.input_files[input_str]["path"].stem self.input_files[input_str]["suffix"] = self.input_files[input_str]["path"].suffix #self.input_files[input_str]["filetype"] = ebook_meta self.input_files[input_str]["path"] - self.info("Added file to queue: %s", self.input_files[input_str]) + self.logger.info("Added file to queue: %s", self.input_files[input_str]) del self._data["input"] # Sanitize replace_map - self.debug(self.replace) + self.logger.debug(self.replace) if self.replace and len(self.replace) % 2 == 0: self.replace_map = { old: new for old, new in self.replace} del self.replace @@ -170,41 +124,73 @@ class App(DataDict): if self._parsed_args.check_env: self.check_env() + self.engine = App.BACKENDS[self.backend](self) - self.engine = App.BACKENDS[args.backend](**vars(args)) - if len(args.input) > 1 and not output_dest.is_dir(): - self.logger.error(f"Output must be a directory when multiple input files are provided") - sys.exit(1) - for file in args.input: - self.logger.info(file) - if not Path(file).exists(): - self.logger.error(f"Input file %s does not exist", file) - sys.exit(1) - output_dest = Path(args.output) + def setup_logger(self, verbose=None, debug_level=None, loglevel: str|int = None, logfile:PathLike=None): + """Initialize the package logger from CLI flags and defaults. - # Log functions - def log(self, *args,**kwargs): - self.logger.log(self.loglevel, *args, **kwargs) + Resolve the effective logging level from the class args (or sys.argv). + --debug (or debug_level=True) takes priority, then --verbose (or verbose=True) + is compared to --loglevel (or loglevel=) and the lower one is proritized. + """ + # Setup logger + self.logger = logger - # 10=DEBUG - def debug(self, *args,**kwargs): - self.logger.debug(*args, **kwargs) + # Find lowest priority loglevel + if debug_level or App.DEBUG or "--debug" in self._args_list: + self.debug_level = True + self.loglevel = "DEBUG" + else: # compare verbose to loglevel + if "--verbose" in self._args_list or verbose == True: + self.verbose = True + for i, a in enumerate(self._args_list): + if a == "--loglevel": + self.loglevel = self._args_list[i+1] + elif a.startswith("--loglevel="): + self.loglevel = a.replace("--loglevel=", "") - # 20=INFO - def info(self, *args,**kwargs): - self.logger.info(*args, **kwargs) + if "loglevel" in self and self.loglevel is None: + if loglevel is not None: + self.loglevel = loglevel + else: + self.loglevel = App.LOGLEVEL - # 30=WARN/WARNING - def warning(self, *args,**kwargs): - self.logger.warning(*args, **kwargs) + # lower = more priority + if loglevel_map[self.loglevel] > loglevel_map["INFO"]: + self.loglevel = "INFO" - # 40=ERROR - def error(self, *args,**kwargs): - self.logger.error(*args, **kwargs) + self.logger.setLevel(loglevel_map[self.loglevel]) - # 50=CRITICAL/FATAL - def critical(self, *args,**kwargs): - self.logger.critical(*args, **kwargs) + # Get logfile + for i, a in enumerate(self._args_list): + if a == "--logfile": + self.logfile = self._args_list[i+1] + elif a.startswith("--logfile="): + self.logfile = a.replace("--logfile=", "") + + if "logfile" in self and self.logfile: + if logfile: + self.logger.warning("Overriding --logfile='%s' from setup_logger(logfile='%s')", self.logfile, logfile) + self.logfile = logfile + fh = logging.FileHandler(logfile) + fh.setLevel(self.loglevel) + fh.setFormatter(logger.formatter) + self.logger.addHandler(fh) + + # if self.debug_level: + # logger.info("Loglevel: DEBUG") + # if self.loglevel: + # logger.warning("Ignoring '--loglevel' flag because DEBUG/'--debug' was also set.") + # if "--verbose" in self._args_list: + # logger.warning("Ignoring '--verbose' flag because '--debug' was also set.") + # self.verbose = False + # if self.loglevel and self.verbose: + # self.warning("Conflict: trying to --loglevel=%s (%d) and --verbose.", self.loglevel, loglevel_map[self.loglevel]) + # if loglevel_map[self.loglevel] < logging.INFO: + # self.logger.info("Setting loglevel to %s (%d), which is more verbose than INFO.", self.loglevel, loglevel_map[self.loglevel]) + + + return self.logger # Utility methods def check_env(self, which=None): @@ -238,8 +224,19 @@ class App(DataDict): # General options p.add_argument( - "--debug", action="store_true", + "--debug", dest="debug_level", action="store_true", help="Enable debug logging and verbose diagnostics") + p.add_argument( + "--verbose", action="store_true", + help="Enable info-level logging") + p.add_argument( + "--loglevel", + type=str.upper, default=logging.INFO, + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set logging level, from most verbose to least verbose:") + p.add_argument( + "--logfile", default=None, + help="Write logs to the specified file") # Input options and processing p.add_argument( diff --git a/src/epub_tts/logger.py b/src/epub_tts/logger.py index c6f1fba..a5883fd 100644 --- a/src/epub_tts/logger.py +++ b/src/epub_tts/logger.py @@ -6,6 +6,7 @@ # Logging # ============================================================ +import enum import logging import os import re @@ -13,15 +14,45 @@ import sys import pprint # Globals +format_str = "[%(name)s:%(filename)s:%(funcName)s:%(lineno)04d] %(asctime)s::%(levelname).4s: %(message)s" +# %(name)s Name of the logger (logging channel) +# %(levelno)s Numeric logging level for the message (DEBUG, INFO, +# WARNING, ERROR, CRITICAL) +# %(levelname)s Text logging level for the message ("DEBUG", "INFO", +# "WARNING", "ERROR", "CRITICAL") +# %(pathname)s Full pathname of the source file where the logging +# call was issued (if available) +# %(filename)s Filename portion of pathname +# %(module)s Module (name portion of filename) +# %(lineno)d Source line number where the logging call was issued +# (if available) +# %(funcName)s Function name +# %(created)f Time when the LogRecord was created (time.time_ns() / 1e9 +# return value) +# %(asctime)s Textual time when the LogRecord was created +# %(msecs)d Millisecond portion of the creation time +# %(relativeCreated)d Time in milliseconds when the LogRecord was created, +# relative to the time the logging module was loaded +# (typically at application startup time) +# %(thread)d Thread ID (if available) +# %(threadName)s Thread name (if available) +# %(taskName)s Task name (if available) +# %(process)d Process ID (if available) +# %(processName)s Process name (if available) +# %(message)s The result of record.getMessage(), computed just as +# the record is emitted +formatter = logging.Formatter(format_str) logging.basicConfig( # 50=CRITICAL/FATAL, 40=ERROR, 30=WARN/WARNING, 20=INFO, 10=DEBUG, 0=NOTSET # loglevel_map = logging.getLevelNamesMapping() level=logging.INFO, - format="%(asctime)s %(levelname)s %(message)s", + format=format_str, ) logger = logging.getLogger(__name__) +loglevel_map = logging.getLevelNamesMapping() + # Logging utils def print(message):