Skip to content

Commit

Permalink
refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
PierreMarchand20 committed Dec 25, 2023
1 parent a1a8332 commit e040fd9
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 208 deletions.
5 changes: 3 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-python.black-formatter",
"ms-python.isort"
"charliermarsh.ruff",
"ms-python.mypy-type-checker",
"tamasfe.even-better-toml"
]
}
},
Expand Down
15 changes: 7 additions & 8 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,24 @@ jobs:

steps:
- uses: actions/checkout@v3

- name: Set up Python package
run: |
python3 -m pip install --upgrade pip
python3 -m pip install .[test]
python3 -m pip install .[dev]
- name: Run regression tests
run: |
set enable-bracketed-paste off
python3 -m pytest -v
- name: Check module imports with isort
- name: Check with ruff
run: |
python3 -m pip install isort
python3 -m isort . --check-only --diff
# uses: isort/isort-action@master
ruff asciinema_automation/ tests/
- name: Check formatting with black
uses: psf/black@stable
- name: Check with mypy
run: |
mypy asciinema_automation/ tests/
- name: Check building a binary wheel and a source tarball
run: |
Expand Down
5 changes: 1 addition & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ asciinema-automation
.. image:: https://badge.fury.io/py/asciinema-automation.svg
:target: https://badge.fury.io/py/asciinema-automation

.. image:: https://github.com/PierreMarchand20/asciinema_automation/actions/workflows/CI.yml/badge.svg
:target: https://github.com/PierreMarchand20/asciinema_automation/actions/workflows/CI.yml

.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black

Asciinema-Automation is a Python package which provides a small CLI utility to automate `asciinema <https://asciinema.org>`_ recordings. The only dependencies are asciinema and `Pexpect <https://pexpect.readthedocs.io/>`_.

Example
Expand Down
22 changes: 13 additions & 9 deletions asciinema_automation/cli.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import argparse
import logging
import pathlib
from typing import List, Optional

from asciinema_automation import parse
from asciinema_automation.script import Script


def cli(argv=None):
def cli(argv: Optional[List[str]] = None) -> None:
# Command line arguments
parser = argparse.ArgumentParser()
parser.add_argument(
Expand Down Expand Up @@ -35,7 +37,8 @@ def cli(argv=None):
"--standard-deviation",
type=int,
default=60,
help="standard deviation for gaussian used to generate time between key strokes",
help="""standard deviation for gaussian used to
generate time between key strokes""",
)
parser.add_argument(
"-t",
Expand All @@ -48,7 +51,8 @@ def cli(argv=None):
group.add_argument(
"-d",
"--debug",
help="set loglevel to DEBUG and output to 'outputfile.log'. Default loglevel to ERROR.",
help="""set loglevel to DEBUG
and output to 'outputfile.log'. Default loglevel to ERROR.""",
action="store_const",
dest="loglevel",
const=logging.DEBUG,
Expand All @@ -57,7 +61,8 @@ def cli(argv=None):
group.add_argument(
"-v",
"--verbose",
help="set loglevel to INFO and output to 'outputfile.log'. Default loglevel to ERROR.",
help="""set loglevel to INFO and output to 'outputfile.log'.
Default loglevel to ERROR.""",
action="store_const",
dest="loglevel",
const=logging.INFO,
Expand All @@ -82,13 +87,12 @@ def cli(argv=None):

# Script
script = Script(
inputfile,
outputfile,
asciinema_arguments,
wait,
delay,
standard_deviation,
timeout,
wait / 1000,
delay / 1000,
standard_deviation / 1000,
parse.parse_script_file(inputfile, timeout),
)

#
Expand Down
42 changes: 20 additions & 22 deletions asciinema_automation/instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,30 @@
import random
import re
import time
from typing import List

logger = logging.getLogger(__name__)

from .script import Instruction, Script

class Instruction:
def run(self, script):
logger.info(self.__class__.__name__)
logger = logging.getLogger(__name__)


class ChangeWaitInstruction(Instruction):
def __init__(self, wait):
def __init__(self, wait: float):
super().__init__()
self.wait = wait

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("%s->%s", script.wait, self.wait)
script.wait = self.wait


class ChangeDelayInstruction(Instruction):
def __init__(self, delay):
def __init__(self, delay: float):
super().__init__()
self.delay = delay

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("%s->%s", script.delay, self.delay)
script.delay = self.delay
Expand All @@ -39,24 +37,24 @@ def __init__(self, expect_value: str, timeout: int):
self.expect_value = expect_value
self.timeout = timeout

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("Expect %s", repr(self.expect_value))
script.process.expect(self.expect_value, timeout=self.timeout)


class SendInstruction(Instruction):
def __init__(self, send_value):
def __init__(self, send_value: str):
super().__init__()
self.send_value = send_value

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("Send %s", repr(self.send_value))
self.receive_value = self.send_value
# self.receive_value = self.send_value

# Check for special character
self.receive_value = [re.escape(c) for c in list(self.send_value)]
self.receive_value: List[str] = [re.escape(c) for c in list(self.send_value)]

# Write intruction
for send_character, receive_character in zip(
Expand All @@ -77,32 +75,32 @@ def run(self, script):


class SendCharacterInstruction(Instruction):
def __init__(self, send_value):
def __init__(self, send_value: str):
super().__init__()
self.send_value = send_value

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("Send '%s'", self.send_value)
script.process.send(self.send_value)


class SendShellInstruction(SendInstruction):
def __init__(self, command):
def __init__(self, command: str):
super().__init__(command)

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("Send '\\n'")
script.process.send("\n")


class SendControlInstruction(Instruction):
def __init__(self, control):
def __init__(self, control: str):
super().__init__()
self.control = control

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("Send ctrl+%s", self.control)
script.process.sendcontrol(self.control)
Expand All @@ -114,7 +112,7 @@ class SendArrowInstruction(Instruction):
KEY_RIGHT = "\x1b[C"
KEY_LEFT = "\x1b[D"

def __init__(self, send, num, enter=False):
def __init__(self, send: str, num: int, enter: bool = False):
super().__init__()
self.mapping = dict()
self.mapping["up"] = "\x1b[A"
Expand All @@ -125,7 +123,7 @@ def __init__(self, send, num, enter=False):
self.num = num
self.enter = enter

def run(self, script):
def run(self, script: Script) -> None:
super().run(script)
logger.debug("Send %s arrow %i times", self.send, self.num)
for _ in range(self.num):
Expand Down
110 changes: 110 additions & 0 deletions asciinema_automation/parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import codecs
import logging
import pathlib
import re

from .instruction import (
ChangeDelayInstruction,
ChangeWaitInstruction,
ExpectInstruction,
SendArrowInstruction,
SendCharacterInstruction,
SendControlInstruction,
SendInstruction,
SendShellInstruction,
)
from .script import Instruction

logger = logging.getLogger(__name__)

# To read escaped character from instructions
# https://stackoverflow.com/a/24519338/5913047
ESCAPE_SEQUENCE_RE = re.compile(
r"""
( \\U........ # 8-digit hex escapes
| \\u.... # 4-digit hex escapes
| \\x.. # 2-digit hex escapes
| \\[0-7]{1,3} # Octal escapes
| \\N\{[^}]+\} # Unicode characters by name
| \\[\\'"abfnrtv] # Single-character escapes
)""",
re.UNICODE | re.VERBOSE,
)


def decode_escapes(s: str) -> str:
def decode_match(match: re.Match[str]) -> str:
return codecs.decode(match.group(0), "unicode-escape")

return ESCAPE_SEQUENCE_RE.sub(decode_match, s)


def parse_script_file(inputfile: pathlib.Path, timeout: int) -> list["Instruction"]:
# Compile regex
wait_time_regex = re.compile(r"^#\$ wait (\d*)(?!\S)")
delay_time_regex = re.compile(r"^#\$ delay (\d*)(?!\S)")
sendcontrol_command_regex = re.compile(r"^#\$ sendcontrol ([a-z])(?!\S)")
sendcharacter_command_regex = re.compile(r"^#\$ sendcharacter (.*)(?!\S)")
expect_regex = re.compile(r"^#\$ expect (.*)(?!\S)")
send_regex = re.compile(r"^#\$ send (.*)(?!\S)")
arrow_command_regex = re.compile(
r"^#\$ sendarrow (down|up|left|right)(?:\s([\d]+))?(?!\S)"
)
arrow_sendline_command_regex = re.compile(
r"^#\$ sendlinearrow (down|up|left|right)(?:\s([\d]+))?(?!\S)"
)

instructions: list[Instruction] = []

with open(inputfile) as file:
previous_line = ""
for line in file:
if line.strip():
line = line.rstrip()

if match := wait_time_regex.search(line, 0):
wait_time = match.group(1)
instructions.append(ChangeWaitInstruction(int(wait_time) / 1000))
elif match := delay_time_regex.search(line, 0):
delay_time = match.group(1)
instructions.append(ChangeDelayInstruction(int(delay_time) / 1000))
elif match := sendcontrol_command_regex.search(line, 0):
sendcontrol_command = match.group(1)
instructions.append(SendControlInstruction(sendcontrol_command))
elif match := sendcharacter_command_regex.search(line, 0):
sendcharacter_command = match.group(1)
instructions.append(SendCharacterInstruction(sendcharacter_command))
elif match := arrow_command_regex.search(line, 0):
arrow_command = match.group(1)
arrow_num = match.group(2)
if arrow_num is None:
arrow_num = 1
instructions.append(
SendArrowInstruction(arrow_command, int(arrow_num), False)
)
elif match := arrow_sendline_command_regex.search(line, 0):
arrow_command = match.group(1)
arrow_num = match.group(2)
if arrow_num is None:
arrow_num = 1
instructions.append(
SendArrowInstruction(arrow_command, int(arrow_num), True)
)
elif match := expect_regex.search(line, 0):
expect_value = match.group(1)
expect_value = decode_escapes(expect_value)
instructions.append(ExpectInstruction(expect_value, timeout))
elif match := send_regex.search(line, 0):
send_value = match.group(1)
send_value = decode_escapes(send_value)
instructions.append(SendInstruction(send_value))
elif line.startswith("#"):
pass
else:
if line.endswith("\\"):
previous_line += line + "\n"
else:
instructions.append(SendShellInstruction(previous_line + line))
previous_line = ""

return instructions
Loading

0 comments on commit e040fd9

Please sign in to comment.