Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Let wrapper log to stderr and stout file seperately. #2474

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/executing/grouping.rst
Expand Up @@ -36,5 +36,7 @@ This makes it possible to define batches of jobs of the same kind that shall be

snakemake --groups somerule=group0 --group-components group0=5


means that given ``n`` jobs spawned from rule ``somerule``, Snakemake will create ``n / 5`` groups which each execute 5 jobs of ``somerule`` together.

For example, with 10 jobs from ``somerule`` you would end up with 2 groups of 5 jobs that are submitted as one piece each.
104 changes: 102 additions & 2 deletions snakemake/script.py
Expand Up @@ -22,6 +22,7 @@
import collections
import re
import json
import warnings
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Tuple, Pattern, Union, Optional, List
Expand Down Expand Up @@ -73,7 +74,7 @@ def __init__(
self.bench_iteration = bench_iteration
self.scriptdir = scriptdir

def log_fmt_shell(self, stdout=True, stderr=True, append=False):
def log_fmt_shell(self, stdout=True, stderr=True, append=False) -> str:
"""
Return a shell redirection string to be used in `shell()` calls

Expand Down Expand Up @@ -109,9 +110,108 @@ def log_fmt_shell(self, stdout=True, stderr=True, append=False):
False True False fn 2> fn
any any any None ""
-------- -------- -------- ----- -----------

If you provide two log files name them err/out or sterr/stout.
The function returns:

2>> sterr > stout
or with append=True
2>> sterr >> stoud

Appending to sterr is required as error messages from the wrapper will be overwritten otherwise.
"""

if stdout and stderr:
# Check if two files are provided
stdout_file, stderr_file = _infer_stdout_and_stderr(self.log)

if stdout_file is not None:
# We have a stderr and a stdout file

return (
_log_shell_redirect(
stderr_file, stdout=False, stderr=True, append=True
)
+ " "
+ _log_shell_redirect(
stdout_file, stdout=True, stderr=False, append=append
)
)

return _log_shell_redirect(self.log, stdout, stderr, append)

def get_stderr_logfile(self) -> Optional[PathLike]:
"""
Return the stderr-log file.
If multiple log files are specified, try to infer which is stdout and which is stderr.
If this fails return the first log file.
If none is specified, return None
"""

_, stderr_file = _infer_stdout_and_stderr(self.log)

return stderr_file

def get_stdout_logfile(self) -> Optional[PathLike]:
"""
Return the stdout-log file.
If multiple log files are specified, try to infer which is stdout and which is stderr.
If this fails return the first log file.
If none is specified, return None
"""

stdout_file, stderr_file = _infer_stdout_and_stderr(self.log)

if stdout_file is not None:
return stdout_file
else:
return stderr_file


def _infer_stdout_and_stderr(
log: Optional[PathLike],
sdout_keys: List[str] = ["stdout", "out"],
stderr_keys: List[str] = ["stderr", "err"],
) -> Tuple[Optional[PathLike], Optional[PathLike]]:
"""
If multiple log files are provided, try to infer which one is for stderr.

If only one log file is provided, or inference fails, return None for stdout_file


Returns
-------
tuple
stdout_file, stderr_file


"""

if (log is None) or (len(log) == 0):
return None, None
elif len(log) == 1:
return None, log[0]
elif len(log) > 1:
stdout_file, stderr_file = None, None
# infer stdout and stderr file from log keys
for key in stderr_keys:
if hasattr(log, key):
stderr_file = log[key]

for key in sdout_keys:
if hasattr(log, key):
stdout_file = log[key]

if (stdout_file is None) or (stderr_file is None):
warnings.warn(
"You have more than one log file, but I cannot infer which logfile is stderr and which is stdout,"
f"I use {stderr_file} file for logging."
)
return None, log[0]

else:
return stdout_file, stderr_file


def _log_shell_redirect(
log: Optional[PathLike],
Expand All @@ -122,7 +222,7 @@ def _log_shell_redirect(
"""
Return a shell redirection string to be used in `shell()` calls

This function allows scripts and wrappers support optional `log` files
This function allows scripts and wrappers to support optional `log` files
specified in the calling rule. If no `log` was specified, then an
empty string "" is returned, regardless of the values of `stdout`,
`stderr`, and `append`.
Expand Down
10 changes: 10 additions & 0 deletions tests/test_get_log_twofiles/Snakefile
@@ -0,0 +1,10 @@
rule:
input: "test.in"
output: "test.out"
log:
stdout="test.log",
sterr= "test.err"
wrapper:
'file://wrapper.py'


3 changes: 3 additions & 0 deletions tests/test_get_log_twofiles/environment.yaml
@@ -0,0 +1,3 @@
channels:
- conda-forge
dependencies: []
2 changes: 2 additions & 0 deletions tests/test_get_log_twofiles/expected-results/test.err
@@ -0,0 +1,2 @@
a sterr message from the wrapper
a sterr message
2 changes: 2 additions & 0 deletions tests/test_get_log_twofiles/expected-results/test.log
@@ -0,0 +1,2 @@
a stdout message from the wrapper
a stdout message
2 changes: 2 additions & 0 deletions tests/test_get_log_twofiles/expected-results/test.out
@@ -0,0 +1,2 @@
foo
bar
2 changes: 2 additions & 0 deletions tests/test_get_log_twofiles/test.in
@@ -0,0 +1,2 @@
foo
bar
18 changes: 18 additions & 0 deletions tests/test_get_log_twofiles/wrapper.py
@@ -0,0 +1,18 @@
from snakemake.shell import shell
shell.executable("bash")

import sys

sys.stderr.write("a stderr message from the wrapper\n")
sys.stdout.write("a stdout message from the wrapper\n")

print("A message goes to no log file")

log = snakemake.log_fmt_shell(append=False)
shell(
"""
cat {snakemake.input} > {snakemake.output}
(>&2 echo "a stderr message") {log}
(echo "a stdout message") {log}
"""
)