Writing Usage Examples#
Warning
This document is not yet formatted for Developing with QIIME 2. Some links or references may not work, and updaes are planned to the content. If you run into issues, please let us know here.
QIIME 2 enables you to write usage examples that give examples of how plugin actions can be run by your users. It uses dependency injection to generate appropriate usage examples for any interface with a āusage driverā. You define and register each usage example once, giving it a parameter that accepts some usage driver. The interface that is running your usage examples will inject one or more of its drivers into your examples, rendering interface-appropriate results.
The API is defined by the Usage class. Individual usage drivers implement the underlying behavior of API functions according to their own needs. As a result, the ExecutionUsage driver will attempt to execute your usage examples, but will disregard comments because it is not a user-facing driver. The ArtifactAPIUsage driver will include your comments as python comments in the rendered usage example, but will not execute your example.
The API is split into two sides - one which allows plugin developers to define usage examples, and one which allows interface developers to write the usage drivers that make those examples go. In this how-to guide, we will focus exclusively on the plugin-developer facing usage example side of the API.
In this how-to guide, we will cover:
Data factories for usage examples#
Because some drivers actually execute these usage examples, there is an expectation that we provide real data for them. Simple assignment is not possible. Inputs and Metadata must be created by a factory function. This allows many drivers to avoid loading data unnecessarily. Parameter literals may be passed directly, and do not require factories.
This example shows a factory function that returns a FeatureTable[Frequency]
.
We use the Python 3 API to import a biom.Table
of the appropriate artifact class.
# from q2-feature-table/examples.py
import numpy as np
from biom import Table
from qiime2 import Artifact
def ft1_factory():
return Artifact.import_data(
'FeatureTable[Frequency]',
Table(np.array([[0, 1, 3], [1, 1, 2]]),
['O1', 'O2'],
['S1', 'S2', 'S3']))
Defining usage examples#
Weāve created some data, now weāll define a usage example.
This is a simple python function with a single parameter (use
by convention).
Interfaces pass their drivers to the example through use
as described in the introduction.
The methods called inside of the function are āpublicā (non-underscore-prefixed) methods
defined in qiime2.sdk.usage.Usage
.
This āUsage APIā is common to all Usage drivers,
which reimplement the methods to meet their own needs.
# also from q2-feature-table/examples.py
def feature_table_merge_example(use):
feature_table1 = use.init_artifact('feature_table1', ft1_factory)
feature_table2 = use.init_artifact('feature_table2', ft2_factory)
merged_table, = use.action(
use.UsageAction(plugin_id='feature_table',
action_id='merge'),
use.UsageInputs(tables=[feature_table1, feature_table2]),
use.UsageOutputNames(merged_table='merged_table'),
)
First, we initialize two feature tables.
(ft2_factory
looks a lot like the ft1_factory
defined above.
Youāll have to use your imagination on the details.)
We then use a proxy method for invoking an action. The action may or may not actually be invoked, depending on implementation details in the usage driver. Beyond ensuring that your example is correct and meaningful, you donāt have to worry about this.
Note that UsageInputs
include both QIIME 2 Inputs and parameters.
Metadata must be initialized, but primitive parameters (and collections of parameters) may be passed directly.
There are examples of this in the identity_with_metadata_column_get_mdc
and variadic_input_simple
examples in the framework.
Registering usage examples#
Like QIIME 2 Actions, the usage examples we have defined must be registered in order to be used.
This registration occurs in plugin_setup.py
,
in the register_function
block for the Action whose usage we are showing.
# from q2-feature-table/plugin_setup.py
# we need to import the examples to use them
from .examples import (feature_table_merge_example,
feature_table_merge_three_tables_example)
plugin.methods.register_function(
function=q2_feature_table.merge,
inputs={'tables': List[i_table]},
# Skipping ahead to the 'examples' keyword argument
# Everything else here should look familiar
...
examples={'basic': feature_table_merge_example,
'three_tables': feature_table_merge_three_tables_example},
)
The keys in the examples
dictionary serve as unique identifiers for the examples themselves.
Some drivers (e.g. q2cli) use them to label rendered examples.
Testing usage examples#
You might be wondering how to confirm that your examples are working.
Great question!
Support for usage example testing is available via QIIME 2ās TestPluginBase.execute_examples()
and the results-and-assertions exposed by the UsageVariable
class
and optionally implemented in its driver-specific subclasses.
You can test your usage examples by making artifact class and file-contents assertions
about the UsageVariables returned by use.action
.
These may be run by any usage driver that cares about them,
allowing both local smoke testing (āCan my examples be executed successfully?ā),
and automated integration testing by interfaces like the QIIME 2 library.
Here, we assert that our results are of the expected type.
def observed_features_example(use):
ft = use.init_artifact('feature_table', ft1_factory)
# NOTE: we must unpack UsageVariables from the returned UsageOutputs
# if we wish to use their assertion methods.
a_div_vector, = use.action(
use.UsageAction(plugin_id='diversity_lib',
action_id='observed_features'),
use.UsageInputs(table=ft),
use.UsageOutputNames(vector='obs_feat_vector'))
a_div_vector.assert_output_type('SampleData[AlphaDiversity]')
If we pass the Execution driver into this function, it will execute the example, capturing actual Results. By testing that our output is of the correct type, we can assert the type of the output and in the process confirm that our example runs successfully with the given test data.
The easiest way to do this is with the execute_examples()
method on TestPluginBase
.
Including a test case that runs `execute_examples()
in your unit tests allows you to smoke test them locally by running unittest or pytest.
A note on scope:
Usage assertions are intended to allow testing of usage drivers and examples, and make it easy for developers to confirm that their examples work. Dedicated unit tests provide much more flexilibity and power, and are the preferred way to confirm that your computational methods work properly.
By adding the following to observed_features_example
,
we could confirm that our test data produced exactly the expected results when executed,
but this hack is clunky, because itās reaching beyond the intended use of this assertion.
exp = zip(sample_ids, [1, 1, 2, 2, 3])
for id, val in exp:
a_div_vector.assert_has_line_matching(
path='alpha-diversity.tsv',
expression=f'{id}\t{val}'
)
Asserting correct behavior of QIIME 2 Actions or their underlying python functions will probably result in cleaner and more maintainable tests than attempting to do the same using usage examples.
Trying it out#
Now that youāve created and registered a usage example
and confirmed that it āworksā, letās see it in action!
Weāll pretend that we just wrote the q2-feature-table
usage examples above.
Make sure your changes are present in the conda environment.
q2-feature-table
is already installed in my QIIME 2 environment, but the version in the environment came from the latest release, not my code. To include my current changes, I can reinstall by runningpip install -e .
from within the repositoryās root directory.Confirm my environment is using the right version. Before re-installing, I called
conda list q2-feature-table
to check what version ofq2-feature-table
was installed. Re-running that command now, I see the version has changed, indicating that my conda environment knows about the changes I made.Iāll check things out first with
q2cli
, so I need to refresh the cache withqiime dev refresh-cache
.Finally, I run the specific version of
qiime <plugin name> <action> --help
that Iām curious about.
>>> qiime feature-table merge --help
Usage: qiime feature-table merge [OPTIONS]
Combines feature tables using the `overlap_method` provided.
...
Examples:
# ### example: basic ###
qiime feature-table merge \
--i-tables feature_table1.qza feature_table2.qza \
--o-merged-table merged_table.qza
# ### example: three tables ###
qiime feature-table merge \
--i-tables feature_table1.qza feature_table2.qza feature_table3.qza \
--p-overlap-method sum \
--o-merged-table merged_table.qza
Note that the unique identifiers we created during example definition and registration
(e.g. āfeature_table1.qzaā, ābasicā and āthree tablesā, and āmerged_tableā)
show up in our rendered example.
Note also that q2cli
ās usage driver was clever enough to format the commands for q2cli
,
including inferring that this action would produce a .qza
file named merged_table
.
If we wanted to see what the Artifact API does with our examples, we would confirm that our conda environment included our code (as above). The cache is a q2cli thing, so we donāt need to refresh anything, and we would render the examples manually.
>>> from qiime2.plugins import feature_table, ArtifactAPIUsage
>>> # Get the examples
>>> examples = feature_table.methods.merge.examples
>>> for example in examples.values():
>>> # Create a usage driver instance
>>> use = ArtifactAPIUsage()
>>> # Inject the usage driver into the example, returning None
>>> example(use)
>>> # display the rendered example
>>> print(use.render())
Which renders the following:
from qiime2.plugins.feature_table.methods import merge
merged_table, = merge(
tables=[feature_table1, feature_table2],
)
from qiime2.plugins.feature_table.methods import merge
merged_table, = merge(
tables=[feature_table1, feature_table2, feature_table3],
overlap_method='sum',
)
The outcome here shows how we might run the merge
command in the Artifact API,
even including the correct import statement.
Comments can provide context#
For complex usage examples, you may want to provide additional context to the user. usage-annotations are available to help with this. The linked documentation provides worked examples.