Skip to content

Commit

Permalink
feat: support for report plugins (#2700)
Browse files Browse the repository at this point in the history
### Description

<!--Add a description of your PR here-->

### QC
<!-- Make sure that you can tick the boxes below. -->

* [x] The PR contains a test case for the changes or the changes are
already covered by an existing test case.
* [ ] The documentation (`docs/`) is updated to reflect the changes or
this is not necessary (e.g. if the change does neither modify the
language nor the behavior or functionalities of Snakemake).

---------

Co-authored-by: fxwiegand <fxwiegand@wgdnet.de>
  • Loading branch information
johanneskoester and fxwiegand committed Feb 21, 2024
1 parent 0c9f681 commit 2f7d4b5
Show file tree
Hide file tree
Showing 54 changed files with 784 additions and 503 deletions.
20 changes: 19 additions & 1 deletion .github/workflows/main.yml
Expand Up @@ -98,6 +98,24 @@ jobs:
tests/test_executor_test_suite.py \
tests/test_api.py
- name: Run tests/test_report/Snakefile to generate report
shell: bash -el {0}
run: |
cd tests/test_report
snakemake --use-conda --cores 1 --report report.zip
- name: List directory structure
run: |
ls -R
- name: Upload report
if: ${{ matrix.test_group == 1 }}
uses: actions/upload-artifact@v4
with:
name: report-${{ matrix.py_ver }}.zip
path: tests/test_report/report.zip


build-container-image:
runs-on: ubuntu-latest
needs: testing
Expand Down Expand Up @@ -144,4 +162,4 @@ jobs:
CI: true
ZENODO_SANDBOX_PAT: "${{ secrets.ZENODO_SANDBOX_PAT }}"
run: |
python -m pytest --show-capture=stderr -v -x --splits 10 --group ${{ matrix.test_group }} --splitting-algorithm=least_duration tests/tests.py
python -m pytest --show-capture=stderr -v -x --splits 10 --group ${{ matrix.test_group }} --splitting-algorithm=least_duration tests/tests.py
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -54,6 +54,7 @@ install_requires =
snakemake-interface-executor-plugins >=8.1.3,<9.0
snakemake-interface-common >=1.17.0,<2.0
snakemake-interface-storage-plugins >=3.1.0,<4.0
snakemake-interface-report-plugins >=1.0.0,<2.0.0
stopit
tabulate
throttler
Expand Down
27 changes: 23 additions & 4 deletions snakemake/api.py
Expand Up @@ -44,6 +44,8 @@
from snakemake_interface_common.exceptions import ApiError
from snakemake_interface_storage_plugins.registry import StoragePluginRegistry
from snakemake_interface_common.plugin_registry.plugin import TaggedSettings
from snakemake_interface_report_plugins.settings import ReportSettingsBase
from snakemake_interface_report_plugins.registry import ReportPluginRegistry

from snakemake.workflow import Workflow
from snakemake.exceptions import print_exception
Expand Down Expand Up @@ -616,19 +618,27 @@ def containerize(self):
@_no_exec
def create_report(
self,
path: Path,
stylesheet: Optional[Path] = None,
reporter: str = "html",
report_settings: Optional[ReportSettingsBase] = None,
):
"""Create a report for the workflow.
Arguments
---------
report: Path -- The path to the report.
report_stylesheet: Optional[Path] -- The path to the report stylesheet.
reporter: str -- report plugin to use (default: html)
"""

report_plugin_registry = _get_report_plugin_registry()
report_plugin = report_plugin_registry.get_plugin(reporter)

if report_settings is not None:
report_plugin.validate_settings(report_settings)

self.workflow_api._workflow.create_report(
path=path,
stylesheet=stylesheet,
report_plugin=report_plugin,
report_settings=report_settings,
)

@_no_exec
Expand Down Expand Up @@ -765,3 +775,12 @@ def _get_executor_plugin_registry():
registry.register_plugin("touch", touch_executor)

return registry


def _get_report_plugin_registry():
from snakemake.report import html_reporter

registry = ReportPluginRegistry()
registry.register_plugin("html", html_reporter)

return registry
33 changes: 29 additions & 4 deletions snakemake/cli.py
Expand Up @@ -17,7 +17,12 @@

import snakemake.common.argparse
from snakemake import logging
from snakemake.api import SnakemakeApi, _get_executor_plugin_registry, resolve_snakefile
from snakemake.api import (
SnakemakeApi,
_get_executor_plugin_registry,
_get_report_plugin_registry,
resolve_snakefile,
)
from snakemake.common import (
SNAKEFILE_CHOICES,
__version__,
Expand Down Expand Up @@ -880,6 +885,13 @@ def get_argument_parser(profiles=None):
help="Custom stylesheet to use for report. In particular, this can be used for "
"branding the report with e.g. a custom logo, see docs.",
)
group_report.add_argument(
"--reporter",
metavar="PLUGIN",
help="Specify a custom report plugin. By default, Snakemake's builtin html "
"reporter will be used. For custom reporters, check out their command line "
"options starting with --report-.",
)

group_notebooks = parser.add_argument_group("NOTEBOOKS")

Expand Down Expand Up @@ -1624,6 +1636,7 @@ def get_argument_parser(profiles=None):
# Add namespaced arguments to parser for each plugin
_get_executor_plugin_registry().register_cli_args(parser)
StoragePluginRegistry().register_cli_args(parser)
_get_report_plugin_registry().register_cli_args(parser)
return parser


Expand Down Expand Up @@ -1780,6 +1793,11 @@ def args_to_api(args, parser):
elif args.executor is None:
args.executor = "local"

if args.report:
args.reporter = "html"
args.report_html_path = args.report
args.report_html_stylesheet_path = args.report_stylesheet

executor_plugin = _get_executor_plugin_registry().get_plugin(args.executor)
executor_settings = executor_plugin.get_settings(args)

Expand All @@ -1788,6 +1806,13 @@ def args_to_api(args, parser):
for name in StoragePluginRegistry().get_registered_plugins()
}

if args.reporter:
report_plugin = _get_report_plugin_registry().get_plugin(args.reporter)
report_settings = report_plugin.get_settings(args)
else:
report_plugin = None
report_settings = None

if args.cores is None:
if executor_plugin.common_settings.local_exec:
# use --jobs as an alias for --cores
Expand Down Expand Up @@ -1931,10 +1956,10 @@ def args_to_api(args, parser):

if args.containerize:
dag_api.containerize()
elif args.report:
elif report_plugin is not None:
dag_api.create_report(
path=args.report,
stylesheet=args.report_stylesheet,
reporter=args.reporter,
report_settings=report_settings,
)
elif args.generate_unit_tests:
dag_api.generate_unit_tests(args.generate_unit_tests)
Expand Down
39 changes: 31 additions & 8 deletions snakemake/common/tests/__init__.py
Expand Up @@ -36,6 +36,7 @@ class TestWorkflowsBase(ABC):
expect_exception = None
omit_tmp = False
latency_wait = 5
create_report = False

@abstractmethod
def get_executor(self) -> str:
Expand Down Expand Up @@ -126,14 +127,20 @@ def run_workflow(self, test_name, tmp_path, deployment_method=frozenset()):

dag_api = workflow_api.dag()

dag_api.execute_workflow(
executor=self.get_executor(),
executor_settings=self.get_executor_settings(),
execution_settings=settings.ExecutionSettings(
latency_wait=self.latency_wait,
),
remote_execution_settings=self.get_remote_execution_settings(),
)
if self.create_report:
dag_api.create_report(
reporter=self.get_reporter(),
report_settings=self.get_report_settings(),
)
else:
dag_api.execute_workflow(
executor=self.get_executor(),
executor_settings=self.get_executor_settings(),
execution_settings=settings.ExecutionSettings(
latency_wait=self.latency_wait,
),
remote_execution_settings=self.get_remote_execution_settings(),
)

@handle_testcase
def test_simple_workflow(self, tmp_path):
Expand All @@ -150,6 +157,12 @@ def _common_settings(self):
registry = ExecutorPluginRegistry()
return registry.get_plugin(self.get_executor()).common_settings

def get_reporter(self):
raise NotImplementedError()

def get_report_settings(self):
raise NotImplementedError()


class TestWorkflowsLocalStorageBase(TestWorkflowsBase):
def get_default_storage_provider(self) -> Optional[str]:
Expand Down Expand Up @@ -216,3 +229,13 @@ def access_key(self):
@property
def secret_key(self):
return "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"


class TestReportBase(TestWorkflowsLocalStorageBase):
create_report = True

def get_executor(self) -> str:
return "local"

def get_executor_settings(self) -> Optional[ExecutorSettingsBase]:
return None
8 changes: 7 additions & 1 deletion snakemake/common/tests/testcases/groups/Snakefile
Expand Up @@ -25,7 +25,13 @@ rule c:
input:
expand("test2.{sample}.out", sample=[1, 2, 3])
output:
"test3.out"
report(
"test3.out",
caption="caption.rst",
category="Test",
subcategory="Subtest",
labels={"label1": "foo", "label2": "bar"},
)
resources:
mem="5MB"
shell:
Expand Down
1 change: 1 addition & 0 deletions snakemake/common/tests/testcases/groups/caption.rst
@@ -0,0 +1 @@
This is a test caption {{ snakemake.output[0] }}.
8 changes: 7 additions & 1 deletion snakemake/common/tests/testcases/simple/Snakefile
Expand Up @@ -27,7 +27,13 @@ rule c:
input:
"test2.out"
output:
"test3.out"
report(
"test3.out",
caption="caption.rst",
category="Test",
subcategory="Subtest",
labels={"label1": "foo", "label2": "bar"},
)
log:
"c.log"
resources:
Expand Down
1 change: 1 addition & 0 deletions snakemake/common/tests/testcases/simple/caption.rst
@@ -0,0 +1 @@
This is a test caption {{ snakemake.output[0] }}.

0 comments on commit 2f7d4b5

Please sign in to comment.