From eab4404e7d2db35e6e6c9d4d33b47f5bc9eaa1c8 Mon Sep 17 00:00:00 2001 From: Thomas Cokelaer Date: Sat, 13 Jan 2024 14:46:07 +0100 Subject: [PATCH] Fix #84 : remove deprecated and old Options --- README.rst | 1 + pyproject.toml | 2 +- sequana_pipetools/misc.py | 8 +- sequana_pipetools/options.py | 513 ------------------ sequana_pipetools/scripts/main.py | 18 +- sequana_pipetools/sequana_manager.py | 7 +- sequana_pipetools/snaketools/errors.py | 19 +- sequana_pipetools/snaketools/module_finder.py | 1 - .../snaketools/pipeline_manager.py | 4 +- .../snaketools/sequana_config.py | 6 +- sequana_pipetools/snaketools/slurm.py | 3 - tests/scripts/data/__init__.py | 0 tests/scripts/data/config.yaml | 81 +++ tests/scripts/test_main.py | 28 +- tests/snaketools/test_filefactory.py | 7 + tests/snaketools/test_pipeline_manager.py | 3 +- tests/snaketools/test_sequana_config.py | 16 +- tests/test_options.py | 82 +-- 18 files changed, 161 insertions(+), 638 deletions(-) create mode 100644 tests/scripts/data/__init__.py create mode 100644 tests/scripts/data/config.yaml diff --git a/README.rst b/README.rst index 3e8c91c..9fcebe7 100644 --- a/README.rst +++ b/README.rst @@ -313,6 +313,7 @@ Changelog ========= ====================================================================== Version Description ========= ====================================================================== +0.17.0 * Remove deprecated options and deprecated functions. More tests. 0.16.9 * Fix slurm sys exit (replaced by print) * upadte doc * more tests diff --git a/pyproject.toml b/pyproject.toml index 56e16c5..f68233c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api" #maintainer ?#maintainer email [tool.poetry] name = "sequana_pipetools" -version = "0.16.9" +version = "0.17.0" description = "A set of tools to help building or using Sequana pipelines" authors = ["Sequana Team"] license = "BSD-3" diff --git a/sequana_pipetools/misc.py b/sequana_pipetools/misc.py index da3c4a1..6aaa340 100644 --- a/sequana_pipetools/misc.py +++ b/sequana_pipetools/misc.py @@ -95,17 +95,17 @@ def print_version(name): print(f"sequana_{name} version: ?") try: - version = get_package_version(f"sequana") + version = get_package_version("sequana") print(f"Sequana version: {version}") except Exception: # pragma: no cover - pass + print(f"Sequana version: not found") try: - version = get_package_version(f"sequana_pipetools") + version = get_package_version("sequana_pipetools") print(f"Sequana_pipetools version: {version}") except Exception as err: # pragma: no cover print(err) - print("Sequana_pipetools version: ?") + print("Sequana_pipetools version: not found") print(Colors().purple("\nHow to help ?\n- Please, consider citing us (see sequana.readthedocs.io)")) print(Colors().purple("- Contribute to the code or documentation")) diff --git a/sequana_pipetools/options.py b/sequana_pipetools/options.py index 22f30dd..6b156eb 100644 --- a/sequana_pipetools/options.py +++ b/sequana_pipetools/options.py @@ -16,20 +16,11 @@ import shutil import sys -from deprecated import deprecated - from sequana_pipetools.snaketools import Pipeline from .misc import print_version __all__ = [ - "GeneralOptions", - "InputOptions", - "KrakenOptions", - "SlurmOptions", - "SnakemakeOptions", - "TrimmingOptions", - "FeatureCountsOptions", "ClickGeneralOptions", "ClickInputOptions", "ClickFeatureCountsOptions", @@ -37,7 +28,6 @@ "ClickSlurmOptions", "ClickSnakemakeOptions", "ClickTrimmingOptions", - "before_pipeline", "init_click", "include_options_from", "OptionEatAll", @@ -160,25 +150,6 @@ def parser_process(value, state): return retval -@deprecated -def before_pipeline(NAME): - """A function to provide --version and --deps for all pipelines""" - - if "--version" in sys.argv or "-v" in sys.argv: - print_version(NAME) - sys.exit(0) - - if "--deps" in sys.argv: - # Means than sequana must be installed, which we assume if someone uses - # a pipeline. so must be here and not global import - - module = Pipeline(NAME) - with open(str(module.requirements), "r") as fin: - data = fin.read() - print("Those software will be required for the pipeline to work correctly:\n{}".format(data)) - sys.exit(0) - - class ClickGeneralOptions: group_name = "General" metadata = { @@ -248,48 +219,6 @@ def deps_callback(ctx, param, value): ctx.exit(0) -class GeneralOptions: - def __init__(self): - pass - - def add_options(self, parser): - parser.add_argument( - "--run-mode", - dest="run_mode", - default=None, - choices=["local", "slurm"], - help="""run_mode can be either 'local' or 'slurm'. Use local to - run the pipeline locally, otherwise use 'slurm' to run on a - cluster with SLURM scheduler. Other clusters are not maintained. - However, you can set to slurm and change the output shell script - to fulfill your needs. If unset, sequana searches for the sbatch - and srun commands. If found, this is set automatically to - 'slurm', otherwise to 'local'. - """, - ) - - parser.add_argument("--version", action="store_true", help="Print the version and quit") - parser.add_argument("--deps", action="store_true", help="Show the known dependencies of the pipeline") - parser.add_argument( - "--level", - dest="level", - default="INFO", - choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"], - help="logging level in INFO, DEBUG, WARNING, ERROR, CRITICAL", - ) - parser.add_argument( - "--from-project", - dest="from_project", - default=None, - help="""You can initiate a new analysis run from an existing project. - In theory, sequana project have a hidden .sequana directory, - which can be used as input. The name of the run directory itself - should suffice (if .sequana is found inside). From there, - the config file and the pipeline files are copied in your new - working directory""", - ) - - def guess_scheduler(): """Guesses whether we are on a SLURM cluster or not. @@ -383,77 +312,6 @@ def __init__(self, working_directory="analysis", caller=None): ] -class SnakemakeOptions: - def __init__(self, group_name="snakemake", working_directory="analysis"): - self.group_name = group_name - self.workdir = working_directory - - def _default_jobs(self): - if guess_scheduler() == "slurm": # pragma: no cover - return 40 - else: - return 4 - - def add_options(self, parser): - group = parser.add_argument_group(self.group_name) - - group.add_argument( - "--jobs", - dest="jobs", - default=self._default_jobs(), - help="""Number of jobs to run at the same time (default 4 on a local - computer, 40 on a SLURM scheduler). This is the --jobs options - of Snakemake""", - ) - group.add_argument( - "--working-directory", - dest="workdir", - default=self.workdir, - help="""where to save the pipeline and its configuration file and - where the analyse can be run""", - ) - group.add_argument( - "--force", - dest="force", - action="store_true", - default=False, - help="""If the working directory exists, proceed anyway.""", - ) - if "--use-singularity" in sys.argv: # pragma: no cover - print("--use-singularity is deprecated, use --use-apptainer instead.") - sys.exit(1) - - group.add_argument( - "--use-apptainer", - dest="use_apptainer", - action="store_true", - default=False, - help="""If set, pipelines will download apptainer files for all external tools.""", - ) # pragma: no cover - - if "--singularity-prefix" in sys.argv: # pragma: no cover - print("--singularity-prefix is deprecated, use --apptainer-prefix instead.") - sys.exit(1) - - group.add_argument( - "--apptainer-prefix", - dest="apptainer_prefix", - default=False, - help="""If set, pipelines will download apptainer files in this directory otherwise they will be downloaded in the working directory of the pipeline .""", - ) - - if "--singularity-args" in sys.argv: # pragma: no cover - print("--singularity-args is deprecated, use --apptainer-args instead.") - sys.exit(1) - - group.add_argument( - "--apptainer-args", - dest="apptainer_args", - default="", - help="""provide any arguments accepted by apptainer. By default, we set -B $HOME:$HOME """, - ) - - class ClickInputOptions: group_name = "Data" metadata = {"name": group_name, "options": ["--input-directory", "--input-pattern", "--input-readtag"]} @@ -499,46 +357,6 @@ def __init__(self, input_directory=".", input_pattern="*fastq.gz", add_input_rea ) -class InputOptions: - def __init__(self, group_name="data", input_directory=".", input_pattern="*fastq.gz", add_input_readtag=True): - """ - - By default, single-end data sets. If paired, set is_paired to True - If so, the add_input_readtag must be set - """ - self.group_name = group_name - self.input_directory = input_directory - self.input_pattern = input_pattern - self.add_input_readtag = add_input_readtag - - def add_options(self, parser): - self.group = parser.add_argument_group(self.group_name) - self.group.add_argument( - "--input-directory", - dest="input_directory", - default=self.input_directory, - # required=True, - help="""Where to find the FastQ files""", - ) - self.group.add_argument( - "--input-pattern", - dest="input_pattern", - default=self.input_pattern, - help="pattern for the input FastQ files ", - ) - - if self.add_input_readtag: - self.group.add_argument( - "--input-readtag", - dest="input_readtag", - default="_R[12]_", - help="""pattern for the paired/single end FastQ. If your files are - tagged with _R1_ or _R2_, please set this value to '_R[12]_'. If your - files are tagged with _1 and _2, you must change this readtag - accordingly to '_[12]'.""", - ) - - class ClickKrakenOptions: group_name = "Kraken" metadata = { @@ -573,35 +391,6 @@ def __init__(self, caller=None): ] -class KrakenOptions: - def __init__(self, group_name="section_kraken"): - self.group_name = group_name - - def add_options(self, parser): - group = parser.add_argument_group(self.group_name) - - group.add_argument( - "--skip-kraken", - action="store_true", - default=False, - help="""If provided, kraken taxonomy is performed. A database must be - provided (see below). """, - ) - - group.add_argument( - "--kraken-databases", - dest="kraken_databases", - type=str, - nargs="+", - default=[], - help="""Path to a valid set of Kraken database(s). - If you do not have any, please see https://sequana.readthedocs.io - or use sequana_taxonomy --download option. - You may use several, in which case, an iterative taxonomy is - performed as explained in online sequana documentation""", - ) - - class ClickTrimmingOptions: group_name = "Trimming" metadata = { @@ -703,209 +492,6 @@ def quality(x): ] -class TrimmingOptions: - description = """ - This section is dedicated to reads trimming and filtering and adapter - trimming. We currently provide supports for Cutadapt/Atropos and FastP tools. - - This section uniformizes the options for such tools - - - """ - - def __init__(self, group_name="section_trimming", software=["cutadapt", "atropos", "fastp"]): - self.group_name = group_name - self.software = software - self.software_default = "fastp" - - def add_options(self, parser): - group = parser.add_argument_group(self.group_name, self.description) - - group.add_argument( - "--software-choice", - dest="trimming_software_choice", - default=self.software_default, - choices=self.software, - help="""additional options understood by cutadapt""", - ) - - group.add_argument( - "--disable-trimming", action="store_true", default=False, help="If provided, disable trimming " - ) - - group.add_argument( - "--trimming-adapter-read1", - dest="trimming_adapter_read1", - default="", - help="""fastp auto-detects adapters. You may specify the -adapter sequence specificically for fastp or cutadapt/atropos with option for -read1""", - ) - - group.add_argument( - "--trimming-adapter-read2", - dest="trimming_adapter_read2", - default="", - help="""fastp auto-detects adapters. You may specify the -adapter sequence specificically for fastp or cutadapt/atropos with option for -read1""", - ) - - group.add_argument( - "--trimming-minimum-length", - default=20, - help="""minimum number of bases required; read discarded -otherwise. For cutadapt, default is 20 and for fastp, 15. We use 20 for both by -default.""", - ) - - def quality(x): - x = int(x) - if x < 0: - raise argparse.ArgumentTypeError("quality must be positive") - return x - - group.add_argument( - "--trimming-quality", - dest="trimming_quality", - default=-1, - type=quality, - help="""Trimming quality parameter depends on the algorithm used by -the software behind the scene an may vary greatly; consequently, not provide -a default value. Cutadapt uses 30 by default, fastp uses 15 by default. If -unset, the rnaseq pipeline set the default to 30 for cutadapt and 15 for fastp""", - ) - - # Cutadapt specific - group.add_argument( - "--trimming-cutadapt-mode", - dest="trimming_cutadapt_mode", - default="b", - choices=["g", "a", "b"], - help="""Mode used to remove adapters. g for 5', a for 3', b for both - 5'/3' as defined in cutadapt documentation""", - ) - group.add_argument( - "--trimming-cutadapt-options", - dest="trimming_cutadapt_options", - default=" -O 6 --trim-n", - help="""additional options understood by cutadapt. Here, we trim the -Ns; -O 6 is the minimum overlap length between read and adapter for an adapter -to be found""", - ) - - -class CutadaptOptions: # pragma: no cover - description = """ - This section allows you to trim bases (--cutadapt-quality) with poor - quality and/or remove adapters. - - To remove adapters, you can provide the adapters directly as a - string (or a file) using --cutadapt-fwd (AND --cutadapt-rev" for paired-end data). - - If you set the --cutadapt-adapter-choice to 'none', fwd and reverse - adapters are set to XXXX (see cutadapt documentation). - - """ - - def __init__(self, group_name="section_cutadapt"): - self.group_name = group_name - print("CutadaptOptions is deprecated. Will be removed in future versions. Use TrimmingOptions instead") - - def add_options(self, parser): - group = parser.add_argument_group(self.group_name, self.description) - - group.add_argument( - "--skip-cutadapt", - action="store_true", - default=False, - help="If provided, fastq cleaning and trimming will be skipped", - ) - - group.add_argument( - "--cutadapt-fwd", - dest="cutadapt_fwd", - default="", - help="""Provide a adapter as a string of stored in a - FASTA file. If the file exists, we will store it as expected - with a preceeding prefix 'file:'""", - ) - - group.add_argument( - "--cutadapt-rev", - dest="cutadapt_rev", - default="", - help="""Provide a adapter as a string of stored in a - FASTA file. If the file exists, we will store it as expected - with a preceeding prefix 'file:'""", - ) - - def quality(x): - x = int(x) - if x < 0: - raise argparse.ArgumentTypeError("quality must be positive") - return x - - group.add_argument( - "--cutadapt-quality", - dest="cutadapt_quality", - default=30, - type=quality, - help="""0 means no trimming, 30 means keep bases with quality - above 30""", - ) - - group.add_argument( - "--cutadapt-tool-choice", - dest="cutadapt_tool_choice", - default="cutadapt", - choices=["cutadapt", "atropos"], - help="Select the prefered tool. Default is cutadapt", - ) - - group.add_argument( - "--cutadapt-mode", - dest="cutadapt_mode", - default="b", - choices=["g", "a", "b"], - help="""Mode used to remove adapters. g for 5', a for 3', b for both - 5'/3' as defined in cutadapt documentation""", - ) - - group.add_argument( - "--cutadapt-options", - dest="cutadapt_options", - default=" -O 6 --trim-n", - help="""additional options understood by cutadapt""", - ) - - def options(self, options): - """ """ - # do we need to remove adapters at all ? - if options.cutadapt_adapter_choice == "none": - options.cutadapt_adapter_choice = None - options.cutadapt_fwd = "XXXX" - options.cutadapt_rev = "XXXX" - # or just the universal ones ? - elif options.cutadapt_adapter_choice == "universal": - options.cutadapt_fwd = "GATCGGAAGAGCACACGTCTGAACTCCAGTCACCGATGTATCTCGTATGCCGTCTTCTGC" - options.cutadapt_rev = "TCTAGCCTTCTCGCAGCACATCCCTTTCTCACATCTAGAGCCACCAGCGGCATAGTAA" - # or do we have a string or files provided for the fwd/rev ? - elif options.cutadapt_adapter_choice is None: - if options.cutadapt_fwd: - # Could be a string or a file. If a file, check the format - if os.path.exists(options.cutadapt_fwd): - options.cutadapt_fwd = "file:{}".format(os.path.abspath(options.cutadapt_fwd)) - if options.cutadapt_rev: - # Could be a string or a file. If a file, check the format - if os.path.exists(options.cutadapt_rev): - options.cutadapt_rev = "file:{}".format(os.path.abspath(options.cutadapt_rev)) - elif options.cutadapt_adapter_choice: - # nothing to do, the cutadapt rules from sequana will use - # the adapter_choice, and fill the fwd/rev automatically - pass - - class ClickFeatureCountsOptions: group_name = "Feature Counts" metadata = { @@ -966,55 +552,6 @@ def __init__(self, feature_type="gene", attribute="ID", options=None, strandness ] -class FeatureCountsOptions: - def __init__(self, group_name="feature_counts", feature_type="gene", attribute="ID", options=None, strandness=None): - self.group_name = group_name - self.feature_type = feature_type - self.attribute = attribute - self.options = options - self.strandness = strandness - - def add_options(self, parser): - group = parser.add_argument_group(self.group_name) - group.add_argument( - "--feature-counts-strandness", - default=self.strandness, - help="""0 for unstranded, 1 for stranded and 2 for reversely - stranded. If you do not know, let the pipeline guess for you.""", - ) - group.add_argument( - "--feature-counts-attribute", - default=self.attribute, - help="""the GFF attribute to use as identifier. If you do not know, - look at the GFF file or use 'sequana summary YOURFILE.gff' command to get - information about attributes and features contained in your annotation file.""", - ) - group.add_argument( - "--feature-counts-extra-attributes", - default=None, - help="""any extra attribute to add in final feature counts files""", - ) - group.add_argument( - "--feature-counts-feature-type", - default=self.feature_type, - help="""the GFF feature type (e.g., gene, exon, mRNA, etc). If you - do not know, look at the GFF file or use 'sequana summary -YOURFILE.gff'. Would you need to perform an analysis on several features, you -can either build your own custom GFF file (see Please see -https://github.com/sequana/rnaseq/wiki) or provide several entries separated by -commas""", - ) - group.add_argument( - "--feature-counts-options", - default=self.options, - help="""Any extra options for feature counts. Note that the -s - option (strandness), the -g option (attribute name) and -t options - (genetic type) have their own options. If you use still use one of - the -s/-g/-t, it will replace the --feature-counts-strandness, - --feature-counts-attribute and -feature-counts-feature options respectively""", - ) - - class ClickSlurmOptions: group_name = "Slurm" metadata = { @@ -1051,53 +588,3 @@ def __init__(self, memory="4G", queue="common", profile=None, caller=None): help="SLURM queue to be used (biomics)", ), ] - - -class SlurmOptions: - def __init__(self, group_name="slurm", memory="4G", queue="common", cores=4, profile=None): - """ - - :: - - class Options(argparse.ArgumentParser, SlurmOptions): - def __init__(self, prog="whatever") - super(Options, self).__init__(usage="todo", - prog="whatever",description="" - self.add_argument(...) - ... - self.add_slurm_options() - - """ - self.group_name = group_name - self.memory = memory - self.cores = cores - self.queue = queue - self.profile = profile - - def add_options(self, parser): - group = parser.add_argument_group(self.group_name) - group.add_argument( - "--slurm-cores-per-job", - dest="slurm_cores_per_job", - default=self.cores, - help="""Number of cores/jobs to be used at the same time.""", - ) - group.add_argument( - "--slurm-queue", - dest="slurm_queue", - default=self.queue, - help="SLURM queue to be used (biomics)", - ) - group.add_argument( - "--slurm-memory", - dest="slurm_memory", - default=self.memory, - help="""Specify the memory required by default. (default 4G; stands for 4 Gbytes).""", - ) - group.add_argument( - "--profile", - dest="profile", - default=self.profile, - choices=["local", "slurm"], - help="Create cluster (HPC) profile directory. By default, it uses local profile", - ) diff --git a/sequana_pipetools/scripts/main.py b/sequana_pipetools/scripts/main.py index e9bfc40..6c7bbfa 100644 --- a/sequana_pipetools/scripts/main.py +++ b/sequana_pipetools/scripts/main.py @@ -18,6 +18,10 @@ from pkg_resources import DistributionNotFound from sequana_pipetools import version +from sequana_pipetools.misc import url2hash +from sequana_pipetools.snaketools.errors import PipeError +from sequana_pipetools.snaketools.pipeline_utils import get_pipeline_statistics +from sequana_pipetools.snaketools.sequana_config import SequanaConfig click.rich_click.USE_MARKDOWN = True click.rich_click.SHOW_METAVARS_COLUMN = False @@ -186,6 +190,7 @@ def set_option_file(self, option_name): help="""Given a config file, this command creates a draft schema file""", ) @click.option("--slurm-diag", is_flag=True, help="Scans slurm files and get summary information") +@click.option("--url2hash", type=click.STRING, help="For developers. Convert a URL to hash mame. ") def main(**kwargs): """Pipetools utilities for the Sequana project (sequana.readthedocs.io) @@ -206,13 +211,16 @@ def main(**kwargs): if kwargs["version"]: click.echo(f"sequana_pipetools v{version}") return + elif kwargs["url2hash"]: + click.echo(url2hash(kwargs["url2hash"])) + elif kwargs["completion"]: if kwargs["force"] is True: choice = "y" - else: + else: # pragma: no cover msg = "This action will replace files stored in ./config/sequana/pipelines. Do you want to proceed y/n: " choice = input(msg) - if choice != "y": + if choice != "y": # pragma: no cover sys.exit(0) name = kwargs["completion"] @@ -226,8 +234,6 @@ def main(**kwargs): click.echo(" source ~/.config/sequana/pipelines/{}.sh".format(name)) click.echo("\nto activate the completion") elif kwargs["stats"]: - from sequana_pipetools.snaketools.pipeline_utils import get_pipeline_statistics - wrappers, rules = get_pipeline_statistics() click.echo("\n ==== Number of wrappers per pipeline") click.echo(wrappers.sum(axis=0)) @@ -236,14 +242,10 @@ def main(**kwargs): click.echo("\n ==== Number of rules used") click.echo(rules) elif kwargs["config_to_schema"]: - from sequana_pipetools.snaketools.sequana_config import SequanaConfig - config_file = kwargs["config_to_schema"] cfg = SequanaConfig(config_file) cfg.create_draft_schema() elif kwargs["slurm_diag"]: - from sequana_pipetools.snaketools.errors import PipeError - p = PipeError() p.status(".") diff --git a/sequana_pipetools/sequana_manager.py b/sequana_pipetools/sequana_manager.py index a63b1a5..c1173bb 100644 --- a/sequana_pipetools/sequana_manager.py +++ b/sequana_pipetools/sequana_manager.py @@ -18,7 +18,6 @@ import sys from pathlib import Path from shutil import which -from urllib.request import urlretrieve import aiohttp import colorlog @@ -29,7 +28,7 @@ from sequana_pipetools.misc import url2hash from sequana_pipetools.snaketools.profile import create_profile -from .misc import Colors, PipetoolsException, print_version +from .misc import Colors, PipetoolsException from .snaketools import Pipeline, SequanaConfig logger = colorlog.getLogger(__name__) @@ -77,7 +76,7 @@ def __init__(self): # have been defined yet. try: logger.setLevel(options.level) - except AttributeError: + except AttributeError: # pragma: no cover logger.warning("Your pipeline does not have a level option.") options.level = "INFO" @@ -450,7 +449,7 @@ def teardown(self, check_schema=True, check_input_files=True): with open(f"{self.workdir}/.sequana/pip.yml", "w") as fout: subprocess.call(cmd.split(), stdout=fout) logger.debug("Saved your pip environment into pip.txt (conda not found)") - else: + else: # pragma: no cover with open(f"{self.workdir}/.sequana/pip.yml", "w") as fout: fout.write("pip not found") diff --git a/sequana_pipetools/snaketools/errors.py b/sequana_pipetools/snaketools/errors.py index e4fdfc7..be3cda8 100644 --- a/sequana_pipetools/snaketools/errors.py +++ b/sequana_pipetools/snaketools/errors.py @@ -12,9 +12,8 @@ ############################################################################## from pathlib import Path -from sequana_pipetools import logger - -from .slurm import SlurmParsing +from sequana_pipetools.options import guess_scheduler +from sequana_pipetools.snaketools.slurm import SlurmParsing class PipeError: @@ -24,8 +23,12 @@ def __init__(self, *args, **kwargs): pass def status(self, working_directory="./", logs_directory="logs"): - try: # let us try to introspect the slurm files - dj = SlurmParsing(working_directory, logs_directory) - print(dj) - except Exception as err: # pragma: no cover - print(err) + + if guess_scheduler == "slurm": + try: # let us try to introspect the slurm files + dj = SlurmParsing(working_directory, logs_directory) + print(dj) + except Exception as err: # pragma: no cover + print(err) + else: + pass diff --git a/sequana_pipetools/snaketools/module_finder.py b/sequana_pipetools/snaketools/module_finder.py index 8dd0b4c..7d6c08c 100644 --- a/sequana_pipetools/snaketools/module_finder.py +++ b/sequana_pipetools/snaketools/module_finder.py @@ -12,7 +12,6 @@ ############################################################################## import os import pkgutil -import sys import colorlog diff --git a/sequana_pipetools/snaketools/pipeline_manager.py b/sequana_pipetools/snaketools/pipeline_manager.py index dc5be8e..00fa9a2 100644 --- a/sequana_pipetools/snaketools/pipeline_manager.py +++ b/sequana_pipetools/snaketools/pipeline_manager.py @@ -174,10 +174,10 @@ def get_html_summary(self, float="left", width=30): vers = get_package_version(f"sequana_{self.name}") - data = {"samples": len(self.samples), "sequana_{}_version".format(self.name): vers} + data = {"samples": len(self.samples), f"sequana_{self.name}_version": vers} try: data["paired"] = self.paired - except AttributeError: + except AttributeError: # pragma: no cover pass df_general = pd.DataFrame( diff --git a/sequana_pipetools/snaketools/sequana_config.py b/sequana_pipetools/snaketools/sequana_config.py index 3d952fa..805d89c 100644 --- a/sequana_pipetools/snaketools/sequana_config.py +++ b/sequana_pipetools/snaketools/sequana_config.py @@ -10,9 +10,7 @@ # Documentation: http://sequana.readthedocs.io # Contributors: https://github.com/sequana/sequana/graphs/contributors ############################################################################## -import json import os -import shutil import warnings import ruamel.yaml @@ -102,7 +100,7 @@ def _read_file(self, data): with open(data, "r") as fh: import yaml as _yaml - config = _yaml.load(fh, Loader=_yaml.FullLoader) + config = _yaml.safe_load(fh) return config else: raise FileNotFoundError(f"input string must be an existing file {data}") @@ -186,8 +184,10 @@ def check_config_with_schema(self, schemafile): # add custom extensions with pkg_resources.path("sequana_pipetools.resources", "ext.py") as ext_name: extensions = [str(ext_name)] + # causes issue with ruamel.yaml 0.12.13. Works for 0.15 warnings.simplefilter("ignore", ruamel.yaml.error.UnsafeLoaderWarning) + try: # open the config and the schema file with TempFile(suffix=".yaml") as fh: diff --git a/sequana_pipetools/snaketools/slurm.py b/sequana_pipetools/snaketools/slurm.py index 7885ac2..9e98ef7 100644 --- a/sequana_pipetools/snaketools/slurm.py +++ b/sequana_pipetools/snaketools/slurm.py @@ -10,9 +10,6 @@ # Documentation: http://sequana.readthedocs.io # Contributors: https://github.com/sequana/sequana/graphs/contributors ############################################################################## -import os -import re -import sys from pathlib import Path import colorlog diff --git a/tests/scripts/data/__init__.py b/tests/scripts/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/data/config.yaml b/tests/scripts/data/config.yaml new file mode 100644 index 0000000..832ae81 --- /dev/null +++ b/tests/scripts/data/config.yaml @@ -0,0 +1,81 @@ +# ============================================================================ +# Config file for Quality Control +# ==========================================[ Sections for the users ]======== +# +# One of input_directory, input_pattern and input_samples must be provided +# If input_directory provided, use it otherwise if input_pattern provided, +# use it, otherwise use input_samples. +# ============================================================================ + +sequana_wrappers: "v23.11.18" + +input_directory: '.' +input_pattern: '*fastq.gz' + + +# See sequana_pipetools.readthedocs.io for details about these 2 options +# common prefixes are removed. addition prefixes may be removed here +#extra_prefixes_to_strip = [] +# in special cases, sample names can be extracted with a pattern +#sample_pattern: '{sample}.fastq.gz' + + +apptainers: + falco: "https://zenodo.org/record/7014954/files/falco_1.0.0.img" + fastqc: "https://zenodo.org/record/7923780/files/fastqc_0.12.1.img" + graphviz: "https://zenodo.org/record/7928262/files/graphviz_7.0.5.img" + multiqc: https://zenodo.org/record/10155626/files/multiqc_1.17.0.img + + +############################################################################## +# general section +# +# Choose one of the standard method to perform QC of your fastq data +# +general: + method_choice: fastqc + +############################################################################## +# FastQC section +# +# :Parameters: +# +# - options: string with any valid FastQC options +fastqc: + options: '' + threads: 4 + resources: + mem: 8G + +############################################################################## +# Falco section +# +# :Parameters: +# +# - options: string with any valid FastQC options +falco: + options: '' + threads: 4 + resources: + mem: 8G + +############################################################################## +# +# +# - options: any multiqc options accepted. Note that if you use --comments, +# it will be appended to the existing --comments added inside sequana. +# By default, -p (create pictures) and -f (for overwritting) are used. +# - indir: The input multiqc (default is local). +multiqc: + do: true + options: -p -f + input_directory: "." + modules: fastqc # falco is not set; the fastqc module works for falco + config_file: + resources: + mem: 8G + +plotting_and_stats: + resources: + mem: 8G + diff --git a/tests/scripts/test_main.py b/tests/scripts/test_main.py index 7c3618d..b94b6c6 100644 --- a/tests/scripts/test_main.py +++ b/tests/scripts/test_main.py @@ -1,10 +1,11 @@ import sys -import pytest from click.testing import CliRunner from sequana_pipetools.scripts.main import main +from . import test_dir + def test_main(): runner = CliRunner() @@ -29,3 +30,28 @@ def test_completion(monkeypatch): runner = CliRunner() results = runner.invoke(main, ["--completion", "fastqc", "--force"]) assert results.exit_code == 0 + + +def test_url2hash(): + runner = CliRunner() + results = runner.invoke(main, ["--url2hash", "test"]) + assert results.exit_code == 0 + assert results.output == "098f6bcd4621d373cade4e832627b4f6\n" + + +def test_stats(): + runner = CliRunner() + results = runner.invoke(main, ["--stats"]) + assert results.exit_code == 0 + + +def test_config_to_schema(): + runner = CliRunner() + results = runner.invoke(main, ["--config-to-schema", f"{test_dir}/data/config.yaml"]) + assert results.exit_code == 0 + + +def test_slurm_diag(): + runner = CliRunner() + results = runner.invoke(main, ["--slurm-diag"]) + assert results.exit_code == 0 diff --git a/tests/snaketools/test_filefactory.py b/tests/snaketools/test_filefactory.py index f3d6449..4d69c4a 100644 --- a/tests/snaketools/test_filefactory.py +++ b/tests/snaketools/test_filefactory.py @@ -41,6 +41,13 @@ def inner_test(ff): except ValueError: assert True + try: + ff = snaketools.FastQFactory("*dummy___not_possible", verbose=True, read_tag="") + + assert False + except ValueError: + assert True + def test_fastqfactory(): with pytest.raises(ValueError): diff --git a/tests/snaketools/test_pipeline_manager.py b/tests/snaketools/test_pipeline_manager.py index 5529463..a21180a 100644 --- a/tests/snaketools/test_pipeline_manager.py +++ b/tests/snaketools/test_pipeline_manager.py @@ -28,7 +28,8 @@ def test_pipeline_manager(tmpdir): cfg.config.input_directory, cfg.config.input_pattern = os.path.split(file1) pm = snaketools.PipelineManager("custom", cfg) assert not pm.paired - pm.teardown() + working_dir = tmpdir.mkdir("temp") + pm.teardown(outdir=working_dir) # here not readtag provided, so data is considered to be non-fastq related # or at least not paired diff --git a/tests/snaketools/test_sequana_config.py b/tests/snaketools/test_sequana_config.py index 9797767..b17c062 100644 --- a/tests/snaketools/test_sequana_config.py +++ b/tests/snaketools/test_sequana_config.py @@ -7,14 +7,14 @@ from .. import test_dir -# def test_valid_config(tmpdir): -# config = SequanaConfig(None) -# -# s = Module("fastqc") -# config = SequanaConfig(s.config) -# fh = tmpdir.join("config.yml") -# config.save(fh) -# config.create_draft_schema() + +def test_valid_config(tmpdir): + + s = Pipeline("fastqc") + config = SequanaConfig(s.config) + fh = tmpdir.join("config.yml") + config.save(fh) + config.create_draft_schema() # this is a regression bug that guarantees that diff --git a/tests/test_options.py b/tests/test_options.py index 17cc579..1d9c820 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -3,92 +3,12 @@ import pytest -from sequana_pipetools.options import ( - ClickGeneralOptions, - FeatureCountsOptions, - KrakenOptions, - OptionEatAll, - SnakemakeOptions, - TrimmingOptions, - before_pipeline, -) +from sequana_pipetools.options import ClickGeneralOptions, OptionEatAll # for test_click_general_options() to work we need to define a global variable NAME = "TEST" -def test_misc(): - - sys.argv.append("--version") - - with pytest.raises(SystemExit): - before_pipeline("fastqc") - sys.argv.remove("--version") - sys.argv.append("--deps") - with pytest.raises(SystemExit): - before_pipeline("fastqc") - - -def test_feature_counts(): - p = argparse.ArgumentParser() - so = FeatureCountsOptions() - so.add_options(p) - - -def test_trimming_options(): - - p = argparse.ArgumentParser() - so = TrimmingOptions() - so.add_options(p) - p.parse_args(["--trimming-quality", "40"]) - with pytest.raises(SystemExit): - p.parse_args(["--trimming-quality", "-40"]) - - -def test_snakemake_options(): - - p = argparse.ArgumentParser() - so = SnakemakeOptions() - so._default_jobs() # test sheduler - so.add_options(p) - p.parse_args([]) - - -def test_krakenl_options(): - - p = argparse.ArgumentParser() - so = KrakenOptions() - so.add_options(p) - p.parse_args([]) - - -def test_slurm_options(): - from sequana_pipetools.options import SlurmOptions - - p = argparse.ArgumentParser() - so = SlurmOptions() - so.add_options(p) - p.parse_args([]) - - -def test_input_options(): - from sequana_pipetools.options import InputOptions - - p = argparse.ArgumentParser() - so = InputOptions() - so.add_options(p) - p.parse_args([]) - - -def test_general_options(): - from sequana_pipetools.options import GeneralOptions - - p = argparse.ArgumentParser() - so = GeneralOptions() - so.add_options(p) - p.parse_args([]) - - def test_click_general_options(): import rich_click as click