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

Improve deprecated messages from verdi setup and verdi code setup #6433

Merged
merged 1 commit into from
Jun 6, 2024

Conversation

agoscinski
Copy link
Contributor

@agoscinski agoscinski commented May 30, 2024

Goal

The two main problems I wanted to solve was to add a deprecation warning to the help page of verdi setup and verdi code setup (easy) as well as printing the deprecation warning before the user has to go through all the prompts (hard). The solution became quite complicated, so I made a second solution that does not do this and just accepts that it is printed afterwards. There are two commits, one for each solution.

Question

Right now my question is if the complexity introduced by making a new class VerdiCommand is worth to have the warning before the prompts. I am not sure. If we choose the second solution (warning comes after prompts), I would change some things (bring back the deprecated_command decorater as it gives us more flexibility).

Technical aspects about the solution printing the warning before the prompt

Setting the deprecated flag from click.command is not enough, since the click.options are parsed (and the prompts appear) before the command is invoked where the logic for printing the deprecated warning lies. Therefore I used VerdiCommandGroup.get_command to print something before. This has the the effect that the deprecation warning is also printed when one requests the help page. One can argue if this is desired behavior.

In principle I thought I could just use click.command's deprecated flag to consume it (set it to false after a print) in VerdiCommandGroup.get_command, but then one gets problems with the validate_consistency test in the verdi-autodocs hook that does not know about this behavior and complains because the help page is not correct (deprecated flag changes also documentation). One could add also this logic to the validate_consistency.py but this adds another point where we need to know about the internal behavior of the cmdline that diverges from click, I was already not a big fan adding one.

@agoscinski agoscinski requested a review from sphuber May 30, 2024 13:08
@agoscinski agoscinski force-pushed the verdi-improve-deprecate-message branch from c29671b to 726a593 Compare May 30, 2024 13:17
@agoscinski
Copy link
Contributor Author

agoscinski commented May 30, 2024

I removed the second solution in the second commit, because I think that makes reviewing more cumbersome. I think one can imagine how it looks like only keeping the deprecate in the help pages

Copy link

codecov bot commented May 30, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 77.87%. Comparing base (ef60b66) to head (048211c).
Report is 112 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6433      +/-   ##
==========================================
+ Coverage   77.51%   77.87%   +0.37%     
==========================================
  Files         560      562       +2     
  Lines       41444    41802     +358     
==========================================
+ Hits        32120    32549     +429     
+ Misses       9324     9253      -71     
Flag Coverage Δ
presto 73.20% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

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

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 @agoscinski . I am not sure I quite understand the use of the VerdiCommand class. Why don't you simply use the deprecated keyword of the click.Command class? It would have made sense if you would have changed the type for verdi_deprecated to str but you declare it as a bool just as the one defined by click.

I like the approach though. And I am pretty sure that with some small changes you can even get the deprecation message to show before the prompts. All you need to do is:

  • get rid of VerdiCommand
  • Just use @click.command('name', deprecated='Use verdi profile setup core.psql_dos instead.') Now, I am passing a streven though click typesdeprecated` as just a bool. But since there are no strict type checks, this works
  • Replace your changes in VerdiCommandGroup.get_command with
    if cmd.deprecated:
        echo_deprecated(cmd.deprecated if isinstance(cmd.deprecated, str) else 'This command is deprecated.')

I think this solution has exactly the desired behavior. It prints the deprecation message before prompts and the commands are automatically prefixed with (Deprecated) in the short help message in the help page.

@sphuber
Copy link
Contributor

sphuber commented Jun 4, 2024

@agoscinski do you have the time to finish this? Happy to take over from here if not

@agoscinski
Copy link
Contributor Author

agoscinski commented Jun 5, 2024

Thanks @agoscinski . I am not sure I quite understand the use of the VerdiCommand class. Why don't you simply use the deprecated keyword of the click.Command class? It would have made sense if you would have changed the type for verdi_deprecated to str but you declare it as a bool just as the one defined by click.

I was using first the deprecated flag of click.command, but my added logic and the logic inside click.Command clashed a bit meaning that we got two deprecated messages (One added by the logic in this PR that comes before the prompts and one after the prompts that comes from the logic in click.Command). In your suggested solution this would also happen as a nonempty string would evaluated to True inside click.Command. I tried to set the flag to false after the first error message was printed, but this causes problems with the help page that is inconsistent between runtime and validation.

Reflecting on all solutions, accepting the fact that there are two deprecation message is worth the simplicity of the implementation (your suggestion) in my opinion.

@agoscinski
Copy link
Contributor Author

Just to give an example how it looks like
image

(here as copyable text)

(aiida-dev) alexgo@fw:~/code/aiida-core(verdi-improve-deprecate-message)$ verdi code setup --label addnew --computer localhost --remote-abs-path /home/alexgo/code/aiida-core/remoteabs -n
Deprecated: This command is deprecated. Please refer to help page for new usage.
DeprecationWarning: The command 'setup' is deprecated.
/home/alexgo/code/aiida-core/src/aiida/orm/utils/builders/code.py:18: AiidaDeprecationWarning: This module is deprecated. To create a new code instance, simply use the constructor. (this will be removed in v3)
  warn_deprecation('This module is deprecated. To create a new code instance, simply use the constructor.', version=3)
Success: Code<22> addnew@localhost created

@agoscinski
Copy link
Contributor Author

With prompts it looks like this
image

@agoscinski agoscinski requested a review from sphuber June 5, 2024 12:13
@sphuber
Copy link
Contributor

sphuber commented Jun 5, 2024

Reflecting on all solutions, accepting the fact that there are two deprecation message is worth the simplicity of the implementation (your suggestion) in my opinion.

Yes, I think having two instead of 1 that comes too late is a much better situation. Another benefit is that the docstrings are automatically updated and we don't risk forgetting it. So please go far the suggested solution

@agoscinski
Copy link
Contributor Author

When implementing this solution I realized that VerdiCommandGroup.get_command is also used when a list of subcommands is retrieved for the help page which then causes the deprecated warning to kick in when verdi code is run for example (because the subcommand setup is retrieved) . This is very confusing.

This is because of this part in the code

 cmd_name, cmd, args = self.resolve_command(ctx, args) 
 assert cmd is not None 
 ctx.invoked_subcommand = cmd_name 
 super().invoke(ctx) 
 sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) 
 with sub_ctx: 
     return _process_result(sub_ctx.command.invoke(sub_ctx)) 

https://github.com/pallets/click/blob/ac56c4551fcc5d4a60a66b252d6666bf58248e03/src/click/core.py#L1678-L1684
Within resolve_command the get_command is used just to check if the command is available. Within make_context the get_command is used to get the subcommands parameters (and if needed activates the prompts with parse_args somewhere down there).

I did not find any attribute that gives me a hint in which context the get_command is invoked, so my only solution was to directly overwrite the parse_args class that invokes these prompts and print the deprecated message there.
https://github.com/pallets/click/blob/ac56c4551fcc5d4a60a66b252d6666bf58248e03/src/click/core.py#L1395
This again requires our own VerdiCommand class.

@sphuber
Copy link
Contributor

sphuber commented Jun 5, 2024

I did not find any attribute that gives me a hint in which context the get_command is invoked, so my only solution was to directly overwrite the parse_args class that invokes these prompts and print the deprecated message there.
https://github.com/pallets/click/blob/ac56c4551fcc5d4a60a66b252d6666bf58248e03/src/click/core.py#L1395
This again requires our own VerdiCommand class.

There might be another solution. The formatting when you call verdi code --help is done in MultiCommand.format_commands. The Group is a subclass of MultiCommand. So we could override the format_commands in our VerdiCommandGroup:

    def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None:
        """Extra format methods for multi methods that adds all the commands
        after the options.
        """
        from gettext import gettext as _

        commands = []
        for subcommand in self.list_commands(ctx):
            cmd = self.get_command(ctx, subcommand, print_deprecation=False)
            # What is this, the tool lied about a command.  Ignore it
            if cmd is None:
                continue
            if cmd.hidden:
                continue

            commands.append((subcommand, cmd))

        # allow for 3 times the default spacing
        if len(commands):
            limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)

            rows = []
            for subcommand, cmd in commands:
                help = cmd.get_short_help_str(limit)
                rows.append((subcommand, help))

            if rows:
                with formatter.section(_("Commands")):
                    formatter.write_dl(rows)

Unfortunately this is quite a bit of code. Only thing I had to change is add the import and then add print_deprecation=False in the self.get_command call.

You then update the signature of that method as:

    def get_command(self, ctx: click.Context, cmd_name: str, print_deprecation: bool = True) -> click.Command | None:

And in the implementation you do:

        if cmd is not None:
            if cmd.deprecated and not ctx.resilient_parsing and print_deprecation:
                from aiida.cmdline.utils.echo import echo_deprecated
                echo_deprecated(cmd.deprecated if isinstance(cmd.deprecated, str) else 'This command is deprecated.')

            return self.add_verbosity_option(cmd)

From some quick testing this seems to work as expected and desired. But I may have missed another edge case. Please give that a try though. Although not ideal that we have to completely copy the format_commands implementation, it might still be preferable than introducing our own command subclass.

@agoscinski
Copy link
Contributor Author

agoscinski commented Jun 5, 2024

I feel like both solutions are equally not great in terms of adding extra logic that requires some deeper understanding how click works. My only argument favoring my suggestion is that we can deprecate commands that use other groups like LazyConfigureGroup and DynamicEntryPointCommandGroup without adding additional code to these groups. Since we might want to add also an option to groups being completely deprecated (for example verdi computer configure), I think it is a better approach to use a custom command class for adding the deprecating logic for individual commands.

@sphuber
Copy link
Contributor

sphuber commented Jun 5, 2024

I think it is a better approach to use a custom command class for adding the deprecating logic for individual commands.

Fair enough. The one downside currently though is that if you are deprecating a command, you have to manually add the cls=VerdiComman and add the import. This can be easily forgotten. But I think there might be a way to change the base command class in the VerdiCommandGroup. If that is possible, then I am fully on board with your solution.

What is the reason that you chose to use the parse_args as the hook for printing the deprecation message? Any particular reason?

@sphuber
Copy link
Contributor

sphuber commented Jun 5, 2024

But I think there might be a way to change the base command class in the VerdiCommandGroup. If that is possible, then I am fully on board with your solution.

@agoscinski I had a look in the source code, and it is possible.
You can set the Group.command_class to the VerdiCommand and then all subcommands should automatically use that class 👍

@agoscinski
Copy link
Contributor Author

What is the reason that you chose to use the parse_args as the hook for printing the deprecation message? Any particular reason?

It seemed to me most understandable point to insert this logic. The functions docstring

        """Given a context and a list of arguments this creates the parser
        and parses the arguments, then modifies the context as necessary.
        This is automatically invoked by :meth:`make_context`.
        """

and the code logic shows this clearly in Command
https://github.com/pallets/click/blob/ac56c4551fcc5d4a60a66b252d6666bf58248e03/src/click/core.py#L1400-L1404 and we want to have the deprecated help before the parsing (the prompt should be created by handle_parse_result). I still must adapt the docstring then I think it becomes quite clear. Something like this

class VerdiCommand(click.Command):
    """Custom Command class to change logic how the deprecation message is handled."""

    def parse_args(self, ctx: click.Context, args: t.List[str]) -> t.List[str]:
        """Prints the deprecation message before the arguments are parsed and any parameters requests are prompted in the parents class implementation
        """
        if self.deprecated:
            # We are abusing click.Command `deprecated` member variable by using a
            # string instead of a bool to also use it as optional deprecated message
            echo_deprecated(
                self.deprecated
                if isinstance(self.deprecated, str)  # type: ignore[redundant-expr]
                else 'This command is deprecated.'
            )

        return super().parse_args(ctx, args)

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 @agoscinski . Think this is a great solution. Just have a suggested change for the docstring. If you are ok with it, we can merge.

src/aiida/cmdline/groups/verdi.py Outdated Show resolved Hide resolved
@sphuber sphuber force-pushed the verdi-improve-deprecate-message branch from 91da784 to e305232 Compare June 6, 2024 09:32
@sphuber sphuber marked this pull request as ready for review June 6, 2024 09:32
@agoscinski
Copy link
Contributor Author

Suggestion for squashed commit message (did not want to squash to make further review easier)

CLI: Enables deprecation warnings to be printed before any prompts (#6433)

`click.Command`s have a deprecated flag that results in a printed deprecation
warning on usage (and adaptions of help page). The `click.option` prompts are
however invoked before that warning is printed. Therefore we customize the
`click.Command` class introducing `aiida.cmdline.groups.VerdiCommand` to move
the printing logic to before the first prompt is shown.

I thought about replacing everywhere usage of decorators.deprecated_command with this solution, but needs some more thoughts and I don't want to block this PR further. The deprecation messages are a bit outdated, and the question is if we update them or remove these commands. I'll open an issue.

@sphuber
Copy link
Contributor

sphuber commented Jun 6, 2024

I thought about replacing everywhere usage of decorators.deprecated_command with this solution, but needs some more thoughts and I don't want to block this PR further. The deprecation messages are a bit outdated, and the question is if we update them or remove these commands. I'll open an issue.

Fully agree, this can and should be done in a separate PR. We can leave the changes to the setup commands as you have already done them. Do you agree with my suggested change to the docstring?

@agoscinski
Copy link
Contributor Author

Yes, wanted to add them, but you have already. Should I rebase?

@sphuber
Copy link
Contributor

sphuber commented Jun 6, 2024

Weird, the warning that fails the doc build is:

/home/docs/checkouts/readthedocs.org/user_builds/aiida-core/envs/6433/lib/python3.11/site-packages/aiida/cmdline/groups/verdi.py:docstring of aiida.cmdline.groups.verdi.VerdiCommand.parse_args:1: WARNING: py:meth reference target not found: make_context

but we don't reference make_context in that docstring 😕

@agoscinski
Copy link
Contributor Author

agoscinski commented Jun 6, 2024

I think because it (the docstring) is inherited. Can I push a fix?

The `click.Command` has a `deprecated` attribute, which is a bool, that
when set to `True` results in a printed deprecation warning on usage,
and adaptions of the help page to indicate the command as deprecated.

The `click.option` prompts are however invoked before that warning is
printed. This can lead to users filling out all prompts before they are
finally greeted with the deprecation message. Therefore we customize
the `click.Command` class introducing `aiida.cmdline.groups.VerdiCommand`
to move the printing logic to before the first prompt is shown.

The `VerdiCommandGroup` sets `command_class` to this new custom class
such that all `verdi` commands automatically use it. To deprecate a
command, the `deprecated` argument now just has to be set to the desired
deprecation message.
@sphuber sphuber force-pushed the verdi-improve-deprecate-message branch from 3e3cf9d to 048211c Compare June 6, 2024 10:18
@sphuber sphuber merged commit deb293d into aiidateam:main Jun 6, 2024
12 checks passed
@sphuber
Copy link
Contributor

sphuber commented Jun 6, 2024

Thanks a lot @agoscinski

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

The `click.Command` has a `deprecated` attribute, which is a bool, that
when set to `True` results in a printed deprecation warning on usage,
and adaptions of the help page to indicate the command as deprecated.

The `click.option` prompts are however invoked before that warning is
printed. This can lead to users filling out all prompts before they are
finally greeted with the deprecation message. Therefore we customize
the `click.Command` class introducing `aiida.cmdline.groups.VerdiCommand`
to move the printing logic to before the first prompt is shown.

The `VerdiCommandGroup` sets `command_class` to this new custom class
such that all `verdi` commands automatically use it. To deprecate a
command, the `deprecated` argument now just has to be set to the desired
deprecation message.
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