Rename to String Substitution
This commit is contained in:
		@@ -1,6 +1,6 @@
 | 
			
		||||
# Text Replacement
 | 
			
		||||
# String Substitution
 | 
			
		||||
 | 
			
		||||
The Text Replacement module lets a user replace one typed sequence of characters with another. If a string of characters you type matches an entry in your dictionary, it gets deleted and replaced with the corresponding replacement string.
 | 
			
		||||
The String Substitution module lets a user replace one typed sequence of characters with another. If a string of characters you type matches an entry in your dictionary, it gets deleted and replaced with the corresponding replacement string.
 | 
			
		||||
 | 
			
		||||
Potential uses:
 | 
			
		||||
 | 
			
		||||
@@ -9,20 +9,20 @@ Potential uses:
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
The Text Replacement module takes a single argument to be passed during initialization: a user-defined dictionary where the keys are the text to be replaced and the values are the replacement text.
 | 
			
		||||
The String Substitution module takes a single argument to be passed during initialization: a user-defined dictionary where the keys are the text to be replaced and the values are the replacement text.
 | 
			
		||||
 | 
			
		||||
Example is as follows:
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
from kmk.modules.text_replacement import TextReplacement
 | 
			
		||||
from kmk.modules.string_substitution import StringSubstitution
 | 
			
		||||
 | 
			
		||||
my_dictionary = {
 | 
			
		||||
    'yuo': 'you',
 | 
			
		||||
    ':sig': 'John Doe',
 | 
			
		||||
    'idk': "I don't know"
 | 
			
		||||
}
 | 
			
		||||
text_replacement = TextReplacement(dictionary=my_dictionary)
 | 
			
		||||
keyboard.modules.append(text_replacement)
 | 
			
		||||
string_substitution = StringSubstitution(dictionary=my_dictionary)
 | 
			
		||||
keyboard.modules.append(string_substitution)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Recommendations
 | 
			
		||||
 
 | 
			
		||||
@@ -1,204 +0,0 @@
 | 
			
		||||
try:
 | 
			
		||||
    from typing import Optional
 | 
			
		||||
except ImportError:
 | 
			
		||||
    # we're not in a dev environment, so we don't need to worry about typing
 | 
			
		||||
    pass
 | 
			
		||||
from micropython import const
 | 
			
		||||
 | 
			
		||||
from kmk.keys import KC, Key
 | 
			
		||||
from kmk.modules import Module
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class State:
 | 
			
		||||
    LISTENING = const(0)
 | 
			
		||||
    DELETING = const(1)
 | 
			
		||||
    SENDING = const(2)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Character:
 | 
			
		||||
    '''Helper class for making a left-shifted key identical to a right-shifted key'''
 | 
			
		||||
 | 
			
		||||
    is_shifted: bool = False
 | 
			
		||||
 | 
			
		||||
    def __init__(self, key_code: Key, is_shifted: bool) -> None:
 | 
			
		||||
        self.is_shifted = is_shifted
 | 
			
		||||
        self.key_code = KC.LSHIFT(key_code) if is_shifted else key_code
 | 
			
		||||
 | 
			
		||||
    def __eq__(self, other: any) -> bool:  # type: ignore
 | 
			
		||||
        try:
 | 
			
		||||
            return (
 | 
			
		||||
                self.key_code.code == other.key_code.code
 | 
			
		||||
                and self.is_shifted == other.is_shifted
 | 
			
		||||
            )
 | 
			
		||||
        except AttributeError:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Phrase:
 | 
			
		||||
    '''Manages a collection of characters and keeps an index of them so that potential matches can be tracked'''
 | 
			
		||||
 | 
			
		||||
    def __init__(self, string: str) -> None:
 | 
			
		||||
        self._characters: list[Character] = []
 | 
			
		||||
        self._index: int = 0
 | 
			
		||||
        for char in string:
 | 
			
		||||
            try:
 | 
			
		||||
                key_code = getattr(KC, char.upper())
 | 
			
		||||
                shifted = char.isupper() or key_code.has_modifiers == {2}
 | 
			
		||||
                self._characters.append(Character(key_code, shifted))
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                raise ValueError(f'Invalid character in dictionary: {char}')
 | 
			
		||||
 | 
			
		||||
    def next_character(self) -> None:
 | 
			
		||||
        '''Increment the current index for this phrase'''
 | 
			
		||||
        if not self.index_at_end():
 | 
			
		||||
            self._index += 1
 | 
			
		||||
 | 
			
		||||
    def get_character_at_index(self, index: int) -> Character:
 | 
			
		||||
        '''Returns the character at the given index'''
 | 
			
		||||
        return self._characters[index]
 | 
			
		||||
 | 
			
		||||
    def get_character_at_current_index(self) -> Character:
 | 
			
		||||
        '''Returns the character at the current index for this phrase'''
 | 
			
		||||
        return self._characters[self._index]
 | 
			
		||||
 | 
			
		||||
    def reset_index(self) -> None:
 | 
			
		||||
        '''Reset the index to the start of the phrase'''
 | 
			
		||||
        self._index = 0
 | 
			
		||||
 | 
			
		||||
    def index_at_end(self) -> bool:
 | 
			
		||||
        '''Returns True if the index is at the end of the phrase'''
 | 
			
		||||
        return self._index == len(self._characters)
 | 
			
		||||
 | 
			
		||||
    def character_is_at_current_index(self, character) -> bool:
 | 
			
		||||
        '''Returns True if the given character is the next character in the phrase'''
 | 
			
		||||
        return self.get_character_at_current_index() == character
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Rule:
 | 
			
		||||
    '''Represents the relationship between a phrase to be substituted and its substitution'''
 | 
			
		||||
 | 
			
		||||
    def __init__(self, to_substitute: Phrase, substitution: Phrase) -> None:
 | 
			
		||||
        self.to_substitute: Phrase = to_substitute
 | 
			
		||||
        self.substitution: Phrase = substitution
 | 
			
		||||
 | 
			
		||||
    def restart(self) -> None:
 | 
			
		||||
        '''Resets this rule's to_substitute and substitution phrases'''
 | 
			
		||||
        self.to_substitute.reset_index()
 | 
			
		||||
        self.substitution.reset_index()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TextReplacement(Module):
 | 
			
		||||
    _shifted: bool = False
 | 
			
		||||
    _rules: list = []
 | 
			
		||||
    _state: State = State.LISTENING
 | 
			
		||||
    _matched_rule: Optional[Phrase] = None
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        dictionary: dict,
 | 
			
		||||
    ):
 | 
			
		||||
        for key,value in dictionary.items():
 | 
			
		||||
            self._rules.append(Rule(Phrase(key), Phrase(value)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def process_key(self, keyboard, key, is_pressed, int_coord):
 | 
			
		||||
        if not self._state == State.LISTENING:
 | 
			
		||||
            return
 | 
			
		||||
        if key is KC.LSFT or key is KC.RSFT:
 | 
			
		||||
            if is_pressed:
 | 
			
		||||
                self._shifted = True
 | 
			
		||||
            else:
 | 
			
		||||
                self._shifted = False
 | 
			
		||||
        if is_pressed:
 | 
			
		||||
            character = Character(key, self._shifted)
 | 
			
		||||
 | 
			
		||||
            # run through the dictionary to check for a possible match on each new keypress
 | 
			
		||||
            for rule in self._rules:
 | 
			
		||||
                if rule.to_substitute.character_is_at_current_index(character):
 | 
			
		||||
                    rule.to_substitute.next_character()
 | 
			
		||||
                else:
 | 
			
		||||
                    rule.restart()
 | 
			
		||||
                    # if character is not a match at the current index,
 | 
			
		||||
                    # it could still be a match at the start of the sequence
 | 
			
		||||
                    # so redo the check after resetting the sequence
 | 
			
		||||
                    if rule.to_substitute.character_is_at_current_index(character):
 | 
			
		||||
                        rule.to_substitute.next_character()
 | 
			
		||||
                # we've matched all of the characters in a phrase to be substituted
 | 
			
		||||
                if rule.to_substitute.index_at_end():
 | 
			
		||||
                    rule.restart()
 | 
			
		||||
                    # set the phrase indexes to where they differ
 | 
			
		||||
                    # so that only the characters that differ are replaced
 | 
			
		||||
                    for character in rule.to_substitute._characters:
 | 
			
		||||
                        if (
 | 
			
		||||
                            character
 | 
			
		||||
                            == rule.substitution.get_character_at_current_index()
 | 
			
		||||
                        ):
 | 
			
		||||
                            rule.to_substitute.next_character()
 | 
			
		||||
                            rule.substitution.next_character()
 | 
			
		||||
                        else:
 | 
			
		||||
                            break
 | 
			
		||||
                        if rule.to_substitute.index_at_end():
 | 
			
		||||
                            break
 | 
			
		||||
                    self._matched_rule = rule
 | 
			
		||||
                    self._state = State.DELETING
 | 
			
		||||
                    # if we have a match there's no reason to continue the full key processing, so return out
 | 
			
		||||
                    return
 | 
			
		||||
        return key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def during_bootup(self, keyboard):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def before_matrix_scan(self, keyboard):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def before_hid_send(self, keyboard):
 | 
			
		||||
 | 
			
		||||
        if self._state == State.LISTENING:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if self._state == State.DELETING:
 | 
			
		||||
            # send backspace taps equivalent to the length of the phrase to be substituted
 | 
			
		||||
            to_substitute: Phrase = self._matched_rule.to_substitute  # type: ignore
 | 
			
		||||
            to_substitute.next_character()
 | 
			
		||||
            if not to_substitute.index_at_end():
 | 
			
		||||
                keyboard.tap_key(KC.BSPC)
 | 
			
		||||
            else:
 | 
			
		||||
                self._state = State.SENDING
 | 
			
		||||
                # force-release modifiers so sending the replacement text doesn't interact with them
 | 
			
		||||
                # it should not be possible for any modifiers other than shift to be held upon rule activation
 | 
			
		||||
                # as a modified key won't send a keycode that is matched against the user's dictionary,
 | 
			
		||||
                # but, just in case, we'll release those too
 | 
			
		||||
                keys_to_release = [
 | 
			
		||||
                    KC.LSHIFT,
 | 
			
		||||
                    KC.RSHIFT,
 | 
			
		||||
                    KC.LCTL,
 | 
			
		||||
                    KC.RCTL,
 | 
			
		||||
                    KC.LALT,
 | 
			
		||||
                    KC.RALT,
 | 
			
		||||
                ]
 | 
			
		||||
                for key in keys_to_release:
 | 
			
		||||
                    keyboard.remove_key(key)
 | 
			
		||||
 | 
			
		||||
        if self._state == State.SENDING:
 | 
			
		||||
            substitution = self._matched_rule.substitution  # type: ignore
 | 
			
		||||
            if not substitution.index_at_end():
 | 
			
		||||
                keyboard.tap_key(substitution.get_character_at_current_index().key_code)
 | 
			
		||||
                substitution.next_character()
 | 
			
		||||
            else:
 | 
			
		||||
                self._state = State.LISTENING
 | 
			
		||||
                self._matched_rule = None
 | 
			
		||||
                for rule in self._rules:
 | 
			
		||||
                    rule.restart()
 | 
			
		||||
 | 
			
		||||
    def after_hid_send(self, keyboard):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def on_powersave_enable(self, keyboard):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def on_powersave_disable(self, keyboard):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def after_matrix_scan(self, keyboard):
 | 
			
		||||
        return
 | 
			
		||||
@@ -1,216 +0,0 @@
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from kmk.keys import ALL_ALPHAS, ALL_NUMBERS, KC
 | 
			
		||||
from kmk.modules.text_replacement import Character, Phrase, Rule, TextReplacement
 | 
			
		||||
from tests.keyboard_test import KeyboardTest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTextReplacement(unittest.TestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.symbols = '`-=[]\\;\',./~!@#$%^&*()_+{}|:\"<>?'
 | 
			
		||||
        self.everything = ALL_NUMBERS + ALL_ALPHAS + ALL_ALPHAS.lower() + self.symbols
 | 
			
		||||
        self.test_dictionary = {'aa': 'b', 'b': 'aa', '!': '@', 'dccc': 'dcbb'}
 | 
			
		||||
        self.text_replacement = TextReplacement(self.test_dictionary)
 | 
			
		||||
        self.keyboard = KeyboardTest(
 | 
			
		||||
            [self.text_replacement],
 | 
			
		||||
            [
 | 
			
		||||
                [KC.A, KC.B, KC.N1, KC.LSHIFT, KC.LCTRL, KC.C, KC.D],
 | 
			
		||||
            ],
 | 
			
		||||
            debug_enabled=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return super().setUp()
 | 
			
		||||
 | 
			
		||||
    def test_keyboard_events_are_correct(self):
 | 
			
		||||
        # backspace doesn't have to fire for the final key pressed
 | 
			
		||||
        # that results in a corresponding match, as that key is never sent
 | 
			
		||||
        # the matching key also never sends a keyup event
 | 
			
		||||
        self.keyboard.test(
 | 
			
		||||
            'multi-character key, single-character value',
 | 
			
		||||
            [(0, True), (0, False), (0, True), (0, False)],
 | 
			
		||||
            [{KC.A}, {}, {KC.BACKSPACE}, {}, {KC.B}, {}],
 | 
			
		||||
        )
 | 
			
		||||
        # note: the pressed key is never sent here, as the event is
 | 
			
		||||
        # intercepted and the replacement is sent instead
 | 
			
		||||
        self.keyboard.test(
 | 
			
		||||
            'multi-character value, single-character key',
 | 
			
		||||
            [(1, True), (1, False)],
 | 
			
		||||
            [{KC.A}, {}, {KC.A}, {}],
 | 
			
		||||
        )
 | 
			
		||||
        # modifiers are force-released if there's a match,
 | 
			
		||||
        # so the keyup event for them isn't sent
 | 
			
		||||
        self.keyboard.test(
 | 
			
		||||
            'shifted alphanumeric or symbol in key and/or value',
 | 
			
		||||
            [(3, True), (2, True), (2, False), (3, False)],
 | 
			
		||||
            [{KC.LSHIFT}, {KC.LSHIFT, KC.N2}, {}],
 | 
			
		||||
        )
 | 
			
		||||
        self.keyboard.test(
 | 
			
		||||
            'backspace is only tapped as many times as necessary to delete the difference between the key and value',
 | 
			
		||||
            [
 | 
			
		||||
                (6, True),
 | 
			
		||||
                (6, False),
 | 
			
		||||
                (5, True),
 | 
			
		||||
                (5, False),
 | 
			
		||||
                (5, True),
 | 
			
		||||
                (5, False),
 | 
			
		||||
                (5, True),
 | 
			
		||||
                (5, False),
 | 
			
		||||
            ],
 | 
			
		||||
            [
 | 
			
		||||
                {KC.D},
 | 
			
		||||
                {},
 | 
			
		||||
                {KC.C},
 | 
			
		||||
                {},
 | 
			
		||||
                {KC.C},
 | 
			
		||||
                {},
 | 
			
		||||
                {KC.BACKSPACE},
 | 
			
		||||
                {},
 | 
			
		||||
                {KC.B},
 | 
			
		||||
                {},
 | 
			
		||||
                {KC.B},
 | 
			
		||||
                {},
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_invalid_character_in_dictionary_throws_error(self):
 | 
			
		||||
        dict = {
 | 
			
		||||
            'illegal_character_in_key': {'é': 'a'},
 | 
			
		||||
            'illegal_character_in_value': {'a': 'é'},
 | 
			
		||||
        }
 | 
			
		||||
        self.assertRaises(ValueError, TextReplacement, dict['illegal_character_in_key'])
 | 
			
		||||
        self.assertRaises(
 | 
			
		||||
            ValueError, TextReplacement, dict['illegal_character_in_value']
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_character_constructs_properly(self):
 | 
			
		||||
        unshifted_character = Character(KC.A, False)
 | 
			
		||||
        shifted_letter = Character(KC.A, True)
 | 
			
		||||
        shifted_symbol = Character(KC.N1, True)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            unshifted_character.key_code,
 | 
			
		||||
            KC.A,
 | 
			
		||||
            'unshifted character key code is correct',
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            shifted_letter.key_code.__dict__,
 | 
			
		||||
            KC.LSHIFT(KC.A).__dict__,
 | 
			
		||||
            'shifted letter key code is correct',
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            shifted_symbol.key_code.__dict__,
 | 
			
		||||
            KC.LSHIFT(KC.N1).__dict__,
 | 
			
		||||
            'shifted symbol key code is correct',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_phrase_constructs_properly(self):
 | 
			
		||||
        combination = ALL_NUMBERS + ALL_ALPHAS + ALL_ALPHAS.lower()
 | 
			
		||||
        multi_character_phrase = Phrase(combination)
 | 
			
		||||
 | 
			
		||||
        # lower case
 | 
			
		||||
        for letter in ALL_ALPHAS:
 | 
			
		||||
            letter = letter.lower()
 | 
			
		||||
            phrase = Phrase(letter)
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                phrase.get_character_at_index(0).key_code,
 | 
			
		||||
                KC[letter],
 | 
			
		||||
                f'Test failed when constructing phrase with lower-case letter {letter}',
 | 
			
		||||
            )
 | 
			
		||||
        # upper case
 | 
			
		||||
        for letter in ALL_ALPHAS:
 | 
			
		||||
            phrase = Phrase(letter)
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                phrase.get_character_at_index(0).key_code.__dict__,
 | 
			
		||||
                KC.LSHIFT(KC[letter]).__dict__,
 | 
			
		||||
                f'Test failed when constructing phrase with upper-case letter {letter}',
 | 
			
		||||
            )
 | 
			
		||||
        # numbers
 | 
			
		||||
        for letter in ALL_NUMBERS:
 | 
			
		||||
            phrase = Phrase(letter)
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                phrase.get_character_at_index(0).key_code,
 | 
			
		||||
                KC[letter],
 | 
			
		||||
                f'Test failed when constructing phrase with number {letter}',
 | 
			
		||||
            )
 | 
			
		||||
        # multi-character phrase
 | 
			
		||||
        for i, character in enumerate(combination):
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                multi_character_phrase.get_character_at_index(i).key_code.__dict__,
 | 
			
		||||
                KC.LSHIFT(KC[character]).__dict__
 | 
			
		||||
                if combination[i].isupper()
 | 
			
		||||
                else KC[character].__dict__,
 | 
			
		||||
                f'Test failed when constructing phrase with character {character}',
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_phrase_with_symbols_constructs_properly(self):
 | 
			
		||||
        phrase = Phrase(self.symbols)
 | 
			
		||||
        for i, symbol in enumerate(self.symbols):
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                phrase.get_character_at_index(i).key_code.__dict__,
 | 
			
		||||
                KC[symbol].__dict__,
 | 
			
		||||
                'Test failed for symbol {}'.format(symbol),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_phrase_indexes_correctly(self):
 | 
			
		||||
        phrase = Phrase(ALL_ALPHAS.lower())
 | 
			
		||||
        i = 0
 | 
			
		||||
        while not phrase.index_at_end():
 | 
			
		||||
            self.assertTrue(
 | 
			
		||||
                phrase.character_is_at_current_index(phrase.get_character_at_index(i)),
 | 
			
		||||
                'Current character in the phrase is not the expected one',
 | 
			
		||||
            )
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                phrase.get_character_at_index(i).key_code.__dict__,
 | 
			
		||||
                KC[ALL_ALPHAS[i]].__dict__,
 | 
			
		||||
                f'Character at index {i} is not {ALL_ALPHAS[i]}',
 | 
			
		||||
            )
 | 
			
		||||
            phrase.next_character()
 | 
			
		||||
            i += 1
 | 
			
		||||
            self.assertLess(
 | 
			
		||||
                i, len(ALL_ALPHAS) + 1, 'While loop checking phrase index ran too long'
 | 
			
		||||
            )
 | 
			
		||||
        phrase.reset_index()
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            phrase.get_character_at_current_index().key_code,
 | 
			
		||||
            KC[ALL_ALPHAS[0]],
 | 
			
		||||
            'Phrase did not reset its index to 0',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_sanity_check(self):
 | 
			
		||||
        '''Test character/phrase construction with every letter, number, and symbol, shifted and unshifted'''
 | 
			
		||||
        phrase = Phrase(self.everything)
 | 
			
		||||
        for i, character in enumerate(self.everything):
 | 
			
		||||
            self.assertEqual(
 | 
			
		||||
                phrase.get_character_at_index(i).key_code.__dict__,
 | 
			
		||||
                KC.LSHIFT(KC[character]).__dict__
 | 
			
		||||
                if self.everything[i].isupper()
 | 
			
		||||
                else KC[character].__dict__,
 | 
			
		||||
                f'Test failed when constructing phrase with character {character}',
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def test_rule(self):
 | 
			
		||||
        phrase1 = Phrase(self.everything)
 | 
			
		||||
        phrase2 = Phrase(self.everything)
 | 
			
		||||
        rule = Rule(phrase1, phrase2)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            rule.to_substitute, phrase1, "Rule's entry to be substituted is correct"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            rule.substitution, phrase2, "Rule's substitution entry is correct"
 | 
			
		||||
        )
 | 
			
		||||
        rule.to_substitute.next_character()
 | 
			
		||||
        rule.substitution.next_character()
 | 
			
		||||
        rule.restart()
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            rule.to_substitute.get_character_at_index(0).key_code,
 | 
			
		||||
            KC[self.everything[0]],
 | 
			
		||||
            'Rule did not call to_substitute.reset_index() when rule.restart() was called',
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            rule.substitution.get_character_at_index(0).key_code,
 | 
			
		||||
            KC[self.everything[0]],
 | 
			
		||||
            'Rule did not call substitution.reset_index() when rule.restart() was called',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    unittest.main()
 | 
			
		||||
		Reference in New Issue
	
	Block a user