Skip to content

Some new_command fixes#485

Open
pslacerda1 wants to merge 11 commits intoschrodinger:masterfrom
pslacerda1:new_command
Open

Some new_command fixes#485
pslacerda1 wants to merge 11 commits intoschrodinger:masterfrom
pslacerda1:new_command

Conversation

@pslacerda1
Copy link
Copy Markdown
Contributor

No description provided.

@pslacerda1
Copy link
Copy Markdown
Contributor Author

I started a wiki page for new_command and think that everything is ok by now.

Can you point me on how to make the community aware so they can also test?

@pslacerda1 pslacerda1 changed the title Some new command fixes Some new_command fixes Nov 29, 2025
@pslacerda1
Copy link
Copy Markdown
Contributor Author

May I add these entries from my .gitignore into yours?

*.egg-info/
*.so
.venv/

@JarrettSJohnson
Copy link
Copy Markdown
Member

Hi Pedro, couple of high-level things first:

  1. For your PRs, can you make a description of a summary/what the PR is trying to accomplish?
  2. Can I ask why you're removing helping.py in tests?
  3. Changes to the .gitignore would be better suited as a separate PR.

@pslacerda1 pslacerda1 force-pushed the new_command branch 2 times, most recently from 3f63232 to a259980 Compare December 5, 2025 23:22
@pslacerda1
Copy link
Copy Markdown
Contributor Author

  1. I added support to the quiet=1 option from PyMOL CLI. Also better error messages explaining the reason of the calling error (in case the user made a calling error). Initially I tried to make multi-line error messages (like Tornado exceptions if I recall correctly), but they were becoming ugly and not user-friendly, so I gave up.
  2. I removed tests/helping.py by mistake!

And I don't know why the tests are failing, in my machine they're ok.

image

@JarrettSJohnson
Copy link
Copy Markdown
Member

Feel free to rebase and we'll look into test failures after.

@pslacerda1
Copy link
Copy Markdown
Contributor Author

I simply rebased and forced with lease. The same inconsistency is happening, no error in my local.

@JarrettSJohnson
Copy link
Copy Markdown
Member

How are you running the tests?

pymol -ckqy testing/testing.py --run all

like in the CI, I can reproduce the issue locally as well with this branch.

@pslacerda1
Copy link
Copy Markdown
Contributor Author

Maybe a context manager could improve these temporary settings (or specifically error handling)...

@JarrettSJohnson
Copy link
Copy Markdown
Member

This seems a little hacky. What is the issue here? Why can't exception be properly caught?

@pslacerda1
Copy link
Copy Markdown
Contributor Author

I think the current behavior is ok because the test must fail early in case of errors so don't abuse servers too much.

However it is failing too early giving me no chance to test exceptions. As the errors occurs inside a cmd.run call, I have no chance to capture the error at the API level. However I'm not really sure about it.

My best opinion is to create a context manager that temporarily set the option than restores:

with cmd.invocation.options(exit_on_error=0):
    cmd.run('fun my_foo=a')
assert 'MyError' in out+err

@pslacerda1
Copy link
Copy Markdown
Contributor Author

But my problem is that the following code don't shows up even if called before exited (as required by the CI)

475                     exc_type, exc_value, tb = colorprinting.print_exc(
476                             [__file__, SCRIPT_TOPLEVEL])

@JarrettSJohnson
Copy link
Copy Markdown
Member

Not sure if I entirely follow.

However it is failing too early giving me no chance to test exceptions

Seems like ArgumentParsingError is being thrown, so why not just catch this in your inner where you're parsing arguments?

@pslacerda1
Copy link
Copy Markdown
Contributor Author

pslacerda1 commented Mar 23, 2026

Because is not an error from inner, it's from the command line user incorrect argument passing. This error is the message is "human-oriented", it's intended to be understandable by users.

However it is failing too early giving me no chance to test exceptions

The sys.exit() call is before the control return to me, because the error happened on the parser layer.

An aspect I don't understand is why print_exc() don't works.

@JarrettSJohnson
Copy link
Copy Markdown
Member

JarrettSJohnson commented Mar 23, 2026

The exception is thrown from the parsing logic in inner. Locally, I can fix this issue by wrapping it in try/catch.

diff --git a/modules/pymol/commanding.py b/modules/pymol/commanding.py
index e783682b9..6cac59aa9 100644
--- a/modules/pymol/commanding.py
+++ b/modules/pymol/commanding.py
@@ -749,18 +749,22 @@ SEE ALSO
                 # special _self argument
                 kwargs.pop("_self", None)
                 new_kwargs = {}
-                for var, param in sign.parameters.items():
-                    if var in kwargs:
-                        value = kwargs[var]
-                        # special 'quiet' argument
-                        if var == 'quiet' and isinstance(value, int):
-                            new_kwargs[var] = bool(value)
+                try:
+                    for var, param in sign.parameters.items():
+                        if var in kwargs:
+                            value = kwargs[var]
+                            # special 'quiet' argument
+                            if var == 'quiet' and isinstance(value, int):
+                                new_kwargs[var] = bool(value)
+                            else:
+                                actual_type = resolved_hints.get(var, param.annotation)
+                                new_kwargs[var] = _into_types(var, actual_type, value)
                         else:
-                            actual_type = resolved_hints.get(var, param.annotation)
-                            new_kwargs[var] = _into_types(var, actual_type, value)
-                    else:
-                        if param.default is sign.empty:
-                            raise RuntimeError(f"Unknow variable '{var}'.")
+                            if param.default is sign.empty:
+                                raise RuntimeError(f"Unknow variable '{var}'.")
+                except ArgumentParsingError as e:
+                    print(e)
+                    return

@pslacerda1
Copy link
Copy Markdown
Contributor Author

Oh, I see the misconception. The error is raised inside new_command (or inner, _into_types), but the origin is actually the user incorrectly passing bad arguments (i.e. arguments that fails at parsing). So it must not be catch by any except clause inside ours code (maybe only if to beautify the error making it better to understand and debug and re-raise).

@pslacerda1
Copy link
Copy Markdown
Contributor Author

Hi @JarrettSJohnson, do you have any other questions?

I'm still unsure on how to promote this feature into the community. However I think that it unlock a lot, like better generated documentation and maybe even better command line features like auto-completion.

@JarrettSJohnson
Copy link
Copy Markdown
Member

I'll take most of the fault here, but I probably should have better anticipated how much complexity would be involved for a project like this. I'm definitely in favor of high-level goals here, but I wonder if we should have spent more time in the beginning with a clearer design doc and possible implementation off-repo (ideally in a branch) that we could iterate through before jumping into an implementation that's merged into master.

One of the things that I wonder is if a lot of this type checking could have been assisted by libraries like argparse or click. These are problems that have been pondered on by experts for a long time, and I worry that there's too much bespoke code here for this to be robust enough for everyone's use-case. Moreover, it's hard for me to review myself, because it feels like I am reviewing "Python typing" code and not "PyMOL" code. A lot of this is out of my wheelhouse and the devs are left to be the maintainers of this code going forward. The advantages of new_command are nice, but the maintenance and complexity is a significant burden (and may even outweigh the benefits) for me especially if it's meant to have the same reach as extend.

IMO, I think at this point, one of the best ways forward is perhaps to mark this as experimental for this upcoming release and reassess afterward--during which we can test it out on some simple plugins. Alternatively we could move new_command to a separate branch and experiment with it there.

As far as advertisement, I think PyMOLWiki is sufficient; though you may need to be your best advocate for something like this and test it on your plugins so we can see observe its strengths.

I'd like to cc the Thomas's @TstewDev and @speleo3 if they have any additional high-level comments or concerns. There's some details in the code that I can point for improvements, but I can follow up on those after once we align on a direction.

@pslacerda1
Copy link
Copy Markdown
Contributor Author

pslacerda1 commented Apr 2, 2026

Be tranquility.

I did a lot of laboratory/testing before myself (e.g. parsing the abstract source tree and PyMOL style docstrings) and opted by this design and like the current overall design even if it's also a bit more complicated then I wanted.

Very much like extend it is designed to be developed for a time span and then be done... and forget. My current plan include innovative documentation declaration on comments and beautiful help visualization for commands that opt in (pretty much only and simple pretty print the formatted function/command signature). Blend source code, function declaration and command options in a single monolithic piece which is the signature is awesome in my opinion.

@pslacerda1
Copy link
Copy Markdown
Contributor Author

Take a look on the wiki: https://pymolwiki.org/index.php/New_Command. My specification is a Runnable.

@pslacerda1
Copy link
Copy Markdown
Contributor Author

Click is nice, they have some nice features like File, Path, IntRange and FloatRange, the remaining types I already got in current implementation; for instance, click.Choice can be an Enum or StrEnum. Anyway, File is just an convenience function over click.Path and click.Path seems somewhat a convenience of pathlib.Path.

@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):
    """Copy contents of INPUT to OUTPUT."""
    while True:
        chunk = input.read(1024)
        if not chunk:
            break
        output.write(chunk)
@click.command()
@click.argument('filename', type=click.Path(exists=True))
def touch(filename):
    """Print FILENAME if the file exists."""
    click.echo(click.format_filename(filename))

@pslacerda1
Copy link
Copy Markdown
Contributor Author

These are components every human-computer interface has to handle case-by-case, while nobody invented something really abstract about this, we'll continuing to re-inventing the interfaces: click, Qt, Jupyter Widgets, Shiny... because of this I think we should focus on PyMOL specific stuff.

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