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

fix: support multiple input files for template_engine rules #1571

Merged
merged 4 commits into from Apr 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 16 additions & 2 deletions docs/snakefiles/rules.rst
Expand Up @@ -2009,10 +2009,24 @@ Consider the following example:
"jinja2"

Here, Snakemake will automatically use the specified template engine `Jinja2 <https://jinja.palletsprojects.com/>`_ 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).

Expand Down
2 changes: 1 addition & 1 deletion snakemake/parser.py
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion snakemake/report/__init__.py
Expand Up @@ -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,
)
Expand Down
2 changes: 2 additions & 0 deletions snakemake/rules.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
50 changes: 34 additions & 16 deletions 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
Expand All @@ -18,24 +32,28 @@ def variables(self):
"params": self.params,
"wildcards": self.wildcards,
"config": self.config,
"input": self.input,
}

@abstractmethod
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)
1 change: 1 addition & 0 deletions snakemake/workflow.py
Expand Up @@ -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
Expand Down