Skip to content

feat!: Logging refactor & add LoggerPluginInterface#3107

Merged
johanneskoester merged 99 commits intosnakemake:mainfrom
cademirch:logging-refactor
Mar 12, 2025
Merged

feat!: Logging refactor & add LoggerPluginInterface#3107
johanneskoester merged 99 commits intosnakemake:mainfrom
cademirch:logging-refactor

Conversation

@cademirch
Copy link
Copy Markdown
Contributor

@cademirch cademirch commented Sep 29, 2024

Followup to #2830.

This PR attempts to make Snakemake's logging more modular to make room for a potential logging plugin interface, or other logging/monitoring additions, as well as make logging in Snakemake more standardized.

Currently, most of logging logic is deep within logging.Logger, mainly in text_handler() with direct calls to logger.info/warn/etc. In this PR, this logic has been refactored to more comply with the standard logging practices. To do so, three new classes were added:

  1. ColorizingTextHandler manages adding color to log records and emitting to stream.
  2. DefaultFormatter formats log records to be printed. Much of the logic from text_handler() is here now.
  3. DefaultFilter filters log records from being emitted based on level and quietness settings.

Together, these classes should output identical logs to the current logging implementation. I've tested this briefly, but further testing would be good.

With these changes, it is much easier to add new logHandlers via logger.addHandler. New handlers can decide how to format and filter logs themselves, an important characteristic missing in the current implementation that motivated this refactoring (#2830 (comment)).

This would be a breaking change for --log-handler-script, as user provided log handlers will need to subclass logging.logHandler. Furthermore, logic for adding the user handler (and formatter/filter) will need to be added.

As for the logging plug interface (#2830 (comment)), I'm not sure that is the route to take, but would appreciate thoughts on this. First, this functionality is already largely provided by --log-handler-script and adding a logging plugin would be redundant. Second, I think that development of such plugins would likely be minimal, and coordinated effort in this repo to maintain a good set of logging options would be preferable. For example:

  • A standard HTTP log handler send emit log records to a user supplied address on which a 3rd party monitoring app is listening.
  • A handler/formatter that uses rich to make a prettier/interactive console on which logs are written to.

I think both of these additions would benefit being standardized to Snakemake, as well as default options to users.

Additionally I think it would be worthwhile to unify logging in Snakemake. Currently, the use of the wrapper logging.Logger class and setup_logger() function being called in multiple places makes it difficult to work with the logger and is likely the source of some issues (#3044, #2974, #2882, #2797, #2727). Unifying the logger and its configuration as recommended by the logging docs would make it easier to work the the logger. Another option would be to use something like loguru. Whichever option, I think simplifying and unifying the logger should be the next step of this PR.

One main question I have is how much we would like to preserve the current stderr output formatting. Most of logging logic is in formatting rule info and such. Moving forward we can now try different formatting. Personally, I find the current detailed rule output helpful for sanity checks when developing and doing a dry run. However, when actually running the workflow (especially a large one) the terminal is flooded and errors can quickly be lost which requires searching the log file. Of course this can be alleviated with --quiet.

Another question is regarding the two calls of setup_logger() brought up in #2797. I still don't understand why these are necessary and understanding this would be helpful in unifying this.

I'm happy to keep pushing this forward but would appreciate your thoughts before continuing @johanneskoester! I

Summary by CodeRabbit

  • New Features

    • Introduced a new CLI option (--logger) for configuring custom loggers.
    • Enhanced structured logging delivers clearer runtime and resource insights, including rule graph logging.
    • Added a new method for logging rule graph information.
    • Added a new enumeration value REASON to the Quietness class.
  • Refactor

    • Overhauled the logging architecture across execution, scheduling, and workflow components for improved traceability and error handling.
  • Chores

    • Updated dependencies and setup configurations to include new logger plugins.
    • Adjusted test configurations to capture all output, ensuring more comprehensive test diagnostics.
  • Bug Fixes

    • Resolved minor command and formatting inconsistencies for smoother operations.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 29, 2024

📝 Walkthrough

Walkthrough

This pull request makes extensive modifications across multiple modules. It updates the executor’s command-line options and fixes indentation issues. The logging system has been overhauled by introducing a new LoggerManager along with several utility functions and structured logging improvements throughout the API, workflow, jobs, scheduler, and shell components. Additionally, the changes streamline the CLI logging configuration and update dependency declarations and settings for enhanced logging granularity.

Changes

File(s) Change Summary
snakemake/executors/local.py Updated additional_general_args to return "--quiet progress rules host" and corrected indentation in the get_python_executable method.
snakemake/logging.py, snakemake/api.py, snakemake/dag.py, snakemake/jobs.py, snakemake/scheduler.py, snakemake/workflow.py, snakemake/shell.py, snakemake/cli.py, snakemake/script/__init__.py Overhauled logging framework: introduced a new LoggerManager with multiple handler configuration methods, refactored logging calls across modules (e.g., replacing custom methods with structured logging via logger.info/logger.error and using extra parameters), updated error handling and callbacks, and streamlined CLI logging arguments (replacing --log-service with --logger).
snakemake/settings/types.py, snakemake/settings/enums.py Modified OutputSettings to implement OutputSettingsLoggerInterface, replacing a sequence of log handlers with a mapping (log_handler_settings) and adding a new dryrun flag; added a new enumeration value REASON = 4 to the Quietness enum.
test-environment.yml, setup.cfg, apidocs/requirements.txt, docs/requirements.txt Added new dependencies for logger plugins: introduced snakemake-interface-logger-plugins (and in some cases snakemake-logger-plugin-rich or related dependencies) and re-added snakemake-interface-storage-plugins where applicable.
.github/workflows/main.yml Updated the pytest commands in both testing and testing-windows jobs to capture all output (--show-capture=all), and made minor formatting changes in the exclusion lists.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant CLI as Command Line Interface
    participant API as Snakemake API
    participant LM as LoggerManager
    participant LH as Log Handler Plugins

    U->>CLI: Run snakemake with --logger option
    CLI->>API: Parse CLI arguments and initialize logging configuration
    API->>LM: Call setup to configure log handlers and initialize logfile
    LM->>LH: Configure default & plugin log handlers
    API->>LM: Log workflow start (including workflow ID, host, and resources)
    API->>LM: Execute workflow actions
    LM->>API: Provide logging context via structured logs
    API->>LM: On completion, call cleanup_logfile() and stop logging services
Loading

Suggested reviewers

  • johanneskoester

📜 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 71f3882 and 07da8cc.

📒 Files selected for processing (1)
  • snakemake/api.py (7 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.py`: Do not try to improve formatting. Do not suggest ...

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

  • snakemake/api.py
⏰ Context from checks skipped due to timeout of 90000ms (29)
  • GitHub Check: testing (10, 3.12, bash)
  • GitHub Check: testing (10, 3.11, bash)
  • GitHub Check: testing (9, 3.12, bash)
  • GitHub Check: testing (9, 3.11, bash)
  • GitHub Check: testing (8, 3.12, bash)
  • GitHub Check: testing (8, 3.11, bash)
  • GitHub Check: testing (7, 3.12, bash)
  • GitHub Check: testing (7, 3.11, bash)
  • GitHub Check: testing (6, 3.12, bash)
  • GitHub Check: testing (6, 3.11, bash)
  • GitHub Check: testing (5, 3.12, bash)
  • GitHub Check: testing-windows (9)
  • GitHub Check: testing (5, 3.11, bash)
  • GitHub Check: testing-windows (8)
  • GitHub Check: testing (4, 3.12, bash)
  • GitHub Check: testing-windows (7)
  • GitHub Check: testing (4, 3.11, bash)
  • GitHub Check: testing (3, 3.12, bash)
  • GitHub Check: testing-windows (6)
  • GitHub Check: testing-windows (5)
  • GitHub Check: testing (3, 3.11, bash)
  • GitHub Check: testing-windows (4)
  • GitHub Check: testing (2, 3.12, bash)
  • GitHub Check: testing (2, 3.11, bash)
  • GitHub Check: testing-windows (3)
  • GitHub Check: testing (1, 3.12, bash)
  • GitHub Check: testing-windows (2)
  • GitHub Check: testing (1, 3.11, bash)
  • GitHub Check: testing-windows (1)
🔇 Additional comments (9)
snakemake/api.py (9)

15-15: New UUID import supports workflow identification.

Adding uuid support enables generation of unique identifiers for workflow executions, which is useful for tracking and logging individual workflow runs.


48-49: Good addition of logger plugin interfaces.

These imports support the new modular logging framework with plugin interfaces, which aligns with the PR objectives of creating a more flexible logging system.


53-53: Appropriate update to include logger_manager.

Including logger_manager alongside the existing logger import supports the transition to centralized logging management.


162-163: Properly updated cleanup method for new logging system.

The change from logger.cleanup() to logger_manager.cleanup_logfile() and logger_manager.stop() correctly implements the new centralized logging management approach.


249-261: Well-structured logger setup with plugin support.

The refactored setup_logger method now properly:

  1. Validates logger initialization state
  2. Creates log handlers from plugins based on settings
  3. Passes handlers to the centralized logger_manager

This implementation aligns well with the modular logging framework objective.


293-293: Now using setup_logger in _no_dag decorator.

The _no_dag decorator properly sets up the logger before execution.


494-498: Enhanced setup_logger call with execution context.

The updated call passes important execution context (stdout, mode, dryrun status) to the logger setup, which improves logging configuration.


591-591: Updated to use logger_manager for logfile setup.

Correctly replaced logger.setup_logfile() with logger_manager.setup_logfile() to align with the centralized logging management approach.


598-605: Well-implemented structured logging for workflow start.

This code now uses structured logging with the extra parameter to log the workflow start event, including:

  1. A unique workflow ID generated with UUID
  2. The snakefile path
  3. Proper event categorization (LogEvent.WORKFLOW_STARTED)

This approach facilitates better log filtering and analysis.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
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: 2

🧹 Outside diff range and nitpick comments (1)
snakemake/logging.py (1)

258-258: Normalize log level to lowercase in DefaultFormatter.format.

In the format method, level is not converted to lowercase before comparisons. This could lead to mismatches if the log level in msg is in uppercase. It's better to normalize level to lowercase for consistent handling.

Apply this change:

-def format(self, record):
     """
     Override format method to handle Snakemake-specific log messages.
     """
     msg = record.msg
-    level = msg.get("level", "INFO")
+    level = msg.get("level", "INFO").lower()
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 19a2ab7 and eb047a0.

📒 Files selected for processing (2)
  • snakemake/executors/local.py (1 hunks)
  • snakemake/logging.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
snakemake/executors/local.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

snakemake/logging.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

🔇 Additional comments (3)
snakemake/executors/local.py (1)

85-85: LGTM. Verify impact on codebase.

The addition of "host" to the return value of additional_general_args aligns with the PR objectives for refactoring the logging system. This change may affect how the Snakemake executor interacts with the host environment during execution.

To ensure this change doesn't introduce unintended side effects, please run the following script to check for any other occurrences of the original argument string and verify that they are updated accordingly:

#!/bin/bash
# Description: Check for occurrences of the original argument string and verify updates.

# Search for the old argument string
echo "Searching for occurrences of the old argument string:"
rg --type python '"--quiet progress rules"'

# Search for the new argument string
echo "Searching for occurrences of the new argument string:"
rg --type python '"--quiet progress rules host"'

# Search for uses of the additional_general_args method
echo "Searching for uses of the additional_general_args method:"
rg --type python 'additional_general_args'
snakemake/logging.py (2)

586-635: DefaultFilter class implementation is correct.

The DefaultFilter class effectively filters log records based on quietness settings and the debug_dag flag. The logic appears sound and aligns with the intended functionality.


960-964: ⚠️ Potential issue

Fix parameter name when instantiating DefaultFormatter.

In the setup_logger function, the keyword argument should match the corrected parameter name in DefaultFormatter. The argument printshellmds should be printshellcmds to ensure proper initialization.

Apply this diff:

     formatter = DefaultFormatter(
         printreason=printreason,
         show_failed_logs=show_failed_logs,
-        printshellmds=printshellcmds,
+        printshellcmds=printshellcmds,
     )

Likely invalid or redundant comment.

Comment on lines +806 to +831
log_level = {
"debug": _logging.DEBUG,
"info": _logging.WARNING,
"warning": _logging.WARNING,
"error": _logging.ERROR,
"critical": _logging.CRITICAL,
"run_info": _logging.WARNING,
"job_info": _logging.INFO,
"group_info": _logging.INFO,
"job_error": _logging.ERROR,
"group_error": _logging.ERROR,
"progress": _logging.INFO,
"job_finished": _logging.INFO,
"shellcmd": _logging.INFO,
"rule_info": _logging.INFO,
"dag_debug": (
_logging.WARNING if self.debug_dag else _logging.DEBUG
), # bump debug_dag to warning if we want it
"resources_info": _logging.WARNING,
"host": _logging.INFO,
"job_stats": _logging.WARNING,
}.get(
custom_level, _logging.INFO
) # Default to INFO if not recognized

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 29, 2024

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct the log level mapping for 'info' in Logger.handler.

The custom log level 'info' is mistakenly mapped to _logging.WARNING. This may cause 'info' messages to be logged at the wrong level. It should be mapped to _logging.INFO to reflect the appropriate logging severity.

Apply this diff to fix the mapping:

         # Map custom levels to standard Python logging levels
         log_level = {
             "debug": _logging.DEBUG,
-            "info": _logging.WARNING,
+            "info": _logging.INFO,
             "warning": _logging.WARNING,
             "error": _logging.ERROR,
             "critical": _logging.CRITICAL,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
log_level = {
"debug": _logging.DEBUG,
"info": _logging.WARNING,
"warning": _logging.WARNING,
"error": _logging.ERROR,
"critical": _logging.CRITICAL,
"run_info": _logging.WARNING,
"job_info": _logging.INFO,
"group_info": _logging.INFO,
"job_error": _logging.ERROR,
"group_error": _logging.ERROR,
"progress": _logging.INFO,
"job_finished": _logging.INFO,
"shellcmd": _logging.INFO,
"rule_info": _logging.INFO,
"dag_debug": (
_logging.WARNING if self.debug_dag else _logging.DEBUG
), # bump debug_dag to warning if we want it
"resources_info": _logging.WARNING,
"host": _logging.INFO,
"job_stats": _logging.WARNING,
}.get(
custom_level, _logging.INFO
) # Default to INFO if not recognized
log_level = {
"debug": _logging.DEBUG,
"info": _logging.INFO,
"warning": _logging.WARNING,
"error": _logging.ERROR,
"critical": _logging.CRITICAL,
"run_info": _logging.WARNING,
"job_info": _logging.INFO,
"group_info": _logging.INFO,
"job_error": _logging.ERROR,
"group_error": _logging.ERROR,
"progress": _logging.INFO,
"job_finished": _logging.INFO,
"shellcmd": _logging.INFO,
"rule_info": _logging.INFO,
"dag_debug": (
_logging.WARNING if self.debug_dag else _logging.DEBUG
), # bump debug_dag to warning if we want it
"resources_info": _logging.WARNING,
"host": _logging.INFO,
"job_stats": _logging.WARNING,
}.get(
custom_level, _logging.INFO
) # Default to INFO if not recognized

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was intentional to preserve the existing log output coloring:

if level == "info" and not self.is_quiet_about("progress"):
self.logger.warning(msg["msg"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@cademirch, understood. Thanks for the clarification that the mapping of 'info' to _logging.WARNING is intentional to preserve the existing log output coloring.

(⁀ᗢ⁀)


✏️ Learnings added
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2024-09-30T17:17:37.227Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I agree with the coderabbit bot - Snakemake should not warn on infos, as already pointed out in #2727 - upgrading all infos to warnings makes it impossible to then later filter out the infos without losing all actual warnings. If info level messages should be coloured differently, that ought to be fixed elsewhere, not by conflating it with warnings.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(Belatedly realized this is on outdated changes, so probably the whole conversation can just get resolved. Sorry for the noise!)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree, however this mapping is now gone and we're just using direct calls to info, warning etc. Happy to accept changes to specific logging calls that you think should go to different levels

Copy link
Copy Markdown
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: 1

🧹 Outside diff range and nitpick comments (7)
snakemake/logging.py (7)

246-509: LGTM! DefaultFormatter class is well-structured.

The DefaultFormatter class is well-organized and follows the single responsibility principle. It provides specific formatting for various log levels, which enhances readability and maintainability.

However, there are a few minor optimizations that could be made:

  1. In the handle_info method, consider using a list comprehension for better performance:
output = [f"{indent}{line}" for line in msg["msg"].split("\n")]
return "\n".join(output)
  1. In the format_percentage method, consider using f-strings for better readability:
fmt_precision = lambda p: f"{{:.{p}%}}"

These are minor suggestions and don't affect the overall functionality of the class.


587-635: LGTM! DefaultFilter class is well-implemented.

The DefaultFilter class is concise and effectively filters log messages based on quietness settings and the debug_dag flag. It makes good use of Snakemake's Quietness enum for filtering.

A minor optimization could be made in the filter method:

def filter(self, record):
    if self.is_quiet_about("all"):
        return False

    msg = record.msg
    level = msg.get("level", "INFO").lower()

    quietness_map = {
        "job_info": "rules",
        "group_info": "rules",
        "job_error": "rules",
        "group_error": "rules",
        "progress": "progress",
        "shellcmd": "progress",
        "job_finished": "progress",
        "resources_info": "progress",
        "run_info": "progress",
        "host": "host",
        "info": "progress",
    }

    if level in quietness_map and self.is_quiet_about(quietness_map[level]):
        return False

    if level == "dag_debug" and not self.debug_dag:
        return False

    return True

This change combines the check for level in quietness_map and the corresponding quietness check, slightly improving readability and performance.

🧰 Tools
🪛 Ruff

627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


Line range hint 737-920: LGTM! Logger class modifications enhance flexibility, but there's a potential issue with log level mapping.

The modifications to the Logger class improve its flexibility and customization options. The addition of default_formatter and default_filter attributes, along with the updated handler method, allows for more fine-grained control over logging behavior.

However, there's a potential issue in the handler method (lines 801-842):

log_level = {
    "debug": _logging.DEBUG,
    "info": _logging.WARNING,  # This mapping seems incorrect
    "warning": _logging.WARNING,
    "error": _logging.ERROR,
    "critical": _logging.CRITICAL,
    # ... other mappings ...
}.get(custom_level, _logging.INFO)

The "info" level is mapped to _logging.WARNING, which seems incorrect. Typically, "info" should be mapped to _logging.INFO. This mapping might cause "info" level messages to be treated as warnings, potentially leading to confusion or incorrect log filtering.

Consider updating this mapping to:

"info": _logging.INFO,

Unless there's a specific reason for this mapping that I'm not aware of, this change would align with standard logging practices.


Line range hint 923-979: LGTM! setup_logger function aligns well with the new logging structure, but contains a minor typo.

The modifications to the setup_logger function effectively integrate the new DefaultFormatter, DefaultFilter, and ColorizingTextHandler. This alignment with the new logging structure enhances consistency and maintainability.

However, there's a minor typo in the DefaultFormatter initialization (lines 961-965):

formatter = DefaultFormatter(
    printreason=printreason,
    show_failed_logs=show_failed_logs,
    printshellmds=printshellcmds,  # Typo here
)

The parameter printshellmds should be printshellcmds. This typo could lead to unexpected behavior if the printshellcmds setting is not properly applied.

Please correct the typo:

formatter = DefaultFormatter(
    printreason=printreason,
    show_failed_logs=show_failed_logs,
    printshellcmds=printshellcmds,
)

This correction ensures that the printshellcmds setting is properly passed to the DefaultFormatter.


513-513: Consider replacing lambda expressions with defined functions.

The static analysis tool suggests replacing lambda expressions with defined functions. This can improve readability and maintainability. For example, on lines 513 and 581:

fmt = lambda fraction: fmt_precision(precision).format(fraction)

Could be rewritten as:

def fmt(fraction):
    return fmt_precision(precision).format(fraction)

This change makes the code more explicit and easier to understand.

Also applies to: 581-581

🧰 Tools
🪛 Ruff

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for file operations.

On line 523, consider using a context manager (with statement) when opening files. This ensures that the file is properly closed after use, even if an exception occurs. For example:

with open(f, "r") as file:
    content = file.read()

This practice helps prevent resource leaks and is considered a Python best practice.

🧰 Tools
🪛 Ruff

523-523: Use a context manager for opening files

(SIM115)


537-537: Avoid ambiguous variable names.

On line 537, the variable name 'l' is used, which can be ambiguous (it looks like the number 1). Consider using a more descriptive variable name, such as 'line' or 'log_line'. This improves code readability.

🧰 Tools
🪛 Ruff

537-537: Ambiguous variable name: l

(E741)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between eb047a0 and 00a2713.

📒 Files selected for processing (1)
  • snakemake/logging.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
snakemake/logging.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

🪛 Ruff
snakemake/logging.py

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🔇 Additional comments (1)
snakemake/logging.py (1)

Line range hint 1-979: Overall assessment: Significant improvements to logging functionality with minor issues.

The changes made to snakemake/logging.py greatly enhance the logging capabilities of Snakemake. The introduction of DefaultFormatter, DefaultFilter, and ColorizingTextHandler classes provides better separation of concerns and increased flexibility in log handling.

Key improvements:

  1. More granular control over log formatting and filtering.
  2. Enhanced colorization capabilities for better log readability.
  3. Improved integration with Snakemake's quietness settings.

While the overall structure and implementation are solid, there are a few minor issues to address:

  1. A potential incorrect log level mapping in the Logger class.
  2. A typo in the setup_logger function parameter.
  3. Some minor optimizations and code style improvements suggested by static analysis.

Addressing these minor issues will further improve the robustness and maintainability of the logging system. Great work on this refactoring effort!

🧰 Tools
🪛 Ruff

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

@johanneskoester
Copy link
Copy Markdown
Contributor

As for the logging plug interface (#2830 (comment)), I'm not sure that is the route to take, but would appreciate thoughts on this. First, this functionality is already largely provided by --log-handler-script and adding a logging plugin would be redundant. Second, I think that development of such plugins would likely be minimal, and coordinated effort in this repo to maintain a good set of logging options would be preferable. For example:
A standard HTTP log handler send emit log records to a user supplied address on which a 3rd party monitoring app is listening.
A handler/formatter that uses rich to make a prettier/interactive console on which logs are written to.
I think both of these additions would benefit being standardized to Snakemake, as well as default options to users.

I think it is still fine to develop them as plugins. They can be default dependencies or internal plugins (like with the dryrun, local, and touch executor plugins).

@johanneskoester
Copy link
Copy Markdown
Contributor

One main question I have is how much we would like to preserve the current stderr output formatting. Most of logging logic is in formatting rule info and such. Moving forward we can now try different formatting. Personally, I find the current detailed rule output helpful for sanity checks when developing and doing a dry run. However, when actually running the workflow (especially a large one) the terminal is flooded and errors can quickly be lost which requires searching the log file. Of course this can be alleviated with --quiet.

Sure, the output can be improved in any way. I have no strong feelings about how things are currently logged.

@johanneskoester
Copy link
Copy Markdown
Contributor

Another question is regarding the two calls of setup_logger() brought up in #2797. I still don't understand why these are necessary and understanding this would be helpful in unifying this.

Answered here: #2797 (comment)
Further, one general problem I have with the Snakemake logger is that it uses a global object which kind of has a state. We can keep it like that but one could keep in mind that maybe at some point this should be somehow different and the logger instance should rather be stored in the API call or so. Would be the start of making Snakemake thread safe.

Copy link
Copy Markdown
Contributor

@johanneskoester johanneskoester left a comment

Choose a reason for hiding this comment

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

See my answers on the PR message content above. Here comes the code review. I like the structured approach to splitting up the code a lot!

Comment on lines +262 to +289
if level == "info":
return self.handle_info(msg)
elif level == "host":
return self.handle_host(msg)
elif level == "job_info":
return self.handle_job_info(msg)
elif level == "group_info":
return self.handle_group_info(msg)
elif level == "job_error":
return self.handle_job_error(msg)
elif level == "group_error":
return self.handle_group_error(msg)
elif level == "progress":
return self.handle_progress(msg)
elif level == "job_finished":
return self.handle_job_finished(msg)
elif level == "shellcmd":
return self.handle_shellcmd(msg)
elif level == "rule_info":
return self.handle_rule_info(msg)
elif level == "d3dag":
return self.handle_d3dag(msg)
elif level == "dag_debug":
return self.handle_dag_debug(msg)
elif level == "run_info":
return self.handle_run_info(msg)
else:
return msg["msg"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess this is something for Python's new match statement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea, agreed.

"""Helper method to show logs."""
for f in logs:
try:
content = open(f, "r").read()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I always thought the idiomatic version of this uses a with statement, even if one reads the entire file into memory.

@cademirch
Copy link
Copy Markdown
Contributor Author

Thanks for the review and answering questions @johanneskoester. This helps with the direction to take this. Currently working on consolidating the custom log levels. Will try to have more updates by end of the week.

Re logger state: I agree this is a bit tricky, I will try to keep it in mind.

Copy link
Copy Markdown
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: 6

🧹 Outside diff range and nitpick comments (3)
snakemake/logging.py (3)

537-537: Rename ambiguous variable l to improve readability

The variable l can be confused with 1 or I. Rename it to a more descriptive name to enhance clarity.

Apply this diff:

-    max_len = min(max(max(len(l) for l in lines), len(logfile_header)), 80)
+    max_len = min(max(max(len(line) for line in lines), len(logfile_header)), 80)
🧰 Tools
🪛 Ruff

537-537: Ambiguous variable name: l

(E741)


678-678: Use != operator instead of not ... ==

Using != is more readable and idiomatic in Python.

Apply this diff:

-            return self.is_tty and not platform.system() == "Windows"
+            return self.is_tty and platform.system() != "Windows"
🧰 Tools
🪛 Ruff

678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Remove unused variable e

The variable e is assigned but never used. Removing it cleans up the code.

Apply this diff:

-                except Exception as e:
+                except Exception:
🧰 Tools
🪛 Ruff

713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 00a2713 and 5b4aa42.

📒 Files selected for processing (2)
  • snakemake/executors/local.py (1 hunks)
  • snakemake/logging.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
snakemake/executors/local.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

snakemake/logging.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

📓 Learnings (1)
snakemake/logging.py (1)
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2024-09-30T17:17:37.360Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
🪛 Ruff
snakemake/logging.py

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🔇 Additional comments (1)
snakemake/executors/local.py (1)

85-85: LGTM. Verify impact on logging system.

The addition of "host" to the general arguments aligns with the PR objectives for refactoring the logging system. This change suggests that host-related information will now be included in the logging output.

To ensure this change doesn't introduce any unexpected behavior, please run the following verification:

Comment on lines +632 to +635
if level == "dag_debug" and not self.debug_dag:
return False

return True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify return statement

You can simplify the logic by returning the boolean expression directly.

Apply this diff:

-    if level == "dag_debug" and not self.debug_dag:
-        return False
-    return True
+    return not (level == "dag_debug" and not self.debug_dag)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if level == "dag_debug" and not self.debug_dag:
return False
return True
return not (level == "dag_debug" and not self.debug_dag)
🧰 Tools
🪛 Ruff

632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)

Comment on lines +627 to +630
if level in quietness_map:
if self.is_quiet_about(quietness_map[level]):
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Combine nested if statements

Simplify the nested if statements by combining their conditions.

Apply this diff:

-    if level in quietness_map:
-        if self.is_quiet_about(quietness_map[level]):
-            return False
+    if level in quietness_map and self.is_quiet_about(quietness_map[level]):
+        return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if level in quietness_map:
if self.is_quiet_about(quietness_map[level]):
return False
if level in quietness_map and self.is_quiet_about(quietness_map[level]):
return False
🧰 Tools
🪛 Ruff

627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)

Comment on lines +523 to +525
content = open(f, "r").read()
except FileNotFoundError:
yield f"Logfile {f} not found."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use a context manager when opening files

Using a context manager with open() ensures that files are properly closed after their use, even if an error occurs.

Apply this diff to use a context manager:

-    content = open(f, "r").read()
+    with open(f, "r") as file_handle:
+        content = file_handle.read()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
content = open(f, "r").read()
except FileNotFoundError:
yield f"Logfile {f} not found."
with open(f, "r") as file_handle:
content = file_handle.read()
except FileNotFoundError:
yield f"Logfile {f} not found."
🧰 Tools
🪛 Ruff

523-523: Use a context manager for opening files

(SIM115)

Comment on lines +513 to +515
fmt = lambda fraction: fmt_precision(precision).format(fraction)
while fmt(fraction) == "100%" or fmt(fraction) == "0%":
precision += 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor lambda expression to a named function

Assigning a lambda expression to a variable can reduce code readability. Consider defining a named function instead.

Apply this diff to refactor the code:

-    fmt = lambda fraction: fmt_precision(precision).format(fraction)
+    def fmt(fraction):
+        return fmt_precision(precision).format(fraction)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fmt = lambda fraction: fmt_precision(precision).format(fraction)
while fmt(fraction) == "100%" or fmt(fraction) == "0%":
precision += 1
def fmt(fraction):
return fmt_precision(precision).format(fraction)
while fmt(fraction) == "100%" or fmt(fraction) == "0%":
precision += 1
🧰 Tools
🪛 Ruff

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)

Comment on lines +581 to +583
fmt = lambda fraction: fmt_precision(precision).format(fraction)
while fmt(fraction) == "100%" or fmt(fraction) == "0%":
precision += 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor lambda expression to a named function

Similar to lines 513-515, consider defining a named function for better readability.

Apply this diff:

-    fmt = lambda fraction: fmt_precision(precision).format(fraction)
+    def fmt(fraction):
+        return fmt_precision(precision).format(fraction)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fmt = lambda fraction: fmt_precision(precision).format(fraction)
while fmt(fraction) == "100%" or fmt(fraction) == "0%":
precision += 1
def fmt(fraction):
return fmt_precision(precision).format(fraction)
while fmt(fraction) == "100%" or fmt(fraction) == "0%":
precision += 1
🧰 Tools
🪛 Ruff

581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)

Comment on lines +549 to +552
if isinstance(dict_like, Namedlist):
items = dict_like.items()
elif isinstance(dict_like, dict):
items = dict_like.items()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify if statements by combining conditions

You can combine the isinstance checks using a tuple to simplify the code.

Apply this diff:

-    if isinstance(dict_like, Namedlist):
-        items = dict_like.items()
-    elif isinstance(dict_like, dict):
+    if isinstance(dict_like, (Namedlist, dict)):
         items = dict_like.items()
     else:
         raise ValueError(
             "bug: format_dict applied to something neither a dict nor a Namedlist"
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if isinstance(dict_like, Namedlist):
items = dict_like.items()
elif isinstance(dict_like, dict):
items = dict_like.items()
if isinstance(dict_like, (Namedlist, dict)):
items = dict_like.items()
else:
raise ValueError(
"bug: format_dict applied to something neither a dict nor a Namedlist"
)
🧰 Tools
🪛 Ruff

549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)

Copy link
Copy Markdown
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: 4

🧹 Outside diff range and nitpick comments (2)
snakemake/logging.py (2)

Line range hint 737-920: Improve Logger class implementation and documentation.

  1. Consider moving the log level mapping dictionary in the handler method to a class attribute for better maintainability.

  2. Add type hints to methods to improve code readability and maintainability.

  3. Enhance the docstring for the logfile_hint method to be more descriptive.

Here's a suggestion for moving the log level mapping to a class attribute:

class Logger:
    LOG_LEVEL_MAPPING = {
        "debug": _logging.DEBUG,
        "info": _logging.WARNING,
        # ... other mappings ...
    }

    def handler(self, msg):
        custom_level = msg.get("level", "info").lower()
        log_level = self.LOG_LEVEL_MAPPING.get(custom_level, _logging.INFO)
        # ... rest of the method ...

Add type hints to methods, for example:

def set_stream_handler(self, stream_handler: _logging.Handler) -> None:
    # ... method implementation ...

def set_level(self, level: int) -> None:
    # ... method implementation ...

def handler(self, msg: dict) -> None:
    # ... method implementation ...

Enhance the logfile_hint method docstring:

def logfile_hint(self):
    """
    Log the location of the logfile if applicable.
    
    This method checks if the current execution mode is DEFAULT and not a dry run.
    If these conditions are met, it logs the relative path of the logfile.
    """
    # ... method implementation ...

These changes will improve the overall code quality and maintainability of the Logger class.


Line range hint 924-978: Improve setup_logger function implementation.

  1. Add type hints to the function parameters and return value to improve code readability and maintainability.

  2. Consider simplifying the quiet parameter handling.

  3. The function is quite long and could potentially be split into smaller, more focused functions.

Here's a suggestion for adding type hints and simplifying the quiet parameter handling:

from typing import List, Optional, Set, Union
from snakemake.settings.types import Quietness
from snakemake_interface_executor_plugins.settings import ExecMode

def setup_logger(
    handler: List[_logging.Handler] = [],
    quiet: Union[bool, Set[Quietness], None] = False,
    printshellcmds: bool = False,
    printreason: bool = True,
    debug_dag: bool = False,
    nocolor: bool = False,
    stdout: bool = False,
    debug: bool = False,
    mode: Optional[ExecMode] = None,
    show_failed_logs: bool = False,
    dryrun: bool = False,
) -> None:
    if mode is None:
        mode = get_default_exec_mode()

    quiet_set = set()
    if isinstance(quiet, bool) and quiet:
        quiet_set = {Quietness.PROGRESS, Quietness.RULES}
    elif isinstance(quiet, set):
        quiet_set = quiet
    elif quiet is not None:
        raise ValueError("Unsupported value provided for quiet mode (either bool, None or set allowed).")

    # ... rest of the function ...

To further improve the function, consider splitting it into smaller, more focused functions:

def create_stream_handler(nocolor: bool, stdout: bool, mode: ExecMode) -> ColorizingTextHandler:
    # ... create and return stream handler ...

def create_formatter(printreason: bool, show_failed_logs: bool, printshellcmds: bool) -> DefaultFormatter:
    # ... create and return formatter ...

def create_filter(quiet: Set[Quietness], debug_dag: bool) -> DefaultFilter:
    # ... create and return filter ...

def setup_logger(...) -> None:
    # ... main setup logic, calling the above functions ...

These changes will improve the overall code quality, readability, and maintainability of the setup_logger function.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 5b4aa42 and 0b76608.

📒 Files selected for processing (1)
  • snakemake/logging.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
snakemake/logging.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

📓 Learnings (1)
snakemake/logging.py (2)
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2024-09-30T17:17:37.360Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2024-10-08T17:41:54.542Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
🪛 Ruff
snakemake/logging.py

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🔇 Additional comments (1)
snakemake/logging.py (1)

Line range hint 1-978: Overall improvements to the logging system with some minor suggestions for enhancement.

The changes made to the snakemake/logging.py file significantly improve the logging system's flexibility and functionality. The introduction of DefaultFormatter, DefaultFilter, and ColorizingTextHandler classes provides better separation of concerns and allows for more customized logging behavior.

Key improvements:

  1. More structured and customizable log formatting.
  2. Better control over log filtering based on quietness settings and debug flags.
  3. Enhanced colorization of log messages with support for different terminal types.
  4. Updated Logger class with new methods and attributes for improved logging capabilities.
  5. Refactored setup_logger function to utilize the new classes and provide more configuration options.

While these changes are generally positive, there are some minor issues and areas for improvement:

  1. Some code duplication (e.g., is_quiet_about method) that could be addressed.
  2. Opportunities for adding type hints to improve code readability and maintainability.
  3. Potential for further refactoring to improve code organization and reduce complexity in some methods.

Addressing these minor issues will further enhance the overall quality and maintainability of the logging system.

🧰 Tools
🪛 Ruff

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

Comment on lines +246 to +252
class DefaultFormatter(_logging.Formatter):

def __init__(self, printreason=False, show_failed_logs=False, printshellcmds=False):
self.printreason = printreason
self.show_failed_logs = show_failed_logs
self.printshellcmds = printshellcmds
self.last_msg_was_job_info = False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo in parameter name and consider adding type hints.

There's a typo in the __init__ method's parameter name. Also, consider adding type hints to improve code readability and maintainability.

Apply this diff to fix the typo and add type hints:

-    def __init__(self, printreason=False, show_failed_logs=False, printshellcmds=False):
+    def __init__(self, printreason: bool = False, show_failed_logs: bool = False, printshellcmds: bool = False):
         self.printreason = printreason
         self.show_failed_logs = show_failed_logs
-        self.printshellcmds = printshellmds
+        self.printshellcmds = printshellcmds
         self.last_msg_was_job_info = False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class DefaultFormatter(_logging.Formatter):
def __init__(self, printreason=False, show_failed_logs=False, printshellcmds=False):
self.printreason = printreason
self.show_failed_logs = show_failed_logs
self.printshellcmds = printshellcmds
self.last_msg_was_job_info = False
class DefaultFormatter(_logging.Formatter):
def __init__(self, printreason: bool = False, show_failed_logs: bool = False, printshellcmds: bool = False):
self.printreason = printreason
self.show_failed_logs = show_failed_logs
self.printshellcmds = printshellcmds
self.last_msg_was_job_info = False

Copy link
Copy Markdown
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

🧹 Outside diff range and nitpick comments (1)
snakemake/logging.py (1)

401-438: LGTM: Well-structured _format_job_info method.

The _format_job_info method is well-implemented, handling various job information fields efficiently. The use of helper functions like format_item improves readability.

Consider using a dictionary for singleitems instead of conditionally appending 'reason'. This could make the code slightly more declarative:

singleitems = ["jobid", "benchmark"]
if self.printreason:
    singleitems.append("reason")

Could be refactored to:

singleitems = {
    "jobid": True,
    "benchmark": True,
    "reason": self.printreason
}

Then, you can iterate over the dictionary:

for item, include in singleitems.items():
    if include:
        fmt = format_item(item, omit=None)
        if fmt:
            output.append(fmt)

This is a minor suggestion and the current implementation is perfectly acceptable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 0b76608 and a69a4ba.

📒 Files selected for processing (1)
  • snakemake/logging.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
snakemake/logging.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

📓 Learnings (1)
snakemake/logging.py (2)
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2024-10-08T17:41:54.542Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2024-09-30T17:17:37.360Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
🪛 Ruff
snakemake/logging.py

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

🔇 Additional comments (5)
snakemake/logging.py (5)

291-308: LGTM: Efficient implementation of format_info method.

The format_info method efficiently handles multiline messages and indentation. The use of list comprehension for applying indentation is a good practice.


Line range hint 737-920: LGTM: Well-structured Logger class with intentional log level mapping.

The Logger class is well-implemented with clear separation of concerns and good practices. The use of a dictionary to map custom log levels to standard Python logging levels in the handler method is particularly noteworthy.

Note: The mapping of the custom log level 'info' to _logging.WARNING is intentional to preserve the existing log output coloring. This is consistent with the previous implementation and should be maintained.


Line range hint 924-979: LGTM: Comprehensive setup_logger function.

The setup_logger function is well-implemented, providing a thorough initialization of the logging system for Snakemake. It correctly handles different quiet modes and sets up the logger with appropriate handlers and formatters.

The function effectively uses the new ColorizingTextHandler, DefaultFormatter, and DefaultFilter classes, integrating them into the logging setup. This approach provides a good balance between customization and standard logging practices.


Line range hint 1-979: Overall assessment: Improved logging system with minor suggestions for refinement.

The changes to snakemake/logging.py represent a significant improvement in the modularity and structure of Snakemake's logging system. The introduction of DefaultFormatter, DefaultFilter, and ColorizingTextHandler classes provides better separation of concerns and enhances customization options.

Key improvements:

  1. More modular and extensible logging architecture.
  2. Better handling of different log levels and message types.
  3. Improved color support for log messages.

Suggestions for further refinement:

  1. Consider refactoring the format method in DefaultFormatter for easier maintenance.
  2. Simplify the filter method in DefaultFilter for better readability.
  3. Address the duplication of the is_quiet_about method between DefaultFilter and ColorizingTextHandler.
  4. Improve exception handling in the emit method of ColorizingTextHandler.

Overall, these changes represent a positive step forward in Snakemake's logging capabilities, with only minor adjustments needed for optimal performance and maintainability.

🧰 Tools
🪛 Ruff

513-513: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


523-523: Use a context manager for opening files

(SIM115)


537-537: Ambiguous variable name: l

(E741)


549-552: Combine if branches using logical or operator

Combine if branches

(SIM114)


568-568: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


581-581: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


246-252: ⚠️ Potential issue

Fix typo and consider adding type hints.

There's a typo in the __init__ method's parameter name. Also, consider adding type hints to improve code readability and maintainability.

Apply this diff to fix the typo and add type hints:

-    def __init__(self, printreason=False, show_failed_logs=False, printshellcmds=False):
+    def __init__(self, printreason: bool = False, show_failed_logs: bool = False, printshellcmds: bool = False):
         self.printreason = printreason
         self.show_failed_logs = show_failed_logs
-        self.printshellcmds = printshellmds
+        self.printshellcmds = printshellcmds
         self.last_msg_was_job_info = False

Likely invalid or redundant comment.

Comment on lines +254 to +289
def format(self, record):
"""
Override format method to format Snakemake-specific log messages.
"""
msg = record.msg
level = msg.get("level", "INFO") # Default to "INFO" if level not in message

# Call specific handlers based on the log level
if level == "info":
return self.format_info(msg)
elif level == "host":
return self.format_host(msg)
elif level == "job_info":
return self.format_job_info(msg)
elif level == "group_info":
return self.format_group_info(msg)
elif level == "job_error":
return self.format_job_error(msg)
elif level == "group_error":
return self.format_group_error(msg)
elif level == "progress":
return self.format_progress(msg)
elif level == "job_finished":
return self.format_job_finished(msg)
elif level == "shellcmd":
return self.format_shellcmd(msg)
elif level == "rule_info":
return self.format_rule_info(msg)
elif level == "d3dag":
return self.format_d3dag(msg)
elif level == "dag_debug":
return self.format_dag_debug(msg)
elif level == "run_info":
return self.format_run_info(msg)
else:
return msg["msg"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider refactoring the format method for better maintainability.

The format method uses a long chain of if-elif statements to handle different log levels. This structure can become hard to maintain as new log levels are added. Consider using a dictionary dispatch or pattern matching (if using Python 3.10+) for better maintainability.

Here's an example of how you could refactor this using a dictionary dispatch:

def format(self, record):
    msg = record.msg
    level = msg.get("level", "INFO").lower()

    format_methods = {
        "info": self.format_info,
        "host": self.format_host,
        "job_info": self.format_job_info,
        # ... add other levels here
    }

    format_method = format_methods.get(level, lambda m: m["msg"])
    return format_method(msg)

This approach makes it easier to add new log levels in the future and improves the overall readability of the method.

Comment on lines +587 to +635
class DefaultFilter:
def __init__(
self,
quiet,
debug_dag,
) -> None:
self.quiet = quiet
self.debug_dag = debug_dag

def is_quiet_about(self, msg_type: str):
from snakemake.settings.enums import Quietness

return (
Quietness.ALL in self.quiet
or Quietness.parse_choice(msg_type) in self.quiet
)

def filter(self, record):
if self.is_quiet_about("all"):
return False

msg = record.msg
level = msg.get("level", "INFO").lower()

# Respect quiet mode filtering
quietness_map = {
"job_info": "rules",
"group_info": "rules",
"job_error": "rules",
"group_error": "rules",
"progress": "progress",
"shellcmd": "progress", # or other quietness types
"job_finished": "progress",
"resources_info": "progress",
"run_info": "progress",
"host": "host",
"info": "progress",
}

# Check quietness for specific levels and skip accordingly
if level in quietness_map:
if self.is_quiet_about(quietness_map[level]):
return False

# Handle dag_debug specifically
if level == "dag_debug" and not self.debug_dag:
return False

return True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Simplify the filter method for better readability.

The filter method in the DefaultFilter class is well-implemented, but it can be slightly simplified for better readability.

Consider refactoring the filter method as follows:

def filter(self, record):
    if self.is_quiet_about("all"):
        return False

    msg = record.msg
    level = msg.get("level", "INFO").lower()

    quietness_map = {
        "job_info": "rules",
        "group_info": "rules",
        "job_error": "rules",
        "group_error": "rules",
        "progress": "progress",
        "shellcmd": "progress",
        "job_finished": "progress",
        "resources_info": "progress",
        "run_info": "progress",
        "host": "host",
        "info": "progress",
    }

    if level in quietness_map and self.is_quiet_about(quietness_map[level]):
        return False

    if level == "dag_debug" and not self.debug_dag:
        return False

    return True

This simplification combines the nested if statements and removes the redundant return True at the end of the method.

🧰 Tools
🪛 Ruff

627-628: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


632-635: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)

Comment on lines +638 to +735
class ColorizingTextHandler(_logging.StreamHandler):
"""
Custom handler that combines colorization and Snakemake-specific formatting.
"""

BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[%dm"
BOLD_SEQ = "\033[1m"

colors = {
"WARNING": YELLOW,
"INFO": GREEN,
"DEBUG": BLUE,
"CRITICAL": MAGENTA,
"ERROR": RED,
}

def __init__(
self,
nocolor=False,
stream=sys.stderr,
mode=None,
):
super().__init__(stream=stream)
self.last_msg_was_job_info = False
self._output_lock = threading.Lock()
self.nocolor = nocolor or not self.can_color_tty(mode)
self.mode = mode

def can_color_tty(self, mode):
"""
Check if the terminal supports colors.
"""
from snakemake_interface_executor_plugins.settings import ExecMode

if "TERM" in os.environ and os.environ["TERM"] == "dumb":
return False
if mode == ExecMode.SUBPROCESS:
return True
return self.is_tty and not platform.system() == "Windows"

@property
def is_tty(self):
isatty = getattr(self.stream, "isatty", None)
return isatty and isatty()

def emit(self, record):
"""
Emit a log message with custom formatting and color.
"""
with self._output_lock:
try:

level = record.msg.get("level", "INFO").lower()

if level == "job_info":
if not self.last_msg_was_job_info:
self.stream.write(
"\n"
) # Add a blank line before a new job_info message
self.last_msg_was_job_info = True
else:
# Reset flag if the message is not a 'job_info'
self.last_msg_was_job_info = False
formatted_message = self.format(record)

# Apply color to the formatted message
self.stream.write(self.decorate(record, formatted_message))
self.stream.write(getattr(self, "terminator", "\n"))
self.flush()
except BrokenPipeError as e:
raise e
except (KeyboardInterrupt, SystemExit):
pass # Ignore exceptions for these cases
except Exception as e:
self.handleError(record)

def is_quiet_about(self, msg_type: str):
from snakemake.settings.enums import Quietness

return (
Quietness.ALL in self.quiet
or Quietness.parse_choice(msg_type) in self.quiet
)

def decorate(self, record, message):
"""
Add color to the log message based on its level.
"""
message = [message] # Treat the formatted message as a list

# Use record.levelname to apply color if applicable
if not self.nocolor and record.levelname in self.colors:
message.insert(0, self.COLOR_SEQ % (30 + self.colors[record.levelname]))
message.append(self.RESET_SEQ)

return "".join(message)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Improve ColorizingTextHandler class implementation.

The ColorizingTextHandler class is generally well-implemented, but there are a few areas for improvement:

  1. The is_quiet_about method (lines 716-722) seems out of place in this class and is duplicated from the DefaultFilter class. Consider removing it or moving it to a common utility module.

  2. The emit method catches and ignores all exceptions, which might hide important errors. Consider logging these exceptions or handling them more specifically.

  3. The can_color_tty method can be simplified.

Here's a suggestion for simplifying the can_color_tty method:

def can_color_tty(self, mode):
    if "TERM" in os.environ and os.environ["TERM"] == "dumb":
        return False
    if mode == ExecMode.SUBPROCESS:
        return True
    return self.is_tty and platform.system() != "Windows"

For the emit method, consider modifying the exception handling:

def emit(self, record):
    with self._output_lock:
        try:
            # ... existing code ...
        except BrokenPipeError:
            raise
        except (KeyboardInterrupt, SystemExit):
            # Re-raise these exceptions
            raise
        except Exception:
            self.handleError(record)

This change will re-raise KeyboardInterrupt and SystemExit exceptions, which are typically used for program flow control, while still catching and handling other exceptions.

Would you like me to propose a common utility module for the is_quiet_about method to resolve the duplication issue?

🧰 Tools
🪛 Ruff

678-678: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


713-713: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

@cademirch
Copy link
Copy Markdown
Contributor Author

Another question is regarding the two calls of setup_logger() brought up in #2797. I still don't understand why these are necessary and understanding this would be helpful in unifying this.

Answered here: #2797 (comment) Further, one general problem I have with the Snakemake logger is that it uses a global object which kind of has a state. We can keep it like that but one could keep in mind that maybe at some point this should be somehow different and the logger instance should rather be stored in the API call or so. Would be the start of making Snakemake thread safe.

I've been thinking about this lately as I've been digging into the execmodes and its effect on logging. I'm still a bit confused with the API and have been looking into the LocalExecutor to try and understand, but I can't seem to understand if/where the API is constructed by the executor? Do you have any resources/diagrams of the how this works?

If I understand what you're saying here is that a logger instance could be passed to the constructor of the API instance?

I'm also not sure how much this matters for local execution to be honest. Currently things are setup such that loggers in subprocess don't log anything (which makes sense to me):

def additional_general_args(self):
return "--quiet progress rules"

I was thinking of making this more explicit by just attaching NullHandler to the logger instead of the StreamHandler when in subproccess mode.

Overall, my understanding is that we only care about logs that come from the main Snakemake process (and by extension any exectuor plugins - which should be in the same process?), so having a global logger makes sense in this way. However its possible I'm missing what you're saying here.

In a remote context, it makes sense that the logger writes to stderr/file, but that doesn't contend with the main Snakemake process so this is okay. In this scenario, we only "care" about the logs from the main Snakemake process, because remote process is totally independent.

Copy link
Copy Markdown
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: 1

🧹 Outside diff range and nitpick comments (3)
snakemake/logging.py (3)

291-549: Well-structured formatter with room for improvement.

The DefaultFormatter class is comprehensive and well-structured, providing formatting for various log types. However, there are a few areas that could be improved:

  1. The format method (lines 298-335) uses a long chain of if-elif statements. Consider refactoring this to use a dictionary dispatch or pattern matching (if using Python 3.10+) for better maintainability.

  2. Some methods like _format_job_info, _format_job_error, and _format_group_error are quite long. Consider breaking these down into smaller, more focused methods to improve readability and maintainability.

Here's an example of how you could refactor the format method using a dictionary dispatch:

def format(self, record):
    msg = record.msg
    level = msg.get("level", "INFO").lower()

    format_methods = {
        "info": self.format_info,
        "host": self.format_host,
        "job_info": self.format_job_info,
        # ... add other levels here
    }

    format_method = format_methods.get(level, lambda m: m["msg"])
    return format_method(msg)

This approach makes it easier to add new log levels in the future and improves the overall readability of the method.


575-623: Well-implemented filter with room for optimization.

The DefaultFilter class is well-structured and provides necessary filtering functionality. However, there are a couple of areas for improvement:

  1. The filter method can be simplified for better readability.

  2. The is_quiet_about method appears to be duplicated in the ColorizingTextHandler class. Consider moving this method to a common utility module to avoid duplication.

Here's a suggested simplification for the filter method:

def filter(self, record):
    if self.is_quiet_about("all"):
        return False

    msg = record.msg
    level = msg.get("level", "INFO").lower()

    quietness_map = {
        "job_info": "rules",
        "group_info": "rules",
        "job_error": "rules",
        "group_error": "rules",
        "progress": "progress",
        "shellcmd": "progress",
        "job_finished": "progress",
        "resources_info": "progress",
        "run_info": "progress",
        "host": "host",
        "info": "progress",
    }

    if level in quietness_map and self.is_quiet_about(quietness_map[level]):
        return False

    if level == "dag_debug" and not self.debug_dag:
        return False

    return True

This simplification combines the nested if statements and removes the redundant return True at the end of the method.

Would you like me to propose a common utility module for the is_quiet_about method to resolve the duplication issue?

🧰 Tools
🪛 Ruff

615-616: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


620-623: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


626-723: Well-implemented handler with areas for improvement.

The ColorizingTextHandler class is generally well-implemented, providing useful colorization functionality. However, there are a few areas for improvement:

  1. The is_quiet_about method (lines 703-709) seems out of place in this class and is duplicated from the DefaultFilter class. Consider removing it or moving it to a common utility module.

  2. The emit method catches and ignores all exceptions, which might hide important errors. Consider logging these exceptions or handling them more specifically.

  3. The can_color_tty method can be simplified.

Here's a suggestion for simplifying the can_color_tty method:

def can_color_tty(self, mode):
    if "TERM" in os.environ and os.environ["TERM"] == "dumb":
        return False
    if mode == ExecMode.SUBPROCESS:
        return True
    return self.is_tty and platform.system() != "Windows"

For the emit method, consider modifying the exception handling:

def emit(self, record):
    with self._output_lock:
        try:
            # ... existing code ...
        except BrokenPipeError:
            raise
        except (KeyboardInterrupt, SystemExit):
            # Re-raise these exceptions
            raise
        except Exception:
            self.handleError(record)

This change will re-raise KeyboardInterrupt and SystemExit exceptions, which are typically used for program flow control, while still catching and handling other exceptions.

Would you like me to propose a common utility module for the is_quiet_about method to resolve the duplication issue?

🧰 Tools
🪛 Ruff

666-666: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


700-700: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between a69a4ba and 547e2b7.

📒 Files selected for processing (1)
  • snakemake/logging.py (5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
snakemake/logging.py (1)

Pattern **/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

🪛 Ruff
snakemake/logging.py

253-256: Combine if branches using logical or operator

Combine if branches

(SIM114)


272-272: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


285-285: Do not assign a lambda expression, use a def

Rewrite fmt as a def

(E731)


555-555: Use a context manager for opening files

(SIM115)


569-569: Ambiguous variable name: l

(E741)


615-616: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


620-623: Return the condition not (level == "dag_debug" and not self.debug_dag) directly

Replace with return not (level == "dag_debug" and not self.debug_dag)

(SIM103)


666-666: Use platform.system() != "Windows" instead of not platform.system() == "Windows"

Replace with != operator

(SIM201)


700-700: Local variable e is assigned to but never used

Remove assignment to unused variable e

(F841)


929-932: Use ternary operator quiet = {Quietness.PROGRESS, Quietness.RULES} if quiet else set() instead of if-else-block

Replace if-else-block with quiet = {Quietness.PROGRESS, Quietness.RULES} if quiet else set()

(SIM108)

🔇 Additional comments (3)
snakemake/logging.py (3)

247-265: LGTM: Well-implemented dictionary formatting function.

The format_dict function is well-implemented, providing flexible formatting for both Namedlist and regular dictionaries. The use of isinstance checks is appropriate for type-specific handling.

🧰 Tools
🪛 Ruff

253-256: Combine if branches using logical or operator

Combine if branches

(SIM114)


Line range hint 725-900: Excellent improvements to the Logger class.

The updates to the Logger class significantly enhance its functionality and integration with the new formatting and filtering components. Key improvements include:

  1. Addition of level, default_formatter, and default_filter attributes, which improve the logger's configurability.
  2. Updated setup_logfile method that now properly configures the formatter and filter for the logfile handler.
  3. Restructured handler method with a comprehensive mapping of custom log levels to standard Python logging levels.
  4. New set_level method for easier log level configuration.

These changes make the logger more flexible and easier to customize, which is crucial for a complex system like Snakemake.


Line range hint 906-959: Well-implemented logger setup with improved flexibility.

The setup_logger function has been effectively updated to integrate the new logging components and provide more flexible configuration options. Notable improvements include:

  1. Use of the new ColorizingTextHandler, DefaultFormatter, and DefaultFilter classes, which enhances the logging capabilities.
  2. More robust handling of the 'quiet' parameter, now supporting boolean, None, or set values, which provides greater flexibility in configuring logging verbosity.
  3. Differentiated setup for subprocess mode, which is an efficient optimization for different execution contexts.

These changes make the logger setup more adaptable to various use cases and execution environments, which is crucial for a versatile tool like Snakemake.

🧰 Tools
🪛 Ruff

929-932: Use ternary operator quiet = {Quietness.PROGRESS, Quietness.RULES} if quiet else set() instead of if-else-block

Replace if-else-block with quiet = {Quietness.PROGRESS, Quietness.RULES} if quiet else set()

(SIM108)

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (2)
.github/workflows/main.yml (2)

128-138: Capture All Pytest Output for Better Logging Consistency

The testing job now uses the --show-capture=all flag, which will capture both stdout and stderr. This aligns well with the refactored logging improvements and should help in diagnosing issues. Please verify that the increased verbosity does not overwhelm the logs.


205-211: Ensure Consistent Output Capture Across Jobs

While the "testing" job uses --show-capture=all, the "testing-windows" job still uses --show-capture=stderr (line 210). For consistency and to utilize the logging enhancements uniformly, consider updating the Windows job's pytest command to use --show-capture=all as well—unless there is a specific reason for the discrepancy.

Proposed Diff if Alignment is Desired:

-          python -m pytest --show-capture=stderr -v -x --splits 10 --group ${{ matrix.test_group }} --splitting-algorithm=least_duration tests/tests.py
+          python -m pytest --show-capture=all -v -x --splits 10 --group ${{ matrix.test_group }} --splitting-algorithm=least_duration tests/tests.py
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 851db6d and f2cafaf.

📒 Files selected for processing (10)
  • .github/workflows/main.yml (3 hunks)
  • snakemake/cli.py (6 hunks)
  • snakemake/dag.py (7 hunks)
  • snakemake/jobs.py (6 hunks)
  • snakemake/scheduler.py (7 hunks)
  • snakemake/script/__init__.py (1 hunks)
  • snakemake/settings/enums.py (1 hunks)
  • snakemake/settings/types.py (2 hunks)
  • snakemake/workflow.py (9 hunks)
  • test-environment.yml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • snakemake/settings/enums.py
  • test-environment.yml
  • snakemake/script/init.py
  • snakemake/settings/types.py
  • snakemake/scheduler.py
  • snakemake/workflow.py
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.py`: Do not try to improve formatting. Do not suggest ...

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

  • snakemake/dag.py
  • snakemake/cli.py
  • snakemake/jobs.py
🪛 actionlint (1.7.4)
.github/workflows/main.yml

53-53: "exclude" section should not be empty

(syntax-check)

🔇 Additional comments (24)
snakemake/dag.py (10)

17-17: Appropriate import addition for enhanced functionality.

Adding the json import supports the updated implementation of the d3dag method which now outputs formatted JSON.


31-31: Good integration with the new logging system.

Importing LogEvent from the new snakemake_interface_logger_plugins.common module aligns with the PR objective of implementing a more modular logging framework.


1028-1030: Improved logging with structured context.

The change from logger.dag_debug to a standard Python logger.debug with structured context in the extra parameter enhances log filtering capabilities and standardizes logging practices.


1116-1119: Structured logging for job selection.

This update follows the same pattern of adding structured context to log messages, making it easier to track which jobs are selected during DAG processing.


1121-1128: Enhanced error logging with structured context.

The change improves error logging by including detailed structured information about exceptions encountered during DAG processing. The LogEvent.DEBUG_DAG categorization helps filter these specific logs.


1204-1211: Structured logging for file presence detection.

This change standardizes the logging for the case where no producers are found for a file, but the file exists on disk. The added structured context improves debuggability.


2780-2780: Modern f-string usage.

Using an f-string for the print statement is a good practice for better readability and maintenance.


2782-2794: Improved JSON output formatting.

The restructured print statement creates more readable and properly formatted JSON output, which is beneficial for downstream consumption by visualization tools.


2812-2812: Enhanced method signature with explicit return type.

Adding the explicit return type annotation tuple[str, dict[str, int]] improves code readability and enables better IDE support and static type checking.


2815-2837: Well-structured refactoring of the stats method.

The method has been refactored to:

  1. Create a tabular output using the tabulate package for better readability
  2. Build a dictionary that can be programmatically accessed
  3. Return both the formatted message and the statistics dictionary

This change enhances the API by providing structured data alongside the human-readable format, which enables better integration with other components.

snakemake/cli.py (7)

20-21: Well-structured import for the new logger plugin system.

This import aligns with the PR objective of creating a more modular logging framework using plugins.


1489-1495: New --logger argument properly replaces previous logging options.

The new argument allows specifying multiple logger plugins and restricts choices to properly registered plugins, which is a good improvement over the previous --log-handler-script and --log-service arguments. This enables more flexible logging configurations as described in the PR objectives.


1871-1875: Well-designed dictionary comprehension for logger settings.

This elegantly collects settings for each specified logger plugin, creating a dictionary that maps plugin names to their settings.


1905-1917: Properly structured OutputSettings configuration.

The OutputSettings object is now properly configured with log_handler_settings, which is a clean approach to passing logging configuration throughout the application.


1919-1924: Robust validation and initialization of log handlers.

The code properly validates settings for each logger plugin before creating the handlers, which is a good practice. The naming conventions align with the discussions in the PR comments about renaming logger_plugin to log_handler.


1925-1930: Centralized logger setup using the new LoggerManager.

This code sets up the centralized logger manager with the appropriate mode, handlers, and settings, which aligns with the PR's goal of standardizing logging practices.


1732-1732: Registration of logger plugin CLI arguments.

This line ensures that any additional CLI arguments from logger plugins are properly registered with the argument parser, which is important for extensibility.

snakemake/jobs.py (7)

33-33: Appropriate import for logging event types.

The import of LogEvent from the logger plugins interface allows for structured categorization of log events, which is essential for the refactored logging system.


1068-1099: Improved structured logging in Job.log_info method.

The refactored code now uses the standard logger.info method with a comprehensive extra parameter containing all relevant job information. This structured approach makes logs more consistent and machine-parsable, which aligns with the PR's goal of creating a modular logging framework.


1100-1104: Separate structured logging for shell commands.

Shell commands are now logged separately with their own event type, making it easier to filter and process these specific log entries. This separation improves clarity and log organization.


1128-1141: Updated error logging field names for consistency.

The error logging info now uses consistent field names (rule_name, rule_msg) which aligns with the naming conventions in the job_info method, improving overall consistency.


1146-1149: Improved error logging with structured data.

The error logging now passes structured data via the extra parameter, making error logs more consistent and machine-parsable. This is a key improvement for the refactored logging system.


1432-1438: Structured logging for group jobs.

The GroupJob.log_info method now uses structured logging with appropriate event types and relevant group information, consistent with the approach used in the Job class.


1446-1455: Well-structured error logging for group jobs.

The GroupJob.log_error method now uses structured logging with a comprehensive set of data in the extra dictionary, providing thorough context for debugging group job errors.

Comment on lines +52 to 55
exclude:
[]
#- shell: dash
# py_ver: "3.11"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Empty "exclude" Section May Cause Configuration Warnings

The empty exclude key (with an empty list) is flagged by actionlint. If no exclusions are intended, consider removing the key entirely or updating it with a meaningful value or a clarifying comment. This will help avoid static analysis warnings and improve workflow clarity.

Proposed Diff:

-        exclude:
-          []
-          #- shell: dash
-          #  py_ver: "3.11"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exclude:
[]
#- shell: dash
# py_ver: "3.11"
🧰 Tools
🪛 actionlint (1.7.4)

53-53: "exclude" section should not be empty

(syntax-check)

Copy link
Copy Markdown
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

♻️ Duplicate comments (1)
snakemake/logging.py (1)

362-362: ⚠️ Potential issue

Fix method reference in _format_group_error method.

The method incorrectly uses self.show_logs(logs) but there's no show_logs method in the DefaultFormatter class. It should use the global show_logs function instead.

-            output.extend(self.show_logs(logs))
+            output.extend(show_logs(logs))
🧹 Nitpick comments (4)
snakemake/logging.py (4)

398-400: Simplify nested if statements.

Combine the nested if statements to improve readability.

-        if event in quietness_map:
-            if quietness_map[event] in self.quiet:
-                return False
+        if event in quietness_map and quietness_map[event] in self.quiet:
+            return False
🧰 Tools
🪛 Ruff (0.8.2)

398-399: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


403-406: Simplify conditional return.

You can simplify this logic by returning the boolean expression directly.

-        if event == LogEvent.DEBUG_DAG and not self.debug_dag:
-            return False
-
-        return True
+        return not (event == LogEvent.DEBUG_DAG and not self.debug_dag)
🧰 Tools
🪛 Ruff (0.8.2)

403-406: Return the negated condition directly

Inline condition

(SIM103)


474-477: Simplify conditional return in can_color_tty method.

The method can be simplified by returning the boolean expression directly.

-        if has_tty and not is_windows:
-            return True
-
-        return False
+        return bool(has_tty and not is_windows)
🧰 Tools
🪛 Ruff (0.8.2)

474-477: Return the condition bool(has_tty and not is_windows) directly

Replace with return bool(has_tty and not is_windows)

(SIM103)


657-657: Simplify dictionary key lookup.

Use key in dict rather than key in dict.keys() for better performance and readability.

-            for handler in self.logfile_handlers.keys():
+            for handler in self.logfile_handlers:
                 handler.close()
🧰 Tools
🪛 Ruff (0.8.2)

657-657: Use key in dict instead of key in dict.keys()

Remove .keys()

(SIM118)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ede02f4 and 4db3c7d.

📒 Files selected for processing (2)
  • snakemake/exceptions.py (2 hunks)
  • snakemake/logging.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • snakemake/exceptions.py
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.py`: Do not try to improve formatting. Do not suggest ...

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

  • snakemake/logging.py
🧠 Learnings (1)
snakemake/logging.py (2)
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2025-03-12T13:29:22.341Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
Learnt from: johanneskoester
PR: snakemake/snakemake#3107
File: snakemake/logging.py:510-515
Timestamp: 2025-03-12T13:29:09.905Z
Learning: In Snakemake's logging handlers, KeyboardInterrupt and SystemExit exceptions should be ignored (pass) rather than re-raised, as these are control flow signals that should not disrupt the logging process. The exception variable in the final except block should be kept for potential debugging purposes.
🪛 Ruff (0.8.2)
snakemake/logging.py

86-86: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


398-399: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


403-406: Return the negated condition directly

Inline condition

(SIM103)


474-477: Return the condition bool(has_tty and not is_windows) directly

Replace with return bool(has_tty and not is_windows)

(SIM103)


657-657: Use key in dict instead of key in dict.keys()

Remove .keys()

(SIM118)

🔇 Additional comments (4)
snakemake/logging.py (4)

512-512: Exception handling follows project convention.

This exception handling correctly ignores KeyboardInterrupt and SystemExit as per project convention, which ensures these control flow signals don't disrupt the logging process.


663-677: Good implementation of error handling in setup_logfile.

The addition of error handling for file system operations in setup_logfile is a good improvement. It will prevent crashes if the log directory cannot be created or the log file cannot be written.


525-528: Intentional mapping of INFO to WARNING color for specific events.

This code correctly implements the intentional mapping of certain INFO-level events to the WARNING color to preserve the existing log output coloring, as mentioned in retrieved learnings.


536-686: Well-structured LoggerManager class for centralized logging.

The new LoggerManager class provides a clear, centralized approach to managing logging, which aligns with the PR objective of creating a more modular logging framework. The implementation handles setup, teardown, and configuration properly.

🧰 Tools
🪛 Ruff (0.8.2)

657-657: Use key in dict instead of key in dict.keys()

Remove .keys()

(SIM118)

Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
snakemake/api.py (1)

597-604: Supply a meaningful log message instead of None.
Passing None may yield unclear logs. Consider this minor improvement:

- logger.info(None, extra=dict(
+ logger.info("Workflow started", extra=dict(
    event=LogEvent.WORKFLOW_STARTED,
    workflow_id=uuid.uuid4(),
    snakefile=self.workflow_api.snakefile,
))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4db3c7d and 418aa03.

📒 Files selected for processing (1)
  • snakemake/api.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.py`: Do not try to improve formatting. Do not suggest ...

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

  • snakemake/api.py
🔇 Additional comments (7)
snakemake/api.py (7)

15-15: No issues with adding the UUID import.
Everything appears correct, allowing for unique workflow identifiers.


48-49: New logger plugin imports look fine.
This prepares the code for plugin-based logging without apparent issues.


53-53: Refactored import from snakemake.logging is appropriate.
Transitioning to a dedicated logger_manager aligns with the revised design.


249-261: Logger setup logic looks solid.
Constructing and validating log handlers on demand aligns well with plugin architecture. No immediate concerns regarding error handling or concurrency.


493-497: Clean integration with the logging setup.
Invoking setup_logger with dynamic parameters (dryrun, exec_mode) is consistent and maintains flexibility.


590-590: File-based logging initialization is consistent with the new design.
Checking logger_manager.setup_logfile() here is logical and maintains clear separation of logger concerns.


162-163:

Details

❓ Verification inconclusive

Verify logger manager behavior upon cleanup.
Calling logger_manager.cleanup_logfile() and logger_manager.stop() is logical, but please ensure no errors arise if the logger was never initialized or is used concurrently.

Run the following script to search for partial or skipped initialization patterns in logger_manager usage:


🏁 Script executed:

#!/bin/bash
# Description: Verify no partial initialization or concurrency pitfalls in logger_manager usage.
rg -A 5 'logger_manager\.'

Length of output: 3416


Verify logger_manager cleanup and concurrent usage

The calls to logger_manager.cleanup_logfile() and logger_manager.stop() at lines 162–163 in snakemake/api.py appear consistent with how logger initialization is conditionally handled elsewhere in the file (e.g., the check for logger_manager.initialized before setup). However, please ensure that:

  • The implementations of cleanup_logfile() and stop() gracefully handle cases where the logger was never initialized.
  • These methods are thread-safe and idempotent to prevent issues when accessed concurrently.

Please review the internal logic of these methods within the logger manager to confirm that these concerns are adequately addressed.

cademirch and others added 2 commits March 12, 2025 15:13
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
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

🧹 Nitpick comments (3)
snakemake/logging.py (3)

367-406: Simplify conditionals in DefaultFilter.

The filter method can be simplified by combining nested conditions and simplifying the return statement.

    def filter(self, record):
        from snakemake.settings.enums import Quietness

        event, level = get_event_level(record)
        if self.dryrun and level == "run_info":
            return True

        if Quietness.ALL in self.quiet and not self.dryrun:
            return False

        quietness_map = {
            LogEvent.JOB_INFO: Quietness.RULES,
            LogEvent.GROUP_INFO: Quietness.RULES,
            LogEvent.JOB_ERROR: Quietness.RULES,
            LogEvent.GROUP_ERROR: Quietness.RULES,
            LogEvent.PROGRESS: Quietness.PROGRESS,
            LogEvent.SHELLCMD: Quietness.PROGRESS,
            LogEvent.JOB_FINISHED: Quietness.PROGRESS,
            LogEvent.RESOURCES_INFO: Quietness.PROGRESS,
            LogEvent.RUN_INFO: Quietness.PROGRESS,
        }

        # Check quietness for specific levels
-       if event in quietness_map:
-           if quietness_map[event] in self.quiet:
-               return False
+       if event in quietness_map and quietness_map[event] in self.quiet:
+           return False

        # Handle dag_debug specifically
-       if event == LogEvent.DEBUG_DAG and not self.debug_dag:
-           return False
-
-       return True
+       return not (event == LogEvent.DEBUG_DAG and not self.debug_dag)
🧰 Tools
🪛 Ruff (0.8.2)

398-399: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


403-406: Return the negated condition directly

Inline condition

(SIM103)


453-482: Simplify can_color_tty method.

The method can be simplified for better readability.

    def can_color_tty(self, mode):
        """
        Colors are supported when:
        1. Terminal is not "dumb"
        2. Running in subprocess mode
        3. Using a TTY on non-Windows systems
        """
        from snakemake_interface_executor_plugins.settings import ExecMode

        # Case 1: Check if terminal is "dumb"
        if os.environ.get("TERM") == "dumb":
            return False

        # Case 2: Always support colors in subprocess mode
        if mode == ExecMode.SUBPROCESS:
            return True

        # Case 3: Support colors on TTY except for Windows
        is_windows = platform.system() == "Windows"
        has_tty = self.is_tty

-       if has_tty and not is_windows:
-           return True
-
-       return False
+       return has_tty and not is_windows
🧰 Tools
🪛 Ruff (0.8.2)

474-477: Return the condition bool(has_tty and not is_windows) directly

Replace with return bool(has_tty and not is_windows)

(SIM103)


653-659: Clean implementation of cleanup_logfile method.

The method correctly closes file handlers to ensure proper resource cleanup.

    def cleanup_logfile(self):
        from snakemake_interface_executor_plugins.settings import ExecMode

        if self.mode == ExecMode.DEFAULT:
-           for handler in self.logfile_handlers.keys():
+           for handler in self.logfile_handlers:
                handler.close()

This simplifies the code slightly by directly iterating over the dictionary keys.

🧰 Tools
🪛 Ruff (0.8.2)

657-657: Use key in dict instead of key in dict.keys()

Remove .keys()

(SIM118)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a4aa36 and c1e717c.

📒 Files selected for processing (1)
  • snakemake/logging.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.py`: Do not try to improve formatting. Do not suggest ...

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

  • snakemake/logging.py
🧠 Learnings (1)
snakemake/logging.py (2)
Learnt from: cademirch
PR: snakemake/snakemake#3107
File: snakemake/logging.py:806-830
Timestamp: 2025-03-12T14:50:45.354Z
Learning: In the Snakemake project's `Logger.handler` method, the mapping of the custom log level `'info'` to `_logging.WARNING` is intentional to preserve the existing log output coloring.
Learnt from: johanneskoester
PR: snakemake/snakemake#3107
File: snakemake/logging.py:510-515
Timestamp: 2025-03-12T14:50:33.209Z
Learning: In Snakemake's logging handlers, KeyboardInterrupt and SystemExit exceptions should be ignored (pass) rather than re-raised, as these are control flow signals that should not disrupt the logging process. The exception variable in the final except block should be kept for potential debugging purposes.
🪛 Ruff (0.8.2)
snakemake/logging.py

86-86: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


398-399: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


403-406: Return the negated condition directly

Inline condition

(SIM103)


474-477: Return the condition bool(has_tty and not is_windows) directly

Replace with return bool(has_tty and not is_windows)

(SIM103)


657-657: Use key in dict instead of key in dict.keys()

Remove .keys()

(SIM118)

⏰ Context from checks skipped due to timeout of 90000ms (30)
  • GitHub Check: testing (10, 3.12, bash)
  • GitHub Check: testing (10, 3.11, bash)
  • GitHub Check: testing (9, 3.12, bash)
  • GitHub Check: testing (9, 3.11, bash)
  • GitHub Check: testing (8, 3.12, bash)
  • GitHub Check: testing (8, 3.11, bash)
  • GitHub Check: testing (7, 3.12, bash)
  • GitHub Check: testing (7, 3.11, bash)
  • GitHub Check: testing (6, 3.12, bash)
  • GitHub Check: testing (6, 3.11, bash)
  • GitHub Check: testing-windows (10)
  • GitHub Check: testing (5, 3.12, bash)
  • GitHub Check: testing-windows (9)
  • GitHub Check: testing (5, 3.11, bash)
  • GitHub Check: testing-windows (8)
  • GitHub Check: testing (4, 3.12, bash)
  • GitHub Check: testing (4, 3.11, bash)
  • GitHub Check: testing-windows (7)
  • GitHub Check: testing (3, 3.12, bash)
  • GitHub Check: testing-windows (6)
  • GitHub Check: testing (3, 3.11, bash)
  • GitHub Check: testing-windows (5)
  • GitHub Check: testing (2, 3.12, bash)
  • GitHub Check: testing-windows (4)
  • GitHub Check: testing (2, 3.11, bash)
  • GitHub Check: testing-windows (3)
  • GitHub Check: testing (1, 3.12, bash)
  • GitHub Check: testing-windows (2)
  • GitHub Check: testing (1, 3.11, bash)
  • GitHub Check: testing-windows (1)
🔇 Additional comments (32)
snakemake/logging.py (32)

32-34: Nice addition of a timestamp helper function.

This helper function improves code clarity and promotes reuse by centralizing timestamp formatting.


37-59: Well-implemented show_logs function with proper error handling.

The implementation properly handles file opening with a context manager, manages different error cases (FileNotFoundError, UnicodeDecodeError), and formats the log output with appropriate headers and separators.


62-79: Good implementation of format_dict function.

The function correctly handles both dict and Namedlist types, properly filters keys and values, and provides clear error messages for invalid inputs.


82-84: Good use of partial function application.

Using partial from functools for creating specialized formatting functions is an elegant solution.


86-87: Well-implemented resource name formatting.

The function correctly filters out specified resources from the output.

🧰 Tools
🪛 Ruff (0.8.2)

86-86: Do not perform function call in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


90-105: Consider simplifying the format_percentage function.

The function is well-implemented but the lambda assignment could be replaced with a proper named function for better readability (which has already been implemented in line 100).


108-121: Good implementation of get_event_level function.

The function correctly extracts the log level from a record and provides good type hints and documentation.


124-129: Well-implemented is_quiet_about function.

The function correctly parses and checks the quietness setting against the message type.


132-142: Clean implementation of DefaultFormatter class initialization.

The initializer properly handles defaults and stores the configuration values.


144-168: Well-organized formatter dispatch in DefaultFormatter.

Using a dictionary to map event types to formatting methods creates a clean, maintainable solution that's easy to extend.


170-187: Good implementation of message formatting with indentation support.

The method handles multiline messages correctly and applies consistent indentation.


189-195: Simple and direct implementation of formatters.

Both the format_run_info and format_host methods are concise and straightforward.


197-209: Well-implemented job info formatting.

The method correctly formats job information including timestamp, reason, and other details.


211-215: Simple and clear group info formatting.

The method correctly formats group information messages.


217-229: Well-implemented error formatting.

Both format_job_error and format_group_error methods delegate to helper methods for detailed formatting.


231-245: Clear implementation of progress and job finished formatting.

The methods provide appropriate formatting for progress and job finished messages, and the shellcmd formatting respects the printshellcmds setting.


247-265: Good implementation of various formatting methods.

The format_d3dag and format_dag_debug methods provide appropriate formatting for their respective message types.


267-304: Well-structured job info formatting helper.

The helper method provides detailed and consistent formatting of job information.


306-334: Comprehensive job error formatting.

The method provides detailed error information and conditionally shows logs based on configuration.


336-364: Thorough group error formatting.

The method provides detailed formatting of group errors, including information about individual jobs and logs.


409-433: Well-structured ColorizingTextHandler class with good organization of constants.

The class constants are well-defined and the documentation clearly explains the purpose of the class.


434-452: Good initialization of ColorizingTextHandler.

The constructor properly initializes the handler with the stream, formatter, and filter.


484-514: Properly implemented emit method with mutex protection.

The emit method correctly uses a lock to ensure thread safety and properly handles record formatting and output. The exception handling follows the project's conventions as per the retrieved learning.


516-533: Good implementation of the decorate method.

The method correctly adds color to messages based on their level and respects the nocolor setting.


536-545: Well-designed LoggerManager class initialization.

The initializer sets up the necessary attributes for managing logging.


546-599: Comprehensive setup method in LoggerManager.

The setup method correctly configures handlers based on the execution mode and settings, and sets up a queue listener for asynchronous logging.


600-617: Good implementation of default configuration methods.

The methods properly configure plugins with default filters and formatters.


619-638: Well-implemented default handler creation methods.

The methods for creating default file and stream handlers properly configure them with the appropriate formatters and filters.


640-651: Good implementation of logfile_hint method.

The method correctly logs the location of logfiles and returns them for potential use.


660-677: Good error handling in setup_logfile method.

The method includes try-except blocks to handle potential file system errors when creating directories and files.


679-681: Proper resource cleanup in the stop method.

The method checks if the queue listener exists and is initialized before stopping it.


684-686: Good global logger configuration.

Setting up a global logger and logger manager makes the logging system accessible throughout the application.

Copy link
Copy Markdown
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

🧹 Nitpick comments (1)
snakemake/cli.py (1)

1871-1875: Naming inconsistency in log handler settings

While the implementation is functionally correct, there's a naming inconsistency based on previous discussions in the PR. You're using log_handler_settings as the variable name (good), but the code still references "logger" in the context.

Based on johanneskoester's feedback, it was decided to rename from logger_plugin to log_handler. Consider updating variable names accordingly.

    log_handler_settings = {
        name: LoggerPluginRegistry().get_plugin(name).get_settings(args)
        for name in args.logger
    }

Additionally, consider whether the registry class itself should be renamed from LoggerPluginRegistry to LogHandlerRegistry for consistency, but that would be a change in the imported package.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cdc495 and 71f3882.

📒 Files selected for processing (1)
  • snakemake/cli.py (6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.py`: Do not try to improve formatting. Do not suggest ...

**/*.py: Do not try to improve formatting.
Do not suggest type annotations for functions that are defined inside of functions or methods.
Do not suggest type annotation of the self argument of methods.
Do not suggest type annotation of the cls argument of classmethods.
Do not suggest return type annotation if a function or method does not contain a return statement.

  • snakemake/cli.py
⏰ Context from checks skipped due to timeout of 90000ms (30)
  • GitHub Check: testing (10, 3.12, bash)
  • GitHub Check: testing (10, 3.11, bash)
  • GitHub Check: testing (9, 3.12, bash)
  • GitHub Check: testing (9, 3.11, bash)
  • GitHub Check: testing (8, 3.12, bash)
  • GitHub Check: testing (8, 3.11, bash)
  • GitHub Check: testing (7, 3.12, bash)
  • GitHub Check: testing (7, 3.11, bash)
  • GitHub Check: testing (6, 3.12, bash)
  • GitHub Check: testing (6, 3.11, bash)
  • GitHub Check: testing (5, 3.12, bash)
  • GitHub Check: testing-windows (10)
  • GitHub Check: testing (5, 3.11, bash)
  • GitHub Check: testing-windows (9)
  • GitHub Check: testing (4, 3.12, bash)
  • GitHub Check: testing (4, 3.11, bash)
  • GitHub Check: testing-windows (7)
  • GitHub Check: testing (3, 3.12, bash)
  • GitHub Check: testing-windows (6)
  • GitHub Check: testing (3, 3.11, bash)
  • GitHub Check: testing-windows (5)
  • GitHub Check: testing (2, 3.12, bash)
  • GitHub Check: testing-windows (4)
  • GitHub Check: testing (2, 3.11, bash)
  • GitHub Check: testing-windows (3)
  • GitHub Check: testing (1, 3.12, bash)
  • GitHub Check: testing-windows (2)
  • GitHub Check: testing (1, 3.11, bash)
  • GitHub Check: testing-windows (1)
  • GitHub Check: apidocs
🔇 Additional comments (5)
snakemake/cli.py (5)

20-21: Import added for the new logger plugin registry

This import correctly brings in the LoggerPluginRegistry class, which is essential for the new logging system.


1488-1495: Logger plugin CLI argument looks good but consider renaming

The implementation adds proper support for specifying one or more custom loggers via the --logger argument. The choices are correctly restricted to registered logger plugins.

Based on previous discussions in the PR comments, there was consideration to rename from logger_plugin to log_handler for consistency. The CLI flag name here still uses "logger" rather than "log-handler", but this might be intentional for user-friendly naming.

Was this naming deliberate? If you want to maintain consistency with the discussed naming scheme, you might consider renaming this to --log-handler.


1732-1732: Registering CLI args for logger plugins

This correctly registers any additional CLI arguments from the logger plugins, similar to how it's done for other plugin types.


1907-1915: API configuration updated with log handler settings

This correctly passes the log handler settings to the OutputSettings object, replacing the previous log_handlers parameter. This is part of the breaking change mentioned in the PR objectives, where user-provided log handlers will now need to follow the new plugin interface pattern.

Note: Based on a previous review comment, there was discussion about changing log_handlers from a sequence of objects to a sequence of strings (plugin names):

@dataclass
class OutputSettings(SettingsBase):
    ...
-   log_handlers: Sequence[object] = tuple()
+   log_handlers: Sequence[str] = tuple()

But I don't see that reflected in this PR. If that change was made elsewhere or postponed, that's fine - just making note of it.


2157-2161: Clean main entry point

This function correctly maintains its simplicity, delegating to the parse_args and args_to_api functions for the actual work.

@sonarqubecloud
Copy link
Copy Markdown

@johanneskoester johanneskoester merged commit 86f1d6e into snakemake:main Mar 12, 2025
40 checks passed
@github-project-automation github-project-automation bot moved this from In progress to Done in Snakemake Hackathon March 2025 Mar 12, 2025
johanneskoester pushed a commit that referenced this pull request Mar 14, 2025
🤖 I have created a release *beep* *boop*
---


##
[9.0.0](v8.30.0...v9.0.0)
(2025-03-14)


### ⚠ BREAKING CHANGES

* Logging refactor & add LoggerPluginInterface
([#3107](#3107))

### Features

* [#3412](#3412) - keep
shadow folder of failed job if --keep-incomplete flag is set.
([#3430](#3430))
([22978c3](22978c3))
* add flag --report-after-run to automatically generate the report after
a successfull workflow run
([#3428](#3428))
([b0a7f03](b0a7f03))
* add flatten function to IO utils
([#3424](#3424))
([67fa392](67fa392))
* add helper functions to parse input files
([#2918](#2918))
([63e45a7](63e45a7))
* Add option to print redacted file names
([#3089](#3089))
([ba4d264](ba4d264))
* add support for validation of polars dataframe and lazyframe
([#3262](#3262))
([c7473a6](c7473a6))
* added support for rendering dag with mermaid js
([#3409](#3409))
([7bf8381](7bf8381))
* adding --replace-workflow-config to fully replace workflow configs
(from config: directive) with --configfile, instead of merging them
([#3381](#3381))
([47504a0](47504a0))
* Dynamic module name
([#3401](#3401))
([024dc32](024dc32))
* Enable saving and reloading IOCache object
([#3386](#3386))
([c935953](c935953))
* files added in rule params with workflow.source_path will be available
in used containers
([#3385](#3385))
([a6e45bf](a6e45bf))
* Fix keep_local in storage directive and more freedom over remote
retrieval behaviour
([#3410](#3410))
([67b4739](67b4739))
* inherit parameters of use rule and extend/replace individual items
them when using 'with' directive
([#3365](#3365))
([93e4b92](93e4b92))
* Logging refactor & add LoggerPluginInterface
([#3107](#3107))
([86f1d6e](86f1d6e))
* Maximal file size for checksums
([#3368](#3368))
([b039f8a](b039f8a))
* Modernize package configuration using Pixi
([#3369](#3369))
([77992d8](77992d8))
* multiext support for named input/output
([#3372](#3372))
([05e1378](05e1378))
* optionally auto-group jobs via temp files in case of remote execution
([#3378](#3378))
([cc9bba2](cc9bba2))


### Bug Fixes

* `--delete-all-output` ignores `--dry-run`
([#3265](#3265))
([23fef82](23fef82))
* 3342 faster touch runs and warning messages for non-existing files
([#3398](#3398))
([cd9c3c3](cd9c3c3))
* add default value to max-jobs-per-timespan
([#3043](#3043))
([2959abe](2959abe))
* checkpoints inside modules are overwritten
([#3359](#3359))
([fba3ac7](fba3ac7))
* Convert Path to IOFile
([#3405](#3405))
([c58684c](c58684c))
* Do not perform storage object cleanup with --keep-storage-local-copies
set ([#3358](#3358))
([9a6d14b](9a6d14b))
* edgecases of source deployment in case of remote execution
([#3396](#3396))
([5da13be](5da13be))
* enhance error message formatting for strict DAG-building mode
([#3376](#3376))
([a1c39ee](a1c39ee))
* fix bug in checkpoint handling that led to exceptions in case
checkpoint output was missing upon rerun
([#3423](#3423))
([8cf4a2f](8cf4a2f))
* force check all required outputs
([#3341](#3341))
([495a4e7](495a4e7))
* group job formatting
([#3442](#3442))
([f0b10a3](f0b10a3))
* in remote jobs, upload storage in topological order such that
modification dates are preserved (e.g. in case of group jobs)
([#3377](#3377))
([eace08f](eace08f))
* only skip eval when resource depends on input
([#3374](#3374))
([4574c92](4574c92))
* Prevent execution of conda in apptainer when not explicitly requested
in software deployment method
([#3388](#3388))
([c43c5c0](c43c5c0))
* print filenames with quotes around them in RuleException
([#3269](#3269))
([6baeda5](6baeda5))
* Re-evaluation of free resources
([#3399](#3399))
([6371293](6371293))
* ReadTheDocs layout issue due to src directory change
([#3419](#3419))
([695b127](695b127))
* robustly escaping quotes in generated bash scripts (v2)
([#3297](#3297))
([#3389](#3389))
([58720bd](58720bd))
* Show apptainer image URL in snakemake report
([#3407](#3407))
([45f0450](45f0450))
* Update ReadTheDocs configuration for documentation build to use Pixi
([#3433](#3433))
([3f227a6](3f227a6))


### Documentation

* Add pixi setup instructions to general use tutorial
([#3382](#3382))
([115e81b](115e81b))
* fix contribution section heading levels, fix docs testing setup order
([#3360](#3360))
([051dc53](051dc53))
* fix link to github.com/snakemake/poetry-snakemake-plugin
([#3436](#3436))
([ec6d97c](ec6d97c))
* fix quoting
([#3394](#3394))
([b40f599](b40f599))
* fix rerun-triggers default
([#3403](#3403))
([4430e23](4430e23))
* fix typo 'safe' -&gt; 'save'
([#3384](#3384))
([7755861](7755861))
* mention code formatting in the contribution section
([#3431](#3431))
([e8682b7](e8682b7))
* remove duplicated 'functions'.
([#3356](#3356))
([7c595db](7c595db))
* update broken links documentation
([#3437](#3437))
([e3d0d88](e3d0d88))
* Updating contributing guidelines with new pixi dev setup
([#3415](#3415))
([8e95a12](8e95a12))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: snakemake-bot <snakemake-bot-admin@googlegroups.com>
kjohnsen pushed a commit to kjohnsen/snakemake that referenced this pull request Dec 15, 2025
Followup to snakemake#2830.

This PR attempts to make Snakemake's logging more modular to make room
for a potential logging plugin interface, or other logging/monitoring
additions, as well as make logging in Snakemake more standardized.

Currently, most of logging logic is deep within `logging.Logger`, mainly
in `text_handler()` with direct calls to `logger.info/warn/etc`. In this
PR, this logic has been refactored to more comply with the standard
`logging` practices. To do so, three new classes were added:
1. `ColorizingTextHandler` manages adding color to log records and
emitting to stream.
2. `DefaultFormatter` formats log records to be printed. Much of the
logic from `text_handler()` is here now.
3. `DefaultFilter` filters log records from being emitted based on level
and quietness settings.

Together, these classes should output identical logs to the current
logging implementation. I've tested this briefly, but further testing
would be good.

With these changes, it is much easier to add new `logHandlers` via
`logger.addHandler`. New handlers can decide how to format and filter
logs themselves, an important characteristic missing in the current
implementation that motivated this refactoring
(snakemake#2830 (comment)).

This would be a breaking change for `--log-handler-script`, as user
provided log handlers will need to subclass `logging.logHandler`.
Furthermore, logic for adding the user handler (and formatter/filter)
will need to be added.

As for the logging plug interface
(snakemake#2830 (comment)),
I'm not sure that is the route to take, but would appreciate thoughts on
this. First, this functionality is already largely provided by
`--log-handler-script` and adding a logging plugin would be redundant.
Second, I think that development of such plugins would likely be
minimal, and coordinated effort in this repo to maintain a good set of
logging options would be preferable. For example:
- A standard HTTP log handler send emit log records to a user supplied
address on which a 3rd party monitoring app is listening.
- A handler/formatter that uses
[rich](https://github.com/Textualize/rich) to make a
prettier/interactive console on which logs are written to.

I think both of these additions would benefit being standardized to
Snakemake, as well as default options to users.

Additionally I think it would be worthwhile to unify logging in
Snakemake. Currently, the use of the wrapper `logging.Logger` class and
`setup_logger()` function being called in multiple places makes it
difficult to work with the logger and is likely the source of some
issues (snakemake#3044, snakemake#2974, snakemake#2882, snakemake#2797, snakemake#2727). Unifying the logger and its
configuration as recommended by the [logging
docs](https://docs.python.org/3/howto/logging-cookbook.html#a-cli-application-starter-template)
would make it easier to work the the logger. Another option would be to
use something like
[loguru](https://loguru.readthedocs.io/en/stable/index.html). Whichever
option, I think simplifying and unifying the logger should be the next
step of this PR.

One main question I have is how much we would like to preserve the
current stderr output formatting. Most of logging logic is in formatting
rule info and such. Moving forward we can now try different formatting.
Personally, I find the current detailed rule output helpful for sanity
checks when developing and doing a dry run. However, when actually
running the workflow (especially a large one) the terminal is flooded
and errors can quickly be lost which requires searching the log file. Of
course this can be alleviated with --quiet.

Another question is regarding the two calls of setup_logger() brought up
in snakemake#2797. I still don't understand why these are necessary and
understanding this would be helpful in unifying this.

I'm happy to keep pushing this forward but would appreciate your
thoughts before continuing @johanneskoester! I


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new CLI option (--logger) for configuring custom loggers.
- Enhanced structured logging delivers clearer runtime and resource
insights, including rule graph logging.
	- Added a new method for logging rule graph information.
	- Added a new enumeration value `REASON` to the `Quietness` class.

- **Refactor**
- Overhauled the logging architecture across execution, scheduling, and
workflow components for improved traceability and error handling.

- **Chores**
- Updated dependencies and setup configurations to include new logger
plugins.
- Adjusted test configurations to capture all output, ensuring more
comprehensive test diagnostics.

- **Bug Fixes**
- Resolved minor command and formatting inconsistencies for smoother
operations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Johannes Köster <johannes.koester@tu-dortmund.de>
Co-authored-by: Johannes Köster <johannes.koester@uni-due.de>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
kjohnsen pushed a commit to kjohnsen/snakemake that referenced this pull request Dec 15, 2025
🤖 I have created a release *beep* *boop*
---


##
[9.0.0](snakemake/snakemake@v8.30.0...v9.0.0)
(2025-03-14)


### ⚠ BREAKING CHANGES

* Logging refactor & add LoggerPluginInterface
([snakemake#3107](snakemake#3107))

### Features

* [snakemake#3412](snakemake#3412) - keep
shadow folder of failed job if --keep-incomplete flag is set.
([snakemake#3430](snakemake#3430))
([22978c3](snakemake@22978c3))
* add flag --report-after-run to automatically generate the report after
a successfull workflow run
([snakemake#3428](snakemake#3428))
([b0a7f03](snakemake@b0a7f03))
* add flatten function to IO utils
([snakemake#3424](snakemake#3424))
([67fa392](snakemake@67fa392))
* add helper functions to parse input files
([snakemake#2918](snakemake#2918))
([63e45a7](snakemake@63e45a7))
* Add option to print redacted file names
([snakemake#3089](snakemake#3089))
([ba4d264](snakemake@ba4d264))
* add support for validation of polars dataframe and lazyframe
([snakemake#3262](snakemake#3262))
([c7473a6](snakemake@c7473a6))
* added support for rendering dag with mermaid js
([snakemake#3409](snakemake#3409))
([7bf8381](snakemake@7bf8381))
* adding --replace-workflow-config to fully replace workflow configs
(from config: directive) with --configfile, instead of merging them
([snakemake#3381](snakemake#3381))
([47504a0](snakemake@47504a0))
* Dynamic module name
([snakemake#3401](snakemake#3401))
([024dc32](snakemake@024dc32))
* Enable saving and reloading IOCache object
([snakemake#3386](snakemake#3386))
([c935953](snakemake@c935953))
* files added in rule params with workflow.source_path will be available
in used containers
([snakemake#3385](snakemake#3385))
([a6e45bf](snakemake@a6e45bf))
* Fix keep_local in storage directive and more freedom over remote
retrieval behaviour
([snakemake#3410](snakemake#3410))
([67b4739](snakemake@67b4739))
* inherit parameters of use rule and extend/replace individual items
them when using 'with' directive
([snakemake#3365](snakemake#3365))
([93e4b92](snakemake@93e4b92))
* Logging refactor & add LoggerPluginInterface
([snakemake#3107](snakemake#3107))
([86f1d6e](snakemake@86f1d6e))
* Maximal file size for checksums
([snakemake#3368](snakemake#3368))
([b039f8a](snakemake@b039f8a))
* Modernize package configuration using Pixi
([snakemake#3369](snakemake#3369))
([77992d8](snakemake@77992d8))
* multiext support for named input/output
([snakemake#3372](snakemake#3372))
([05e1378](snakemake@05e1378))
* optionally auto-group jobs via temp files in case of remote execution
([snakemake#3378](snakemake#3378))
([cc9bba2](snakemake@cc9bba2))


### Bug Fixes

* `--delete-all-output` ignores `--dry-run`
([snakemake#3265](snakemake#3265))
([23fef82](snakemake@23fef82))
* 3342 faster touch runs and warning messages for non-existing files
([snakemake#3398](snakemake#3398))
([cd9c3c3](snakemake@cd9c3c3))
* add default value to max-jobs-per-timespan
([snakemake#3043](snakemake#3043))
([2959abe](snakemake@2959abe))
* checkpoints inside modules are overwritten
([snakemake#3359](snakemake#3359))
([fba3ac7](snakemake@fba3ac7))
* Convert Path to IOFile
([snakemake#3405](snakemake#3405))
([c58684c](snakemake@c58684c))
* Do not perform storage object cleanup with --keep-storage-local-copies
set ([snakemake#3358](snakemake#3358))
([9a6d14b](snakemake@9a6d14b))
* edgecases of source deployment in case of remote execution
([snakemake#3396](snakemake#3396))
([5da13be](snakemake@5da13be))
* enhance error message formatting for strict DAG-building mode
([snakemake#3376](snakemake#3376))
([a1c39ee](snakemake@a1c39ee))
* fix bug in checkpoint handling that led to exceptions in case
checkpoint output was missing upon rerun
([snakemake#3423](snakemake#3423))
([8cf4a2f](snakemake@8cf4a2f))
* force check all required outputs
([snakemake#3341](snakemake#3341))
([495a4e7](snakemake@495a4e7))
* group job formatting
([snakemake#3442](snakemake#3442))
([f0b10a3](snakemake@f0b10a3))
* in remote jobs, upload storage in topological order such that
modification dates are preserved (e.g. in case of group jobs)
([snakemake#3377](snakemake#3377))
([eace08f](snakemake@eace08f))
* only skip eval when resource depends on input
([snakemake#3374](snakemake#3374))
([4574c92](snakemake@4574c92))
* Prevent execution of conda in apptainer when not explicitly requested
in software deployment method
([snakemake#3388](snakemake#3388))
([c43c5c0](snakemake@c43c5c0))
* print filenames with quotes around them in RuleException
([snakemake#3269](snakemake#3269))
([6baeda5](snakemake@6baeda5))
* Re-evaluation of free resources
([snakemake#3399](snakemake#3399))
([6371293](snakemake@6371293))
* ReadTheDocs layout issue due to src directory change
([snakemake#3419](snakemake#3419))
([695b127](snakemake@695b127))
* robustly escaping quotes in generated bash scripts (v2)
([snakemake#3297](snakemake#3297))
([snakemake#3389](snakemake#3389))
([58720bd](snakemake@58720bd))
* Show apptainer image URL in snakemake report
([snakemake#3407](snakemake#3407))
([45f0450](snakemake@45f0450))
* Update ReadTheDocs configuration for documentation build to use Pixi
([snakemake#3433](snakemake#3433))
([3f227a6](snakemake@3f227a6))


### Documentation

* Add pixi setup instructions to general use tutorial
([snakemake#3382](snakemake#3382))
([115e81b](snakemake@115e81b))
* fix contribution section heading levels, fix docs testing setup order
([snakemake#3360](snakemake#3360))
([051dc53](snakemake@051dc53))
* fix link to github.com/snakemake/poetry-snakemake-plugin
([snakemake#3436](snakemake#3436))
([ec6d97c](snakemake@ec6d97c))
* fix quoting
([snakemake#3394](snakemake#3394))
([b40f599](snakemake@b40f599))
* fix rerun-triggers default
([snakemake#3403](snakemake#3403))
([4430e23](snakemake@4430e23))
* fix typo 'safe' -&gt; 'save'
([snakemake#3384](snakemake#3384))
([7755861](snakemake@7755861))
* mention code formatting in the contribution section
([snakemake#3431](snakemake#3431))
([e8682b7](snakemake@e8682b7))
* remove duplicated 'functions'.
([snakemake#3356](snakemake#3356))
([7c595db](snakemake@7c595db))
* update broken links documentation
([snakemake#3437](snakemake#3437))
([e3d0d88](snakemake@e3d0d88))
* Updating contributing guidelines with new pixi dev setup
([snakemake#3415](snakemake#3415))
([8e95a12](snakemake@8e95a12))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: snakemake-bot <snakemake-bot-admin@googlegroups.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

3 participants