diff --git a/docs/snakefiles/rules.rst b/docs/snakefiles/rules.rst index 3b22e815c..80efe2175 100644 --- a/docs/snakefiles/rules.rst +++ b/docs/snakefiles/rules.rst @@ -2009,10 +2009,24 @@ Consider the following example: "jinja2" Here, Snakemake will automatically use the specified template engine `Jinja2 `_ to render the template given as input file into the given output file. -Template rendering rules may only have a single input and output file. The template_engine instruction has to be specified at the end of the rule. +Template rendering rules may only have a single output file. +If the rule needs more than one input file, there has to be one input file called ``template``, pointing to the main template to be used for the rendering: -The template itself has access to ``params``, ``wildcards``, and ``config``, +.. code-block:: python + + rule render_jinja2_template: + input: + template="some-jinja2-template.txt", + other_file="some-other-input-file-used-by-the-template.txt" + output: + "results/{sample}.rendered-version.txt" + params: + foo=0.1 + template_engine: + "jinja2" + +The template itself has access to ``input``, ``params``, ``wildcards``, and ``config``, which are the same objects you can use for example in the ``shell`` or ``run`` directive, and the same objects as can be accessed from ``script`` or ``notebook`` directives (but in the latter two cases they are stored behind the ``snakemake`` object which serves as a dedicated namespace to avoid name clashes). diff --git a/snakemake/parser.py b/snakemake/parser.py index 521d3c8c0..31c3a010f 100644 --- a/snakemake/parser.py +++ b/snakemake/parser.py @@ -654,7 +654,7 @@ class TemplateEngine(Script): end_func = "render_template" def args(self): - yield (", input, output, params, wildcards, config") + yield (", input, output, params, wildcards, config, rule") class CWL(Script): diff --git a/snakemake/report/__init__.py b/snakemake/report/__init__.py index 67ad901c8..08d8482ee 100644 --- a/snakemake/report/__init__.py +++ b/snakemake/report/__init__.py @@ -473,7 +473,7 @@ def render(self, env, rst_links, categories, files): job.threads, job.resources, job.log, - job.dag.workflow.config, + job.rule.module_globals["config"], job.rule.name, None, ) diff --git a/snakemake/rules.py b/snakemake/rules.py index 7e57d6778..4d7ef7bea 100644 --- a/snakemake/rules.py +++ b/snakemake/rules.py @@ -124,6 +124,7 @@ def __init__(self, *args, lineno=None, snakefile=None, restart_times=0): self.log_modifier = None self.benchmark_modifier = None self.ruleinfo = None + self.module_globals = None elif len(args) == 1: other = args[0] self.name = other.name @@ -177,6 +178,7 @@ def __init__(self, *args, lineno=None, snakefile=None, restart_times=0): self.log_modifier = other.log_modifier self.benchmark_modifier = other.benchmark_modifier self.ruleinfo = other.ruleinfo + self.module_globals = other.module_globals def dynamic_branch(self, wildcards, input=True): def get_io(rule): diff --git a/snakemake/template_rendering/__init__.py b/snakemake/template_rendering/__init__.py index a16672edc..f0ee37036 100644 --- a/snakemake/template_rendering/__init__.py +++ b/snakemake/template_rendering/__init__.py @@ -1,12 +1,26 @@ from abc import ABC, abstractmethod +from snakemake.exceptions import WorkflowError + class TemplateRenderer(ABC): def __init__(self, input, output, params, wildcards, config): - assert len(input) == 1 - assert len(output) == 1 - - self.input_file = input[0] + if len(output) != 1: + raise ValueError( + "More than one output file specified for template_engine rule." + ) + if len(input) != 1: + if "template" not in input.keys(): + raise ValueError( + "More than one input file specified for template engine rule, but no " + "input file named as 'template'." + ) + else: + self.input_file = input.template + else: + self.input_file = input[0] + + self.input = input self.output_file = output[0] self.params = params self.wildcards = wildcards @@ -18,6 +32,7 @@ def variables(self): "params": self.params, "wildcards": self.wildcards, "config": self.config, + "input": self.input, } @abstractmethod @@ -25,17 +40,20 @@ def render(self): ... -def render_template(engine, input, output, params, wildcards, config): - if engine == "yte": - from snakemake.template_rendering.yte import YteRenderer +def render_template(engine, input, output, params, wildcards, config, rule): + try: + if engine == "yte": + from snakemake.template_rendering.yte import YteRenderer - return YteRenderer(input, output, params, wildcards, config).render() - elif engine == "jinja2": - from snakemake.template_rendering.jinja2 import Jinja2Renderer + return YteRenderer(input, output, params, wildcards, config).render() + elif engine == "jinja2": + from snakemake.template_rendering.jinja2 import Jinja2Renderer - return Jinja2Renderer(input, output, params, wildcards, config).render() - else: - raise WorkflowError( - f"Unsupported template engine {engine}. " - "So far, only yte and jinja2 are supported." - ) + return Jinja2Renderer(input, output, params, wildcards, config).render() + else: + raise WorkflowError( + f"Unsupported template engine {engine} in rule {rule}. " + "So far, only yte and jinja2 are supported." + ) + except Exception as e: + raise WorkflowError(f"Error rendering template in rule {rule}.", e) diff --git a/snakemake/workflow.py b/snakemake/workflow.py index cf6bad0bd..4321d6375 100644 --- a/snakemake/workflow.py +++ b/snakemake/workflow.py @@ -1391,6 +1391,7 @@ def decorate(ruleinfo): ) rule = self.get_rule(name) rule.is_checkpoint = checkpoint + rule.module_globals = self.modifier.globals def decorate(ruleinfo): nonlocal name