Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: minor change on verdi plugin list logic to show description #6411

Merged
merged 4 commits into from
May 23, 2024
Merged

CLI: minor change on verdi plugin list logic to show description #6411

merged 4 commits into from
May 23, 2024

Conversation

khsrali
Copy link
Contributor

@khsrali khsrali commented May 22, 2024

If a plugin that is not a Process but hasn't set plugin.is_process_function=False supports get_description()
aiida-core should understand in this scenario, plugin.is_process_function is obviously False.
Currently, AttributeError could get caught by not having is_process_function instead of get_description()

@sphuber
Copy link
Contributor

sphuber commented May 22, 2024

I don't understand the problem from your description to be honest. Could you please provide an example where this failed? It would anyway be good to add that as a regression test.

@khsrali
Copy link
Contributor Author

khsrali commented May 23, 2024

I don't understand the problem from your description to be honest. Could you please provide an example where this failed? It would anyway be good to add that as a regression test.

Alright, I try to improve the description:

Suppose you write a plugin named X.

Class X(Transport):
    ..
    @classmethod
    def get_description(cls) -> str:
         return "I'm  X"
    ..

Once the plugin is installed, try verdi plugin list aiida.transports X

What you get is:
Error: No description available for X

Which means this AttributeError was handled only because is_process_function was not found in the plugin, despite the existence of get_description.

I agree, it would be great to write a test for this, but one has to know how to mock a plugin, which I don't.
Do we have a fixture for that? I would appreciate any hint.
Thank you,

@sphuber
Copy link
Contributor

sphuber commented May 23, 2024

Which means this AttributeError was handled only because is_process_function was not found in the plugin, despite the existence of get_description.

Thanks, I understand now. I missed this because I thought the conditional had and instead of or and so I mistakenly thought that if the plugin was not a process, it would have been caught by the first part of the conditional and woulnd't even get to the plugin.is_process_function.

Now that we understand that though, a better solution is not to just broadly catch this AttributeError but more explicitly defend against this in the conditional. So I would propose instead

if (inspect.isclass(plugin) and issubclass(plugin, Process)) or hasattr(plugin, 'is_process_function'):

I agree, it would be great to write a test for this, but one has to know how to mock a plugin, which I don't. Do we have a fixture for that? I would appreciate any hint.

https://aiida.readthedocs.io/projects/aiida-core/en/stable/topics/plugins.html#topics-plugins-testfixtures-entry-points

@khsrali
Copy link
Contributor Author

khsrali commented May 23, 2024

Now that we understand that though, a better solution is not to just broadly catch this AttributeError but more explicitly defend against this in the conditional. So I would propose instead

if (inspect.isclass(plugin) and issubclass(plugin, Process)) or hasattr(plugin, 'is_process_function'):

I would still check the value of is_process_function . It could be that the plugin has that attribute but the value is False
So now, the latest commit should be fine.

https://aiida.readthedocs.io/projects/aiida-core/en/stable/topics/plugins.html#topics-plugins-testfixtures-entry-points

Unfortunately, that example doesn't seem to work. pytest returns:

src/aiida/tools/pytest_fixtures/entry_points.py:46: ValueError
ValueError: invalid `entry_point_string` format, should `group:name`.

And then if I do:

    def test_parser(entry_points):
        from aiida.parsers import Parser
        from aiida.plugins import ParserFactory
    
        class CustomParser(Parser):
                return ''
    
        entry_points.add(CustomParser, 'aiida.parsers:test')
    
>       assert ParserFactory('test', CustomParser)

still, returns
aiida.common.exceptions.InvalidEntryPointTypeError: entry point `test` registered in group `aiida.parsers` is invalid because its type is not one of: Parser

I'm sorry to log the error here, but it's kind of time consuming to dig into this without further information.

@sphuber
Copy link
Contributor

sphuber commented May 23, 2024

There are more problems with the example. The assert statement also doesn't make sense. It should be

assert ParserFactory('test') is CustomParser

and then the final problem is that the CustomParser should be defined on the module level. Like it is currently, defined in the local scope of the test function, the factory will not be possible to load it. So the correct version is

from aiida.parsers import Parser

class CustomParser(Parser):
    """Parser implementation."""


def test_parser(entry_points):
    """Test a custom ``Parser`` implementation."""
    from aiida.plugins import ParserFactory

    entry_points.add(CustomParser, 'aiida.parsers:custom.parser')

    assert ParserFactory('custom.parser') is CustomParser

I will submit a PR to fix the docs.

@sphuber
Copy link
Contributor

sphuber commented May 23, 2024

#6412

Copy link
Contributor

@sphuber sphuber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @khsrali just a few minor corrections

assert ParserFactory('custom.parser') is CustomParser

result = run_cli_command(cmd_plugin.plugin_list, ['aiida.parsers', 'custom.parser'])
assert result.stdout_bytes.decode('utf-8').strip() == 'str69'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert result.stdout_bytes.decode('utf-8').strip() == 'str69'
assert result.output.strip() == 'str69'



def test_plugin_description(run_cli_command, entry_points):
"""Test if `verdi plugin list` would show description of an external plugin."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Test if `verdi plugin list` would show description of an external plugin."""
"""Test that ``verdi plugin list`` uses ``get_description`` if defined."""

Comment on lines 51 to 53
if (inspect.isclass(plugin) and issubclass(plugin, Process)) or (
False if not hasattr(plugin, 'is_process_function') else plugin.is_process_function
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This phrasing with a nested if feels a bit awkward. I think a more typical formulation would be

Suggested change
if (inspect.isclass(plugin) and issubclass(plugin, Process)) or (
False if not hasattr(plugin, 'is_process_function') else plugin.is_process_function
):
if (inspect.isclass(plugin) and issubclass(plugin, Process)) or (
hasattr(plugin, 'is_process_function') and plugin.is_process_function
):

Copy link
Contributor

@sphuber sphuber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @khsrali

@sphuber sphuber merged commit e952d77 into aiidateam:main May 23, 2024
18 of 19 checks passed
@edan-bainglass
Copy link
Member

Hmm, not sure if related to this PR, but I now get the following on verdi plugin list aiida.data core.array

Traceback (most recent call last):
  File "/opt/conda/bin/verdi", line 10, in <module>
    sys.exit(verdi())
  File "/opt/conda/lib/python3.9/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "/opt/conda/lib/python3.9/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "/opt/conda/lib/python3.9/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/opt/conda/lib/python3.9/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/opt/conda/lib/python3.9/site-packages/aiida/cmdline/groups/verdi.py", line 117, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/opt/conda/lib/python3.9/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "/opt/conda/lib/python3.9/site-packages/aiida/cmdline/commands/cmd_plugin.py", line 56, in plugin_list
    echo.echo(str(plugin.get_description()))
TypeError: get_description() missing 1 required positional argument: 'self'

Happens with a few (if not all) data entry points.

v2.6.2

@sphuber
Copy link
Contributor

sphuber commented Aug 14, 2024

It is in fact related to this PR. The PR fixed a bug where the conditional would always raise an AttributeError for non-process plugins and so would always skip and simply print that no description was available. After this fix, the code now actually goes to the else-clause and calls plugin.get_description, but this is also a bug that simply was always hidden by the first bug. The get_description method is an instance method and the plugin is a class, so that call fails. There is no get_description classmethod though that provides a description of the plugin. The best we can do I think is to simply display the docstring of the plugin

mikibonacci pushed a commit to mikibonacci/aiida-core that referenced this pull request Sep 3, 2024
…iidateam#6411)

The implementation would call `get_description` on the plugin to retrieve
a verbose description and would catch the `AttributeError` in case the
method was not defined. However, the conditional determining whether the
plugin is a process or process function was flawed and it would always
call `plugin.is_process_function` for plugins that were not processes.
This would therefore raise an `AttributeError` for any other plugins
besides process functions and so the command would always print that no
additional information was available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants