From 5d2e4fc03c1adc9607e74fbed70c44e99ea8c393 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sat, 8 Aug 2026 20:29:35 +0200 Subject: [PATCH 1/6] lib.log.log(): Fix early return without flags set When position, prio, and date are all absent from the log flags, the msg variable is empty. The early return 'if not len(msg): return' would then skip printing the actual message in margs. Fix by checking both msg and margs before returning. This ensures messages are still printed even when no prefix flags are set. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/jw/pkg/lib/log.py b/src/python/jw/pkg/lib/log.py index 6e9f70e7..ee636f72 100644 --- a/src/python/jw/pkg/lib/log.py +++ b/src/python/jw/pkg/lib/log.py @@ -240,7 +240,7 @@ def log( # export msg += _log_prefix - if not len(msg): + if not len(msg) and not len(margs): return if len(margs): -- 2.55.0 From 404b05ca262152dbd02825f041923cdef6af5ad6 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sat, 8 Aug 2026 20:53:54 +0200 Subject: [PATCH 2/6] lib.log.log(): Fix superfluous message whitespace When prefix flags (position, prio, date) are combined with message arguments, log() would double-space the output (e.g. ' Created') or omit the separator entirely (e.g. 'Created'). The root cause was that log() prepended a space to every argument, regardless of whether the prefix already ended with one. Fix: only prepend a space before the first argument when the prefix doesn't already end with one. This produces exactly one separator between prefix and content regardless of which flags are active. Also fix log_m(): skip empty strings (the sentinel '') so it doesn't contribute a space. And fix the early return that dropped messages when no prefix flags were set (previous commit). Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/log.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/python/jw/pkg/lib/log.py b/src/python/jw/pkg/lib/log.py index ee636f72..57f38b09 100644 --- a/src/python/jw/pkg/lib/log.py +++ b/src/python/jw/pkg/lib/log.py @@ -181,6 +181,8 @@ def log_m(prio: int, *args: Any, **kwargs: Any) -> None: # export if isinstance(a, list): margs += '\n'.join([str(elem) for elem in a]) continue + if a == '': + continue margs += ' ' + str(a) if 'caller' not in kwargs: caller = get_caller_pos(1) @@ -205,7 +207,7 @@ def log( # export color_off = '' if f_date in _flags: - msg += datetime.now().strftime("%b %d %H:%M:%S.%f ") + msg += datetime.now().strftime("%b %d %H:%M:%S.%f") + ' ' if f_prio in _flags: msg += _short_prio_str[prio] + ' ' @@ -227,8 +229,8 @@ def log( # export margs = '' if len(args): - for a in args: - margs += ' ' + str(a) + for i, a in enumerate(args): + margs += (' ' if (i or (msg and not msg.endswith(' '))) else '') + str(a) if only_printable: margs = _special_char_regex.sub( lambda mo: _special_chars[mo.string[mo.start():mo.end()]], margs -- 2.55.0 From c784c8ecb1760b674f438b065d1912d18fe201e6 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sat, 8 Aug 2026 20:57:05 +0200 Subject: [PATCH 3/6] lib.log.log(): Use .join() to build argument string The argument string builder in log() previously iterated with enumerate and checked per-iteration whether to prepend a space. Replace the loop with ' '.join(str(a) for a in args), which is a single C-level operation. Move the leading-space logic after the only_printable block so the regex transformations see the raw joined content. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/log.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/python/jw/pkg/lib/log.py b/src/python/jw/pkg/lib/log.py index 57f38b09..0c6276f2 100644 --- a/src/python/jw/pkg/lib/log.py +++ b/src/python/jw/pkg/lib/log.py @@ -229,13 +229,14 @@ def log( # export margs = '' if len(args): - for i, a in enumerate(args): - margs += (' ' if (i or (msg and not msg.endswith(' '))) else '') + str(a) + margs = ' '.join(str(a) for a in args) if only_printable: margs = _special_char_regex.sub( lambda mo: _special_chars[mo.string[mo.start():mo.end()]], margs ) margs = re.sub('[\x01-\x1f]', '.', margs) + if msg and not msg.endswith(' '): + margs = ' ' + margs for file in _log_file_streams: print(msg + _clean_log_prefix + margs, file = file) -- 2.55.0 From 34ca0a7a95beaba4fbdfe4eacbbfbc39bf1f6010 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 9 Aug 2026 04:20:56 +0200 Subject: [PATCH 4/6] lib.log.log(): Convert string-based flags to Flag Replace the set of flag strings with a Flag enum for better type safety and bitwise operations. Add parse_flags() to convert comma-separated strings into a LogFlag value. Update set_flags() and set_log_flags() to accept str | LogFlag | None. set_flags() and set_log_flags() don't return str anylonger which is a breaking change, but shouldn't be a problem because it's not used anywhere. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/log.py | 103 ++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/src/python/jw/pkg/lib/log.py b/src/python/jw/pkg/lib/log.py index 0c6276f2..87371181 100644 --- a/src/python/jw/pkg/lib/log.py +++ b/src/python/jw/pkg/lib/log.py @@ -5,9 +5,11 @@ import re import sys import syslog +from enum import Flag, auto + from datetime import datetime from os.path import basename -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, override if TYPE_CHECKING: import io @@ -81,16 +83,44 @@ CONSOLE_FONT_WHITE = '\033[37m' CONSOLE_FONT_BLINK = '\033[5m' CONSOLE_FONT_OFF = '\033[m' -f_position = 'position' -f_module = 'module' -f_date = 'date' -f_stderr = 'stderr' -f_stdout = 'stdout' -f_prio = 'prio' -f_color = 'color' -f_default = [ f_position, f_stderr, f_prio, f_color ] +# -- ytools compatible log flags +#syslog +#console +#color +#prio +#position +#ide +#trace_rename_thread_to_shorter +#trace_rename_thread_to_longer +#trace_inout +#skip_openlog +#id +#date +#pid +#highlight_first_error + +class LogFlag(Flag): + POSITION = auto() + MODULE = auto() + DATE = auto() + STDERR = auto() + STDOUT = auto() + PRIO = auto() + COLOR = auto() + + @override + def __str__(self) -> str: + def _get_name(f: LogFlag) -> str: + return f.name or '' + + return ','.join( + f.name.lower() + for f in sorted(self, key = _get_name) + if f.name + ) + +_flags = LogFlag.POSITION | LogFlag.STDERR | LogFlag.PRIO | LogFlag.COLOR -_flags = set(f_default) _log_prefix = '' _clean_log_prefix = '' _file_name_len = 20 @@ -206,25 +236,25 @@ def log( # export color_on = '' color_off = '' - if f_date in _flags: + if LogFlag.DATE & _flags: msg += datetime.now().strftime("%b %d %H:%M:%S.%f") + ' ' - if f_prio in _flags: + if LogFlag.PRIO & _flags: msg += _short_prio_str[prio] + ' ' - if f_position in _flags: + if LogFlag.POSITION & _flags: if 'caller' in kwargs: mod, name, line = kwargs['caller'] else: mod, name, line = get_caller_pos(1) - if f_module in _flags: + if LogFlag.MODULE & _flags: msg += pad(mod, _module_name_len) msg += pad(name, _file_name_len) + '[' + pad(str(line), 4, True) + ']' - if f_color in _flags: + if LogFlag.COLOR & _flags: color_on, color_off = console_color_chars(prio) margs = '' @@ -255,10 +285,10 @@ def log( # export elif _streams: files = [s.stream for s in _streams.values()] else: - if f_stdout in _flags: + if LogFlag.STDOUT & _flags: files.append(sys.stdout) - if f_stderr in _flags: + if LogFlag.STDERR & _flags: files.append(sys.stderr) if not len(files): @@ -310,30 +340,27 @@ def set_log_level(level: str | int | None = None) -> int: def set_level(level: str | int | None = None) -> int: # export return set_log_level(level) -def set_flags(flags: str | None) -> str: # export - global _flags - ret = ','.join(_flags) - if flags is not None: - _flags = set(flags.split(',')) +def parse_flags(flags: str) -> LogFlag: + ret: LogFlag = LogFlag(0) + s = set(flags.split(',')) + for f in LogFlag: + assert f.name is not None + if f.name.lower() in s: + ret |= f return ret -def set_log_flags(flags: str) -> str: - return set_flags(flags) +parse_log_flags = parse_flags - #syslog - #console - #color - #prio - #position - #ide - #trace_rename_thread_to_shorter - #trace_rename_thread_to_longer - #trace_inout - #skip_openlog - #id - #date - #pid - #highlight_first_error +def set_flags(flags: str | LogFlag | None) -> LogFlag: # export + global _flags + if isinstance(flags, str): + flags = parse_flags(flags) + ret = _flags + if flags is not None: + _flags = flags + return ret + +set_log_flags = set_flags def append_to_prefix(prefix: str) -> str: # export global _log_prefix -- 2.55.0 From 90e221e106934a6363789f336d5f66dcb1354aed Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 9 Aug 2026 04:58:43 +0200 Subject: [PATCH 5/6] lib.App: Make default env var names overridable Add _default_*_env() helper methods that return the environment variable names, and use them in __init__() instead of hard-coded strings. This allows subclasses to override the names. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/App.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index e7bcbc17..560fe3ee 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -43,6 +43,18 @@ class App: # export default = None, ) + def _default_log_flags_env(self) -> str: + return 'JW_DEFAULT_LOG_FLAGS' + + def _default_log_level_env(self) -> str: + return 'JW_DEFAULT_LOG_LEVEL' + + def _default_log_file_env(self) -> str: + return 'JW_DEFAULT_LOG_FILE' + + def _default_show_backtrace_env(self) -> str: + return 'JW_DEFAULT_SHOW_BACKTRACE' + def __init__( self, description: str = '', @@ -114,15 +126,15 @@ class App: # export self.__args: Namespace | None = None self.__cmdline: str | None = None self.__default_log_flags: str = os.getenv( - 'JW_DEFAULT_LOG_FLAGS', default = 'stderr,position,prio,color' + self._default_log_flags_env(), default = 'stderr,position,prio,color' ) self.__default_log_level: str | int | None = os.getenv( - 'JW_DEFAULT_LOG_LEVEL', default = NOTICE + self._default_log_level_env(), default = NOTICE ) self.__default_log_file: str | None = os.getenv( - 'JW_DEFAULT_LOG_FILE', default = None + self._default_log_file_env(), default = None ) - backtrace: str | bool = os.getenv('JW_DEFAULT_SHOW_BACKTRACE', False) + backtrace: str | bool = os.getenv(self._default_show_backtrace_env(), False) self.__back_trace = isinstance(backtrace, str) and backtrace.lower() in { '1', 'true', -- 2.55.0 From 2eeaaf96814906de26809a65bbdd90cc26121e98 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 9 Aug 2026 03:42:28 +0200 Subject: [PATCH 6/6] lib.App: Make default log values overridable The __init__() method reads default log configuration directly from environment variables, making it impossible for subclasses to change defaults. Extract the defaults into _default_log_flags(), _default_log_level(), _default_log_file(), and _default_show_backtrace() methods, then call them from __init__(). Subclasses can now override these methods to customize defaults without needing to override the entire __init__() method. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/App.py | 57 ++++++++++++++----- .../integration/jw-pkg/help/test-expected.txt | 2 +- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/python/jw/pkg/lib/App.py b/src/python/jw/pkg/lib/App.py index 560fe3ee..016ddaa2 100644 --- a/src/python/jw/pkg/lib/App.py +++ b/src/python/jw/pkg/lib/App.py @@ -9,7 +9,19 @@ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace from typing import Any, cast, override, TYPE_CHECKING from .AsyncRunner import AsyncRunner -from .log import DEBUG, ERR, NOTICE, log, log_m, set_log_flags, set_log_level +from .log import ( + DEBUG, + ERR, + NOTICE, + LogFlag, + log, + log_m, + parse_log_flags, + parse_log_level, + set_log_flags, + set_log_level +) + from .Types import LoadTypes from .util import pretty_cmd @@ -46,15 +58,27 @@ class App: # export def _default_log_flags_env(self) -> str: return 'JW_DEFAULT_LOG_FLAGS' + def _default_log_flags(self, default: LogFlag) -> LogFlag: + return default + def _default_log_level_env(self) -> str: return 'JW_DEFAULT_LOG_LEVEL' + def _default_log_level(self, default: int) -> int: + return default + def _default_log_file_env(self) -> str: return 'JW_DEFAULT_LOG_FILE' + def _default_log_file(self, default: str | None) -> str | None: + return default + def _default_show_backtrace_env(self) -> str: return 'JW_DEFAULT_SHOW_BACKTRACE' + def _default_show_backtrace(self, default: bool) -> bool: + return default + def __init__( self, description: str = '', @@ -125,20 +149,25 @@ class App: # export self.__args: Namespace | None = None self.__cmdline: str | None = None - self.__default_log_flags: str = os.getenv( - self._default_log_flags_env(), default = 'stderr,position,prio,color' + + self.__default_log_flags = self._default_log_flags( + LogFlag.STDERR | LogFlag.POSITION | LogFlag.PRIO | LogFlag.COLOR ) - self.__default_log_level: str | int | None = os.getenv( - self._default_log_level_env(), default = NOTICE - ) - self.__default_log_file: str | None = os.getenv( - self._default_log_file_env(), default = None - ) - backtrace: str | bool = os.getenv(self._default_show_backtrace_env(), False) - self.__back_trace = isinstance(backtrace, str) and backtrace.lower() in { - '1', - 'true', - } + if (env := os.getenv(self._default_log_flags_env(), None)) is not None: + self.__default_log_flags = parse_log_flags(env) + + self.__default_log_level = self._default_log_level(NOTICE) + if (env := os.getenv(self._default_log_level_env(), None)) is not None: + self.__default_log_level = parse_log_level(env) + + self.__default_log_file = self._default_log_file(None) + if (env := os.getenv(self._default_log_file_env(), None)) is not None: + self.__default_log_file = env + + self.__back_trace = self._default_show_backtrace(False) + if (env := os.getenv(self._default_show_backtrace_env())) is not None: + self.__back_trace = env.lower() in ['1', 'true'] + set_log_flags(self.__default_log_flags) set_log_level(self.__default_log_level) diff --git a/test/integration/jw-pkg/help/test-expected.txt b/test/integration/jw-pkg/help/test-expected.txt index 95651dcb..391580ec 100644 --- a/test/integration/jw-pkg/help/test-expected.txt +++ b/test/integration/jw-pkg/help/test-expected.txt @@ -11,7 +11,7 @@ jw-pkg swiss army knife options: --log-flags LOG_FLAGS - Log flags (default: stderr,position,prio,color) + Log flags (default: color,position,prio,stderr) --log-level LOG_LEVEL Log level (default: 5) --log-file LOG_FILE Log file (default: None) -- 2.55.0