Skip to content

Commit ea0782f

Browse files
committed
fix: Restore python rules changes triggering reruns. (GH: #3213)
1 parent 216efef commit ea0782f

File tree

3 files changed

+31
-4
lines changed

3 files changed

+31
-4
lines changed

src/snakemake/persistence.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,11 +553,13 @@ def _b64id(self, s):
553553

554554
@lru_cache()
555555
def _code(self, rule):
556-
# We only consider shell commands for now.
557-
# Plain python code rules are hard to capture because the pickling of the code
558-
# can change with different python versions.
559556
# Scripts and notebooks are triggered by changes in the script mtime.
560-
return rule.shellcmd if rule.shellcmd is not None else None
557+
# Changes to python and shell rules are triggered by changes in the plain text.
558+
if rule.shellcmd is not None:
559+
return rule.shellcmd
560+
if rule.run_func_src is not None:
561+
return rule.run_func_src
562+
return None
561563

562564
@lru_cache()
563565
def _conda_env(self, job):

src/snakemake/rules.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def __init__(self, name, workflow, lineno=None, snakefile=None):
107107
self._lineno = lineno
108108
self._snakefile = snakefile
109109
self.run_func = None
110+
self.run_func_src = None
110111
self.shellcmd = None
111112
self.script = None
112113
self.notebook = None

src/snakemake/workflow.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import subprocess
1111
import sys
1212
import platform
13+
import dis
14+
import linecache
1315
from collections import OrderedDict, namedtuple
1416
from collections.abc import Mapping
1517
from itertools import filterfalse, chain
@@ -1712,6 +1714,26 @@ def ruleorder(self, *rulenames):
17121714
def localrules(self, *rulenames):
17131715
self._localrules.update(rulenames)
17141716

1717+
def get_rule_source(self, func):
1718+
# This can't use `inspect` because the functions are compiled into intermediate python code
1719+
# in parser.py and that intermediate source is not available anymore (or desirable).
1720+
# Instead, we're using dis to retrieve the line numbers of the function in the intermediate
1721+
# code and then map it to the original file using `self.linemaps`.
1722+
sourcefile = func.__code__.co_filename
1723+
line_numbers = []
1724+
linemap = self.linemaps[sourcefile]
1725+
for func_offset, line in dis.findlinestarts(func.__code__):
1726+
# The first instruction in the compiled function is RESUME, which
1727+
# with snakemake is mapped to the 'rule: ' line and is not considered
1728+
# part of the rule source.
1729+
if func_offset == 0:
1730+
continue
1731+
if line in linemap:
1732+
line_numbers.append(linemap[line])
1733+
return "".join(
1734+
[linecache.getline(sourcefile, lineno) for lineno in sorted(line_numbers)]
1735+
)
1736+
17151737
def rule(self, name=None, lineno=None, snakefile=None, checkpoint=False):
17161738
# choose a name for an unnamed rule
17171739
if name is None:
@@ -1930,6 +1952,8 @@ def check_may_use_software_deployment(method):
19301952
rule.name = ruleinfo.name
19311953
rule.docstring = ruleinfo.docstring
19321954
rule.run_func = ruleinfo.func
1955+
if rule.run_func is not None:
1956+
rule.run_func_src = self.get_rule_source(rule.run_func)
19331957
rule.shellcmd = ruleinfo.shellcmd
19341958
rule.script = ruleinfo.script
19351959
rule.notebook = ruleinfo.notebook

0 commit comments

Comments
 (0)