From f1a75bacb27301b0c67ca8aec3884387bad88236 Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Tue, 14 Mar 2023 14:51:16 +0100 Subject: [PATCH] Handle string input to Program.fromAssembly() (#3038) * Add tests for fromAssembly() output (#3036) * Handle string input to fromAssembly() (Fixes #3036) * Fixups suggested by @anthrotype * Add some more typing annotations * Program.assembly always returns List[str] now * Add annotation for bytecode * Move code from setter to fromAssembly * Remove property * Fix attribute names --- Lib/fontTools/ttLib/tables/ttProgram.py | 41 ++++++++++++++----------- Tests/ttLib/tables/ttProgram_test.py | 19 ++++++++++++ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/ttProgram.py b/Lib/fontTools/ttLib/tables/ttProgram.py index 104ae4a3e..5c46f6666 100644 --- a/Lib/fontTools/ttLib/tables/ttProgram.py +++ b/Lib/fontTools/ttLib/tables/ttProgram.py @@ -1,8 +1,10 @@ """ttLib.tables.ttProgram.py -- Assembler/disassembler for TrueType bytecode programs.""" +from __future__ import annotations from fontTools.misc.textTools import num2binary, binary2num, readHex, strjoin import array from io import StringIO +from typing import List import re import logging @@ -1064,30 +1066,35 @@ def _skipWhite(data, pos): class Program(object): - def __init__(self): + def __init__(self) -> None: pass - def fromBytecode(self, bytecode): + def fromBytecode(self, bytecode: bytes) -> None: self.bytecode = array.array("B", bytecode) if hasattr(self, "assembly"): del self.assembly - def fromAssembly(self, assembly): - self.assembly = assembly + def fromAssembly(self, assembly: List[str] | str) -> None: + if isinstance(assembly, list): + self.assembly = assembly + elif isinstance(assembly, str): + self.assembly = assembly.splitlines() + else: + raise TypeError(f"expected str or List[str], got {type(assembly).__name__}") if hasattr(self, "bytecode"): del self.bytecode - def getBytecode(self): + def getBytecode(self) -> bytes: if not hasattr(self, "bytecode"): self._assemble() return self.bytecode.tobytes() - def getAssembly(self, preserve=True): + def getAssembly(self, preserve=True) -> List[str]: if not hasattr(self, "assembly"): self._disassemble(preserve=preserve) return self.assembly - def toXML(self, writer, ttFont): + def toXML(self, writer, ttFont) -> None: if ( not hasattr(ttFont, "disassembleInstructions") or ttFont.disassembleInstructions @@ -1128,7 +1135,7 @@ class Program(object): i = i + 1 if m: nValues = int(m.group(1)) - line = [] + line: List[str] = [] j = 0 for j in range(nValues): if j and not (j % 25): @@ -1155,7 +1162,7 @@ class Program(object): writer.endtag("bytecode") writer.newline() - def fromXML(self, name, attrs, content, ttFont): + def fromXML(self, name, attrs, content, ttFont) -> None: if name == "assembly": self.fromAssembly(strjoin(content)) self._assemble() @@ -1164,11 +1171,9 @@ class Program(object): assert name == "bytecode" self.fromBytecode(readHex(content)) - def _assemble(self): - assembly = getattr(self, "assembly", []) - if isinstance(assembly, type([])): - assembly = " ".join(assembly) - bytecode = [] + def _assemble(self) -> None: + assembly = " ".join(getattr(self, "assembly", [])) + bytecode: List[int] = [] push = bytecode.append lenAssembly = len(assembly) pos = _skipWhite(assembly, 0) @@ -1311,7 +1316,7 @@ class Program(object): assert max(bytecode) < 256 and min(bytecode) >= 0 self.bytecode = array.array("B", bytecode) - def _disassemble(self, preserve=False): + def _disassemble(self, preserve=False) -> None: assembly = [] i = 0 bytecode = getattr(self, "bytecode", []) @@ -1376,7 +1381,7 @@ class Program(object): i = i + 1 self.assembly = assembly - def __bool__(self): + def __bool__(self) -> bool: """ >>> p = Program() >>> bool(p) @@ -1406,12 +1411,12 @@ class Program(object): __nonzero__ = __bool__ - def __eq__(self, other): + def __eq__(self, other) -> bool: if type(self) != type(other): return NotImplemented return self.__dict__ == other.__dict__ - def __ne__(self, other): + def __ne__(self, other) -> bool: result = self.__eq__(other) return result if result is NotImplemented else not result diff --git a/Tests/ttLib/tables/ttProgram_test.py b/Tests/ttLib/tables/ttProgram_test.py index 2bed538ca..10a029587 100644 --- a/Tests/ttLib/tables/ttProgram_test.py +++ b/Tests/ttLib/tables/ttProgram_test.py @@ -12,6 +12,13 @@ DATA_DIR = os.path.join(CURR_DIR, "data") TTPROGRAM_TTX = os.path.join(DATA_DIR, "ttProgram.ttx") # TTPROGRAM_BIN = os.path.join(DATA_DIR, "ttProgram.bin") +ASSEMBLY = [ + "PUSH[ ]", + "0 4 3", + "INSTCTRL[ ]", + "POP[ ]", +] + BYTECODE = deHexStr( "403b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a" "191817161514131211100f0e0d0c0b0a090807060504030201002c01b0184358456ab0" @@ -90,6 +97,18 @@ class ProgramTest(unittest.TestCase): assert p.assembly.pop() == "SVTCA[0]" assert not bool(p) + def test_from_assembly_list(self): + p = Program() + p.fromAssembly(ASSEMBLY) + asm = p.getAssembly() + assert ASSEMBLY == asm + + def test_from_assembly_str(self): + p = Program() + p.fromAssembly("\n".join(ASSEMBLY)) + asm = p.getAssembly() + assert ASSEMBLY == asm + def test_roundtrip(self): p = Program() p.fromBytecode(BYTECODE)