Skip to content

Commit 2baf2cd

Browse files
Merge branch 'main' into feature_localtemp
2 parents 1c6e694 + 5da13be commit 2baf2cd

File tree

7 files changed

+104
-10
lines changed

7 files changed

+104
-10
lines changed

apidocs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
# If extensions (or modules to document with autodoc) are in another directory,
2323
# add these directories to sys.path here. If the directory is relative to the
2424
# documentation root, use os.path.abspath to make it absolute, like shown here.
25-
sys.path.insert(0, os.path.abspath("../"))
25+
sys.path.insert(0, os.path.abspath("../src/"))
2626

2727
# -- General configuration ------------------------------------------------
2828

docs/conf.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
# If extensions (or modules to document with autodoc) are in another directory,
2323
# add these directories to sys.path here. If the directory is relative to the
2424
# documentation root, use os.path.abspath to make it absolute, like shown here.
25-
sys.path.insert(0, os.path.abspath("../"))
26-
25+
sys.path.insert(0, os.path.abspath("../src/"))
2726
# -- General configuration ------------------------------------------------
2827

2928
# If your documentation needs a minimal Sphinx version, state it here.

src/snakemake/cli.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
StorageSettings,
6969
WorkflowSettings,
7070
StrictDagEvaluation,
71+
PrintDag,
7172
)
7273
from snakemake.target_jobs import parse_target_jobs_cli_args
7374
from snakemake.utils import available_cpu_count, update_config
@@ -1045,20 +1046,26 @@ def get_argument_parser(profiles=None):
10451046
action="store_true",
10461047
help="Show available target rules in given Snakefile.",
10471048
)
1048-
group_utils.add_argument(
1049+
group_exec.add_argument(
10491050
"--dag",
1050-
action="store_true",
1051+
nargs="?",
1052+
choices=PrintDag.choices(),
1053+
const=str(PrintDag.DOT),
1054+
default=None,
10511055
help="Do not execute anything and print the directed "
1052-
"acyclic graph of jobs in the dot language. Recommended "
1056+
"acyclic graph of jobs in the dot language or in mermaid-js. Recommended "
10531057
"use on Unix systems: snakemake `--dag | dot | display`. "
10541058
"Note print statements in your Snakefile may interfere "
10551059
"with visualization.",
10561060
)
10571061
group_utils.add_argument(
10581062
"--rulegraph",
1059-
action="store_true",
1063+
nargs="?",
1064+
choices=PrintDag.choices(),
1065+
const=str(PrintDag.DOT),
1066+
default=None,
10601067
help="Do not execute anything and print the dependency graph "
1061-
"of rules in the dot language. This will be less "
1068+
"of rules in the dot language or in mermaid-js. This will be less "
10621069
"crowded than above DAG of jobs, but also show less information. "
10631070
"Note that each rule is displayed once, hence the displayed graph will be "
10641071
"cyclic if a rule appears in several steps of the workflow. "
@@ -2025,6 +2032,13 @@ def args_to_api(args, parser):
20252032
elif args.print_compilation:
20262033
workflow_api.print_compilation()
20272034
else:
2035+
2036+
print_dag_as = None
2037+
if args.dag:
2038+
print_dag_as = args.dag
2039+
elif args.rulegraph:
2040+
print_dag_as = args.rulegraph
2041+
20282042
dag_api = workflow_api.dag(
20292043
dag_settings=DAGSettings(
20302044
targets=args.targets,
@@ -2042,6 +2056,7 @@ def args_to_api(args, parser):
20422056
max_inventory_wait_time=args.max_inventory_time,
20432057
max_checksum_file_size=args.max_checksum_file_size,
20442058
strict_evaluation=args.strict_dag_evaluation,
2059+
print_dag_as=print_dag_as,
20452060
),
20462061
)
20472062

src/snakemake/dag.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
WorkflowError,
5656
print_exception_warning,
5757
)
58+
from snakemake.settings.types import PrintDag
5859
from snakemake.io import (
5960
_IOFile,
6061
PeriodicityDetector,
@@ -2382,7 +2383,10 @@ def rule_dot(self):
23822383
graph = defaultdict(set)
23832384
for job in self.jobs:
23842385
graph[job.rule].update(dep.rule for dep in self._dependencies[job])
2385-
return self._dot(graph)
2386+
if self.workflow.dag_settings.print_dag_as == str(PrintDag.DOT):
2387+
return self._dot(graph)
2388+
elif self.workflow.dag_settings.print_dag_as == str(PrintDag.MERMAID_JS):
2389+
return self._mermaid_js(graph)
23862390

23872391
def dot(self):
23882392
def node2style(job):
@@ -2407,6 +2411,70 @@ def format_wildcard(wildcard):
24072411
dag, node2rule=node2rule, node2style=node2style, node2label=node2label
24082412
)
24092413

2414+
def mermaid_js(self):
2415+
def node2style(job):
2416+
if not self.needrun(job):
2417+
return ",stroke-dasharray: 5 5"
2418+
return ""
2419+
2420+
def format_wildcard(wildcard):
2421+
name, value = wildcard
2422+
return f"{name}: {value}"
2423+
2424+
node2label = lambda job: " - ".join(
2425+
chain(
2426+
[job.rule.name], sorted(map(format_wildcard, self.new_wildcards(job)))
2427+
)
2428+
)
2429+
2430+
dag = {job: self._dependencies[job] for job in self.jobs}
2431+
2432+
return self._mermaid_js(dag, node2style=node2style, node2label=node2label)
2433+
2434+
def _mermaid_js(
2435+
self, graph, node2style=lambda node: "", node2label=lambda node: node
2436+
):
2437+
def hsv_to_htmlhexrgb(h, s, v):
2438+
import colorsys
2439+
2440+
hex_r, hex_g, hex_b = (round(255 * x) for x in colorsys.hsv_to_rgb(h, s, v))
2441+
return "#{hex_r:0>2X}{hex_g:0>2X}{hex_b:0>2X}".format(
2442+
hex_r=hex_r, hex_g=hex_g, hex_b=hex_b
2443+
)
2444+
2445+
# color the rules - sorting by name first gives deterministic output
2446+
rules = sorted(self.rules, key=lambda r: r.name)
2447+
huefactor = 2 / (3 * len(rules))
2448+
rulecolor = {
2449+
rule.name: hsv_to_htmlhexrgb(i * huefactor, 0.6, 0.85)
2450+
for i, rule in enumerate(rules)
2451+
}
2452+
nodes_headers = [
2453+
f"\tid{index}[{node2label(node)}]" for index, node in enumerate(graph)
2454+
]
2455+
nodes_styles = [
2456+
f"\tstyle id{index} fill:{rulecolor[str(node)]},stroke-width:2px,color:#333333{node2style(node)}"
2457+
for index, node in enumerate(graph)
2458+
]
2459+
edges = [
2460+
f"\tid{index} --> id{index_dep}"
2461+
for index, (_, deps) in enumerate(graph.items())
2462+
for index_dep, _ in enumerate(deps)
2463+
]
2464+
return (
2465+
textwrap.dedent(
2466+
"""\
2467+
---
2468+
title: DAG
2469+
---
2470+
flowchart TB
2471+
"""
2472+
)
2473+
+ "{}\n{}\n{}".format(
2474+
"\n".join(nodes_headers), "\n".join(nodes_styles), "\n".join(edges)
2475+
)
2476+
)
2477+
24102478
def _dot(
24112479
self,
24122480
graph,
@@ -3021,7 +3089,10 @@ def norm_rule_relpath(f, rule):
30213089
return files
30223090

30233091
def __str__(self):
3024-
return self.dot()
3092+
if self.workflow.dag_settings.print_dag_as == str(PrintDag.DOT):
3093+
return self.dot()
3094+
if self.workflow.dag_settings.print_dag_as == str(PrintDag.MERMAID_JS):
3095+
return self.mermaid_js()
30253096

30263097
def __len__(self):
30273098
return self._len

src/snakemake/settings/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ class StrictDagEvaluation(SettingsEnumBase):
3333
FUNCTIONS = 0
3434
CYCLIC_GRAPH = 1
3535
PERIODIC_WILDCARDS = 2
36+
37+
38+
class PrintDag(SettingsEnumBase):
39+
DOT = 0
40+
MERMAID_JS = 1

src/snakemake/settings/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
CondaCleanupPkgs,
4040
Quietness,
4141
StrictDagEvaluation,
42+
PrintDag,
4243
)
4344

4445

@@ -209,6 +210,7 @@ class DAGSettings(SettingsBase):
209210
max_inventory_wait_time: int = 20
210211
max_checksum_file_size: int = 1000000
211212
strict_evaluation: AnySet[StrictDagEvaluation] = frozenset()
213+
print_dag_as: PrintDag = PrintDag.DOT
212214
# strict_functions_evaluation: bool = False
213215
# strict_cycle_evaluation: bool = False
214216
# strict_wildcards_recursion_evaluation: bool = False

src/snakemake/workflow.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,8 @@ def execute(
12631263
and self.exec_mode == ExecMode.DEFAULT
12641264
and self.remote_execution_settings.job_deploy_sources
12651265
and not executor_plugin.common_settings.can_transfer_local_files
1266+
and not self.dryrun
1267+
and len(self.dag)
12661268
)
12671269
if should_deploy_sources:
12681270
# no shared FS, hence we have to upload the sources to the storage

0 commit comments

Comments
 (0)