Skip to content

Conversation

@FBumann
Copy link
Member

@FBumann FBumann commented Nov 21, 2025

Description

Brief description of the changes in this PR.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Code refactoring

Related Issues

Closes #(issue number)

Testing

  • I have tested my changes
  • Existing tests still pass

Checklist

  • My code follows the project style
  • I have updated documentation if needed
  • I have added tests for new functionality (if applicable)

Summary by CodeRabbit

  • New Features

    • Simplified logging API: enable_console(), enable_file(), disable(), presets (exploring, notebook, production, silent, debug); SUCCESS level and multi-line boxed formatting with optional colors.
  • Breaking Changes

    • Logging backend migrated to Python stdlib logging (colorlog optional); users relying on the previous backend may need to update logging calls/config.
  • Documentation

    • Updated getting-started, migration guide and examples to show the new logging approach.
  • Tests

    • Tests updated and expanded to cover new formatters, presets and file/console behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

  ✅ Summary of Changes

  Key Features Implemented:

  1. Stdout by default (was stderr in loguru)
    - Logs now go to sys.stdout by default
    - Configurable via stream parameter: CONFIG.Logging.enable_console('INFO', stream=sys.stderr)
  2. SUCCESS log level preserved
    - Custom level 25 (between INFO and WARNING)
    - Green color in console output
    - logger.success() method available
  3. Multi-line formatting with box borders
    - Single-line: INFO     Message
    - Multi-line:
    INFO     ┌─ First line
           │  Middle line
           └─ Last line
  4. Colored output with colorlog
    - DEBUG: cyan
    - INFO: white
    - SUCCESS: green
    - WARNING: yellow
    - ERROR: red
    - CRITICAL: red on white background
  5. Simplified API
    - CONFIG.Logging.enable_console('INFO') - stdout, colored
    - CONFIG.Logging.enable_console('INFO', colored=False) - plain text
    - CONFIG.Logging.enable_console('INFO', stream=sys.stderr) - stderr output
    - CONFIG.Logging.enable_file('INFO', 'app.log') - file logging
    - CONFIG.Logging.disable() - disable all logging
  6. Backward compatibility
    - change_logging_level() still works (deprecated)
    - Helper methods preserved: CONFIG.debug(), CONFIG.exploring(), etc.
  Summary of the improved format:

  Format: [dimmed time] [colored level] │ message

  Features:
  - ⏰ Time displayed in HH:MM:SS format (dimmed so not distracting)
  - 🎨 Level colored for quick scanning (DEBUG=cyan, INFO=white, SUCCESS=green, WARNING=yellow, ERROR=red, CRITICAL=bold red)
  - 📊 Clean separator │ between level and message
  - 📦 Multi-line support with box borders (┌─, │, └─)
  - 📍 Stdout by default (configurable)
…oredMultilineFormatter

CONFIG.Logging.set_colors()
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Migrates project logging from Loguru to Python's stdlib logging with optional colorlog; adds Multiline/ColoredMultiline formatters and a SUCCESS level; introduces CONFIG.Logging helpers (enable_console, enable_file, disable, set_colors) and presets; updates modules to use logging.getLogger('flixopt'); replaces loguru dependency with colorlog.

Changes

Cohort / File(s) Summary
Config core
flixopt/config.py
New stdlib-based logging subsystem: MultilineFormatter and ColoredMultilineFormatter, COLORLOG_AVAILABLE, new SUCCESS level and Logger.success(), RotatingFileHandler support, public CONFIG.Logging API (enable_console, enable_file, disable, set_colors), presets (exploring, notebook, production, silent, debug, browser_plotting), reset() and to_dict(); removed legacy CONFIG.apply/load flow and reworked defaults/handler lifecycle.
Module logger replacements
Multiple modules
flixopt/{aggregation,calculation,color_processing,components,core,effects,elements,flow_system,interface,io,linear_converters,modeling,network_app,plotting,results,solvers,structure}.py
Replaced from loguru import logger with import logging and logger = logging.getLogger('flixopt'); removed Loguru-specific APIs (e.g., opt(lazy=True)), minor import reorderings, and adapted logging calls to stdlib.
Package init
flixopt/__init__.py
Add module-level 'flixopt' logger creation (WARNING level) and attach NullHandler.
Examples & docs
docs/getting-started.md, docs/user-guide/migration-guide-v3.md, examples/05_Two-stage-optimization/two_stage_optimization.py, .github/ISSUE_TEMPLATE/bug_report.yml
Update examples and docs to use CONFIG.Logging.enable_console(...), show stdlib logging snippets, and reflect silent-by-default behavior and migration guidance; example-level logger now created via logging.getLogger('flixopt').
Tests
tests/test_config.py, test_deprecations.py
Tests updated to use stdlib logging (logging.getLogger('flixopt')), to import and validate MultilineFormatter, to reflect silent default and new CONFIG.Logging API, and to exercise console/file handlers, color/no-color paths, SUCCESS level and handler deduplication; DEPRECATION_REMOVAL_VERSION import moved to flixopt.config.
Dependency manifest
pyproject.toml
Removed loguru dependency; added colorlog >= 6.8.0, < 7.
Changelog & docs
CHANGELOG.md, docs/*
Added 4.1.0 Unreleased entry documenting logging migration, breaking changes, migration notes, deprecations, removals, and docs updates.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Application
    participant Config as CONFIG (flixopt.config)
    participant Logger as logging.getLogger('flixopt')
    participant Console as StreamHandler
    participant File as RotatingFileHandler
    participant Color as colorlog (optional)

    Note over Config,Logger: Configure logging via CONFIG helpers
    App->>Config: CONFIG.Logging.enable_console('INFO')
    Config->>Logger: getLogger('flixopt') / setLevel(INFO)
    alt colorlog available
        Config->>Color: create ColoredMultilineFormatter
        Color->>Console: attach colored formatter
    else
        Config->>Console: attach MultilineFormatter
    end

    App->>Logger: logger.success("multi\nline\nresult")
    Logger->>Console: format (Multiline/ColoredMultiline)
    Console-->>App: write to stdout/stderr

    App->>Config: CONFIG.Logging.enable_file('app.log')
    Config->>File: attach RotatingFileHandler + MultilineFormatter
    Logger->>File: write rotated logs

    App->>Config: CONFIG.Logging.disable()
    Config->>Logger: remove handlers / silence logger
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • flixopt/config.py (formatter correctness, handler lifecycle, colorlog fallback, SUCCESS level integration).
    • tests/test_config.py (stream capture, color vs non-color outputs, file rotation/deduplication).
    • Spot-check across modules for any remaining Loguru idioms or incorrect logger placement (after TYPE_CHECKING where applicable).

Possibly related PRs

Poem

🐰 I hopped through code with eager feet,

swapped Loguru for stdlib neat.
Colors shimmer, boxes line the text,
enable_console and files connect.
Quiet by default — a tidy feat.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is a template placeholder with minimal detail. It does not explain what changes were made, why (security concerns with loguru CVE), or the migration approach. Fill in the description with actual change details: explain the loguru-to-standard-logging migration, mention the CVE security concern, describe the new CONFIG API, and clarify the colorlog integration.
Title check ❓ Inconclusive The title 'Feature/loguru cve2' is vague and uses a branch-naming convention rather than a descriptive summary. It does not clearly convey the main objective (replacing loguru with standard logging to address security concerns). Replace with a clear, descriptive title such as 'Replace loguru with Python standard logging and colorlog' or 'Remove loguru dependency and migrate to standard logging for security'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 88.00% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c73b8a8 and 4b44aac.

📒 Files selected for processing (10)
  • .github/ISSUE_TEMPLATE/bug_report.yml (1 hunks)
  • CHANGELOG.md (1 hunks)
  • docs/user-guide/migration-guide-v3.md (1 hunks)
  • flixopt/__init__.py (2 hunks)
  • flixopt/aggregation.py (3 hunks)
  • flixopt/calculation.py (3 hunks)
  • flixopt/config.py (7 hunks)
  • flixopt/linear_converters.py (2 hunks)
  • flixopt/network_app.py (3 hunks)
  • tests/test_config.py (2 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
flixopt/linear_converters.py (1)

1107-1146: Fix loguru-style logger.warning calls in check_bounds (they now break with stdlib logging)

After switching logger to logging.getLogger(), the logger.warning() calls in check_bounds() still use loguru's {}-placeholder + positional-args style. Stdlib logging expects %-style formatting; with no % tokens in the message but extra args passed, these calls will raise a TypeError at runtime the first time an out-of-bounds value is hit.

Refactor them to use f-strings:

     if not np.all(value_arr > lower_bound):
         logger.warning(
-            "'{}.{}' <= lower bound {}. {}.min={}, shape={}",
-            element_label,
-            parameter_label,
-            lower_bound,
-            parameter_label,
-            float(np.min(value_arr)),
-            np.shape(value_arr),
+            f"'{element_label}.{parameter_label}' <= lower bound {lower_bound}. "
+            f"{parameter_label}.min={float(np.min(value_arr))}, shape={np.shape(value_arr)}"
         )
     if not np.all(value_arr < upper_bound):
         logger.warning(
-            "'{}.{}' >= upper bound {}. {}.max={}, shape={}",
-            element_label,
-            parameter_label,
-            upper_bound,
-            parameter_label,
-            float(np.max(value_arr)),
-            np.shape(value_arr),
+            f"'{element_label}.{parameter_label}' >= upper bound {upper_bound}. "
+            f"{parameter_label}.max={float(np.max(value_arr))}, shape={np.shape(value_arr)}"
         )

Lines 1128–1129 and 1138–1139.

🧹 Nitpick comments (4)
flixopt/calculation.py (1)

258-263: Consider guarding the expensive main-results YAML formatting with a log-level check.

logger.info(f'{" Main Results ":#^80}\n' + fx_io.format_yaml_string(...)) eagerly formats a potentially large YAML string whenever log_main_results is True, even if the effective logger level is above INFO. With loguru you previously relied on opt(lazy=True) to avoid this cost.

To better mirror the old behavior and avoid unnecessary work when INFO is disabled, you could add a level check:

-        if should_log:
-            logger.info(
-                f'{" Main Results ":#^80}\n'
-                + fx_io.format_yaml_string(self.main_results, compact_numeric_lists=True)
-            )
+        if should_log and logger.isEnabledFor(logging.INFO):
+            logger.info(
+                f'{" Main Results ":#^80}\n'
+                + fx_io.format_yaml_string(self.main_results, compact_numeric_lists=True)
+            )

This keeps output identical when INFO is enabled but avoids computing the YAML when it would be dropped anyway.

flixopt/network_app.py (1)

768-788: Consider logging the startup URL instead of using print

shownetwork() currently prints the URL (print(f'Network visualization started on http://127.0.0.1:{port}/')). For consistency with the new logging scheme and better controllability in non-interactive environments, consider emitting this via logger.info instead.

flixopt/aggregation.py (1)

84-109: Guard expensive describe_clusters() call with log-level check (optional)

cluster() now does logger.info(self.describe_clusters()), meaning describe_clusters() runs unconditionally even if INFO is disabled. With loguru this was lazy-evaluated. While this likely isn’t a major hotspot, you can restore the lazy behaviour cheaply:

@@     def cluster(self) -> None:
-        self.clustering_duration_seconds = timeit.default_timer() - start_time  # Zeit messen:
-        logger.info(self.describe_clusters())
+        self.clustering_duration_seconds = timeit.default_timer() - start_time  # Zeit messen:
+        if logger.isEnabledFor(logging.INFO):
+            logger.info(self.describe_clusters())
flixopt/__init__.py (1)

65-68: Consider clarifying the comment and variable usage.

Two minor observations:

  1. The comment says "no handlers" but NullHandler() is added on line 68. While NullHandler effectively suppresses output, it's technically a handler. Consider: "(silent: WARNING level, NullHandler)".

  2. The _logger variable is only used for initialization and never referenced again. Since logging.getLogger() returns the same instance everywhere, you could simplify to:

-# Initialize logger with default configuration (silent: WARNING level, no handlers)
-_logger = logging.getLogger('flixopt')
-_logger.setLevel(logging.WARNING)
-_logger.addHandler(logging.NullHandler())
+# Initialize logger with default configuration (silent: WARNING level, NullHandler)
+logger = logging.getLogger('flixopt')
+logger.setLevel(logging.WARNING)
+logger.addHandler(logging.NullHandler())

That said, the current implementation is functionally correct and follows library logging best practices.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b41c92a and 5b1a738.

📒 Files selected for processing (24)
  • CHANGELOG.md (1 hunks)
  • docs/getting-started.md (1 hunks)
  • examples/05_Two-stage-optimization/two_stage_optimization.py (1 hunks)
  • flixopt/__init__.py (4 hunks)
  • flixopt/aggregation.py (3 hunks)
  • flixopt/calculation.py (3 hunks)
  • flixopt/color_processing.py (1 hunks)
  • flixopt/components.py (2 hunks)
  • flixopt/config.py (7 hunks)
  • flixopt/core.py (1 hunks)
  • flixopt/effects.py (2 hunks)
  • flixopt/elements.py (2 hunks)
  • flixopt/flow_system.py (2 hunks)
  • flixopt/interface.py (2 hunks)
  • flixopt/io.py (2 hunks)
  • flixopt/linear_converters.py (2 hunks)
  • flixopt/modeling.py (1 hunks)
  • flixopt/network_app.py (2 hunks)
  • flixopt/plotting.py (2 hunks)
  • flixopt/results.py (2 hunks)
  • flixopt/solvers.py (1 hunks)
  • flixopt/structure.py (2 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_config.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tests/test_config.py (1)
flixopt/config.py (1)
  • CONFIG (173-723)
flixopt/modeling.py (2)
flixopt/config.py (1)
  • CONFIG (173-723)
flixopt/structure.py (1)
  • Submodel (1361-1540)
flixopt/solvers.py (1)
flixopt/config.py (1)
  • CONFIG (173-723)
flixopt/calculation.py (1)
flixopt/io.py (1)
  • format_yaml_string (255-302)
flixopt/__init__.py (1)
flixopt/config.py (3)
  • CONFIG (173-723)
  • change_logging_level (726-747)
  • get_logger (116-137)
🪛 GitHub Actions: Python Package CI/CD
examples/05_Two-stage-optimization/two_stage_optimization.py

[error] 19-19: Ruff Linting: E402 Module level import not at top of file.

🪛 GitHub Check: lint
flixopt/config.py

[failure] 83-84: Ruff (I001)
flixopt/config.py:83:13: I001 Import block is un-sorted or un-formatted

🔇 Additional comments (23)
pyproject.toml (1)

43-43: colorlog dependency range looks fine; please verify version choice against target Python versions.

The colorlog >= 6.8.0, < 7 constraint is consistent with the new standard-logging + optional color backend. Please just double-check that this range is tested against all supported Python versions (3.10–3.13) and that there are no known CVEs in the selected series before release.

flixopt/modeling.py (1)

1-1: Shared logger initialization in modeling.py is consistent with the new logging setup.

Using logging.getLogger('flixopt') here aligns this module with the project-wide logging refactor and is safe given no behavior change elsewhere in the file.

Also applies to: 10-10

flixopt/flow_system.py (1)

7-7: FlowSystem now correctly uses the shared stdlib logger.

Switching to logging.getLogger('flixopt') while keeping the same log calls (warning, info, debug, critical, error) preserves behavior and fits the new CONFIG-based logging setup.

Also applies to: 37-38

flixopt/elements.py (1)

7-7: Elements now use the shared stdlib logger without changing behavior.

Bus and Flow plausibility warnings now go through logging.getLogger('flixopt'), which matches the new global logging strategy and keeps semantics intact.

Also applies to: 46-47

flixopt/solvers.py (1)

7-7: Shared logger added to solvers.py without behavior change.

Defining logger = logging.getLogger('flixopt') here is consistent with other modules and does not affect current solver option handling; it’s ready for future logging without altering behavior now.

Also applies to: 13-13

flixopt/interface.py (1)

8-8: Interface module logging now correctly targets the shared flixopt logger.

The warnings/debug messages in InvestParameters.transform_data are now routed through stdlib logging while keeping the same messaging, which is exactly what we want under the new CONFIG-based logging system.

Also applies to: 25-26

flixopt/core.py (1)

6-6: Core now uses the shared stdlib logger for debug output in drop_constant_arrays.

Replacing the prior logger with logging.getLogger('flixopt') keeps the debug message behavior while aligning with the new centralized logging configuration.

Also applies to: 17-18

flixopt/calculation.py (1)

20-20: ****

The custom SUCCESS level and logger.success method are already properly implemented and registered in flixopt/config.py (lines 20–33). The setup includes logging.addLevelName() to register the level, and logging.Logger.success is directly attached to the Logger class. Since calculation.py imports from .config early (line 28), before any logger calls, the wiring is guaranteed to be in place. No additional setup or test coverage is required.

Likely an incorrect or invalid review comment.

flixopt/plotting.py (1)

28-51: Stdlib logger wiring in plotting module looks correct

Using a module-level logger = logging.getLogger('flixopt') and f-string messages is consistent with the new logging setup; no loguru-specific formatting remains here.

flixopt/io.py (1)

5-25: Logging migration in io module is consistent

Module-level logger = logging.getLogger('flixopt') and the existing f-string-based warnings integrate cleanly with stdlib logging.

flixopt/color_processing.py (1)

8-16: Color processing logger migration is safe

The new module-level logger integrates cleanly; all logger.debug/logger.warning calls are f-string or literal messages, so no formatting issues with stdlib logging.

flixopt/linear_converters.py (1)

7-23: Logger initialization matches the new logging design

Importing logging and using logging.getLogger('flixopt') keeps this module aligned with the rest of the package-wide migration.

flixopt/components.py (1)

7-29: Components module logger hook is consistent

The stdlib logging import and shared logger = logging.getLogger('flixopt') are wired correctly, and the single warning in _plausibility_checks() uses f-strings compatible with stdlib logging.

flixopt/network_app.py (1)

3-29: Network app now uses shared stdlib logger correctly

The new logging import and logger = logging.getLogger('flixopt') plus the logger.error(...) use in flow_graph() align with the rest of the logging migration.

flixopt/aggregation.py (1)

12-41: Aggregation module logger setup is aligned with the new logging system

Introducing import logging and logger = logging.getLogger('flixopt') integrates this module with the shared logger used elsewhere.

flixopt/effects.py (1)

10-29: Effects module logging migration is straightforward and correct

Switching to logging.getLogger('flixopt') and using f-string messages (e.g., when registering new effects) works cleanly with stdlib logging and matches the rest of the codebase.

flixopt/__init__.py (3)

6-6: LGTM: Standard logging import added.

The import is correctly placed and enables the module-level logger initialization below.


27-27: LGTM: Public API extended with get_logger.

The import correctly exposes the new get_logger helper function from the config module.


38-38: LGTM: get_logger added to public API.

Correctly exposes the function in __all__ for public use.

flixopt/results.py (2)

5-5: LGTM: Logging import added.

Correctly placed in the standard library imports section.


30-30: LGTM: Module-level logger initialized.

Correctly creates a logger using the 'flixopt' namespace, which will inherit configuration from the module-level setup in __init__.py.

tests/test_config.py (2)

3-3: LGTM: Test logging import added.

Import correctly placed to support the test suite's logging migration.


11-11: LGTM: Test logger migrated to standard logging.

Correctly creates a logger using the 'flixopt' namespace, enabling the test suite to verify the new standard logging implementation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
flixopt/linear_converters.py (1)

1128-1147: Fix logging format in check_bounds (currently raises TypeError)

logger.warning here still uses Loguru-style {} placeholders with positional args. With stdlib logging, record.getMessage() does msg % args, so this will raise a TypeError whenever a bound is violated, instead of just logging a warning.

Update the messages to use %s placeholders (or an f-string with no extra args). For example:

@@
-    if not np.all(value_arr > lower_bound):
-        logger.warning(
-            "'{}.{}' <= lower bound {}. {}.min={}, shape={}",
-            element_label,
-            parameter_label,
-            lower_bound,
-            parameter_label,
-            float(np.min(value_arr)),
-            np.shape(value_arr),
-        )
-    if not np.all(value_arr < upper_bound):
-        logger.warning(
-            "'{}.{}' >= upper bound {}. {}.max={}, shape={}",
-            element_label,
-            parameter_label,
-            upper_bound,
-            parameter_label,
-            float(np.max(value_arr)),
-            np.shape(value_arr),
-        )
+    if not np.all(value_arr > lower_bound):
+        logger.warning(
+            "'%s.%s' <= lower bound %s. %s.min=%s, shape=%s",
+            element_label,
+            parameter_label,
+            lower_bound,
+            parameter_label,
+            float(np.min(value_arr)),
+            np.shape(value_arr),
+        )
+    if not np.all(value_arr < upper_bound):
+        logger.warning(
+            "'%s.%s' >= lower bound %s. %s.max=%s, shape=%s",
+            element_label,
+            parameter_label,
+            upper_bound,
+            parameter_label,
+            float(np.max(value_arr)),
+            np.shape(value_arr),
+        )

(Adjust wording as desired; the key is to use %-style placeholders that match stdlib logging.)

♻️ Duplicate comments (2)
flixopt/config.py (2)

11-17: Move escape_codes import to module scope to fix Ruff I001 and avoid per-call imports

Ruff is correctly flagging the in-function from colorlog.escape_codes import escape_codes import in ColoredMultilineFormatter.format. Pulling it into the existing module-level try block alongside import colorlog both satisfies the linter and avoids repeated imports on every log call.

Suggested change:

-try:
-    import colorlog
-
-    COLORLOG_AVAILABLE = True
-except ImportError:
-    COLORLOG_AVAILABLE = False
+try:
+    import colorlog
+    from colorlog.escape_codes import escape_codes
+
+    COLORLOG_AVAILABLE = True
+except ImportError:
+    COLORLOG_AVAILABLE = False
+    escape_codes = None
@@
-if COLORLOG_AVAILABLE:
-
-    class ColoredMultilineFormatter(colorlog.ColoredFormatter):
+if COLORLOG_AVAILABLE:
+
+    class ColoredMultilineFormatter(colorlog.ColoredFormatter):
@@
-        def format(self, record):
-            """Format multi-line messages with colors and box-style borders."""
-            # Split into lines
-            lines = record.getMessage().split('\n')
-
-            # Format time with date and milliseconds (YYYY-MM-DD HH:MM:SS.mmm)
-            import datetime
-            from colorlog.escape_codes import escape_codes
+        def format(self, record):
+            """Format multi-line messages with colors and box-style borders."""
+            # Split into lines
+            lines = record.getMessage().split('\n')
+
+            # Format time with date and milliseconds (YYYY-MM-DD HH:MM:SS.mmm)
+            import datetime

After this, the formatter continues to use the module-level escape_codes symbol when COLORLOG_AVAILABLE is True.

Also applies to: 85-88


38-72: Restore exception and stack traces in custom formatters

Both MultilineFormatter.format and ColoredMultilineFormatter.format override logging.Formatter.format but only use record.getMessage(). Any exc_info or stack_info passed to the logger is silently dropped, so logger.exception(..., exc_info=True) no longer shows tracebacks, which is a significant regression for debugging.

You can keep the box-style layout and still include exception/stack information by extending the lines list before building the box:

 class MultilineFormatter(logging.Formatter):
@@
     def format(self, record):
         """Format multi-line messages with box-style borders for better readability."""
-        # Split into lines
-        lines = record.getMessage().split('\n')
+        # Split into lines
+        lines = record.getMessage().split('\n')
+        # Append exception and stack information if present
+        if record.exc_info:
+            lines.extend(self.formatException(record.exc_info).split('\n'))
+        if record.stack_info:
+            lines.extend(record.stack_info.rstrip().split('\n'))
@@
 if COLORLOG_AVAILABLE:
@@
     class ColoredMultilineFormatter(colorlog.ColoredFormatter):
@@
         def format(self, record):
             """Format multi-line messages with colors and box-style borders."""
-            # Split into lines
-            lines = record.getMessage().split('\n')
+            # Split into lines
+            lines = record.getMessage().split('\n')
+            # Append exception and stack information if present
+            if record.exc_info:
+                lines.extend(self.formatException(record.exc_info).split('\n'))
+            if record.stack_info:
+                lines.extend(record.stack_info.rstrip().split('\n'))

This restores full exception/stack visibility while preserving your multi-line box formatting.

Also applies to: 77-116

🧹 Nitpick comments (1)
flixopt/results.py (1)

341-360: Simplify logger level restoration in flow_system

The temporary suppression via flixopt_logger.setLevel(logging.CRITICAL + 1) is fine, but you reset the level both in the except block (Line 353) and again in finally (Line 359). The except reset is redundant because finally always runs. You can safely drop the reset in the except block to reduce duplication.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b1a738 and 4a9b176.

📒 Files selected for processing (11)
  • flixopt/__init__.py (2 hunks)
  • flixopt/components.py (1 hunks)
  • flixopt/config.py (7 hunks)
  • flixopt/core.py (1 hunks)
  • flixopt/elements.py (2 hunks)
  • flixopt/interface.py (1 hunks)
  • flixopt/linear_converters.py (1 hunks)
  • flixopt/results.py (3 hunks)
  • flixopt/structure.py (3 hunks)
  • test_deprecations.py (1 hunks)
  • tests/test_config.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • flixopt/core.py
  • flixopt/components.py
🧰 Additional context used
🧬 Code graph analysis (6)
flixopt/linear_converters.py (2)
flixopt/components.py (1)
  • LinearConverter (32-247)
flixopt/core.py (1)
  • TimeSeriesData (35-163)
flixopt/interface.py (4)
flixopt/config.py (1)
  • CONFIG (152-702)
flixopt/structure.py (1)
  • flow_system (329-347)
flixopt/results.py (1)
  • flow_system (341-360)
flixopt/flow_system.py (1)
  • FlowSystem (40-1618)
flixopt/structure.py (1)
flixopt/core.py (2)
  • TimeSeriesData (35-163)
  • get_dataarray_stats (576-600)
tests/test_config.py (1)
flixopt/config.py (19)
  • CONFIG (152-702)
  • MultilineFormatter (38-72)
  • reset (517-543)
  • Modeling (447-458)
  • Solving (460-481)
  • Logging (180-445)
  • enable_console (255-325)
  • enable_file (328-374)
  • disable (377-387)
  • format (48-72)
  • format (80-116)
  • exploring (614-632)
  • debug (596-611)
  • notebook (659-677)
  • Plotting (483-512)
  • production (635-656)
  • silent (576-593)
  • to_dict (546-573)
  • change_logging_level (705-726)
flixopt/elements.py (1)
flixopt/config.py (1)
  • CONFIG (152-702)
flixopt/results.py (2)
flixopt/flow_system.py (3)
  • FlowSystem (40-1618)
  • from_dataset (556-612)
  • _connect_network (986-1021)
flixopt/structure.py (1)
  • from_dataset (821-854)
🪛 GitHub Check: lint
flixopt/config.py

[failure] 86-87: Ruff (I001)
flixopt/config.py:86:13: I001 Import block is un-sorted or un-formatted

🔇 Additional comments (10)
flixopt/__init__.py (1)

64-67: Central logger default setup is appropriate

Initializing a flixopt logger at WARNING with a NullHandler keeps the library silent by default while allowing CONFIG.Logging to attach real handlers later. This matches the behavior exercised in tests/test_config.py.

test_deprecations.py (1)

8-8: Importing DEPRECATION_REMOVAL_VERSION from flixopt.config is correct

Updating the tests to source DEPRECATION_REMOVAL_VERSION from flixopt.config matches the new central definition and keeps the deprecation message checks aligned with the actual library constant.

flixopt/elements.py (1)

7-16: Elements module logging/config integration looks consistent

Switching to logging.getLogger('flixopt') and importing DEPRECATION_REMOVAL_VERSION from flixopt.config aligns this module with the new centralized logging and deprecation configuration design used elsewhere in the package.

Also applies to: 45-45

tests/test_config.py (1)

3-168: Tests comprehensively exercise the new config/logging API

These tests validate default silence, console/file handlers, the custom SUCCESS level, multiline formatting, all key presets, reset(), to_dict(), and the deprecation path for change_logging_level, with proper isolation via CONFIG.reset(). They are well-aligned with the new implementation in flixopt.config.

flixopt/interface.py (3)

8-8: LGTM: Standard logging import added.

The migration from Loguru to Python's standard logging library is appropriate and aligns with the PR's security objectives.


16-17: LGTM: Import consolidation improves readability.

Grouping imports from the same module into a single statement is a best practice. All imported symbols (CONFIG, DEPRECATION_REMOVAL_VERSION, Interface, register_class_for_io) are used in the file.


25-25: Logger initialization is correctly implemented and migration from loguru is verified complete.

The logging.getLogger('flixopt') at line 25 follows best practices. Verification confirms:

  • loguru has been completely removed from dependencies and codebase
  • colorlog is properly integrated as the replacement (pyproject.toml, config.py)
  • No loguru references remain anywhere
  • Standard logging is consistently used across all 17+ modules
flixopt/structure.py (3)

9-9: LGTM: Standard logging import added.

Consistent with the project-wide migration from Loguru to Python's standard logging library.


27-28: LGTM: Import updates reflect configuration refactoring.

The imports correctly reflect that DEPRECATION_REMOVAL_VERSION has been moved from flixopt.core to flixopt.config, and the consolidation of imports from .core improves readability.


38-38: LGTM: Logger initialization is correct and consistent.

The module-level logger using the shared name 'flixopt' enables centralized logging configuration and is consistent with the project-wide logging migration pattern.

    - Changed from "no handlers" to "NullHandler" which is more accurate
    - NullHandler is technically a handler, just one that suppresses all output
  2. Renamed variable (line 65):
    - Changed from _logger to logger
    - Since the variable is only used for initialization and logging.getLogger() returns the same instance everywhere, using logger is cleaner and
   more conventional
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
examples/05_Two-stage-optimization/two_stage_optimization.py (1)

10-20: Example logger initialization is now Ruff‑compliant and consistent

logger = logging.getLogger('flixopt') is placed after the import block and reuses the shared application logger, resolving the earlier E402 and matching the new logging config.

flixopt/config.py (2)

11-17: Move escape_codes import to module scope and avoid function‑level import

ColoredMultilineFormatter.format still imports escape_codes inside the method, which will trip Ruff (I001) and repeatedly re‑import on each log call. Pull this import into the existing module‑level try block and reuse the name here.

Suggested change:

-try:
-    import colorlog
-
-    COLORLOG_AVAILABLE = True
-except ImportError:
-    COLORLOG_AVAILABLE = False
+try:
+    import colorlog
+    from colorlog.escape_codes import escape_codes
+
+    COLORLOG_AVAILABLE = True
+except ImportError:
+    COLORLOG_AVAILABLE = False
+    escape_codes = None
@@ if COLORLOG_AVAILABLE:
-        def format(self, record):
+        def format(self, record):
@@
-            # Format time with date and milliseconds (YYYY-MM-DD HH:MM:SS.mmm)
-            import datetime
-
-            from colorlog.escape_codes import escape_codes
+            # Format time with date and milliseconds (YYYY-MM-DD HH:MM:SS.mmm)
+            import datetime
+            # `escape_codes` is imported at module level when colorlog is available

This keeps lint happy and avoids repeated in‑method imports while preserving behavior.

Also applies to: 76-118


38-73: Custom formatters currently drop exceptions and stack traces

Both MultilineFormatter.format and ColoredMultilineFormatter.format ignore record.exc_info and record.stack_info, so logger.exception(..., exc_info=True) produces messages with no traceback, which is a serious diagnostics regression.

You can preserve the box layout while restoring this information by folding the formatted exception/stack into lines before applying your multi‑line formatting, e.g.:

-        # Split into lines
-        lines = record.getMessage().split('\n')
+        # Split into lines, including exception/stack info when present
+        lines = record.getMessage().split('\n')
+        if record.exc_info:
+            lines.extend(self.formatException(record.exc_info).split('\n'))
+        if record.stack_info:
+            lines.extend(record.stack_info.rstrip().split('\n'))
@@
-        # Single line - return standard format
+        # Single line - return standard format
         if len(lines) == 1:
@@
-        # Multi-line - use box format
+        # Multi-line - use box format

and the same pattern inside ColoredMultilineFormatter.format. This restores parity with the standard logging.Formatter while keeping the new box/colored layout.

Also applies to: 76-118

🧹 Nitpick comments (1)
tests/test_config.py (1)

81-100: Formatter and stream‑selection tests are useful; consider adding exception coverage later

The multiline, stderr, and non‑colored output tests validate the most important behaviors; once exception/stack formatting is added to the formatters, an extra assertion covering logger.exception(..., exc_info=True) output would close the gap.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a9b176 and c73b8a8.

📒 Files selected for processing (16)
  • examples/05_Two-stage-optimization/two_stage_optimization.py (1 hunks)
  • flixopt/__init__.py (2 hunks)
  • flixopt/aggregation.py (3 hunks)
  • flixopt/calculation.py (3 hunks)
  • flixopt/color_processing.py (1 hunks)
  • flixopt/components.py (1 hunks)
  • flixopt/config.py (7 hunks)
  • flixopt/core.py (1 hunks)
  • flixopt/effects.py (2 hunks)
  • flixopt/interface.py (1 hunks)
  • flixopt/io.py (2 hunks)
  • flixopt/modeling.py (1 hunks)
  • flixopt/network_app.py (2 hunks)
  • flixopt/results.py (3 hunks)
  • flixopt/solvers.py (1 hunks)
  • tests/test_config.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • flixopt/io.py
  • flixopt/solvers.py
  • flixopt/results.py
  • flixopt/init.py
  • flixopt/components.py
  • flixopt/color_processing.py
  • flixopt/aggregation.py
  • flixopt/network_app.py
  • flixopt/core.py
  • flixopt/calculation.py
🧰 Additional context used
🧬 Code graph analysis (3)
flixopt/modeling.py (2)
flixopt/config.py (1)
  • CONFIG (154-711)
flixopt/structure.py (1)
  • Submodel (1362-1541)
tests/test_config.py (1)
flixopt/config.py (19)
  • CONFIG (154-711)
  • MultilineFormatter (38-73)
  • reset (526-552)
  • Modeling (456-467)
  • Solving (469-490)
  • Logging (182-454)
  • enable_console (257-328)
  • enable_file (331-382)
  • disable (385-395)
  • format (48-73)
  • format (81-118)
  • exploring (623-641)
  • debug (605-620)
  • notebook (668-686)
  • Plotting (492-521)
  • production (644-665)
  • silent (585-602)
  • to_dict (555-582)
  • change_logging_level (714-735)
flixopt/interface.py (3)
flixopt/structure.py (3)
  • Interface (279-956)
  • register_class_for_io (43-52)
  • flow_system (329-347)
flixopt/results.py (1)
  • flow_system (342-361)
flixopt/flow_system.py (1)
  • FlowSystem (40-1618)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (3.10)
🔇 Additional comments (9)
flixopt/effects.py (1)

10-10: Stdlib logger wiring for flixopt looks correct and consistent

Using logging.getLogger('flixopt') at module level and the existing info log in EffectCollection.add_effects() integrates cleanly with the new centralized logging setup; no issues spotted here.

Please just confirm that the shared 'flixopt' logger is configured once in your logging config (as suggested in the PR description) and that tests exercising EffectCollection.add_effects() still behave as expected (log level, no unexpected output).

Also applies to: 29-29

flixopt/modeling.py (1)

1-10: Stdlib logger wiring in modeling module looks correct

Using logging.getLogger('flixopt') at module scope aligns with the new central logging configuration and is safe; no further changes needed here.

flixopt/interface.py (1)

8-18: Interface module now correctly targets the shared ‘flixopt’ logger

Switching to logging.getLogger('flixopt') and consolidating imports is consistent with the new logging API and doesn’t affect the public interface.

Also applies to: 25-26

tests/test_config.py (5)

3-11: Test module correctly targets the configured ‘flixopt’ logger

Importing CONFIG/MultilineFormatter and then getting logging.getLogger('flixopt') ensures all tests exercise the same logger configuration as the library.


17-24: Good coverage of default silence and basic console/file behaviors

Setup/teardown via CONFIG.reset() plus tests for “silent by default”, console logging, file logging, console+file, disable, and the custom SUCCESS level match the intended semantics of CONFIG.Logging and should catch regressions in handler/level handling.

Also applies to: 33-80


101-136: Preset tests accurately encode the contract for exploring/debug/notebook/production/silent

These tests pin down how each preset affects logging, plotting, and solver flags, which is valuable given the richer CONFIG surface; they look consistent with the implementations in flixopt.config.


137-160: Reset, to_dict, and attribute modification tests align with CONFIG design

Verifying that reset() restores defaults and clears handlers, and that to_dict/attribute mutations behave as expected, matches the documented behavior of CONFIG and strengthens confidence in config changes.


161-166: Deprecation test for change_logging_level is appropriate

Asserting a DeprecationWarning with the documented message ensures this legacy helper won’t silently linger past its deprecation window.

flixopt/config.py (1)

525-553: CONFIG.reset and presets are wired coherently with the logging helpers

reset() restoring defaults and calling Logging.disable(), and the presets (silent, debug, exploring, production, notebook) composing the Logging helpers with Solving/Plotting flags, form a clear and consistent configuration story that your tests in tests/test_config.py already validate.

Also applies to: 585-603, 605-621, 623-642, 643-666, 668-686

…ng works correctly:

  - test_exception_logging - Tests colored output includes exception tracebacks
  - test_exception_logging_non_colored - Tests non-colored output includes exception tracebacks

  Both tests verify that when using logger.exception(), the output contains:
  - The error message
  - The exception type (ValueError)
  - The exception details
  - The full traceback
@FBumann FBumann merged commit f4fa055 into main Nov 21, 2025
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants