diff --git a/README.md b/README.md index 124569e..1200f0d 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,29 @@ cd snakemake-storage-plugin-myfancystorage # Scaffold the project as a snakemake executor plugin poetry scaffold-snakemake-storage-plugin +# Next, edit the scaffolded code according to your needs, and publish +# the resulting plugin into a github repository. The scaffold command also +# creates github actions workflows that will immediately start to check and test +# the plugin. +``` + +## Scaffolding a report plugin + +Lets assume that you want to create a snakemake report plugin with the name `snakemake-report-plugin-myfancyreport`. + +```bash + +# Install poetry plugin via +poetry self add poetry-snakemake-plugin + +# Create a new poetry project via +poetry new snakemake-report-plugin-myfancyreport + +cd snakemake-storage-plugin-myfancyreport + +# Scaffold the project as a snakemake executor plugin +poetry scaffold-snakemake-report-plugin + # Next, edit the scaffolded code according to your needs, and publish # the resulting plugin into a github repository. The scaffold command also # creates github actions workflows that will immediately start to check and test diff --git a/poetry_snakemake_plugin/__init__.py b/poetry_snakemake_plugin/__init__.py index 19b5af9..1d0ba22 100644 --- a/poetry_snakemake_plugin/__init__.py +++ b/poetry_snakemake_plugin/__init__.py @@ -3,6 +3,7 @@ from poetry_snakemake_plugin.executor_plugins import ( ScaffoldSnakemakeExecutorPluginCommand, ) +from poetry_snakemake_plugin.report_plugins import ScaffoldSnakemakeReportPluginCommand from poetry_snakemake_plugin.storage_plugins import ( ScaffoldSnakemakeStoragePluginCommand, ) @@ -18,3 +19,7 @@ def activate(self, application): ScaffoldSnakemakeStoragePluginCommand.name, ScaffoldSnakemakeStoragePluginCommand, ) + application.command_loader.register_factory( + ScaffoldSnakemakeReportPluginCommand.name, + ScaffoldSnakemakeReportPluginCommand, + ) diff --git a/poetry_snakemake_plugin/report_plugins.py b/poetry_snakemake_plugin/report_plugins.py new file mode 100644 index 0000000..f60323b --- /dev/null +++ b/poetry_snakemake_plugin/report_plugins.py @@ -0,0 +1,21 @@ +from pathlib import Path +from typing import List + +from poetry_snakemake_plugin.common import ScaffoldSnakemakePluginCommandBase + + +class ScaffoldSnakemakeReportPluginCommand(ScaffoldSnakemakePluginCommandBase): + name = "scaffold-snakemake-report-plugin" + description = ( + "Scaffolds a snakemake report plugin by adding recommended " + "dependencies and code snippets." + ) + + def get_templates(self, module_path: Path, tests_path: Path) -> List[str]: + return [ + ("report-plugins/init.py", module_path / "__init__.py"), + ("report-plugins/tests.py", tests_path / "tests.py"), + ] + + def get_plugin_type(self) -> str: + return "report" diff --git a/poetry_snakemake_plugin/templates/report-plugins/init.py b/poetry_snakemake_plugin/templates/report-plugins/init.py new file mode 100644 index 0000000..7233dec --- /dev/null +++ b/poetry_snakemake_plugin/templates/report-plugins/init.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass, field +from typing import Optional + +from snakemake_interface_common.exceptions import WorkflowError # noqa: F401 +from snakemake_interface_report_plugins.reporter import ReporterBase +from snakemake_interface_report_plugins.settings import ReportSettingsBase + + +# Optional: +# Define additional settings for your reporter. +# They will occur in the Snakemake CLI as --report-- +# Omit this class if you don't need any. +# Make sure that all defined fields are Optional (or bool) and specify a default value +# of None (or False) or anything else that makes sense in your case. +@dataclass +class ReportSettings(ReportSettingsBase): + myparam: Optional[int] = field( + default=None, + metadata={ + "help": "Some help text", + # Optionally request that setting is also available for specification + # via an environment variable. The variable will be named automatically as + # SNAKEMAKE_REPORT__, all upper case. + # This mechanism should ONLY be used for passwords and usernames. + # For other items, we rather recommend to let people use a profile + # for setting defaults + # (https://snakemake.readthedocs.io/en/stable/executing/cli.html#profiles). + "env_var": False, + # Optionally specify a function that parses the value given by the user. + # This is useful to create complex types from the user input. + "parse_func": ..., + # If a parse_func is specified, you also have to specify an unparse_func + # that converts the parsed value back to a string. + "unparse_func": ..., + # Optionally specify that setting is required when the reporter is in use. + "required": True, + }, + ) + + +# Required: +# Implementation of your reporter +class Reporter(ReporterBase): + def __post_init__(self): + # initialize additional attributes + # Do not overwrite the __init__ method as this is kept in control of the base + # class in order to simplify the update process. + # See https://github.com/snakemake/snakemake-interface-report-plugins/snakemake_interface_report_plugins/reporter.py # noqa: E501 + # for attributes of the base class. + # In particular, the settings of above ReportSettings class are accessible via + # self.settings. + ... + + def render(self): + # Render the report, using attributes of the base class. + ... diff --git a/poetry_snakemake_plugin/templates/report-plugins/tests.py b/poetry_snakemake_plugin/templates/report-plugins/tests.py new file mode 100644 index 0000000..cfa5125 --- /dev/null +++ b/poetry_snakemake_plugin/templates/report-plugins/tests.py @@ -0,0 +1,16 @@ +from typing import Optional +import snakemake.common.tests +from snakemake_interface_report_plugins.settings import ReportSettingsBase + + +# Check out the base classes found here for all possible options and methods: +# https://github.com/snakemake/snakemake/blob/main/snakemake/common/tests/__init__.py +class TestWorkflowsBase(snakemake.common.tests.TestReportBase): + __test__ = True + + def get_reporter(self) -> str: + return "{{plugin_name}}" + + def get_report_settings(self) -> Optional[ReportSettingsBase]: + # instantiate ReportSettings of this plugin as appropriate + ... diff --git a/poetry_snakemake_plugin/templates/storage-plugins/tests.py b/poetry_snakemake_plugin/templates/storage-plugins/tests.py index 29dd800..b8457f4 100644 --- a/poetry_snakemake_plugin/templates/storage-plugins/tests.py +++ b/poetry_snakemake_plugin/templates/storage-plugins/tests.py @@ -16,6 +16,7 @@ def get_query(self, tmp_path) -> str: ... def get_query_not_existing(self, tmp_path) -> str: + # Return a query that is not present in the storage. ... def get_storage_provider_cls(self) -> Type[StorageProviderBase]: diff --git a/pyproject.toml b/pyproject.toml index 703eb0d..cb83389 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,4 +27,4 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.plugins."poetry.application.plugin"] -scaffold-snakemake-executor-plugin = "poetry_snakemake_plugin:ScaffoldSnakemakeExecutorPlugin" \ No newline at end of file +scaffold-snakemake-plugin = "poetry_snakemake_plugin:ScaffoldSnakemakeExecutorPlugin" \ No newline at end of file diff --git a/tests/tests.py b/tests/tests.py index 5eed801..32c251f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -28,6 +28,19 @@ def test_new_snakemake_storage_plugin(tmp_path): os.chdir(orig_dir) +def test_new_snakemake_report_plugin(tmp_path): + orig_dir = os.getcwd() + os.chdir(tmp_path) + + run_subcommand("new", "snakemake-report-plugin-test") + os.chdir("snakemake-report-plugin-test") + run_subcommand("scaffold-snakemake-report-plugin") + + run_subcommand("run", "black", "--check", "--diff", ".") + run_subcommand("run", "flake8") + os.chdir(orig_dir) + + def run_subcommand(*args): cmd = ["poetry"] cmd.extend(args)