Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* TRACE log level and `patch_logging()` for fine-grained protocol-level debugging
* Support for `conf_override()` context manager to temporarily override configuration values
* Docstring documentation for `xlsx_to_map` function
* Unit tests for `xlsx_raw` function in formats module
Expand Down
6 changes: 6 additions & 0 deletions src/quorum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
get_handler,
get_bundle,
is_devel,
is_trace,
finalize,
before_request,
after_request,
Expand All @@ -123,6 +124,7 @@
has_context,
ensure_context,
onrun,
_level,
)
from .config import (
conf,
Expand Down Expand Up @@ -215,13 +217,17 @@
)
from .jsonf import load_json
from .log import (
SILENT,
TRACE,
MemoryHandler,
BaseFormatter,
ThreadFormatter,
rotating_handler,
smtp_handler,
patch_logging,
in_signature,
has_exception,
trace,
debug,
info,
warning,
Expand Down
26 changes: 24 additions & 2 deletions src/quorum/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,9 +680,22 @@ def start_log(
app,
name=None,
level=logging.WARN,
format_base=log.LOGGING_FORMAT,
format_tid=log.LOGGING_FORMAT_TID,
format_base=None,
format_tid=None,
):
# patches the logging infra-structure so that the TRACE level
# is properly registered and available for usage, this call
# is idempotent and safe to be called multiple times
log.patch_logging()

is_trace = level <= log.TRACE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize log level before TRACE comparison

start_log() now does level <= log.TRACE before any normalization, which raises TypeError on Python 3 when callers pass string levels (for example "DEBUG" or "TRACE"). This is a regression from the previous behavior where string levels could flow to logger.setLevel(...) without crashing, and it affects direct users of the public quorum.start_log API.

Useful? React with 👍 / 👎.

format_base = format_base or (
log.LOGGING_FORMAT_TRACE if is_trace else log.LOGGING_FORMAT
)
format_tid = format_tid or (
log.LOGGING_FORMAT_TRACE_TID if is_trace else log.LOGGING_FORMAT_TID
)
Comment on lines +686 to +697

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start_log() computes is_trace = level <= log.TRACE, which will raise TypeError if callers pass a string level (eg 'INFO') or another non-int level value. Since start_log() is part of the public API, consider normalizing level to an int at the start (eg via _level(level) / logging._checkLevel) before comparing and before logger.setLevel(level).

Copilot uses AI. Check for mistakes.

# tries to retrieve some of the default configuration values
# that are going to be used in the logger startup
format = config.conf("LOGGING_FORMAT", None)
Expand Down Expand Up @@ -918,6 +931,13 @@ def is_devel(app=None):
return level < logging.INFO


def is_trace(app=None):
level = get_level(app=app)
if not level:
return False
return level <= log.TRACE


def finalize(value):
# returns an empty string as value representation
# for unset values, this is the default representation
Expand Down Expand Up @@ -1127,6 +1147,8 @@ def _level(level):
return level
if level == "SILENT":
return log.SILENT
if level == "TRACE":
return log.TRACE
if hasattr(logging, "_checkLevel"):
return logging._checkLevel(level)
return logging.getLevelName(level)
Expand Down
60 changes: 58 additions & 2 deletions src/quorum/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@
includes the thread identification number and should be
used for messages called from outside the main thread """

LOGGING_FORMAT_TRACE_T = "%%(asctime)s [%%(name)s] [%%(levelname)s] %s%%(pathname)s:%%(lineno)d | %%(message)s"
""" The format to be used when the logging level is set to TRACE,
includes file path and line number to allow for fine-grained debugging
of low-level protocol operations """

LOGGING_FORMAT_TRACE_TID_T = "%%(asctime)s [%%(name)s] [%%(levelname)s] %s[%%(thread)d] %%(pathname)s:%%(lineno)d | %%(message)s"
Comment on lines +58 to +63

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TRACE format templates already include %(name)s, but they are still parameterized with LOGGING_EXTRA (%s). If LOGGING_EXTRA is enabled (it expands to [%(name)s] ), TRACE logs will end up duplicating the logger name segment. Consider not applying LOGGING_EXTRA to TRACE formats (or making TRACE templates omit %(name)s and rely on LOGGING_EXTRA).

Suggested change
LOGGING_FORMAT_TRACE_T = "%%(asctime)s [%%(name)s] [%%(levelname)s] %s%%(pathname)s:%%(lineno)d | %%(message)s"
""" The format to be used when the logging level is set to TRACE,
includes file path and line number to allow for fine-grained debugging
of low-level protocol operations """
LOGGING_FORMAT_TRACE_TID_T = "%%(asctime)s [%%(name)s] [%%(levelname)s] %s[%%(thread)d] %%(pathname)s:%%(lineno)d | %%(message)s"
LOGGING_FORMAT_TRACE_T = "%%(asctime)s [%%(name)s] [%%(levelname)s] %.0s%%(pathname)s:%%(lineno)d | %%(message)s"
""" The format to be used when the logging level is set to TRACE,
includes file path and line number to allow for fine-grained debugging
of low-level protocol operations """
LOGGING_FORMAT_TRACE_TID_T = "%%(asctime)s [%%(name)s] [%%(levelname)s] %.0s[%%(thread)d] %%(pathname)s:%%(lineno)d | %%(message)s"

Copilot uses AI. Check for mistakes.
""" The format to be used when the logging level is set to TRACE and
the thread is not the main one, includes file path, line number and
thread identification number """

LOGGING_EXTRA = "[%(name)s] " if config.conf("LOGGING_EXTRA", cast=bool) else ""
""" The extra logging attributes that are going to be applied
to the format strings to obtain the final on the logging """
Expand All @@ -75,12 +85,19 @@
or an handler, this is used as an utility for debugging
purposes more that a real feature for production systems """

LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
TRACE = logging.DEBUG - 5
""" The trace level used for extremely detailed and verbose
logging of protocol-level operations, this is meant to be
used for fine-grained debugging of low-level operations
like raw byte transfers and frame parsing """

LEVELS = ("TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
""" The sequence of levels from the least sever to the
most sever this sequence may be used to find all the
Comment on lines +88 to 96

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TRACE is introduced, but LEVELS doesn’t include 'TRACE'. As a result, MemoryHandler.get_messages_l() will treat TRACE records as an unknown level and won’t store them in messages_l, so get_latest(level=...) filtering can’t work for TRACE. Consider adding 'TRACE' to LEVELS (and possibly LEVEL_ALIAS) so TRACE messages participate in severity filtering consistently.

Copilot uses AI. Check for mistakes.
levels that are considered more sever that a level """

LEVEL_ALIAS = {
"TRAC": "TRACE",
"DEBU": "DEBUG",
"WARN": "WARNING",
"INF": "INFO",
Expand All @@ -96,6 +113,8 @@

LOGGING_FORMAT = LOGGING_FORMAT_T % LOGGING_EXTRA
LOGGING_FORMAT_TID = LOGGING_FORMAT_TID_T % LOGGING_EXTRA
LOGGING_FORMAT_TRACE = LOGGING_FORMAT_TRACE_T % LOGGING_EXTRA
LOGGING_FORMAT_TRACE_TID = LOGGING_FORMAT_TRACE_TID_T % LOGGING_EXTRA


class MemoryHandler(logging.Handler):
Expand Down Expand Up @@ -326,26 +345,50 @@ def smtp_handler(
)


def patch_logging():
if hasattr(logging, "_quorum_patched"):
return

# patches the logging infra-structure adding the trace level
# support and the corresponding trace method to the logger
logging.addLevelName(TRACE, "TRACE")
logging.Logger.trace = _trace

logging._quorum_patched = True


def in_signature(callable, name):
has_full = hasattr(inspect, "getfullargspec")
if has_full:
spec = inspect.getfullargspec(callable)
else:
spec = inspect.getargspec(callable)
args, _varargs, kwargs = spec[:3]
return (args and name in args) or (kwargs and "secure" in kwargs)
return bool((args and name in args) or (kwargs and "secure" in kwargs))
Comment on lines 360 to +367

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in_signature() is meant to check whether a callable accepts a parameter named by name, but the current implementation hard-codes 'secure' for the **kwargs case (kwargs and 'secure' in kwargs). With inspect.getfullargspec(), kwargs here is the name of the var-keyword parameter (eg 'kwargs'), so this check will almost always be false and doesn’t correctly represent “accepts arbitrary kwargs”. Consider updating this to (a) check name against positional/kw-only args, and (b) treat presence of spec.varkw as accepting the parameter.

Copilot uses AI. Check for mistakes.
Comment on lines 360 to +367

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: in_signature() kwargs check uses hardcoded "secure" instead of name parameter.

The function checks if name is in args, but the kwargs check incorrectly uses the hardcoded string "secure" instead of the name parameter. This will cause incorrect results when checking for any parameter other than "secure".

Additionally, the static analysis correctly flags that callable shadows a Python builtin.

🐛 Proposed fix
-def in_signature(callable, name):
-    has_full = hasattr(inspect, "getfullargspec")
-    if has_full:
-        spec = inspect.getfullargspec(callable)
-    else:
-        spec = inspect.getargspec(callable)
-    args, _varargs, kwargs = spec[:3]
-    return bool((args and name in args) or (kwargs and "secure" in kwargs))
+def in_signature(func, name):
+    has_full = hasattr(inspect, "getfullargspec")
+    if has_full:
+        spec = inspect.getfullargspec(func)
+    else:
+        spec = inspect.getargspec(func)
+    args, _varargs, kwargs = spec[:3]
+    return bool((args and name in args) or (kwargs and name in kwargs))
🧰 Tools
🪛 Ruff (0.15.9)

[error] 359-359: Function argument callable is shadowing a Python builtin

(A002)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/quorum/log.py` around lines 359 - 366, The in_signature function has two
issues: it uses the hardcoded string "secure" when checking kwargs instead of
the provided name parameter, and its parameter named callable shadows a Python
builtin; fix by renaming the parameter (e.g., to func or fn) and updating the
kwargs check to use the name variable (i.e., check if kwargs and name in
kwargs). Ensure you update all references inside in_signature (including the
inspect.getfullargspec/getargspec call) to use the new parameter name.



def has_exception():
info = sys.exc_info()
return not info == (None, None, None)


def trace(message, *args, **kwargs):
kwargs.pop("log_trace", False)
logger = common.base().get_log()
if not logger:
return
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.trace(message, *args, **kwargs)
Comment on lines +380 to +382

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trace() calls logger.trace(...) unconditionally. If patch_logging() hasn’t been called yet (eg apps that don’t go through start_log()), this will raise AttributeError because logging.Logger won’t have a trace method. Consider calling patch_logging() inside trace() (idempotent) or falling back to logger.log(TRACE, ...) when .trace is missing.

Suggested change
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.trace(message, *args, **kwargs)
patch_logging()
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
if hasattr(logger, "trace"):
logger.trace(message, *args, **kwargs)
else:
logger.log(TRACE, message, *args, **kwargs)

Copilot uses AI. Check for mistakes.


def debug(message, *args, **kwargs):
kwargs.pop("log_trace", False)
logger = common.base().get_log()
if not logger:
return
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.debug(message, *args, **kwargs)


Expand All @@ -354,6 +397,8 @@ def info(message, *args, **kwargs):
logger = common.base().get_log()
if not logger:
return
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.info(message, *args, **kwargs)
if not log_trace or not has_exception():
return
Expand All @@ -367,6 +412,8 @@ def warning(message, *args, **kwargs):
logger = common.base().get_log()
if not logger:
return
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.warning(message, *args, **kwargs)
if not log_trace or not has_exception():
return
Expand All @@ -380,6 +427,8 @@ def error(message, *args, **kwargs):
logger = common.base().get_log()
if not logger:
return
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.error(message, *args, **kwargs)
if not log_trace or not has_exception():
return
Expand All @@ -393,9 +442,16 @@ def critical(message, *args, **kwargs):
logger = common.base().get_log()
if not logger:
return
if sys.version_info >= (3, 8):
kwargs.setdefault("stacklevel", 2)
logger.critical(message, *args, **kwargs)
if not log_trace or not has_exception():
return
lines = traceback.format_exc().splitlines()
for line in lines:
logger.error(line, *args, **kwargs)


def _trace(self, message, *args, **kwargs):
if self.isEnabledFor(TRACE):
self._log(TRACE, message, args, **kwargs)
Loading
Loading