[med-svn] [Git][med-team/q2cli][master] 8 commits: Fix d/rules

Andreas Tille (@tille) gitlab at salsa.debian.org
Sun Feb 18 17:15:02 GMT 2024



Andreas Tille pushed to branch master at Debian Med / q2cli


Commits:
d329db5b by Andreas Tille at 2024-02-18T15:57:35+01:00
Fix d/rules

- - - - -
2dece376 by Andreas Tille at 2024-02-18T15:57:52+01:00
New upstream version 2024.2.0
- - - - -
1152aebf by Andreas Tille at 2024-02-18T15:57:52+01:00
routine-update: New upstream version

- - - - -
402a2d1a by Andreas Tille at 2024-02-18T15:57:53+01:00
Update upstream source from tag 'upstream/2024.2.0'

Update to upstream version '2024.2.0'
with Debian dir 1159f2adba15ca759666c989782d8f961331f959
- - - - -
805b6bae by Andreas Tille at 2024-02-18T16:08:21+01:00
Fix spacing

- - - - -
ada98b66 by Andreas Tille at 2024-02-18T16:09:02+01:00
Update debian/control

- - - - -
8cef1eef by Andreas Tille at 2024-02-18T18:06:00+01:00
Drop test using options that was removed

- - - - -
50725d8b by Andreas Tille at 2024-02-18T18:10:21+01:00
Upload to unstable

- - - - -


17 changed files:

- .github/workflows/ci-dev.yaml
- README.md
- debian/changelog
- debian/control
- debian/rules
- debian/tests/run-unit-test
- q2cli/_version.py
- q2cli/builtin/tools.py
- q2cli/click/option.py
- q2cli/core/cache.py
- q2cli/core/state.py
- q2cli/core/usage.py
- q2cli/tests/test_cli.py
- q2cli/tests/test_core.py
- q2cli/tests/test_tools.py
- q2cli/tests/test_usage.py
- q2cli/util.py


Changes:

=====================================
.github/workflows/ci-dev.yaml
=====================================
@@ -9,4 +9,4 @@ jobs:
   ci:
     uses: qiime2/distributions/.github/workflows/lib-ci-dev.yaml at dev
     with:
-      distro: core
\ No newline at end of file
+      distro: amplicon


=====================================
README.md
=====================================
@@ -1,6 +1,6 @@
 # q2cli
 
-![](https://github.com/qiime2/q2cli/workflows/ci/badge.svg)
+![](https://github.com/qiime2/q2cli/workflows/ci-dev/badge.svg)
 
 A [click-based](http://click.pocoo.org/) command line interface for [QIIME
 2](https://github.com/qiime2/qiime2).


=====================================
debian/changelog
=====================================
@@ -1,11 +1,12 @@
-q2cli (2023.9.1-1) UNSTABLE; urgency=medium
+q2cli (2024.2.0-1) unstable; urgency=medium
 
   * Team upload.
   * New upstream version
   * Build-Depends: s/dh-python/dh-sequence-python3/ (routine-update)
   * Generate debian/control automatically to refresh version number
+  * Drop test using options that was removed
 
- -- Andreas Tille <tille at debian.org>  Thu, 25 Jan 2024 11:07:37 +0100
+ -- Andreas Tille <tille at debian.org>  Sun, 18 Feb 2024 18:09:55 +0100
 
 q2cli (2022.11.1-2) unstable; urgency=medium
 


=====================================
debian/control
=====================================
@@ -10,7 +10,7 @@ Build-Depends: debhelper-compat (= 13),
                python3,
                python3-pytest <!nocheck>,
                python3-setuptools,
-               qiime (>= 2023.9),
+               qiime (>= 2024.2),
                python3-click
 Standards-Version: 4.6.2
 Vcs-Browser: https://salsa.debian.org/med-team/q2cli
@@ -23,8 +23,8 @@ Architecture: all
 Depends: ${shlibs:Depends},
          ${misc:Depends},
          ${python3:Depends},
-         q2-feature-table (>= 2023.9),
-         qiime (>= 2023.9),
+         q2-feature-table (>= 2024.2),
+         qiime (>= 2024.2),
          python3-setuptools,
          python3-click
 Description: Click-based command line interface for QIIME 2


=====================================
debian/rules
=====================================
@@ -21,3 +21,16 @@ override_dh_install:
 	mkdir -p debian/$(DEB_SOURCE)/usr/share/bash-completion/completions
 	mv debian/$(DEB_SOURCE)/usr/bin/tab-qiime debian/$(DEB_SOURCE)/usr/share/bash-completion/completions/qiime
 	chmod -x debian/$(DEB_SOURCE)/usr/share/bash-completion/completions/qiime
+
+debian/control: debian/control.in
+	echo "# This file is autogenerated from control.in to update versioned dependencies" > $@
+	sed -e"s/@DEB_VERSION_UPSTREAM@/$(VERSION_UPSTREAM)/g" $< >> $@
+
+# FIXME: similarly to the qiime package, the build time testing fails, as the
+#        Qiime plugin system is not fully available at this point of the package
+#        construction.  The test suite is only run at autopkgtest time, but this
+#        is not ideal, since the only tested python version is the default one.
+override_dh_auto_test:
+ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
+	dh_auto_test --no-parallel || true
+endif


=====================================
debian/tests/run-unit-test
=====================================
@@ -69,8 +69,6 @@ set -v
 
 	qiime tools peek "$QZA_SAMPLE"
 
-	qiime tools import --show-importable-types
-	qiime tools import --show-importable-formats
 	qiime tools import \
 		--type 'FeatureTable[Frequency]' \
 		--input-path '861451cc-c250-43fc-818a-ddc2159dbe25/data' \


=====================================
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: 2023.9.1, refs/pull/314/head, Release-2023.9)"
-    git_full = "1d091d6908622c80cabeddb1dda697b2313d3eab"
-    git_date = "2023-10-04 14:13:55 +0000"
+    git_refnames = " (tag: 2024.2.0, Release-2024.2)"
+    git_full = "722997ebe6be5973716cc5f3e9f0bbd4ce722095"
+    git_date = "2024-02-16 21:49:58 +0000"
     keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
     return keywords
 


=====================================
q2cli/builtin/tools.py
=====================================
@@ -207,26 +207,6 @@ def show_formats(queries, importable, exportable, strict, tsv):
     _print_descriptions(descriptions, tsv)
 
 
-def show_importable_types(ctx, param, value):
-    if not value or ctx.resilient_parsing:
-        return
-    click.secho('This functionality has been moved to the list-types command.',
-                fg='red', bold=True)
-    click.secho('Run `qiime tools list-types --help` for more information.',
-                fg='red', bold=True)
-    ctx.exit()
-
-
-def show_importable_formats(ctx, param, value):
-    if not value or ctx.resilient_parsing:
-        return
-    click.secho('This functionality has been moved to the list-formats '
-                'command.', fg='red', bold=True)
-    click.secho('Run `qiime tools list-formats --help` for more information.',
-                fg='red', bold=True)
-    ctx.exit()
-
-
 @tools.command(name='import',
                short_help='Import data into a new QIIME 2 Artifact.',
                help="Import data to create a new QIIME 2 Artifact. See "
@@ -236,7 +216,7 @@ def show_importable_formats(ctx, param, value):
                     cls=ToolCommand)
 @click.option('--type', required=True,
               help='The semantic type of the artifact that will be created '
-                   'upon importing. Use --show-importable-types to see what '
+                   'upon importing. Use `qiime tools list-types` to see what '
                    'importable semantic types are available in the current '
                    'deployment.')
 @click.option('--input-path', required=True,
@@ -250,29 +230,22 @@ def show_importable_formats(ctx, param, value):
 @click.option('--input-format', required=False,
               help='The format of the data to be imported. If not provided, '
                    'data must be in the format expected by the semantic type '
-                   'provided via --type.')
- at click.option('--show-importable-types', is_flag=True, is_eager=True,
-              callback=show_importable_types, expose_value=False,
-              help='Show the semantic types that can be supplied to --type '
-                   'to import data into an artifact.')
- at click.option('--show-importable-formats', is_flag=True, is_eager=True,
-              callback=show_importable_formats, expose_value=False,
-              help='Show formats that can be supplied to --input-format to '
-                   'import data into an artifact.')
-def import_data(type, input_path, output_path, input_format):
-    import qiime2.sdk
-    import qiime2.plugin
+                   'provided via --type. Use `qiime tools list-formats '
+                   '--importable` to see which formats of input data are '
+                   'importable.')
+ at click.option('--validate-level', default='max',
+              type=click.Choice(['min', 'max']),
+              help='How much to validate the imported data before creating the'
+                   ' artifact. A value of "max" will generally read the entire'
+                   ' file or directory, whereas "min" will not usually do so.'
+                   ' [default: "max"]')
+def import_data(type, input_path, output_path, input_format, validate_level):
+
     from q2cli.core.config import CONFIG
-    try:
-        artifact = qiime2.sdk.Artifact.import_data(type, input_path,
-                                                   view_type=input_format)
-    except qiime2.plugin.ValidationError as e:
-        header = 'There was a problem importing %s:' % input_path
-        q2cli.util.exit_with_error(e, header=header, traceback=None)
-    except Exception as e:
-        header = 'An unexpected error has occurred:'
-        q2cli.util.exit_with_error(e, header=header)
+
+    artifact = _import(type, input_path, input_format, validate_level)
     artifact.save(output_path)
+
     if input_format is None:
         input_format = artifact.format.__name__
 
@@ -820,6 +793,75 @@ def cache_store(cache, artifact_path, key):
     click.echo(CONFIG.cfg_style('success', success))
 
 
+ at tools.command(name='cache-import',
+               short_help='Imports data into an Artifact in the cache under a '
+                          'key.',
+               help='Imports data into an Artifact in the cache under a key.',
+               cls=ToolCommand)
+ at click.option('--type', required=True,
+              help='The semantic type of the artifact that will be created '
+                   'upon importing. Use `qiime tools list-types` to see what '
+                   'importable semantic types are available in the current '
+                   'deployment.')
+ at click.option('--input-path', required=True,
+              type=click.Path(exists=True, file_okay=True, dir_okay=True,
+                              readable=True),
+              help='Path to file or directory that should be imported.')
+ 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('--key', required=True,
+              help='The key to save the artifact under (must be a valid '
+                   'Python identifier).')
+ at click.option('--input-format', required=False,
+              help='The format of the data to be imported. If not provided, '
+                   'data must be in the format expected by the semantic type '
+                   'provided via --type. Use `qiime tools list-formats '
+                   '--importable` to see which formats of input data are '
+                   'importable.')
+ at click.option('--validate-level', required=False, default='max',
+              type=click.Choice(['min', 'max']),
+              help='How much to validate the imported data before creating the'
+                   ' artifact. A value of "max" will generally read the entire'
+                   ' file or directory, whereas "min" will not usually do so.'
+                   ' [default: "max"]')
+def cache_import(type, input_path, cache, key, input_format, validate_level):
+    from qiime2 import Cache
+    from q2cli.core.config import CONFIG
+
+    artifact = _import(type, input_path, input_format, validate_level)
+    _cache = Cache(cache)
+    _cache.save(artifact, key)
+
+    if input_format is None:
+        input_format = artifact.format.__name__
+
+    success = 'Imported %s as %s to %s:%s' % (input_path,
+                                              input_format,
+                                              cache,
+                                              key)
+    click.echo(CONFIG.cfg_style('success', success))
+
+
+def _import(type, input_path, input_format, validate_level):
+    import qiime2.sdk
+    import qiime2.plugin
+
+    try:
+        artifact = qiime2.sdk.Artifact.import_data(
+            type, input_path, view_type=input_format,
+            validate_level=validate_level)
+    except qiime2.plugin.ValidationError as e:
+        header = 'There was a problem importing %s:' % input_path
+        q2cli.util.exit_with_error(e, header=header, traceback=None)
+    except Exception as e:
+        header = 'An unexpected error has occurred:'
+        q2cli.util.exit_with_error(e, header=header)
+
+    return artifact
+
+
 @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 '


=====================================
q2cli/click/option.py
=====================================
@@ -107,13 +107,22 @@ class GeneratedOption(click.Option):
 
     def _consume_metadata(self, ctx, opts):
         # double consume
+        # this consume deals with the metadata file
         md_file, source = super().consume_value(ctx, opts)
         # consume uses self.name, so mutate but backup for after
         backup, self.name = self.name, self.q2_extra_dest
-        md_col, _ = super().consume_value(ctx, opts)
+        try:
+            # this consume deals with the metadata column
+            md_col, _ = super().consume_value(ctx, opts)
+        # If `--m-metadata-column` isn't provided, need to set md_col to None
+        # in order for the click.MissingParameter errors below to be raised
+        except click.MissingParameter:
+            md_col = None
 
         self.name = backup
-
+        # These branches won't get hit unless there's a value associated with
+        # md_col - the try/except case above handled the situation where the
+        # metadata_column parameter itself wasn't provided (vs just a value)
         if (md_col is None) != (md_file is None):
             # missing one or the other
             if md_file is None:


=====================================
q2cli/core/cache.py
=====================================
@@ -161,10 +161,10 @@ class DeploymentCache:
         for entry_point in pkg_resources.iter_entry_points(
                 group='qiime2.plugins'):
             if 'QIIMETEST' in os.environ:
-                if entry_point.name == 'dummy-plugin':
+                if entry_point.name in ('dummy-plugin', 'other-plugin'):
                     reqs.add(entry_point.dist.as_requirement())
             else:
-                if entry_point.name != 'dummy-plugin':
+                if entry_point.name not in ('dummy-plugin', 'other-plugin'):
                     reqs.add(entry_point.dist.as_requirement())
 
         return reqs


=====================================
q2cli/core/state.py
=====================================
@@ -135,6 +135,8 @@ def _get_metavar(type):
         'Str': 'TEXT',
         'Float': 'NUMBER',
         'Bool': '',
+        'Jobs': 'NJOBS',
+        'Threads': 'NTHREADS',
     }
 
     style = qiime2.sdk.util.interrogate_collection_type(type)


=====================================
q2cli/core/usage.py
=====================================
@@ -173,6 +173,55 @@ class CLIUsage(usage.Usage):
 
         return variable
 
+    def construct_artifact_collection(self, name, members):
+        variable = super().construct_artifact_collection(
+            name, members
+        )
+
+        rc_dir = variable.to_interface_name()
+
+        keys = members.keys()
+        names = [name.to_interface_name() for name in members.values()]
+
+        keys_arg = '( '
+        for key in keys:
+            keys_arg += f'{key} '
+        keys_arg += ')'
+        names_arg = '( '
+        for name in names:
+            names_arg += f'{name} '
+        names_arg += ')'
+
+        lines = [
+            '## constructing result collection ##',
+            f'rc_name={rc_dir}',
+            'ext=.qza',
+            f'keys={keys_arg}',
+            f'names={names_arg}',
+            'construct_result_collection',
+            '##',
+        ]
+        self.recorder.extend(lines)
+
+        return variable
+
+    def get_artifact_collection_member(self, name, variable, key):
+        accessed_variable = super().get_artifact_collection_member(
+            name, variable, key
+        )
+
+        rc_dir = variable.to_interface_name()
+        member_fp = os.path.join(rc_dir, f'{key}.qza')
+
+        lines = [
+            '## accessing result collection member ##',
+            f'ln -s {member_fp} {accessed_variable.to_interface_name()}',
+            '##',
+        ]
+        self.recorder.extend(lines)
+
+        return variable
+
     def import_from_format(self, name, semantic_type,
                            variable, view_type=None):
         imported_var = super().import_from_format(
@@ -684,6 +733,25 @@ class ReplayCLIUsage(CLIUsage):
             self.shebang, self.header_boundary, self.copyright, self.how_to
         ))
 
+        # for creating result collections in bash
+        bash_rc_function = [
+            'construct_result_collection () {',
+            '\tmkdir $rc_name',
+            '\ttouch $rc_name.order',
+            '\tfor key in "${keys[@]}"; do',
+            '\t\techo $key >> $rc_name.order',
+            '\tdone',
+            '\tfor i in "${!keys[@]}"; do',
+            '\t\tln -s ../"${names[i]}" $rc_name"${keys[i]}"$ext',
+            '\tdone',
+            '}'
+        ]
+        self.header.extend([
+            '## function to create result collections ##',
+            *bash_rc_function,
+            '##',
+        ])
+
     def build_footer(self, dag: ProvDAG):
         '''
         Constructs a renderable footer using the terminal uuids of a ProvDAG.


=====================================
q2cli/tests/test_cli.py
=====================================
@@ -8,6 +8,7 @@
 
 import os.path
 import unittest
+import contextlib
 import unittest.mock
 import tempfile
 import shutil
@@ -52,6 +53,7 @@ class CliTests(unittest.TestCase):
         self.assertIn('System versions', result.output)
         self.assertIn('Installed plugins', result.output)
         self.assertIn('dummy-plugin', result.output)
+        self.assertIn('other-plugin', result.output)
 
     def test_list_commands(self):
         # top level commands, including a plugin, are present
@@ -60,6 +62,7 @@ class CliTests(unittest.TestCase):
         self.assertIn('info', commands)
         self.assertIn('tools', commands)
         self.assertIn('dummy-plugin', commands)
+        self.assertIn('other-plugin', commands)
 
     def test_plugin_list_commands(self):
         # plugin commands are present including a method and visualizer
@@ -566,7 +569,8 @@ class TestMetadataSupport(MetadataTestsBase):
 
 
 class TestMetadataColumnSupport(MetadataTestsBase):
-    def test_required_missing(self):
+    # Neither md file or column params provided
+    def test_required_missing_file_and_column(self):
         result = self._run_command(
             'identity-with-metadata-column', '--i-ints', self.input_artifact,
             '--o-out', self.output_artifact)
@@ -575,6 +579,27 @@ class TestMetadataColumnSupport(MetadataTestsBase):
         self.assertTrue(result.output.startswith('Usage:'))
         self.assertIn("Missing option '--m-metadata-file'", result.output)
 
+    # md file param missing, md column param & value provided
+    def test_required_missing_file(self):
+        result = self._run_command(
+            'identity-with-metadata-column', '--i-ints', self.input_artifact,
+            '--m-metadata-column', 'a', '--o-out', self.output_artifact)
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertTrue(result.output.startswith('Usage:'))
+        self.assertIn("Missing option '--m-metadata-file'", result.output)
+
+    # md file param & value provided, md column param missing
+    def test_required_missing_column(self):
+        result = self._run_command(
+            'identity-with-metadata-column', '--i-ints', self.input_artifact,
+            '--m-metadata-file', self.metadata_file1,
+            '--o-out', self.output_artifact)
+
+        self.assertEqual(result.exit_code, 1)
+        self.assertTrue(result.output.startswith('Usage:'))
+        self.assertIn("Missing option '--m-metadata-column'", result.output)
+
     def test_optional_metadata_missing(self):
         result = self._run_command(
             'identity-with-optional-metadata-column', '--i-ints',
@@ -894,6 +919,27 @@ class TestCollectionSupport(unittest.TestCase):
                       ' values. All values must be keyed or unkeyed',
                       str(result.exception))
 
+    def test_keyed_path_with_tilde(self):
+        self.art1.save(self.art1_path)
+        self.art2.save(self.art2_path)
+
+        tmp = tempfile.gettempdir()
+        tempdir = os.path.basename(self.tempdir)
+
+        with modified_environ(HOME=tmp):
+            result = self._run_command(
+                'dict-of-ints',
+                '--i-ints', f'foo:{os.path.join("~", tempdir, "art1.qza")}',
+                '--i-ints', f'bar:{os.path.join("~", tempdir, "art2.qza")}',
+                '--o-output', self.output, '--verbose')
+
+        self.assertEqual(result.exit_code, 0)
+        collection = ResultCollection.load(self.output)
+
+        self.assertEqual(collection['foo'].view(int), 0)
+        self.assertEqual(collection['bar'].view(int), 1)
+        self.assertEqual(list(collection.keys()), ['foo', 'bar'])
+
     def test_directory_with_non_artifacts(self):
         input_dir = os.path.join(self.tempdir, 'in')
         os.mkdir(input_dir)
@@ -924,5 +970,43 @@ class TestCollectionSupport(unittest.TestCase):
                       result.output)
 
 
+ at contextlib.contextmanager
+def modified_environ(*remove, **update):
+    """
+    Taken from: https://stackoverflow.com/a/34333710.
+
+    Updating the os.environ dict only modifies the environment variables from
+    the perspective of the current Python process, so this isn't dangerous at
+    all.
+
+    Temporarily updates the ``os.environ`` dictionary in-place.
+
+    The ``os.environ`` dictionary is updated in-place so that the modification
+    is sure to work in all situations.
+
+    :param remove: Environment variables to remove.
+    :param update: Dictionary of environment variables and values to
+                   add/update.
+    """
+    env = os.environ
+    update = update or {}
+    remove = remove or []
+
+    # List of environment variables being updated or removed.
+    stomped = (set(update.keys()) | set(remove)) & set(env.keys())
+    # Environment variables and values to restore on exit.
+    update_after = {k: env[k] for k in stomped}
+    # Environment variables and values to remove on exit.
+    remove_after = frozenset(k for k in update if k not in env)
+
+    try:
+        env.update(update)
+        [env.pop(k, None) for k in remove]
+        yield
+    finally:
+        env.update(update_after)
+        [env.pop(k) for k in remove_after]
+
+
 if __name__ == "__main__":
     unittest.main()


=====================================
q2cli/tests/test_core.py
=====================================
@@ -61,9 +61,9 @@ class TestOption(unittest.TestCase):
     def test_repeated_eager_option_with_callback(self):
         result = self.runner.invoke(
             q2cli.builtin.tools.tools,
-            ['import', '--show-importable-types', '--show-importable-types'])
+            ['list-types', '--tsv', '--tsv'])
 
-        self._assertRepeatedOptionError(result, '--show-importable-types')
+        self._assertRepeatedOptionError(result, '--tsv')
 
     def test_repeated_builtin_flag(self):
         result = self.runner.invoke(
@@ -412,12 +412,13 @@ class WriteReproducibilitySupplementTests(unittest.TestCase):
             self.assertTrue(zipfile.is_zipfile(out_fp))
 
             exp = {
-                'python3_replay.py',
-                'cli_replay.sh',
-                'citations.bib',
-                'recorded_metadata/',
-                'recorded_metadata/dummy_plugin_identity_with_metadata_0/'
-                'metadata_0.tsv',
+                'supplement/',
+                'supplement/python3_replay.py',
+                'supplement/cli_replay.sh',
+                'supplement/citations.bib',
+                'supplement/recorded_metadata/',
+                'supplement/recorded_metadata/'
+                'dummy_plugin_identity_with_metadata_0/metadata_0.tsv',
             }
             with zipfile.ZipFile(out_fp, 'r') as myzip:
                 namelist_set = set(myzip.namelist())
@@ -438,12 +439,13 @@ class WriteReproducibilitySupplementTests(unittest.TestCase):
             self.assertTrue(zipfile.is_zipfile(out_fp))
 
             exp = {
-                'python3_replay.py',
-                'cli_replay.sh',
-                'citations.bib',
-                'recorded_metadata/',
-                'recorded_metadata/dummy_plugin_identity_with_metadata_0/'
-                'metadata_0.tsv',
+                'supplement/',
+                'supplement/python3_replay.py',
+                'supplement/cli_replay.sh',
+                'supplement/citations.bib',
+                'supplement/recorded_metadata/',
+                'supplement/recorded_metadata/'
+                'dummy_plugin_identity_with_metadata_0/metadata_0.tsv',
             }
             with zipfile.ZipFile(out_fp, 'r') as myzip:
                 namelist_set = set(myzip.namelist())


=====================================
q2cli/tests/test_tools.py
=====================================
@@ -455,6 +455,90 @@ class TestExportToFileFormat(TestInspectMetadata):
         self.assertEqual(success, result.output)
 
 
+class TestImport(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.runner = CliRunner()
+
+        cls.tempdir = tempfile.mkdtemp(prefix='qiime2-q2cli-test-temp-')
+
+        cls.in_dir1 = os.path.join(cls.tempdir, 'input1')
+        os.mkdir(cls.in_dir1)
+        with open(os.path.join(cls.in_dir1, 'ints.txt'), 'w') as fh:
+            for i in range(5):
+                fh.write(f'{i}\n')
+            fh.write('a\n')
+
+        cls.in_dir2 = os.path.join(cls.tempdir, 'input2')
+        os.mkdir(cls.in_dir2)
+        with open(os.path.join(cls.in_dir2, 'ints.txt'), 'w') as fh:
+            fh.write('1\n')
+            fh.write('a\n')
+            fh.write('3\n')
+
+        cls.cache = Cache(os.path.join(cls.tempdir, 'new_cache'))
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.tempdir)
+
+    def test_import_min_validate(self):
+        out_fp = os.path.join(self.tempdir, 'out1.qza')
+
+        # import with min allows format error outside of min purview
+        # (validate level min checks only first 5 items)
+        result = self.runner.invoke(tools, [
+            'import', '--type', 'IntSequence1', '--input-path', self.in_dir1,
+            '--output-path', out_fp, '--validate-level', 'min'
+        ])
+        self.assertEqual(result.exit_code, 0)
+
+        # import with max should catch all format errors, max is default
+        result = self.runner.invoke(tools, [
+            'import', '--type', 'IntSequence1', '--input-path',
+            self.in_dir1, '--output-path', out_fp
+        ])
+        self.assertEqual(result.exit_code, 1)
+        self.assertIn('Line 6 is not an integer', result.output)
+
+        out_fp = os.path.join(self.tempdir, 'out2.qza')
+
+        # import with min catches format errors within its purview
+        result = self.runner.invoke(tools, [
+            'import', '--type', 'IntSequence1', '--input-path',
+            self.in_dir2, '--output-path', out_fp, '--validate-level', 'min'
+        ])
+        self.assertEqual(result.exit_code, 1)
+        self.assertIn('Line 2 is not an integer', result.output)
+
+    def test_cache_import_min_validate(self):
+        # import with min allows format error outside of min purview
+        # (validate level min checks only first 5 items)
+        result = self.runner.invoke(tools, [
+            'cache-import', '--type', 'IntSequence1', '--input-path',
+            self.in_dir1, '--cache', str(self.cache.path), '--key', 'foo',
+            '--validate-level', 'min'
+        ])
+        self.assertEqual(result.exit_code, 0)
+
+        # import with max should catch all format errors, max is default
+        result = self.runner.invoke(tools, [
+            'cache-import', '--type', 'IntSequence1', '--input-path',
+            self.in_dir1, '--cache', str(self.cache.path), '--key', 'foo'
+        ])
+        self.assertEqual(result.exit_code, 1)
+        self.assertIn('Line 6 is not an integer', result.output)
+
+        # import with min catches format errors within its purview
+        result = self.runner.invoke(tools, [
+            'cache-import', '--type', 'IntSequence1', '--input-path',
+            self.in_dir2, '--cache', str(self.cache.path), '--key', 'foo',
+            '--validate-level', 'min'
+        ])
+        self.assertEqual(result.exit_code, 1)
+        self.assertIn('Line 2 is not an integer', result.output)
+
+
 class TestCacheTools(unittest.TestCase):
     def setUp(self):
         get_dummy_plugin()
@@ -469,6 +553,8 @@ class TestCacheTools(unittest.TestCase):
         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.to_import = os.path.join(self.tempdir.name, 'to_import')
+        self.art1.export_data(self.to_import)
         self.cache = Cache(os.path.join(self.tempdir.name, 'new_cache'))
 
     def tearDown(self):
@@ -632,6 +718,16 @@ class TestCacheTools(unittest.TestCase):
                                 pool_output)
         self.assertEqual(success, result.output)
 
+    def test_cache_import(self):
+        self.max_diff = None
+        result = self.runner.invoke(
+            tools, ['cache-import', '--type', 'IntSequence1', '--input-path',
+                    self.to_import, '--cache', f'{self.cache.path}', '--key',
+                    'foo'])
+        success = 'Imported %s as IntSequenceDirectoryFormat to %s:foo\n' % \
+            (self.to_import, self.cache.path)
+        self.assertEqual(success, result.output)
+
 
 def _get_cache_contents(cache):
     """Gets contents of cache not including contents of the artifacts
@@ -1071,7 +1167,12 @@ class TestReplay(unittest.TestCase):
         self.assertEqual(result.exit_code, 0)
         self.assertTrue(zipfile.is_zipfile(out_fp))
 
-        exp = {'python3_replay.py', 'cli_replay.sh', 'citations.bib'}
+        exp = {
+            'supplement/',
+            'supplement/python3_replay.py',
+            'supplement/cli_replay.sh',
+            'supplement/citations.bib'
+        }
         with zipfile.ZipFile(out_fp, 'r') as zfh:
             self.assertEqual(exp, set(zfh.namelist()))
 
@@ -1086,13 +1187,15 @@ class TestReplay(unittest.TestCase):
         self.assertTrue(zipfile.is_zipfile(out_fp))
 
         exp = {
-            'python3_replay.py',
-            'cli_replay.sh',
-            'citations.bib',
-            'recorded_metadata/',
-            'recorded_metadata/dummy_plugin_identity_with_metadata_0/',
-            'recorded_metadata/dummy_plugin_identity_with_metadata_0/'
-            'metadata_0.tsv',
+            'supplement/',
+            'supplement/python3_replay.py',
+            'supplement/cli_replay.sh',
+            'supplement/citations.bib',
+            'supplement/recorded_metadata/',
+            'supplement/recorded_metadata/'
+            'dummy_plugin_identity_with_metadata_0/',
+            'supplement/recorded_metadata/'
+            'dummy_plugin_identity_with_metadata_0/metadata_0.tsv',
         }
         with zipfile.ZipFile(out_fp, 'r') as zfh:
             self.assertEqual(exp, set(zfh.namelist()))
@@ -1126,6 +1229,25 @@ class TestReplay(unittest.TestCase):
             'no available usage drivers', str(result.exception)
         )
 
+    def test_replay_supplement_zipfile(self):
+        with tempfile.TemporaryDirectory() as tempdir:
+            in_fp = os.path.join(self.tempdir, 'concated_ints.qza')
+            out_fp = os.path.join(tempdir, 'supplement.zip')
+
+            result = self.runner.invoke(
+                tools,
+                ['replay-supplement', '--in-fp', in_fp, '--out-fp', out_fp]
+            )
+            self.assertEqual(result.exit_code, 0)
+            self.assertTrue(zipfile.is_zipfile(out_fp))
+
+            unzipped_path = os.path.join(tempdir, 'extracted')
+            os.makedirs(unzipped_path)
+            with zipfile.ZipFile(out_fp, 'r') as zfh:
+                zfh.extractall(unzipped_path)
+
+            self.assertEqual(os.listdir(unzipped_path), ['supplement'])
+
 
 if __name__ == "__main__":
     unittest.main()


=====================================
q2cli/tests/test_usage.py
=====================================
@@ -7,8 +7,10 @@
 # ----------------------------------------------------------------------------
 
 import os
+import sys
 import subprocess
 import tempfile
+import unittest
 
 from q2cli.core.usage import CLIUsage
 
@@ -189,11 +191,44 @@ def test_round_trip(action, example):
     use = CLIUsage(enable_assertions=True)
     example_f(use)
     rendered = use.render()
+    if sys.platform.startswith('linux'):
+        # TODO: remove me when arrays are not used in shell
+        extra = dict(executable='/bin/bash')
+    else:
+        extra = dict()
     with tempfile.TemporaryDirectory() as tmpdir:
         for ref, data in use.get_example_data():
             data.save(os.path.join(tmpdir, ref))
-        subprocess.run([rendered],
+        subprocess.run(rendered,
                        shell=True,
                        check=True,
                        cwd=tmpdir,
-                       env={**os.environ})
+                       env={**os.environ},
+                       **extra)
+
+
+class ReplayResultCollectionTests(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.plugin = get_dummy_plugin()
+
+    def test_construct_and_access_collection(self):
+        action = self.plugin.actions['dict_of_ints']
+        use = CLIUsage()
+        action.examples['construct_and_access_collection'](use)
+        exp = """\
+## constructing result collection ##
+rc_name=rc-in/
+ext=.qza
+keys=( a b )
+names=( ints-a.qza ints-b.qza )
+construct_result_collection
+##
+qiime dummy-plugin dict-of-ints \\
+  --i-ints rc-in/ \\
+  --o-output rc-out/
+## accessing result collection member ##
+ln -s rc-out/b.qza ints-b-from-collection.qza
+##"""
+
+        self.assertEqual(exp, use.render())


=====================================
q2cli/util.py
=====================================
@@ -471,6 +471,10 @@ def _load_input_cache(fp):
 
 def _load_input_file(fp):
     import qiime2.sdk
+    from os.path import expanduser
+
+    # If there is a leading ~ we expand it to be the path to home
+    fp = expanduser(fp)
 
     # test if valid
     peek = None



View it on GitLab: https://salsa.debian.org/med-team/q2cli/-/compare/74297f3ce018e05a62569fa62b3b55ab3e8085d6...50725d8b42099f78c99fb1eeedc2bce2d6a941a6

-- 
View it on GitLab: https://salsa.debian.org/med-team/q2cli/-/compare/74297f3ce018e05a62569fa62b3b55ab3e8085d6...50725d8b42099f78c99fb1eeedc2bce2d6a941a6
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/20240218/b63627a4/attachment-0001.htm>


More information about the debian-med-commit mailing list