From f1494f19595191e4968235858f57a3f44767db46 Mon Sep 17 00:00:00 2001 From: Ye11owSub <exactlythatguy@gmail.com> Date: Sat, 17 Aug 2024 11:02:45 +0100 Subject: [PATCH 1/3] refactoring shortcut.py --- .github/workflows/build.yml | 15 +- modules/pymol/cmd.py | 4 +- modules/pymol/shortcut.py | 351 ++++++++++++++++------------------ pyproject.toml | 3 +- testing/tests/api/shortcut.py | 6 +- tests/pymol/test_shortcut.py | 123 ++++++++++++ 6 files changed, 307 insertions(+), 195 deletions(-) create mode 100644 tests/pymol/test_shortcut.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b5462115b..8c4f43e0c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,8 +28,6 @@ jobs: python3-dev python3-setuptools python3-numpy - python3-pil - python3-pytest python3-pip - name: Install collada2gltf @@ -45,13 +43,14 @@ jobs: - name: Build run: | pip install --upgrade pip - pip install -v --config-settings testing=True . + pip install -v --config-settings testing=True '.[dev]' env: DEBUG: 1 - name: Test run: | pymol -ckqy testing/testing.py --run all + python -m pytest tests -vv build-Windows: @@ -74,7 +73,7 @@ jobs: shell: cmd run: |- CALL %CONDA_ROOT%\\Scripts\\activate.bat - conda install -y -c conda-forge -c schrodinger python cmake libpng freetype pyside6 glew libxml2 numpy=1.26.4 catch2=2.13.3 glm libnetcdf collada2gltf biopython pillow msgpack-python pytest pip python-build + conda install -y -c conda-forge -c schrodinger python cmake libpng freetype pyside6 glew libxml2 numpy=1.26.4 catch2=2.13.3 glm libnetcdf collada2gltf biopython msgpack-python pip python-build - name: Conda info shell: cmd @@ -94,13 +93,14 @@ jobs: shell: cmd run: | CALL %CONDA_ROOT%\\Scripts\\activate.bat - pip install -v --config-settings testing=True . + pip install -v --config-settings testing=True .[dev] - name: Test shell: cmd run: | CALL %CONDA_ROOT%\\Scripts\\activate.bat pymol -ckqy testing\\testing.py --run all + python -m pytest tests -vv build-MacOS: @@ -117,7 +117,7 @@ jobs: bash $CONDA_ROOT.sh -b -p $CONDA_ROOT export PATH="$CONDA_ROOT/bin:$PATH" conda config --set quiet yes - conda install -y -c conda-forge -c schrodinger python cmake libpng freetype pyside6 glew libxml2 numpy=1.26.4 catch2=2.13.3 glm libnetcdf collada2gltf biopython pillow msgpack-python pytest pip python-build + conda install -y -c conda-forge -c schrodinger python cmake libpng freetype pyside6 glew libxml2 numpy=1.26.4 catch2=2.13.3 glm libnetcdf collada2gltf biopython msgpack-python pip python-build conda info - name: Get additional sources @@ -131,9 +131,10 @@ jobs: run: |- export MACOSX_DEPLOYMENT_TARGET=12.0 export PATH="$CONDA_ROOT/bin:$PATH" - pip install -v --config-settings testing=True . + pip install -v --config-settings testing=True '.[dev]' - name: Test run: |- export PATH="$CONDA_ROOT/bin:$PATH" pymol -ckqy testing/testing.py --run all + python -m pytest tests -vv diff --git a/modules/pymol/cmd.py b/modules/pymol/cmd.py index 09ae92f9e..c2dcb6da8 100644 --- a/modules/pymol/cmd.py +++ b/modules/pymol/cmd.py @@ -33,6 +33,7 @@ # # In rare cases, certain nonserious error or warning output should # also be suppressed. Set "quiet" to 2 for this behavior. +from pymol.shortcut import Shortcut def _deferred_init_pymol_internals(_pymol): # set up some global session tasks @@ -54,7 +55,7 @@ def _deferred_init_pymol_internals(_pymol): # take care of some deferred initialization - _pymol._view_dict_sc = Shortcut({}) + _pymol._view_dict_sc = Shortcut() # if True: @@ -78,7 +79,6 @@ def _deferred_init_pymol_internals(_pymol): _pymol = pymol - from .shortcut import Shortcut from chempy import io diff --git a/modules/pymol/shortcut.py b/modules/pymol/shortcut.py index ed654b79b..1f172a85d 100644 --- a/modules/pymol/shortcut.py +++ b/modules/pymol/shortcut.py @@ -1,184 +1,171 @@ -#A* ------------------------------------------------------------------- -#B* This file contains source code for the PyMOL computer program -#C* Copyright (c) Schrodinger, LLC. -#D* ------------------------------------------------------------------- -#E* It is unlawful to modify or remove this copyright notice. -#F* ------------------------------------------------------------------- -#G* Please see the accompanying LICENSE file for further information. -#H* ------------------------------------------------------------------- -#I* Additional authors of this source file include: -#-* -#-* -#-* -#Z* ------------------------------------------------------------------- - -if __name__=='pymol.shortcut': - from . import parsing - from .checking import is_string, is_list - -if True: - def mkabbr(a, m=1): - b = a.split('_') - b[:-1] = [c[0:m] for c in b[:-1]] - return '_'.join(b) - - class Shortcut: - - def __call__(self): - return self - - def __init__(self, keywords=(), filter_leading_underscore=1): - self.filter_leading_underscore = filter_leading_underscore - if filter_leading_underscore: - self.keywords = [x for x in keywords if x[:1]!='_'] - else: - self.keywords = list(keywords) - self.shortcut = {} - self.abbr_dict = {} - self.rebuild() - - def add_one(self,a): - # optimize symbols - hash = self.shortcut - abbr_dict = self.abbr_dict - for b in range(1,len(a)): - sub = a[0:b] - hash[sub] = 0 if sub in hash else a - if '_' in a: - for n in (1, 2): - abbr = mkabbr(a, n) - if a!=abbr: - if abbr in abbr_dict: - if a not in abbr_dict[abbr]: - abbr_dict[abbr].append(a) - else: - abbr_dict[abbr]=[a] - for b in range(abbr.find('_')+1,len(abbr)): - sub = abbr[0:b] - hash[sub] = 0 if sub in hash else a - - def rebuild(self, keywords=None): - if keywords is not None: - if self.filter_leading_underscore: - self.keywords = [x for x in keywords if x[:1]!='_'] - else: - self.keywords = list(keywords) - # optimize symbols - self.shortcut = {} - hash = self.shortcut - self.abbr_dict = {} - abbr_dict = self.abbr_dict - # - for a in self.keywords: - for b in range(1,len(a)): - sub = a[0:b] - hash[sub] = 0 if sub in hash else a - if '_' in a: - for n in (1, 2): - abbr = mkabbr(a, n) - if a!=abbr: - if abbr in abbr_dict: - abbr_dict[abbr].append(a) - else: - abbr_dict[abbr]=[a] - for b in range(abbr.find('_')+1,len(abbr)): - sub = abbr[0:b] - hash[sub] = 0 if sub in hash else a - - self._rebuild_finalize() - - def _rebuild_finalize(self): - hash = self.shortcut - for a, adk in self.abbr_dict.items(): - if len(adk)==1: - hash[a]=adk[0] - for a in self.keywords: - hash[a]=a - - def interpret(self,kee, mode=0): - ''' - Returns None (no hit), str (one hit) or list (multiple hits) - - kee = str: query string, setting prefix or shortcut - mode = 0/1: if mode=1, do prefix search even if kee has exact match - ''' - if not len(kee): # empty string matches everything - return list(self.keywords) - - try: - r = self.shortcut[kee] - except KeyError: - return None - if r and not mode: - return r - - # prefix search - lst_set = set(a for a in self.keywords if a.startswith(kee)) - for abbr, a_list in self.abbr_dict.items(): - if abbr.startswith(kee): - lst_set.update(a_list) - - # no match - if not lst_set: - return None - - # single match: str - lst = list(lst_set) - if len(lst) == 1: - return lst[0] - - # multiple matches: list - return lst - - def has_key(self,kee): - return kee in self.shortcut - - __contains__ = has_key - - def __getitem__(self,kee): - return self.shortcut.get(kee, None) - - def __delitem__(self,kee): - self.keywords.remove(kee) - self.rebuild() - - def append(self,kee): - self.keywords.append(kee) - self.add_one(kee) - self._rebuild_finalize() - - def auto_err(self,kee,descrip=None): - result = None - if kee not in self.shortcut: - if descrip is not None: - msg = "Error: unknown %s: '%s'." % (descrip, kee) - lst = self.interpret('') - if is_list(lst): - if len(lst)<100: - lst.sort() - lst = parsing.list_to_str_list(lst) - msg += " Choices:\n" - msg += "\n".join(lst) - raise parsing.QuietException(msg) - - else: - result = self.interpret(kee) - if not is_string(result): - if descrip is not None: - lst = parsing.list_to_str_list(result) - msg = "Error: ambiguous %s:\n%s" % (descrip, '\n'.join(lst)) - raise parsing.QuietException(msg) +# A* ------------------------------------------------------------------- +# B* This file contains source code for the PyMOL computer program +# C* Copyright (c) Schrodinger, LLC. +# D* ------------------------------------------------------------------- +# E* It is unlawful to modify or remove this copyright notice. +# F* ------------------------------------------------------------------- +# G* Please see the accompanying LICENSE file for further information. +# H* ------------------------------------------------------------------- +# I* Additional authors of this source file include: +# -* +# -* +# -* +# Z* ------------------------------------------------------------------- + +from typing import Iterable, Optional +from collections import defaultdict +from pymol import parsing + + +class Shortcut: + def __init__( + self, + keywords: Optional[Iterable] = None, + filter_leading_underscore: bool = True, + ): + keywords = list(keywords) if keywords is not None else [] + self.filter_leading_underscore = filter_leading_underscore + self.keywords = ( + [keyword for keyword in keywords if keyword[:1] != "_"] + if filter_leading_underscore + else keywords + ) + self.shortcut: dict[str, str | int] = {} + self.abbreviation_dict = defaultdict(list) + + for keyword in self.keywords: + self.optimize_symbols(keyword) + + self._rebuild_finalize() + + def __contains__(self, keyword: str) -> bool: + return keyword in self.shortcut + + def __getitem__(self, keyword: str) -> Optional[int | str]: + return self.shortcut.get(keyword) + + def __delitem__(self, keyword: str) -> None: + self.keywords.remove(keyword) + self.rebuild() + + def make_abbreviation(self, s: str, groups_length: int) -> str: + """ + Example 1: + Input: s:'abc_def_ghig', groups_length: 1 + Output: 'a_d_ghig' + Example 2: + Input: s:'abc_def', groups_length: 2 + Output: 'a_def' + """ + groups = s.split("_") + groups[:-1] = [c[0:groups_length] for c in groups[:-1]] + return "_".join(groups) + + def optimize_symbols(self, keyword: str) -> None: + for i in range(1, len(keyword)): + substr = keyword[0:i] + self.shortcut[substr] = 0 if substr in self.shortcut else keyword + + if "_" not in keyword: + return + + for n in (1, 2): + abbreviation = self.make_abbreviation(keyword, n) + + if keyword == abbreviation: + continue + + self.abbreviation_dict[abbreviation].append(keyword) + + for i in range(abbreviation.find("_") + 1, len(abbreviation)): + sub = abbreviation[0:i] + self.shortcut[sub] = 0 if sub in self.shortcut else keyword + + def rebuild(self, keywords: Optional[Iterable] = None) -> None: + keywords = list(keywords) if keywords is not None else [] + self.keywords = ( + [keyword for keyword in keywords if keyword[:1] != "_"] + if self.filter_leading_underscore + else keywords + ) + # optimize symbols + self.shortcut = {} + self.abbreviation_dict = defaultdict(list) + for keyword in self.keywords: + self.optimize_symbols(keyword) + + self._rebuild_finalize() + + def _rebuild_finalize(self) -> None: + for abbreviation, keywords in self.abbreviation_dict.items(): + if len(keywords) == 1: + self.shortcut[abbreviation] = keywords[0] + for keyword in self.keywords: + self.shortcut[keyword] = keyword + + def interpret( + self, keyword: str, mode: bool = False + ) -> Optional[int | str | list[str]]: + """ + Returns None (no hit), str (one hit) or list (multiple hits) + + keyword = str: query string, setting prefix or shortcut + mode = True/False: if mode=True, do prefix search even if kee has exact match + """ + if keyword == "": + return self.keywords + + result = self.shortcut.get(keyword) + if result is None: + return + if result and not mode: return result -if __name__=='__main__': - sc = Shortcut(['warren','wasteland','electric','well']) - tv = sc.has_key('a') - print(tv==0,tv) - tv = sc.has_key('w') - print(tv==1,tv) - tv = sc.has_key('war') - print(tv==1,tv) - tv = sc.interpret('w') - print(sorted(tv)==['warren', 'wasteland', 'well'],tv) - tv = sc.interpret('e') - print(isinstance(tv, str), tv) + # prefix search + unique_keywords = set( + word for word in self.keywords if word.startswith(keyword) + ) + for abbreviation, keywords in self.abbreviation_dict.items(): + if abbreviation.startswith(keyword): + unique_keywords.update(keywords) + # no match + if not unique_keywords: + return + + # single match: str + # multiple matches: list + return ( + unique_keywords.pop() + if len(unique_keywords) == 1 + else list(unique_keywords) + ) + + def append(self, keyword) -> None: + self.keywords.append(keyword) + self.optimize_symbols(keyword) + self._rebuild_finalize() + + def auto_err( + self, keyword: str, descrip: Optional[str] = None + ) -> Optional[int | str | list[str]]: + if keyword == "": + return + + result = self.interpret(keyword) + + if result is None and descrip is not None: + msg = f"Error: unknown {descrip}: '{keyword}'." + lst = self.interpret("") + if isinstance(lst, list) and len(lst) < 100: + lst.sort() + lst = parsing.list_to_str_list(lst) + msg += " Choices:\n" + "\n".join(lst) + raise parsing.QuietException(msg) + + if isinstance(result, list) and descrip is not None: + lst = parsing.list_to_str_list(result) + options = "\n".join(lst) + msg = f"Error: ambiguous {descrip}\\n {options}" + raise parsing.QuietException(msg) + + return result diff --git a/pyproject.toml b/pyproject.toml index d8629e40a..f1f2dc117 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,8 @@ requires = [ ] [project.optional-dependencies] -test = [ +dev = [ + "numpy>=1.26.4,<2", "pillow==10.3.0", "pytest==8.2.2", ] diff --git a/testing/tests/api/shortcut.py b/testing/tests/api/shortcut.py index c8d4103d2..a1830b501 100644 --- a/testing/tests/api/shortcut.py +++ b/testing/tests/api/shortcut.py @@ -11,7 +11,7 @@ class TestShortcut(testing.PyMOLTestCase): def testShortcut(self): # build shortcut sc = cmd.Shortcut(words) - + # get all keywords self.assertItemsEqual(words, sc.interpret('')) @@ -33,8 +33,8 @@ def testShortcut(self): self.assertItemsEqual(['foo', 'foo_new'], sc.interpret('f')) self.assertEqual('foo', sc.interpret('foo')) self.assertEqual('foo_new', sc.interpret('foo_')) - - self.assertEqual(False, sc.has_key('')) + + self.assertEqual(False, '' in sc) # abbreviations diff --git a/tests/pymol/test_shortcut.py b/tests/pymol/test_shortcut.py new file mode 100644 index 000000000..d7e86aa51 --- /dev/null +++ b/tests/pymol/test_shortcut.py @@ -0,0 +1,123 @@ +import pytest + +from pymol.shortcut import Shortcut + + +@pytest.fixture +def sc() -> Shortcut: + return Shortcut(["foo", "bar", "baz", "com", "com_bla", "com_xxx"]) + + +@pytest.mark.parametrize( + "keyword, expected_result", + [ + ("a", False), + ("w", True), + ("war", True), + ], +) +def test_contains(keyword: str, expected_result: bool): + shortcut = Shortcut(["warren", "wasteland", "electric", "well"]) + assert (keyword in shortcut) is expected_result + + +def test_interpret(): + shortcut = Shortcut(["warren", "wasteland", "electric", "well"]) + list_result = shortcut.interpret("w") + assert list_result is not None + assert not isinstance(list_result, int) + assert sorted(list_result) == ["warren", "wasteland", "well"] + + string_result = shortcut.interpret("e") + assert list_result is not None + assert string_result == "electric" + + +def test_all_keywords(sc: Shortcut): + assert ["foo", "bar", "baz", "com", "com_bla", "com_xxx"] == sc.interpret("") + + +@pytest.mark.parametrize( + "prefixs, expected_result", + [ + (["f", "fo", "foo"], "foo"), + (["b", "ba"], ["bar", "baz"]), + (["bar"], "bar"), + (["c", "co"], ["com", "com_bla", "com_xxx"]), + (["com"], "com"), + ], +) +def test_full_prefix_hits( + sc: Shortcut, prefixs: list[str], expected_result: str | list[str] +): + for prefix in prefixs: + result = sc.interpret(prefix) + result = sorted(result) if isinstance(result, list) else result + assert expected_result == result + + +def test_append(sc: Shortcut): + sc.append("foo_new") + + assert ["foo", "foo_new"], sc.interpret("f") + assert "foo", sc.interpret("foo") + assert "foo_new", sc.interpret("foo_") + + assert "" not in sc + + +def test_abbreviations(sc: Shortcut): + sc.append("foo_new") + + assert "foo_new" == sc.interpret("f_") + assert "foo_new" == sc.interpret("f_new") + assert "foo_new" == sc.interpret("fo_") + assert "com_xxx" == sc.interpret("c_x") + assert "com_xxx" == sc.interpret("c_xxx") + assert "com_xxx" == sc.interpret("co_x") + + +def test_missing_key(sc: Shortcut): + assert None is sc.interpret("missing_key") + + +def test_auto_error(sc: Shortcut): + assert None is sc.auto_err("") + assert None is sc.auto_err("missing_key") + + result = sc.auto_err("co") + assert isinstance(result, list) + assert ["com", "com_bla", "com_xxx"] == sorted(result) + assert "com", sc.auto_err("com") + + +def test_interpret_mode_true(sc: Shortcut): + assert "foo" == sc.interpret("f", True) + + result = sc.interpret("com", True) + assert isinstance(result, list) + assert ["com", "com_bla", "com_xxx"] == sorted(result) + + sc.append("foo_new") + result = sc.interpret("foo", True) + assert isinstance(result, list) + assert ["foo", "foo_new"] == sorted(result) + + +def test_rebuild(sc: Shortcut): + coms = ["com", "com_bla", "com_xxx"] + sc.rebuild(coms) + + assert None is sc.interpret("f") + assert None is sc.interpret("foo") + + result = sc.interpret("c") + assert isinstance(result, list) + assert coms == sorted(result) + + result = sc.interpret("com", True) + assert isinstance(result, list) + assert coms == sorted(result) + + assert "com" == sc.interpret("com") + assert "com_xxx" == sc.interpret("c_x") From cf243e2ca32b7fba719a2a35141c65fee35691b9 Mon Sep 17 00:00:00 2001 From: ye11owSub <exactlythatguy@gmail.com> Date: Sat, 17 Aug 2024 16:04:56 +0100 Subject: [PATCH 2/3] fixing imports for class Shortcut --- modules/pymol/commanding.py | 4 +++- modules/pymol/completing.py | 30 ++++++++++++++++-------------- modules/pymol/constants.py | 2 +- modules/pymol/controlling.py | 4 ++-- modules/pymol/creating.py | 3 ++- modules/pymol/editing.py | 3 ++- modules/pymol/experimenting.py | 2 +- modules/pymol/exporting.py | 3 ++- modules/pymol/externing.py | 2 +- modules/pymol/feedingback.py | 3 ++- modules/pymol/fitting.py | 2 +- modules/pymol/internal.py | 3 ++- modules/pymol/movie.py | 3 ++- modules/pymol/moving.py | 3 ++- modules/pymol/plugins/__init__.py | 3 ++- modules/pymol/querying.py | 2 +- modules/pymol/selecting.py | 3 ++- modules/pymol/setting.py | 4 +++- modules/pymol/viewing.py | 3 ++- modules/pymol/wizarding.py | 2 +- 20 files changed, 50 insertions(+), 34 deletions(-) diff --git a/modules/pymol/commanding.py b/modules/pymol/commanding.py index 470994f33..27d724e05 100644 --- a/modules/pymol/commanding.py +++ b/modules/pymol/commanding.py @@ -12,6 +12,8 @@ #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut + if True: import sys @@ -39,7 +41,7 @@ cmd = sys.modules["pymol.cmd"] import pymol - from .cmd import _cmd, Shortcut, QuietException, \ + from .cmd import _cmd, QuietException, \ fb_module, fb_mask, is_list, \ DEFAULT_ERROR, DEFAULT_SUCCESS, is_ok, is_error, is_string diff --git a/modules/pymol/completing.py b/modules/pymol/completing.py index e9280582c..a9d525220 100644 --- a/modules/pymol/completing.py +++ b/modules/pymol/completing.py @@ -1,14 +1,16 @@ +from pymol.shortcut import Shortcut + cmd = __import__("sys").modules["pymol.cmd"] -class ExprShortcut(cmd.Shortcut): +class ExprShortcut(Shortcut): ''' Expression shortcut for iterate/alter/label with "s." prefix setting autocompletion. ''' - def interpret(self, kee, mode=0): - if not kee.startswith('s.'): - return cmd.Shortcut.interpret(self, kee, mode) - v = cmd.setting.setting_sc.interpret(kee[2:]) + def interpret(self, keyword, mode=False): + if not keyword.startswith('s.'): + return super().interpret(keyword, mode) + v = cmd.setting.setting_sc.interpret(keyword[2:]) if isinstance(v, str): return 's.' + v if isinstance(v, list): @@ -32,7 +34,7 @@ def interpret(self, kee, mode=0): def fragments_sc(): import os import chempy - return cmd.Shortcut([ + return Shortcut([ f[:-4] for f in os.listdir(chempy.path + 'fragments') if f.endswith('.pkl') ]) @@ -40,9 +42,9 @@ def fragments_sc(): def vol_ramp_sc(): from . import colorramping - return cmd.Shortcut(colorramping.namedramps) + return Shortcut(colorramping.namedramps) -names_sc = lambda: cmd.Shortcut(cmd.get_names('public')) +names_sc = lambda: Shortcut(cmd.get_names('public')) aa_nam_e = [ names_sc , 'name' , '' ] aa_nam_s = [ names_sc , 'name' , ' ' ] @@ -58,24 +60,24 @@ def vol_ramp_sc(): aa_rep_c = [ cmd.repres_sc , 'representation' , ', ' ] aa_rem_c = [ cmd.repmasks_sc , 'representation' , ', ' ] aa_v_r_c = [ vol_ramp_sc , 'volume ramp' , ', ' ] -aa_ali_e = [ cmd.Shortcut(['align', 'super', 'cealign']), 'alignment method', ''] +aa_ali_e = [ Shortcut(['align', 'super', 'cealign']), 'alignment method', ''] def wizard_sc(): import os, pymol.wizard names_glob = [name[:-3] for p in pymol.wizard.__path__ for name in os.listdir(p) if name.endswith('.py')] - return cmd.Shortcut(names_glob) + return Shortcut(names_glob) def get_auto_arg_list(self_cmd=cmd): self_cmd = self_cmd._weakrefproxy aa_vol_c = [ lambda: - cmd.Shortcut(self_cmd.get_names_of_type('object:volume')), + Shortcut(self_cmd.get_names_of_type('object:volume')), 'volume', '' ] aa_ramp_c = [ lambda: - cmd.Shortcut(self_cmd.get_names_of_type('object:ramp')), + Shortcut(self_cmd.get_names_of_type('object:ramp')), 'ramp', '' ] - aa_scene_e = [lambda: cmd.Shortcut(cmd.get_scene_list()), 'scene', ''] + aa_scene_e = [lambda: Shortcut(cmd.get_scene_list()), 'scene', ''] return [ # 1st @@ -168,7 +170,7 @@ def get_auto_arg_list(self_cmd=cmd): 'sculpt_iterate' : aa_obj_c, 'set' : aa_set_c, 'set_bond' : aa_set_c, - 'set_key' : [ lambda: cmd.Shortcut(cmd.key_mappings), 'key' , ', ' ], + 'set_key' : [ lambda: Shortcut(cmd.key_mappings), 'key' , ', ' ], 'set_name' : aa_nam_c, 'set_title' : aa_obj_c, 'show' : aa_rem_c, diff --git a/modules/pymol/constants.py b/modules/pymol/constants.py index dc19cdebc..caa70aab4 100644 --- a/modules/pymol/constants.py +++ b/modules/pymol/constants.py @@ -1,8 +1,8 @@ # constant objects +from pymol.shortcut import Shortcut from .parsing import QuietException -from .shortcut import Shortcut from .constants_palette import palette_dict import re diff --git a/modules/pymol/controlling.py b/modules/pymol/controlling.py index 74929e149..9de1de7ca 100644 --- a/modules/pymol/controlling.py +++ b/modules/pymol/controlling.py @@ -12,10 +12,11 @@ #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut + if True: try: from . import selector, internal - from .shortcut import Shortcut cmd = __import__("sys").modules["pymol.cmd"] from .cmd import _cmd, QuietException, is_string, \ boolean_dict, boolean_sc, \ @@ -23,7 +24,6 @@ location_code, location_sc import pymol except: - from shortcut import Shortcut cmd = None diff --git a/modules/pymol/creating.py b/modules/pymol/creating.py index ac9c7fbc9..6ea15cc45 100644 --- a/modules/pymol/creating.py +++ b/modules/pymol/creating.py @@ -12,6 +12,7 @@ #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut from .constants import CURRENT_STATE, ALL_STATES if True: @@ -23,7 +24,7 @@ import re import gzip import os - from .cmd import _cmd, Shortcut, is_list, is_string, \ + from .cmd import _cmd, is_list, is_string, \ safe_list_eval, safe_alpha_list_eval, \ DEFAULT_ERROR, DEFAULT_SUCCESS, is_ok, is_error, \ is_tuple diff --git a/modules/pymol/editing.py b/modules/pymol/editing.py index 4f288b237..420073fa6 100644 --- a/modules/pymol/editing.py +++ b/modules/pymol/editing.py @@ -13,6 +13,7 @@ #Z* ------------------------------------------------------------------- import pymol +from pymol.shortcut import Shortcut from .constants import CURRENT_STATE, ALL_STATES @@ -67,7 +68,7 @@ def _iterate_prepare_args(expression, space, _self): import math from . import selector cmd = __import__("sys").modules["pymol.cmd"] - from .cmd import _cmd,lock,unlock,Shortcut,is_string, \ + from .cmd import _cmd,lock,unlock,is_string, \ boolean_sc,boolean_dict,safe_list_eval, is_sequence, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error from chempy import cpv diff --git a/modules/pymol/experimenting.py b/modules/pymol/experimenting.py index 5dda05c17..e91c1abed 100644 --- a/modules/pymol/experimenting.py +++ b/modules/pymol/experimenting.py @@ -15,7 +15,7 @@ if True: from . import selector - from .cmd import _cmd,lock,unlock,Shortcut,QuietException, \ + from .cmd import _cmd,lock,unlock,QuietException, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error cmd = __import__("sys").modules["pymol.cmd"] import threading diff --git a/modules/pymol/exporting.py b/modules/pymol/exporting.py index 132a8407a..e6cd9f18c 100644 --- a/modules/pymol/exporting.py +++ b/modules/pymol/exporting.py @@ -12,6 +12,7 @@ #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut from . import colorprinting if True: @@ -24,7 +25,7 @@ import pymol cmd = sys.modules["pymol.cmd"] - from .cmd import _cmd,Shortcut,QuietException + from .cmd import _cmd,QuietException from chempy import io from chempy.pkl import cPickle from .cmd import _feedback,fb_module,fb_mask, \ diff --git a/modules/pymol/externing.py b/modules/pymol/externing.py index 7f43cf792..20a6169bf 100644 --- a/modules/pymol/externing.py +++ b/modules/pymol/externing.py @@ -23,7 +23,7 @@ import traceback from glob import glob - from .cmd import _cmd,lock,unlock,Shortcut,QuietException, \ + from .cmd import _cmd,lock,unlock,QuietException, \ _feedback,fb_module,fb_mask, exp_path, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error diff --git a/modules/pymol/feedingback.py b/modules/pymol/feedingback.py index 3e4d0bcb8..56e6b8266 100644 --- a/modules/pymol/feedingback.py +++ b/modules/pymol/feedingback.py @@ -1,6 +1,7 @@ import sys +from pymol.shortcut import Shortcut cmd = __import__("sys").modules["pymol.cmd"] -from .cmd import Shortcut, is_string, QuietException +from .cmd import is_string, QuietException from .cmd import fb_module, fb_mask, fb_action,_raising import copy diff --git a/modules/pymol/fitting.py b/modules/pymol/fitting.py index 7097b2080..213b15d42 100644 --- a/modules/pymol/fitting.py +++ b/modules/pymol/fitting.py @@ -20,7 +20,7 @@ import os import pymol - from .cmd import _cmd,lock,unlock,Shortcut, \ + from .cmd import _cmd,lock,unlock, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error diff --git a/modules/pymol/internal.py b/modules/pymol/internal.py index 087ed03fa..0e5c5fb79 100644 --- a/modules/pymol/internal.py +++ b/modules/pymol/internal.py @@ -1,6 +1,7 @@ import os import sys cmd = sys.modules["pymol.cmd"] +from pymol.shortcut import Shortcut from pymol import _cmd import threading import traceback @@ -14,7 +15,7 @@ import chempy.io -from .cmd import DEFAULT_ERROR, DEFAULT_SUCCESS, loadable, _load2str, Shortcut, \ +from .cmd import DEFAULT_ERROR, DEFAULT_SUCCESS, loadable, _load2str, \ is_string, is_ok # cache management: diff --git a/modules/pymol/movie.py b/modules/pymol/movie.py index 1c9bdf43b..3bde747fd 100644 --- a/modules/pymol/movie.py +++ b/modules/pymol/movie.py @@ -21,6 +21,7 @@ import threading import time from . import colorprinting +from pymol.shortcut import Shortcut def get_movie_fps(_self): r = _self.get_setting_float('movie_fps') @@ -817,7 +818,7 @@ def _encode(filename,first,last,preserve, 'ray' : 2, } -produce_mode_sc = cmd.Shortcut(produce_mode_dict.keys()) +produce_mode_sc = Shortcut(produce_mode_dict.keys()) def find_exe(exe): diff --git a/modules/pymol/moving.py b/modules/pymol/moving.py index 8c489c9e3..67c03eec2 100644 --- a/modules/pymol/moving.py +++ b/modules/pymol/moving.py @@ -11,6 +11,7 @@ #-* #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut if True: @@ -21,7 +22,7 @@ import pymol import re cmd = sys.modules["pymol.cmd"] - from .cmd import _cmd,Shortcut, \ + from .cmd import _cmd, \ toggle_dict,toggle_sc, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error diff --git a/modules/pymol/plugins/__init__.py b/modules/pymol/plugins/__init__.py index 91772d764..23c236431 100644 --- a/modules/pymol/plugins/__init__.py +++ b/modules/pymol/plugins/__init__.py @@ -11,6 +11,7 @@ import pymol from pymol import cmd from pymol import colorprinting +from pymol.shortcut import Shortcut from .legacysupport import * # variables @@ -434,6 +435,6 @@ def initialize(pmgapp=-1): cmd.extend('plugin_pref_save', pref_save) # autocompletion -cmd.auto_arg[0]['plugin_load'] = [ lambda: cmd.Shortcut(plugins), 'plugin', '' ] +cmd.auto_arg[0]['plugin_load'] = [ lambda: Shortcut(plugins), 'plugin', '' ] # vi:expandtab:smarttab:sw=4 diff --git a/modules/pymol/querying.py b/modules/pymol/querying.py index 6750eaaf0..bf8b3eae2 100644 --- a/modules/pymol/querying.py +++ b/modules/pymol/querying.py @@ -20,7 +20,7 @@ from . import selector import pymol cmd = __import__("sys").modules["pymol.cmd"] - from .cmd import _cmd,lock,unlock,Shortcut, \ + from .cmd import _cmd,lock,unlock, \ _feedback,fb_module,fb_mask,is_list, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error diff --git a/modules/pymol/selecting.py b/modules/pymol/selecting.py index 3be7b5f93..c8552aa69 100644 --- a/modules/pymol/selecting.py +++ b/modules/pymol/selecting.py @@ -11,6 +11,7 @@ #-* #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut if True: @@ -18,7 +19,7 @@ cmd = __import__("sys").modules["pymol.cmd"] - from .cmd import _cmd,Shortcut, \ + from .cmd import _cmd, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error import pymol diff --git a/modules/pymol/setting.py b/modules/pymol/setting.py index 21ebba646..f6c36cc61 100644 --- a/modules/pymol/setting.py +++ b/modules/pymol/setting.py @@ -13,6 +13,9 @@ #Z* ------------------------------------------------------------------- # must match layer1/Setting.h +from pymol.shortcut import Shortcut + + cSetting_tuple = -1 cSetting_blank = 0 cSetting_boolean = 1 @@ -26,7 +29,6 @@ import traceback from . import selector - from .shortcut import Shortcut cmd = __import__("sys").modules["pymol.cmd"] from .cmd import _cmd,lock,lock_attempt,unlock,QuietException, \ is_string, \ diff --git a/modules/pymol/viewing.py b/modules/pymol/viewing.py index a65970949..ba2f7dbc2 100644 --- a/modules/pymol/viewing.py +++ b/modules/pymol/viewing.py @@ -12,6 +12,7 @@ #-* #Z* ------------------------------------------------------------------- +from pymol.shortcut import Shortcut from . import colorprinting if True: @@ -25,7 +26,7 @@ import re cmd = sys.modules["pymol.cmd"] - from .cmd import _cmd, Shortcut, \ + from .cmd import _cmd, \ _feedback,fb_module,fb_mask, \ repres,repres_sc, is_string, is_list, \ repmasks,repmasks_sc, \ diff --git a/modules/pymol/wizarding.py b/modules/pymol/wizarding.py index bf707d48c..48512575e 100644 --- a/modules/pymol/wizarding.py +++ b/modules/pymol/wizarding.py @@ -17,7 +17,7 @@ import pymol import sys cmd = __import__("sys").modules["pymol.cmd"] - from .cmd import _cmd,lock,unlock,Shortcut,QuietException,_raising, \ + from .cmd import _cmd,lock,unlock,QuietException,_raising, \ _feedback,fb_module,fb_mask, \ DEFAULT_ERROR, DEFAULT_SUCCESS, _raising, is_ok, is_error From 5c63bea80319aedb145edfb8235cc18ea7165c64 Mon Sep 17 00:00:00 2001 From: ye11owSub <exactlythatguy@gmail.com> Date: Sun, 5 Jan 2025 18:02:46 +0000 Subject: [PATCH 3/3] fixes after review --- .github/workflows/build.yml | 3 - modules/pymol/completing.py | 13 +-- modules/pymol/shortcut.py | 50 +++++++++--- pyproject.toml | 1 - testing/tests/api/shortcut.py | 80 ------------------- .../tests/api}/test_shortcut.py | 0 6 files changed, 49 insertions(+), 98 deletions(-) delete mode 100644 testing/tests/api/shortcut.py rename {tests/pymol => testing/tests/api}/test_shortcut.py (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c4f43e0c..2abbf922b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,6 @@ jobs: - name: Test run: | pymol -ckqy testing/testing.py --run all - python -m pytest tests -vv build-Windows: @@ -100,7 +99,6 @@ jobs: run: | CALL %CONDA_ROOT%\\Scripts\\activate.bat pymol -ckqy testing\\testing.py --run all - python -m pytest tests -vv build-MacOS: @@ -137,4 +135,3 @@ jobs: run: |- export PATH="$CONDA_ROOT/bin:$PATH" pymol -ckqy testing/testing.py --run all - python -m pytest tests -vv diff --git a/modules/pymol/completing.py b/modules/pymol/completing.py index a9d525220..169006032 100644 --- a/modules/pymol/completing.py +++ b/modules/pymol/completing.py @@ -1,3 +1,5 @@ +from typing import Optional + from pymol.shortcut import Shortcut cmd = __import__("sys").modules["pymol.cmd"] @@ -7,14 +9,15 @@ class ExprShortcut(Shortcut): Expression shortcut for iterate/alter/label with "s." prefix setting autocompletion. ''' - def interpret(self, keyword, mode=False): + def interpret(self, keyword: str, mode: bool = False): if not keyword.startswith('s.'): return super().interpret(keyword, mode) - v = cmd.setting.setting_sc.interpret(keyword[2:]) - if isinstance(v, str): - return 's.' + v + v: Optional[int | str | list[str]] = cmd.setting.setting_sc.interpret(keyword[2:]) + + if isinstance(v, str) or isinstance(v, int): + return f"s.{v}" if isinstance(v, list): - return ['s.' + v for v in v] + return [f"s.{v}" for v in v] return None expr_sc = ExprShortcut([ diff --git a/modules/pymol/shortcut.py b/modules/pymol/shortcut.py index 1f172a85d..f97e3272f 100644 --- a/modules/pymol/shortcut.py +++ b/modules/pymol/shortcut.py @@ -34,7 +34,7 @@ def __init__( self.abbreviation_dict = defaultdict(list) for keyword in self.keywords: - self.optimize_symbols(keyword) + self._optimize_symbols(keyword) self._rebuild_finalize() @@ -48,20 +48,30 @@ def __delitem__(self, keyword: str) -> None: self.keywords.remove(keyword) self.rebuild() - def make_abbreviation(self, s: str, groups_length: int) -> str: + def _make_abbreviation(self, s: str, groups_length: int) -> str: """ + Creates an abbreviation for a string by shortening its components. + The abbreviation takes the first `groups_length` + characters of each part before the last component. + Example 1: Input: s:'abc_def_ghig', groups_length: 1 Output: 'a_d_ghig' + Example 2: Input: s:'abc_def', groups_length: 2 - Output: 'a_def' + Output: 'ab_def' """ groups = s.split("_") groups[:-1] = [c[0:groups_length] for c in groups[:-1]] return "_".join(groups) - def optimize_symbols(self, keyword: str) -> None: + def _optimize_symbols(self, keyword: str) -> None: + """ + Optimizes the given keyword by adding abbreviations and shortening + components. This method also builds a shortcut dictionary + for the keyword and its abbreviated forms. + """ for i in range(1, len(keyword)): substr = keyword[0:i] self.shortcut[substr] = 0 if substr in self.shortcut else keyword @@ -70,7 +80,7 @@ def optimize_symbols(self, keyword: str) -> None: return for n in (1, 2): - abbreviation = self.make_abbreviation(keyword, n) + abbreviation = self._make_abbreviation(keyword, n) if keyword == abbreviation: continue @@ -81,7 +91,13 @@ def optimize_symbols(self, keyword: str) -> None: sub = abbreviation[0:i] self.shortcut[sub] = 0 if sub in self.shortcut else keyword - def rebuild(self, keywords: Optional[Iterable] = None) -> None: + def rebuild(self, keywords: Optional[Iterable[str]] = None) -> None: + """ + Rebuilds the shortcuts and abbreviation dictionaries + based on the provided list of keywords. + This method clears the existing shortcuts and optimizes symbols + for the new list of keywords. + """ keywords = list(keywords) if keywords is not None else [] self.keywords = ( [keyword for keyword in keywords if keyword[:1] != "_"] @@ -92,11 +108,18 @@ def rebuild(self, keywords: Optional[Iterable] = None) -> None: self.shortcut = {} self.abbreviation_dict = defaultdict(list) for keyword in self.keywords: - self.optimize_symbols(keyword) + self._optimize_symbols(keyword) self._rebuild_finalize() def _rebuild_finalize(self) -> None: + """ + Finalizes the rebuild process + by setting shortcuts for abbreviations and keywords. + + This method ensures that each abbreviation points to a single keyword and that + each keyword has a shortcut. + """ for abbreviation, keywords in self.abbreviation_dict.items(): if len(keywords) == 1: self.shortcut[abbreviation] = keywords[0] @@ -140,14 +163,23 @@ def interpret( else list(unique_keywords) ) - def append(self, keyword) -> None: + def append(self, keyword: str) -> None: + """Adds a new keyword to the list and rebuilds the shortcuts.""" + self.keywords.append(keyword) - self.optimize_symbols(keyword) + self._optimize_symbols(keyword) self._rebuild_finalize() def auto_err( self, keyword: str, descrip: Optional[str] = None ) -> Optional[int | str | list[str]]: + """ + Automatically raises an error if a keyword is unknown or ambiguous. + + This method checks if a keyword is valid, and if not, + raises a descriptive error with suggestions for possible matches. + """ + if keyword == "": return diff --git a/pyproject.toml b/pyproject.toml index f1f2dc117..fc0cfab43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ requires = [ [project.optional-dependencies] dev = [ - "numpy>=1.26.4,<2", "pillow==10.3.0", "pytest==8.2.2", ] diff --git a/testing/tests/api/shortcut.py b/testing/tests/api/shortcut.py deleted file mode 100644 index a1830b501..000000000 --- a/testing/tests/api/shortcut.py +++ /dev/null @@ -1,80 +0,0 @@ -import pymol -from pymol import cmd, testing, stored - -foos = ['foo'] -ba_s = ['bar', 'baz'] -coms = ['com', 'com_bla', 'com_xxx'] -words = foos + ba_s + coms - -class TestShortcut(testing.PyMOLTestCase): - - def testShortcut(self): - # build shortcut - sc = cmd.Shortcut(words) - - # get all keywords - self.assertItemsEqual(words, sc.interpret('')) - - # full/prefix hits - self.assertEqual('foo', sc.interpret('f')) - self.assertEqual('foo', sc.interpret('fo')) - self.assertEqual('foo', sc.interpret('foo')) - - self.assertItemsEqual(ba_s, sc.interpret('b')) - self.assertItemsEqual(ba_s, sc.interpret('ba')) - self.assertEqual('bar', sc.interpret('bar')) - - self.assertItemsEqual(coms, sc.interpret('c')) - self.assertItemsEqual(coms, sc.interpret('co')) - self.assertEqual('com', sc.interpret('com')) - - # add one - sc.append('foo_new') - self.assertItemsEqual(['foo', 'foo_new'], sc.interpret('f')) - self.assertEqual('foo', sc.interpret('foo')) - self.assertEqual('foo_new', sc.interpret('foo_')) - - self.assertEqual(False, '' in sc) - - - # abbreviations - self.assertEqual('foo_new', sc.interpret('f_')) - self.assertEqual('foo_new', sc.interpret('f_new')) - self.assertEqual('foo_new', sc.interpret('fo_')) - self.assertEqual('com_xxx', sc.interpret('c_x')) - self.assertEqual('com_xxx', sc.interpret('c_xxx')) - self.assertEqual('com_xxx', sc.interpret('co_x')) - - # missing key - self.assertEqual(None, sc.interpret('missing_key')) - - # auto error - self.assertEqual(None, sc.auto_err('')) - self.assertEqual(None, sc.auto_err('missing_key')) - self.assertItemsEqual(coms, sc.auto_err('co')) - self.assertEqual('com', sc.auto_err('com')) - - def testShortcutMode1(self): - # build shortcut - sc = cmd.Shortcut(words) - - # full/prefix hits - self.assertEqual('foo', sc.interpret('f', 1)) - self.assertItemsEqual(coms, sc.interpret('com', 1)) - - # add one - sc.append('foo_new') - self.assertItemsEqual(['foo', 'foo_new'], sc.interpret('foo', 1)) - - def testShortcutRebuild(self): - sc = cmd.Shortcut(words) - sc.rebuild(coms) - - self.assertEqual(None, sc.interpret('f')) - self.assertEqual(None, sc.interpret('foo')) - - self.assertItemsEqual(coms, sc.interpret('c')) - self.assertItemsEqual(coms, sc.interpret('com', 1)) - self.assertEqual('com', sc.interpret('com')) - self.assertEqual('com_xxx', sc.interpret('c_x')) - diff --git a/tests/pymol/test_shortcut.py b/testing/tests/api/test_shortcut.py similarity index 100% rename from tests/pymol/test_shortcut.py rename to testing/tests/api/test_shortcut.py