pythonでkey入力テスト

# 目的

pythonでkey入力のテストを行う。
seleniumでのwebへの自動入力でテストを行う方法は良く使われます。
ただ、もっと単純にキー入力のエミュレートだけしたい場合もあると思います。
pythonのライブラリで[keyboard](https://pypi.org/project/keyboard/)というものがありましたので、使ってみたいと思います。

***
## keyboardライブラリについて

keyboardはキーボード入力のエミュレート等ができます。
今回の目的に必要なのは、以下の関数となります。
[keyboard.press_and_release](https://github.com/boppreh/keyboard?tab=readme-ov-file#keyboard.send)（[keyboard.send](https://github.com/boppreh/keyboard?tab=readme-ov-file#keyboard.send)）

とりあえず、複雑なことは考えずにキーコードで送るだけを考えます。

***
# 各機能の実装
## 入力データ

スクリプトや文字列で入力データを指定できるように考えます。
また、wait時間も欲しいため、文字列中に埋め込めるようにします。

### 入力仕様概要

入力文字列の仕様は以下のような感じにします。

1. @&#xFEFF;wait(&lt;num&gt;)
  &lt;num&gt; [sec] 待機する

1. "&lt;string&gt;"
  任意の文字列（&lt;string&gt;）を送信する
  エスケープ文字列に対応する

1. 空白や改行はスキップする


***
## key送信処理について

キーコード送信には、[keyboard.press_and_release](https://github.com/boppreh/keyboard?tab=readme-ov-file#keyboard.send) を使用します。、
エスケープシーケンス文字列をコードに戻すために、codecs.decodeを使用します。

```python
    def send_keys(self, keys: str):
        """Send keys to emulate keyboard actions."""
        for key in keys:
            keyboard.press_and_release(key)

    def send_escape_text(self, text: str):
        """Send escape text to emulate keyboard actions."""
        keys = codecs.decode(text, 'unicode-escape')
        self.send_keys(keys)

```


***
## 入力データ解析

今回はお手軽に正規表現で対応します。

```python
    def __init__(self):
        """Initialize EmuKeyboard class."""
        self.pattern = r'(@wait\((?P<wait>\d+(\.\d+)?)\)|"(?P<string>[^"]*)"|(?P<other>\S+))'
        self.text = ""

    def analyze(self, text: str):
        """Analyze the given text and yield key actions."""
        matches = re.finditer(self.pattern, text)
        for match in matches:
            groups = match.groupdict()
            if groups['wait']:
                yield ('wait', float(groups['wait']))
            elif groups['string']:
                yield ('string', groups['string'])
            elif groups['other']:
                yield ('string', groups['other'])

```
analyze関数に文字列を渡すと、正規表現を使って分類して返します。


***
# 全体

コマンドライン引数を受け取る処理も追加して、全体を作成します。

```python:emukeyboard.py
import keyboard
import re
import time
import codecs
import argparse
import sys

class EmuKeyboard:
    def __init__(self):
        """Initialize EmuKeyboard class."""
        self.pattern = r'(@wait\((?P<wait>\d+(\.\d+)?)\)|"(?P<string>[^"]*)"|(?P<other>\S+))'
        self.text = ""

    def wait(self, sec: float = 1.0):
        """Wait for the specified seconds."""
        time.sleep(sec)

    def send_keys(self, keys: str):
        """Send keys to emulate keyboard actions."""
        for key in keys:
            keyboard.press_and_release(key)

    def send_escape_text(self, text: str):
        """Send escape text to emulate keyboard actions."""
        keys = codecs.decode(text, 'unicode-escape')
        self.send_keys(keys)

    def load(self, script: str):
        """Load script file and return its content."""
        try:
            with open(script, 'r', encoding='utf-8') as f:
                self.text = f.read()
                return self.text
        except FileNotFoundError:
            print(f"File not found: {script}")
            return None

    def send_text(self, text: str):
        """Send text to emulate keyboard actions."""
        self.text = text
        self.run()

    def run(self):
        """Run the script to emulate keyboard actions."""
        for keypair in self.analyze(self.text):
            if keypair[0] == 'wait':
                self.wait(keypair[1])
            else:
                self.send_escape_text(keypair[1])

    def analyze(self, text: str):
        """Analyze the given text and yield key actions."""
        matches = re.finditer(self.pattern, text)
        for match in matches:
            groups = match.groupdict()
            if groups['wait']:
                yield ('wait', float(groups['wait']))
            elif groups['string']:
                yield ('string', groups['string'])
            elif groups['other']:
                yield ('string', groups['other'])
            else:
                pass

def argparser() -> argparse.Namespace:
    """Parse command-line arguments and return them."""
    parser = argparse.ArgumentParser(description='Key Emulator')

    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-k', '--keys', type=str, default=None, help='Keys to be emulated')
    group.add_argument('-s', '--script', type=str, default=None, help='Script file containing keys to be emulated')

    parser.add_argument('-w', '--wait', type=float, default=1.0, help='Wait time before sending the keys')

    try:
        return parser.parse_args()
    except argparse.ArgumentError as e:
        print(f"Error: {str(e)}")
        sys.exit(1)
    except SystemExit:
        parser.print_help()
        sys.exit(2)

def main() -> None:
    """Main function to parse arguments and execute EmuKeyboard."""
    args = argparser()
    ek = EmuKeyboard()
    ek.wait(args.wait)
    if args.script is not None:
        if (ek.load(args.script) is not None):
            ek.run()
    else:
        ek.send_text(args.keys)

if __name__ == '__main__':
    main()

```