From 4ca45ebc7398c223fff493b98cea2821e124b8b0 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 11 Jul 2025 19:06:49 +0900 Subject: [PATCH] Move var helpers into their own file, log update with additional levels Add levels for ALERT, EMERGENCY to be syslog compatible Add direct wrappers for all, but they are not yet fully usable because the stack fix is not yet implemented Add a new debug helepr to get the stack as a string --- ToDo.md | 5 +- pyproject.toml | 3 + .../config_handling/settings_loader.py | 2 +- src/corelibs/debug_handling/debug_helpers.py | 33 +++ src/corelibs/logging_handling/log.py | 149 +++++++++-- .../logging_level_handling/logging_level.py | 6 +- .../string_handling/string_helpers.py | 59 ----- src/corelibs/var_handling/__init__.py | 0 src/corelibs/var_handling/var_helpers.py | 65 +++++ test-run/logging_handling/log.py | 14 +- test-run/logging_handling/log_pool.py | 4 +- test-run/logging_handling/log_queue.py | 5 +- .../test_logging_level.py | 43 +++- .../string_handling/test_string_helpers.py | 237 +---------------- .../unit/string_handling/test_var_helpers.py | 241 ++++++++++++++++++ 15 files changed, 522 insertions(+), 344 deletions(-) create mode 100644 src/corelibs/debug_handling/debug_helpers.py create mode 100644 src/corelibs/var_handling/__init__.py create mode 100644 src/corelibs/var_handling/var_helpers.py create mode 100644 tests/unit/string_handling/test_var_helpers.py diff --git a/ToDo.md b/ToDo.md index bad72e4..8e27e5f 100644 --- a/ToDo.md +++ b/ToDo.md @@ -1,4 +1,5 @@ # ToDo list -- stub files .pyi -- fix all remaning check errors +- [ ] stub files .pyi +- [ ] Add tests for all, we need 100% test coverate +- [ ] Log: add custom format for "stack_correct" if set, this will override the normal stack block diff --git a/pyproject.toml b/pyproject.toml index 0f341f8..9f76985 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,9 @@ notes = ["FIXME", "TODO"] notes-rgx = '(FIXME|TODO)(\((TTD-|#)\[0-9]+\))' [tool.flake8] max-line-length = 120 +ignore = [ + "E741" # ignore ambigious variable name +] [tool.pylint.MASTER] # this is for the tests/etc folders init-hook='import sys; sys.path.append("src/")' diff --git a/src/corelibs/config_handling/settings_loader.py b/src/corelibs/config_handling/settings_loader.py index 308dadb..8d8f130 100644 --- a/src/corelibs/config_handling/settings_loader.py +++ b/src/corelibs/config_handling/settings_loader.py @@ -11,7 +11,7 @@ from typing import Any, Tuple, Sequence, cast from pathlib import Path from corelibs.logging_handling.log import Log from corelibs.iterator_handling.list_helpers import convert_to_list, is_list_in_list -from corelibs.string_handling.string_helpers import is_int, is_float, str_to_bool +from corelibs.var_handling.var_helpers import is_int, is_float, str_to_bool from corelibs.config_handling.settings_loader_handling.settings_loader_check import SettingsLoaderCheck diff --git a/src/corelibs/debug_handling/debug_helpers.py b/src/corelibs/debug_handling/debug_helpers.py new file mode 100644 index 0000000..a9b14fa --- /dev/null +++ b/src/corelibs/debug_handling/debug_helpers.py @@ -0,0 +1,33 @@ +""" +Various debug helpers +""" + +import traceback +import os + + +def traceback_call_str(start: int = 2, depth: int = 1): + """ + get the trace for the last entry + + Keyword Arguments: + start {int} -- _description_ (default: {2}) + depth {int} -- _description_ (default: {1}) + + Returns: + _type_ -- _description_ + """ + # can't have more than in the stack for depth + depth = min(depth, start) + depth = start - depth + # 0 is full stack length from start + if depth == 0: + stack = traceback.extract_stack()[-start:] + else: + stack = traceback.extract_stack()[-start:-depth] + return ' -> '.join( + f"{os.path.basename(f.filename)}:{f.name}:{f.lineno}" + for f in stack + ) + +# __END__ diff --git a/src/corelibs/logging_handling/log.py b/src/corelibs/logging_handling/log.py index f679fef..9b9798c 100644 --- a/src/corelibs/logging_handling/log.py +++ b/src/corelibs/logging_handling/log.py @@ -9,9 +9,10 @@ import logging.handlers import logging import time from pathlib import Path -from typing import Mapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast +from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel from corelibs.string_handling.text_colors import Colors +from corelibs.debug_handling.debug_helpers import traceback_call_str if TYPE_CHECKING: from multiprocessing import Queue @@ -41,12 +42,14 @@ class CustomConsoleFormatter(logging.Formatter): """ COLORS = { - "DEBUG": Colors.cyan, - "INFO": Colors.green, - "WARNING": Colors.yellow, - "ERROR": Colors.red, - "CRITICAL": Colors.red_bold, - "EXCEPTION": Colors.magenta_bold, # will never be written to console + LoggingLevel.DEBUG.name: Colors.cyan, + LoggingLevel.INFO.name: Colors.green, + LoggingLevel.WARNING.name: Colors.yellow, + LoggingLevel.ERROR.name: Colors.red, + LoggingLevel.CRITICAL.name: Colors.red_bold, + LoggingLevel.ALERT.name: Colors.yellow_bold, + LoggingLevel.EMERGENCY.name: Colors.magenta_bold, + LoggingLevel.EXCEPTION.name: Colors.magenta_bright, # will never be written to console } def format(self, record: logging.LogRecord) -> str: @@ -63,7 +66,7 @@ class CustomConsoleFormatter(logging.Formatter): reset = Colors.reset color = self.COLORS.get(record.levelname, reset) # only highlight level for basic - if record.levelname in ['DEBUG', 'INFO']: + if record.levelname in [LoggingLevel.DEBUG.name, LoggingLevel.INFO.name]: record.levelname = f"{color}{record.levelname}{reset}" return super().format(record) # highlight whole line @@ -71,6 +74,10 @@ class CustomConsoleFormatter(logging.Formatter): return f"{color}{message}{reset}" +# TODO: add custom handlers for stack_correct, if not set fill with %(filename)s:%(funcName)s:%(lineno)d +# hasattr(record, 'stack_correct') + + # MARK: Log class class Log: """ @@ -86,14 +93,14 @@ class Log: DEFAULT_LOG_LEVEL_CONSOLE: LoggingLevel = LoggingLevel.WARNING # default settings DEFAULT_LOG_SETTINGS: LogSettings = { - "log_level_console": LoggingLevel.WARNING, - "log_level_file": LoggingLevel.DEBUG, - "console_enabled": True, - "console_color_output_enabled": True, - "add_start_info": True, - "add_end_info": False, - "log_queue": None, - } + "log_level_console": LoggingLevel.WARNING, + "log_level_file": LoggingLevel.DEBUG, + "console_enabled": True, + "console_color_output_enabled": True, + "add_start_info": True, + "add_end_info": False, + "log_queue": None, + } # MARK: constructor def __init__( @@ -103,8 +110,10 @@ class Log: log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str]'] | LogSettings | None = None, other_handlers: dict[str, Any] | None = None ): - # add new level for EXCEPTION - logging.addLevelName(LoggingLevel.EXCEPTION.value, 'EXCEPTION') + # add new level for alert, emergecny and exception + logging.addLevelName(LoggingLevel.ALERT.value, LoggingLevel.ALERT.name) + logging.addLevelName(LoggingLevel.EMERGENCY.value, LoggingLevel.EMERGENCY.name) + logging.addLevelName(LoggingLevel.EXCEPTION.value, LoggingLevel.EXCEPTION.name) # parse the logging settings self.log_settings = self.__parse_log_settings(log_settings) # if path, set log name with .log @@ -126,7 +135,7 @@ class Log: self.log_queue: 'Queue[str] | None' = None self.listener: logging.handlers.QueueListener | None = None - self.logger: logging.Logger | None = None + self.logger: logging.Logger # setup handlers # NOTE if console with color is set first, some of the color formatting is set @@ -220,7 +229,7 @@ class Log: """ if self.handlers.get(handler_name): return False - if self.listener is not None or self.logger is not None: + if self.listener is not None or hasattr(self, 'logger'): raise ValueError( f"Cannot add handler {handler_name}: {handler.get_name()} because logger is already running" ) @@ -335,6 +344,9 @@ class Log: # set maximum logging level for all logging output # log level filtering is done per handler self.logger.setLevel(logging.DEBUG) + # short name + self.lg = self.logger + self.l = self.logger # MARK: init logger for Fork/Thread @staticmethod @@ -355,18 +367,107 @@ class Log: return root_logger + # FIXME: all below will only work if we add a custom format interface for the stack_correct part + # Important note, although they exist, it is recommended to use self.logger.NAME directly + # so that the correct filename, method and row number is set + # for > 50 use logger.log(LoggingLevel..value, ...) + # for exception logger.log(LoggingLevel.EXCEPTION.value, ..., execInfo=True) # MARK: log message - def exception(self, msg: object, *args: object, extra: Mapping[str, object] | None = None) -> None: + def log(self, level: int, msg: object, *args: object, extra: MutableMapping[str, object] | None = None): + """log general""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.log(level, msg, *args, extra=extra) + + # MARK: DEBUG 10 + def debug(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """debug""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.debug(msg, *args, extra=extra) + + # MARK: INFO 20 + def info(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """info""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.info(msg, *args, extra=extra) + + # MARK: WARNING 30 + def warning(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """warning""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.warning(msg, *args, extra=extra) + + # MARK: ERROR 40 + def error(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """error""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.error(msg, *args, extra=extra) + + # MARK: CRITICAL 50 + def critical(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """critcal""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.critical(msg, *args, extra=extra) + + # MARK: ALERT 55 + def alert(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """alert""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + # extra_dict = dict(extra) + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.log(LoggingLevel.ALERT.value, msg, *args, extra=extra) + + # MARK: EMERGECNY: 60 + def emergency(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: + """emergency""" + if not hasattr(self, 'logger'): + raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) + self.logger.log(LoggingLevel.EMERGENCY.value, msg, *args, extra=extra) + + # MARK: EXCEPTION: 70 + def exception(self, msg: object, *args: object, extra: MutableMapping[str, object] | None = None) -> None: """ - log on exceotion level + log on exceotion level, this is log.exception, but logs with a new level Args: msg (object): _description_ *args (object): arguments for msg extra: Mapping[str, object] | None: extra arguments for the formatting if needed """ - if self.logger is None: + if not hasattr(self, 'logger'): raise ValueError('Logger is not yet initialized') + if extra is None: + extra = {} + extra['stack_correct'] = traceback_call_str(start=3) self.logger.log(LoggingLevel.EXCEPTION.value, msg, *args, exc_info=True, extra=extra) # MARK: break line @@ -377,7 +478,7 @@ class Log: Keyword Arguments: info {str} -- _description_ (default: {"BREAK"}) """ - if self.logger is None: + if not hasattr(self, 'logger'): raise ValueError('Logger is not yet initialized') self.logger.info("[%s] %s>", info, self.SPACER_CHAR * self.SPACER_LENGTH) diff --git a/src/corelibs/logging_handling/logging_level_handling/logging_level.py b/src/corelibs/logging_handling/logging_level_handling/logging_level.py index 87a106b..2f00197 100644 --- a/src/corelibs/logging_handling/logging_level_handling/logging_level.py +++ b/src/corelibs/logging_handling/logging_level_handling/logging_level.py @@ -17,7 +17,9 @@ class LoggingLevel(Enum): WARNING = logging.WARNING # 30 ERROR = logging.ERROR # 40 CRITICAL = logging.CRITICAL # 50 - EXCEPTION = 60 # 60 (manualy set) + ALERT = 55 # 55 (for Sys log) + EMERGENCY = 60 # 60 (for Sys log) + EXCEPTION = 70 # 70 (manualy set, error but with higher level) # Alternative names WARN = logging.WARN # 30 (alias for WARNING) FATAL = logging.FATAL # 50 (alias for CRITICAL) @@ -83,6 +85,6 @@ class LoggingLevel(Enum): def is_lower_than(self, level: 'LoggingLevel'): """if given value is lower than set""" - return self.value > level.value + return self.value < level.value # __END__ diff --git a/src/corelibs/string_handling/string_helpers.py b/src/corelibs/string_handling/string_helpers.py index e470d6e..366c483 100644 --- a/src/corelibs/string_handling/string_helpers.py +++ b/src/corelibs/string_handling/string_helpers.py @@ -2,7 +2,6 @@ String helpers """ -from typing import Any from decimal import Decimal, getcontext from textwrap import shorten @@ -102,62 +101,4 @@ def format_number(number: float, precision: int = 0) -> str: "f}" ).format(_number) - -def is_int(string: Any) -> bool: - """ - check if a value is int - - Arguments: - string {Any} -- _description_ - - Returns: - bool -- _description_ - """ - try: - int(string) - return True - except TypeError: - return False - except ValueError: - return False - - -def is_float(string: Any) -> bool: - """ - check if a value is float - - Arguments: - string {Any} -- _description_ - - Returns: - bool -- _description_ - """ - try: - float(string) - return True - except TypeError: - return False - except ValueError: - return False - - -def str_to_bool(string: str): - """ - convert string to bool - - Arguments: - s {str} -- _description_ - - Raises: - ValueError: _description_ - - Returns: - _type_ -- _description_ - """ - if string == "True" or string == "true": - return True - if string == "False" or string == "false": - return False - raise ValueError(f"Invalid boolean string: {string}") - # __END__ diff --git a/src/corelibs/var_handling/__init__.py b/src/corelibs/var_handling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/corelibs/var_handling/var_helpers.py b/src/corelibs/var_handling/var_helpers.py new file mode 100644 index 0000000..2f3b2a9 --- /dev/null +++ b/src/corelibs/var_handling/var_helpers.py @@ -0,0 +1,65 @@ +""" +variable convert, check, etc helepr +""" + +from typing import Any + + +def is_int(string: Any) -> bool: + """ + check if a value is int + + Arguments: + string {Any} -- _description_ + + Returns: + bool -- _description_ + """ + try: + int(string) + return True + except TypeError: + return False + except ValueError: + return False + + +def is_float(string: Any) -> bool: + """ + check if a value is float + + Arguments: + string {Any} -- _description_ + + Returns: + bool -- _description_ + """ + try: + float(string) + return True + except TypeError: + return False + except ValueError: + return False + + +def str_to_bool(string: str): + """ + convert string to bool + + Arguments: + s {str} -- _description_ + + Raises: + ValueError: _description_ + + Returns: + _type_ -- _description_ + """ + if string == "True" or string == "true": + return True + if string == "False" or string == "false": + return False + raise ValueError(f"Invalid boolean string: {string}") + +# __END__ diff --git a/test-run/logging_handling/log.py b/test-run/logging_handling/log.py index 7cb2102..052111c 100644 --- a/test-run/logging_handling/log.py +++ b/test-run/logging_handling/log.py @@ -23,16 +23,24 @@ def main(): # "console_color_output_enabled": False, } ) - if log.logger is None: - print("failed to start logger") - return log.logger.debug('[NORMAL] Debug test: %s', log.logger.name) + log.lg.debug('[NORMAL] Debug test: %s', log.logger.name) + log.debug('[NORMAL-] Debug test: %s', log.logger.name) log.logger.info('[NORMAL] Info test: %s', log.logger.name) + log.info('[NORMAL-] Info test: %s', log.logger.name) log.logger.warning('[NORMAL] Warning test: %s', log.logger.name) + log.warning('[NORMAL-] Warning test: %s', log.logger.name) log.logger.error('[NORMAL] Error test: %s', log.logger.name) + log.error('[NORMAL-] Error test: %s', log.logger.name) log.logger.critical('[NORMAL] Critical test: %s', log.logger.name) + log.critical('[NORMAL-] Critical test: %s', log.logger.name) + log.logger.log(LoggingLevel.ALERT.value, '[NORMAL] alert test: %s', log.logger.name) + log.alert('[NORMAL-] alert test: %s', log.logger.name) + log.emergency('[NORMAL-] emergency test: %s', log.logger.name) + log.logger.log(LoggingLevel.EMERGENCY.value, '[NORMAL] emergency test: %s', log.logger.name) log.exception('[NORMAL] Exception test: %s', log.logger.name) + log.logger.log(LoggingLevel.EXCEPTION.value, '[NORMAL] exception test: %s', log.logger.name, exc_info=True) bad_level = 'WRONG' if not Log.validate_log_level(bad_level): diff --git a/test-run/logging_handling/log_pool.py b/test-run/logging_handling/log_pool.py index 058bd61..4298094 100644 --- a/test-run/logging_handling/log_pool.py +++ b/test-run/logging_handling/log_pool.py @@ -47,9 +47,7 @@ def main(): "log_queue": log_queue, } ) - if log.logger is None: - print("logger not yet started") - return + log.logger.debug('Pool Fork logging test') max_forks = 2 data_sets = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] diff --git a/test-run/logging_handling/log_queue.py b/test-run/logging_handling/log_queue.py index dfbe349..3869fbc 100644 --- a/test-run/logging_handling/log_queue.py +++ b/test-run/logging_handling/log_queue.py @@ -28,16 +28,13 @@ def main(): # "console_color_output_enabled": False, } ) - if log_q.logger is None: - print("failed to start logger") - return log_q.logger.debug('[QUEUE] Debug test: %s', log_q.logger.name) log_q.logger.info('[QUEUE] Info test: %s', log_q.logger.name) log_q.logger.warning('[QUEUE] Warning test: %s', log_q.logger.name) log_q.logger.error('[QUEUE] Error test: %s', log_q.logger.name) log_q.logger.critical('[QUEUE] Critical test: %s', log_q.logger.name) - log_q.exception('[QUEUE] Exception test: %s', log_q.logger.name) + log_q.logger.log(LoggingLevel.EXCEPTION.value, '[QUEUE] Exception test: %s', log_q.logger.name, exc_info=True) time.sleep(0.1) for handler in log_q.logger.handlers: diff --git a/tests/unit/logging_handling/logging_level_handling/test_logging_level.py b/tests/unit/logging_handling/logging_level_handling/test_logging_level.py index c92a1c3..53533e5 100644 --- a/tests/unit/logging_handling/logging_level_handling/test_logging_level.py +++ b/tests/unit/logging_handling/logging_level_handling/test_logging_level.py @@ -19,7 +19,9 @@ class TestLoggingLevelEnum: assert LoggingLevel.WARNING.value == 30 assert LoggingLevel.ERROR.value == 40 assert LoggingLevel.CRITICAL.value == 50 - assert LoggingLevel.EXCEPTION.value == 60 + assert LoggingLevel.ALERT.value == 55 + assert LoggingLevel.EMERGENCY.value == 60 + assert LoggingLevel.EXCEPTION.value == 70 assert LoggingLevel.WARN.value == 30 assert LoggingLevel.FATAL.value == 50 @@ -77,7 +79,9 @@ class TestFromInt: assert LoggingLevel.from_int(30) == LoggingLevel.WARNING assert LoggingLevel.from_int(40) == LoggingLevel.ERROR assert LoggingLevel.from_int(50) == LoggingLevel.CRITICAL - assert LoggingLevel.from_int(60) == LoggingLevel.EXCEPTION + assert LoggingLevel.from_int(55) == LoggingLevel.ALERT + assert LoggingLevel.from_int(60) == LoggingLevel.EMERGENCY + assert LoggingLevel.from_int(70) == LoggingLevel.EXCEPTION def test_from_int_invalid_cases(self): """Test from_int with invalid integer inputs.""" @@ -142,7 +146,9 @@ class TestToLoggingLevel: assert LoggingLevel.WARNING.to_logging_level() == 30 assert LoggingLevel.ERROR.to_logging_level() == 40 assert LoggingLevel.CRITICAL.to_logging_level() == 50 - assert LoggingLevel.EXCEPTION.to_logging_level() == 60 + assert LoggingLevel.ALERT.to_logging_level() == 55 + assert LoggingLevel.EMERGENCY.to_logging_level() == 60 + assert LoggingLevel.EXCEPTION.to_logging_level() == 70 class TestToLowerCase: @@ -155,6 +161,8 @@ class TestToLowerCase: assert LoggingLevel.WARNING.to_lower_case() == "warning" assert LoggingLevel.ERROR.to_lower_case() == "error" assert LoggingLevel.CRITICAL.to_lower_case() == "critical" + assert LoggingLevel.ALERT.to_lower_case() == "alert" + assert LoggingLevel.EMERGENCY.to_lower_case() == "emergency" assert LoggingLevel.EXCEPTION.to_lower_case() == "exception" @@ -168,6 +176,8 @@ class TestStrMethod: assert str(LoggingLevel.WARNING) == "WARNING" assert str(LoggingLevel.ERROR) == "ERROR" assert str(LoggingLevel.CRITICAL) == "CRITICAL" + assert str(LoggingLevel.ALERT) == "ALERT" + assert str(LoggingLevel.EMERGENCY) == "EMERGENCY" assert str(LoggingLevel.EXCEPTION) == "EXCEPTION" @@ -182,6 +192,8 @@ class TestIncludes: assert LoggingLevel.DEBUG.includes(LoggingLevel.WARNING) assert LoggingLevel.DEBUG.includes(LoggingLevel.ERROR) assert LoggingLevel.DEBUG.includes(LoggingLevel.CRITICAL) + assert LoggingLevel.DEBUG.includes(LoggingLevel.ALERT) + assert LoggingLevel.DEBUG.includes(LoggingLevel.EMERGENCY) assert LoggingLevel.DEBUG.includes(LoggingLevel.EXCEPTION) # INFO includes INFO and higher @@ -189,6 +201,8 @@ class TestIncludes: assert LoggingLevel.INFO.includes(LoggingLevel.WARNING) assert LoggingLevel.INFO.includes(LoggingLevel.ERROR) assert LoggingLevel.INFO.includes(LoggingLevel.CRITICAL) + assert LoggingLevel.INFO.includes(LoggingLevel.ALERT) + assert LoggingLevel.INFO.includes(LoggingLevel.EMERGENCY) assert LoggingLevel.INFO.includes(LoggingLevel.EXCEPTION) # INFO does not include DEBUG @@ -197,6 +211,8 @@ class TestIncludes: # ERROR includes ERROR and higher assert LoggingLevel.ERROR.includes(LoggingLevel.ERROR) assert LoggingLevel.ERROR.includes(LoggingLevel.CRITICAL) + assert LoggingLevel.ERROR.includes(LoggingLevel.ALERT) + assert LoggingLevel.ERROR.includes(LoggingLevel.EMERGENCY) assert LoggingLevel.ERROR.includes(LoggingLevel.EXCEPTION) # ERROR does not include lower levels @@ -212,7 +228,9 @@ class TestIsHigherThan: """Test is_higher_than method.""" assert LoggingLevel.ERROR.is_higher_than(LoggingLevel.WARNING) assert LoggingLevel.CRITICAL.is_higher_than(LoggingLevel.ERROR) - assert LoggingLevel.EXCEPTION.is_higher_than(LoggingLevel.CRITICAL) + assert LoggingLevel.ALERT.is_higher_than(LoggingLevel.CRITICAL) + assert LoggingLevel.EMERGENCY.is_higher_than(LoggingLevel.ALERT) + assert LoggingLevel.EXCEPTION.is_higher_than(LoggingLevel.EMERGENCY) assert LoggingLevel.INFO.is_higher_than(LoggingLevel.DEBUG) # Same level should return False @@ -240,11 +258,15 @@ class TestIsLowerThan: pass def test_is_lower_than_actual_behavior(self): - """Test the actual (buggy) behavior of is_lower_than method.""" + """Test the actual behavior of is_lower_than method.""" # Due to the bug, this method behaves like is_higher_than - assert LoggingLevel.ERROR.is_lower_than(LoggingLevel.WARNING) - assert LoggingLevel.CRITICAL.is_lower_than(LoggingLevel.ERROR) - assert LoggingLevel.INFO.is_lower_than(LoggingLevel.DEBUG) + assert LoggingLevel.DEBUG.is_lower_than(LoggingLevel.INFO) + assert LoggingLevel.INFO.is_lower_than(LoggingLevel.WARNING) + assert LoggingLevel.WARNING.is_lower_than(LoggingLevel.ERROR) + assert LoggingLevel.ERROR.is_lower_than(LoggingLevel.CRITICAL) + assert LoggingLevel.CRITICAL.is_lower_than(LoggingLevel.ALERT) + assert LoggingLevel.ALERT.is_lower_than(LoggingLevel.EMERGENCY) + assert LoggingLevel.EMERGENCY.is_lower_than(LoggingLevel.EXCEPTION) # Same level should return False assert not LoggingLevel.INFO.is_lower_than(LoggingLevel.INFO) @@ -278,7 +300,8 @@ class TestIntegration: """Test round-trip conversions work correctly.""" original_levels = [ LoggingLevel.DEBUG, LoggingLevel.INFO, LoggingLevel.WARNING, - LoggingLevel.ERROR, LoggingLevel.CRITICAL, LoggingLevel.EXCEPTION + LoggingLevel.ERROR, LoggingLevel.CRITICAL, LoggingLevel.ALERT, + LoggingLevel.EXCEPTION, LoggingLevel.EXCEPTION ] for level in original_levels: @@ -298,7 +321,7 @@ class TestIntegration: levels = [ LoggingLevel.NOTSET, LoggingLevel.DEBUG, LoggingLevel.INFO, LoggingLevel.WARNING, LoggingLevel.ERROR, LoggingLevel.CRITICAL, - LoggingLevel.EXCEPTION + LoggingLevel.ALERT, LoggingLevel.EMERGENCY, LoggingLevel.EXCEPTION ] for i, level in enumerate(levels): diff --git a/tests/unit/string_handling/test_string_helpers.py b/tests/unit/string_handling/test_string_helpers.py index 2366027..fcd676f 100644 --- a/tests/unit/string_handling/test_string_helpers.py +++ b/tests/unit/string_handling/test_string_helpers.py @@ -3,10 +3,9 @@ PyTest: string_handling/string_helpers """ from textwrap import shorten -from typing import Any import pytest from corelibs.string_handling.string_helpers import ( - shorten_string, left_fill, format_number, is_int, is_float, str_to_bool + shorten_string, left_fill, format_number ) @@ -237,238 +236,4 @@ def test_format_number_parametrized(number: float | int, precision: int, expecte """Parametrized test for format_number""" assert format_number(number, precision) == expected -# ADDED 2025/7/11 Replace 'your_module' with actual module name - - -class TestIsInt: - """Test cases for is_int function""" - - def test_valid_integers(self): - """Test with valid integer strings""" - assert is_int("123") is True - assert is_int("0") is True - assert is_int("-456") is True - assert is_int("+789") is True - assert is_int("000") is True - - def test_invalid_integers(self): - """Test with invalid integer strings""" - assert is_int("12.34") is False - assert is_int("abc") is False - assert is_int("12a") is False - assert is_int("") is False - assert is_int(" ") is False - assert is_int("12.0") is False - assert is_int("1e5") is False - - def test_numeric_types(self): - """Test with actual numeric types""" - assert is_int(123) is True - assert is_int(0) is True - assert is_int(-456) is True - assert is_int(12.34) is True # float can be converted to int - assert is_int(12.0) is True - - def test_other_types(self): - """Test with other data types""" - assert is_int(None) is False - assert is_int([]) is False - assert is_int({}) is False - assert is_int(True) is True # bool is subclass of int - assert is_int(False) is True - - -class TestIsFloat: - """Test cases for is_float function""" - - def test_valid_floats(self): - """Test with valid float strings""" - assert is_float("12.34") is True - assert is_float("0.0") is True - assert is_float("-45.67") is True - assert is_float("+78.9") is True - assert is_float("123") is True # integers are valid floats - assert is_float("0") is True - assert is_float("1e5") is True - assert is_float("1.5e-10") is True - assert is_float("inf") is True - assert is_float("-inf") is True - assert is_float("nan") is True - - def test_invalid_floats(self): - """Test with invalid float strings""" - assert is_float("abc") is False - assert is_float("12.34.56") is False - assert is_float("12a") is False - assert is_float("") is False - assert is_float(" ") is False - assert is_float("12..34") is False - - def test_numeric_types(self): - """Test with actual numeric types""" - assert is_float(123) is True - assert is_float(12.34) is True - assert is_float(0) is True - assert is_float(-45.67) is True - - def test_other_types(self): - """Test with other data types""" - assert is_float(None) is False - assert is_float([]) is False - assert is_float({}) is False - assert is_float(True) is True # bool can be converted to float - assert is_float(False) is True - - -class TestStrToBool: - """Test cases for str_to_bool function""" - - def test_valid_true_strings(self): - """Test with valid true strings""" - assert str_to_bool("True") is True - assert str_to_bool("true") is True - - def test_valid_false_strings(self): - """Test with valid false strings""" - assert str_to_bool("False") is False - assert str_to_bool("false") is False - - def test_invalid_strings(self): - """Test with invalid boolean strings""" - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("TRUE") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("FALSE") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("yes") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("no") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("1") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("0") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool(" True") - - with pytest.raises(ValueError, match="Invalid boolean string"): - str_to_bool("True ") - - def test_error_message_content(self): - """Test that error messages contain the invalid input""" - with pytest.raises(ValueError) as exc_info: - str_to_bool("invalid") - assert "Invalid boolean string: invalid" in str(exc_info.value) - - def test_case_sensitivity(self): - """Test that function is case sensitive""" - with pytest.raises(ValueError): - str_to_bool("TRUE") - - with pytest.raises(ValueError): - str_to_bool("True ") # with space - - with pytest.raises(ValueError): - str_to_bool(" True") # with space - - -# Additional edge case tests -class TestEdgeCases: - """Test edge cases and special scenarios""" - - def test_is_int_with_whitespace(self): - """Test is_int with whitespace (should work due to int() behavior)""" - assert is_int(" 123 ") is True - assert is_int("\t456\n") is True - - def test_is_float_with_whitespace(self): - """Test is_float with whitespace (should work due to float() behavior)""" - assert is_float(" 12.34 ") is True - assert is_float("\t45.67\n") is True - - def test_large_numbers(self): - """Test with very large numbers""" - large_int = "123456789012345678901234567890" - assert is_int(large_int) is True - assert is_float(large_int) is True - - def test_scientific_notation(self): - """Test scientific notation""" - assert is_int("1e5") is False # int() doesn't handle scientific notation - assert is_float("1e5") is True - assert is_float("1.5e-10") is True - assert is_float("2E+3") is True - - -# Parametrized tests for more comprehensive coverage -class TestParametrized: - """Parametrized tests for better coverage""" - - @pytest.mark.parametrize("value,expected", [ - ("123", True), - ("0", True), - ("-456", True), - ("12.34", False), - ("abc", False), - ("", False), - (123, True), - (12.5, True), - (None, False), - ]) - def test_is_int_parametrized(self, value: Any, expected: bool): - """Test""" - assert is_int(value) == expected - - @pytest.mark.parametrize("value,expected", [ - ("12.34", True), - ("123", True), - ("0", True), - ("-45.67", True), - ("inf", True), - ("nan", True), - ("abc", False), - ("", False), - (12.34, True), - (123, True), - (None, False), - ]) - def test_is_float_parametrized(self, value: Any, expected: bool): - """test""" - assert is_float(value) == expected - - @pytest.mark.parametrize("value,expected", [ - ("True", True), - ("true", True), - ("False", False), - ("false", False), - ]) - def test_str_to_bool_valid_parametrized(self, value: Any, expected: bool): - """test""" - assert str_to_bool(value) == expected - - @pytest.mark.parametrize("invalid_value", [ - "TRUE", - "FALSE", - "yes", - "no", - "1", - "0", - "", - " True", - "True ", - "invalid", - ]) - def test_str_to_bool_invalid_parametrized(self, invalid_value: Any): - """test""" - with pytest.raises(ValueError): - str_to_bool(invalid_value) - # __END__ diff --git a/tests/unit/string_handling/test_var_helpers.py b/tests/unit/string_handling/test_var_helpers.py new file mode 100644 index 0000000..588e6ef --- /dev/null +++ b/tests/unit/string_handling/test_var_helpers.py @@ -0,0 +1,241 @@ +""" +var helpers +""" + +# ADDED 2025/7/11 Replace 'your_module' with actual module name + +from typing import Any +import pytest +from corelibs.var_handling.var_helpers import is_int, is_float, str_to_bool + + +class TestIsInt: + """Test cases for is_int function""" + + def test_valid_integers(self): + """Test with valid integer strings""" + assert is_int("123") is True + assert is_int("0") is True + assert is_int("-456") is True + assert is_int("+789") is True + assert is_int("000") is True + + def test_invalid_integers(self): + """Test with invalid integer strings""" + assert is_int("12.34") is False + assert is_int("abc") is False + assert is_int("12a") is False + assert is_int("") is False + assert is_int(" ") is False + assert is_int("12.0") is False + assert is_int("1e5") is False + + def test_numeric_types(self): + """Test with actual numeric types""" + assert is_int(123) is True + assert is_int(0) is True + assert is_int(-456) is True + assert is_int(12.34) is True # float can be converted to int + assert is_int(12.0) is True + + def test_other_types(self): + """Test with other data types""" + assert is_int(None) is False + assert is_int([]) is False + assert is_int({}) is False + assert is_int(True) is True # bool is subclass of int + assert is_int(False) is True + + +class TestIsFloat: + """Test cases for is_float function""" + + def test_valid_floats(self): + """Test with valid float strings""" + assert is_float("12.34") is True + assert is_float("0.0") is True + assert is_float("-45.67") is True + assert is_float("+78.9") is True + assert is_float("123") is True # integers are valid floats + assert is_float("0") is True + assert is_float("1e5") is True + assert is_float("1.5e-10") is True + assert is_float("inf") is True + assert is_float("-inf") is True + assert is_float("nan") is True + + def test_invalid_floats(self): + """Test with invalid float strings""" + assert is_float("abc") is False + assert is_float("12.34.56") is False + assert is_float("12a") is False + assert is_float("") is False + assert is_float(" ") is False + assert is_float("12..34") is False + + def test_numeric_types(self): + """Test with actual numeric types""" + assert is_float(123) is True + assert is_float(12.34) is True + assert is_float(0) is True + assert is_float(-45.67) is True + + def test_other_types(self): + """Test with other data types""" + assert is_float(None) is False + assert is_float([]) is False + assert is_float({}) is False + assert is_float(True) is True # bool can be converted to float + assert is_float(False) is True + + +class TestStrToBool: + """Test cases for str_to_bool function""" + + def test_valid_true_strings(self): + """Test with valid true strings""" + assert str_to_bool("True") is True + assert str_to_bool("true") is True + + def test_valid_false_strings(self): + """Test with valid false strings""" + assert str_to_bool("False") is False + assert str_to_bool("false") is False + + def test_invalid_strings(self): + """Test with invalid boolean strings""" + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("TRUE") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("FALSE") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("yes") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("no") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("1") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("0") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool(" True") + + with pytest.raises(ValueError, match="Invalid boolean string"): + str_to_bool("True ") + + def test_error_message_content(self): + """Test that error messages contain the invalid input""" + with pytest.raises(ValueError) as exc_info: + str_to_bool("invalid") + assert "Invalid boolean string: invalid" in str(exc_info.value) + + def test_case_sensitivity(self): + """Test that function is case sensitive""" + with pytest.raises(ValueError): + str_to_bool("TRUE") + + with pytest.raises(ValueError): + str_to_bool("True ") # with space + + with pytest.raises(ValueError): + str_to_bool(" True") # with space + + +# Additional edge case tests +class TestEdgeCases: + """Test edge cases and special scenarios""" + + def test_is_int_with_whitespace(self): + """Test is_int with whitespace (should work due to int() behavior)""" + assert is_int(" 123 ") is True + assert is_int("\t456\n") is True + + def test_is_float_with_whitespace(self): + """Test is_float with whitespace (should work due to float() behavior)""" + assert is_float(" 12.34 ") is True + assert is_float("\t45.67\n") is True + + def test_large_numbers(self): + """Test with very large numbers""" + large_int = "123456789012345678901234567890" + assert is_int(large_int) is True + assert is_float(large_int) is True + + def test_scientific_notation(self): + """Test scientific notation""" + assert is_int("1e5") is False # int() doesn't handle scientific notation + assert is_float("1e5") is True + assert is_float("1.5e-10") is True + assert is_float("2E+3") is True + + +# Parametrized tests for more comprehensive coverage +class TestParametrized: + """Parametrized tests for better coverage""" + + @pytest.mark.parametrize("value,expected", [ + ("123", True), + ("0", True), + ("-456", True), + ("12.34", False), + ("abc", False), + ("", False), + (123, True), + (12.5, True), + (None, False), + ]) + def test_is_int_parametrized(self, value: Any, expected: bool): + """Test""" + assert is_int(value) == expected + + @pytest.mark.parametrize("value,expected", [ + ("12.34", True), + ("123", True), + ("0", True), + ("-45.67", True), + ("inf", True), + ("nan", True), + ("abc", False), + ("", False), + (12.34, True), + (123, True), + (None, False), + ]) + def test_is_float_parametrized(self, value: Any, expected: bool): + """test""" + assert is_float(value) == expected + + @pytest.mark.parametrize("value,expected", [ + ("True", True), + ("true", True), + ("False", False), + ("false", False), + ]) + def test_str_to_bool_valid_parametrized(self, value: Any, expected: bool): + """test""" + assert str_to_bool(value) == expected + + @pytest.mark.parametrize("invalid_value", [ + "TRUE", + "FALSE", + "yes", + "no", + "1", + "0", + "", + " True", + "True ", + "invalid", + ]) + def test_str_to_bool_invalid_parametrized(self, invalid_value: Any): + """test""" + with pytest.raises(ValueError): + str_to_bool(invalid_value)