New Command: Difference between revisions

From PyMOLWiki
Jump to navigation Jump to search
 
(28 intermediate revisions by the same user not shown)
Line 1: Line 1:
==The problem==
[[new_command]] is a new API-only feature which exposes an user defined Python function as a command to other users.


Current PyMOL approach to new plugin commands is outdated.
== In a brief ==


==The proposal==
<source lang=python>
from pymol import cmd
from pathlib import Path
from pprint import pprint
from typing import Any, Optional
from enum import Enum


Introduce a new system based on modern Python with type checking and implicit type coercion.
class CoolFlag(Enum):
    STRUCTURE = 1
    SEQUENCE = 2


==What works right now?==
## Supported only on Python 3.11+
#from enum import StrEnum
#class Method(StrEnum):
#    SINGLE = "single"
#    COMPLETE = "complete"
#    WARD = "ward"


On PyMOL open-source, but not on Incentive, there's some support. However it isn't working for all possible cases.
Point = tuple[float, float, float]


@cmd.new_command
def nice_tool(
    # Regular types are supported
    title: str,
    my_var: int | float,
    extended_calculation: bool,
   
    # Simple composite types
    a_point: Point,
    other_point: tuple[int, int, int] = (0, 0, 0),
    this_list: Optional[list[bool]] = None,


  <source lang="python">
    # Support for Enum types
@cmd.declare_command
    flag: CoolFlag = CoolFlag.STRUCTURE,
def new_command(
  #  method: Method = Method.WARD,
     dirname: Path = '.',
 
     #nullable_point: Optional[Tuple[int, int, int]] = None,
    # Arbitrary types that allows initialization with str values
     my_var: int | float
     dirname: Path = Path('~'),
    extended_calculation: bool = True,
 
     old_style: Any = "Support anything currently not supported",
     # Special arguments
    old_style: Any = "anything as string", # raw unparsed str
     quiet: bool = True, # automatic 'quiet=1' on command-line
     _self=cmd  # used in multi-threaded applications
   
):
):
     """
     pprint(locals())
    A cool docstring.
</source>
    """
 
    pass
These code blocks ahead are sample usage of the above function.
 
<source>
PyMOL> nice_tool Have a nice tool, 10, False, 0.1 2.3 4.5, this_list=true 0 yes 0, flag=SEQUENCE
</source>
 
<source lang=python>
{'_self': <module 'pymol.cmd' from '/home/peu/Desktop/pymol-open-source/modules/pymol/cmd.py'>,
'a_point': (0.1, 2.3, 4.5),
'dirname': PosixPath('~'),
'extended_calculation': False,
'flag': <CoolFlag.SEQUENCE: 2>,
'method': <Method.WARD: 'ward'>,
'my_var': 10,
'old_style': 'anything as string',
'other_point': (0, 0, 0),
'quiet': False,
'this_list': [True, False, True, False],
'title': 'Have a nice tool'}
</source>
 
If you need more examples, here a non exhaustive list of examples [https://github.com/schrodinger/pymol-open-source/blob/9c923999732644743565deb99efcc642ecd15442/testing/tests/api/test_commanding.py]. Look for and inspect <code>cmd.do()</code> calls and because they contain code exactly as they would be written into command line.
 
== Advantages ==
 
It improves on [[extend]], the standard command definition mechanism. It works by parsing the arguments given at command-line by users, enforcing correct types at runtime, and ensuring typing strictness. It is also advantageous for developers consuming the function/command directly by the API because types can also be enforced statically by MyPy.
 
== Zero-overhead with direct access ==
 
Before parsing arguments, this feature introspects quickly if it was called by the PyMOL parser and so can further benefit from parsing refinement, or if it was called by another function and this feature is not necessary. The introspection mechanism is a simple observation on the caller stack frame filename, which can be a slowness factor. Because of this, we also provide direct access with zero overhead for developers by the .func attribute.
 
<source lang=python>
# works the same for developers
>>> nice_tool('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)
>>> nice_tool.func('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)
</source>
 
Here a quick benchmark (remove the print statement before trying it).
 
<source lang=python>
# 2.5x improvement for 1000000 calls
>>> from timeit import timeit
>>> timeit("nice_tool('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)", globals=locals())
0.374392487006844
>>> timeit("nice_tool.func('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)", globals=locals())
0.12843744499696186
</source>
</source>

Latest revision as of 01:20, 30 November 2025

new_command is a new API-only feature which exposes an user defined Python function as a command to other users.

In a brief

from pymol import cmd
from pathlib import Path
from pprint import pprint
from typing import Any, Optional
from enum import Enum

class CoolFlag(Enum):
    STRUCTURE = 1
    SEQUENCE = 2

## Supported only on Python 3.11+
#from enum import StrEnum
#class Method(StrEnum):
#    SINGLE = "single"
#    COMPLETE = "complete"
#    WARD = "ward"

Point = tuple[float, float, float]

@cmd.new_command
def nice_tool(
    # Regular types are supported
    title: str,
    my_var: int | float,
    extended_calculation: bool,
    
    # Simple composite types
    a_point: Point,
    other_point: tuple[int, int, int] = (0, 0, 0),
    this_list: Optional[list[bool]] = None,

    # Support for Enum types
    flag: CoolFlag = CoolFlag.STRUCTURE,
 #   method: Method = Method.WARD,

    # Arbitrary types that allows initialization with str values
    dirname: Path = Path('~'),

    # Special arguments
    old_style: Any = "anything as string", # raw unparsed str
    quiet: bool = True,  # automatic 'quiet=1' on command-line
    _self=cmd  # used in multi-threaded applications
    
):
    pprint(locals())

These code blocks ahead are sample usage of the above function.

PyMOL> nice_tool Have a nice tool, 10, False, 0.1 2.3 4.5, this_list=true 0 yes 0, flag=SEQUENCE
{'_self': <module 'pymol.cmd' from '/home/peu/Desktop/pymol-open-source/modules/pymol/cmd.py'>,
 'a_point': (0.1, 2.3, 4.5),
 'dirname': PosixPath('~'),
 'extended_calculation': False,
 'flag': <CoolFlag.SEQUENCE: 2>,
 'method': <Method.WARD: 'ward'>,
 'my_var': 10,
 'old_style': 'anything as string',
 'other_point': (0, 0, 0),
 'quiet': False,
 'this_list': [True, False, True, False],
 'title': 'Have a nice tool'}

If you need more examples, here a non exhaustive list of examples [1]. Look for and inspect cmd.do() calls and because they contain code exactly as they would be written into command line.

Advantages

It improves on extend, the standard command definition mechanism. It works by parsing the arguments given at command-line by users, enforcing correct types at runtime, and ensuring typing strictness. It is also advantageous for developers consuming the function/command directly by the API because types can also be enforced statically by MyPy.

Zero-overhead with direct access

Before parsing arguments, this feature introspects quickly if it was called by the PyMOL parser and so can further benefit from parsing refinement, or if it was called by another function and this feature is not necessary. The introspection mechanism is a simple observation on the caller stack frame filename, which can be a slowness factor. Because of this, we also provide direct access with zero overhead for developers by the .func attribute.

# works the same for developers
>>> nice_tool('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)
>>> nice_tool.func('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)

Here a quick benchmark (remove the print statement before trying it).

# 2.5x improvement for 1000000 calls
>>> from timeit import timeit
>>> timeit("nice_tool('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)", globals=locals())
0.374392487006844
>>> timeit("nice_tool.func('Have a nice tool', 10, False, [0.1, 2.3, 4.5], this_list=[True, False, True, False], flag=CoolFlag.SEQUENCE)", globals=locals())
0.12843744499696186