Skip to content

Conversation

@evenharder
Copy link
Contributor

Replace main typer app of pyodide-cli with click group.

As pytest depends on Typer's rich help integration which yields the origin of the subcommand,
it was implemented by subclassing click.Group. The following is an excerpt from pytest.

pyodide_cli/tests/test_cli.py::test_plugin_origin Usage: pyodide [OPTIONS] COMMAND [ARGS]...

  A command line interface for Pyodide.

  Other CLI subcommands are registered via the plugin system by installing
  Pyodide ecosystem packages (e.g. pyodide-build, pyodide-pack, auditwheel-
  emscripten, etc.)

Options:
  --version  Show the version of the Pyodide CLI
  --help     Show this message and exit.

Commands Registered by plugin-test:
  plugin_test_app   Test help message short desc
  plugin_test_cli   Test help message short desc
  plugin_test_func  Test help message short desc

Technically typer_kwargs is still supported. Perhaps we may need to introduce click_kwargs later on as a substitute.

I expect the overall behavior to be compatible, barring subtle difference due to different default behavior of click and typer.

Related: pyodide/pyodide-build#204

evenharder and others added 7 commits August 10, 2025 21:31
Typer app is now a click cli
as Typer rich panel is not supported in click,
pytest fails as package source is not printed
as click does not support `rich_help_panel` by default,
grouping by origin was reimplemented by overriding
some of the functions of click.Group
even though our mission is to drop Typer,
we need to defer breaking changes
we need to update help message anyway,
whether the module is either click command, typer app,
or any other format
introduce click cli object with custom commands in pytest
note that `test_plugin_origin` checks OriginGroup help
while `test_plugin_origin_subcommand` checks imported module's help
let click checks if name is None and raise
Copy link
Member

@ryanking13 ryanking13 left a comment

Choose a reason for hiding this comment

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

Thanks for working on this @evenharder!

Overall the change looks good. But I found that I am getting an error when I call any subcommands on my local machine. Could you please take a look on that?

image

Interestingly, --help works fine.

image


def test_plugin_origin(plugins):
output = check_output(["pyodide", "--help"]).decode("utf-8")
msg = "Registered by: plugin-test"
Copy link
Member

Choose a reason for hiding this comment

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

Why is this message changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The command section message is currently displayed as Commands Registered by <source>:, instead of Commands Registered by: <source> If the original message suits better, I will revert the change.

Copy link
Member

Choose a reason for hiding this comment

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

I see. I think this one is better. Thanks. Could you also update here too for consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The referenced message is appended to the long help message of each subcommands, which is slightly different from what test_plugin_origin is checking (it is checked in test_plugin_origin_subcommand instead). Would you like to update it as well?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think it is good to be consistent.

Comment on lines 74 to 76
# NOTE: gettext is used in click! Any i18n support?
with formatter.section(f"{gettext('Commands')}{source_desc}"):
formatter.write_dl(rows)
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this prefix. I don't think we need to care about i18n, and it looks a bit verbose to me.

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you prefer removing Commands in this case? If that's the case, I would like to preserve it only when the subcommand was added without origin.

Copy link
Member

Choose a reason for hiding this comment

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

when the subcommand was added without origin

When can this happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought the commands provided by pyodide-cli would be, please correct me if I am wrong or it is an unintended scenario.

Copy link
Member

Choose a reason for hiding this comment

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

pyodide-cli is a meta package and will not provide any subcommands.

Comment on lines 185 to 189
cmd = click.Command(
plugin_name,
callback=module,
help=help_with_origin,
)
Copy link
Member

Choose a reason for hiding this comment

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

👍

@evenharder
Copy link
Contributor Author

Overall the change looks good. But I found that I am getting an error when I call any subcommands on my local machine. Could you please take a look on that?

I am also testing on my local and it does yield some unexpected behaviors. I will investigate it a bit more.

@evenharder
Copy link
Contributor Author

Overall the change looks good. But I found that I am getting an error when I call any subcommands on my local machine. Could you please take a look on that?

I am also testing on my local and it does yield some unexpected behaviors. I will investigate it a bit more.

After some thorough investigation, non-typer functions should not be directly passed as a callback. One example is pyodide venv, which behaves like typer app, accepts typer arguments but is not a typer object. This means we have to stick to the original implementation as of now (wrap the callable in the typer object, then acquire as a click command).

@ryanking13
Copy link
Member

After some thorough investigation, non-typer functions should not be directly passed as a callback. One example is pyodide venv, which behaves like typer app, accepts typer arguments but is not a typer object. This means we have to stick to the original implementation as of now (wrap the callable in the typer object, then acquire as a click command).

Huh, I see. Thanks for the explanation. This kind of refactoring is always tricky and have many edge cases.

Copy link
Member

@ryanking13 ryanking13 left a comment

Choose a reason for hiding this comment

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

Thanks! I left some minor comments, otherwise looks good to me.

I think we can start refactoring other downstream packages after merging this. Could you please update CHANGELOG? I think we can release this with a version 0.4.0

Directly passing module as a click callback yields a weird behavior
when the module is a function which accepts yper arguments and options
In this case, their default values are neither checked nor assigned.
To properly handle, construct typer app and append as a command.
Actually this is identical to the original implementation
except registering origins!

Additionally, we have to reassign the help message of the original
command. I am not sure if direct manipulation of attribute of click is a
good idea...
Print 'Commands' section only once (if command exists)
with removal of i18n support of 'Commands'.
@evenharder
Copy link
Contributor Author

I have updated the changelog as well, please review!

@ryanking13 ryanking13 merged commit e81bb02 into pyodide:main Sep 12, 2025
3 checks passed
@ryanking13
Copy link
Member

@evenharder I just released 0.4.0. Thanks!

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.

2 participants