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)