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