[Idle-dev] KeyConfig, KeyBinding and other related issues.

Saimadhav Heblikar saimadhavheblikar at gmail.com
Wed Jun 18 18:23:53 CEST 2014


In this version, I have attempted an API like solution. It does not
raise any error.(except for couple of asserts for REALLY bad input,
and that too only in internal functions)

All mistakes are caught(i.e. it does not fail on the first
"mistake".). I have documented every where, so should be easy to read.
Each aspect is a method. So is easy to unittest and has been
unittested.

It can detect invalid begin character, invalid end character, repeated
keys, invalid keys, invalid ordering and with reason, where not
directly obvious.

-- 
Regards
Saimadhav Heblikar
-------------- next part --------------
diff -r 4cf322536328 Lib/idlelib/idle_test/test_keybinding.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/idlelib/idle_test/test_keybinding.py	Wed Jun 18 21:49:40 2014 +0530
@@ -0,0 +1,254 @@
+"""Unittest for idlelib.keybindingDialog.py"""
+import unittest
+import string
+from unittest.mock import Mock
+
+modifierKeys = ['Control', 'Alt', 'Shift', 'Meta']
+typeKeys = ['Key']
+functionKeys = ('F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9',
+                'F10', 'F11', 'F12')
+alphanumKeys = tuple(string.ascii_lowercase + string.digits)
+whitespaceKeys = ('Tab', 'space', 'Return')
+editKeys = ('BackSpace', 'Delete', 'Insert')
+moveKeys = ('Home', 'End', 'Prior', 'Next', 'Left',
+            'Right', 'Up', 'Down')
+otherKeys = ('Escape', 'bracketright', 'bracketleft', 'KP_Enter')
+detailKeys = alphanumKeys + functionKeys + editKeys + moveKeys + \
+    whitespaceKeys + otherKeys
+
+MODIFERS = 1
+TYPEKEY = 2
+DETAIL = 3
+
+
+class Keyseq:
+
+    def __init__(self, keyseq):
+        """
+        keyseq - A tkinter keysequence starting that has to be validated
+        process_keyseq - bool
+        """
+        self.keyseq = keyseq
+        self.result = self.process_keyseq()
+
+    def process_keyseq(self):
+        """Return result dict described below:
+        The dict is as below
+        valid_beginning   - bool
+        has_invalid_keys  - bool
+        invalid_keys      - List of invalid keys, [] if there are none
+        has_repeated_keys - bool, whether keyseq contains repeated keys
+        repeated_keys     - List of repeated keys, [] if there are none
+        valid_ending      - bool
+        is_valid          - bool, whether keyseq is semantically valid/not valid       
+
+        """
+        result = {}
+        result['valid_beginning'] = self.is_valid_beginning()
+        result['invalid_keys'] = self.tokenize()['invalid_keys']
+        result['has_invalid_keys'] = result['invalid_keys'] != []
+        result['has_repeated_keys'] = self.has_repeated_keys()
+        result['repeated_keys'] = self.repeated_keys()
+        result['valid_ending'] = self.is_valid_ending()
+        result['is_valid'] = result['valid_beginning'] \
+            and result['valid_ending']  \
+            and not result['has_invalid_keys'] \
+            and not result['has_repeated_keys']
+
+        return result
+
+    def _get_type(self, key):
+        """Given a key, return its type.
+        Invalid key raises ValueError"""
+        if key in modifierKeys:
+            return MODIFERS
+        if key in typeKeys:
+            return TYPEKEY
+        if key in detailKeys:
+            return DETAIL
+        raise ValueError('invalid key beginning key')
+
+    def tokenize(self):
+        """Tokenize a keysequence.
+        Return a dict containing keys:
+        valid_keys - list of tuple of form (key, type)
+        invalid_keys - list of tuple of form (invalid_keys, reason)
+        """
+        keyseq = self.keyseq
+        if self.is_valid_beginning():
+            keyseq = keyseq[1:]
+        if self.is_valid_ending():
+            keyseq = keyseq[:-1]
+        self.keyseq_split = keyseq_split = keyseq.split('-')
+        d = {'valid_keys': [],
+             'invalid_keys': []
+             }
+        prev_type = current_type = -1
+        for i in range(len(keyseq_split)):
+            key = keyseq_split[i]
+            try:
+                current_type = self._get_type(key)
+
+            except ValueError:
+                d['invalid_keys'].append(key)
+                continue
+
+            if prev_type == MODIFERS:
+                if current_type != MODIFERS and current_type != TYPEKEY and current_type != DETAIL:
+                    d['invalid_keys'].append((key, ('{} key is invalid key for this position. Was expecting'
+                                                    ' a modifier key, a type key or a detail key.'.format(key))))
+                    continue
+            if prev_type == TYPEKEY:
+                if current_type == MODIFERS:
+                    d['invalid_keys'].append((key, (
+                        '{} key is invalid to be used after Typekey'.format(key))))
+                    continue
+            if prev_type == DETAIL:
+                d['invalid_keys'].append((key, (
+                    '{} key is invalid to be used after Detail key.'.format(key))))
+                continue
+
+            d['valid_keys'].append((key, current_type))
+            prev_type = current_type
+
+        return d
+
+    def is_valid_beginning(self):
+        return self.keyseq[0] is '<'
+
+    def is_valid_ending(self):
+        return self.keyseq[-1] is '>'
+
+    def has_repeated_keys(self):
+        return len(set(self.keyseq_split)) < len(self.keyseq_split)
+
+    def repeated_keys(self):
+        """Return list of repated keys or [] if there are none"""
+        return [key for key in set(self.keyseq_split) if self.keyseq_split.count(key) > 1]
+
+    def has_invalid_keys(self):
+        return self.tokenize()['invalid_keys'] != []
+
+    def invalid_keys(self):
+        """Return list of invalid keys or [] if there are none"""
+        return self.tokenize()['invalid_keys']
+
+    def is_valid(self):
+        """Return whether keyseq is logically valid.
+        Fail early if keyseq has invalid/repeated keys"""
+        assert self.has_repeated_keys() == False
+        assert self.tokenize()['invalid_keys'] == []
+        keyseq_split = self.tokenize()['valid_keys']
+        prev_type = current_type = -1
+
+
+class TestIsValidKeyseq(unittest.TestCase):
+
+    def test_get_type(self):
+        s = Mock()
+        get = Keyseq._get_type
+
+        self.assertEqual(get(s, 'Control'), MODIFERS)
+        self.assertEqual(get(s, 'Key'), TYPEKEY)
+        self.assertEqual(get(s, 'x'), DETAIL)
+        self.assertEqual(get(s, 'space'), DETAIL)
+        with self.assertRaises(ValueError) as ve:
+            get(s, 'Space')
+        self.assertIn('invalid key', str(ve.exception))
+        with self.assertRaises(ValueError) as ve:
+            get(s, 'Contro')
+        self.assertIn('invalid key', str(ve.exception))
+
+    def test_tokenize(self):
+        k = Keyseq('<Control-Alt-Key-x>')
+        self.assertTupleEqual(k.tokenize()['valid_keys'][1], ('Alt', MODIFERS))
+        self.assertTupleEqual(k.tokenize()['valid_keys'][2], ('Key', TYPEKEY))
+        self.assertTupleEqual(k.tokenize()['valid_keys'][3], ('x', DETAIL))
+
+        k = Keyseq('<Contro-x>')
+        self.assertListEqual(k.tokenize()['invalid_keys'], ['Contro'])
+
+        k = Keyseq('<Key-Control-x>')
+        invalid_keys = k.tokenize()['invalid_keys']
+        self.assertEqual(invalid_keys[0][0], 'Control')
+        self.assertIn('after Typekey', invalid_keys[0][1])
+
+        k = Keyseq('<Control-Key-x-Alt>')
+        invalid_keys = k.tokenize()['invalid_keys']
+        self.assertEqual(invalid_keys[0][0], 'Alt')
+        self.assertIn('after Detail key', invalid_keys[0][1])
+
+    def test_valid_beginning(self):
+        valid = Keyseq.is_valid_beginning
+        k = Mock()
+        k.keyseq = '<Control-x>'
+        self.assertTrue(valid(k))
+        k.keyseq = 'Control-x'
+        self.assertFalse(valid(k))
+
+    def test_valid_ending(self):
+        valid = Keyseq.is_valid_ending
+        k = Mock()
+        k.keyseq = '<Control-x>'
+        self.assertTrue(valid(k))
+        k.keyseq = '<Control-x'
+        self.assertFalse(valid(k))
+
+    def test_has_repeated_keys(self):
+        k = Keyseq('<Control-Alt-Key-x>')
+        self.assertFalse(k.has_repeated_keys())
+
+        k = Keyseq('<Control-Alt-Control-x>')
+        self.assertTrue(k.has_repeated_keys())
+
+    def test_repeated_keys(self):
+        k = Keyseq('<Control-Alt-Key-x>')
+        self.assertEqual(k.repeated_keys(), [])
+
+        k = Keyseq('<Control-Alt-Control-Key-x>')
+        self.assertEqual(k.repeated_keys(), ['Control'])
+
+        k = Keyseq('<Control-Alt-Control-Alt-x>')
+        self.assertListEqual(sorted(k.repeated_keys()), ['Alt', 'Control'])
+
+    def test_has_invalid_keys(self):
+        k = Keyseq('<Control-Key-x>')
+        self.assertFalse(k.has_invalid_keys())
+
+        k = Keyseq('<Contro-Key-x>')
+        self.assertTrue(k.has_invalid_keys())
+
+    def test_invalid_keys(self):
+        k = Keyseq('<Control-Key-x>')
+        self.assertListEqual(k.invalid_keys(), [])
+
+        k = Keyseq('<Contro-Key-x>')
+        self.assertListEqual(k.invalid_keys(), ['Contro'])
+
+    def test_is_valid(self):
+        k = Keyseq('<Contro-Key-x>')
+        with self.assertRaises(AssertionError):
+            k.is_valid()
+        k = Keyseq('<Control-Alt-Control-x>')
+        with self.assertRaises(AssertionError):
+            k.is_valid()
+
+    def test_process_keyseq(self):
+        k = Keyseq('<Control-Alt-Key-x>')
+        result = k.result
+        self.assertTrue(result['valid_beginning'])
+        self.assertTrue(result['valid_ending'])
+        self.assertFalse(result['has_invalid_keys'])
+        self.assertFalse(result['has_repeated_keys'])
+        self.assertTrue(result['is_valid'])
+
+        k = Keyseq('<Control-Alt-Control-Key-x')
+        result = k.result
+        self.assertTrue(result['valid_beginning'])
+        self.assertFalse(result['valid_ending'])
+        self.assertFalse(result['has_invalid_keys'])
+        self.assertTrue(result['has_repeated_keys'])
+        self.assertFalse(result['is_valid'])
+
+if __name__ == '__main__':
+    unittest.main(verbosity=2)


More information about the IDLE-dev mailing list