-
Notifications
You must be signed in to change notification settings - Fork 328
Declaring new commands #470
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
Conversation
|
I'll look closer to these this weekend, but if your tests aren't running move the |
|
The tests effectively run with the command line provided:
But it doesn't mix well with other tests yet. |
|
Can you try this is how I normally run individual test files. |
|
Either way, I would stick with the convention with what we have (separating the files and putting them in files preceding with |
|
Oh, I can see better now. Is it better now? A current problem is the reuse the function name, how to declare a new command with the same name? The old behavior was to override the previous command in favor of the newer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds support for declaring new PyMOL commands with type annotations through a new new_command decorator. The change replaces the old declare_command function with enhanced functionality for command argument type parsing and documentation extraction.
- Introduces a new
new_commanddecorator with improved type annotation support - Adds comprehensive test coverage for various command parameter types
- Implements automatic documentation parsing from function parameters
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| testing/tests/api/test_commanding.py | New comprehensive test file for command declaration functionality |
| testing/tests/api/commanding.py | Removes old declare_command tests that are replaced by new test file |
| modules/pymol/commanding.py | Implements new new_command decorator with enhanced type parsing and documentation features |
| modules/pymol/cmd.py | Updates import to use new_command instead of declare_command |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
modules/pymol/commanding.py
Outdated
|
|
||
| def _parse_list_str(value): | ||
| return shlex.split(value) | ||
| raise pymol.CmdException("Invalid boolean value: %s" % value) |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message should use f-string formatting for consistency with similar errors elsewhere in the function.
| raise pymol.CmdException("Invalid boolean value: %s" % value) | |
| raise pymol.CmdException(f"Invalid boolean value: {value}") |
modules/pymol/commanding.py
Outdated
| except: | ||
| found = False | ||
| if not found: | ||
| raise pymol.CmdException("Union was not able to cast %s" % value) |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message should use f-string formatting for consistency with similar errors elsewhere in the function.
| raise pymol.CmdException("Union was not able to cast %s" % value) | |
| raise pymol.CmdException(f"Union was not able to cast {value}") |
modules/pymol/commanding.py
Outdated
| elif isinstance(type, str): | ||
| return str(value) | ||
|
|
||
| raise pymol.CmdException(f"Unsupported argument type {type}") |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message is misleading. The variable type here is a type annotation, not a value type. Consider using {type!r} or a more descriptive message like 'Unsupported type annotation {type}'.
| raise pymol.CmdException(f"Unsupported argument type {type}") | |
| raise pymol.CmdException(f"Unsupported type annotation {type!r}") |
modules/pymol/commanding.py
Outdated
| while True: | ||
| i += 1 | ||
| if tokens[i].string == "def": | ||
| while tokens[i].string == "(": |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop will run indefinitely if it encounters a '(' token. It should use an if statement instead of while, or include proper bounds checking to prevent infinite loops.
| while tokens[i].string == "(": | |
| if tokens[i+1].string == "(": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop will run indefinitely if it encounters a '(' token. It should use an if statement instead of while, or include proper bounds checking to prevent infinite loops.
Maybe the "(" happens not immediately after the "def", in fact there is the function name in between, and I always saw them in the same line. Pretty sure that that tokens[i+1] doesn't suffice to skip the full function name. This is a while cursor is not parenthesis increase i+=1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I'm wrong. While loop and if clause are equivalent here same there as after "def" always comes the function name and then "(".
I.e. they are the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be happy if you @JarrettSJohnson could write or ask someone to write some code challenging the current implementation?
modules/pymol/commanding.py
Outdated
| name = tokens[i].string | ||
| name_line = tokens[i].line | ||
| i += 1 | ||
| while not (tokens[i].type == tokenize.NAME and tokens[i+1].string == ":"): |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop can cause an IndexError when accessing tokens[i+1] if i reaches the end of the tokens list. Add bounds checking: i+1 < len(tokens) to the condition.
| while not (tokens[i].type == tokenize.NAME and tokens[i+1].string == ":"): | |
| while i+1 < len(tokens) and not (tokens[i].type == tokenize.NAME and tokens[i+1].string == ":"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a read only function. If it is broken, may be easier to rewrite than to fix.
This loop can cause an IndexError when accessing
tokens[i+1]ifireaches the end of the tokens list. Add bounds checking:i+1 < len(tokens)to the condition.
I don't know, but seems that if you are inside a function declaration, the current token is a name, and your code is valid Python, the next token sometime it will happen to be a ":".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mom always says to me to think before to talk. As you can see, she is correct right now as usual.
| _self.help_sc.append(name) | ||
|
|
||
| inner.__arg_docs = parse_documentation(function) | ||
| _self.keyword[name] = [inner, 0,0,',',parsing.STRICT] |
Copilot
AI
Oct 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spacing is inconsistent with missing space after the first comma. Should be [inner, 0, 0, ',', parsing.STRICT] for better readability.
| _self.keyword[name] = [inner, 0,0,',',parsing.STRICT] | |
| _self.keyword[name] = [inner, 0, 0, ',', parsing.STRICT] |
How often do we see this in the wild, though? Keeping the same behavior sounds fine to me though I could be convinced with alternatives (logging when new_command overrides, etc...) |
|
Of course this isn't a real problem! I is relevant only Is wonderful this Copilot feature, i'm more than amazed. My talent being replaced by a machine, i'm also very worried. Today i'll check into the review. |
|
Okay, Copilot it is like smarter and dumber than anyone. |
|
It can (usually) be wrong about a lot of things. Feel free to just ignore suggestions that you don't agree with, especially if it's easily refutable. |
|
Oh Jarrett, I do my best on PyMOL, i care about it even more than my master thesis which I'm researching. But I'm just a developer finding where I can contribute the most. (I talk lot of bullshit also, and i'm unfortunately a bit weird) I want to say that i like your suggestions and considerations. |
|
Master's thesis is important to focus on!😂 PyMOL will be here for a longer while ;) |
|
Did you saw? I just talked nonsense! My thoughts are weaker in english. I use PyMOL as a tool to study protein-ligand and protein-short peptide systems. Computer predicted hotspots suggests probes where ligands or peptides should overlap in order to enhance biological affinity or binding efficiency to their targets. I use PyMOL at various steps through my XDrugPy plugin. |
|
My account were shadow banned because of the shared link! I'm back now. Do you think I can move to the command help? Maybe just pretty printing the function signature including docstring and showing on the (HTML?) console? Would this introduce a library? Edit: would this require a library? |
|
@JarrettSJohnson what do you think about the current version? Is it ready to merge? |
| elif isinstance(type, str): | ||
| return str(value) | ||
|
|
||
| raise pymol.CmdException(f"Unsupported argument type annotation {type}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what are the consequences of a function without proper type annotations? Do they just not run? And if there's a bug in this implementation, is the command also just expected to not run?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is supposed to be comprehensive and support most cases a typical command line plugin require. The cmd.extend would not be deprecated and may be used as a fallback.
If there is some bug in this implementation, we just add more tests and fix it. Did you saw something bugged on this implementation I'm unaware of?
modules/pymol/commanding.py
Outdated
|
|
||
| raise pymol.CmdException(f"Unsupported argument type annotation {type}") | ||
|
|
||
| def parse_documentation(func): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docstring/annotation. I assume this returns a dict[param_name] = param_comment ?
modules/pymol/commanding.py
Outdated
|
|
||
| raise pymol.CmdException(f"Unsupported argument type annotation {type}") | ||
|
|
||
| def parse_documentation(func): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, given the tests, this only retrieves comments within the def(...) scope? I am not sure if I've ever come across parameter documentation this way--this probably should be noted somewhere.
def func(
# some value
x: int): -> int
return x * xBut
def func(x: int): -> int
:param x: some value
return x * xis what i'm more familiar with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also move the docstring for immediately before the declaration, so parameters came after docstring, if you like...
This is very good in my opinion because would tidify docstring with parameter documentation and typing. But tooling would not get it (e.g. integrated documentation in IDEs).
Python isn't the most syntactically flexible language but I like it plasticity enough...
I'll probably drop the documentation for now. But in my opinion we show use some strong convention, so we can generate rich documentation for new style command line plugins.
modules/pymol/commanding.py
Outdated
| # Inner function that will be callable every time the command is executed | ||
| @wraps(function) | ||
| def inner(*args, **kwargs): | ||
| caller = traceback.extract_stack(limit=2)[0].filename | ||
|
|
||
| frame = traceback.format_stack()[-2] | ||
| caller = frame.split("\"", maxsplit=2)[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having all of this being called every time a command runs makes me a little nervous, especially if these commands are done in a loop. A lot of this info is just redundantly calculated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added an attribute to direct assess the original unwrapped function. It's just cmd.foo.func() instead of cmd.foo(). Both works the same, but the first works faster.
|
I did a toy benchmark and it took 5 seconds of my machine. Pretty fast and enough for me. import traceback
def c():
caller = traceback.extract_stack(limit=2)[0].filename
caller == "asdasodas"
def b():
for i in range(1_000_000):
c()
def a():
b()
if __name__ == "__main__":
a()But also introduced an assessor to the original non-wrapped function so plugin developers can reduce any slowness caused because the |
|
@JarrettSJohnson Can we get this PR in 3.2, please? The previous implementation didn't worked well. Should I recover the "commentstrings"? :-) |
|
Hi, Pedro, I'm getting ready to merge your PRs, for tests that are failing, can you fix those that are fixable in this PR. Ones that depend on fixes from other PRs can just be commented out and marked as |
|
I really don't know if there a useful purpose on If you think it is ok, I may dedicate time to finish Again, any general scheme I didn't thought before can be made by |
|
Hi @JarrettSJohnson @speleo3, I think we can give a go on this |
|
Cannot merge this until your tests pass. Seems like something(s) not compatible with Python 3.10. |
|
This code use the package strenum, which provides an implementation of StrEnum while Python 3.11 doesn't come yet. |
|
Can you provide a workaround for 3.10? Oh, you patched. One sec. |
|
Is it alright? Or better to skip with Currently all tests will run but this one will fail because require Python 3.11 or the package strenum. |
|
Can you mark it the test with python > 3.10? |
|
i'm noob now, |
|
I'm less noob now. But don't know what is the cause of failure just by observing logs. |
Please excuse me if I say that I have mixed feelings about this implementation and don't want to endorse it. I assume it's an improvement over the already existing |
|
It is fully compatible with mypy! It is just another utility for type annotations, which currently is mostly for type enforcing and checks. |
|
I never read too much of mypy, but I like it. The above/inline comment strings of arguments that would be a bit disruptive in terms of standard tools support. I think it is a win, but your opinion may diverge: @cmd.new_command
def my_cmd(
# Docs about argument foo: it ranges a lot
foo: int = 0,
# newer PyMOL-only documentation style about float bar numbers.
bar: float = 0.0 # more docs for bar
):
passI do not think very much of validation and beyond, I think more like a specific case to support modern command line prompts and get correct typing in the developer side instead of parsing everything always. As it is simple MyPy regular code, you may opt to do your own implementation in case this one becomes infeasible. However I see a good coverage of PyMOL-scripts-repo. Just type enforcing and automatic casting (more like parsing). |
|
This commit 3f49c4f is unnecessary. MyPy works just fine. |
|
Does the above address your concerns? @speleo3 |
Why don't we? What benefit does your run-time checking provide that a statically enforced |
|
Type conversion from text based commands. The user writes with a keyboard. |
|
You have it at runtime and statically enforced if you like also. |
|
I can tidy better my code if you want, like beautify the following. elif issubclass(type, Enum):
if value in type:
return type(value)
else:
raise pymol.CmdException(f"Invalid value for enum {type.__name__}: {value}")
elif isinstance(type, str):
return str(value)But the real thing is that the subset of typing annotations supported by my implementation is fully supported statically by mypy. Of course is also supported at runtime doing type conversion and high level parsing. You may diverge on the list or tuple implementation, but I think is fine enough also. Any way, I focused on the function signature like a contract between plugin developers, PyMOL software and command line users. This I propose to be done with mypy based typing annotations. |
|
its just type enforcing and data parsing... if you become fast, we can move to autocompletion, documentation and maybe other features. Speed... ok (a workaround), mypy strictness.... ok, python 3.10... ok. Would be awesome, but I'm reticent you don't like something about my implementation... if you talk we can maybe manage it. |
|
Apologies, I can't reply quickly this week because it's Thanksgiving in the US and I'm traveling a bit. Was mostly waiting for any further objections to the implementation from a previous reply, but haven't heard anything else. We can consider this was experimental for plugins, so I'm not too worried. |
|
Happy traveling. I'll document this experimental feature on the wiki soon. |


This is a continuation of #448.
I test it with the following command line. It doesn't get collected by the default test runner, I guess, because my tests were written in a
pytestparlance.PYTHONPATH=~/Desktop/pymol-open-source/testing/ pytest tests/api/commanding.pyIt is still missing windows support.