From 8d64af2cb905fef95585055c7b69fd1c45d44108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20K=C3=B6ster?= Date: Thu, 3 Mar 2022 12:08:09 +0100 Subject: [PATCH] fix: more details on input and output exceptions (missing input, protected output, etc.) (#1453) --- snakemake/dag.py | 4 ++-- snakemake/exceptions.py | 47 ++++++++++++++++++----------------------- snakemake/jobs.py | 2 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/snakemake/dag.py b/snakemake/dag.py index a1c066caf..a6e99ca9f 100755 --- a/snakemake/dag.py +++ b/snakemake/dag.py @@ -516,7 +516,7 @@ def check_and_touch_output( if (f.is_directory and not f.remote_object and not os.path.isdir(f)) or ( not f.remote_object and os.path.isdir(f) and not f.is_directory ): - raise ImproperOutputException(job.rule, [f]) + raise ImproperOutputException(job, [f]) # It is possible, due to archive expansion or cluster clock skew, that # the files appear older than the input. But we know they must be new, @@ -921,7 +921,7 @@ def update_( if missing_input: self.delete_job(job, recursive=False) # delete job from tree - raise MissingInputException(job.rule, missing_input) + raise MissingInputException(job, missing_input) if skip_until_dynamic: self._dynamic.add(job) diff --git a/snakemake/exceptions.py b/snakemake/exceptions.py index c753d4556..e8752ce4d 100644 --- a/snakemake/exceptions.py +++ b/snakemake/exceptions.py @@ -245,18 +245,23 @@ def __init__( class IOException(RuleException): - def __init__(self, prefix, rule, files, include=None, lineno=None, snakefile=None): - message = ( - "{} for rule {}:\n{}".format(prefix, rule, "\n".join(files)) - if files - else "" - ) + def __init__(self, prefix, job, files, include=None, lineno=None, snakefile=None): + from snakemake.logging import format_wildcards + + msg = "" + if files: + msg = f"{prefix} for rule {job.rule}:" + if job.output: + msg += "\n" + f" output: {', '.join(job.output)}" + if job.wildcards: + msg += "\n" + f" wildcards: {format_wildcards(job.wildcards)}" + msg += "\n affected files:\n " + "\n ".join(files) super().__init__( - message=message, + message=msg, include=include, lineno=lineno, snakefile=snakefile, - rule=rule, + rule=job.rule, ) @@ -277,8 +282,9 @@ def __init__( class MissingInputException(IOException): - def __init__(self, rule, files, include=None, lineno=None, snakefile=None): + def __init__(self, job, files, include=None, lineno=None, snakefile=None): msg = "Missing input files" + if any(map(lambda f: f.startswith("~"), files)): msg += ( "(Using '~' in your paths is not allowed as such platform " @@ -286,7 +292,7 @@ def __init__(self, rule, files, include=None, lineno=None, snakefile=None): "try sticking to relative paths for everything inside the " "working directory.)" ) - super().__init__(msg, rule, files, include, lineno=lineno, snakefile=snakefile) + super().__init__(msg, job, files, include, lineno=lineno, snakefile=snakefile) class PeriodicWildcardError(RuleException): @@ -294,10 +300,10 @@ class PeriodicWildcardError(RuleException): class ProtectedOutputException(IOException): - def __init__(self, rule, files, include=None, lineno=None, snakefile=None): + def __init__(self, job, files, include=None, lineno=None, snakefile=None): super().__init__( "Write-protected output files", - rule, + job, files, include, lineno=lineno, @@ -306,24 +312,11 @@ def __init__(self, rule, files, include=None, lineno=None, snakefile=None): class ImproperOutputException(IOException): - def __init__(self, rule, files, include=None, lineno=None, snakefile=None): + def __init__(self, job, files, include=None, lineno=None, snakefile=None): super().__init__( "Outputs of incorrect type (directories when expecting files or vice versa). " "Output directories must be flagged with directory().", - rule, - files, - include, - lineno=lineno, - snakefile=snakefile, - ) - - -class UnexpectedOutputException(IOException): - def __init__(self, rule, files, include=None, lineno=None, snakefile=None): - super().__init__( - "Unexpectedly present output files " - "(accidentally created by other rule?)", - rule, + job, files, include, lineno=lineno, diff --git a/snakemake/jobs.py b/snakemake/jobs.py index 786a4301c..d8a36a7e6 100644 --- a/snakemake/jobs.py +++ b/snakemake/jobs.py @@ -703,7 +703,7 @@ def existing_output(self): def check_protected_output(self): protected = list(filter(lambda f: f.protected, self.expanded_output)) if protected: - raise ProtectedOutputException(self.rule, protected) + raise ProtectedOutputException(self, protected) def remove_existing_output(self): """Clean up both dynamic and regular output before rules actually run"""