stratified_packager.toolbelt.logging

A thread-safe, structured logging suite for QGIS 4.0+ plugins.

This module provides a QgisHandler — a logging.Handler subclass — that routes standard-library log records to any combination of:

Plugin developers call QgisLoggerWrapper.setup() once per plugin to create the root logger wired to a QgisHandler, then call QgisLoggerWrapper.get_logger() in each sub-module to obtain a named child wrapper that propagates records up to the root handler automatically:

# In the plugin's __init__.py — once per plugin:
root = QgisLoggerWrapper.setup(
    name="MyPlugin",
    targets=Target.LOG | Target.BAR,
)

# In any sub-module — no handler needed:
log = QgisLoggerWrapper.get_logger("MyPlugin.utils")
log = QgisLoggerWrapper.get_logger("MyPlugin.network.http")

# Per-call overrides work on any wrapper regardless of hierarchy level.
log.warning(
    "No features selected.",
    targets=Target.DIALOG,
    box_config=MessageBoxConfig(title="Selection warning"),
)

On plugin unload, call QgisLoggerWrapper.teardown() to detach and dispose everything setup() attached: the logging registry is process-global and survives QGIS plugin reloads, so a handler left behind would render every record one extra time per reload.

Thread safety

QgisHandler owns a QgisHandlerSignals instance that lives on the GUI thread. Cross-thread dispatch is handled by connecting the private _render_requested signal to QgisHandler._render() with the default AutoConnection, which Qt automatically promotes to a QueuedConnection when the signal is emitted from a worker thread.

Classes

QgisHandler(plugin_name[, targets, tag, ...])

A logging.Handler that routes records to QGIS UI targets.

QgisHandlerSignals(*args, **kwargs)

Public Qt signals emitted by QgisHandler on every handled record.

QgisLoggerWrapper(logger)

A logging.Logger wrapper that takes QGIS-specific keyword arguments on logging calls.

class stratified_packager.toolbelt.logging.QgisHandler(plugin_name, targets=<Target.LOG: 1>, tag=None, bar=None, box_parent=None, bar_config=None, box_config=None, level=0)[source]

A logging.Handler that routes records to QGIS UI targets.

Handler-level defaults apply to every record. When the record was emitted by a QgisLoggerWrapper, per-call overrides stored under the private sentinel keys take precedence for that record only.

Each handler owns a QgisHandlerSignals instance accessible via signals. Connect to QgisHandlerSignals.message_emitted or QgisHandlerSignals.error_emitted to react to log records from other parts of a plugin without subclassing or patching the handler.

Note

Cross-thread dispatch is handled by the private _render_requested signal on signals, which is connected to _render() with an AutoConnection. Qt automatically promotes this to a QueuedConnection when the signal is emitted from a worker thread, ensuring all Qt widget operations execute on the GUI thread.

Parameters:
  • plugin_name (str)

  • targets (Target)

  • tag (str | None)

  • bar (QgsMessageBar | None)

  • box_parent (QWidget | None)

  • bar_config (MessageBarConfig | None)

  • box_config (MessageBoxConfig | None)

  • level (int)

static _render_log(payload)[source]

Write to qgis.core.QgsMessageLog.

Parameters:

payload (EmitPayload) – The EmitPayload describing the message.

Return type:

None

__init__(plugin_name, targets=<Target.LOG: 1>, tag=None, bar=None, box_parent=None, bar_config=None, box_config=None, level=0)[source]

Initialize the handler.

Parameters:
  • plugin_name (str) – Human-readable name shown in log tags and dialog titles.

  • targets (Target) – Default combination of Target flags. Can be overridden per call via QgisLoggerWrapper.

  • tag (str | None) – Tag shown in qgis.core.QgsMessageLog; falls back to plugin_name when None.

  • bar (QgsMessageBar | None) – The qgis.gui.QgsMessageBar from the QGIS interface. Required when Target.BAR may ever be active.

  • box_parent (QWidget | None) – Parent widget for QMessageBox modal dialogs (typically iface.mainWindow()); when None, dialogs are created without a parent (not recommended).

  • bar_config (MessageBarConfig | None) – Default message-bar appearance.

  • box_config (MessageBoxConfig | None) – Default modal-dialog appearance.

  • level (int) – Minimum logging level forwarded by this handler.

Raises:

ValueError – If Target.BAR is enabled but no bar was provided.

_render(payload)[source]

Dispatch payload to each active target and emit public signals.

Always runs on the GUI thread — either called directly from emit() when already on the GUI thread, or delivered via the QueuedConnection on _render_requested when called from a worker thread.

After all render targets have been called, QgisHandlerSignals.message_emitted is emitted with the formatted message and logging level. If the level is logging.WARNING or above, QgisHandlerSignals.error_emitted is also emitted.

A logging sink must never raise into the caller of log.xxx() (sync path) or into Qt’s event loop (queued path), so any exception from a render target or from a user-connected signal slot is routed to logging.Handler.handleError().

Parameters:

payload (EmitPayload) – The EmitPayload describing the message.

Return type:

None

_render_bar(payload)[source]

Push a notification onto qgis.gui.QgsMessageBar.

If show_progress is True, a QProgressBar is created and embedded. Any caller-supplied buttons are also added.

Parameters:

payload (EmitPayload) – The EmitPayload describing the message.

Raises:

RuntimeError – If the handler was not configured with a qgis.gui.QgsMessageBar but the BAR target is active, or if the message bar item layout cannot be obtained when buttons or a progress bar need to be added.

Return type:

None

_render_dialog(payload)[source]

Show a QMessageBox modal dialog.

The dialog is parented to the box_parent widget supplied at construction (typically the QGIS main window) so Qt manages its lifetime and stacking; when no parent was supplied it is created parentless.

Parameters:

payload (EmitPayload) – The EmitPayload describing the message.

Return type:

None

emit(record)[source]

Format record and dispatch it to all active targets.

Per-call overrides injected by QgisLoggerWrapper — stored on the record under the _K_TARGETS, _K_BAR_CFG, and _K_BOX_CFG keys — take precedence over the handler’s own defaults. When Target.BAR is requested via a per-call override but no qgis.gui.QgsMessageBar was supplied to the handler, the BAR target is silently dropped from that record’s targets to avoid a runtime error.

When called from a worker thread, the private _render_requested signal is emitted; Qt delivers it to _render() on the GUI thread via a QueuedConnection. When called from the GUI thread, _render() is called directly with no event-loop round-trip.

Parameters:

record (LogRecord) – The logging.LogRecord to handle.

Return type:

None

_bar: QgsMessageBar | None
_bar_config: MessageBarConfig
_box_config: MessageBoxConfig
_box_parent: QWidget | None
_plugin_name: str
_signals: QgisHandlerSignals
_tag: str
_targets: Target
property bar_config: MessageBarConfig

Default message-bar configuration (may be overridden per call).

Returns:

The current MessageBarConfig.

property box_config: MessageBoxConfig

Default message-box configuration (may be overridden per call).

Returns:

The current MessageBoxConfig.

property plugin_name: str

Human-readable plugin identifier.

Returns:

The plugin name string.

property signals: QgisHandlerSignals

Public Qt signals for this handler.

Connect to QgisHandlerSignals.message_emitted or QgisHandlerSignals.error_emitted to react to log records without subclassing the handler:

handler.signals.message_emitted.connect(my_slot)
Returns:

The QgisHandlerSignals instance owned by this handler.

property targets: Target

Default output targets (may be overridden per call).

Returns:

A Target flag combination.

class stratified_packager.toolbelt.logging.QgisHandlerSignals(*args, **kwargs)[source]

Public Qt signals emitted by QgisHandler on every handled record.

This class is a QObject signals carrier: it exists solely to host pyqtSignal() definitions, sidestepping the QObject multiple-inheritance limitations of PyQt6 while still providing fully typed, connectable Qt signals.

An instance is created automatically by QgisHandler and exposed via QgisHandler.signals. Plugin developers connect to its signals to drive custom UI elements — for example a status-bar label or a secondary log widget — without needing to subclass or patch the handler.

Signals are emitted on the GUI thread regardless of which thread originally called QgisHandler.emit(), because the internal _render_requested signal uses a QueuedConnection for cross-thread delivery and these public signals are emitted from QgisHandler._render(), which always runs on the GUI thread.

handler = QgisHandler(plugin_name="MyPlugin")

# Show every log message in a custom label.
handler.signals.message_emitted.connect(lambda msg, level: status_label.setText(msg))

# Flash the toolbar red on errors.
handler.signals.error_emitted.connect(lambda msg: toolbar.setStyleSheet("background: red"))
Parameters:
_render_requested

Private signal used for cross-thread dispatch to QgisHandler._render().

Plugin code must not connect to this signal directly.

alias of object

error_emitted

Emitted only when the record’s level is logging.WARNING or above.

Argument is the formatted message string. Useful for connecting a single error-notification slot without a level-filtering wrapper.

alias of str

message_emitted

Emitted for every successfully handled record, after all render targets have been called.

Arguments are the formatted message string and the integer logging level (e.g. logging.INFO).

alias of str

class stratified_packager.toolbelt.logging.QgisLoggerWrapper(logger)[source]

A logging.Logger wrapper that takes QGIS-specific keyword arguments on logging calls.

Rather than subclassing logging.Logger — which would require overriding the private logging.Logger._log method with a signature incompatible with the base class, violating the Liskov Substitution Principle — this class holds a plain logging.Logger instance via composition and delegates all logging calls to it after packing any QGIS-specific kwargs into the extra dict.

The wrapped logger is accessible via the logger property for the rare cases where a plain logging.Logger reference may be needed.

All standard logging methods (debug(), info(), warning(), error(), critical(), exception(), log()) — plus the custom-level success() — accept three optional keyword-only arguments in addition to the standard ones:

Typical usage — one setup() call per plugin, then get_logger() in every sub-module:

# plugin/__init__.py
root = QgisLoggerWrapper.setup("MyPlugin", targets=Target.LOG)

# plugin/plugin_main.py
log = QgisLoggerWrapper.get_logger("MyPlugin.plugin_main")

# plugin/processing/provider.py
log = QgisLoggerWrapper.get_logger("MyPlugin.processing.provider")

Do not call the constructor directly; use setup() or get_logger() so that the logging registry is managed correctly and the wrapped logger participates in the hierarchy.

Parameters:

logger (Logger)

classmethod get_logger(name)[source]

Retrieve or create a named child logger with no handler attached.

Use this in every sub-module that needs a logger. Records propagate to the nearest ancestor that has a handler — which should be the root plugin logger created by setup():

log = QgisLoggerWrapper.get_logger("MyPlugin.utils")
log = QgisLoggerWrapper.get_logger("MyPlugin.network.http")
Parameters:

name (str) – Dotted logger name. Should start with the root plugin name to participate in the plugin’s logging hierarchy (e.g. "MyPlugin.utils").

Return type:

QgisLoggerWrapper

Returns:

A QgisLoggerWrapper around the named logger, which has no handlers and propagate set to True (the logging default for new loggers).

classmethod setup(name, targets=<Target.LOG: 1>, iface=None, tag=None, bar_config=None, box_config=None, level=10, formatter=None, *, propagate=False, filters=None)[source]

Create or retrieve the root plugin logger, wired to a QgisHandler.

Call this once per plugin (typically in the plugin’s __init__.py or classFactory). It obtains a logging.Logger named name from the logging registry, attaches a QgisHandler to it, wraps it in a QgisLoggerWrapper, and returns the wrapper.

Repeated calls are safe and idempotent: handlers and filters attached by a previous call are detached and disposed first, then rebuilt from the current arguments. This also holds across QGIS plugin reloads — the reloader purges the plugin’s modules and re-imports them, so the handler left attached to the process-global logging registry by the previous load is an instance of a stale class object; it is detected by type name and replaced. Leaving it attached would render every record one extra time per reload. Call teardown() from the plugin’s unload() to detach without re-attaching.

Sub-module loggers should be created with get_logger() using dotted child names; they propagate records to this root logger automatically and require no handler of their own.

Parameters:
  • name (str) – Logger name and human-readable identifier used in QGIS UI targets (e.g. the QgsMessageLog tag). Typically the plugin’s display name (e.g. "MyPlugin").

  • targets (Target) – Default output targets. Can be overridden per call. Defaults to Target.LOG.

  • iface (QgisInterface | None) – The QGIS interface instance, used to obtain the message bar for Target.BAR output and the main window as the parent for Target.DIALOG modal dialogs.

  • tag (str | None) – Topic label for QgsMessageLog; falls back to name when None.

  • bar_config (MessageBarConfig | None) – Default message-bar display options.

  • box_config (MessageBoxConfig | None) – Default modal-dialog display options.

  • level (int) – Minimum level this logger forwards to its handler.

  • formatter (logging.Formatter | None) – A custom logging.Formatter. When None, _default_formatter() is used.

  • propagate (bool) – Whether to propagate records to ancestor loggers. Defaults to False to prevent duplicate output via the root logging handler.

  • filters (Iterable[_FilterType] | None) – Iterable of filters to add to the logger. Filters of the same types attached by a previous call are replaced.

Return type:

QgisLoggerWrapper

Returns:

A QgisLoggerWrapper wrapping the configured root logger.

classmethod teardown(name)[source]

Detach and dispose everything setup() attached to the logger named name.

Call this from the plugin’s unload() so the handler does not outlive the plugin: the logging registry is process-global, so a handler left attached would both leak (its QgisHandlerSignals carrier is a QObject) and render every record once more alongside the handler installed by the next load. The handler is closed and its signals carrier disconnected and scheduled for deletion; cross-thread render payloads still queued on it are dropped. Filters attached by setup() are removed; handlers and filters attached by other code are left untouched.

Safe to call when setup() never ran for name. Records logged after teardown fall back to logging.lastResort (stderr) until setup() runs again.

Parameters:

name (str) – Root plugin logger name previously passed to setup().

Return type:

None

static _default_formatter()[source]

Build the default structured log formatter.

The format string produces records like:

2024-11-01 14:23:05,123 [WARNING ] MyPlugin.plugin_main:42 — File not found.
Return type:

Formatter

Returns:

A logging.Formatter instance.

static _pack_extra(extra, *, targets=None, bar_config=None, box_config=None)[source]
Overloads:
  • extra (None), targets (None), bar_config (None), box_config (None) → None

  • extra (Mapping[str, object]), targets (Target | None), bar_config (MessageBarConfig | None), box_config (MessageBoxConfig | None) → dict[str, object]

  • extra (Mapping[str, object] | None), targets (Target), bar_config (MessageBarConfig | None), box_config (MessageBoxConfig | None) → dict[str, object]

  • extra (Mapping[str, object] | None), targets (Target | None), bar_config (MessageBarConfig), box_config (MessageBoxConfig | None) → dict[str, object]

  • extra (Mapping[str, object] | None), targets (Target | None), bar_config (MessageBarConfig | None), box_config (MessageBoxConfig) → dict[str, object]

Merge QGIS-specific overrides into a fresh copy of extra.

When all arguments are None, None is returned immediately so the common no-override path has no allocation cost.

Parameters:
Returns:

A new dict merging the provided extra with the QGIS-specific overrides under the private keys defined in this module, or None if all arguments are None.

__init__(logger)[source]

Private constructor; use setup() or get_logger().

Parameters:

logger (Logger) – The logging.Logger to wrap.

critical(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity CRITICAL.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

debug(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity DEBUG.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

error(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity ERROR.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

exception(msg, *args, exc_info=True, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity ERROR, including exception information.

Equivalent to calling error() with exc_info=True. Meant to be called from an except block.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

info(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity INFO.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

log(level, msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with the integer severity level.

To pass exception information, use the keyword argument exc_info with a true value, e.g.

`python logger.log(level, "We have a %s", "mysterious problem", exc_info=True) `

The QGIS-specific parameters are removed from kwargs so that the standard logging.Logger.log() never sees them. They are instead stored in the extra dict under the private sentinel keys _K_TARGETS, _K_BAR_CFG, and _K_BOX_CFG, where QgisHandler.emit() will retrieve them.

Parameters:
  • level (int) – Integer logging level.

  • msg (object) – The log message (may contain %-style format placeholders).

  • *args (object) – Positional arguments for message formatting.

  • exc_info (bool | tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | BaseException | None) – If it doesn’t evaluate to False, it causes exception information to be added to the logging message. If an exception tuple (in the format returned by sys.exc_info()) or an exception instance is provided, it’s used; otherwise, sys.exc_info() is called to get the exception information.

  • stack_info (bool) – If true, stack information is added to the logging message, including the actual logging call.

  • stacklevel (int) – If greater than 1, the corresponding number of stack frames are skipped when computing the line number and function name set in the LogRecord created for the logging event.

  • extra (Mapping[str, object] | None) – A dictionary used to populate the __dict__ of the LogRecord created for the logging event with user-defined attributes.

  • targets (Target | None) – Per-call Target override, or None to use the handler default.

  • bar_config (MessageBarConfig | None) – Per-call MessageBarConfig override, or None to use the handler default.

  • box_config (MessageBoxConfig | None) – Per-call MessageBoxConfig override, or None to use the handler default.

Return type:

None

success(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity SUCCESS.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

warning(msg, *args, exc_info=None, stack_info=False, stacklevel=1, extra=None, targets=None, bar_config=None, box_config=None)[source]

Log msg % args with severity WARNING.

See QgisLoggerWrapper.log() for details on parameters.

Parameters:
Return type:

None

_logger: Logger
property logger: Logger

The underlying logging.Logger instance.

Returns:

The wrapped logging.Logger.

class stratified_packager.toolbelt.logging._SupportsFilter(*args, **kwargs)[source]
__init__(*args, **kwargs)
filter(record, /)[source]
Parameters:

record (LogRecord)

Return type:

bool | LogRecord

_abc_impl = <_abc._abc_data object>
_is_protocol = True
stratified_packager.toolbelt.logging._detach_setup_artifacts(logger, filter_types=())[source]

Detach and dispose everything QgisLoggerWrapper.setup() attached to logger.

Handlers are detached when they carry the _K_SETUP_OWNED marker or are (possibly stale) QgisHandler instances; each one is closed, its QgisHandlerSignals carrier is disconnected and scheduled for deletion, and cross-thread render payloads still queued on it are dropped. Filters are detached when they carry the marker or when their type’s (__module__, __qualname__) pair is in filter_types. Handlers and filters attached by other code are left untouched.

Disposal of each handler is contained: a failure is logged at WARNING level and the remaining cleanup proceeds.

Parameters:
  • logger (Logger) – The logger to clean.

  • filter_types (Container[tuple[str, str]]) – (__module__, __qualname__) pairs of filter types whose unmarked instances (attached by a build of this module predating the marker) should also be detached.

Return type:

None

stratified_packager.toolbelt.logging._is_main_thread()[source]

Return True when called from Qt’s GUI thread.

Uses qgis.PyQt.QtCore.QThread.currentThread() compared against the thread that owns the QApplication instance, which is always Qt’s GUI thread. This is reliable across all threading models — Python threads, Qt threads created via QThread, and threads from Qt’s C++ thread pool (QThreadPool / QtConcurrent) — unlike threading.current_thread() is threading.main_thread(), which only tracks threads known to the CPython runtime.

Return type:

bool | None

Returns:

True if the caller is running on Qt’s GUI thread, None if there is no running QApplication instance, or False otherwise.

stratified_packager.toolbelt.logging._is_qgis_handler_like(handler)[source]

Return whether handler is a QgisHandler, including stale-class instances.

An isinstance() check alone is not reload-safe: QGIS purges the plugin’s modules from sys.modules on reload and re-imports them, so a handler attached by a previous load is an instance of that load’s — now stale — class object, which fails isinstance() against the freshly imported QgisHandler. Such instances are recognized by their type’s __module__ / __qualname__ pair instead.

Parameters:

handler (Handler) – The handler to inspect.

Return type:

bool

Returns:

True for current and stale QgisHandler instances.

stratified_packager.toolbelt.logging._is_setup_owned(obj)[source]

Return whether obj carries the _K_SETUP_OWNED ownership marker.

Parameters:

obj (object) – A handler or filter currently attached to a logger.

Return type:

bool

Returns:

True when obj was attached by QgisLoggerWrapper.setup().

stratified_packager.toolbelt.logging._mark_setup_owned(obj)[source]

Stamp the _K_SETUP_OWNED ownership marker onto obj, best-effort.

Objects that reject new attributes (e.g. slotted classes) are left unmarked; stale instances of such filters are still replaced by the type matching in QgisLoggerWrapper.setup().

Parameters:

obj (object) – A handler or filter being attached by QgisLoggerWrapper.setup().

Return type:

None

stratified_packager.toolbelt.logging._K_SETUP_OWNED: Final = '_qgis_setup_owned'

Marker attribute stamped on handlers and filters attached by QgisLoggerWrapper.setup().

Stored on the attached objects themselves (not on records) so that a later call — possibly made by a fresh import of this module after a QGIS plugin reload, when all class objects have been re-created — can still recognize and replace them.

type stratified_packager.toolbelt.logging._FilterType = Filter | Callable[[LogRecord], bool | LogRecord] | _SupportsFilter
type stratified_packager.toolbelt.logging._SysExcInfoType = tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None]