Skip to content

Commit 80eb0ce

Browse files
committed
Fix merges.
1 parent 3943835 commit 80eb0ce

File tree

3 files changed

+315
-3
lines changed

3 files changed

+315
-3
lines changed

modules/pymol/cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def as_pathstr(path):
202202

203203
# for extending the language
204204

205-
from .commanding import extend, extendaa, alias
205+
from .commanding import declare_command, extend, extendaa, alias
206206

207207
# for documentation etc
208208

modules/pymol/commanding.py

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,23 @@
1818
if True:
1919
import _thread as thread
2020
import urllib.request as urllib2
21-
from io import FileIO as file
21+
from io import FileIO as file, BytesIO
22+
23+
import inspect
24+
import glob
25+
import shlex
26+
import tokenize
27+
from enum import Enum
28+
from functools import wraps
29+
from pathlib import Path
30+
from textwrap import dedent
31+
from typing import Tuple, Iterable, get_args, Optional, Union, Any, NewType, List, get_origin
32+
2233

2334
import re
2435
import os
2536
import time
37+
import builtins
2638
import threading
2739
import traceback
2840
from . import colorprinting
@@ -529,6 +541,168 @@ def delete(name, *, _self=cmd):
529541
if _self._raising(r,_self): raise pymol.CmdException
530542
return r
531543

544+
# Selection = NewType('Selection', str)
545+
546+
def _into_types(type, value):
547+
if repr(type) == 'typing.Any':
548+
return value
549+
elif type is bool:
550+
if isinstance(value, bool):
551+
return value
552+
if value.lower() in ["yes", "1", "true", "on", "y"]:
553+
return True
554+
elif value.lower() in ["no", "0", "false", "off", "n"]:
555+
return False
556+
else:
557+
raise pymol.CmdException("Invalid boolean value: %s" % value)
558+
559+
elif isinstance(type, builtins.type):
560+
return type(value)
561+
562+
if origin := get_origin(type):
563+
if not repr(origin).startswith('typing.') and issubclass(origin, tuple):
564+
args = get_args(type)
565+
new_values = []
566+
for i, new_value in enumerate(shlex.split(value)):
567+
new_values.append(_into_types(args[i], new_value))
568+
return tuple(new_values)
569+
570+
elif origin == Union:
571+
args = get_args(type)
572+
found = False
573+
for i, arg in enumerate(args):
574+
try:
575+
found = True
576+
return _into_types(arg, value)
577+
except:
578+
found = False
579+
if not found:
580+
raise pymol.CmdException(f"Union was not able to cast %s" % value)
581+
582+
elif issubclass(list, origin):
583+
args = get_args(type)
584+
if len(args) > 0:
585+
f = args[0]
586+
else:
587+
f = lambda x: x
588+
return [f(i) for i in shlex.split(value)]
589+
590+
591+
# elif value is None:
592+
# origin = get_origin(type)
593+
# if origin is None:
594+
# return None
595+
# else:
596+
# return _into_types(origin)
597+
# for arg in get_args(origin):
598+
# return _into_types(get_args(origin), value)
599+
600+
elif isinstance(type, str):
601+
return str(value)
602+
603+
raise pymol.CmdException(f"Unsupported argument type {type}")
604+
605+
def parse_documentation(func):
606+
source = inspect.getsource(func)
607+
tokens = tokenize.tokenize(BytesIO(source.encode('utf-8')).readline)
608+
tokens = list(tokens)
609+
comments = []
610+
params = {}
611+
i = -1
612+
started = False
613+
while True:
614+
i += 1
615+
if tokens[i].string == "def":
616+
while tokens[i].string == "(":
617+
i += 1
618+
started = True
619+
continue
620+
if not started:
621+
continue
622+
if tokens[i].string == "->":
623+
break
624+
if tokens[i].type == tokenize.NEWLINE:
625+
break
626+
if tokens[i].string == ")":
627+
break
628+
if tokens[i].type == tokenize.COMMENT:
629+
comments.append(tokens[i].string)
630+
continue
631+
if tokens[i].type == tokenize.NAME and tokens[i+1].string == ":":
632+
name = tokens[i].string
633+
name_line = tokens[i].line
634+
i += 1
635+
while not (tokens[i].type == tokenize.NAME and tokens[i+1].string == ":"):
636+
if tokens[i].type == tokenize.COMMENT and tokens[i].line == name_line:
637+
comments.append(tokens[i].string)
638+
break
639+
elif tokens[i].type == tokenize.NEWLINE:
640+
break
641+
i += 1
642+
else:
643+
i -= 3
644+
docs = ' '.join(c[1:].strip() for c in comments)
645+
params[name] = docs
646+
comments = []
647+
return params
648+
649+
650+
def declare_command(name, function=None, _self=cmd):
651+
652+
if function is None:
653+
name, function = name.__name__, name
654+
655+
# docstring text, if present, should be dedented
656+
if function.__doc__ is not None:
657+
function.__doc__ = dedent(function.__doc__)
658+
659+
# Analysing arguments
660+
spec = inspect.getfullargspec(function)
661+
kwargs_ = {}
662+
args_ = spec.args[:]
663+
defaults = list(spec.defaults or [])
664+
665+
args2_ = args_[:]
666+
while args_ and defaults:
667+
kwargs_[args_.pop(-1)] = defaults.pop(-1)
668+
669+
funcs = {}
670+
for idx, (var, func) in enumerate(spec.annotations.items()):
671+
funcs[var] = func
672+
673+
# Inner function that will be callable every time the command is executed
674+
@wraps(function)
675+
def inner(*args, **kwargs):
676+
frame = traceback.format_stack()[-2]
677+
caller = frame.split("\"", maxsplit=2)[1]
678+
# It was called from command line or pml script, so parse arguments
679+
if caller.endswith("pymol/parser.py"):
680+
kwargs = {**kwargs, **dict(zip(args2_, args))}
681+
kwargs.pop("_self", None)
682+
new_kwargs = {}
683+
for var, type in funcs.items():
684+
if var in kwargs:
685+
value = kwargs[var]
686+
new_kwargs[var] = _into_types(type, value)
687+
final_kwargs = {}
688+
for k, v in kwargs_.items():
689+
final_kwargs[k] = v
690+
for k, v in new_kwargs.items():
691+
if k not in final_kwargs:
692+
final_kwargs[k] = v
693+
return function(**final_kwargs)
694+
695+
# It was called from Python, so pass the arguments as is
696+
else:
697+
return function(*args, **kwargs)
698+
inner.__arg_docs = parse_documentation(function)
699+
700+
_self.keyword[name] = [inner, 0,0,',',parsing.STRICT]
701+
_self.kwhash.append(name)
702+
_self.help_sc.append(name)
703+
return inner
704+
705+
532706
def extend(name, function=None, _self=cmd):
533707

534708
'''

testing/tests/api/commanding.py

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import print_function
22

33
import sys
4-
import pymol
4+
55
import __main__
66
from pymol import cmd, testing, stored
7+
from typing import List
8+
from typing import Optional, Any, Tuple, Union
9+
from pathlib import Path
10+
711

812
class TestCommanding(testing.PyMOLTestCase):
913

@@ -171,3 +175,137 @@ def testRun(self, namespace, mod, rw):
171175
self.assertTrue(stored.tmp)
172176
if mod:
173177
self.assertEqual(rw, hasattr(sys.modules[mod], varname))
178+
179+
180+
def test_declare_command_casting():
181+
from pathlib import Path
182+
183+
184+
@cmd.declare_command
185+
def func(a: int, b: Path):
186+
assert isinstance(a, int) and a == 1
187+
assert isinstance(b, Path) and "/tmp" == str(b)
188+
cmd.do('func 1, /tmp')
189+
190+
def test_declare_command_optional(capsys):
191+
@cmd.declare_command
192+
def func(a: Optional[int] = None):
193+
assert a is None
194+
cmd.do("func")
195+
out, err = capsys.readouterr()
196+
assert out+err == ''
197+
198+
@cmd.declare_command
199+
def func(a: Optional[int] = None):
200+
assert a is 10
201+
cmd.do("func 10")
202+
out, err = capsys.readouterr()
203+
assert out+err == ''
204+
205+
def test_declare_command_docstring():
206+
@cmd.declare_command
207+
def func():
208+
"""docstring"""
209+
assert func.__doc__ == "docstring"
210+
211+
212+
def test_declare_command_bool(capsys):
213+
@cmd.declare_command
214+
def func(a: bool, b: bool):
215+
assert a
216+
assert not b
217+
218+
cmd.do("func yes, 0")
219+
out, err = capsys.readouterr()
220+
assert out == '' and err == ''
221+
222+
223+
def test_declare_command_generic(capsys):
224+
@cmd.declare_command
225+
def func(
226+
nullable_point: Tuple[float, float, float],
227+
my_var: Union[int, float] = 10,
228+
my_foo: Union[int, float] = 10.0,
229+
extended_calculation: bool = True,
230+
old_style: Any = "Old behavior"
231+
):
232+
assert nullable_point == (1., 2., 3.)
233+
assert extended_calculation
234+
assert isinstance(my_var, int)
235+
assert isinstance(my_foo, float)
236+
assert old_style == "Old behavior"
237+
238+
cmd.do("func nullable_point=1 2 3, my_foo=11.0")
239+
out, err = capsys.readouterr()
240+
assert out + err == ''
241+
242+
def test_declare_command_path(capsys):
243+
@cmd.declare_command
244+
def func(dirname: Path = Path('.')):
245+
assert dirname.exists()
246+
cmd.do('func ..')
247+
cmd.do('func')
248+
out, err = capsys.readouterr()
249+
assert out + err == ''
250+
251+
def test_declare_command_any(capsys):
252+
@cmd.declare_command
253+
def func(old_style: Any):
254+
assert old_style != "RuntimeError"
255+
cmd.do("func RuntimeError")
256+
out, err = capsys.readouterr()
257+
assert 'AssertionError' in out+err
258+
259+
def test_declare_command_list(capsys):
260+
@cmd.declare_command
261+
def func(a: List):
262+
assert a[1] == "2"
263+
cmd.do("func 1 2 3")
264+
out, err = capsys.readouterr()
265+
assert out + err == ''
266+
267+
@cmd.declare_command
268+
def func(a: List[int]):
269+
assert a[1] == 2
270+
cmd.do("func 1 2 3")
271+
out, err = capsys.readouterr()
272+
assert out + err == ''
273+
274+
def test_declare_command_tuple(capsys):
275+
@cmd.declare_command
276+
def func(a: Tuple[str, int]):
277+
assert a == ("fooo", 42)
278+
cmd.do("func fooo 42")
279+
out, err = capsys.readouterr()
280+
assert out + err == ''
281+
282+
def test_declare_command_arg_docs():
283+
@cmd.declare_command
284+
def func(
285+
# multiline
286+
# documentation works
287+
foo: int, # inline
288+
a: str,
289+
# bar are strings
290+
bar: Tuple[str, int], # continued...
291+
b: Any = 10, # The new old age
292+
# aaaa
293+
c: Any = 'a' # b
294+
):
295+
"main description"
296+
pass
297+
298+
assert func.__arg_docs['foo'] == "multiline documentation works inline"
299+
assert func.__arg_docs['a'] == ""
300+
assert func.__arg_docs['bar'] == "bar are strings continued..."
301+
assert func.__arg_docs['b'] == 'The new old age'
302+
assert func.__arg_docs['c'] == 'aaaa b'
303+
assert func.__annotations__['foo'] == int
304+
assert func.__annotations__['bar'] == Tuple[str, int]
305+
306+
def test_declare_command_default():
307+
@cmd.declare_command
308+
def func(a: str="sele"):
309+
assert a == "a"
310+
func("a")
311+
cmd.do('func a')

0 commit comments

Comments
 (0)