[docs]classLogWriter:"""File-like object to write standard output to logging functions. Attributes: fn_logging (func): Logging function buffer (list[str]): String buffer. """def__init__(self,fn_logging):"""Create a writer for a logging function."""self.fn_logger=fn_loggingself.buffer=[]
[docs]defwrite(self,msg):"""Write to logging function with buffering. Args: msg (str): Line to write, from which trailing newlines will be removed. """ifmsg.endswith("\n"):# remove trailing newlines in buffer and pass to logging functionself.buffer.append(msg.rstrip("\n"))self.fn_logger("".join(self.buffer))self.buffer=[]else:self.buffer.append(msg)
[docs]defflush(self):"""Empty function, deferring to logging handler's flush."""pass
[docs]defsetup_logger():"""Set up a basic root logger with a stream handler. Returns: :class:`logging.Logger`: Root logger for the application. """logger=logging.getLogger()logger.setLevel(logging.INFO)# set up handler for consolehandler_stream=logging.StreamHandler()handler_stream.setLevel(logging.INFO)handler_stream.setFormatter(logging.Formatter("%(name)s - %(levelname)s - %(message)s"))logger.addHandler(handler_stream)returnlogger
[docs]defupdate_log_level(logger,level):"""Update the logging level. Args: logger (:class:`logging.Logger`): Logger to update. level (Union[str, int]): Level given either as a string corresponding to ``Logger`` levels, or their corresponding integers, ranging from 0 (``NOTSET``) to 50 (``CRITICAL``). For convenience, values can be given from 0-5, which will be multiplied by 10. Returns: :class:`logging.Logger`: The logger for chained calls. """ifisinstance(level,str):# specify level by level namelevel=level.upper()elifisinstance(level,int):# specify by level integer (0-50)iflevel<10:# for convenience, assume values under 10 are 10-foldlevel*=10else:returntry:# set level for the logger and all its handlerslogger.setLevel(level)forhandlerinlogger.handlers:handler.setLevel(level)iflevel<logging.INFO:# avoid lots of debugging messages from Matplotliblogging.getLogger("matplotlib").setLevel(logging.INFO)except(TypeError,ValueError)ase:logger.error(e,exc_info=True)returnlogger
[docs]defadd_file_handler(logger:logging.Logger,path:str,backups:int=5)->pathlib.Path:"""Add a rotating log file handler with a new log file. Rotates the file each time this function is called for the given number of backups rather than by size or time. Avoids file conflicts from multiple instances when permission errors occur (eg on Windows), instead creating a log filed named with an incremented number (eg ``out1.log``). Args: logger: Logger to update. path: Path to log. Increments to ``path<n>.<ext>`` if the file at ``path`` cannot be rotated. backups: Number of backups to maintain; defaults to 5. Returns: The log output path. """# check if log file already existspathl=pathlib.Path(path)path_log=pathlroll=pathl.is_file()# create a rotations file handler to manage number of backups while# manually managing rollover based on file presence rather than sizepathl.parent.mkdir(parents=True,exist_ok=True)i=0handler_file=Nonewhilehandler_fileisNone:try:# if the existing file at path cannot be rotated, increment the# filename to create a new series of rotating log filespath_log=pathlifi==0elsepathlib.Path(f"{pathl.parent/pathl.stem}{i}{pathl.suffix}")logger.debug(f"Trying logger path: {path_log}")handler_file=handlers.RotatingFileHandler(path_log,backupCount=backups,encoding="utf-8")ifroll:# create a new log file if exists, backing up the old onehandler_file.doRollover()except(PermissionError,FileNotFoundError)ase:# permission errors occur on Windows if the log is opened by# another application instance; file not found errors occur if a# backup file is skipped in backup serieslogger.debug(e)handler_file=Nonei+=1handler_file.setLevel(logger.level)handler_file.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))logger.addHandler(handler_file)returnpath_log