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: make it possible to exclude rules that will be imported when using 'use rule' statement #1717

Merged
merged 6 commits into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/snakefiles/modularization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ With Snakemake 6.0 and later, it is possible to define external workflows as mod
# here, plain paths, URLs and the special markers for code hosting providers (see below) are possible.
"other_workflow/Snakefile"

use rule * from other_workflow as other_*
use rule * from other_workflow exclude ruleC as other_*

The ``module other_workflow:`` statement registers the external workflow as a module, by defining the path to the main snakefile of ``other_workflow``.
Here, plain paths, HTTP/HTTPS URLs and special markers for code hosting providers like Github or Gitlab are possible (see :ref:`snakefile-code-hosting-providers`).
The second statement, ``use rule * from other_workflow as other_*``, declares all rules of that module to be used in the current one.
The second statement, ``use rule * from other_workflow exclude ruleC as other_*``, declares all rules of that module to be used in the current one, except for ruleC.
Thereby, the ``as other_*`` at the end renames all those rules with a common prefix.
This can be handy to avoid rule name conflicts (note that rules from modules can otherwise overwrite rules from your current workflow or other modules).

Expand Down
8 changes: 7 additions & 1 deletion snakemake/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def use_rules(
self,
rules=None,
name_modifier=None,
exclude_rules=None,
ruleinfo=None,
skip_global_report_caption=False,
):
Expand All @@ -84,6 +85,7 @@ def use_rules(
skip_configfile=self.config is not None,
skip_validation=self.skip_validation,
skip_global_report_caption=skip_global_report_caption,
rule_exclude_list=exclude_rules,
rule_whitelist=self.get_rule_whitelist(rules),
rulename_modifier=get_name_modifier_func(rules, name_modifier),
ruleinfo_overwrite=ruleinfo,
Expand Down Expand Up @@ -140,6 +142,7 @@ def __init__(
skip_global_report_caption=False,
rulename_modifier=None,
rule_whitelist=None,
rule_exclude_list=None,
ruleinfo_overwrite=None,
allow_rule_overwrite=False,
replace_prefix=None,
Expand All @@ -156,6 +159,7 @@ def __init__(
self.skip_validation = parent_modifier.skip_validation
self.skip_global_report_caption = parent_modifier.skip_global_report_caption
self.rule_whitelist = parent_modifier.rule_whitelist
self.rule_exclude_list = parent_modifier.rule_exclude_list
self.ruleinfo_overwrite = parent_modifier.ruleinfo_overwrite
self.allow_rule_overwrite = parent_modifier.allow_rule_overwrite
self.path_modifier = parent_modifier.path_modifier
Expand All @@ -178,14 +182,16 @@ def __init__(
self.skip_validation = skip_validation
self.skip_global_report_caption = skip_global_report_caption
self.rule_whitelist = rule_whitelist
self.rule_exclude_list = rule_exclude_list
self.ruleinfo_overwrite = ruleinfo_overwrite
self.allow_rule_overwrite = allow_rule_overwrite
self.path_modifier = PathModifier(replace_prefix, prefix, workflow)
self.replace_wrapper_tag = replace_wrapper_tag
self.namespace = namespace

def skip_rule(self, rulename):
return self.rule_whitelist is not None and rulename not in self.rule_whitelist
return (self.rule_whitelist is not None and rulename not in self.rule_whitelist) or \
(self.rule_exclude_list is not None and rulename in self.rule_exclude_list)

def modify_rulename(self, rulename):
if self.rulename_modifier is not None:
Expand Down
39 changes: 37 additions & 2 deletions snakemake/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@ def __init__(self, snakefile, base_indent=0, dedent=0, root=True):
super().__init__(snakefile, base_indent=base_indent, dedent=dedent, root=root)
self.state = self.state_keyword_rule
self.rules = []
self.exclude_rules = []
self.has_with = False
self.name_modifier = []
self.from_module = None
Expand All @@ -955,8 +956,8 @@ def __init__(self, snakefile, base_indent=0, dedent=0, root=True):

def end(self):
name_modifier = "".join(self.name_modifier) if self.name_modifier else None
yield "@workflow.userule(rules={!r}, from_module={!r}, name_modifier={!r}, lineno={})".format(
self.rules, self.from_module, name_modifier, self.lineno
yield "@workflow.userule(rules={!r}, from_module={!r}, exclude_rules={!r}, name_modifier={!r}, lineno={})".format(
self.rules, self.from_module, self.exclude_rules, name_modifier, self.lineno
)
yield "\n"

Expand Down Expand Up @@ -1060,6 +1061,9 @@ def state_modifier(self, token):
if token.string == "as" and not self.name_modifier:
self.state = self.state_as
yield from ()
elif token.string == "exclude":
self.state = self.state_exclude
yield from ()
elif token.string == "with":
yield from self.handle_with(token)
else:
Expand Down Expand Up @@ -1114,6 +1118,37 @@ def state_with(self, token):
"Expecting colon after 'with' keyword in 'use rule' statement.", token
)

def state_exclude(self, token):
if is_name(token):
self.exclude_rules.append(token.string)
self.state = self.state_exclude_comma_or_end
yield from ()
else:
self.error(
"Expecting rule name(s) after 'exclude' keyword in 'use rule' statement.", token
)

def state_exclude_comma_or_end(self, token):
if is_name(token):
if token.string == "from" or token.string == "as":
if not self.exclude_rules:
self.error(
"Expecting rule names after 'exclude' statement.", token
)
if token.string == "from":
self.state = self.state_from
else:
self.state = self.state_as
yield from ()
else:
yield from ()
elif is_comma(token):
self.state = self.state_exclude
yield from ()
else:
self.state = self.state_modifier
yield from ()

def block_content(self, token):
if is_comment(token):
yield "\n", token
Expand Down
3 changes: 2 additions & 1 deletion snakemake/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1922,7 +1922,7 @@ def module(
prefix=prefix,
)

def userule(self, rules=None, from_module=None, name_modifier=None, lineno=None):
def userule(self, rules=None, from_module=None, exclude_rules=None, name_modifier=None, lineno=None):
def decorate(maybe_ruleinfo):
if from_module is not None:
try:
Expand All @@ -1936,6 +1936,7 @@ def decorate(maybe_ruleinfo):
module.use_rules(
rules,
name_modifier,
exclude_rules=exclude_rules,
ruleinfo=None if callable(maybe_ruleinfo) else maybe_ruleinfo,
skip_global_report_caption=self.report_text
is not None, # do not overwrite existing report text via module
Expand Down
17 changes: 17 additions & 0 deletions tests/test_modules_all_exclude/Snakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
shell.executable("bash")

configfile: "config/config.yaml"


module test:
snakefile:
"module-test/Snakefile"
config:
config
replace_prefix:
{"results/": "results/testmodule/"}


use rule * from test as test_*

assert test.some_func() == 15
17 changes: 17 additions & 0 deletions tests/test_modules_all_exclude/Snakefile_exclude
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
shell.executable("bash")

configfile: "config/config.yaml"


module test:
snakefile:
"module-test/Snakefile"
config:
config
replace_prefix:
{"results/": "results/testmodule/"}


use rule * from test exclude b, d

assert test.some_func() == 15
4 changes: 4 additions & 0 deletions tests/test_modules_all_exclude/config/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test: 1
testb: 2
testc: 3
testd: 4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3
35 changes: 35 additions & 0 deletions tests/test_modules_all_exclude/module-test/Snakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
configfile: "config.yaml" # does not exist, but this statement should be ignored on module import


def some_func():
return 15


rule all:
input:
"results/test.out", "results/test2.out"


rule a:
output:
"results/test.out"
shell:
"echo {config[test]} > {output}"

rule b:
output:
"results/test2.out"
shell:
"echo {config[testb]} > {output}"

rule c:
output:
"results/test2.out"
shell:
"echo {config[testc]} > {output}"

rule d:
output:
"results/test2.out"
shell:
"echo {config[testc]} > {output}"
10 changes: 10 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,16 @@ def test_modules_all():
run(dpath("test_modules_all"), targets=["a"])


def test_modules_all_exclude_1():
# Fail due to conflicting rules
run(dpath("test_modules_all_exclude"), shouldfail=True)


def test_modules_all_exclude_2():
# Successed since the conflicting rules have been excluded
run(dpath("test_modules_all_exclude"), snakefile="Snakefile_exclude", shouldfail=False)


@skip_on_windows
def test_modules_prefix():
run(dpath("test_modules_prefix"), targets=["a"])
Expand Down