Why is my newly-registered transformer keyed by strings not types?

Hi, Folks,
I'm developing a plugin, and I'm starting to worry I'm going crazy :slight_smile: I have defined and registered a new SemanticType and a new Format to go with it (see q2-pysyndna/q2_pysyndna/_formats_and_types.py at ee98757234fa66b27cb6d6ec1a3a7848528e8b45 · AmandaBirmingham/q2-pysyndna · GitHub and q2-pysyndna/q2_pysyndna/plugin_setup.py at ee98757234fa66b27cb6d6ec1a3a7848528e8b45 · AmandaBirmingham/q2-pysyndna · GitHub) and I have defined and registered a transformer for my new Format (see q2-pysyndna/q2_pysyndna/plugin_setup.py at ee98757234fa66b27cb6d6ec1a3a7848528e8b45 · AmandaBirmingham/q2-pysyndna · GitHub). However, when I try to test this with a unit test that uses TestPluginBase's transform_format method, I find that

        _, obs_df = self.transform_format(
            SyndnaPoolCsvFormat, pandas.DataFrame,
            filename=input_fname)

... gives AssertionError: Could not find registered transformer from <class 'q2_pysyndna._formats_and_types.SyndnaPoolCsvFormat'> to <class 'pandas.core.frame.DataFrame'>.

[Edit: strike the below]
However, passing the string names of the types :

        _, obs_df = self.transform_format(
             "SyndnaPoolCsvFormat", "pandas.DataFrame",
            filename=input_fname)

[ end edit strikethrough]

However, while debugging, calling self.plugin.transformers[from_type, to_type] with the string names of the types (imitating qiime2/qiime2/plugin/testing.py at c95b12bde54170b18a80c2c95f13decfb893ee14 · qiime2/qiime2 · GitHub ) retrieves my transformer just fine! This makes good sense to me as qiime2.plugin.Plugin.register_transformer (see qiime2/qiime2/plugin/plugin.py at c95b12bde54170b18a80c2c95f13decfb893ee14 · qiime2/qiime2 · GitHub) keys the TransformerRecords using the strings from the transformer.__annotations__ dict (which, in my case, is {'ff': 'SyndnaPoolCsvFormat', 'return': 'pandas.DataFrame'}):

annotations = transformer.__annotations__.copy() 
<snip />
output = annotations.pop('return')
<snip />
input = list(annotations.values())[0]
<snip />
self.transformers[input, output] = TransformerRecord(
                transformer=transformer, plugin=self, citations=citations)

What doesn't make sense to me is why passing the actual types to transform_format seems to work for everyone else :slight_smile: I'm looking at e.g. q2-sapienns/q2_sapienns/plugin_setup.py at 474f133719b15c753bc2093969a38b6a88c7d8f7 · caporaso-lab/q2-sapienns · GitHub :

        _, obs = self.transform_format(
            HumannGeneFamilyFormat, pd.DataFrame,
            filename='humann-genefamilies-2.tsv')

I assume I need to figure out how to key my transformer like everyone else does. What am I doing wrong here??

In case it is relevant, I'm running the versions shown below:
System versions
Python version: 3.8.18
QIIME 2 release: 2023.11
QIIME 2 version: 2023.11.0.dev0+12.gc4ec793.dirty
q2cli version: 2023.9.1

So to make this just a bit more confusing, we actually guard against source_format not inheriting from FormatBase right here, and that has been there for 8 years, so passing in a string should cause an error, and if I change existing tests of this type to pass in string I do get an error.

You are completely right--the place I said I was able to use the strings to retrieve my transformer was not actually where I was able to do it (sorry, I've been tangled up in this for a while and tried a bunch of different things!). I have corrected the original post (but left the old description as struck-out for continuity). In reality, I am able to get my transformer by passing string names of the types into self.plugin.transformers[from_type, to_type] (imitating qiime2/qiime2/plugin/testing.py at c95b12bde54170b18a80c2c95f13decfb893ee14 · qiime2/qiime2 · GitHub ), when I am in the debugger stepping through the lower reaches of the self.transform_format call. Sorry for the red herring!

@Amanda_Birmingham ok good that would have been REALLY bizarre. As it stands, I unfortunately don't have any good answers for why this is happening.

@Amanda_Birmingham, we'll look into this in a little more detail follow up shortly. Sorry for the trouble!

Undoubtedly the trouble is with my code, somewhere; I just can't work out where :slight_smile:

@Amanda_Birmingham would it be possible for me to install your plugin and try to replicate this? If so can I get instruction on how to do so? I was unable to on my own. Thank you.

@Oddant1 Well, the plugin isn't really ready for installation, but for debugging purposes, I've commented out most of the functionality of the action and the dependencies on non-standard libraries so we can just try to exercise the transformer. I verified that I can install this minimal version by running git clone https://github.com/AmandaBirmingham/q2-pysyndna.git, changing to the top-level q2-pysyndna directory, and running pip install -e . I then ran the below:

(qiime2-dev) me@X q2-pysyndna % qiime dev refresh-cache
QIIME is caching your current deployment for improved performance. This may take a few moments and should only happen once per deployment.
(qiime2-dev) me@X q2-pysyndna % qiime pysyndna fit --i-syndna-concs "/Users/me/Desktop/syndna_pool.qza" --i-syndna-counts "/Users/me/Desktop/example_test.qza" --m-metadata-file "/Users/me/Desktop/example_test_metadata.tsv" --output-dir "/Users/me/Desktop/example_test"
Plugin error from pysyndna:

  issubclass() arg 1 must be a class

Debug info has been saved to /var/folders/nr/dw4r9zh155v2_19yk6kvw7800000gq/T/qiime2-q2cli-err-m47bde4a.log

The log file says:

Traceback (most recent call last):
  File "/Applications/miniconda3/envs/qiime2-dev/lib/python3.8/site-packages/q2cli/commands.py", line 520, in __call__
    results = self._execute_action(
  File "/Applications/miniconda3/envs/qiime2-dev/lib/python3.8/site-packages/q2cli/commands.py", line 581, in _execute_action
    results = action(**arguments)
  File "<decorator-gen-710>", line 2, in fit
  File "/Users/me/Work/Repositories/fork_qiime2/qiime2/sdk/action.py", line 339, in bound_callable
    self.signature.transform_and_add_callable_args_to_prov(
  File "/Users/me/Work/Repositories/fork_qiime2/qiime2/core/type/signature.py", line 391, in transform_and_add_callable_args_to_prov
    self._transform_and_add_input_to_prov(
  File "/Users/me/Work/Repositories/fork_qiime2/qiime2/core/type/signature.py", line 424, in _transform_and_add_input_to_prov
    transformed_input = _input._view(spec.view_type, recorder)
  File "/Users/me/Work/Repositories/fork_qiime2/qiime2/sdk/result.py", line 403, in _view
    to_type = transform.ModelType.from_view_type(view_type)
  File "/Users/me/Work/Repositories/fork_qiime2/qiime2/core/transform.py", line 22, in from_view_type
    if issubclass(view_type, model.base.FormatBase):
TypeError: issubclass() arg 1 must be a class

This is definitely falling down during a transform attempt, but I haven't been able to dig further yet, so any help would be greatly appreciated! Please let me know if you would like me to provide the test input files I used (syndna_pool.qza, example_test.qza, and example_test_metadata.tsv)--they are all teeny.

1 Like

@Amanda_Birmingham, thank you for setting things up so I could run the tests. @colinvwood and I figured it out. The problem is coming from the from __future__ import annotations at the top of the file. The newer version of annotations uses strings not classes.

If you want to continue using the type hints for dict and list and all that in a way that is compatible with QIIME 2 you will need to use Python's typing module.

3 Likes

@Oddant1 (and @colinvwood): Thanks so much, that was absolutely the problem! My transformer is working as expected after rewriting the type hints using the typing library's syntax and removing the __future__ import. I am now on to my next question :smiley: but I will make a new post for that. Thanks again for your in-depth help!

3 Likes

Ah - is this part of the type annotation oddity I asked about on Fix inconsistent function name; make annotation oddity explicit by peterjc · Pull Request #71 · qiime2/dev-docs · GitHub (a pull request to clarify the documentation on writing plugins)?

@peterjc, I just saw your pull request on qiime2/dev-docs. FYI, I am in the process of porting that content to a new repository (GitHub - caporaso-lab/developing-with-qiime2: Your guide for writing, testing, and distributing QIIME 2 plugins, interfaces, and documentation. ; rendered version here) as part of a complete re-write of the QIIME 2 Developer Documentation. Within the next two weeks, I expect to replace the content at https://dev.qiime2.org with the content at https://cap-lab.bio/developing-with-qiime2/.

I just pulled your suggestions from #71 over to the new docs. That should go live in the new version tomorrow, most likely. Since you seem to be very actively using the developer docs at the moment, please feel free to add issues/suggestions/etc to the new issue tracker. Thanks for the pull requests, and sorry for any confusion caused by this transition.

1 Like