Compare commits

...

15 Commits

Author SHA1 Message Date
Clemens Schwaighofer
dc7e56106e v0.42.0: Move text colors to external lib and depreacte the ones in corelibs collection 2025-11-20 11:05:34 +09:00
Clemens Schwaighofer
90e5179980 Remove text color handling from corelibs and use corelibs_text_colors instead
Also update enum with proper pyi file for deprecation warnings
2025-11-20 10:59:44 +09:00
Clemens Schwaighofer
9db39003c4 v0.41.0: settings parsers, make arguments override no longer automatic 2025-11-20 10:11:41 +09:00
Clemens Schwaighofer
4ffe372434 Change that the args overload has to be set to override settings from arguments
So we do not have issues with values change because an arugment has the same name as a setting name
2025-11-20 10:00:36 +09:00
Clemens Schwaighofer
a00c27c465 v0.40.0: Fix for settings loader with arguments 2025-11-19 19:03:35 +09:00
Clemens Schwaighofer
1f7f4b8d53 Update settings loader with skip argument set if not matching settings type or ignore flag is set
We have "args:no" that can be set to avoid override from arguments.
Also arguments that do not match the exepected type are not loaded
2025-11-19 19:01:29 +09:00
Clemens Schwaighofer
baca79ce82 v0.39.2: [Fix] Skip Log format update if it did not change 2025-11-19 17:45:50 +09:00
Clemens Schwaighofer
4265be6430 Merge branch 'development' 2025-11-19 17:45:08 +09:00
Clemens Schwaighofer
c16b086467 v0.39.1: Skip Log format update if it did not change 2025-11-19 17:44:44 +09:00
Clemens Schwaighofer
48a98c0206 Merge branch 'master' into development 2025-11-19 17:43:13 +09:00
Clemens Schwaighofer
f1788f057f Log skip format change it format flags have not changed 2025-11-19 17:42:47 +09:00
Clemens Schwaighofer
0ad8883809 v0.39.0: Add Log LEVEL flag for console format 2025-11-19 17:37:00 +09:00
Clemens Schwaighofer
51e9b1ce7c Add "LEVEL" option to console log format
So we can set output to onle the message without any information (NONE),
only level (BARE), time and level (MINIMAL), time, file, line and level (CONDENSED) or
(ALL) full information.
2025-11-19 17:35:27 +09:00
Clemens Schwaighofer
0d3104f60a v0.38.0: Log console format update 2025-11-19 15:45:49 +09:00
Clemens Schwaighofer
d29f827fc9 Add a function to Log system to update the console formatter dynamically. 2025-11-19 15:17:25 +09:00
15 changed files with 662 additions and 783 deletions

View File

@@ -1,19 +1,20 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.37.0"
version = "0.42.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"corelibs-datetime>=1.0.1",
"corelibs-enum-base>=1.0.0",
"corelibs-text-colors>=1.0.0",
"corelibs-var>=1.0.0",
"cryptography>=46.0.3",
"jmespath>=1.0.1",
"jsonpath-ng>=1.7.0",
"psutil>=7.0.0",
"requests>=2.32.4",
"requests[proxy]>=2.32.4",
]
# MARK: build system
@@ -33,12 +34,14 @@ publish-url = "https://git.egplusww.jp/api/packages/PyPI/pypi"
corelibs-enum-base = { index = "opj-pypi" }
corelibs-datetime = { index = "opj-pypi" }
corelibs-var = { index = "opj-pypi" }
corelibs-text-colors = { index = "opj-pypi" }
[dependency-groups]
dev = [
"deepdiff>=8.6.1",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
"typing-extensions>=4.15.0",
]
# MARK: Python linting

View File

@@ -93,6 +93,8 @@ class SettingsLoader:
entry_split_char: dict[str, str] = {}
# entries that should be converted
entry_convert: dict[str, str] = {}
# no args to set
args_overrride: list[str] = []
# all the settings for the config id given
settings: dict[str, dict[str, Any]] = {
config_id: {},
@@ -162,6 +164,8 @@ class SettingsLoader:
f"[!] In [{config_id}] the split character setup for entry failed: {check}: {e}",
'CRITICAL'
)) from e
if check == "args_override:yes":
args_overrride.append(key)
if skip:
continue
settings[config_id][key] = [
@@ -185,9 +189,16 @@ class SettingsLoader:
error: bool = False
for entry, validate in config_validate.items():
# if we have command line option set, this one overrides config
if self.__get_arg(entry):
if (args_entry := self.__get_arg(entry)) is not None:
self.__print(f"[*] Command line option override for: {entry}", 'WARNING')
settings[config_id][entry] = self.args.get(entry)
if (
# only set if flagged as allowed override from args
entry in args_overrride and
(isinstance(args_entry, list) and entry_split_char.get(entry)) or
(not isinstance(args_entry, list) and not entry_split_char.get(entry))
):
# args is list, but entry has not split, do not set
settings[config_id][entry] = args_entry
# validate checks
for check in validate:
# CHECKS
@@ -277,10 +288,8 @@ class SettingsLoader:
elif convert_type in ["float", "any"] and is_float(settings[config_id][entry]):
settings[config_id][entry] = float(settings[config_id][entry])
elif convert_type in ["bool", "any"] and (
settings[config_id][entry] == "true" or
settings[config_id][entry] == "True" or
settings[config_id][entry] == "false" or
settings[config_id][entry] == "False"
settings[config_id][entry].lower() == "true" or
settings[config_id][entry].lower() == "false"
):
try:
settings[config_id][entry] = str_to_bool(settings[config_id][entry])

View File

@@ -13,8 +13,8 @@ from pathlib import Path
import atexit
from enum import Flag, auto
from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast
from corelibs_text_colors.text_colors import Colors
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 call_stack, exception_stack
if TYPE_CHECKING:
@@ -32,6 +32,7 @@ class ConsoleFormat(Flag):
FILE = auto()
FUNCTION = auto()
LINENO = auto()
LEVEL = auto()
class ConsoleFormatSettings:
@@ -43,14 +44,17 @@ class ConsoleFormatSettings:
ConsoleFormat.NAME |
ConsoleFormat.FILE |
ConsoleFormat.FUNCTION |
ConsoleFormat.LINENO
ConsoleFormat.LINENO |
ConsoleFormat.LEVEL
)
# show time with no time zone, file and line
CONDENSED = ConsoleFormat.TIME | ConsoleFormat.FILE | ConsoleFormat.LINENO
# only time
MINIMAL = ConsoleFormat.TIME
# show time with no time zone, file, line and level
CONDENSED = ConsoleFormat.TIME | ConsoleFormat.FILE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
# only time and level
MINIMAL = ConsoleFormat.TIME | ConsoleFormat.LEVEL
# only level
BARE = ConsoleFormat.LEVEL
# only message
BARE = ConsoleFormat(0)
NONE = ConsoleFormat(0)
@staticmethod
def from_string(setting_str: str, default: ConsoleFormat | None = None) -> ConsoleFormat | None:
@@ -445,6 +449,9 @@ class Log(LogParent):
logger setup
"""
CONSOLE_HANDLER: str = 'stream_handler'
FILE_HANDLER: str = 'file_handler'
# spacer lenght characters and the character
SPACER_CHAR: str = '='
SPACER_LENGTH: int = 32
@@ -510,13 +517,13 @@ class Log(LogParent):
# in the file writer too, for the ones where color is set BEFORE the format
# Any is logging.StreamHandler, logging.FileHandler and all logging.handlers.*
self.handlers: dict[str, Any] = {}
self.add_handler('file_handler', self.__create_file_handler(
'file_handler', self.log_settings['log_level_file'], log_path)
self.add_handler(self.FILE_HANDLER, self.__create_file_handler(
self.FILE_HANDLER, self.log_settings['log_level_file'], log_path)
)
if self.log_settings['console_enabled']:
# console
self.add_handler('stream_handler', self.__create_console_handler(
'stream_handler',
self.add_handler(self.CONSOLE_HANDLER, self.__create_console_handler(
self.CONSOLE_HANDLER,
self.log_settings['log_level_console'],
console_format_type=self.log_settings['console_format_type'],
))
@@ -613,19 +620,17 @@ class Log(LogParent):
self.handlers[handler_name] = handler
return True
# MARK: console handler
def __create_console_handler(
self, handler_name: str,
log_level_console: LoggingLevel = LoggingLevel.WARNING,
filter_exceptions: bool = True,
console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL,
) -> logging.StreamHandler[TextIO]:
# console logger
if not self.validate_log_level(log_level_console):
log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE
console_handler = logging.StreamHandler()
print(f"Console format type: {console_format_type}")
# build the format string based on what flags are set
# MARK: console logger format
def __build_console_format_from_string(self, console_format_type: ConsoleFormat) -> str:
"""
Build console format string from the given console format type
Arguments:
console_format_type {ConsoleFormat} -- _description_
Returns:
str -- _description_
"""
format_string = ''
# time part if any of the times are requested
if (
@@ -654,17 +659,23 @@ class Log(LogParent):
set_group.append('%(lineno)d')
format_string += ':'.join(set_group)
format_string += '] '
# always level + message
format_string += '<%(levelname)s> %(message)s'
# basic date, but this will be overridden to ISO in formatTime
# format_date = "%Y-%m-%d %H:%M:%S"
# color or not
if self.log_settings['console_color_output_enabled']:
# formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
formatter_console = CustomConsoleFormatter(format_string)
else:
# formatter_console = logging.Formatter(format_string, datefmt=format_date)
formatter_console = logging.Formatter(format_string)
# level if wanted
if ConsoleFormat.LEVEL in console_format_type:
format_string += '<%(levelname)s> '
# always message
format_string += '%(message)s'
return format_string
def __set_time_format_for_console_formatter(
self, formatter_console: CustomConsoleFormatter | logging.Formatter, console_format_type: ConsoleFormat
) -> None:
"""
Format time for a given format handler, this is for console format only
Arguments:
formatter_console {CustomConsoleFormatter | logging.Formatter} -- _description_
console_format_type {ConsoleFormat} -- _description_
"""
# default for TIME is milliseconds
# if we have multiple set, the smallest precision wins
if ConsoleFormat.TIME_MICROSECONDS in console_format_type:
@@ -701,11 +712,80 @@ class Log(LogParent):
.fromtimestamp(record.created)
.isoformat(sep=" ", timespec=iso_precision)
)
def __set_console_formatter(self, console_format_type: ConsoleFormat) -> CustomConsoleFormatter | logging.Formatter:
"""
Build the full formatter and return it
Arguments:
console_format_type {ConsoleFormat} -- _description_
Returns:
CustomConsoleFormatter | logging.Formatter -- _description_
"""
format_string = self.__build_console_format_from_string(console_format_type)
if self.log_settings['console_color_output_enabled']:
# formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
formatter_console = CustomConsoleFormatter(format_string)
else:
# formatter_console = logging.Formatter(format_string, datefmt=format_date)
formatter_console = logging.Formatter(format_string)
self.__set_time_format_for_console_formatter(formatter_console, console_format_type)
self.log_settings['console_format_type'] = console_format_type
return formatter_console
# MARK: console handler update
def update_console_formatter(
self,
console_format_type: ConsoleFormat,
):
"""
Update the console formatter for format layout and time stamp format
Arguments:
console_format_type {ConsoleFormat} -- _description_
"""
# skip if console not enabled
if not self.log_settings['console_enabled']:
return
# skip if format has not changed
if self.log_settings['console_format_type'] == console_format_type:
return
# update the formatter
self.handlers[self.CONSOLE_HANDLER].setFormatter(
self.__set_console_formatter(console_format_type)
)
# MARK: console handler
def __create_console_handler(
self, handler_name: str,
log_level_console: LoggingLevel = LoggingLevel.WARNING,
filter_exceptions: bool = True,
console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL,
) -> logging.StreamHandler[TextIO]:
# console logger
if not self.validate_log_level(log_level_console):
log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE
console_handler = logging.StreamHandler()
# print(f"Console format type: {console_format_type}")
# build the format string based on what flags are set
# format_string = self.__build_console_format_from_string(console_format_type)
# # basic date, but this will be overridden to ISO in formatTime
# # format_date = "%Y-%m-%d %H:%M:%S"
# # color or not
# if self.log_settings['console_color_output_enabled']:
# # formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
# formatter_console = CustomConsoleFormatter(format_string)
# else:
# # formatter_console = logging.Formatter(format_string, datefmt=format_date)
# formatter_console = logging.Formatter(format_string)
# # set the time format
# self.__set_time_format_for_console_formatter(formatter_console, console_format_type)
console_handler.set_name(handler_name)
console_handler.setLevel(log_level_console.name)
# do not show exceptions logs on console
console_handler.addFilter(CustomHandlerFilter('console', filter_exceptions))
console_handler.setFormatter(formatter_console)
console_handler.setFormatter(self.__set_console_formatter(console_format_type))
return console_handler
# MARK: file handler

View File

@@ -5,152 +5,14 @@ Set colors with print(f"something {Colors.yellow}colorful{Colors.end})
bold + underline + color combinations are possible.
"""
from warnings import deprecated
from corelibs_text_colors.text_colors import Colors as ColorsNew
class Colors:
@deprecated("Use src.corelibs_text_colors.text_colors instead")
class Colors(ColorsNew):
"""
ANSI colors defined
"""
# General sets, these should not be accessd
__BOLD = '\033[1m'
__UNDERLINE = '\033[4m'
__END = '\033[0m'
__RESET = '\033[0m'
# Define ANSI color codes as class attributes
__BLACK = "\033[30m"
__RED = "\033[31m"
__GREEN = "\033[32m"
__YELLOW = "\033[33m"
__BLUE = "\033[34m"
__MAGENTA = "\033[35m"
__CYAN = "\033[36m"
__WHITE = "\033[37m"
# Define bold/bright versions of the colors
__BLACK_BOLD = "\033[1;30m"
__RED_BOLD = "\033[1;31m"
__GREEN_BOLD = "\033[1;32m"
__YELLOW_BOLD = "\033[1;33m"
__BLUE_BOLD = "\033[1;34m"
__MAGENTA_BOLD = "\033[1;35m"
__CYAN_BOLD = "\033[1;36m"
__WHITE_BOLD = "\033[1;37m"
# BRIGHT, alternative
__BLACK_BRIGHT = '\033[90m'
__RED_BRIGHT = '\033[91m'
__GREEN_BRIGHT = '\033[92m'
__YELLOW_BRIGHT = '\033[93m'
__BLUE_BRIGHT = '\033[94m'
__MAGENTA_BRIGHT = '\033[95m'
__CYAN_BRIGHT = '\033[96m'
__WHITE_BRIGHT = '\033[97m'
# set access vars
bold = __BOLD
underline = __UNDERLINE
end = __END
reset = __RESET
# normal
black = __BLACK
red = __RED
green = __GREEN
yellow = __YELLOW
blue = __BLUE
magenta = __MAGENTA
cyan = __CYAN
white = __WHITE
# bold
black_bold = __BLACK_BOLD
red_bold = __RED_BOLD
green_bold = __GREEN_BOLD
yellow_bold = __YELLOW_BOLD
blue_bold = __BLUE_BOLD
magenta_bold = __MAGENTA_BOLD
cyan_bold = __CYAN_BOLD
white_bold = __WHITE_BOLD
# bright
black_bright = __BLACK_BRIGHT
red_bright = __RED_BRIGHT
green_bright = __GREEN_BRIGHT
yellow_bright = __YELLOW_BRIGHT
blue_bright = __BLUE_BRIGHT
magenta_bright = __MAGENTA_BRIGHT
cyan_bright = __CYAN_BRIGHT
white_bright = __WHITE_BRIGHT
@staticmethod
def disable():
"""
No colors
"""
Colors.bold = ''
Colors.underline = ''
Colors.end = ''
Colors.reset = ''
# normal
Colors.black = ''
Colors.red = ''
Colors.green = ''
Colors.yellow = ''
Colors.blue = ''
Colors.magenta = ''
Colors.cyan = ''
Colors.white = ''
# bold/bright
Colors.black_bold = ''
Colors.red_bold = ''
Colors.green_bold = ''
Colors.yellow_bold = ''
Colors.blue_bold = ''
Colors.magenta_bold = ''
Colors.cyan_bold = ''
Colors.white_bold = ''
# bold/bright alt
Colors.black_bright = ''
Colors.red_bright = ''
Colors.green_bright = ''
Colors.yellow_bright = ''
Colors.blue_bright = ''
Colors.magenta_bright = ''
Colors.cyan_bright = ''
Colors.white_bright = ''
@staticmethod
def reset_colors():
"""
reset colors to the original ones
"""
# set access vars
Colors.bold = Colors.__BOLD
Colors.underline = Colors.__UNDERLINE
Colors.end = Colors.__END
Colors.reset = Colors.__RESET
# normal
Colors.black = Colors.__BLACK
Colors.red = Colors.__RED
Colors.green = Colors.__GREEN
Colors.yellow = Colors.__YELLOW
Colors.blue = Colors.__BLUE
Colors.magenta = Colors.__MAGENTA
Colors.cyan = Colors.__CYAN
Colors.white = Colors.__WHITE
# bold
Colors.black_bold = Colors.__BLACK_BOLD
Colors.red_bold = Colors.__RED_BOLD
Colors.green_bold = Colors.__GREEN_BOLD
Colors.yellow_bold = Colors.__YELLOW_BOLD
Colors.blue_bold = Colors.__BLUE_BOLD
Colors.magenta_bold = Colors.__MAGENTA_BOLD
Colors.cyan_bold = Colors.__CYAN_BOLD
Colors.white_bold = Colors.__WHITE_BOLD
# bright
Colors.black_bright = Colors.__BLACK_BRIGHT
Colors.red_bright = Colors.__RED_BRIGHT
Colors.green_bright = Colors.__GREEN_BRIGHT
Colors.yellow_bright = Colors.__YELLOW_BRIGHT
Colors.blue_bright = Colors.__BLUE_BRIGHT
Colors.magenta_bright = Colors.__MAGENTA_BRIGHT
Colors.cyan_bright = Colors.__CYAN_BRIGHT
Colors.white_bright = Colors.__WHITE_BRIGHT
# __END__

View File

@@ -2,82 +2,24 @@
Enum base classes
"""
from enum import Enum
from warnings import deprecated
from typing import Any
# from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
import warnings
from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
class EnumBase(Enum):
class EnumBase(CorelibsEnumBase):
"""
base for enum
.. deprecated::
Use corelibs_enum_base.EnumBase instead
DEPRECATED: Use corelibs_enum_base.EnumBase instead
lookup_any and from_any will return "EnumBase" and the sub class name
run the return again to "from_any" to get a clean value, or cast it
"""
@classmethod
@deprecated("Use corelibs_enum_base.EnumBase instead")
def lookup_key(cls, enum_key: str):
"""Lookup from key side (must be string)"""
# if there is a ":", then this is legacy, replace with ___
if ":" in enum_key:
enum_key = enum_key.replace(':', '___')
try:
return cls[enum_key.upper()]
except KeyError as e:
raise ValueError(f"Invalid key: {enum_key}") from e
except AttributeError as e:
raise ValueError(f"Invalid key: {enum_key}") from e
@classmethod
@deprecated("Use corelibs_enum_base.EnumBase instead")
def lookup_value(cls, enum_value: Any):
"""Lookup through value side"""
try:
return cls(enum_value)
except ValueError as e:
raise ValueError(f"Invalid value: {enum_value}") from e
# At the module level, issue a deprecation warning
warnings.warn("Use corelibs_enum_base.EnumBase instead", DeprecationWarning, stacklevel=2)
@classmethod
@deprecated("Use corelibs_enum_base.EnumBase instead")
def from_any(cls, enum_any: Any):
"""
This only works in the following order
-> class itself, as is
-> str, assume key lookup
-> if failed try other
Arguments:
enum_any {Any} -- _description_
Returns:
_type_ -- _description_
"""
if isinstance(enum_any, cls):
return enum_any
# try key first if it is string
# if failed try value
if isinstance(enum_any, str):
try:
return cls.lookup_key(enum_any)
except (ValueError, AttributeError):
try:
return cls.lookup_value(enum_any)
except ValueError as e:
raise ValueError(f"Could not find as key or value: {enum_any}") from e
return cls.lookup_value(enum_any)
@deprecated("Use corelibs_enum_base.EnumBase instead")
def to_value(self) -> Any:
"""Convert to value"""
return self.value
@deprecated("Use corelibs_enum_base.EnumBase instead")
def to_lower_case(self) -> str:
"""return lower case"""
return self.name.lower()
@deprecated("Use corelibs_enum_base.EnumBase instead")
def __str__(self) -> str:
"""return [Enum].NAME like it was called with .name"""
return self.name
# __EMD__

View File

@@ -0,0 +1,15 @@
"""
Enum base classes [STPUB]
"""
from typing_extensions import deprecated
from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
@deprecated("Use corelibs_enum_base.EnumBase instead")
class EnumBase(CorelibsEnumBase):
"""
base for enum
lookup_any and from_any will return "EnumBase" and the sub class name
run the return again to "from_any" to get a clean value, or cast it
"""

View File

@@ -1,7 +1,12 @@
[TestA]
foo=bar
overload_from_args=bar
foobar=1
bar=st
arg_overload=should_not_be_set_because_of_command_line_is_list
arg_overload_list=too,be,long
arg_overload_not_set=this should not be set because of override flag
just_values=too,be,long
some_match=foo
some_match_list=foo,bar
test_list=a,b,c,d f, g h

View File

@@ -39,7 +39,10 @@ def main():
sl = SettingsLoader(
{
'foo': 'OVERLOAD'
'overload_from_args': 'OVERLOAD from ARGS',
'arg_overload': ['should', 'not', 'be', 'set'],
'arg_overload_list': ['overload', 'this', 'list'],
'arg_overload_not_set': "DO_NOT_SET",
},
ROOT_PATH.joinpath(CONFIG_DIR, CONFIG_FILE),
log=log
@@ -50,9 +53,11 @@ def main():
config_load,
{
# "doesnt": ["split:,"],
"foo": ["mandatory:yes"],
"overload_from_args": ["args_override:yes", "mandatory:yes"],
"foobar": ["check:int"],
"bar": ["mandatory:yes"],
"arg_overload_list": ["args_override:yes", "split:,",],
"arg_overload_not_set": [],
"some_match": ["matching:foo|bar"],
"some_match_list": ["split:,", "matching:foo|bar"],
"test_list": [

View File

@@ -25,12 +25,13 @@ def main():
"log_level_file": 'DEBUG',
# "console_color_output_enabled": False,
"per_run_log": True,
# Set console log type, must be sent as value for ConsoleFormat or bitwise of ConsoleFormatType
# "console_format_type": ConsoleFormatSettings.BARE,
# "console_format_type": ConsoleFormatSettings.NONE,
# "console_format_type": ConsoleFormatSettings.MINIMAL,
# "console_format_type": ConsoleFormatType.TIME_MICROSECONDS | ConsoleFormatType.NAME,
# "console_format_type": ConsoleFormatType.NAME,
"console_format_type": ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO,
"console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME | ConsoleFormat.LEVEL,
# "console_format_type": ConsoleFormat.NAME,
# "console_format_type": (
# ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
# ),
}
)
logn = Logger(log.get_logger_settings())
@@ -104,10 +105,16 @@ def main():
for key, handler in log.handlers.items():
print(f"Handler (handlers) [{key}] {handler} -> {handler.level} -> {LoggingLevel.from_any(handler.level)}")
log.set_log_level('stream_handler', LoggingLevel.ERROR)
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.ERROR)
log.logger.warning('[NORMAL] Invisible Warning test: %s', log.logger.name)
log.logger.error('[NORMAL] Visible Error test: %s', log.logger.name)
# log.handlers['stream_handler'].se
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.DEBUG)
log.debug('Current logging format: %s', log.log_settings['console_format_type'])
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less')
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less B')
if __name__ == "__main__":

View File

@@ -528,6 +528,123 @@ class TestLoadSettings:
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_args_no_flag(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior (no args_override:yes) with list argument that has split"""
config_file = tmp_path / "test.ini"
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
loader = SettingsLoader(
args={"value": ["x", "y", "z"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["split:,"]}
)
# Without args_override:yes flag, should use config value (no override)
assert result["value"] == ["a", "b", "c"]
captured = capsys.readouterr()
# Message is printed but without args_override:yes flag, override doesn't happen
assert "Command line option override" in captured.out
def test_load_settings_args_list_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that list arguments without split entry are skipped"""
config_file = tmp_path / "test.ini"
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
args={"value": ["arg1", "arg2", "arg3"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": []}
)
# Should keep config value since args is list but no split defined
assert result["value"] == "config_value"
captured = capsys.readouterr()
# Message is printed but list without split prevents the override
assert "Command line option override" in captured.out
def test_load_settings_args_list_with_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that list arguments with split entry and args_override:yes are applied"""
config_file = tmp_path / "test.ini"
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
loader = SettingsLoader(
args={"value": ["arg1", "arg2", "arg3"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["split:,", "args_override:yes"]}
)
# Should use args value because split is defined AND args_override:yes is set
assert result["value"] == ["arg1", "arg2", "arg3"]
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_args_no_with_mandatory(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior (no args_override:yes) with mandatory field and list args with split"""
config_file = tmp_path / "test.ini"
config_file.write_text("[TestSection]\nvalue=config1,config2\n")
loader = SettingsLoader(
args={"value": ["arg1", "arg2"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["mandatory:yes", "split:,"]}
)
# Should use config value because args_override:yes is not set (default: no override)
assert result["value"] == ["config1", "config2"]
captured = capsys.readouterr()
# Message is printed but without args_override:yes flag, override doesn't happen
assert "Command line option override" in captured.out
def test_load_settings_args_no_with_mandatory_valid(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior with string args (always overrides due to current logic)"""
config_file = tmp_path / "test.ini"
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
args={"value": "arg_value"},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["mandatory:yes"]}
)
# Current behavior: string args without split always override (regardless of args_override:yes)
assert result["value"] == "arg_value"
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_args_string_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that string arguments with args_override:yes work normally"""
config_file = tmp_path / "test.ini"
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
args={"value": "arg_value"},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["args_override:yes"]}
)
# Should use args value for non-list args with args_override:yes
assert result["value"] == "arg_value"
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_no_config_file_with_args(self, tmp_path: Path):
"""Test loading settings without config file but with mandatory args"""
config_file = tmp_path / "missing.ini"
@@ -704,5 +821,48 @@ class TestComplexScenarios:
assert result["emails"] == "test@example.com"
assert result["date"] == "2025-01-15"
def test_args_no_and_list_skip_combination(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test combination of args_override:yes flag and list argument skip behavior"""
config_file = tmp_path / "test.ini"
config_file.write_text(
"[Settings]\n"
"no_override=a,b,c\n"
"list_no_split=config_list\n"
"list_with_split=x,y,z\n"
"normal=config_normal\n"
)
loader = SettingsLoader(
args={
"no_override": ["arg1", "arg2"],
"list_no_split": ["arg1", "arg2"],
"list_with_split": ["p", "q", "r"],
"normal": "arg_normal"
},
config_file=config_file
)
result = loader.load_settings(
"Settings",
{
"no_override": ["split:,"],
"list_no_split": [],
"list_with_split": ["split:,", "args_override:yes"],
"normal": ["args_override:yes"]
}
)
# Should use config value (no args_override:yes flag for list with split)
assert result["no_override"] == ["a", "b", "c"]
# Should use config value because args is list without split
assert result["list_no_split"] == "config_list"
# Should use args value because split is defined AND args_override:yes is set
assert result["list_with_split"] == ["p", "q", "r"]
# Should use args value (args_override:yes set for string arg)
assert result["normal"] == "arg_normal"
captured = capsys.readouterr()
# Should see override messages (even though list_no_split prints, it doesn't apply)
assert "Command line option override" in captured.out
# __END__

View File

@@ -153,6 +153,19 @@ class TestLogSettingsParsing:
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.BARE
def test_parse_console_format_type_none(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to NONE"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.NONE,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.NONE
def test_parse_console_format_type_invalid(self, tmp_log_path: Path):
"""Test parsing with invalid console_format_type raises TypeError"""
settings: dict[str, Any] = {
@@ -207,6 +220,11 @@ class TestConsoleFormatSettingsFromString:
result = ConsoleFormatSettings.from_string('BARE')
assert result == ConsoleFormatSettings.BARE
def test_from_string_none(self):
"""Test from_string with 'NONE' returns correct format"""
result = ConsoleFormatSettings.from_string('NONE')
assert result == ConsoleFormatSettings.NONE
def test_from_string_invalid_returns_none(self):
"""Test from_string with invalid string returns None"""
result = ConsoleFormatSettings.from_string('INVALID')
@@ -234,6 +252,7 @@ class TestConsoleFormatSettingsFromString:
("CONDENSED", ConsoleFormatSettings.CONDENSED),
("MINIMAL", ConsoleFormatSettings.MINIMAL),
("BARE", ConsoleFormatSettings.BARE),
("NONE", ConsoleFormatSettings.NONE),
])
def test_from_string_all_valid_settings(self, setting_name: str, expected: Any):
"""Test from_string with all valid setting names"""

View File

@@ -140,4 +140,172 @@ class TestCustomConsoleFormatter:
assert "Critical message" in result
assert "CRITICAL" in result
# MARK: Test update_console_formatter
class TestUpdateConsoleFormatter:
"""Test cases for update_console_formatter method"""
def test_update_console_formatter_to_minimal(self, log_instance: Log):
"""Test updating console formatter to MINIMAL format"""
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_condensed(self, log_instance: Log):
"""Test updating console formatter to CONDENSED format"""
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_bare(self, log_instance: Log):
"""Test updating console formatter to BARE format"""
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_none(self, log_instance: Log):
"""Test updating console formatter to NONE format"""
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_all(self, log_instance: Log):
"""Test updating console formatter to ALL format"""
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_when_disabled(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test that update_console_formatter does nothing when console is disabled"""
# Disable console
basic_log_settings['console_enabled'] = False
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# This should not raise an error and should return early
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Verify console handler doesn't exist
assert log.CONSOLE_HANDLER not in log.handlers
def test_update_console_formatter_with_color_enabled(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test updating console formatter with color output enabled"""
basic_log_settings['console_color_output_enabled'] = True
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log.handlers[log.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter is CustomConsoleFormatter when colors enabled
assert isinstance(formatter, CustomConsoleFormatter)
def test_update_console_formatter_without_color(self, log_instance: Log):
"""Test updating console formatter without color output"""
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter is standard Formatter when colors disabled
assert isinstance(formatter, logging.Formatter)
# But not the colored version
assert not isinstance(formatter, CustomConsoleFormatter)
def test_update_console_formatter_multiple_times(self, log_instance: Log):
"""Test updating console formatter multiple times"""
# Update to MINIMAL
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter1 = console_handler.formatter
# Update to CONDENSED
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
formatter2 = console_handler.formatter
# Update to ALL
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
formatter3 = console_handler.formatter
# Verify each update created a new formatter
assert formatter1 is not formatter2
assert formatter2 is not formatter3
assert formatter1 is not formatter3
def test_update_console_formatter_preserves_handler_level(self, log_instance: Log):
"""Test that updating formatter preserves the handler's log level"""
original_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
new_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level
assert original_level == new_level
def test_update_console_formatter_format_output(
self, log_instance: Log, caplog: pytest.LogCaptureFixture
):
"""Test that updated formatter actually affects log output"""
# Set to BARE format (message only)
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
# Configure caplog to capture at the appropriate level
with caplog.at_level(logging.WARNING):
log_instance.warning("Test warning message")
# Verify message was logged
assert "Test warning message" in caplog.text
def test_update_console_formatter_none_format_output(
self, log_instance: Log, caplog: pytest.LogCaptureFixture
):
"""Test that NONE formatter outputs only the message without any formatting"""
# Set to NONE format (message only, no level indicator)
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
# Configure caplog to capture at the appropriate level
with caplog.at_level(logging.WARNING):
log_instance.warning("Test warning message")
# Verify message was logged
assert "Test warning message" in caplog.text
# __END__

View File

@@ -12,6 +12,7 @@ from corelibs.logging_handling.log import (
LogParent,
LogSettings,
ConsoleFormatSettings,
ConsoleFormat,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -108,4 +109,101 @@ class TestHandlerManagement:
result2 = log.add_handler("test", handler2)
assert result2 is False
def test_change_console_format_to_minimal(self, log_instance: Log):
"""Test changing console handler format to MINIMAL"""
original_formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
new_formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert new_formatter is not original_formatter
assert new_formatter is not None
def test_change_console_format_to_condensed(self, log_instance: Log):
"""Test changing console handler format to CONDENSED"""
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_bare(self, log_instance: Log):
"""Test changing console handler format to BARE"""
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_none(self, log_instance: Log):
"""Test changing console handler format to NONE"""
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_all(self, log_instance: Log):
"""Test changing console handler format to ALL"""
# Start with a different format
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_multiple_times(self, log_instance: Log):
"""Test changing console handler format multiple times"""
formatters: list[logging.Formatter | None] = []
for format_type in [
ConsoleFormatSettings.MINIMAL,
ConsoleFormatSettings.CONDENSED,
ConsoleFormatSettings.BARE,
ConsoleFormatSettings.NONE,
ConsoleFormatSettings.ALL,
]:
log_instance.update_console_formatter(format_type)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
formatters.append(formatter)
assert formatter is not None
# Verify each formatter is unique (new instance each time)
for i, formatter in enumerate(formatters):
for j, other_formatter in enumerate(formatters):
if i != j:
assert formatter is not other_formatter
def test_change_console_format_with_disabled_console(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test changing console format when console is disabled does nothing"""
basic_log_settings['console_enabled'] = False
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# Should not raise error, just return early
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Console handler should not exist
assert log.CONSOLE_HANDLER not in log.handlers
@pytest.mark.parametrize("format_type", [
ConsoleFormatSettings.ALL,
ConsoleFormatSettings.CONDENSED,
ConsoleFormatSettings.MINIMAL,
ConsoleFormatSettings.BARE,
ConsoleFormatSettings.NONE,
])
def test_change_console_format_parametrized(
self, log_instance: Log, format_type: ConsoleFormat # type: ignore
):
"""Test changing console format with all format types"""
log_instance.update_console_formatter(format_type)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
assert isinstance(formatter, logging.Formatter)
# __END__

View File

@@ -1,516 +0,0 @@
"""
PyTest: string_handling/text_colors
"""
import pytest
from corelibs.string_handling.text_colors import Colors
class TestColorsInitialState:
"""Tests for Colors class initial state"""
def test_bold_initial_value(self):
"""Test that bold has correct ANSI code"""
assert Colors.bold == '\033[1m'
def test_underline_initial_value(self):
"""Test that underline has correct ANSI code"""
assert Colors.underline == '\033[4m'
def test_end_initial_value(self):
"""Test that end has correct ANSI code"""
assert Colors.end == '\033[0m'
def test_reset_initial_value(self):
"""Test that reset has correct ANSI code"""
assert Colors.reset == '\033[0m'
class TestColorsNormal:
"""Tests for normal color ANSI codes"""
def test_black_normal(self):
"""Test black color code"""
assert Colors.black == "\033[30m"
def test_red_normal(self):
"""Test red color code"""
assert Colors.red == "\033[31m"
def test_green_normal(self):
"""Test green color code"""
assert Colors.green == "\033[32m"
def test_yellow_normal(self):
"""Test yellow color code"""
assert Colors.yellow == "\033[33m"
def test_blue_normal(self):
"""Test blue color code"""
assert Colors.blue == "\033[34m"
def test_magenta_normal(self):
"""Test magenta color code"""
assert Colors.magenta == "\033[35m"
def test_cyan_normal(self):
"""Test cyan color code"""
assert Colors.cyan == "\033[36m"
def test_white_normal(self):
"""Test white color code"""
assert Colors.white == "\033[37m"
class TestColorsBold:
"""Tests for bold color ANSI codes"""
def test_black_bold(self):
"""Test black bold color code"""
assert Colors.black_bold == "\033[1;30m"
def test_red_bold(self):
"""Test red bold color code"""
assert Colors.red_bold == "\033[1;31m"
def test_green_bold(self):
"""Test green bold color code"""
assert Colors.green_bold == "\033[1;32m"
def test_yellow_bold(self):
"""Test yellow bold color code"""
assert Colors.yellow_bold == "\033[1;33m"
def test_blue_bold(self):
"""Test blue bold color code"""
assert Colors.blue_bold == "\033[1;34m"
def test_magenta_bold(self):
"""Test magenta bold color code"""
assert Colors.magenta_bold == "\033[1;35m"
def test_cyan_bold(self):
"""Test cyan bold color code"""
assert Colors.cyan_bold == "\033[1;36m"
def test_white_bold(self):
"""Test white bold color code"""
assert Colors.white_bold == "\033[1;37m"
class TestColorsBright:
"""Tests for bright color ANSI codes"""
def test_black_bright(self):
"""Test black bright color code"""
assert Colors.black_bright == '\033[90m'
def test_red_bright(self):
"""Test red bright color code"""
assert Colors.red_bright == '\033[91m'
def test_green_bright(self):
"""Test green bright color code"""
assert Colors.green_bright == '\033[92m'
def test_yellow_bright(self):
"""Test yellow bright color code"""
assert Colors.yellow_bright == '\033[93m'
def test_blue_bright(self):
"""Test blue bright color code"""
assert Colors.blue_bright == '\033[94m'
def test_magenta_bright(self):
"""Test magenta bright color code"""
assert Colors.magenta_bright == '\033[95m'
def test_cyan_bright(self):
"""Test cyan bright color code"""
assert Colors.cyan_bright == '\033[96m'
def test_white_bright(self):
"""Test white bright color code"""
assert Colors.white_bright == '\033[97m'
class TestColorsDisable:
"""Tests for Colors.disable() method"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_disable_bold_and_underline(self):
"""Test that disable() sets bold and underline to empty strings"""
Colors.disable()
assert Colors.bold == ''
assert Colors.underline == ''
def test_disable_end_and_reset(self):
"""Test that disable() sets end and reset to empty strings"""
Colors.disable()
assert Colors.end == ''
assert Colors.reset == ''
def test_disable_normal_colors(self):
"""Test that disable() sets all normal colors to empty strings"""
Colors.disable()
assert Colors.black == ''
assert Colors.red == ''
assert Colors.green == ''
assert Colors.yellow == ''
assert Colors.blue == ''
assert Colors.magenta == ''
assert Colors.cyan == ''
assert Colors.white == ''
def test_disable_bold_colors(self):
"""Test that disable() sets all bold colors to empty strings"""
Colors.disable()
assert Colors.black_bold == ''
assert Colors.red_bold == ''
assert Colors.green_bold == ''
assert Colors.yellow_bold == ''
assert Colors.blue_bold == ''
assert Colors.magenta_bold == ''
assert Colors.cyan_bold == ''
assert Colors.white_bold == ''
def test_disable_bright_colors(self):
"""Test that disable() sets all bright colors to empty strings"""
Colors.disable()
assert Colors.black_bright == ''
assert Colors.red_bright == ''
assert Colors.green_bright == ''
assert Colors.yellow_bright == ''
assert Colors.blue_bright == ''
assert Colors.magenta_bright == ''
assert Colors.cyan_bright == ''
assert Colors.white_bright == ''
def test_disable_all_colors_at_once(self):
"""Test that all color attributes are empty after disable()"""
Colors.disable()
# Check that all public attributes are empty strings
for attr in dir(Colors):
if not attr.startswith('_') and attr not in ['disable', 'reset_colors']:
assert getattr(Colors, attr) == '', f"{attr} should be empty after disable()"
class TestColorsResetColors:
"""Tests for Colors.reset_colors() method"""
def setup_method(self):
"""Disable colors before each test"""
Colors.disable()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_reset_bold_and_underline(self):
"""Test that reset_colors() restores bold and underline"""
Colors.reset_colors()
assert Colors.bold == '\033[1m'
assert Colors.underline == '\033[4m'
def test_reset_end_and_reset(self):
"""Test that reset_colors() restores end and reset"""
Colors.reset_colors()
assert Colors.end == '\033[0m'
assert Colors.reset == '\033[0m'
def test_reset_normal_colors(self):
"""Test that reset_colors() restores all normal colors"""
Colors.reset_colors()
assert Colors.black == "\033[30m"
assert Colors.red == "\033[31m"
assert Colors.green == "\033[32m"
assert Colors.yellow == "\033[33m"
assert Colors.blue == "\033[34m"
assert Colors.magenta == "\033[35m"
assert Colors.cyan == "\033[36m"
assert Colors.white == "\033[37m"
def test_reset_bold_colors(self):
"""Test that reset_colors() restores all bold colors"""
Colors.reset_colors()
assert Colors.black_bold == "\033[1;30m"
assert Colors.red_bold == "\033[1;31m"
assert Colors.green_bold == "\033[1;32m"
assert Colors.yellow_bold == "\033[1;33m"
assert Colors.blue_bold == "\033[1;34m"
assert Colors.magenta_bold == "\033[1;35m"
assert Colors.cyan_bold == "\033[1;36m"
assert Colors.white_bold == "\033[1;37m"
def test_reset_bright_colors(self):
"""Test that reset_colors() restores all bright colors"""
Colors.reset_colors()
assert Colors.black_bright == '\033[90m'
assert Colors.red_bright == '\033[91m'
assert Colors.green_bright == '\033[92m'
assert Colors.yellow_bright == '\033[93m'
assert Colors.blue_bright == '\033[94m'
assert Colors.magenta_bright == '\033[95m'
assert Colors.cyan_bright == '\033[96m'
assert Colors.white_bright == '\033[97m'
class TestColorsDisableAndReset:
"""Tests for disable and reset cycle"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_disable_then_reset_cycle(self):
"""Test that colors can be disabled and then reset multiple times"""
# Initial state
original_red = Colors.red
# Disable
Colors.disable()
assert Colors.red == ''
# Reset
Colors.reset_colors()
assert Colors.red == original_red
# Disable again
Colors.disable()
assert Colors.red == ''
# Reset again
Colors.reset_colors()
assert Colors.red == original_red
def test_multiple_disables(self):
"""Test that calling disable() multiple times is safe"""
Colors.disable()
Colors.disable()
Colors.disable()
assert Colors.red == ''
assert Colors.blue == ''
def test_multiple_resets(self):
"""Test that calling reset_colors() multiple times is safe"""
Colors.reset_colors()
Colors.reset_colors()
Colors.reset_colors()
assert Colors.red == "\033[31m"
assert Colors.blue == "\033[34m"
class TestColorsUsage:
"""Tests for practical usage of Colors class"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_colored_string_with_reset(self):
"""Test creating a colored string with reset"""
result = f"{Colors.red}Error{Colors.end}"
assert result == "\033[31mError\033[0m"
def test_bold_colored_string(self):
"""Test creating a bold colored string"""
result = f"{Colors.bold}{Colors.yellow}Warning{Colors.end}"
assert result == "\033[1m\033[33mWarning\033[0m"
def test_underline_colored_string(self):
"""Test creating an underlined colored string"""
result = f"{Colors.underline}{Colors.blue}Info{Colors.end}"
assert result == "\033[4m\033[34mInfo\033[0m"
def test_bold_underline_colored_string(self):
"""Test creating a bold and underlined colored string"""
result = f"{Colors.bold}{Colors.underline}{Colors.green}Success{Colors.end}"
assert result == "\033[1m\033[4m\033[32mSuccess\033[0m"
def test_multiple_colors_in_string(self):
"""Test using multiple colors in one string"""
result = f"{Colors.red}Red{Colors.end} {Colors.blue}Blue{Colors.end}"
assert result == "\033[31mRed\033[0m \033[34mBlue\033[0m"
def test_bright_color_usage(self):
"""Test using bright color variants"""
result = f"{Colors.cyan_bright}Bright Cyan{Colors.end}"
assert result == "\033[96mBright Cyan\033[0m"
def test_bold_color_shortcut(self):
"""Test using bold color shortcuts"""
result = f"{Colors.red_bold}Bold Red{Colors.end}"
assert result == "\033[1;31mBold Red\033[0m"
def test_disabled_colors_produce_plain_text(self):
"""Test that disabled colors produce plain text without ANSI codes"""
Colors.disable()
result = f"{Colors.red}Error{Colors.end}"
assert result == "Error"
assert "\033[" not in result
def test_disabled_bold_underline_produce_plain_text(self):
"""Test that disabled formatting produces plain text"""
Colors.disable()
result = f"{Colors.bold}{Colors.underline}{Colors.green}Success{Colors.end}"
assert result == "Success"
assert "\033[" not in result
class TestColorsPrivateAttributes:
"""Tests to ensure private attributes are not directly accessible"""
def test_private_bold_not_accessible(self):
"""Test that __BOLD is private"""
with pytest.raises(AttributeError):
_ = Colors.__BOLD
def test_private_colors_not_accessible(self):
"""Test that private color attributes are not accessible"""
with pytest.raises(AttributeError):
_ = Colors.__RED
with pytest.raises(AttributeError):
_ = Colors.__GREEN
# Parametrized tests
@pytest.mark.parametrize("color_attr,expected_code", [
("black", "\033[30m"),
("red", "\033[31m"),
("green", "\033[32m"),
("yellow", "\033[33m"),
("blue", "\033[34m"),
("magenta", "\033[35m"),
("cyan", "\033[36m"),
("white", "\033[37m"),
])
def test_normal_colors_parametrized(color_attr: str, expected_code: str):
"""Parametrized test for normal colors"""
Colors.reset_colors()
assert getattr(Colors, color_attr) == expected_code
@pytest.mark.parametrize("color_attr,expected_code", [
("black_bold", "\033[1;30m"),
("red_bold", "\033[1;31m"),
("green_bold", "\033[1;32m"),
("yellow_bold", "\033[1;33m"),
("blue_bold", "\033[1;34m"),
("magenta_bold", "\033[1;35m"),
("cyan_bold", "\033[1;36m"),
("white_bold", "\033[1;37m"),
])
def test_bold_colors_parametrized(color_attr: str, expected_code: str):
"""Parametrized test for bold colors"""
Colors.reset_colors()
assert getattr(Colors, color_attr) == expected_code
@pytest.mark.parametrize("color_attr,expected_code", [
("black_bright", '\033[90m'),
("red_bright", '\033[91m'),
("green_bright", '\033[92m'),
("yellow_bright", '\033[93m'),
("blue_bright", '\033[94m'),
("magenta_bright", '\033[95m'),
("cyan_bright", '\033[96m'),
("white_bright", '\033[97m'),
])
def test_bright_colors_parametrized(color_attr: str, expected_code: str):
"""Parametrized test for bright colors"""
Colors.reset_colors()
assert getattr(Colors, color_attr) == expected_code
@pytest.mark.parametrize("color_attr", [
"bold", "underline", "end", "reset",
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
"black_bold", "red_bold", "green_bold", "yellow_bold",
"blue_bold", "magenta_bold", "cyan_bold", "white_bold",
"black_bright", "red_bright", "green_bright", "yellow_bright",
"blue_bright", "magenta_bright", "cyan_bright", "white_bright",
])
def test_disable_all_attributes_parametrized(color_attr: str):
"""Parametrized test that all color attributes are disabled"""
Colors.reset_colors()
Colors.disable()
assert getattr(Colors, color_attr) == ''
@pytest.mark.parametrize("color_attr", [
"bold", "underline", "end", "reset",
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
"black_bold", "red_bold", "green_bold", "yellow_bold",
"blue_bold", "magenta_bold", "cyan_bold", "white_bold",
"black_bright", "red_bright", "green_bright", "yellow_bright",
"blue_bright", "magenta_bright", "cyan_bright", "white_bright",
])
def test_reset_all_attributes_parametrized(color_attr: str):
"""Parametrized test that all color attributes are reset"""
Colors.disable()
Colors.reset_colors()
assert getattr(Colors, color_attr) != ''
assert '\033[' in getattr(Colors, color_attr)
# Edge case tests
class TestColorsEdgeCases:
"""Tests for edge cases and special scenarios"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_colors_class_is_not_instantiable(self):
"""Test that Colors class can be instantiated (it's not abstract)"""
# The class uses static methods, but can be instantiated
instance = Colors()
assert isinstance(instance, Colors)
def test_static_methods_work_on_instance(self):
"""Test that static methods work when called on instance"""
instance = Colors()
instance.disable()
assert Colors.red == ''
instance.reset_colors()
assert Colors.red == "\033[31m"
def test_concatenation_of_multiple_effects(self):
"""Test concatenating multiple color effects"""
result = f"{Colors.bold}{Colors.underline}{Colors.red_bright}Test{Colors.reset}"
assert "\033[1m" in result # bold
assert "\033[4m" in result # underline
assert "\033[91m" in result # red bright
assert "\033[0m" in result # reset
def test_empty_string_with_colors(self):
"""Test applying colors to empty string"""
result = f"{Colors.red}{Colors.end}"
assert result == "\033[31m\033[0m"
def test_nested_color_changes(self):
"""Test nested color changes in string"""
result = f"{Colors.red}Red {Colors.blue}Blue{Colors.end} Red again{Colors.end}"
assert result == "\033[31mRed \033[34mBlue\033[0m Red again\033[0m"
# __END__

26
uv.lock generated
View File

@@ -108,11 +108,12 @@ wheels = [
[[package]]
name = "corelibs"
version = "0.37.0"
version = "0.42.0"
source = { editable = "." }
dependencies = [
{ name = "corelibs-datetime" },
{ name = "corelibs-enum-base" },
{ name = "corelibs-text-colors" },
{ name = "corelibs-var" },
{ name = "cryptography" },
{ name = "jmespath" },
@@ -126,18 +127,20 @@ dev = [
{ name = "deepdiff" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "typing-extensions" },
]
[package.metadata]
requires-dist = [
{ name = "corelibs-datetime", specifier = ">=1.0.1", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "corelibs-enum-base", specifier = ">=1.0.0", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "corelibs-text-colors", specifier = ">=1.0.0", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "corelibs-var", specifier = ">=1.0.0", index = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" },
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "jmespath", specifier = ">=1.0.1" },
{ name = "jsonpath-ng", specifier = ">=1.7.0" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "requests", specifier = ">=2.32.4" },
{ name = "requests", extras = ["proxy"], specifier = ">=2.32.4" },
]
[package.metadata.requires-dev]
@@ -145,6 +148,7 @@ dev = [
{ name = "deepdiff", specifier = ">=8.6.1" },
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" },
{ name = "typing-extensions", specifier = ">=4.15.0" },
]
[[package]]
@@ -168,6 +172,15 @@ wheels = [
{ url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-enum-base/1.0.0/corelibs_enum_base-1.0.0-py3-none-any.whl", hash = "sha256:c305d4063c69021aaf9ef75fbcce961039dae3c3de7820febeac7082c998a1f8" },
]
[[package]]
name = "corelibs-text-colors"
version = "1.0.0"
source = { registry = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/" }
sdist = { url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-text-colors/1.0.0/corelibs_text_colors-1.0.0.tar.gz", hash = "sha256:482fee54eb94d6b8acff091b531213ea312fac866836951dc06b26994fa52161" }
wheels = [
{ url = "https://git.egplusww.jp/api/packages/PyPI/pypi/files/corelibs-text-colors/1.0.0/corelibs_text_colors-1.0.0-py3-none-any.whl", hash = "sha256:ed0ba1799ce437e3fec67c94f0d9adfddde26094143d6a019e05d7557fecb03c" },
]
[[package]]
name = "corelibs-var"
version = "1.0.0"
@@ -470,6 +483,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"