[med-svn] [Git][med-team/q2cli][master] 8 commits: New upstream version 2022.8.0
Andreas Tille (@tille)
gitlab at salsa.debian.org
Thu Jan 12 19:19:19 GMT 2023
Andreas Tille pushed to branch master at Debian Med / q2cli
Commits:
7f1d1d25 by Nilesh Patra at 2022-09-05T19:10:39+05:30
New upstream version 2022.8.0
- - - - -
c68797f8 by Andreas Tille at 2023-01-12T18:43:13+01:00
Bump versioned Depends
- - - - -
09a6e214 by Andreas Tille at 2023-01-12T18:43:24+01:00
New upstream version 2022.11.1
- - - - -
b75e2c62 by Andreas Tille at 2023-01-12T18:43:24+01:00
routine-update: New upstream version
- - - - -
bf27048e by Andreas Tille at 2023-01-12T18:43:25+01:00
Update upstream source from tag 'upstream/2022.11.1'
Update to upstream version '2022.11.1'
with Debian dir ab30ccbd7e5889ca5006bb38811f204468f8f0b8
- - - - -
32202c40 by Andreas Tille at 2023-01-12T18:43:25+01:00
routine-update: Standards-Version: 4.6.2
- - - - -
4c45736e by Andreas Tille at 2023-01-12T18:52:33+01:00
Test-Depends: python3-sklearn
- - - - -
72b86e92 by Andreas Tille at 2023-01-12T20:17:48+01:00
Upload to experimental
- - - - -
23 changed files:
- + .github/CONTRIBUTING.md
- + .github/ISSUE_TEMPLATE/1-user-need-help.md
- + .github/ISSUE_TEMPLATE/2-dev-need-help.md
- + .github/ISSUE_TEMPLATE/3-found-bug.md
- + .github/ISSUE_TEMPLATE/4-make-better.md
- + .github/ISSUE_TEMPLATE/5-make-new.md
- + .github/ISSUE_TEMPLATE/6-where-to-go.md
- + .github/SUPPORT.md
- + .github/pull_request_template.md
- + .github/rubric.png
- + .github/workflows/add-to-project-ci.yml
- + .github/workflows/ci.yml
- debian/changelog
- debian/control
- debian/tests/control
- q2cli/_version.py
- q2cli/builtin/tools.py
- q2cli/click/type.py
- q2cli/commands.py
- + q2cli/tests/test_cache_cli.py
- q2cli/tests/test_cli.py
- q2cli/tests/test_tools.py
- q2cli/util.py
Changes:
=====================================
.github/CONTRIBUTING.md
=====================================
@@ -0,0 +1,23 @@
+# Contributing to this project
+
+Thanks for thinking of us :heart: :tada: - we would love a helping hand!
+
+## I just have a question
+
+> Note: Please don't file an issue to ask a question. You'll get faster results
+> by using the resources below.
+
+### QIIME 2 Users
+
+Check out the [User Docs](https://docs.qiime2.org) - there are many tutorials,
+walkthroughs, and guides available. If you still need help, please visit us at
+the [QIIME 2 Forum](https://forum.qiime2.org/c/user-support).
+
+### QIIME 2 Developers
+
+Check out the [Developer Docs](https://dev.qiime2.org) - there are many
+tutorials, walkthroughs, and guides available. If you still need help, please
+visit us at the [QIIME 2 Forum](https://forum.qiime2.org/c/dev-discussion).
+
+This document is based heavily on the following:
+https://github.com/atom/atom/blob/master/CONTRIBUTING.md
=====================================
.github/ISSUE_TEMPLATE/1-user-need-help.md
=====================================
@@ -0,0 +1,14 @@
+---
+name: I am a user and I need help with QIIME 2...
+about: I am using QIIME 2 and have a question or am experiencing a problem
+
+---
+
+Have you had a chance to check out the docs?
+https://docs.qiime2.org
+There are many tutorials, walkthroughs, and guides available.
+
+If you still need help, please visit:
+https://forum.qiime2.org/c/user-support
+
+Help requests filed here will not be answered.
=====================================
.github/ISSUE_TEMPLATE/2-dev-need-help.md
=====================================
@@ -0,0 +1,12 @@
+---
+name: I am a developer and I need help with QIIME 2...
+about: I am developing a QIIME 2 plugin or interface and have a question or a problem
+
+---
+
+Have you had a chance to check out the developer docs?
+https://dev.qiime2.org
+There are many tutorials, walkthroughs, and guides available.
+
+If you still need help, please visit:
+https://forum.qiime2.org/c/dev-discussion
=====================================
.github/ISSUE_TEMPLATE/3-found-bug.md
=====================================
@@ -0,0 +1,36 @@
+---
+name: I am a developer and I found a bug...
+about: I am a developer and I found a bug that I can describe
+
+---
+
+**Bug Description**
+A clear and concise description of what the bug is.
+
+**Steps to reproduce the behavior**
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Computation Environment**
+- OS: [e.g. macOS High Sierra]
+- QIIME 2 Release [e.g. 2018.6]
+
+**Questions**
+1. An enumerated list with any questions about the problem here.
+2. If not applicable, please delete this section.
+
+**Comments**
+1. An enumerated list with any other context or comments about the problem here.
+2. If not applicable, please delete this section.
+
+**References**
+1. An enumerated list of links to relevant references, including forum posts, stack overflow, etc.
+2. If not applicable, please delete this section.
=====================================
.github/ISSUE_TEMPLATE/4-make-better.md
=====================================
@@ -0,0 +1,26 @@
+---
+name: I am a developer and I have an idea for an improvement...
+about: I am a developer and I have an idea for an improvement to existing functionality
+
+---
+
+**Improvement Description**
+A clear and concise description of what the improvement is.
+
+**Current Behavior**
+Please provide a brief description of the current behavior.
+
+**Proposed Behavior**
+Please provide a brief description of the proposed behavior.
+
+**Questions**
+1. An enumerated list of questions related to the proposal.
+2. If not applicable, please delete this section.
+
+**Comments**
+1. An enumerated list of comments related to the proposal that don't fit anywhere else.
+2. If not applicable, please delete this section.
+
+**References**
+1. An enumerated list of links to relevant references, including forum posts, stack overflow, etc.
+2. If not applicable, please delete this section.
=====================================
.github/ISSUE_TEMPLATE/5-make-new.md
=====================================
@@ -0,0 +1,26 @@
+---
+name: I am a developer and I have an idea for a new feature...
+about: I am a developer and I have an idea for new functionality
+
+---
+
+**Addition Description**
+A clear and concise description of what the addition is.
+
+**Current Behavior**
+Please provide a brief description of the current behavior, if applicable.
+
+**Proposed Behavior**
+Please provide a brief description of the proposed behavior.
+
+**Questions**
+1. An enumerated list of questions related to the proposal.
+2. If not applicable, please delete this section.
+
+**Comments**
+1. An enumerated list of comments related to the proposal that don't fit anywhere else.
+2. If not applicable, please delete this section.
+
+**References**
+1. An enumerated list of links to relevant references, including forum posts, stack overflow, etc.
+2. If not applicable, please delete this section.
=====================================
.github/ISSUE_TEMPLATE/6-where-to-go.md
=====================================
@@ -0,0 +1,147 @@
+---
+name: I don't know where to file my issue...
+about: I am a developer and I don't know which repo to file this in
+
+---
+
+The repos within the QIIME 2 GitHub Organization are listed below, with a brief description about the repo.
+
+Sorted alphabetically by repo name.
+
+- The CI automation engine that builds and distributes QIIME 2
+ https://github.com/qiime2/busywork/issues
+
+- A Concourse resource for working with conda
+ https://github.com/qiime2/conda-channel-resource/issues
+
+- Web app for vanity URLs for QIIME 2 data assets
+ https://github.com/qiime2/data.qiime2.org/issues
+
+- The Developer Documentation
+ https://github.com/qiime2/dev-docs/issues
+
+- A discourse plugin for handling queued/unqueued topics
+ https://github.com/qiime2/discourse-unhandled-tagger/issues
+
+- The User Documentation
+ https://github.com/qiime2/docs/issues
+
+- Rendered QIIME 2 environment files for conda
+ https://github.com/qiime2/environment-files/issues
+
+- Google Sheets Add-On for validating tabular data
+ https://github.com/qiime2/Keemei/issues
+
+- A docker image for linux-based busywork workers
+ https://github.com/qiime2/linux-worker-docker/issues
+
+- Official project logos
+ https://github.com/qiime2/logos/issues
+
+- The q2-alignment plugin
+ https://github.com/qiime2/q2-alignment/issues
+
+- The q2-composition plugin
+ https://github.com/qiime2/q2-composition/issues
+
+- The q2-cutadapt plugin
+ https://github.com/qiime2/q2-cutadapt/issues
+
+- The q2-dada2 plugin
+ https://github.com/qiime2/q2-dada2/issues
+
+- The q2-deblur plugin
+ https://github.com/qiime2/q2-deblur/issues
+
+- The q2-demux plugin
+ https://github.com/qiime2/q2-demux/issues
+
+- The q2-diversity plugin
+ https://github.com/qiime2/q2-diversity/issues
+
+- The q2-diversity-lib plugin
+ https://github.com/qiime2/q2-diversity-lib/issues
+
+- The q2-emperor plugin
+ https://github.com/qiime2/q2-emperor/issues
+
+- The q2-feature-classifier plugin
+ https://github.com/qiime2/q2-feature-classifier/issues
+
+- The q2-feature-table plugin
+ https://github.com/qiime2/q2-feature-table/issues
+
+- The q2-fragment-insertion plugin
+ https://github.com/qiime2/q2-fragment-insertion/issues
+
+- The q2-gneiss plugin
+ https://github.com/qiime2/q2-gneiss/issues
+
+- The q2-longitudinal plugin
+ https://github.com/qiime2/q2-longitudinal/issues
+
+- The q2-metadata plugin
+ https://github.com/qiime2/q2-metadata/issues
+
+- The q2-phylogeny plugin
+ https://github.com/qiime2/q2-phylogeny/issues
+
+- The q2-quality-control plugin
+ https://github.com/qiime2/q2-quality-control/issues
+
+- The q2-quality-filter plugin
+ https://github.com/qiime2/q2-quality-filter/issues
+
+- The q2-sample-classifier plugin
+ https://github.com/qiime2/q2-sample-classifier/issues
+
+- The q2-shogun plugin
+ https://github.com/qiime2/q2-shogun/issues
+
+- The q2-taxa plugin
+ https://github.com/qiime2/q2-taxa/issues
+
+- The q2-types plugin
+ https://github.com/qiime2/q2-types/issues
+
+- The q2-vsearch plugin
+ https://github.com/qiime2/q2-vsearch/issues
+
+- The CLI interface
+ https://github.com/qiime2/q2cli/issues
+
+- The prototype CWL interface
+ https://github.com/qiime2/q2cwl/issues
+
+- The prototype Galaxy interface
+ https://github.com/qiime2/q2galaxy/issues
+
+- An internal tool for ensuring header text and copyrights are present
+ https://github.com/qiime2/q2lint/issues
+
+- The prototype GUI interface
+ https://github.com/qiime2/q2studio/issues
+
+- A base template for use in official QIIME 2 plugins
+ https://github.com/qiime2/q2templates/issues
+
+- The read-only web interface at view.qiime2.org
+ https://github.com/qiime2/q2view/issues
+
+- The QIIME 2 homepage at qiime2.org
+ https://github.com/qiime2/qiime2.github.io/issues
+
+- The QIIME 2 framework
+ https://github.com/qiime2/qiime2/issues
+
+- Centralized templates for repo assets
+ https://github.com/qiime2/template-repo/issues
+
+- Scripts for building QIIME 2 VMs
+ https://github.com/qiime2/vm-playbooks/issues
+
+- Scripts for building QIIME 2 workshop clusters
+ https://github.com/qiime2/workshop-playbooks/issues
+
+- The web app that runs workshops.qiime2.org
+ https://github.com/qiime2/workshops.qiime2.org/issues
=====================================
.github/SUPPORT.md
=====================================
@@ -0,0 +1,112 @@
+# QIIME 2 Users
+
+Check out the [User Docs](https://docs.qiime2.org) - there are many tutorials,
+walkthroughs, and guides available. If you still need help, please visit us at
+the [QIIME 2 Forum](https://forum.qiime2.org/c/user-support).
+
+# QIIME 2 Developers
+
+Check out the [Developer Docs](https://dev.qiime2.org) - there are many
+tutorials, walkthroughs, and guides available. If you still need help, please
+visit us at the [QIIME 2 Forum](https://forum.qiime2.org/c/dev-discussion).
+
+# General Bug/Issue Triage Discussion
+
+![rubric](./rubric.png?raw=true)
+
+# Projects/Repositories in the QIIME 2 GitHub Organization
+
+Sorted alphabetically by repo name.
+
+- [busywork](https://github.com/qiime2/busywork/issues)
+ | The CI automation engine that builds and distributes QIIME 2
+- [conda-channel-resource](https://github.com/qiime2/conda-channel-resource/issues)
+ | A Concourse resource for working with conda
+- [data.qiime2.org](https://github.com/qiime2/data.qiime2.org/issues)
+ | Web app for vanity URLs for QIIME 2 data assets
+- [dev-docs](https://github.com/qiime2/dev-docs/issues)
+ | The Developer Documentation
+- [discourse-unhandled-tagger](https://github.com/qiime2/discourse-unhandled-tagger/issues)
+ | A discourse plugin for handling queued/unqueued topics
+- [docs](https://github.com/qiime2/docs/issues)
+ | The User Documentation
+- [environment-files](https://github.com/qiime2/environment-files/issues)
+ | Rendered QIIME 2 environment files for conda
+- [Keemei](https://github.com/qiime2/Keemei/issues)
+ | Google Sheets Add-On for validating tabular data
+- [linux-worker-docker](https://github.com/qiime2/linux-worker-docker/issues)
+ | A docker image for linux-based busywork workers
+- [logos](https://github.com/qiime2/logos/issues)
+ | Official project logos
+- [q2-alignment](https://github.com/qiime2/q2-alignment/issues)
+ | The q2-alignment plugin
+- [q2-composition](https://github.com/qiime2/q2-composition/issues)
+ | The q2-composition plugin
+- [q2-cutadapt](https://github.com/qiime2/q2-cutadapt/issues)
+ | The q2-cutadapt plugin
+- [q2-dada2](https://github.com/qiime2/q2-dada2/issues)
+ | The q2-dada2 plugin
+- [q2-deblur](https://github.com/qiime2/q2-deblur/issues)
+ | The q2-deblur plugin
+- [q2-demux](https://github.com/qiime2/q2-demux/issues)
+ | The q2-demux plugin
+- [q2-diversity](https://github.com/qiime2/q2-diversity/issues)
+ | The q2-diversity plugin
+- [q2-diversity-lib](https://github.com/qiime2/q2-diversity-lib/issues)
+ | The q2-diversity-lib plugin
+- [q2-emperor](https://github.com/qiime2/q2-emperor/issues)
+ | The q2-emperor plugin
+- [q2-feature-classifier](https://github.com/qiime2/q2-feature-classifier/issues)
+ | The q2-feature-classifier plugin
+- [q2-feature-table](https://github.com/qiime2/q2-feature-table/issues)
+ | The q2-feature-table plugin
+- [q2-fragment-insertion](https://github.com/qiime2/q2-fragment-insertion/issues)
+ | The q2-fragment-insertion plugin
+- [q2-gneiss](https://github.com/qiime2/q2-gneiss/issues)
+ | The q2-gneiss plugin
+- [q2-longitudinal](https://github.com/qiime2/q2-longitudinal/issues)
+ | The q2-longitudinal plugin
+- [q2-metadata](https://github.com/qiime2/q2-metadata/issues)
+ | The q2-metadata plugin
+- [q2-phylogeny](https://github.com/qiime2/q2-phylogeny/issues)
+ | The q2-phylogeny plugin
+- [q2-quality-control](https://github.com/qiime2/q2-quality-control/issues)
+ | The q2-quality-control plugin
+- [q2-quality-filter](https://github.com/qiime2/q2-quality-filter/issues)
+ | The q2-quality-filter plugin
+- [q2-sample-classifier](https://github.com/qiime2/q2-sample-classifier/issues)
+ | The q2-sample-classifier plugin
+- [q2-shogun](https://github.com/qiime2/q2-shogun/issues)
+ | The q2-shogun plugin
+- [q2-taxa](https://github.com/qiime2/q2-taxa/issues)
+ | The q2-taxa plugin
+- [q2-types](https://github.com/qiime2/q2-types/issues)
+ | The q2-types plugin
+- [q2-vsearch](https://github.com/qiime2/q2-vsearch/issues)
+ | The q2-vsearch plugin
+- [q2cli](https://github.com/qiime2/q2cli/issues)
+ | The CLI interface
+- [q2cwl](https://github.com/qiime2/q2cwl/issues)
+ | The prototype CWL interface
+- [q2galaxy](https://github.com/qiime2/q2galaxy/issues)
+ | The prototype Galaxy interface
+- [q2lint](https://github.com/qiime2/q2lint/issues)
+ | An internal tool for ensuring header text and copyrights are present
+- [q2studio](https://github.com/qiime2/q2studio/issues)
+ | The prototype GUI interface
+- [q2templates](https://github.com/qiime2/q2templates/issues)
+ | A base template for use in official QIIME 2 plugins
+- [q2view](https://github.com/qiime2/q2view/issues)
+ | The read-only web interface at view.qiime2.org
+- [qiime2.github.io](https://github.com/qiime2/qiime2.github.io/issues)
+ | The QIIME 2 homepage at qiime2.org
+- [qiime2](https://github.com/qiime2/qiime2/issues)
+ | The QIIME 2 framework
+- [template-repo](https://github.com/qiime2/template-repo/issues)
+ | Centralized templates for repo assets
+- [vm-playbooks](https://github.com/qiime2/vm-playbooks/issues)
+ | Scripts for building QIIME 2 VMs
+- [workshop-playbooks](https://github.com/qiime2/workshop-playbooks/issues)
+ | Scripts for building QIIME 2 workshop clusters
+- [workshops.qiime2.org](https://github.com/qiime2/workshops.qiime2.org/issues)
+ | The web app that runs workshops.qiime2.org
=====================================
.github/pull_request_template.md
=====================================
@@ -0,0 +1,11 @@
+Brief summary of the Pull Request, including any issues it may fix using the GitHub closing syntax:
+
+https://help.github.com/articles/closing-issues-using-keywords/
+
+Also, include any co-authors or contributors using the GitHub coauthor tag:
+
+https://help.github.com/articles/creating-a-commit-with-multiple-authors/
+
+---
+
+Include any questions for reviewers, screenshots, sample outputs, etc.
=====================================
.github/rubric.png
=====================================
Binary files /dev/null and b/.github/rubric.png differ
=====================================
.github/workflows/add-to-project-ci.yml
=====================================
@@ -0,0 +1,21 @@
+name: Add new issues and PRs to triage project board
+
+on:
+ issues:
+ types:
+ - opened
+ pull_request_target:
+ types:
+ - opened
+
+jobs:
+ add-to-project:
+ name: Add issue to project
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/add-to-project at v0.3.0
+ with:
+ project-url: https://github.com/orgs/qiime2/projects/36
+ github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
+ labeled: skip-triage
+ label-operator: NOT
=====================================
.github/workflows/ci.yml
=====================================
@@ -0,0 +1,55 @@
+# This file is automatically generated by busywork.qiime2.org and
+# template-repos - any manual edits made to this file will be erased when
+# busywork performs maintenance updates.
+
+name: ci
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: checkout source
+ uses: actions/checkout at v2
+
+ - name: set up python 3.8
+ uses: actions/setup-python at v1
+ with:
+ python-version: 3.8
+
+ - name: install dependencies
+ run: python -m pip install --upgrade pip
+
+ - name: lint
+ run: |
+ pip install -q https://github.com/qiime2/q2lint/archive/master.zip
+ q2lint
+ pip install -q flake8
+ flake8
+
+ build-and-test:
+ needs: lint
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: checkout source
+ uses: actions/checkout at v2
+ with:
+ fetch-depth: 0
+
+ - name: set up git repo for versioneer
+ run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
+
+ - uses: qiime2/action-library-packaging at alpha1
+ with:
+ package-name: q2cli
+ build-target: dev
+ additional-tests: QIIMETEST= py.test --pyargs q2cli
+ library-token: ${{ secrets.LIBRARY_TOKEN }}
=====================================
debian/changelog
=====================================
@@ -1,3 +1,13 @@
+q2cli (2022.11.1-1) experimental; urgency=medium
+
+ * Team upload.
+ * New upstream version
+ * Bump versioned Depends
+ * Standards-Version: 4.6.2 (routine-update)
+ * Test-Depends: python3-sklearn
+
+ -- Andreas Tille <tille at debian.org> Thu, 12 Jan 2023 18:43:42 +0100
+
q2cli (2022.8.0-1) unstable; urgency=medium
* Team upload.
=====================================
debian/control
=====================================
@@ -9,9 +9,9 @@ Build-Depends: debhelper-compat (= 13),
python3,
python3-pytest <!nocheck>,
python3-setuptools,
- qiime (>= 2022.8.0),
+ qiime (>= 2022.11.1),
python3-click
-Standards-Version: 4.6.1
+Standards-Version: 4.6.2
Vcs-Browser: https://salsa.debian.org/med-team/q2cli
Vcs-Git: https://salsa.debian.org/med-team/q2cli.git
Homepage: https://qiime2.org/
@@ -22,8 +22,8 @@ Architecture: all
Depends: ${shlibs:Depends},
${misc:Depends},
${python3:Depends},
- q2-feature-table (>= 2021.8.0),
- qiime (>= 2021.8.0),
+ q2-feature-table (>= 2021.11.1),
+ qiime (>= 2021.11.1),
python3-setuptools,
python3-click
Description: Click-based command line interface for QIIME 2
=====================================
debian/tests/control
=====================================
@@ -1,3 +1,3 @@
Tests: run-unit-test
-Depends: @, q2-sample-classifier, file
+Depends: @, q2-sample-classifier, file, python3-sklearn
Restrictions: allow-stderr
=====================================
q2cli/_version.py
=====================================
@@ -23,9 +23,9 @@ def get_keywords():
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
- git_refnames = " (tag: 2022.8.0)"
- git_full = "469e5eb69d7fe6b213f628d299fff1545cdec9f7"
- git_date = "2022-08-22 21:21:57 +0000"
+ git_refnames = " (HEAD -> master, tag: 2022.11.1)"
+ git_full = "da2a37b63494b67a5bc7a8b9f5b226b5cfd1f921"
+ git_date = "2022-12-21 21:36:54 +0000"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
=====================================
q2cli/builtin/tools.py
=====================================
@@ -177,22 +177,69 @@ def import_data(type, input_path, output_path, input_format):
help="Display basic information about a QIIME 2 Artifact or "
"Visualization, including its UUID and type.",
cls=ToolCommand)
- at click.argument('path', type=click.Path(exists=True, file_okay=True,
- dir_okay=False, readable=True),
- metavar=_COMBO_METAVAR)
-def peek(path):
+ at click.argument('paths', nargs=-1, required=True,
+ type=click.Path(exists=True, file_okay=True, dir_okay=False,
+ readable=True), metavar=_COMBO_METAVAR)
+ at click.option('--tsv/--no-tsv', default=False,
+ help='Print as machine-readable tab separated values.')
+def peek(paths, tsv):
import qiime2.sdk
from q2cli.core.config import CONFIG
- metadata = qiime2.sdk.Result.peek(path)
+ metadatas = {os.path.basename(path):
+ qiime2.sdk.Result.peek(path) for path in paths}
- click.echo(CONFIG.cfg_style('type', "UUID")+": ", nl=False)
- click.echo(metadata.uuid)
- click.echo(CONFIG.cfg_style('type', "Type")+": ", nl=False)
- click.echo(metadata.type)
- if metadata.format is not None:
- click.echo(CONFIG.cfg_style('type', "Data format")+": ", nl=False)
- click.echo(metadata.format)
+ if tsv:
+ click.echo("Filename\tType\tUUID\tData Format")
+ for path, m in metadatas.items():
+ click.echo(f"{path}\t{m.type}\t{m.uuid}\t{m.format}")
+
+ elif len(metadatas) == 1:
+ metadata = metadatas[os.path.basename(paths[0])]
+ click.echo(CONFIG.cfg_style('type', "UUID")+": ", nl=False)
+ click.echo(metadata.uuid)
+ click.echo(CONFIG.cfg_style('type', "Type")+": ", nl=False)
+ click.echo(metadata.type)
+ if metadata.format is not None:
+ click.echo(CONFIG.cfg_style('type', "Data format")+": ", nl=False)
+ click.echo(metadata.format)
+
+ else:
+ COLUMN_FILENAME = "Filename"
+ COLUMN_TYPE = "Type"
+ COLUMN_UUID = "UUID"
+ COLUMN_DATA_FORMAT = "Data Format"
+
+ filename_width = max([len(p) for p in paths]
+ + [len(COLUMN_FILENAME)])
+ type_width = max([len(i.type) for i in metadatas.values()]
+ + [len(COLUMN_TYPE)])
+ uuid_width = max([len(i.uuid) for i in metadatas.values()]
+ + [len(COLUMN_UUID)])
+ data_format_width = \
+ max([len(i.format) if i.format is not None else 0
+ for i in metadatas.values()] + [len(COLUMN_DATA_FORMAT)])
+
+ padding = 2
+ format_string = f"{{f:<{filename_width + padding}}} " + \
+ f"{{t:<{type_width + padding}}} " + \
+ f"{{u:<{uuid_width + padding}}} " + \
+ f"{{d:<{data_format_width + padding}}}"
+
+ click.secho(
+ format_string.format(
+ f=COLUMN_FILENAME,
+ t=COLUMN_TYPE,
+ u=COLUMN_UUID,
+ d=COLUMN_DATA_FORMAT),
+ bold=True, fg="green")
+ for path, m in metadatas.items():
+ click.echo(
+ format_string.format(
+ f=path,
+ t=m.type,
+ u=m.uuid,
+ d=(m.format if m.format is not None else 'N/A')))
_COLUMN_TYPES = ['categorical', 'numeric']
@@ -324,8 +371,7 @@ def cast_metadata(paths, cast, output_file, ignore_extra,
@click.option('--tsv/--no-tsv', default=False,
help='Print as machine-readable TSV instead of text.')
@click.argument('paths', nargs=-1, required=True, metavar='METADATA...',
- type=click.Path(exists=True, file_okay=True, dir_okay=False,
- readable=True))
+ type=click.Path(file_okay=True, dir_okay=False, readable=True))
@q2cli.util.pretty_failure(traceback=None)
def inspect_metadata(paths, tsv, failure):
metadata = _merge_metadata(paths)
@@ -373,30 +419,8 @@ def inspect_metadata(paths, tsv, failure):
click.echo(metadata.column_count)
-def _load_metadata(path):
- import qiime2
- import qiime2.sdk
-
- # TODO: clean up duplication between this and the metadata handlers.
- try:
- artifact = qiime2.sdk.Result.load(path)
- except Exception:
- metadata = qiime2.Metadata.load(path)
- else:
- if isinstance(artifact, qiime2.Visualization):
- raise Exception("Visualizations cannot be viewed as QIIME 2"
- " metadata:\n%r" % path)
- elif artifact.has_metadata():
- metadata = artifact.view(qiime2.Metadata)
- else:
- raise Exception("Artifacts with type %r cannot be viewed as"
- " QIIME 2 metadata:\n%r" % (artifact.type, path))
-
- return metadata
-
-
def _merge_metadata(paths):
- m = [_load_metadata(p) for p in paths]
+ m = [q2cli.util.load_metadata(p) for p in paths]
metadata = m[0]
if m[1:]:
metadata = metadata.merge(*m[1:])
@@ -410,14 +434,15 @@ def _merge_metadata(paths):
"used after the command exits, use 'qiime tools extract'.",
cls=ToolCommand)
@click.argument('visualization-path', metavar='VISUALIZATION',
- type=click.Path(exists=True, file_okay=True, dir_okay=False,
- readable=True))
+ type=click.Path(file_okay=True, dir_okay=False, readable=True))
@click.option('--index-extension', required=False, default='html',
help='The extension of the index file that should be opened. '
'[default: html]')
def view(visualization_path, index_extension):
# Guard headless envs from having to import anything large
import sys
+ from qiime2 import Visualization
+ from q2cli.util import _load_input
from q2cli.core.config import CONFIG
if not os.getenv("DISPLAY") and sys.platform != "darwin":
raise click.UsageError(
@@ -426,16 +451,11 @@ def view(visualization_path, index_extension):
'https://view.qiime2.org, or move the Visualization to an '
'environment with a display and view it with `qiime tools view`.')
- import zipfile
- import qiime2.sdk
-
if index_extension.startswith('.'):
index_extension = index_extension[1:]
- try:
- visualization = qiime2.sdk.Visualization.load(visualization_path)
- # TODO: currently a KeyError is raised if a zipped file that is not a
- # QIIME 2 result is passed. This should be handled better by the framework.
- except (zipfile.BadZipFile, KeyError, TypeError):
+
+ visualization = _load_input(visualization_path, view=True)[0]
+ if not isinstance(visualization, Visualization):
raise click.BadParameter(
'%s is not a QIIME 2 Visualization. Only QIIME 2 Visualizations '
'can be viewed.' % visualization_path)
@@ -580,3 +600,199 @@ def citations(path):
click.echo(CONFIG.cfg_style('problem', 'No citations found.'),
err=True)
ctx.exit(1)
+
+
+ at tools.command(name='cache-create',
+ short_help='Create an empty cache at the given location.',
+ help='Create an empty cache at the given location.',
+ cls=ToolCommand)
+ at click.option('--cache', required=True,
+ type=click.Path(exists=False, readable=True),
+ help='Path to a nonexistent directory to be created as a cache.')
+def cache_create(cache):
+ from qiime2.core.cache import Cache
+ from q2cli.core.config import CONFIG
+
+ try:
+ Cache(cache)
+ except Exception as e:
+ header = "There was a problem creating a cache at '%s':" % cache
+ q2cli.util.exit_with_error(e, header=header, traceback=None)
+
+ success = "Created cache at '%s'" % cache
+ click.echo(CONFIG.cfg_style('success', success))
+
+
+ at tools.command(name='cache-remove',
+ short_help='Removes a given key from a cache.',
+ help='Removes a given key from a cache then runs garbage '
+ 'collection on the cache.',
+ cls=ToolCommand)
+ at click.option('--cache', required=True,
+ type=click.Path(exists=True, file_okay=False, dir_okay=True,
+ readable=True),
+ help='Path to an existing cache to remove the key from.')
+ at click.option('--key', required=True,
+ help='The key to remove from the cache.')
+def cache_remove(cache, key):
+ from qiime2.core.cache import Cache
+ from q2cli.core.config import CONFIG
+
+ try:
+ _cache = Cache(cache)
+ _cache.remove(key)
+ except Exception as e:
+ header = "There was a problem removing the key '%s' from the " \
+ "cache '%s':" % (key, cache)
+ q2cli.util.exit_with_error(e, header=header, traceback=None)
+
+ success = "Removed key '%s' from cache '%s'" % (key, cache)
+ click.echo(CONFIG.cfg_style('success', success))
+
+
+ at tools.command(name='cache-garbage-collection',
+ short_help='Runs garbage collection on the cache at the '
+ 'specified location.',
+ help='Runs garbage collection on the cache at the specified '
+ 'location if the specified location is a cache.',
+ cls=ToolCommand)
+ at click.option('--cache', required=True,
+ type=click.Path(exists=True, file_okay=False, dir_okay=True,
+ readable=True),
+ help='Path to an existing cache to run garbage collection on.')
+def cache_garbage_collection(cache):
+ from qiime2.core.cache import Cache
+ from q2cli.core.config import CONFIG
+
+ try:
+ _cache = Cache(cache)
+ _cache.garbage_collection()
+ except Exception as e:
+ header = "There was a problem running garbage collection on the " \
+ "cache at '%s':" % cache
+ q2cli.util.exit_with_error(e, header=header, traceback=None)
+
+ success = "Ran garbage collection on cache at '%s'" % cache
+ click.echo(CONFIG.cfg_style('success', success))
+
+
+ at tools.command(name='cache-store',
+ short_help='Stores a .qza in the cache under a key.',
+ help='Stores a .qza in the cache under a key.',
+ cls=ToolCommand)
+ at click.option('--cache', required=True,
+ type=click.Path(exists=True, file_okay=False, dir_okay=True,
+ readable=True),
+ help='Path to an existing cache to save into.')
+ at click.option('--artifact-path', required=True,
+ type=click.Path(exists=True, file_okay=True, dir_okay=False,
+ readable=True),
+ help='Path to a .qza to save into the cache.')
+ at click.option('--key', required=True,
+ help='The key to save the artifact under (must be a valid '
+ 'Python identifier).')
+def cache_store(cache, artifact_path, key):
+ from qiime2.sdk.result import Result
+ from qiime2.core.cache import Cache
+ from q2cli.core.config import CONFIG
+
+ try:
+ artifact = Result.load(artifact_path)
+ _cache = Cache(cache)
+ _cache.save(artifact, key)
+ except Exception as e:
+ header = "There was a problem saving the artifact '%s' to the cache " \
+ "'%s' under the key '%s':" % (artifact_path, cache, key)
+ q2cli.util.exit_with_error(e, header=header, traceback=None)
+
+ success = "Saved the artifact '%s' to the cache '%s' under the key " \
+ "'%s'" % (artifact_path, cache, key)
+ click.echo(CONFIG.cfg_style('success', success))
+
+
+ at tools.command(name='cache-fetch',
+ short_help='Fetches an artifact out of a cache into a .qza.',
+ help='Fetches the artifact saved to the specified cache under '
+ 'the specified key into a .qza at the specified location.',
+ cls=ToolCommand)
+ at click.option('--cache', required=True,
+ type=click.Path(exists=True, file_okay=False, dir_okay=True,
+ readable=True),
+ help='Path to an existing cache to load from.')
+ at click.option('--key', required=True,
+ help='The key to the artifact being loaded.')
+ at click.option('--output-path', required=True,
+ type=click.Path(exists=False, readable=True),
+ help='Path to put the .qza we are loading the artifact into.')
+def cache_fetch(cache, key, output_path):
+ from qiime2.core.cache import Cache
+ from q2cli.core.config import CONFIG
+
+ try:
+ _cache = Cache(cache)
+ artifact = _cache.load(key)
+ artifact.save(output_path)
+ except Exception as e:
+ header = "There was a problem loading the artifact with the key " \
+ "'%s' from the cache '%s' and saving it to the file '%s':" % \
+ key, cache, output_path
+ q2cli.util.exit_with_error(e, header=header, traceback=None)
+
+ success = "Loaded artifact with the key '%s' from the cache '%s' and " \
+ "saved it to the file '%s'" % (key, cache, output_path)
+ click.echo(CONFIG.cfg_style('success', success))
+
+
+ at tools.command(name='cache-status',
+ short_help='Checks the status of the cache.',
+ help='Lists all keys in the given cache. Peeks artifacts '
+ 'pointed to by keys to data and lists the number of '
+ 'artifacts in the pool for keys to pools.',
+ cls=ToolCommand)
+ at click.option('--cache', required=True,
+ type=click.Path(exists=True, file_okay=False, dir_okay=True,
+ readable=True),
+ help='Path to an existing cache to check the status of.')
+def cache_status(cache):
+ from qiime2.core.cache import Cache
+ from qiime2.sdk.result import Result
+
+ from q2cli.core.config import CONFIG
+
+ data_output = []
+ pool_output = []
+ try:
+ _cache = Cache(cache)
+ with _cache.lock:
+ for key in _cache.get_keys():
+ key_values = _cache.read_key(key)
+
+ if (data := key_values['data']) is not None:
+ data_output.append(
+ 'data: %s -> %s' %
+ (key, str(Result.peek(_cache.data / data))))
+ elif (pool := key_values['pool']) is not None:
+ pool_output.append(
+ 'pool: %s -> size = %s' %
+ (key, str(len(os.listdir(_cache.pools / pool)))))
+ except Exception as e:
+ header = "There was a problem getting the status of the cache at " \
+ "path '%s':" % cache
+ q2cli.util.exit_with_error(e, header=header, traceback=None)
+
+ if not data_output:
+ data_output = 'No data keys in cache'
+ else:
+ data_output = '\n'.join(data_output)
+ data_output = 'Data keys in cache:\n' + data_output
+
+ if not pool_output:
+ pool_output = 'No pool keys in cache'
+ else:
+ pool_output = '\n'.join(pool_output)
+ pool_output = 'Pool keys in cache:\n' + pool_output
+
+ output = data_output + '\n\n' + pool_output
+ success = "Status of the cache at the path '%s':\n\n%s" % \
+ (cache, output)
+ click.echo(CONFIG.cfg_style('success', success))
=====================================
q2cli/click/type.py
=====================================
@@ -46,10 +46,6 @@ class OutDirType(click.Path):
return value
-class ControlFlowException(Exception):
- pass
-
-
class QIIME2Type(click.ParamType):
def __init__(self, type_ast, type_repr, is_output=False):
self.type_repr = type_repr
@@ -84,8 +80,14 @@ class QIIME2Type(click.ParamType):
def _convert_output(self, value, param, ctx):
import os
+ from q2cli.util import output_in_cache
# Click path fails to validate writability on new paths
+ # Check if our output path is actually in a cache and if it is skip our
+ # other checks
+ if output_in_cache(value):
+ return value
+
if os.path.exists(value):
if os.path.isdir(value):
self.fail('%r is already a directory.' % (value,), param, ctx)
@@ -102,40 +104,21 @@ class QIIME2Type(click.ParamType):
def _convert_input(self, value, param, ctx):
import os
- import tempfile
import qiime2.sdk
import qiime2.sdk.util
import q2cli.util
try:
- try:
- q2cli.util.get_plugin_manager()
- result = qiime2.sdk.Result.load(value)
- except OSError as e:
- if e.errno == 28:
- temp = tempfile.tempdir
- self.fail(f'There was not enough space left on {temp!r} '
- f'to extract the artifact {value!r}. '
- '(Try setting $TMPDIR to a directory with '
- 'more space, or increasing the size of '
- f'{temp!r})', param, ctx)
- else:
- raise ControlFlowException
- except ValueError as e:
- if 'does not exist' in str(e):
- self.fail(f'{value!r} is not a valid filepath', param, ctx)
- else:
- raise ControlFlowException
- except Exception as e:
- # If we made it here, QIIME 2 was confident that the thing we
- # are trying to load is a QIIME 2 Result, however, we have run
- # into some kind of catastrophic error.
- header = ('There was a problem loading %s as a '
- 'QIIME 2 Result:' % value)
- q2cli.util.exit_with_error(e, header=header)
- except ControlFlowException:
- self.fail('%r is not a QIIME 2 Artifact (.qza)' % value, param,
- ctx)
+ result, error = q2cli.util._load_input(value)
+ except Exception as e:
+ header = f'There was a problem loading {value!r} as an artifact:'
+ q2cli.util.exit_with_error(
+ e, header=header, traceback='stderr')
+
+ if error:
+ self.fail(str(error), param, ctx)
+ # We want to use click's fail to pretty print whatever error we got
+ # from get_input
if isinstance(result, qiime2.sdk.Visualization):
maybe = value[:-1] + 'a'
@@ -158,33 +141,12 @@ class QIIME2Type(click.ParamType):
return result
def _convert_metadata(self, value, param, ctx):
- import sys
- import qiime2
import q2cli.util
if self.type_expr.name == 'MetadataColumn':
value, column = value
- fp = value
- try:
- q2cli.util.get_plugin_manager()
- artifact = qiime2.Artifact.load(fp)
- except Exception:
- try:
- metadata = qiime2.Metadata.load(fp)
- except Exception as e:
- header = ("There was an issue with loading the file %s as "
- "metadata:" % fp)
- tb = 'stderr' if '--verbose' in sys.argv else None
- q2cli.util.exit_with_error(e, header=header, traceback=tb)
- else:
- try:
- metadata = artifact.view(qiime2.Metadata)
- except Exception as e:
- header = ("There was an issue with viewing the artifact "
- "%s as QIIME 2 Metadata:" % fp)
- tb = 'stderr' if '--verbose' in sys.argv else None
- q2cli.util.exit_with_error(e, header=header, traceback=tb)
+ metadata = q2cli.util.load_metadata(value)
if self.type_expr.name != 'MetadataColumn':
return metadata
=====================================
q2cli/commands.py
=====================================
@@ -286,8 +286,21 @@ class ActionCommand(BaseCommandMixin, click.Command):
"""Called when user hits return, **kwargs are Dict[click_names, Obj]"""
import os
import qiime2.util
+ from q2cli.util import output_in_cache
+ from qiime2.core.cache import Cache
output_dir = kwargs.pop('output_dir')
+ # If they gave us a cache and key combo as an output dir, we want to
+ # error out, so we check if their output dir contains a : and the part
+ # before it is a cache
+ if output_dir:
+ potential_cache = output_dir.rsplit(':', 1)[0]
+ if potential_cache and os.path.exists(potential_cache) and \
+ Cache.is_cache(potential_cache):
+ raise ValueError(f"The given output dir '{output_dir}' "
+ "appears to be a cache:key combo. Cache keys "
+ "cannot be used as output dirs.")
+
verbose = kwargs.pop('verbose')
if verbose is None:
verbose = False
@@ -359,7 +372,14 @@ class ActionCommand(BaseCommandMixin, click.Command):
os.makedirs(output_dir)
for result, output in zip(results, outputs):
- path = result.save(output)
+ if output_in_cache(output) and output_dir is None:
+ cache_path, key = output.split(':')
+ cache = Cache(cache_path)
+ cache.save(result, key)
+ path = output
+ else:
+ path = result.save(output)
+
if not quiet:
click.echo(
CONFIG.cfg_style('success', 'Saved %s to: %s' %
=====================================
q2cli/tests/test_cache_cli.py
=====================================
@@ -0,0 +1,312 @@
+# ----------------------------------------------------------------------------
+# Copyright (c) 2016-2022, QIIME 2 development team.
+#
+# Distributed under the terms of the Modified BSD License.
+#
+# The full license is in the file LICENSE, distributed with this software.
+# ----------------------------------------------------------------------------
+
+import os.path
+import unittest
+import unittest.mock
+import tempfile
+
+from click.testing import CliRunner
+from qiime2 import Artifact
+from qiime2.core.testing.type import IntSequence1, IntSequence2, Mapping
+from qiime2.core.testing.util import get_dummy_plugin
+from qiime2.core.cache import Cache
+
+from q2cli.commands import RootCommand
+from q2cli.builtin.tools import tools
+
+
+class TestCacheCli(unittest.TestCase):
+ def setUp(self):
+ get_dummy_plugin()
+
+ self.runner = CliRunner()
+ self.plugin_command = RootCommand().get_command(
+ ctx=None, name='dummy-plugin')
+ self.tempdir = \
+ tempfile.TemporaryDirectory(prefix='qiime2-q2cli-test-temp-')
+ self.cache = Cache(os.path.join(self.tempdir.name, 'new_cache'))
+
+ self.art1 = Artifact.import_data(IntSequence1, [0, 1, 2])
+ self.art2 = Artifact.import_data(IntSequence1, [3, 4, 5])
+ self.art3 = Artifact.import_data(IntSequence2, [6, 7, 8])
+ self.mapping = Artifact.import_data(Mapping, {'a': '1', 'b': '2'})
+
+ self.non_cache_output = os.path.join(self.tempdir.name, 'output.qza')
+ self.art3_non_cache = os.path.join(self.tempdir.name, 'art3.qza')
+
+ def tearDown(self):
+ self.tempdir.cleanup()
+
+ def _run_command(self, *args):
+ return self.runner.invoke(self.plugin_command, args)
+
+ def test_inputs_from_cache(self):
+ self.cache.save(self.art1, 'art1')
+ self.cache.save(self.art2, 'art2')
+ self.cache.save(self.art3, 'art3')
+
+ art1_path = str(self.cache.path) + ':art1'
+ art2_path = str(self.cache.path) + ':art2'
+ art3_path = str(self.cache.path) + ':art3'
+
+ result = self._run_command(
+ 'concatenate-ints', '--i-ints1', art1_path, '--i-ints2', art2_path,
+ '--i-ints3', art3_path, '--p-int1', '9', '--p-int2', '10',
+ '--o-concatenated-ints', self.non_cache_output, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(Artifact.load(self.non_cache_output).view(list),
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+
+ def test_inputs_split(self):
+ self.cache.save(self.art1, 'art1')
+ self.cache.save(self.art2, 'art2')
+ self.art3.save(self.art3_non_cache)
+
+ art1_path = str(self.cache.path) + ':art1'
+ art2_path = str(self.cache.path) + ':art2'
+
+ result = self._run_command(
+ 'concatenate-ints', '--i-ints1', art1_path, '--i-ints2', art2_path,
+ '--i-ints3', self.art3_non_cache, '--p-int1', '9', '--p-int2',
+ '10', '--o-concatenated-ints', self.non_cache_output, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(Artifact.load(self.non_cache_output).view(list),
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+
+ def test_colon_in_input_path_not_cache(self):
+ art_path = os.path.join(self.tempdir.name, 'art:1.qza')
+ self.art1.save(art_path)
+
+ left_path = os.path.join(self.tempdir.name, 'left.qza')
+ right_path = os.path.join(self.tempdir.name, 'right.qza')
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art_path, '--o-left', left_path,
+ '--o-right', right_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(Artifact.load(left_path).view(list), [0])
+ self.assertEqual(Artifact.load(right_path).view(list), [1, 2])
+
+ def test_colon_in_cache_path(self):
+ cache = Cache(os.path.join(self.tempdir.name, 'new:cache'))
+ cache.save(self.art1, 'art')
+
+ art_path = str(cache.path) + ':art'
+
+ left_path = os.path.join(self.tempdir.name, 'left.qza')
+ right_path = os.path.join(self.tempdir.name, 'right.qza')
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art_path, '--o-left', left_path,
+ '--o-right', right_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(Artifact.load(left_path).view(list), [0])
+ self.assertEqual(Artifact.load(right_path).view(list), [1, 2])
+
+ def test_output_to_cache(self):
+ self.cache.save(self.art1, 'art1')
+ self.cache.save(self.art2, 'art2')
+ self.cache.save(self.art3, 'art3')
+
+ art1_path = str(self.cache.path) + ':art1'
+ art2_path = str(self.cache.path) + ':art2'
+ art3_path = str(self.cache.path) + ':art3'
+
+ out_path = str(self.cache.path) + ':out'
+
+ result = self._run_command(
+ 'concatenate-ints', '--i-ints1', art1_path, '--i-ints2', art2_path,
+ '--i-ints3', art3_path, '--p-int1', '9', '--p-int2', '10',
+ '--o-concatenated-ints', out_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(self.cache.load('out').view(list),
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+
+ def test_outputs_to_cache(self):
+ self.cache.save(self.art1, 'art1')
+ art1_path = str(self.cache.path) + ':art1'
+
+ left_path = str(self.cache.path) + ':left'
+ right_path = str(self.cache.path) + ':right'
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art1_path, '--o-left', left_path,
+ '--o-right', right_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(self.cache.load('left').view(list), [0])
+ self.assertEqual(self.cache.load('right').view(list), [1, 2])
+
+ def test_outputs_split(self):
+ self.cache.save(self.art1, 'art1')
+ art1_path = str(self.cache.path) + ':art1'
+
+ left_path = str(self.cache.path) + ':left'
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art1_path, '--o-left', left_path,
+ '--o-right', self.non_cache_output, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(self.cache.load('left').view(list), [0])
+ self.assertEqual(Artifact.load(self.non_cache_output).view(list),
+ [1, 2])
+
+ def test_invalid_cache_path_input(self):
+ art1_path = 'not_a_cache:art1'
+
+ left_path = str(self.cache.path) + ':left'
+ right_path = str(self.cache.path) + ':right'
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art1_path, '--o-left', left_path,
+ '--o-right', right_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertRegex(result.output, r"cache")
+
+ def test_invalid_cache_path_output(self):
+ self.cache.save(self.art1, 'art1')
+ art1_path = str(self.cache.path) + ':art1'
+
+ left_path = '/this/is/not_a_cache:left'
+ right_path = str(self.cache.path) + ':right'
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art1_path, '--o-left', left_path,
+ '--o-right', right_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn('does not exist', result.output)
+
+ def test_colon_in_out_path_not_cache(self):
+ self.cache.save(self.art1, 'art1')
+ self.cache.save(self.art2, 'art2')
+ self.cache.save(self.art3, 'art3')
+
+ art1_path = str(self.cache.path) + ':art1'
+ art2_path = str(self.cache.path) + ':art2'
+ art3_path = str(self.cache.path) + ':art3'
+
+ out_path = os.path.join(self.tempdir.name, 'out:put.qza')
+
+ result = self._run_command(
+ 'concatenate-ints', '--i-ints1', art1_path, '--i-ints2', art2_path,
+ '--i-ints3', art3_path, '--p-int1', '9', '--p-int2', '10',
+ '--o-concatenated-ints', out_path, '--verbose'
+ )
+
+ if result.exception:
+ raise result.exception
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(Artifact.load(out_path).view(list),
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
+
+ def test_nonexistent_input_key(self):
+ art1_path = str(self.cache.path) + ':art1'
+
+ left_path = str(self.cache.path) + ':left'
+
+ result = self._run_command(
+ 'split-ints', '--i-ints', art1_path, '--o-left', left_path,
+ '--o-right', self.non_cache_output, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn("does not contain the key 'art1'",
+ str(result.output))
+
+ def test_output_key_invalid(self):
+ self.cache.save(self.art1, 'art1')
+ self.cache.save(self.art2, 'art2')
+ self.cache.save(self.art3, 'art3')
+
+ art1_path = str(self.cache.path) + ':art1'
+ art2_path = str(self.cache.path) + ':art2'
+ art3_path = str(self.cache.path) + ':art3'
+
+ out_path = str(self.cache.path) + ':not_valid_identifier$&;'
+
+ result = self._run_command(
+ 'concatenate-ints', '--i-ints1', art1_path, '--i-ints2', art2_path,
+ '--i-ints3', art3_path, '--p-int1', '9', '--p-int2', '10',
+ '--o-concatenated-ints', out_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn('Keys must be valid Python identifiers',
+ str(result.exception))
+
+ def test_artifact_as_metadata_cache(self):
+ self.cache.save(self.mapping, 'mapping')
+ mapping_path = str(self.cache.path) + ':mapping'
+
+ result = self.runner.invoke(tools, ['inspect-metadata', mapping_path])
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn('COLUMN NAME TYPE', result.output)
+ self.assertIn("=========== ===========", result.output)
+ self.assertIn("a categorical", result.output)
+ self.assertIn("b categorical", result.output)
+ self.assertIn("IDS: 1", result.output)
+ self.assertIn("COLUMNS: 2", result.output)
+
+ def test_artifact_as_metadata_cache_bad_key(self):
+ mapping_path = str(self.cache.path) + ':mapping'
+
+ result = self.runner.invoke(tools, ['inspect-metadata', mapping_path])
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn("does not contain the key 'mapping'", result.output)
+
+ def test_artifact_as_metadata_cache_bad_cache(self):
+ result = self.runner.invoke(
+ tools, ['inspect-metadata', 'not_a_cache:key'])
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn('is not a valid cache', result.output)
+
+ def test_output_dir_as_cache(self):
+ self.cache.save(self.art1, 'art1')
+ self.cache.save(self.art2, 'art2')
+ self.cache.save(self.art3, 'art3')
+
+ art1_path = str(self.cache.path) + ':art1'
+ art2_path = str(self.cache.path) + ':art2'
+ art3_path = str(self.cache.path) + ':art3'
+
+ out_path = str(self.cache.path) + ':out'
+
+ result = self._run_command(
+ 'concatenate-ints', '--i-ints1', art1_path, '--i-ints2', art2_path,
+ '--i-ints3', art3_path, '--p-int1', '9', '--p-int2', '10',
+ '--output-dir', out_path, '--verbose'
+ )
+
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn(
+ 'Cache keys cannot be used as output dirs.', str(result.exception))
+
+
+if __name__ == "__main__":
+ unittest.main()
=====================================
q2cli/tests/test_cli.py
=====================================
@@ -16,6 +16,7 @@ import errno
from click.testing import CliRunner
from qiime2 import Artifact, Visualization
+from qiime2.core.cache import get_cache
from qiime2.core.testing.type import IntSequence1, IntSequence2
from qiime2.core.testing.util import get_dummy_plugin
@@ -56,17 +57,17 @@ class CliTests(unittest.TestCase):
# top level commands, including a plugin, are present
qiime_cli = RootCommand()
commands = qiime_cli.list_commands(ctx=None)
- self.assertTrue('info' in commands)
- self.assertTrue('tools' in commands)
- self.assertTrue('dummy-plugin' in commands)
+ self.assertIn('info', commands)
+ self.assertIn('tools', commands)
+ self.assertIn('dummy-plugin', commands)
def test_plugin_list_commands(self):
# plugin commands are present including a method and visualizer
qiime_cli = RootCommand()
command = qiime_cli.get_command(ctx=None, name='dummy-plugin')
commands = command.list_commands(ctx=None)
- self.assertTrue('split-ints' in commands)
- self.assertTrue('mapping-viz' in commands)
+ self.assertIn('split-ints', commands)
+ self.assertIn('mapping-viz', commands)
self.assertFalse('split_ints' in commands)
self.assertFalse('mapping_viz' in commands)
@@ -96,25 +97,25 @@ class CliTests(unittest.TestCase):
result = self.runner.invoke(
tools, ['import', '--show-importable-types'])
self.assertEqual(result.exit_code, 0)
- self.assertTrue('FourInts' in result.output)
- self.assertTrue('IntSequence1' in result.output)
- self.assertTrue('IntSequence2' in result.output)
- self.assertTrue('Kennel[Cat]' in result.output)
- self.assertTrue('Kennel[Dog]' in result.output)
- self.assertTrue('Mapping' in result.output)
+ self.assertIn('FourInts', result.output)
+ self.assertIn('IntSequence1', result.output)
+ self.assertIn('IntSequence2', result.output)
+ self.assertIn('Kennel[Cat]', result.output)
+ self.assertIn('Kennel[Dog]', result.output)
+ self.assertIn('Mapping', result.output)
def test_show_importable_formats(self):
result = self.runner.invoke(
tools, ['import', '--show-importable-formats'])
self.assertEqual(result.exit_code, 0)
- self.assertTrue('FourIntsDirectoryFormat' in result.output)
- self.assertTrue('IntSequenceDirectoryFormat' in result.output)
- self.assertFalse('UnimportableFormat' in result.output)
- self.assertFalse('UnimportableDirectoryFormat' in result.output)
- self.assertTrue('MappingDirectoryFormat' in result.output)
- self.assertTrue('IntSequenceFormat' in result.output)
- self.assertTrue('IntSequenceFormatV2' in result.output)
- self.assertTrue('IntSequenceV2DirectoryFormat' in result.output)
+ self.assertIn('FourIntsDirectoryFormat', result.output)
+ self.assertIn('IntSequenceDirectoryFormat', result.output)
+ self.assertNotIn('UnimportableFormat', result.output)
+ self.assertNotIn('UnimportableDirectoryFormat', result.output)
+ self.assertIn('MappingDirectoryFormat', result.output)
+ self.assertIn('IntSequenceFormat', result.output)
+ self.assertIn('IntSequenceFormatV2', result.output)
+ self.assertIn('IntSequenceV2DirectoryFormat', result.output)
def test_extract(self):
result = self.runner.invoke(
@@ -134,17 +135,17 @@ class CliTests(unittest.TestCase):
result = self.runner.invoke(
tools, ['validate', self.artifact1_path, '--level', 'min'])
self.assertEqual(result.exit_code, 0)
- self.assertTrue('appears to be valid at level=min' in result.output)
+ self.assertIn('appears to be valid at level=min', result.output)
def test_validate_max(self):
result = self.runner.invoke(
tools, ['validate', self.artifact1_path, '--level', 'max'])
self.assertEqual(result.exit_code, 0)
- self.assertTrue('appears to be valid at level=max' in result.output)
+ self.assertIn('appears to be valid at level=max', result.output)
result = self.runner.invoke(tools, ['validate', self.artifact1_path])
self.assertEqual(result.exit_code, 0)
- self.assertTrue('appears to be valid at level=max' in result.output)
+ self.assertIn('appears to be valid at level=max', result.output)
def test_split_ints(self):
qiime_cli = RootCommand()
@@ -290,18 +291,18 @@ class CliTests(unittest.TestCase):
obj = QIIME2Type(IntSequence1.to_ast(), repr(IntSequence1))
with self.assertRaisesRegex(click.exceptions.BadParameter,
- f'{self.tempdir!r} is not a QIIME 2 '
- 'Artifact'):
+ f'{self.tempdir!r} is a directory,'
+ ' not a QIIME 2 Artifact'):
obj._convert_input(self.tempdir, None, None)
with self.assertRaisesRegex(click.exceptions.BadParameter,
- "'x' is not a valid filepath"):
+ "x does not exist"):
obj._convert_input('x', None, None)
# This is to ensure the temp in the regex matches the temp used in the
# method under test in type.py
- temp = tempfile.tempdir
- with unittest.mock.patch('qiime2.sdk.Result.load',
+ temp = str(get_cache().path)
+ with unittest.mock.patch('qiime2.sdk.Result.peek',
side_effect=OSError(errno.ENOSPC,
'No space left on '
'device')):
@@ -317,15 +318,15 @@ class CliTests(unittest.TestCase):
viz_path = os.path.join(self.tempdir, 'viz')
- with unittest.mock.patch('qiime2.sdk.Result.load',
+ with unittest.mock.patch('qiime2.sdk.Result.peek',
side_effect=SyntaxError):
result = self.runner.invoke(
command, ['most-common-viz', '--i-ints', self.artifact1_path,
'--o-visualization', viz_path, '--verbose'])
self.assertEqual(result.exit_code, 1)
- self.assertTrue('problem loading' in result.output)
- self.assertTrue(self.artifact1_path in result.output)
+ self.assertIn('problem loading', result.output)
+ self.assertIn(self.artifact1_path, result.output)
def test_deprecated_help_text(self):
qiime_cli = RootCommand()
@@ -334,8 +335,8 @@ class CliTests(unittest.TestCase):
result = self.runner.invoke(command, ['deprecated-method', '--help'])
self.assertEqual(result.exit_code, 0)
- self.assertTrue('WARNING' in result.output)
- self.assertTrue('deprecated' in result.output)
+ self.assertIn('WARNING', result.output)
+ self.assertIn('deprecated', result.output)
def test_run_deprecated_gets_warning_msg(self):
qiime_cli = RootCommand()
@@ -354,7 +355,7 @@ class CliTests(unittest.TestCase):
# Just make sure that the command ran as expected
self.assertEqual(artifact.view(dict), {'foo': '43'})
- self.assertTrue('deprecated' in result.output)
+ self.assertIn('deprecated', result.output)
class TestOptionalArtifactSupport(unittest.TestCase):
@@ -612,6 +613,8 @@ class TestMetadataColumnSupport(MetadataTestsBase):
'--m-metadata-column', 'col1', '--verbose')
exp_tsv = 'id\tcol1\n#q2:types\tcategorical\n0\tfoo\nid1\tbar\n'
+ if result.exit_code != 0:
+ raise ValueError(result.exception)
self._assertMetadataOutput(
result, exp_tsv=exp_tsv,
exp_yaml="metadata: !metadata 'metadata.tsv'")
=====================================
q2cli/tests/test_tools.py
=====================================
@@ -7,6 +7,7 @@
# ----------------------------------------------------------------------------
import os
+import gc
import shutil
import unittest
import tempfile
@@ -15,8 +16,11 @@ from click.testing import CliRunner
from qiime2 import Artifact
from qiime2.core.testing.util import get_dummy_plugin
from qiime2.metadata.base import SUPPORTED_COLUMN_TYPES
+from qiime2.core.cache import Cache
+from qiime2.sdk.result import Result
-from q2cli.builtin.tools import tools, _load_metadata
+from q2cli.util import load_metadata
+from q2cli.builtin.tools import tools
from q2cli.commands import RootCommand
@@ -102,7 +106,7 @@ class TestCastMetadata(unittest.TestCase):
'--ignore-extra', '--output-file', self.output_file])
self.assertEqual(result.exit_code, 0)
- casted_metadata = _load_metadata(self.output_file)
+ casted_metadata = load_metadata(self.output_file)
self.assertNotIn('extra', casted_metadata.columns.keys())
def test_complete_successful_run(self):
@@ -111,10 +115,10 @@ class TestCastMetadata(unittest.TestCase):
'numbers:categorical', '--output-file', self.output_file])
self.assertEqual(result.exit_code, 0)
- input_metadata = _load_metadata(self.metadata_file)
+ input_metadata = load_metadata(self.metadata_file)
self.assertEqual('numeric', input_metadata.columns['numbers'].type)
- casted_metadata = _load_metadata(self.output_file)
+ casted_metadata = load_metadata(self.output_file)
self.assertEqual('categorical',
casted_metadata.columns['numbers'].type)
@@ -445,5 +449,280 @@ class TestExportToFileFormat(TestInspectMetadata):
self.assertEqual(success, result.output)
+class TestCacheTools(unittest.TestCase):
+ def setUp(self):
+ get_dummy_plugin()
+
+ self.runner = CliRunner()
+ self.plugin_command = RootCommand().get_command(
+ ctx=None, name='dummy-plugin')
+ self.tempdir = \
+ tempfile.TemporaryDirectory(prefix='qiime2-q2cli-test-temp-')
+
+ self.art1 = Artifact.import_data('IntSequence1', [0, 1, 2])
+ self.art2 = Artifact.import_data('IntSequence1', [3, 4, 5])
+ self.art3 = Artifact.import_data('IntSequence1', [6, 7, 8])
+ self.art4 = Artifact.import_data('IntSequence2', [9, 10, 11])
+ self.cache = Cache(os.path.join(self.tempdir.name, 'new_cache'))
+
+ def tearDown(self):
+ self.tempdir.cleanup()
+
+ def test_cache_create(self):
+ cache_path = os.path.join(self.tempdir.name, 'created_cache')
+
+ result = self.runner.invoke(
+ tools, ['cache-create', '--cache', cache_path])
+
+ success = "Created cache at '%s'\n" % cache_path
+ self.assertEqual(success, result.output)
+ self.assertTrue(Cache.is_cache(cache_path))
+
+ def test_cache_remove(self):
+ self.cache.save(self.art1, 'key')
+ self.assertTrue('key' in self.cache.get_keys())
+
+ result = self.runner.invoke(
+ tools,
+ ['cache-remove', '--cache', str(self.cache.path), '--key', 'key'])
+
+ success = "Removed key 'key' from cache '%s'\n" % self.cache.path
+ self.assertEqual(success, result.output)
+ self.assertFalse('key' in self.cache.get_keys())
+
+ def test_cache_garbage_collection(self):
+ # Data referenced directly by key
+ self.cache.save(self.art1, 'foo')
+ # Data referenced by pool that is referenced by key
+ pool = self.cache.create_pool(['bar'])
+ pool.save(self.art2)
+ # We will be manually deleting the keys that back these two
+ self.cache.save(self.art3, 'baz')
+ pool = self.cache.create_pool(['qux'])
+ pool.save(self.art4)
+
+ # What we expect to see before and after gc
+ expected_pre_gc_contents = \
+ set(('./VERSION', 'keys/foo', 'keys/bar',
+ 'keys/baz', 'keys/qux',
+ f'pools/bar/{self.art2.uuid}',
+ f'pools/qux/{self.art4.uuid}',
+ f'data/{self.art1.uuid}', f'data/{self.art2.uuid}',
+ f'data/{self.art3.uuid}', f'data/{self.art4.uuid}'))
+
+ expected_post_gc_contents = \
+ set(('./VERSION', 'keys/foo', 'keys/bar',
+ f'pools/bar/{self.art2.uuid}',
+ f'data/{self.art1.uuid}', f'data/{self.art2.uuid}'))
+
+ # Assert cache looks how we want pre gc
+ pre_gc_contents = _get_cache_contents(self.cache)
+ self.assertEqual(expected_pre_gc_contents, pre_gc_contents)
+
+ # Delete keys
+ self.cache.remove(self.cache.keys / 'baz')
+ self.cache.remove(self.cache.keys / 'qux')
+
+ # Make sure Python's garbage collector gets the process pool symlinks
+ # to the artifact that was keyed on baz and the one in the qux pool
+ gc.collect()
+ result = self.runner.invoke(
+ tools,
+ ['cache-garbage-collection', '--cache', str(self.cache.path)])
+
+ success = "Ran garbage collection on cache at '%s'\n" % self.cache.path
+ self.assertEqual(success, result.output)
+
+ # Assert cache looks how we want post gc
+ post_gc_contents = _get_cache_contents(self.cache)
+ self.assertEqual(expected_post_gc_contents, post_gc_contents)
+
+ def test_cache_store(self):
+ artifact = os.path.join(self.tempdir.name, 'artifact.qza')
+ self.art1.save(artifact)
+
+ result = self.runner.invoke(
+ tools, ['cache-store', '--cache', str(self.cache.path),
+ '--artifact-path', artifact, '--key', 'key'])
+
+ success = "Saved the artifact '%s' to the cache '%s' under the key " \
+ "'key'\n" % (artifact, self.cache.path)
+ self.assertEqual(success, result.output)
+
+ def test_cache_fetch(self):
+ artifact = os.path.join(self.tempdir.name, 'artifact.qza')
+ self.cache.save(self.art1, 'key')
+
+ result = self.runner.invoke(
+ tools, ['cache-fetch', '--cache', str(self.cache.path),
+ '--key', 'key', '--output-path', artifact])
+
+ success = "Loaded artifact with the key 'key' from the cache '%s' " \
+ "and saved it to the file '%s'\n" % (self.cache.path, artifact)
+ self.assertEqual(success, result.output)
+
+ def test_cache_roundtrip(self):
+ in_artifact = os.path.join(self.tempdir.name, 'in_artifact.qza')
+ out_artifact = os.path.join(self.tempdir.name, 'out_artifact.qza')
+
+ self.art1.save(in_artifact)
+
+ result = self.runner.invoke(
+ tools, ['cache-store', '--cache', str(self.cache.path),
+ '--artifact-path', in_artifact, '--key', 'key'])
+
+ success = "Saved the artifact '%s' to the cache '%s' under the key " \
+ "'key'\n" % (in_artifact, self.cache.path)
+ self.assertEqual(success, result.output)
+
+ result = self.runner.invoke(
+ tools, ['cache-fetch', '--cache', str(self.cache.path),
+ '--key', 'key', '--output-path', out_artifact])
+
+ success = "Loaded artifact with the key 'key' from the cache '%s' " \
+ "and saved it to the file '%s'\n" % (self.cache.path, out_artifact)
+ self.assertEqual(success, result.output)
+
+ artifact = Artifact.load(out_artifact)
+ self.assertEqual([0, 1, 2], artifact.view(list))
+
+ def test_cache_status(self):
+ success_template = \
+ "Status of the cache at the path '%s':\n\n%s\n\n%s\n"
+
+ # Empty cache
+ result = self.runner.invoke(
+ tools, ['cache-status', '--cache', str(self.cache.path)])
+ success = \
+ success_template % (str(self.cache.path), 'No data keys in cache',
+ 'No pool keys in cache')
+ self.assertEqual(success, result.output)
+
+ # Cache with only data
+ in_artifact = os.path.join(self.tempdir.name, 'in_artifact.qza')
+ self.art1.save(in_artifact)
+ self.runner.invoke(
+ tools, ['cache-store', '--cache', str(self.cache.path),
+ '--artifact-path', in_artifact, '--key', 'key'])
+
+ result = self.runner.invoke(
+ tools, ['cache-status', '--cache', str(self.cache.path)])
+ data_output = 'Data keys in cache:\ndata: key -> %s' % \
+ str(Result.peek(self.cache.data / str(self.art1.uuid)))
+ success = \
+ success_template % (str(self.cache.path), data_output,
+ 'No pool keys in cache')
+ self.assertEqual(success, result.output)
+
+ # Cache with data and pool
+ pool = self.cache.create_pool(keys=['pool_key'])
+ pool.save(self.art2)
+
+ result = self.runner.invoke(
+ tools, ['cache-status', '--cache', str(self.cache.path)])
+ pool_output = 'Pool keys in cache:\npool: pool_key -> size = 1'
+ success = \
+ success_template % (str(self.cache.path), data_output,
+ pool_output)
+ self.assertEqual(success, result.output)
+
+
+def _get_cache_contents(cache):
+ """Gets contents of cache not including contents of the artifacts
+ themselves relative to the root of the cache
+ """
+ cache_contents = set()
+
+ rel_keys = os.path.relpath(cache.keys, cache.path)
+ rel_data = os.path.relpath(cache.data, cache.path)
+ rel_pools = os.path.relpath(cache.pools, cache.path)
+ rel_cache = os.path.relpath(cache.path, cache.path)
+
+ for key in os.listdir(cache.keys):
+ cache_contents.add(os.path.join(rel_keys, key))
+
+ for art in os.listdir(cache.data):
+ cache_contents.add(os.path.join(rel_data, art))
+
+ for pool in os.listdir(cache.pools):
+ for link in os.listdir(os.path.join(cache.pools, pool)):
+ cache_contents.add(os.path.join(rel_pools, pool, link))
+
+ for elem in os.listdir(cache.path):
+ if os.path.isfile(os.path.join(cache.path, elem)):
+ cache_contents.add(os.path.join(rel_cache, elem))
+
+ return cache_contents
+
+
+class TestPeek(unittest.TestCase):
+ def setUp(self):
+ self.runner = CliRunner()
+ self.tempdir = tempfile.mkdtemp(prefix='qiime2-q2cli-test-temp-')
+
+ # create artifact
+ self.artifact = os.path.join(self.tempdir, 'artifact.qza')
+ Artifact.import_data(
+ 'Mapping', {'foo': 'bar'}).save(self.artifact)
+
+ # create visualization
+ qiime_cli = RootCommand()
+ command = qiime_cli.get_command(ctx=None, name='dummy-plugin')
+ self.viz = os.path.join(self.tempdir, 'viz.qzv')
+
+ self.ints = os.path.join(self.tempdir, 'ints.qza')
+ ints = Artifact.import_data(
+ 'IntSequence1', [0, 42, 43], list)
+ ints.save(self.ints)
+
+ self.runner.invoke(
+ command, ['most-common-viz', '--i-ints', self.ints,
+ '--o-visualization', self.viz, '--verbose'])
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_single_artifact(self):
+ result = self.runner.invoke(tools, ['peek', self.artifact])
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn("UUID:", result.output)
+ self.assertIn("Type:", result.output)
+ self.assertIn("Data format:", result.output)
+ self.assertEqual(result.output.count('\n'), 3)
+
+ def test_single_visualization(self):
+ result = self.runner.invoke(tools, ['peek', self.viz])
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn("UUID:", result.output)
+ self.assertIn("Type:", result.output)
+ self.assertNotIn("Data format:", result.output)
+ self.assertEqual(result.output.count('\n'), 2)
+
+ def test_artifact_and_visualization(self):
+ result = self.runner.invoke(tools, ['peek', self.artifact, self.viz])
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn("UUID", result.output)
+ self.assertIn("Type", result.output)
+ self.assertIn("Data Format", result.output)
+ self.assertIn("N/A", result.output)
+ self.assertEqual(result.output.count('\n'), 3)
+
+ def test_single_file_tsv(self):
+ result = self.runner.invoke(tools, ['peek', '--tsv', self.artifact])
+ self.assertIn("Filename\tType\tUUID\tData Format\n", result.output)
+ self.assertIn("artifact.qza", result.output)
+ self.assertEqual(result.output.count('\t'), 6)
+ self.assertEqual(result.output.count('\n'), 2)
+
+ def test_multiple_file_tsv(self):
+ result = self.runner.invoke(tools, ['peek', '--tsv', self.artifact,
+ self.viz])
+ self.assertIn("Filename\tType\tUUID\tData Format\n", result.output)
+ self.assertIn("artifact.qza", result.output)
+ self.assertIn("viz.qzv", result.output)
+ self.assertEqual(result.output.count('\t'), 9)
+ self.assertEqual(result.output.count('\n'), 3)
+
+
if __name__ == "__main__":
unittest.main()
=====================================
q2cli/util.py
=====================================
@@ -7,6 +7,10 @@
# ----------------------------------------------------------------------------
+class OutOfDisk(Exception):
+ pass
+
+
def get_app_dir():
import os
conda_prefix = os.environ.get('CONDA_PREFIX')
@@ -70,7 +74,43 @@ def exit_with_error(e, header='An error has been encountered:',
if not footer:
click.echo(err=True) # extra newline to look normal
- click.get_current_context().exit(status)
+ try:
+ click.get_current_context().exit(status)
+ except RuntimeError:
+ sys.exit(status)
+
+
+def output_in_cache(fp):
+ """Determines if an output path follows the format
+ /path_to_extant_cache:key
+ """
+ from qiime2.core.cache import Cache
+
+ # Tells us right away this isn't in a cache
+ if ':' not in fp:
+ return False
+
+ cache_path, key = _get_cache_path_and_key(fp)
+
+ try:
+ if Cache.is_cache(cache_path):
+ if not key.isidentifier():
+ raise ValueError(
+ f"Key '{key}' is not a valid Python identifier. Keys must "
+ "be valid Python identifiers. Python identifier rules may "
+ "be found here https://www.askpython.com/python/"
+ "python-identifiers-rules-best-practices")
+ else:
+ return True
+ except FileNotFoundError as e:
+ # If cache_path doesn't exist, don't treat this as a cache output
+ if 'No such file or directory' in str(e):
+ pass
+ else:
+ raise e
+
+ # We don't have a cache at all
+ return False
def get_close_matches(name, possibilities):
@@ -112,10 +152,12 @@ class pretty_failure:
return self
def __exit__(self, exc_type, exc_val, exc_tb):
- if exc_val is not None:
+ # if exit_with_error is called twice, then click.exit(1) or sys.exit(1)
+ # will happen, no need to exit_with_error again in that case.
+ if exc_val is not None and str(exc_val) != '1':
exit_with_error(exc_val, self.header, self.traceback, self.status)
- return True
+ return False
def convert_primitive(ast):
@@ -234,3 +276,157 @@ def get_plugin_manager():
return pm
return qiime2.sdk.PluginManager()
+
+
+def load_metadata(fp):
+ import qiime2
+ import sys
+
+ metadata, error = _load_metadata_artifact(fp)
+ if metadata is None:
+ try:
+ metadata = qiime2.Metadata.load(fp)
+ except Exception as e:
+ if error and ':' in fp:
+ e = error
+ header = ("There was an issue with loading the file %s as "
+ "metadata:" % fp)
+ tb = 'stderr' if '--verbose' in sys.argv else None
+ exit_with_error(e, header=header, traceback=tb)
+
+ return metadata
+
+
+def _load_metadata_artifact(fp):
+ import qiime2
+ import sys
+
+ artifact, error = _load_input(fp)
+ if isinstance(error, OutOfDisk):
+ raise error
+
+ default_tb = 'stderr'
+ # if that worked, we have an artifact or we've
+ # already raised a critical error
+ # otherwise, any normal errors can be ignored as its
+ # most likely actually metadata not a qza
+ if artifact:
+ try:
+ default_tb = None
+ if isinstance(artifact, qiime2.Visualization):
+ raise Exception(
+ 'Visualizations cannot be viewed as QIIME 2 metadata.')
+ if not artifact.has_metadata():
+ raise Exception(
+ f"Artifacts with type {artifact.type!r} cannot be viewed"
+ " as QIIME 2 metadata.")
+
+ default_tb = 'stderr'
+ return artifact.view(qiime2.Metadata), None
+
+ except Exception as e:
+ header = ("There was an issue with viewing the artifact "
+ f"{fp!r} as QIIME 2 Metadata:")
+ tb = 'stderr' if '--verbose' in sys.argv else default_tb
+ exit_with_error(e, header=header, traceback=tb)
+
+ else:
+ return None, error
+
+
+def _load_input(fp, view=False):
+ # Just initialize the plugin manager. This is slow and not necessary if we
+ # called this from qiime tools view.
+ if not view:
+ _ = get_plugin_manager()
+
+ if ':' in fp:
+ artifact, error = _load_input_cache(fp)
+ if error:
+ artifact, _ = _load_input_file(fp)
+ if artifact is not None:
+ error = None
+ # ignore this error (`_`), it was more likely
+ # a bad cache than an really weird filepath
+ else:
+ artifact, error = _load_input_file(fp)
+
+ if isinstance(error, OSError) and error.errno == 28:
+ # abort as there's nothing anyone can do about this
+ from qiime2.core.cache import get_cache
+
+ path = str(get_cache().path)
+ return None, OutOfDisk(f'There was not enough space left on {path!r} '
+ f'to use the artifact {fp!r}. (Try '
+ f'setting $TMPDIR to a directory with more '
+ f'space, or increasing the size of {path!r})')
+
+ return artifact, error
+
+
+def _load_input_cache(fp):
+ artifact = error = None
+ try:
+ artifact = try_as_cache_input(fp)
+ except Exception as e:
+ error = e
+
+ return artifact, error
+
+
+def _load_input_file(fp):
+ import qiime2.sdk
+ import os
+
+ if os.path.exists(fp) and os.path.isdir(fp):
+ return None, ValueError(
+ f"{fp!r} is a directory, not a QIIME 2 Artifact.")
+
+ # test if valid
+ peek = None
+ try:
+ peek = qiime2.sdk.Result.peek(fp)
+ except Exception as error:
+ if isinstance(error, SyntaxError):
+ raise error
+ # ideally ValueError: X is not a QIIME archive.
+ # but sometimes SyntaxError or worse
+ return None, error
+
+ # try to actually load
+ try:
+ artifact = qiime2.sdk.Result.load(fp)
+ return artifact, None
+
+ except Exception as e:
+ if peek:
+ # abort early as there's nothing else to do
+ raise ValueError(
+ "It looks like you have an Artifact but are missing the"
+ " plugin(s) necessary to load it. Artifact has type"
+ f" {peek.type!r} and format {peek.format!r}") from e
+ else:
+ error = e
+
+ return None, error
+
+
+def try_as_cache_input(fp):
+ """ Determine if an input is in a cache and load it from the cache if it is
+ """
+ import os
+ from qiime2 import Cache
+
+ cache_path, key = _get_cache_path_and_key(fp)
+
+ # We don't want to invent a new cache on disk here because if their input
+ # exists their cache must also already exist
+ if not os.path.exists(cache_path) or not Cache.is_cache(cache_path):
+ raise ValueError(f"The path {cache_path!r} is not a valid cache.")
+
+ cache = Cache(cache_path)
+ return cache.load(key)
+
+
+def _get_cache_path_and_key(fp):
+ return fp.rsplit(':', 1)
View it on GitLab: https://salsa.debian.org/med-team/q2cli/-/compare/f7facc8e6c0d9d19f48c1c03fd1a7c52cb080b27...72b86e920938b5f3246e09342ad312a86360c743
--
View it on GitLab: https://salsa.debian.org/med-team/q2cli/-/compare/f7facc8e6c0d9d19f48c1c03fd1a7c52cb080b27...72b86e920938b5f3246e09342ad312a86360c743
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20230112/6ad753b3/attachment-0001.htm>
More information about the debian-med-commit
mailing list