__authors__ = "Johannes Köster"
__copyright__ = "Copyright 2022, Johannes Köster"
__email__ = "johannes.koester@uni-due.de"
__license__ = "MIT"
from pathlib import Path
import types
import re
from snakemake.common import Rules
from snakemake.exceptions import WorkflowError
from snakemake.path_modifier import PathModifier
from snakemake import wrapper
[docs]
def get_name_modifier_func(rules=None, name_modifier=None, parent_modifier=None):
if name_modifier is None:
return None
else:
if parent_modifier is None:
parent_modifier_func = lambda rulename: rulename
else:
parent_modifier_func = parent_modifier.modify_rulename
if "*" in name_modifier:
return lambda rulename: parent_modifier_func(
name_modifier.replace("*", rulename)
)
elif name_modifier is not None:
if len(rules) > 1:
raise SyntaxError(
"Multiple rules in 'use rule' statement but name modification ('as' statement) does not contain a wildcard '*'."
)
return lambda rulename: parent_modifier_func(name_modifier)
[docs]
class ModuleInfo:
[docs]
def __init__(
self,
workflow,
name,
snakefile=None,
meta_wrapper=None,
config=None,
skip_validation=False,
replace_prefix=None,
prefix=None,
):
self.workflow = workflow
self.name = name
self.snakefile = snakefile
self.meta_wrapper = meta_wrapper
self.config = config
self.skip_validation = skip_validation
self.parent_modifier = self.workflow.modifier
self.rule_proxies = Rules()
if prefix is not None:
if isinstance(prefix, Path):
prefix = str(prefix)
if not isinstance(prefix, str):
raise WorkflowError(
"Prefix definition in module statement must be string or Path."
)
if replace_prefix is not None:
raise WorkflowError(
"Module definition contains both prefix and replace_prefix. "
"Only one at a time is allowed."
)
self.replace_prefix = replace_prefix
self.prefix = prefix
def use_rules(
self,
rules=None,
name_modifier=None,
exclude_rules=None,
ruleinfo=None,
skip_global_report_caption=False,
):
snakefile = self.get_snakefile()
modifier = WorkflowModifier(
self.workflow,
config=self.config,
base_snakefile=snakefile,
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),
resolved_rulename_modifier=get_name_modifier_func(
rules, name_modifier, parent_modifier=self.parent_modifier
),
local_rulename_modifier=get_name_modifier_func(rules, name_modifier),
ruleinfo_overwrite=ruleinfo,
allow_rule_overwrite=True,
namespace=self.name,
replace_prefix=self.replace_prefix,
prefix=self.prefix,
replace_wrapper_tag=self.get_wrapper_tag(),
rule_proxies=self.rule_proxies,
)
with modifier:
self.workflow.include(snakefile, overwrite_default_target=True)
self.parent_modifier.inherit_rule_proxies(modifier)
def get_snakefile(self):
if self.meta_wrapper:
return wrapper.get_path(
self.meta_wrapper + "/test/Snakefile",
self.workflow.workflow_settings.wrapper_prefix,
)
elif self.snakefile:
return self.snakefile
else:
raise WorkflowError(
"Module statement must either define snakefile or meta_wrapper to use."
)
def get_wrapper_tag(self):
if self.meta_wrapper:
if wrapper.is_url(self.meta_wrapper):
raise WorkflowError(
"meta_wrapper directive of module statement currently does not support full URLs."
)
return self.meta_wrapper.split("/", 1)[0]
return None
def get_rule_whitelist(self, rules):
if "*" in rules:
if len(rules) != 1:
raise SyntaxError(
"The 'use rule' statement uses a wildcard '*' but lists multiple additional rules."
)
else:
return None
return set(rules)
[docs]
class WorkflowModifier:
[docs]
def __init__(
self,
workflow,
parent_modifier=None,
globals=None,
config=None,
base_snakefile=None,
skip_configfile=False,
skip_validation=False,
skip_global_report_caption=False,
resolved_rulename_modifier=None,
local_rulename_modifier=None,
rule_whitelist=None,
rule_exclude_list=None,
ruleinfo_overwrite=None,
allow_rule_overwrite=False,
replace_prefix=None,
prefix=None,
replace_wrapper_tag=None,
namespace=None,
rule_proxies=None,
):
if parent_modifier is not None:
# init with values from parent modifier
self.base_snakefile = parent_modifier.base_snakefile
self.globals = parent_modifier.globals
self.skip_configfile = parent_modifier.skip_configfile
self.resolved_rulename_modifier = parent_modifier.resolved_rulename_modifier
self.local_rulename_modifier = parent_modifier.local_rulename_modifier
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
self.replace_wrapper_tag = parent_modifier.replace_wrapper_tag
self.namespace = parent_modifier.namespace
self.wildcard_constraints = parent_modifier.wildcard_constraints
self.rules = parent_modifier.rules
self.rule_proxies = parent_modifier.rule_proxies
else:
# default settings for globals if not inheriting from parent
self.globals = (
globals if globals is not None else dict(workflow.vanilla_globals)
)
self.wildcard_constraints = dict()
self.rules = set()
self.rule_proxies = rule_proxies or Rules()
self.globals["rules"] = self.rule_proxies
self.workflow = workflow
self.base_snakefile = base_snakefile
if config is not None:
self.globals["config"] = config
self.skip_configfile = skip_configfile
self.resolved_rulename_modifier = resolved_rulename_modifier
self.local_rulename_modifier = local_rulename_modifier
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 inherit_rule_proxies(self, child_modifier):
for name, rule in child_modifier.rule_proxies._rules.items():
if child_modifier.local_rulename_modifier is not None:
name = child_modifier.local_rulename_modifier(name)
self.rule_proxies._register_rule(name, rule)
def skip_rule(self, rulename):
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.resolved_rulename_modifier is not None:
return self.resolved_rulename_modifier(rulename)
return rulename
def modify_path(self, path, property=None):
return self.path_modifier.modify(path, property)
def modify_wrapper_uri(self, wrapper_uri, pattern=re.compile("^master/")):
if self.replace_wrapper_tag is None or wrapper.is_url(wrapper_uri):
return wrapper_uri
else:
return pattern.sub(self.replace_wrapper_tag + "/", wrapper_uri)
def __enter__(self):
# put this modifier on the stack, it becomes the currently valid modifier
self.workflow.modifier_stack.append(self)
def __exit__(self, type, value, traceback):
# remove this modifier from the stack
self.workflow.modifier_stack.pop()
if self.namespace:
namespace = types.ModuleType(self.namespace)
namespace.__dict__.update(self.globals)
self.workflow.globals[self.namespace] = namespace