Skip to content

🐛 verdi code create: fix crash rendering help#7381

Open
officialasishkumar wants to merge 1 commit into
aiidateam:mainfrom
officialasishkumar:fix/7379/code-create-abstract-help
Open

🐛 verdi code create: fix crash rendering help#7381
officialasishkumar wants to merge 1 commit into
aiidateam:mainfrom
officialasishkumar:fix/7379/code-create-abstract-help

Conversation

@officialasishkumar

Copy link
Copy Markdown
Contributor

Fixes #7379

Problem

Running verdi code create -h (or --help) crashes with an UnsupportedSchemaError instead of showing the help menu:

aiida.common.exceptions.UnsupportedSchemaError: 'data.core.code.abstract.AbstractCode.' does not support CLI-based creation.

Root cause

verdi code create is a DynamicEntryPointCommandGroup that builds its subcommands from the aiida.data entry points matching core.code.*. That filter also matches core.code.abstract, which resolves to the abstract base class AbstractCode.

When click renders the group help, it calls get_command for every listed subcommand in order to extract its short help, which builds the command's options via list_options. For AbstractCode this accesses the CliModel property, which deliberately raises UnsupportedSchemaError because an abstract code cannot be instantiated. list_options reads it with getattr(cls, 'CliModel', None), but getattr only suppresses AttributeError, so the exception propagates and breaks the rendering of the whole group.

Fix

Exclude entry points whose class is abstract when listing and resolving the subcommands of a DynamicEntryPointCommandGroup. This is done with inspect.isabstract, in addition to the existing cli_exposed opt-out (used e.g. by the core.sqlite_temp storage backend). The two checks are factored into a small _is_exposed helper that is shared by list_commands and get_command.

As a result:

  • verdi code create -h now renders the help and lists the concrete plugins (core.code.installed, core.code.portable, core.code.containerized).
  • Invoking an abstract plugin directly (verdi code create core.code.abstract) now fails with the regular user-facing is not a create command error (with suggestions) instead of an internal traceback.

The fix is generic and also protects the other dynamic group (verdi profile setup) and any third-party plugin that registers an abstract base class under a CLI entry point group.

Tests

  • tests/cmdline/groups/test_dynamic.py: unit tests asserting that abstract and cli_exposed = False plugin classes are excluded from list_commands and that resolving them raises the user-facing UsageError rather than the internal UnsupportedSchemaError.
  • tests/cmdline/commands/test_code.py::test_code_create_help: regression test that verdi code create --help renders and lists the concrete code plugins but not core.code.abstract.

All of tests/cmdline/commands/test_code.py, tests/cmdline/groups/test_dynamic.py and tests/cmdline/commands/test_presto.py pass locally, and pre-commit (ruff, mypy) is clean.

@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.00%. Comparing base (a5be454) to head (e7a07da).

Files with missing lines Patch % Lines
src/aiida/cmdline/groups/dynamic.py 33.34% 4 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (a5be454) and HEAD (e7a07da). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (a5be454) HEAD (e7a07da)
3 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #7381       +/-   ##
===========================================
- Coverage   80.38%   30.00%   -50.38%     
===========================================
  Files         578      578               
  Lines       46132    46138        +6     
===========================================
- Hits        37079    13839    -23240     
- Misses       9053    32299    +23246     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

`verdi code create` is a `DynamicEntryPointCommandGroup` that builds its
subcommands from the `aiida.data` entry points matching `core.code.*`.
This includes `core.code.abstract`, which resolves to the abstract base
class `AbstractCode`. Since `AbstractCode` cannot be instantiated, its
`CliModel` property raises `UnsupportedSchemaError`, so building the
command options for it crashed the rendering of the whole group, e.g.
`verdi code create -h` or `--help`.

Exclude entry points whose class is abstract (in addition to the
existing `cli_exposed` opt-out) when listing and resolving subcommands.
Abstract plugins are now omitted from the help, and resolving one
directly fails with the regular user-facing "is not a command" error
with suggestions, instead of an internal traceback.

Fixes aiidateam#7379
@GeigerJ2 GeigerJ2 force-pushed the fix/7379/code-create-abstract-help branch from aad9ca1 to e7a07da Compare June 17, 2026 19:29
@GeigerJ2

GeigerJ2 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Thanks, @officialasishkumar. I just updated the branch (I didn't really get why CI was failing, and updating and re-triggering often can fix it), and as I did a rebase, I'm now shown as co-author. Sorry about that, I'll remove myself again when merging this, as this is your contribution =)

Comment on lines +58 to +60
assert 'core.code.installed' in result.output
assert 'core.code.portable' in result.output
assert 'core.code.abstract' not in result.output

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: Could also assert for containerized code here

Suggested change
assert 'core.code.installed' in result.output
assert 'core.code.portable' in result.output
assert 'core.code.abstract' not in result.output
assert 'core.code.containerized' in result.output
assert 'core.code.installed' in result.output
assert 'core.code.portable' in result.output
assert 'core.code.abstract' not in result.output

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Non-blocking nit on the two new tests here: test_list_commands_excludes_non_exposed and test_get_command_non_exposed share the same setup and a near-identical docstring, and they're really two facets of one contract ("non-exposed plugins are neither listed nor resolvable"). They could collapse into a fixture + one parametrized test across the subjects:

fixture + parametrized test_subcommand_exposure
@pytest.fixture
def custom_group(entry_points):
    """A dynamic group with one concrete, one abstract, and one ``cli_exposed = False`` plugin registered."""
    entry_points.add(CustomClass, 'aiida.custom:custom')
    entry_points.add(AbstractCustomClass, 'aiida.custom:abstract')
    entry_points.add(HiddenCustomClass, 'aiida.custom:hidden')
    return DynamicEntryPointCommandGroup(lambda *args, **kwargs: True, name='create', entry_point_group='aiida.custom')


@pytest.mark.parametrize(
    'cmd_name, exposed',
    [
        pytest.param('custom', True, id='concrete'),
        pytest.param('abstract', False, id='abstract'),
        pytest.param('hidden', False, id='cli_exposed_false'),
        pytest.param('non_existent', False, id='unknown'),
    ],
)
def test_subcommand_exposure(custom_group, cmd_name, exposed):
    """Only exposed plugins are listed and resolvable; abstract / ``cli_exposed = False`` / unknown are excluded.

    Regression test for https://github.com/aiidateam/aiida-core/issues/7379: an abstract entry point cannot be
    instantiated, so building its CLI options raised ``UnsupportedSchemaError`` and broke rendering of the whole
    group. Such plugins must be silently dropped from the listing and resolve to the regular user-facing
    :class:`click.exceptions.UsageError` instead of an internal traceback.
    """
    ctx = click.Context(custom_group)
    assert (cmd_name in custom_group.list_commands(ctx)) is exposed
    if exposed:
        assert custom_group.get_command(ctx, cmd_name) is not None
    else:
        with pytest.raises(click.exceptions.UsageError):
            custom_group.get_command(ctx, cmd_name)

No loss of coverage: since only those three are registered and the group has no static commands, asserting custom in / abstract not in / hidden not in pins list_commands == ['custom'] exactly, and it also folds in the positive get_command('custom') case and the non_existent path. That said, this is just my personal preference and it's your call to take it or leave it =)

@GeigerJ2

GeigerJ2 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Hm, was a bit too trigger happy here now. I see CI is failing. Ofc, we'll need to fix that before we can merge. Currently looking into it, out of curiosity. It's not immediately evident to me, from the code changes here, why it should start crashing the tests... maybe one needs to adapt some other test, elsewhere.

EDIT: found it, and I don't think it's a bug in this PR, it's surfacing a now-wrong test assertion. The 4 ci-code/tests reds are all one test, test_profile.py::test_setup[core.sqlite_temp]. _is_exposed now gates get_command too, so a cli_exposed = False backend isn't resolvable anymore, not just hidden from the menu, and verdi profile setup core.sqlite_temp used to work. That's arguably the more correct behavior, and the PR's own new test already asserts cli_exposed = False shouldn't resolve; the old test_setup is just still assuming the previous hidden-but-invocable behavior. Nothing real CLI-creates a temp profile anyway (presto, docs, test_config.py all use the create_profile() Python API directly), so the fix would be to just drop core.sqlite_temp from the test_setup parametrization.

Looping in @edan-bainglass: should cli_exposed = False mean fully non-CLI or just hidden-from-menu, per your #6990 work? If the former, it's just the single-line test adoption I mention above.

@GeigerJ2 GeigerJ2 self-requested a review June 19, 2026 09:48
@GeigerJ2

GeigerJ2 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Just posting this here, as I still had it pending locally. Thanks a lot for the PR, @officialasishkumar! Once CI passes, I'll approve again. I think this is the ideal solution (for now, up until v3, in which we redesign the Code code [pun intended] 😅). Some things I checked beyond reading the diff:

  • Dogfooded both flows. Before the PR, both verdi code create --help and verdi code create core.code.abstract raised the full UnsupportedSchemaError traceback (not just -h: rendering the help calls get_command on every listed subcommand, and the explicit form resolves it the same way). After the PR, --help renders and lists the three concrete plugins (installed/portable/containerized), and verdi code create core.code.abstract fails cleanly with `core.code.abstract` is not a create command + suggestions (exit code 2, no traceback). New tests pass locally.
  • Confirmed the root cause: CliModel is a classproperty that raises UnsupportedSchemaError, and getattr(cls, 'CliModel', None) only swallows AttributeError, so it propagates. Both halves were introduced by Update node serialization/deserialization and other Pydantic issues #6990, which merged after v2.8.0, so this bug is only on main and was never in a release (so, no need for v2.8.1, we ship it with 2.9 alongside the PR that caused it).
  • Checked whether this could surface elsewhere or wants a more global fix. The sibling dynamic group verdi profile setup is unaffected (every storage backend defines a CliModel or sets cli_exposed = False, none abstract), and DynamicEntryPointCommandGroup is the only generic consumer that builds CLI options from every entry point in a group, so fixing it there covers both groups (and future verdi ... create commands) in one place. There's no other live spot this crash can appear.
  • One alternative I explored: instead of filtering on inspect.isabstract at the group level, make the CliModel access tolerant of the raise (catch UnsupportedSchemaError, return None), and then also guard the legacy fallback. I tried this and it's whack-a-mole down the stack: an abstract class chokes at four independent layers, and suppressing one just exposes the next.
    • list_options's getattr(cls, 'CliModel', None)UnsupportedSchemaError. Catch it → None, which falls into the legacy if not CliModel: branch and calls cls.get_cli_options(), which AbstractCode doesn't define → AttributeError, so --help still crashes.
    • Guard that too (return []) and --help renders, but core.code.abstract is still listed as a command (verified) — list_options returning [] just builds an option-less command; the listing decision lives in list_commands, which this never touches.
    • Invoking it still raises: call_command has its own getattr(cls, 'CliModel', None) (separate site), and past that, create_instanceAbstractCode(**kwargs)TypeError (can't instantiate an abstract class).
    • And catching AttributeError broadly is unsafe: get_cli_options() is a real (deprecated) API, so a genuine bug/typo in a plugin's implementation would get silently swallowed as "no options".
    • The single correct gate is "is this a creatable command?", asked once in list_commands/get_command — which both keeps it out of the menu and yields the clean is not a create command error. That's exactly where this PR's isabstract filter sits.

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.

bug: verdi code create -h raises UnsupportedSchemaError and fails to show help menu

2 participants