Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 115 additions & 51 deletions modules/pymol/commanding.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@
from io import FileIO as file

import inspect
import glob
import shlex
import tokenize
import builtins
from io import BytesIO
from enum import Enum
if sys.version_info >= (3, 11):
from enum import StrEnum
from functools import wraps
from pathlib import Path
from textwrap import dedent
from typing import Tuple, Iterable, get_args, Optional, Union, Any, NewType, List, get_origin
from typing import get_args, Union, Any, get_origin
from types import UnionType

import re
import os
Expand Down Expand Up @@ -602,61 +602,121 @@ def get_state_list(states_str):
output = get_state_list(states)
states_list = sorted(set(map(int, output)))
return _cmd.delete_states(_self._COb, name, states_list)


class ArgumentParsingError(ValueError):
"Error on argument parsing."

def _into_types(type, value):
if repr(type) == 'typing.Any':
def __init__(self, arg_name, message):
message = dedent(message).strip()
if arg_name:
s = f"Failed at parsing '{arg_name}'. {message}"
else:
s = message
super().__init__(s)


def _into_types(var, type, value):

# Untyped string
if type == Any:
return value

# Boolean flags
elif type is bool:
if isinstance(value, bool):
return value
if value.lower() in ["yes", "1", "true", "on", "y"]:
trues = ["yes", "1", "true", "on", "y"]
falses = ["no", "0", "false", "off", "n"]
if value.lower() in trues:
return True
elif value.lower() in ["no", "0", "false", "off", "n"]:
elif value.lower() in falses:
return False
else:
raise pymol.CmdException(f"Invalid boolean value: {value}")
raise ArgumentParsingError(
var,
f"Can't parse {value!r} as bool."
f" Supported true values are {', '.join(trues)}."
f" Supported false values are {', '.join(falses)}."
)

elif isinstance(type, builtins.type):
return type(value)

if origin := get_origin(type):
if not repr(origin).startswith('typing.') and issubclass(origin, tuple):
args = get_args(type)
new_values = []
for i, new_value in enumerate(shlex.split(value)):
new_values.append(_into_types(args[i], new_value))
return tuple(new_values)
# Types from typing module
elif origin := get_origin(type):

if origin in {Union, UnionType}:
funcs = get_args(type)
for func in funcs:
try:
return _into_types(None, func, value)
except:
continue
raise ArgumentParsingError(
var,
f"Can't parse {value!r} into {type}."
f" The parser tried each union type and none was suitable."
)

elif origin == Union:
args = get_args(type)
found = False
for i, arg in enumerate(args):
elif issubclass(origin, tuple):
funcs = get_args(type)
if funcs:
values = shlex.split(value)
if len(funcs) > 0 and len(funcs) != len(values):
raise ArgumentParsingError(
var,
f"Can't parse {value!r} into {type}."
f" The number of tuple arguments are incorrect."
)
try:
found = True
return _into_types(arg, value)
return tuple(_into_types(None, f, v) for f, v in zip(funcs, values))
except:
found = False
if not found:
raise pymol.CmdException(f"Union was not able to cast {value}")

elif issubclass(list, origin):
args = get_args(type)
if len(args) > 0:
f = args[0]
raise ArgumentParsingError(
var,
f"Can't parse {value!r} into {type}."
f" One or more tuple values are of incorrect types."
)
else:
f = lambda x: x
return [f(i) for i in shlex.split(value)]
return tuple(shlex.split(value))

elif issubclass(origin, list):
funcs = get_args(type)
if len(funcs) == 1:
func = funcs[0]
return [_into_types(None, func, a) for a in shlex.split(value)]
return shlex.split(value)

elif issubclass(type, Enum):
if value in type:
elif sys.version_info >= (3, 11) and issubclass(type, StrEnum):
try:
return type(value)
else:
raise pymol.CmdException(f"Invalid value for enum {type.__name__}: {value}")
except:
names = [e.value for e in list(type)]
raise ArgumentParsingError(
var,
f"Invalid value for {type.__name__}."
f" Accepted values are {', '.join(names)}."
)

# Specific types must go before other generic types
# isinstance(type, builtins.type) comes after
elif issubclass(type, Enum):
value = type.__members__.get(value)
if value is None:
raise ArgumentParsingError(
var,
f"Invalid value for {type.__name__}."
f" Accepted values are {', '.join(type.__members__)}."
)
return value

elif isinstance(type, str):
return str(value)

raise pymol.CmdException(f"Unsupported argument type annotation {type}")
# Generic types must accept str as single argument to __init__(s)
elif isinstance(type, builtins.type):
try:
return type(value)
except Exception as exc:
raise ArgumentParsingError(
var,
f"Invalid value {value!r} for custom type {type.__name__}."
f" The type must accept str as the solo argument to __init__(s)."
) from exc


def new_command(name, function=None, _self=cmd):
Expand Down Expand Up @@ -685,22 +745,25 @@ def new_command(name, function=None, _self=cmd):
# 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
caller = sys._getframe(1).f_code.co_filename
# It was called from command line or pml script, so parse arguments
if caller == _parser_filename:
kwargs = {**kwargs, **dict(zip(args2_, args))}
# special _self argument
kwargs.pop("_self", None)
new_kwargs = {}
for var, type in funcs.items():
if var in kwargs:
value = kwargs[var]
new_kwargs[var] = _into_types(type, value)
final_kwargs = {}
for k, v in kwargs_.items():
final_kwargs[k] = v
for k, v in new_kwargs.items():
if k not in final_kwargs:
final_kwargs[k] = v
# special 'quiet' argument
if var == 'quiet' and isinstance(value, int):
new_kwargs[var] = bool(value)
else:
new_kwargs[var] = _into_types(var, type, value)
final_kwargs = {
**kwargs_,
**new_kwargs
}
return function(**final_kwargs)

# It was called from Python, so pass the arguments as is
Expand All @@ -714,6 +777,7 @@ def inner(*args, **kwargs):
inner.func = inner.__wrapped__
return inner


def extend(name, function=None, _self=cmd):

'''
Expand Down
50 changes: 0 additions & 50 deletions testing/tests/api/helping.py

This file was deleted.

Loading