From 393ea090c92a367230a6364f8228ea1c1c459cf3 Mon Sep 17 00:00:00 2001 From: Pavel Dubrovsky Date: Mon, 14 Oct 2024 14:00:01 +0400 Subject: [PATCH] Fix status inheritance (#125) * save current code state * added Singleton metaclass, Status converted to singleton * Move Status to the own namespace * Remove Status from the inheritance * Split Mood enum to the own file * Status renamed to Logger * Introduce and implement LoggerMixin stub instead of previous Status implementation * Implementing own logging model * tests for LoggerMixin * tests for SelectiveLogger * Code formatted * Logger adjusments * Delete Logger and tests * Use different formatters for stdout/file logging * Return previous status logging code * Comment debug code * remove default argument * PEP 8 style * type hint * set_position() fixed for negative coordinates * tests for LoggerMixin * LoggerMixin renamed to StatusMixin * Status documentation part removed from documentation --- docs/modules.md | 4 - sinner/BatchProcessingCore.py | 7 +- sinner/Benchmark.py | 6 +- sinner/Singleton.py | 10 ++ sinner/Status.py | 126 ------------------ sinner/gui/GUIForm.py | 5 +- sinner/gui/GUIModel.py | 14 +- sinner/handlers/frame/BaseFrameHandler.py | 6 +- sinner/handlers/frame/CV2VideoHandler.py | 2 +- sinner/handlers/frame/FFmpegVideoHandler.py | 4 +- sinner/handlers/frame/ImageHandler.py | 2 +- sinner/models/State.py | 7 +- sinner/models/audio/BaseAudioBackend.py | 5 +- sinner/models/audio/PygameAudioBackend.py | 2 +- sinner/models/status/Mood.py | 11 ++ sinner/models/status/StatusMixin.py | 64 +++++++++ sinner/processors/frame/BaseFrameProcessor.py | 6 +- sinner/processors/frame/FaceSwapper.py | 4 +- sinner/processors/frame/FrameExtractor.py | 6 - sinner/validators/AttributeDocumenter.py | 2 - sinner/webcam/WebCam.py | 7 +- tests/models/status/test_status_mixin.py | 74 ++++++++++ tests/test_status.py | 45 ------- 23 files changed, 199 insertions(+), 220 deletions(-) create mode 100644 sinner/Singleton.py delete mode 100644 sinner/Status.py create mode 100644 sinner/models/status/Mood.py create mode 100644 sinner/models/status/StatusMixin.py create mode 100644 tests/models/status/test_status_mixin.py delete mode 100644 tests/test_status.py diff --git a/docs/modules.md b/docs/modules.md index 11c8283e..8ea589b4 100644 --- a/docs/modules.md +++ b/docs/modules.md @@ -16,10 +16,6 @@ * `--processors`, `--frame-processor`, `--processor`: the frame processor module or modules that you want to apply to your files. See the [Built-in frame processors](../README.md#built-in-frame-processors) documentation for the list of built-in modules and their possibilities. * `--keep-frames`: keeps processed frames in the temp directory after finishing. Defaults to `false`. -# Status: The status messaging module -* `--logfile`, `--log`: optional path to a logfile where all status messages will be logged (if ignored, no logs will be stored). -* `--enable-emoji`: enable modules emoji prefixes in their message statuses, if supported in the current console. -* # GUI: GUI module * `--frames-widget`, `--show-frames-widget`: show processed frames widget. It shows all stages of selected frame processing. * `--frames-widget-width`, `--fw-width`: processed widget maximum width, -1 to set as 10% of original image size. diff --git a/sinner/BatchProcessingCore.py b/sinner/BatchProcessingCore.py index 0b05d9ed..e6f1c35e 100644 --- a/sinner/BatchProcessingCore.py +++ b/sinner/BatchProcessingCore.py @@ -9,19 +9,20 @@ from tqdm import tqdm from sinner.models.State import State -from sinner.Status import Status, Mood from sinner.handlers.frame.BaseFrameHandler import BaseFrameHandler from sinner.handlers.frame.DirectoryHandler import DirectoryHandler from sinner.handlers.frame.ImageHandler import ImageHandler from sinner.handlers.frame.VideoHandler import VideoHandler from sinner.models.NumberedFrame import NumberedFrame +from sinner.models.status.StatusMixin import StatusMixin +from sinner.models.status.Mood import Mood from sinner.processors.frame.BaseFrameProcessor import BaseFrameProcessor from sinner.typing import Frame from sinner.utilities import list_class_descendants, resolve_relative_path, is_image, is_video, get_mem_usage, suggest_max_memory, path_exists, is_dir, normalize_path, suggest_execution_threads, suggest_temp_dir -from sinner.validators.AttributeLoader import Rules +from sinner.validators.AttributeLoader import Rules, AttributeLoader -class BatchProcessingCore(Status): +class BatchProcessingCore(AttributeLoader, StatusMixin): target_path: str output_path: str frame_processor: List[str] diff --git a/sinner/Benchmark.py b/sinner/Benchmark.py index ff365af8..87787b06 100644 --- a/sinner/Benchmark.py +++ b/sinner/Benchmark.py @@ -10,12 +10,12 @@ import torch from sinner.BatchProcessingCore import BatchProcessingCore -from sinner.Status import Status +from sinner.models.status.StatusMixin import StatusMixin from sinner.utilities import resolve_relative_path, get_app_dir, suggest_execution_providers, decode_execution_providers, list_class_descendants -from sinner.validators.AttributeLoader import Rules +from sinner.validators.AttributeLoader import Rules, AttributeLoader -class Benchmark(Status): +class Benchmark(AttributeLoader, StatusMixin): emoji: str = '๐Ÿ“' source_path: str diff --git a/sinner/Singleton.py b/sinner/Singleton.py new file mode 100644 index 00000000..26481fcc --- /dev/null +++ b/sinner/Singleton.py @@ -0,0 +1,10 @@ +from typing import Any + + +class Singleton(type): + _instances: dict[type, Any] = {} + + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/sinner/Status.py b/sinner/Status.py deleted file mode 100644 index 78b8289f..00000000 --- a/sinner/Status.py +++ /dev/null @@ -1,126 +0,0 @@ -import locale -import logging -import shutil -import sys -from enum import Enum - -from colorama import Fore, Back - -from sinner.validators.AttributeLoader import AttributeLoader, Rules - - -class Mood(Enum): - GOOD = (0, f'{Fore.LIGHTWHITE_EX}{Back.BLACK}') - BAD = (1, f'{Fore.BLACK}{Back.RED}') - NEUTRAL = (2, f'{Fore.YELLOW}{Back.BLACK}') - - def __str__(self) -> str: - return self.value[1] - - -class Status(AttributeLoader): - logfile: str | None = None - logger: logging.Logger | None = None - emoji: str = '๐Ÿ˜ˆ' - enable_emoji: bool - - def rules(self) -> Rules: - return [ - { - 'parameter': {'log', 'logfile'}, - 'attribute': 'logfile', - 'default': None, - 'valid': lambda attribute, value: self.init_logger(value), - 'help': 'Path to the log file' - }, - { - 'parameter': {'enable-emoji'}, - 'attribute': 'enable_emoji', - 'default': lambda: self.is_emoji_supported(), - 'help': 'Enable emojis in status messages' - }, - { - 'module_help': 'The status messaging module' - } - ] - - @staticmethod - def set_position(position: tuple[int, int] | None = None) -> None: - if position is not None: - y = position[0] - x = position[1] - if y < 0 or x < 0: - terminal_size = shutil.get_terminal_size() - lines, columns = terminal_size.lines, terminal_size.columns - if y < 0: - y = lines - y - if x < 0: - x = columns - x - - sys.stdout.write(f"\033[{y};{x}H") - - @staticmethod - def restore_position(position: tuple[int, int] | None = None) -> None: - if position is not None: - sys.stdout.write("\033[u") - - @staticmethod - def is_emoji_supported() -> bool: - try: - return locale.getpreferredencoding().lower() == "utf-8" - except Exception: - return False - - def update_status(self, message: str, caller: str | None = None, mood: Mood = Mood.GOOD, emoji: str | None = None, position: tuple[int, int] | None = None) -> None: - """ - Print the specified status message - :param message: the status message text - :param caller: the caller class name, None to a current class name - :param mood: the mood of the message (good, bad, neutral) - :param emoji: prefix emoji. Note: emoji may be skipped, if not supported in the current terminal - :param position: output position as (line, column). Negative values interprets as positions from the bottom/right - side of the console. Skip to print status at the current cursor position. - """ - if self.enable_emoji: - if emoji is None: - emoji = self.emoji - else: - emoji = '' - if caller is None: - caller = self.__class__.__name__ - self.set_position(position) - sys.stdout.write(f'{emoji}{mood}{caller}: {message}{Back.RESET}{Fore.RESET}') - if position is None: - sys.stdout.write("\n") - self.restore_position(position) - log_level = logging.DEBUG - if mood is Mood.GOOD: - log_level = logging.INFO - elif mood is Mood.BAD: - log_level = logging.ERROR - self.log(level=log_level, msg=f"{emoji}{caller}: {message}") - - def log(self, level: int = logging.INFO, msg: str = "") -> None: - if self.logger: - self.logger.log(level, msg) - - def init_logger(self, value: str) -> bool: - try: - if value and not self.logger: - self.logger = logging.getLogger(__name__) - self.logger.setLevel(logging.DEBUG) - - file_handler = logging.FileHandler(value, encoding='utf-8', mode='w') - file_handler.setLevel(logging.DEBUG) - - formatter = logging.Formatter('%(levelname)s: %(message)s') - file_handler.setFormatter(formatter) - - self.logger.addHandler(file_handler) - # - # logging.getLogger('PIL.PngImagePlugin').setLevel(logging.CRITICAL + 1) - # logging.getLogger('PIL.PngImagePlugin').addHandler(logging.NullHandler()) - return True - except Exception: - pass - return False diff --git a/sinner/gui/GUIForm.py b/sinner/gui/GUIForm.py index 0dd03fe2..a887a1dc 100644 --- a/sinner/gui/GUIForm.py +++ b/sinner/gui/GUIForm.py @@ -9,7 +9,6 @@ from sinner.gui.controls.FramePlayer.BaseFramePlayer import ROTATE_90_CLOCKWISE, ROTATE_180, ROTATE_90_COUNTERCLOCKWISE from sinner.models.Event import Event as SinnerEvent -from sinner.Status import Status from sinner.gui.GUIModel import GUIModel from sinner.gui.controls.FramePosition.BaseFramePosition import BaseFramePosition from sinner.gui.controls.FramePosition.SliderFramePosition import SliderFramePosition @@ -21,12 +20,12 @@ from sinner.models.Config import Config from sinner.models.audio.BaseAudioBackend import BaseAudioBackend from sinner.utilities import is_int, get_app_dir, get_type_extensions, is_image, is_dir, get_directory_file_list, halt -from sinner.validators.AttributeLoader import Rules +from sinner.validators.AttributeLoader import Rules, AttributeLoader # GUI View -class GUIForm(Status): +class GUIForm(AttributeLoader): # class attributes parameters: Namespace GUIModel: GUIModel diff --git a/sinner/gui/GUIModel.py b/sinner/gui/GUIModel.py index da32ae14..bd69b4b7 100644 --- a/sinner/gui/GUIModel.py +++ b/sinner/gui/GUIModel.py @@ -3,12 +3,10 @@ import time from argparse import Namespace from concurrent.futures import ThreadPoolExecutor, Future -from logging import DEBUG from tkinter import IntVar from typing import List, Callable, Any from sinner.BatchProcessingCore import BatchProcessingCore -from sinner.Status import Status, Mood from sinner.gui.controls.FramePlayer.BaseFramePlayer import BaseFramePlayer from sinner.gui.controls.FramePlayer.PygameFramePlayer import PygameFramePlayer from sinner.gui.controls.ProgressBarManager import ProgressBarManager @@ -23,17 +21,19 @@ from sinner.models.PerfCounter import PerfCounter from sinner.models.State import State from sinner.models.audio.BaseAudioBackend import BaseAudioBackend +from sinner.models.status.StatusMixin import StatusMixin +from sinner.models.status.Mood import Mood from sinner.processors.frame.BaseFrameProcessor import BaseFrameProcessor from sinner.processors.frame.FrameExtractor import FrameExtractor from sinner.typing import FramesList from sinner.utilities import list_class_descendants, resolve_relative_path, suggest_execution_threads, suggest_temp_dir, seconds_to_hmsms, normalize_path, get_mem_usage -from sinner.validators.AttributeLoader import Rules +from sinner.validators.AttributeLoader import Rules, AttributeLoader BUFFERING_PROGRESS_NAME = "Buffering" EXTRACTING_PROGRESS_NAME = "Extracting" -class GUIModel(Status): +class GUIModel(AttributeLoader, StatusMixin): # configuration variables frame_processor: List[str] _source_path: str @@ -456,7 +456,7 @@ def process_done(future_: Future[tuple[float, int] | None]) -> None: if step < 1: # preventing going backwards step = 1 next_frame += step - self.log(level=DEBUG, msg=f"NEXT: {next_frame}, STEP: {step}, DELTA: {processing_delta}, LAST: {self.TimeLine.last_added_index}, AVG: {self._average_frame_skip.get_average()} ") + # self.status.debug(msg=f"NEXT: {next_frame}, STEP: {step}, DELTA: {processing_delta}, LAST: {self.TimeLine.last_added_index}, AVG: {self._average_frame_skip.get_average()} ") def _process_frame(self, frame_index: int) -> tuple[float, int] | None: """ @@ -473,7 +473,7 @@ def _process_frame(self, frame_index: int) -> tuple[float, int] | None: with PerfCounter() as frame_render_time: for _, processor in self.processors.items(): n_frame.frame = processor.process_frame(n_frame.frame) - self.log(level=DEBUG, msg=f"DONE: {n_frame.index}") + # self.status.debug(msg=f"DONE: {n_frame.index}") self.TimeLine.add_frame(n_frame) return frame_render_time.execution_time, n_frame.index @@ -489,7 +489,7 @@ def _show_frames(self) -> None: break if n_frame is not None: if n_frame.index != last_shown_frame_index: # check if frame is really changed - self.log(level=DEBUG, msg=f"REQ: {self.TimeLine.last_requested_index}, SHOW: {n_frame.index}, ASYNC: {self.TimeLine.last_requested_index - n_frame.index}") + # self.status.debug(msg=f"REQ: {self.TimeLine.last_requested_index}, SHOW: {n_frame.index}, ASYNC: {self.TimeLine.last_requested_index - n_frame.index}") self.Player.show_frame(n_frame.frame) last_shown_frame_index = n_frame.index if self.TimeLine.last_returned_index is None: diff --git a/sinner/handlers/frame/BaseFrameHandler.py b/sinner/handlers/frame/BaseFrameHandler.py index 588f6e1b..ae2d6ce4 100644 --- a/sinner/handlers/frame/BaseFrameHandler.py +++ b/sinner/handlers/frame/BaseFrameHandler.py @@ -4,14 +4,14 @@ from argparse import Namespace from typing import List -from sinner.Status import Status from sinner.models.NumberedFrame import NumberedFrame -from sinner.validators.AttributeLoader import Rules +from sinner.models.status.StatusMixin import StatusMixin +from sinner.validators.AttributeLoader import Rules, AttributeLoader from sinner.typing import NumeratedFramePath from sinner.utilities import load_class, get_file_name, is_file, normalize_path -class BaseFrameHandler(Status, ABC): +class BaseFrameHandler(AttributeLoader, ABC, StatusMixin): current_frame_index: int = 0 _target_path: str diff --git a/sinner/handlers/frame/CV2VideoHandler.py b/sinner/handlers/frame/CV2VideoHandler.py index 265eb97a..7d44a52d 100644 --- a/sinner/handlers/frame/CV2VideoHandler.py +++ b/sinner/handlers/frame/CV2VideoHandler.py @@ -8,7 +8,7 @@ from cv2 import VideoCapture from tqdm import tqdm -from sinner.Status import Mood +from sinner.models.status.Mood import Mood from sinner.handlers.frame.BaseFrameHandler import BaseFrameHandler from sinner.handlers.frame.EOutOfRange import EOutOfRange from sinner.helpers.FrameHelper import write_to_image, read_from_image diff --git a/sinner/handlers/frame/FFmpegVideoHandler.py b/sinner/handlers/frame/FFmpegVideoHandler.py index a3c766fd..ab736fc8 100644 --- a/sinner/handlers/frame/FFmpegVideoHandler.py +++ b/sinner/handlers/frame/FFmpegVideoHandler.py @@ -8,10 +8,10 @@ import cv2 from numpy import uint8, frombuffer -from sinner.Status import Mood from sinner.handlers.frame.BaseFrameHandler import BaseFrameHandler from sinner.handlers.frame.EOutOfRange import EOutOfRange from sinner.models.NumberedFrame import NumberedFrame +from sinner.models.status.Mood import Mood from sinner.typing import NumeratedFramePath from sinner.validators.AttributeLoader import Rules @@ -93,7 +93,7 @@ def resolution(self) -> tuple[int, int]: if self._resolution is None: try: command = ['ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height', '-of', 'csv=s=x:p=0', self._target_path] - output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode('utf-8').strip() # can be very slow! + output = subprocess.check_output(command, stderr=subprocess.STDOUT).decode().strip() # can be very slow! if 'N/A' == output: self._resolution = (0, 0) # non-frame files, still processable w, h = output.split('x') diff --git a/sinner/handlers/frame/ImageHandler.py b/sinner/handlers/frame/ImageHandler.py index 8b4144c0..bf72a085 100644 --- a/sinner/handlers/frame/ImageHandler.py +++ b/sinner/handlers/frame/ImageHandler.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import List -from sinner.Status import Mood +from sinner.models.status.Mood import Mood from sinner.handlers.frame.BaseFrameHandler import BaseFrameHandler from sinner.handlers.frame.EOutOfRange import EOutOfRange from sinner.helpers.FrameHelper import read_from_image diff --git a/sinner/models/State.py b/sinner/models/State.py index b232738a..6c91cc25 100644 --- a/sinner/models/State.py +++ b/sinner/models/State.py @@ -3,14 +3,15 @@ from pathlib import Path from typing import Any, Dict, List -from sinner.Status import Status, Mood from sinner.helpers.FrameHelper import write_to_image, EmptyFrame from sinner.models.NumberedFrame import NumberedFrame +from sinner.models.status.StatusMixin import StatusMixin +from sinner.models.status.Mood import Mood from sinner.utilities import is_absolute_path, format_sequences, path_exists, is_file, normalize_path -from sinner.validators.AttributeLoader import Rules +from sinner.validators.AttributeLoader import Rules, AttributeLoader -class State(Status): +class State(AttributeLoader, StatusMixin): emoji: str = '๐Ÿ‘€' source_path: str | None = None initial_target_path: str | None = None diff --git a/sinner/models/audio/BaseAudioBackend.py b/sinner/models/audio/BaseAudioBackend.py index 84f8bf3f..9e5dfd3c 100644 --- a/sinner/models/audio/BaseAudioBackend.py +++ b/sinner/models/audio/BaseAudioBackend.py @@ -3,11 +3,12 @@ from argparse import Namespace from typing import Any -from sinner.Status import Status +from sinner.models.status.StatusMixin import StatusMixin from sinner.utilities import normalize_path, load_class, list_class_descendants +from sinner.validators.AttributeLoader import AttributeLoader -class BaseAudioBackend(Status, ABC): +class BaseAudioBackend(AttributeLoader, ABC, StatusMixin): _media_path: str | None = None @staticmethod diff --git a/sinner/models/audio/PygameAudioBackend.py b/sinner/models/audio/PygameAudioBackend.py index c7761b9c..24b7d699 100644 --- a/sinner/models/audio/PygameAudioBackend.py +++ b/sinner/models/audio/PygameAudioBackend.py @@ -5,7 +5,7 @@ from moviepy.editor import AudioFileClip import pygame -from sinner.Status import Mood +from sinner.models.status.Mood import Mood from sinner.models.audio.BaseAudioBackend import BaseAudioBackend from sinner.utilities import get_file_name, normalize_path diff --git a/sinner/models/status/Mood.py b/sinner/models/status/Mood.py new file mode 100644 index 00000000..4878e49d --- /dev/null +++ b/sinner/models/status/Mood.py @@ -0,0 +1,11 @@ +from enum import Enum +from colorama import Fore, Back + + +class Mood(Enum): + GOOD = (0, f'{Fore.LIGHTWHITE_EX}{Back.BLACK}') + BAD = (1, f'{Fore.BLACK}{Back.RED}') + NEUTRAL = (2, f'{Fore.YELLOW}{Back.BLACK}') + + def __str__(self) -> str: + return self.value[1] diff --git a/sinner/models/status/StatusMixin.py b/sinner/models/status/StatusMixin.py new file mode 100644 index 00000000..b1a30e31 --- /dev/null +++ b/sinner/models/status/StatusMixin.py @@ -0,0 +1,64 @@ +import locale +import shutil +import sys + +from colorama import Back, Fore + +from sinner.models.status.Mood import Mood + + +class StatusMixin: + enable_emoji: bool = False + emoji: str + + @staticmethod + def set_position(position: tuple[int, int] | None = None) -> None: + if position is not None: + y = position[0] + x = position[1] + if y < 0 or x < 0: + terminal_size = shutil.get_terminal_size() + lines, columns = terminal_size.lines, terminal_size.columns + if y < 0: + y += lines + if x < 0: + x += columns + + sys.stdout.write(f"\033[{y};{x}H") + + @staticmethod + def restore_position(position: tuple[int, int] | None = None) -> None: + if position is not None: + sys.stdout.write("\033[u") + + @staticmethod + def is_emoji_supported() -> bool: + try: + return locale.getpreferredencoding().lower() == "utf-8" + except Exception: + return False + + def update_status(self, message: str, caller: str | None = None, mood: Mood = Mood.GOOD, emoji: str | None = None, position: tuple[int, int] | None = None) -> None: + """ + Print the specified status message + :param message: the status message text + :param caller: the caller class name, None to a current class name + :param mood: the mood of the message (good, bad, neutral) + :param emoji: prefix emoji. Note: emoji may be skipped, if not supported in the current terminal + :param position: output position as (line, column). Negative values interprets as positions from the bottom/right + side of the console. Skip to print status at the current cursor position. + """ + if self.enable_emoji: + if emoji is None: + emoji = self.emoji + else: + emoji = '' + if caller is None: + caller = self.__class__.__name__ + self.set_position(position) + sys.stdout.write(f'{emoji}{mood}{caller}: {message}{Back.RESET}{Fore.RESET}') + if position is None: + sys.stdout.write("\n") + self.restore_position(position) + + diff --git a/sinner/processors/frame/BaseFrameProcessor.py b/sinner/processors/frame/BaseFrameProcessor.py index 2fa03d73..e2210f3d 100644 --- a/sinner/processors/frame/BaseFrameProcessor.py +++ b/sinner/processors/frame/BaseFrameProcessor.py @@ -6,13 +6,13 @@ from sinner.handlers.frame.BaseFrameHandler import BaseFrameHandler from sinner.models.State import State -from sinner.Status import Status -from sinner.validators.AttributeLoader import Rules +from sinner.models.status.StatusMixin import StatusMixin +from sinner.validators.AttributeLoader import Rules, AttributeLoader from sinner.typing import Frame from sinner.utilities import load_class, suggest_execution_providers, decode_execution_providers -class BaseFrameProcessor(ABC, Status): +class BaseFrameProcessor(ABC, AttributeLoader, StatusMixin): execution_provider: List[str] self_processing: bool = False diff --git a/sinner/processors/frame/FaceSwapper.py b/sinner/processors/frame/FaceSwapper.py index 187b48dc..292d3636 100644 --- a/sinner/processors/frame/FaceSwapper.py +++ b/sinner/processors/frame/FaceSwapper.py @@ -10,7 +10,7 @@ from insightface.app.common import Face from sinner.FaceAnalyser import FaceAnalyser -from sinner.Status import Mood +from sinner.models.status.Mood import Mood from sinner.helpers.FrameHelper import read_from_image from sinner.validators.AttributeLoader import Rules from sinner.processors.frame.BaseFrameProcessor import BaseFrameProcessor @@ -147,4 +147,4 @@ def release_resources(self) -> None: def configure_output_filename(self, callback: Callable[[str], None]) -> None: source_name, _ = os.path.splitext(os.path.basename(self.source_path)) - callback(source_name) \ No newline at end of file + callback(source_name) diff --git a/sinner/processors/frame/FrameExtractor.py b/sinner/processors/frame/FrameExtractor.py index 36924a12..ba372c39 100644 --- a/sinner/processors/frame/FrameExtractor.py +++ b/sinner/processors/frame/FrameExtractor.py @@ -1,11 +1,9 @@ import os -from argparse import Namespace from tqdm import tqdm from sinner.handlers.frame.BaseFrameHandler import BaseFrameHandler from sinner.models.State import State -from sinner.Status import Status from sinner.typing import Frame from sinner.validators.AttributeLoader import Rules from sinner.processors.frame.BaseFrameProcessor import BaseFrameProcessor @@ -22,10 +20,6 @@ def rules(self) -> Rules: } ] - def __init__(self, parameters: Namespace) -> None: - self.parameters = parameters - Status.__init__(self, self.parameters) - def configure_state(self, state: State) -> None: state.path = os.path.abspath(os.path.join(state.temp_dir, self.__class__.__name__, str(os.path.basename(str(state.target_path))))) diff --git a/sinner/validators/AttributeDocumenter.py b/sinner/validators/AttributeDocumenter.py index 4080e92b..4127c91b 100644 --- a/sinner/validators/AttributeDocumenter.py +++ b/sinner/validators/AttributeDocumenter.py @@ -5,7 +5,6 @@ from sinner.Benchmark import Benchmark from sinner.BatchProcessingCore import BatchProcessingCore from sinner.Sinner import Sinner -from sinner.Status import Status from sinner.gui.GUIForm import GUIForm from sinner.gui.GUIModel import GUIModel from sinner.webcam.WebCam import WebCam @@ -20,7 +19,6 @@ DocumentedClasses: List[Type[AttributeLoader]] = [ Sinner, BatchProcessingCore, - Status, # State, GUIForm, GUIModel, diff --git a/sinner/webcam/WebCam.py b/sinner/webcam/WebCam.py index 5fdb31f3..1553a133 100644 --- a/sinner/webcam/WebCam.py +++ b/sinner/webcam/WebCam.py @@ -11,18 +11,19 @@ from psutil import WINDOWS, LINUX, MACOS from pyvirtualcam import Camera -from sinner.Status import Status, Mood from sinner.models.PerfCounter import PerfCounter +from sinner.models.status.StatusMixin import StatusMixin +from sinner.models.status.Mood import Mood from sinner.processors.frame.BaseFrameProcessor import BaseFrameProcessor from sinner.typing import Frame from sinner.utilities import list_class_descendants, resolve_relative_path, is_image, is_video -from sinner.validators.AttributeLoader import Rules +from sinner.validators.AttributeLoader import Rules, AttributeLoader from sinner.webcam.ImageCamera import ImageCamera from sinner.webcam.NoDevice import NoDevice from sinner.webcam.VideoCamera import VideoCamera -class WebCam(Status): +class WebCam(AttributeLoader, StatusMixin): emoji: str = '๐Ÿคณ' stop: bool = False diff --git a/tests/models/status/test_status_mixin.py b/tests/models/status/test_status_mixin.py new file mode 100644 index 00000000..df52c96a --- /dev/null +++ b/tests/models/status/test_status_mixin.py @@ -0,0 +1,74 @@ +import pytest +from unittest.mock import patch, MagicMock +from io import StringIO + +from sinner.models.status.StatusMixin import StatusMixin +from sinner.models.status.Mood import Mood + + +class TestStatusMixin: + @pytest.fixture + def logger(self): + class TestStatus(StatusMixin): + pass + + return TestStatus() + + def test_set_position(self): + with patch('sys.stdout.write') as mock_write: + StatusMixin.set_position((5, 10)) + mock_write.assert_called_once_with("\033[5;10H") + + def test_set_position_negative(self): + with patch('sys.stdout.write') as mock_write, \ + patch('shutil.get_terminal_size') as mock_get_terminal_size: + mock_get_terminal_size.return_value = MagicMock(lines=24, columns=80) + StatusMixin.set_position((-1, -1)) + mock_write.assert_called_once_with("\033[23;79H") + + def test_restore_position(self): + with patch('sys.stdout.write') as mock_write: + StatusMixin.restore_position((5, 10)) + mock_write.assert_called_once_with("\033[u") + + @pytest.mark.parametrize("encoding,expected", [ + ("utf-8", True), + ("ascii", False), + ]) + def test_is_emoji_supported(self, encoding, expected): + with patch('locale.getpreferredencoding', return_value=encoding): + assert StatusMixin.is_emoji_supported() == expected + + def test_update_status(self, logger): + logger.enable_emoji = True + logger.emoji = "๐Ÿ“ข" + + with patch('sys.stdout', new=StringIO()) as fake_out: + logger.update_status("Test message", mood=Mood.GOOD) + assert "๐Ÿ“ข" in fake_out.getvalue() + assert "Test message" in fake_out.getvalue() + assert logger.__class__.__name__ in fake_out.getvalue() + + def test_update_status_without_emoji(self, logger): + logger.enable_emoji = False + + with patch('sys.stdout', new=StringIO()) as fake_out: + logger.update_status("Test message", mood=Mood.BAD) + assert "Test message" in fake_out.getvalue() + assert logger.__class__.__name__ in fake_out.getvalue() + + def test_update_status_with_position(self, logger): + with patch('sys.stdout.write') as mock_write: + logger.update_status("Test message", position=(1, 1)) + assert mock_write.call_count == 3 # set_position, message, restore_position + + def test_update_status_with_custom_caller(self, logger): + with patch('sys.stdout', new=StringIO()) as fake_out: + logger.update_status("Test message", caller="CustomCaller") + assert "CustomCaller" in fake_out.getvalue() + + @pytest.mark.parametrize("mood", [Mood.GOOD, Mood.BAD, Mood.NEUTRAL]) + def test_update_status_moods(self, logger, mood): + with patch('sys.stdout', new=StringIO()) as fake_out: + logger.update_status("Test message", mood=mood) + assert str(mood) in fake_out.getvalue() diff --git a/tests/test_status.py b/tests/test_status.py deleted file mode 100644 index ac2411b5..00000000 --- a/tests/test_status.py +++ /dev/null @@ -1,45 +0,0 @@ -from argparse import Namespace - -import pytest -from colorama import Fore, Back - -from sinner.Parameters import Parameters -from sinner.Status import Status, Mood -from sinner.typing import UTF8 -from sinner.validators.LoaderException import LoadingException -from tests.constants import test_logfile - - -def test_status(capsys) -> None: - status = Status(Namespace()) - - status.update_status('test', 'self', Mood.BAD) - captured = capsys.readouterr() - captured = captured.out.strip() - assert captured.find(f'{Fore.BLACK}{Back.RED}self: test{Back.RESET}{Fore.RESET}') != -1 - - -def test_status_force_emoji() -> None: - parameters: Namespace = Parameters(f'--log="{test_logfile}" --enable_emoji=1').parameters - status = Status(parameters=parameters) - - status.update_status('test', 'self', Mood.BAD) - with open(test_logfile, encoding=UTF8) as file: - actual_content = file.read() - assert actual_content.find('๐Ÿ˜ˆself: test') != -1 - - -def test_status_log() -> None: - parameters: Namespace = Parameters(f'--log="{test_logfile}" --enable_emoji=0').parameters - status = Status(parameters=parameters) - - status.update_status('test', 'self', Mood.BAD) - with open(test_logfile, encoding=UTF8) as file: - actual_content = file.read() - assert actual_content.find('self: test') != -1 - - -def test_status_error() -> None: - parameters: Namespace = Parameters(f'--log="/dev/random/incorrect:file\\path*?"').parameters - with pytest.raises(LoadingException): - assert Status(parameters=parameters)