From fc01a568d7581b0f588ad69570b4597ac764d9de Mon Sep 17 00:00:00 2001 From: PabloLec Date: Thu, 30 Sep 2021 20:24:38 +0200 Subject: [PATCH 01/16] Move coloring from logger to read --- .gitignore | 2 +- livelog/__init__.py | 2 +- livelog/__main__.py | 2 +- livelog/logger.py | 36 +++++++++--------------------------- livelog/reader.py | 43 +++++++++++++++++++++++++++++++++---------- 5 files changed, 45 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index ba72183..ec803c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -__pycache__ +__pycache__/ poetry.lock dev.py diff --git a/livelog/__init__.py b/livelog/__init__.py index 4572e4f..02ca798 100644 --- a/livelog/__init__.py +++ b/livelog/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from .logger import Logger +from livelog.logger import Logger diff --git a/livelog/__main__.py b/livelog/__main__.py index 584ec6f..fd9a88a 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -2,7 +2,7 @@ from sys import argv from os import _exit -from .reader import start_reader +from livelog.reader import start_reader if __name__ == "__main__": if len(argv) == 1: diff --git a/livelog/logger.py b/livelog/logger.py index 4463f5f..2497426 100644 --- a/livelog/logger.py +++ b/livelog/logger.py @@ -32,7 +32,7 @@ def __new__(cls, *args, **kwargs): def __init__( self, output_file: str = None, - level: str = "INFO", + level: str = "DEBUG", enabled: bool = True, colors: bool = True, erase: bool = True, @@ -42,7 +42,7 @@ def __init__( Args: output_file (str, optional): Output file path. Defaults to None. - level (str, optional): Minimum log level. Defaults to "INFO". + level (str, optional): Minimum log level. Defaults to "DEBUG". enabled (bool, optional): Is log enabled ? Defaults to True. colors (bool, optional): Are colors enabled ? Defaults to True. erase (bool, optional): Should preexisting file be erased ? Defaults to True. @@ -116,7 +116,7 @@ def _clear_file(self): with open(self._output_file, "w") as f: pass - def _write(self, content: str): + def _write(self, level: str, content: str): """Write provided content to output file. Args: @@ -126,16 +126,10 @@ def _write(self, content: str): if not self._enabled: return - if self._colors: - time = Style.DIM + datetime.now().strftime("%H:%M:%S.%f")[:-3] - dash = Style.BRIGHT + " - " - content = f"{Style.NORMAL}{content}{Style.RESET_ALL}" - else: - time = datetime.now().strftime("%H:%M:%S.%f")[:-3] - dash = " - " + time = datetime.now().strftime("%H:%M:%S.%f")[:-3] with open(self._output_file, "a") as f: - f.write(f"{time}{dash}{content}\n") + f.write(f"{level} | {time} - {content}\n") def _is_valid_level(self, level: str): """Verify if the given log level should be written. @@ -156,10 +150,7 @@ def error(self, message: str): message (str): Log message """ - if self._colors: - self._write(content=Fore.RED + message) - else: - self._write(content="error | " + message) + self._write(level="ERR!", content=message) def warn(self, message: str): """Write warning message. @@ -170,10 +161,7 @@ def warn(self, message: str): if not self._is_valid_level("WARNING"): return - if self._colors: - self._write(content=Fore.YELLOW + message) - else: - self._write(content="warning | " + message) + self._write(level="WARN", content=message) def info(self, message: str): """Write info message. @@ -184,10 +172,7 @@ def info(self, message: str): if not self._is_valid_level("INFO"): return - if self._colors: - self._write(content=Fore.BLUE + message) - else: - self._write(content="info | " + message) + self._write(level="INFO", content=message) def debug(self, message: str): """Write debug message. @@ -198,7 +183,4 @@ def debug(self, message: str): if not self._is_valid_level("DEBUG"): return - if self._colors: - self._write(content=Fore.WHITE + message) - else: - self._write(content="debug | " + message) + self._write(level="DBUG", content=message) diff --git a/livelog/reader.py b/livelog/reader.py index 88b4c8a..c055149 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -5,7 +5,15 @@ from shutil import get_terminal_size from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler, DirModifiedEvent, FileModifiedEvent -from colorama import Style +from colorama import Style, Fore + +CLEAR_CMD = "cls" if name == "nt" else "clear" + + +def clear(): + """Clear terminal.""" + + system(CLEAR_CMD) def tail(file_name: str, lines: int): @@ -28,7 +36,28 @@ def tail(file_name: str, lines: int): finally: rows = list(f) pos *= 2 - print("".join(rows[-lines:])) + + colored_lines = map(color_line, rows[-lines:]) + clear() + print("".join(list(colored_lines))) + + +LEVEL_COLORS = { + "ERR!": Fore.RED, + "WARN": Fore.YELLOW, + "INFO": Fore.BLUE, + "DBUG": Fore.WHITE, +} + + +def color_line(line: str): + level = line[:4] + + output = ( + f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" + f"{LEVEL_COLORS[level]}{line[26:]}{Style.RESET_ALL}" + ) + return output class ReadFile(FileSystemEventHandler): @@ -44,11 +73,6 @@ def __init__(self, file: str): self._file = file self.on_modified(event=None) - def clear(self): - """Clear terminal.""" - - _ = system("cls") if name == "nt" else system("clear") - def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): """File modification callback. @@ -56,10 +80,9 @@ def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): event (Union[DirModifiedEvent, FileModifiedEvent, None]): Watchdog event """ - self.clear() - print(Style.RESET_ALL) + print(Style.RESET_ALL, end="") tail(self._file, get_terminal_size(fallback=(120, 50))[1]) - print(Style.RESET_ALL) + print(Style.RESET_ALL, end="") def start_reader(file: str): From 3bb4d0e0aadac103907acfc4864eba664d59f652 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 10:01:28 +0200 Subject: [PATCH 02/16] Add default path for reader --- livelog/__main__.py | 17 ++++++++++++++--- livelog/reader.py | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index fd9a88a..52d6fcf 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -1,12 +1,23 @@ # -*- coding: utf-8 -*- from sys import argv -from os import _exit +from platform import system +from tempfile import gettempdir +from pathlib import Path from livelog.reader import start_reader if __name__ == "__main__": if len(argv) == 1: print("! No file specified !") - _exit(1) + file = Path( + "/tmp/livelog.log" + if system() == "Darwin" + else Path(gettempdir()) / "livelog.log" + ) + else: + file=Path(argv[1]) + if not file.is_file(): + print("Bad file path") + exit() - start_reader(file=argv[1]) + start_reader(file=file) diff --git a/livelog/reader.py b/livelog/reader.py index c055149..f761f21 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -3,6 +3,7 @@ from typing import Union from os import _exit, system, name, SEEK_END from shutil import get_terminal_size +from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler, DirModifiedEvent, FileModifiedEvent from colorama import Style, Fore From 4a125563e42d4385349763d49ca51d7ef401c07a Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 10:08:36 +0200 Subject: [PATCH 03/16] Add FileNotFound handling for reader --- livelog/reader.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/livelog/reader.py b/livelog/reader.py index f761f21..e11dec5 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -2,19 +2,20 @@ from typing import Union from os import _exit, system, name, SEEK_END +from time import sleep from shutil import get_terminal_size from pathlib import Path from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler, DirModifiedEvent, FileModifiedEvent from colorama import Style, Fore -CLEAR_CMD = "cls" if name == "nt" else "clear" - - -def clear(): - """Clear terminal.""" - - system(CLEAR_CMD) +_CLEAR_CMD = "cls" if name == "nt" else "clear" +_LEVEL_COLORS = { + "ERR!": Fore.RED, + "WARN": Fore.YELLOW, + "INFO": Fore.BLUE, + "DBUG": Fore.WHITE, +} def tail(file_name: str, lines: int): @@ -39,16 +40,9 @@ def tail(file_name: str, lines: int): pos *= 2 colored_lines = map(color_line, rows[-lines:]) - clear() - print("".join(list(colored_lines))) - - -LEVEL_COLORS = { - "ERR!": Fore.RED, - "WARN": Fore.YELLOW, - "INFO": Fore.BLUE, - "DBUG": Fore.WHITE, -} + output = "".join(list(colored_lines)) + system(_CLEAR_CMD) + print(output) def color_line(line: str): @@ -56,7 +50,7 @@ def color_line(line: str): output = ( f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" - f"{LEVEL_COLORS[level]}{line[26:]}{Style.RESET_ALL}" + f"{_LEVEL_COLORS[level]}{line[26:]}{Style.RESET_ALL}" ) return output @@ -72,7 +66,14 @@ def __init__(self, file: str): """ self._file = file - self.on_modified(event=None) + while True: + try: + self.on_modified(event=None) + break + except FileNotFoundError: + system(_CLEAR_CMD) + print("File not found, waiting for creation.") + sleep(1) def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): """File modification callback. From 21702839b4617094f5c5d77f33e15cf16b51a906 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 10:24:24 +0200 Subject: [PATCH 04/16] Make LoggerSingleton a proper class --- livelog/__init__.py | 2 +- livelog/logger.py | 48 ++++++++++++++++++++++++--------------------- livelog/reader.py | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/livelog/__init__.py b/livelog/__init__.py index 02ca798..3fb8349 100644 --- a/livelog/__init__.py +++ b/livelog/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from livelog.logger import Logger +from livelog.logger import Logger, LoggerSingleton diff --git a/livelog/logger.py b/livelog/logger.py index 2497426..e2d1469 100644 --- a/livelog/logger.py +++ b/livelog/logger.py @@ -20,33 +20,25 @@ class Logger: write to output path """ - __instance = None _LEVELS = {"ERROR": 3, "WARNING": 2, "INFO": 1, "DEBUG": 0} - def __new__(cls, *args, **kwargs): - is_singleton = (len(args) == 6 and args[5] == True) or kwargs.get("singleton") - if is_singleton and not Logger.__instance or not is_singleton: - Logger.__instance = object.__new__(cls) - return Logger.__instance def __init__( self, - output_file: str = None, + file: str = None, level: str = "DEBUG", enabled: bool = True, colors: bool = True, erase: bool = True, - singleton: bool = False, ): """Logger initialization. Args: - output_file (str, optional): Output file path. Defaults to None. + file (str, optional): Output file path. Defaults to None. level (str, optional): Minimum log level. Defaults to "DEBUG". enabled (bool, optional): Is log enabled ? Defaults to True. colors (bool, optional): Are colors enabled ? Defaults to True. erase (bool, optional): Should preexisting file be erased ? Defaults to True. - singleton (bool, optional): Is singleton ? Defaults to False. Raises: LogLevelDoesNotExist: [description] @@ -56,15 +48,17 @@ def __init__( self._colors = colors self._erase = erase - if output_file is None: - self._output_file = Path( + if file is None: + self._file = Path( "/tmp/livelog.log" if system() == "Darwin" else Path(gettempdir()) / "livelog.log" ) else: - self._output_file = Path(output_file) - self._verify_file(self._output_file) + self._file = Path(file) + self._verify_file(self._file) + + print(self._file) level = level.upper() if level not in self._LEVELS: @@ -72,14 +66,14 @@ def __init__( self._level = level @property - def output_file(self): - return self._output_file + def file(self): + return self._file - @output_file.setter - def set_output_file(self, value: str): + @file.setter + def set_file(self, value: str): path = Path(value) self._verify_file(file=path) - self._output_file = path + self._file = path @property def level(self): @@ -111,9 +105,9 @@ def _verify_file(self, file: Path): def _clear_file(self): """Clear output file content.""" - if not self._erase or not self._output_file.is_file(): + if not self._erase or not self._file.is_file(): return - with open(self._output_file, "w") as f: + with open(self._file, "w") as f: pass def _write(self, level: str, content: str): @@ -128,7 +122,7 @@ def _write(self, level: str, content: str): time = datetime.now().strftime("%H:%M:%S.%f")[:-3] - with open(self._output_file, "a") as f: + with open(self._file, "a") as f: f.write(f"{level} | {time} - {content}\n") def _is_valid_level(self, level: str): @@ -184,3 +178,13 @@ def debug(self, message: str): if not self._is_valid_level("DEBUG"): return self._write(level="DBUG", content=message) + +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + +class LoggerSingleton(Logger, metaclass=Singleton): + pass \ No newline at end of file diff --git a/livelog/reader.py b/livelog/reader.py index e11dec5..57f1b82 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -50,7 +50,7 @@ def color_line(line: str): output = ( f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" - f"{_LEVEL_COLORS[level]}{line[26:]}{Style.RESET_ALL}" + f"{_LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" ) return output From 8b63678934601479113475b34423a8f20e264109 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 10:41:57 +0200 Subject: [PATCH 05/16] Add better file handling for Reader --- livelog/__main__.py | 5 +- livelog/reader.py | 113 ++++++++++++++++++++++++-------------------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index 52d6fcf..dc5ab3a 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -5,6 +5,7 @@ from tempfile import gettempdir from pathlib import Path from livelog.reader import start_reader +from livelog.logger import Logger if __name__ == "__main__": if len(argv) == 1: @@ -16,8 +17,6 @@ ) else: file=Path(argv[1]) - if not file.is_file(): - print("Bad file path") - exit() + start_reader(file=file) diff --git a/livelog/reader.py b/livelog/reader.py index 57f1b82..8450897 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from typing import Union -from os import _exit, system, name, SEEK_END +from os import _exit, system, name, access, R_OK, SEEK_END from time import sleep from shutil import get_terminal_size from pathlib import Path @@ -9,72 +9,83 @@ from watchdog.events import FileSystemEventHandler, DirModifiedEvent, FileModifiedEvent from colorama import Style, Fore -_CLEAR_CMD = "cls" if name == "nt" else "clear" -_LEVEL_COLORS = { - "ERR!": Fore.RED, - "WARN": Fore.YELLOW, - "INFO": Fore.BLUE, - "DBUG": Fore.WHITE, -} - -def tail(file_name: str, lines: int): - """Emulate tail command behavior by printing n last lines. - - Args: - file_name (str): Log file name - lines (int): Number of lines to print - """ - - pos = lines + 1 - rows = [] - with open(file_name) as f: - while len(rows) <= lines: - try: - f.seek(-pos, 2) - except IOError: - f.seek(0) - break - finally: - rows = list(f) - pos *= 2 - - colored_lines = map(color_line, rows[-lines:]) - output = "".join(list(colored_lines)) - system(_CLEAR_CMD) - print(output) - - -def color_line(line: str): - level = line[:4] - - output = ( - f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" - f"{_LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" - ) - return output - - -class ReadFile(FileSystemEventHandler): +class Reader(FileSystemEventHandler): """Watchdog handler to monitor file changes.""" + _CLEAR_CMD = "cls" if name == "nt" else "clear" + _LEVEL_COLORS = { + "ERR!": Fore.RED, + "WARN": Fore.YELLOW, + "INFO": Fore.BLUE, + "DBUG": Fore.WHITE, + } + def __init__(self, file: str): - """ReadFile initialization. + """Reader initialization. Args: file (str): Path of file to monitor """ self._file = file + self._verify_file() while True: try: self.on_modified(event=None) break except FileNotFoundError: - system(_CLEAR_CMD) + system(self._CLEAR_CMD) print("File not found, waiting for creation.") sleep(1) + def _verify_file(self): + """Verify if the file is a valid log file.""" + + dir = self._file.parent.resolve() + if self._file.is_dir(): + raise LogFileIsADirectory(path=self._file) + if not dir.is_dir(): + raise LogPathDoesNotExist(path=dir) + if not access(dir, R_OK): + raise LogPathInsufficientPermissions(path=dir) + + + def tail(self, lines: int): + """Emulate tail command behavior by printing n last lines. + + Args: + lines (int): Number of lines to print + """ + + pos = lines + 1 + rows = [] + with open(self._file) as f: + while len(rows) <= lines: + try: + f.seek(-pos, 2) + except IOError: + f.seek(0) + break + finally: + rows = list(f) + pos *= 2 + + colored_lines = map(self.color_line, rows[-lines:]) + output = "".join(list(colored_lines)) + system(self.self._CLEAR_CMD) + print(output) + + + def color_line(self, line: str): + level = line[:4] + + output = ( + f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" + f"{self.self._LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" + ) + return output + def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): """File modification callback. @@ -83,7 +94,7 @@ def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): """ print(Style.RESET_ALL, end="") - tail(self._file, get_terminal_size(fallback=(120, 50))[1]) + self.tail(get_terminal_size(fallback=(120, 50))[1]) print(Style.RESET_ALL, end="") @@ -94,7 +105,7 @@ def start_reader(file: str): file (str): File to be read """ - event_handler = ReadFile(file=file) + event_handler = Reader(file=file) observer = Observer() observer.schedule(event_handler, file, recursive=True) observer.start() From 02b74a0b0b041521adcfc0661f6d058cb65b17e8 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 10:51:50 +0200 Subject: [PATCH 06/16] Fix bad var naming --- livelog/logger.py | 17 ++++++----------- livelog/reader.py | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/livelog/logger.py b/livelog/logger.py index e2d1469..b7992bf 100644 --- a/livelog/logger.py +++ b/livelog/logger.py @@ -56,9 +56,8 @@ def __init__( ) else: self._file = Path(file) - self._verify_file(self._file) - print(self._file) + self._verify_file() level = level.upper() if level not in self._LEVELS: @@ -86,16 +85,12 @@ def level(self, value: str): raise LogLevelDoesNotExist(level) self._level = level - def _verify_file(self, file: Path): - """Verify if the file is a valid log file and clear its preexisting content. + def _verify_file(self): + """Verify if the file is a valid log file and clear its preexisting content.""" - Args: - file (Path): File path to verify. - """ - - dir = file.parent.resolve() - if file.is_dir(): - raise LogFileIsADirectory(path=file) + dir = self._file.parent.resolve() + if self._file.is_dir(): + raise LogFileIsADirectory(path=self._file) if not dir.is_dir(): raise LogPathDoesNotExist(path=dir) if not access(dir, X_OK): diff --git a/livelog/reader.py b/livelog/reader.py index 8450897..7632d1e 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -73,7 +73,7 @@ def tail(self, lines: int): colored_lines = map(self.color_line, rows[-lines:]) output = "".join(list(colored_lines)) - system(self.self._CLEAR_CMD) + system(self._CLEAR_CMD) print(output) @@ -82,7 +82,7 @@ def color_line(self, line: str): output = ( f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" - f"{self.self._LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" + f"{self._LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" ) return output From 1b76dbbc844476219e509c66d82a9f00ae57c05a Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 11:44:11 +0200 Subject: [PATCH 07/16] Add reader level filtering capability --- livelog/reader.py | 50 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/livelog/reader.py b/livelog/reader.py index 7632d1e..302ef81 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -8,7 +8,7 @@ from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler, DirModifiedEvent, FileModifiedEvent from colorama import Style, Fore - +from .errors import * class Reader(FileSystemEventHandler): """Watchdog handler to monitor file changes.""" @@ -20,8 +20,10 @@ class Reader(FileSystemEventHandler): "INFO": Fore.BLUE, "DBUG": Fore.WHITE, } + _LONG_LEVEL_TO_SHORT = {"ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", "DEBUG": "DBUG"} + _LEVELS = {"ERR!": 3, "WARN": 2, "INFO": 1, "DBUG": 0} - def __init__(self, file: str): + def __init__(self, file: str, level: str = "DEBUG"): """Reader initialization. Args: @@ -30,6 +32,12 @@ def __init__(self, file: str): self._file = file self._verify_file() + + level = level.upper() + if level not in self._LONG_LEVEL_TO_SHORT: + raise LogLevelDoesNotExist(level) + self._level = self._LONG_LEVEL_TO_SHORT[level] + while True: try: self.on_modified(event=None) @@ -51,17 +59,17 @@ def _verify_file(self): raise LogPathInsufficientPermissions(path=dir) - def tail(self, lines: int): + def tail(self, length: int): """Emulate tail command behavior by printing n last lines. Args: - lines (int): Number of lines to print + length (int): Number of lines to print """ - pos = lines + 1 + pos = length + 1 rows = [] with open(self._file) as f: - while len(rows) <= lines: + while len(rows) <= length: try: f.seek(-pos, 2) except IOError: @@ -71,12 +79,37 @@ def tail(self, lines: int): rows = list(f) pos *= 2 - colored_lines = map(self.color_line, rows[-lines:]) + return rows[-length:], len(rows) + + def get_output(self, length: int): + """Emulate tail command behavior by printing n last lines. + + Args: + length (int): Number of lines to print + """ + + rows, total_rows = [], length + 1 + if self._level == "DBUG": + rows, _ = self.tail(length=length) + else: + while len(rows) < length and total_rows > length: + rows, total_rows = self.tail(length=length) + rows = self.filter_lines(rows) + length += 1 + + colored_lines = map(self.color_line, rows) output = "".join(list(colored_lines)) system(self._CLEAR_CMD) print(output) + def filter_lines(self, lines: list): + for i, line in enumerate(lines): + level = line[:4] + if self._LEVELS[self._level] >= self._LEVELS[level]: + del lines[i] + return lines + def color_line(self, line: str): level = line[:4] @@ -94,7 +127,8 @@ def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): """ print(Style.RESET_ALL, end="") - self.tail(get_terminal_size(fallback=(120, 50))[1]) + self.get_output(get_terminal_size(fallback=(120, 50))[1]) + #self.get_output(5) print(Style.RESET_ALL, end="") From ee8b0d80bf2e59fcea8a5d7ec09e2948aab22d5e Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 13:44:54 +0200 Subject: [PATCH 08/16] Change reader behavior to just append new lines --- livelog/__main__.py | 25 +++++++++--- livelog/reader.py | 93 +++++++++++++++++++++------------------------ 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index dc5ab3a..01ba297 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -4,19 +4,34 @@ from platform import system from tempfile import gettempdir from pathlib import Path +from argparse import ArgumentParser from livelog.reader import start_reader from livelog.logger import Logger -if __name__ == "__main__": - if len(argv) == 1: - print("! No file specified !") + +def parse_args(): + parser = ArgumentParser(description='Live read a log file') + parser.add_argument('-f', '--file', action='store', type=str, required=False) + parser.add_argument('-l', '--level', action='store', type=str, required=False) + parser.add_argument('--nocolors', action='store_true', required=False) + args = parser.parse_args() + + print(args) + + if args.file is not None: + file=Path(args.file) + else: file = Path( "/tmp/livelog.log" if system() == "Darwin" else Path(gettempdir()) / "livelog.log" ) - else: - file=Path(argv[1]) + + return file + +if __name__ == "__main__": + file = parse_args() + start_reader(file=file) diff --git a/livelog/reader.py b/livelog/reader.py index 302ef81..451cf3d 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -20,8 +20,8 @@ class Reader(FileSystemEventHandler): "INFO": Fore.BLUE, "DBUG": Fore.WHITE, } - _LONG_LEVEL_TO_SHORT = {"ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", "DEBUG": "DBUG"} - _LEVELS = {"ERR!": 3, "WARN": 2, "INFO": 1, "DBUG": 0} + _LONG_LEVEL_TO_SHORT = {"ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", "DEBUG": "DBUG",} + _LEVELS = {"ERR!": 3, "WARN": 2, "INFO": 1, "DBUG": 0,} def __init__(self, file: str, level: str = "DEBUG"): """Reader initialization. @@ -38,14 +38,15 @@ def __init__(self, file: str, level: str = "DEBUG"): raise LogLevelDoesNotExist(level) self._level = self._LONG_LEVEL_TO_SHORT[level] - while True: - try: - self.on_modified(event=None) - break - except FileNotFoundError: - system(self._CLEAR_CMD) - print("File not found, waiting for creation.") - sleep(1) + self._read_index = 0 + self._empty_read_count = 0 + + while not self._file_exists(): + system(self._CLEAR_CMD) + print("File not found, waiting for creation.") + sleep(1) + + self.on_modified(event=None) def _verify_file(self): """Verify if the file is a valid log file.""" @@ -58,55 +59,49 @@ def _verify_file(self): if not access(dir, R_OK): raise LogPathInsufficientPermissions(path=dir) + def _file_exists(self): + return self._file.is_file() - def tail(self, length: int): - """Emulate tail command behavior by printing n last lines. - - Args: - length (int): Number of lines to print - """ - - pos = length + 1 - rows = [] - with open(self._file) as f: - while len(rows) <= length: - try: - f.seek(-pos, 2) - except IOError: - f.seek(0) - break - finally: - rows = list(f) - pos *= 2 - - return rows[-length:], len(rows) - - def get_output(self, length: int): + def print_output(self): """Emulate tail command behavior by printing n last lines. Args: length (int): Number of lines to print """ - rows, total_rows = [], length + 1 - if self._level == "DBUG": - rows, _ = self.tail(length=length) - else: - while len(rows) < length and total_rows > length: - rows, total_rows = self.tail(length=length) - rows = self.filter_lines(rows) - length += 1 + rows = self.get_new_lines() + if rows is None: + return + rows = self.filter_log_level(rows) colored_lines = map(self.color_line, rows) output = "".join(list(colored_lines)) - system(self._CLEAR_CMD) - print(output) + print(output, end="") + + + def get_new_lines(self): + """Emulate tail command behavior by printing n last lines.""" + + with open(self._file, "r") as f: + f.seek(self._read_index) + rows = list(f) + if len(rows) == 0: + self._empty_read_count += 1 + if self._empty_read_count >= 3: + self._read_index = 0 + self._empty_read_count = 0 + return self.get_new_lines() + return None + self._read_index += sum(map(len, rows)) - def filter_lines(self, lines: list): + return rows + + + def filter_log_level(self, lines: list): for i, line in enumerate(lines): level = line[:4] - if self._LEVELS[self._level] >= self._LEVELS[level]: + if self._LEVELS[self._level] > self._LEVELS[level]: del lines[i] return lines @@ -119,16 +114,16 @@ def color_line(self, line: str): ) return output - def on_modified(self, event: Union[DirModifiedEvent, FileModifiedEvent, None]): + + def on_modified(self, event: Union[FileModifiedEvent, None]): """File modification callback. Args: - event (Union[DirModifiedEvent, FileModifiedEvent, None]): Watchdog event + event (Union[FileModifiedEvent, None]): Watchdog event """ print(Style.RESET_ALL, end="") - self.get_output(get_terminal_size(fallback=(120, 50))[1]) - #self.get_output(5) + self.print_output() print(Style.RESET_ALL, end="") From ee99ef245c1d6e7154ce6cf3ba85885d1263b2d6 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 14:28:01 +0200 Subject: [PATCH 09/16] Add CLI args for reader --- livelog/__main__.py | 10 ++++++---- livelog/reader.py | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index 01ba297..3b499e8 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -12,7 +12,7 @@ def parse_args(): parser = ArgumentParser(description='Live read a log file') parser.add_argument('-f', '--file', action='store', type=str, required=False) - parser.add_argument('-l', '--level', action='store', type=str, required=False) + parser.add_argument('-l', '--level', action='store', type=str, default="DEBUG", required=False) parser.add_argument('--nocolors', action='store_true', required=False) args = parser.parse_args() @@ -27,11 +27,13 @@ def parse_args(): else Path(gettempdir()) / "livelog.log" ) - return file + + + return file, args.level, args. nocolors if __name__ == "__main__": - file = parse_args() + file, level, nocolors = parse_args() - start_reader(file=file) + start_reader(file=file, level=level, nocolors=nocolors) diff --git a/livelog/reader.py b/livelog/reader.py index 451cf3d..02497b8 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -23,7 +23,7 @@ class Reader(FileSystemEventHandler): _LONG_LEVEL_TO_SHORT = {"ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", "DEBUG": "DBUG",} _LEVELS = {"ERR!": 3, "WARN": 2, "INFO": 1, "DBUG": 0,} - def __init__(self, file: str, level: str = "DEBUG"): + def __init__(self, file: str, level: str, nocolors: bool): """Reader initialization. Args: @@ -37,12 +37,13 @@ def __init__(self, file: str, level: str = "DEBUG"): if level not in self._LONG_LEVEL_TO_SHORT: raise LogLevelDoesNotExist(level) self._level = self._LONG_LEVEL_TO_SHORT[level] + self._nocolors = nocolors self._read_index = 0 self._empty_read_count = 0 + system(self._CLEAR_CMD) while not self._file_exists(): - system(self._CLEAR_CMD) print("File not found, waiting for creation.") sleep(1) @@ -74,8 +75,11 @@ def print_output(self): return rows = self.filter_log_level(rows) - colored_lines = map(self.color_line, rows) - output = "".join(list(colored_lines)) + if self._nocolors: + output = "".join(rows) + else: + colored_lines = map(self.color_line, rows) + output = "".join(list(colored_lines)) print(output, end="") @@ -88,9 +92,11 @@ def get_new_lines(self): rows = list(f) if len(rows) == 0: self._empty_read_count += 1 + # NOT WORKING, NEED A FIX if self._empty_read_count >= 3: self._read_index = 0 self._empty_read_count = 0 + print(" - - - FAIL - - - ") return self.get_new_lines() return None self._read_index += sum(map(len, rows)) @@ -127,14 +133,14 @@ def on_modified(self, event: Union[FileModifiedEvent, None]): print(Style.RESET_ALL, end="") -def start_reader(file: str): +def start_reader(file: str, level: str, nocolors: bool): """Start reader process. Args: file (str): File to be read """ - event_handler = Reader(file=file) + event_handler = Reader(file=file, level=level, nocolors=nocolors) observer = Observer() observer.schedule(event_handler, file, recursive=True) observer.start() From e93a3145bf1f4a28ed71d32a7cc02426e4465d28 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 14:43:45 +0200 Subject: [PATCH 10/16] Fix reader bug with reset file --- livelog/reader.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/livelog/reader.py b/livelog/reader.py index 02497b8..9bb61e6 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -40,7 +40,6 @@ def __init__(self, file: str, level: str, nocolors: bool): self._nocolors = nocolors self._read_index = 0 - self._empty_read_count = 0 system(self._CLEAR_CMD) while not self._file_exists(): @@ -90,15 +89,12 @@ def get_new_lines(self): with open(self._file, "r") as f: f.seek(self._read_index) rows = list(f) - if len(rows) == 0: - self._empty_read_count += 1 - # NOT WORKING, NEED A FIX - if self._empty_read_count >= 3: - self._read_index = 0 - self._empty_read_count = 0 - print(" - - - FAIL - - - ") - return self.get_new_lines() - return None + if len(rows) == 0 and self._read_index > 0: + f.seek(self._read_index-1) + if len(list(f)) > 0: + return None + self._read_index = 0 + return self.get_new_lines() self._read_index += sum(map(len, rows)) return rows @@ -145,10 +141,6 @@ def start_reader(file: str, level: str, nocolors: bool): observer.schedule(event_handler, file, recursive=True) observer.start() - try: - input("") - _exit(1) - - finally: - observer.stop() - observer.join() + input("") + observer.stop() + observer.join() From 49ad98d84d0ed8748b15d58a016f870005f4e37a Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 14:46:31 +0200 Subject: [PATCH 11/16] Fix terminal clear --- livelog/reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/livelog/reader.py b/livelog/reader.py index 9bb61e6..c83fded 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -45,6 +45,7 @@ def __init__(self, file: str, level: str, nocolors: bool): while not self._file_exists(): print("File not found, waiting for creation.") sleep(1) + system(self._CLEAR_CMD) self.on_modified(event=None) From 97ee8d589867dbdbad012abfaba798f376ab5be2 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Fri, 1 Oct 2021 14:51:04 +0200 Subject: [PATCH 12/16] linting --- livelog/__main__.py | 19 +++++++++---------- livelog/logger.py | 6 ++++-- livelog/reader.py | 26 ++++++++++++++++---------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index 3b499e8..8e7516d 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -10,16 +10,18 @@ def parse_args(): - parser = ArgumentParser(description='Live read a log file') - parser.add_argument('-f', '--file', action='store', type=str, required=False) - parser.add_argument('-l', '--level', action='store', type=str, default="DEBUG", required=False) - parser.add_argument('--nocolors', action='store_true', required=False) + parser = ArgumentParser(description="Live read a log file") + parser.add_argument("-f", "--file", action="store", type=str, required=False) + parser.add_argument( + "-l", "--level", action="store", type=str, default="DEBUG", required=False + ) + parser.add_argument("--nocolors", action="store_true", required=False) args = parser.parse_args() print(args) if args.file is not None: - file=Path(args.file) + file = Path(args.file) else: file = Path( "/tmp/livelog.log" @@ -27,13 +29,10 @@ def parse_args(): else Path(gettempdir()) / "livelog.log" ) - - - return file, args.level, args. nocolors - + return file, args.level, args.nocolors if __name__ == "__main__": file, level, nocolors = parse_args() - + start_reader(file=file, level=level, nocolors=nocolors) diff --git a/livelog/logger.py b/livelog/logger.py index b7992bf..d2773b0 100644 --- a/livelog/logger.py +++ b/livelog/logger.py @@ -22,7 +22,6 @@ class Logger: _LEVELS = {"ERROR": 3, "WARNING": 2, "INFO": 1, "DEBUG": 0} - def __init__( self, file: str = None, @@ -174,12 +173,15 @@ def debug(self, message: str): return self._write(level="DBUG", content=message) + class Singleton(type): _instances = {} + def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + class LoggerSingleton(Logger, metaclass=Singleton): - pass \ No newline at end of file + pass diff --git a/livelog/reader.py b/livelog/reader.py index c83fded..99ad811 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -10,6 +10,7 @@ from colorama import Style, Fore from .errors import * + class Reader(FileSystemEventHandler): """Watchdog handler to monitor file changes.""" @@ -20,8 +21,18 @@ class Reader(FileSystemEventHandler): "INFO": Fore.BLUE, "DBUG": Fore.WHITE, } - _LONG_LEVEL_TO_SHORT = {"ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", "DEBUG": "DBUG",} - _LEVELS = {"ERR!": 3, "WARN": 2, "INFO": 1, "DBUG": 0,} + _LONG_LEVEL_TO_SHORT = { + "ERROR": "ERR!", + "WARN": "WARNING", + "INFO": "INFO", + "DEBUG": "DBUG", + } + _LEVELS = { + "ERR!": 3, + "WARN": 2, + "INFO": 1, + "DBUG": 0, + } def __init__(self, file: str, level: str, nocolors: bool): """Reader initialization. @@ -77,21 +88,20 @@ def print_output(self): if self._nocolors: output = "".join(rows) + print(output, end="") else: colored_lines = map(self.color_line, rows) output = "".join(list(colored_lines)) - print(output, end="") - + print(Style.RESET_ALL + output + Style.RESET_ALL, end="") def get_new_lines(self): """Emulate tail command behavior by printing n last lines.""" - with open(self._file, "r") as f: f.seek(self._read_index) rows = list(f) if len(rows) == 0 and self._read_index > 0: - f.seek(self._read_index-1) + f.seek(self._read_index - 1) if len(list(f)) > 0: return None self._read_index = 0 @@ -100,7 +110,6 @@ def get_new_lines(self): return rows - def filter_log_level(self, lines: list): for i, line in enumerate(lines): level = line[:4] @@ -117,7 +126,6 @@ def color_line(self, line: str): ) return output - def on_modified(self, event: Union[FileModifiedEvent, None]): """File modification callback. @@ -125,9 +133,7 @@ def on_modified(self, event: Union[FileModifiedEvent, None]): event (Union[FileModifiedEvent, None]): Watchdog event """ - print(Style.RESET_ALL, end="") self.print_output() - print(Style.RESET_ALL, end="") def start_reader(file: str, level: str, nocolors: bool): From 7d3a81b4777c910b6af855b54524a67f872f6aae Mon Sep 17 00:00:00 2001 From: PabloLec Date: Sat, 2 Oct 2021 10:42:13 +0200 Subject: [PATCH 13/16] Clean and add doctrings to `reader` --- livelog/reader.py | 133 ++++++++++++++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/livelog/reader.py b/livelog/reader.py index 99ad811..22877e5 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -1,33 +1,41 @@ # -*- coding: utf-8 -*- -from typing import Union -from os import _exit, system, name, access, R_OK, SEEK_END +from os import system, access, name, R_OK from time import sleep -from shutil import get_terminal_size -from pathlib import Path from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler, DirModifiedEvent, FileModifiedEvent +from watchdog.events import FileSystemEventHandler from colorama import Style, Fore from .errors import * class Reader(FileSystemEventHandler): - """Watchdog handler to monitor file changes.""" + """Reading handler. + + Attributes: + CLEAR_CMD (str): OS specific command to clear terminal + LEVEL_COLORS (dict): Log levels corresponding colors + LONGlevel_TO_SHORT (dict): Log level long to short format + LEVELS (dict): Log levels value for fast filtering + file (str): Log file path + level (str): Minimum log level to be displayed + nocolors (bool): If colors should not be printed + read_index (int): Current position of the cursor in the log file + """ - _CLEAR_CMD = "cls" if name == "nt" else "clear" - _LEVEL_COLORS = { + CLEAR_CMD = "cls" if name == "nt" else "clear" + LEVEL_COLORS = { "ERR!": Fore.RED, "WARN": Fore.YELLOW, "INFO": Fore.BLUE, "DBUG": Fore.WHITE, } - _LONG_LEVEL_TO_SHORT = { + LONGlevel_TO_SHORT = { "ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", "DEBUG": "DBUG", } - _LEVELS = { + LEVELS = { "ERR!": 3, "WARN": 2, "INFO": 1, @@ -39,46 +47,53 @@ def __init__(self, file: str, level: str, nocolors: bool): Args: file (str): Path of file to monitor + level (str): Minimum log level to be displayed + nocolors (bool): If colors should not be printed + Raises: + LogLevelDoesNotExist: If provided log level is not listed. """ - self._file = file - self._verify_file() - + self.file = file + self.verify_file() level = level.upper() - if level not in self._LONG_LEVEL_TO_SHORT: + if level not in self.LONGlevel_TO_SHORT: raise LogLevelDoesNotExist(level) - self._level = self._LONG_LEVEL_TO_SHORT[level] - self._nocolors = nocolors + self.level = self.LONGlevel_TO_SHORT[level] + self.nocolors = nocolors - self._read_index = 0 + self.read_index = 0 - system(self._CLEAR_CMD) - while not self._file_exists(): + system(self.CLEAR_CMD) + while not self.file_exists(): print("File not found, waiting for creation.") sleep(1) - system(self._CLEAR_CMD) + system(self.CLEAR_CMD) - self.on_modified(event=None) + self.on_modified() - def _verify_file(self): + def verify_file(self): """Verify if the file is a valid log file.""" - dir = self._file.parent.resolve() - if self._file.is_dir(): - raise LogFileIsADirectory(path=self._file) + dir = self.file.parent.resolve() + if self.file.is_dir(): + raise LogFileIsADirectory(path=self.file) if not dir.is_dir(): raise LogPathDoesNotExist(path=dir) if not access(dir, R_OK): raise LogPathInsufficientPermissions(path=dir) - def _file_exists(self): - return self._file.is_file() + def file_exists(self): + """Check if file exists. - def print_output(self): - """Emulate tail command behavior by printing n last lines. + Returns: + bool: File exists + """ - Args: - length (int): Number of lines to print + return self.file.isfile() + + def print_output(self): + """Drive the printing process by getting new lines, filtering log + level and coloring the output. """ rows = self.get_new_lines() @@ -86,7 +101,7 @@ def print_output(self): return rows = self.filter_log_level(rows) - if self._nocolors: + if self.nocolors: output = "".join(rows) print(output, end="") else: @@ -95,43 +110,65 @@ def print_output(self): print(Style.RESET_ALL + output + Style.RESET_ALL, end="") def get_new_lines(self): - """Emulate tail command behavior by printing n last lines.""" + """Get newly created lines in log file based on stored cursor index. + + Returns: + list: List of new lines + """ - with open(self._file, "r") as f: - f.seek(self._read_index) + with open(self.file, "r") as f: + f.seek(self.read_index) rows = list(f) - if len(rows) == 0 and self._read_index > 0: - f.seek(self._read_index - 1) + if not rows and self.read_index > 0: + # If a modification event was triggered without any + # new rows, we should verify if there is content just before + # current cursor position to determine if file have been reset + # or just a false positive. + f.seek(self.read_index - 1) if len(list(f)) > 0: return None - self._read_index = 0 + self.read_index = 0 return self.get_new_lines() - self._read_index += sum(map(len, rows)) + self.read_index += sum(map(len, rows)) return rows def filter_log_level(self, lines: list): + """Remove lines based on log level. + + Args: + lines (list): Lines to be filtered + + Returns: + list: Filtered lines + """ + for i, line in enumerate(lines): level = line[:4] - if self._LEVELS[self._level] > self._LEVELS[level]: + if self.LEVELS[self.level] > self.LEVELS[level]: del lines[i] return lines def color_line(self, line: str): + """Parse and color a line based on its log level. + + Args: + line (str): Line to be colored + + Returns: + str: Colored line + """ + level = line[:4] output = ( f"{Style.DIM}{line[7:19]}{Style.BRIGHT} - {Style.NORMAL}" - f"{self._LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" + f"{self.LEVEL_COLORS[level]}{line[22:]}{Style.RESET_ALL}" ) return output - def on_modified(self, event: Union[FileModifiedEvent, None]): - """File modification callback. - - Args: - event (Union[FileModifiedEvent, None]): Watchdog event - """ + def on_modified(self, *args, **kwargs): + """File modification callback.""" self.print_output() @@ -141,6 +178,8 @@ def start_reader(file: str, level: str, nocolors: bool): Args: file (str): File to be read + level (str): Minimum log level to be displayed + nocolors (bool): If colors should not be printed """ event_handler = Reader(file=file, level=level, nocolors=nocolors) From 8b627f86afbcc9c5de96b5111f599c85ddc67bf7 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Sat, 2 Oct 2021 10:51:55 +0200 Subject: [PATCH 14/16] Clean and add doctrings to `logger` --- livelog/logger.py | 28 ++++++++++++++++++---------- livelog/reader.py | 19 ++++++++++++------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/livelog/logger.py b/livelog/logger.py index d2773b0..e24594a 100644 --- a/livelog/logger.py +++ b/livelog/logger.py @@ -5,12 +5,18 @@ from platform import system from tempfile import gettempdir from datetime import datetime -from colorama import Fore, Style from .errors import * class Logger: - """Main logger object. + """Logging handler. + + Attributes: + LEVELS (dict): Log levels value for fast filtering + enabled (bool): If logging is enabled + erase (bool): If preexisting file should be erased + file (str): Log file path + level (str): Minimum log level to be displayed Raises: LogLevelDoesNotExist: If user provide an unknown log level @@ -20,14 +26,18 @@ class Logger: write to output path """ - _LEVELS = {"ERROR": 3, "WARNING": 2, "INFO": 1, "DEBUG": 0} + _LEVELS = { + "ERROR": 3, + "WARNING": 2, + "INFO": 1, + "DEBUG": 0, + } def __init__( self, file: str = None, level: str = "DEBUG", enabled: bool = True, - colors: bool = True, erase: bool = True, ): """Logger initialization. @@ -36,15 +46,10 @@ def __init__( file (str, optional): Output file path. Defaults to None. level (str, optional): Minimum log level. Defaults to "DEBUG". enabled (bool, optional): Is log enabled ? Defaults to True. - colors (bool, optional): Are colors enabled ? Defaults to True. erase (bool, optional): Should preexisting file be erased ? Defaults to True. - - Raises: - LogLevelDoesNotExist: [description] """ self._enabled = enabled - self._colors = colors self._erase = erase if file is None: @@ -85,7 +90,9 @@ def level(self, value: str): self._level = level def _verify_file(self): - """Verify if the file is a valid log file and clear its preexisting content.""" + """Verify if provided file path is a valid log file and clear its + preexisting content. + """ dir = self._file.parent.resolve() if self._file.is_dir(): @@ -108,6 +115,7 @@ def _write(self, level: str, content: str): """Write provided content to output file. Args: + level (str); Log level content (str): Content to be written """ diff --git a/livelog/reader.py b/livelog/reader.py index 22877e5..5eddc5f 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -14,12 +14,19 @@ class Reader(FileSystemEventHandler): Attributes: CLEAR_CMD (str): OS specific command to clear terminal LEVEL_COLORS (dict): Log levels corresponding colors - LONGlevel_TO_SHORT (dict): Log level long to short format + LONG_LEVEL_TO_SHORT (dict): Log level long to short format LEVELS (dict): Log levels value for fast filtering file (str): Log file path level (str): Minimum log level to be displayed nocolors (bool): If colors should not be printed read_index (int): Current position of the cursor in the log file + + Raises: + LogLevelDoesNotExist: If user provide an unknown log level + LogFileIsADirectory: If user provide a directory as output path + LogPathDoesNotExist: If user provide a non existing output path + LogPathInsufficientPermissions: If user does not have permissions to + write to output path """ CLEAR_CMD = "cls" if name == "nt" else "clear" @@ -29,7 +36,7 @@ class Reader(FileSystemEventHandler): "INFO": Fore.BLUE, "DBUG": Fore.WHITE, } - LONGlevel_TO_SHORT = { + LONG_LEVEL_TO_SHORT = { "ERROR": "ERR!", "WARN": "WARNING", "INFO": "INFO", @@ -49,16 +56,14 @@ def __init__(self, file: str, level: str, nocolors: bool): file (str): Path of file to monitor level (str): Minimum log level to be displayed nocolors (bool): If colors should not be printed - Raises: - LogLevelDoesNotExist: If provided log level is not listed. """ self.file = file self.verify_file() level = level.upper() - if level not in self.LONGlevel_TO_SHORT: + if level not in self.LONG_LEVEL_TO_SHORT: raise LogLevelDoesNotExist(level) - self.level = self.LONGlevel_TO_SHORT[level] + self.level = self.LONG_LEVEL_TO_SHORT[level] self.nocolors = nocolors self.read_index = 0 @@ -72,7 +77,7 @@ def __init__(self, file: str, level: str, nocolors: bool): self.on_modified() def verify_file(self): - """Verify if the file is a valid log file.""" + """Verify if provided file path is a valid log file.""" dir = self.file.parent.resolve() if self.file.is_dir(): From 70595a0b49b61cef886dfe65d3b9d0568d717ea7 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Sat, 2 Oct 2021 11:06:36 +0200 Subject: [PATCH 15/16] Clean and add doctrings to `__main__` --- livelog/__main__.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index 8e7516d..6684ffe 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -1,25 +1,45 @@ # -*- coding: utf-8 -*- -from sys import argv from platform import system from tempfile import gettempdir from pathlib import Path from argparse import ArgumentParser from livelog.reader import start_reader -from livelog.logger import Logger -def parse_args(): +def _parse_args(): + """Parse CLI arguments. + + Returns: + tuple: Provided arguments + """ + parser = ArgumentParser(description="Live read a log file") - parser.add_argument("-f", "--file", action="store", type=str, required=False) parser.add_argument( - "-l", "--level", action="store", type=str, default="DEBUG", required=False + "-f", + "--file", + action="store", + type=str, + required=False, + help="Log file to be read", + ) + parser.add_argument( + "-l", + "--level", + action="store", + type=str, + default="DEBUG", + required=False, + help="Minimum log level. Default: DEBUG", + ) + parser.add_argument( + "--nocolors", + action="store_true", + required=False, + help="Do not color lines", ) - parser.add_argument("--nocolors", action="store_true", required=False) args = parser.parse_args() - print(args) - if args.file is not None: file = Path(args.file) else: @@ -33,6 +53,5 @@ def parse_args(): if __name__ == "__main__": - file, level, nocolors = parse_args() - + file, level, nocolors = _parse_args() start_reader(file=file, level=level, nocolors=nocolors) From 564c92343b2297c3cdc0346521273bf3ea95c505 Mon Sep 17 00:00:00 2001 From: PabloLec Date: Sat, 2 Oct 2021 11:16:39 +0200 Subject: [PATCH 16/16] Handle inotify related exception --- livelog/__main__.py | 4 ++-- livelog/reader.py | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/livelog/__main__.py b/livelog/__main__.py index 6684ffe..0f2792a 100644 --- a/livelog/__main__.py +++ b/livelog/__main__.py @@ -43,8 +43,8 @@ def _parse_args(): if args.file is not None: file = Path(args.file) else: - file = Path( - "/tmp/livelog.log" + file = ( + Path("/tmp/livelog.log") if system() == "Darwin" else Path(gettempdir()) / "livelog.log" ) diff --git a/livelog/reader.py b/livelog/reader.py index 5eddc5f..1b38e34 100644 --- a/livelog/reader.py +++ b/livelog/reader.py @@ -94,7 +94,7 @@ def file_exists(self): bool: File exists """ - return self.file.isfile() + return self.file.is_file() def print_output(self): """Drive the printing process by getting new lines, filtering log @@ -177,6 +177,13 @@ def on_modified(self, *args, **kwargs): self.print_output() + def loop_without_event(self): + """If inotify instance limit reached, loop without watching file.""" + + while True: + self.print_output() + sleep(1) + def start_reader(file: str, level: str, nocolors: bool): """Start reader process. @@ -190,8 +197,11 @@ def start_reader(file: str, level: str, nocolors: bool): event_handler = Reader(file=file, level=level, nocolors=nocolors) observer = Observer() observer.schedule(event_handler, file, recursive=True) - observer.start() - - input("") - observer.stop() - observer.join() + try: + observer.start() + input("") + observer.stop() + observer.join() + except OSError: + # Handle "OSError: [Errno 24] inotify instance limit reached" exception + event_handler.loop_without_event()