functions kinda working

This commit is contained in:
Jesse Bannon 2023-07-10 23:58:08 -07:00
parent 67364b29e5
commit 03c90b36f2
4 changed files with 257 additions and 45 deletions

View file

@ -1,7 +1,4 @@
class Functions: class Functions:
@staticmethod @staticmethod
def lower(string: str) -> str: def lower(string: str) -> str:
""" """
@ -28,4 +25,3 @@ class Functions:
Capitalized string Capitalized string
""" """
return string.capitalize() return string.capitalize()

View file

@ -1,75 +1,231 @@
from dataclasses import dataclass from dataclasses import dataclass
from queue import LifoQueue from queue import LifoQueue
from typing import Optional, List from typing import List
from typing import Optional
from typing import Union
from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.validators.string_formatter_validators import is_valid_source_variable_name from ytdl_sub.validators.string_formatter_validators import is_valid_source_variable_name
@dataclass # pylint: disable=invalid-name
@dataclass(frozen=True)
class Integer:
value: int
@dataclass(frozen=True)
class Float:
value: float
@dataclass(frozen=True)
class Boolean:
value: bool
@dataclass(frozen=True)
class String:
value: str
@dataclass(frozen=True)
class Variable: class Variable:
name: str name: str
@dataclass
NumericType = Union[Integer, Float]
ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function"]
@dataclass(frozen=True)
class Function: class Function:
name: str name: str
args: List[str] args: List[ArgumentType]
@dataclass(frozen=True)
class LiteralString:
value: str
class Parser: class Parser:
def __init__(self, text: str): def __init__(self, text: str):
self._text = text self._text = text
self._pos = 0 self._pos = 0
self._stack: LifoQueue[Variable | Function] = LifoQueue() self._stack: LifoQueue[LiteralString | Variable | Function] = LifoQueue()
def read(self) -> Optional[str]: def read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]:
try: try:
ch = self._text[self._pos] ch = self._text[self._pos : (self._pos + length)]
except IndexError: except IndexError:
return None return None
self._pos += 1 if increment_pos:
self._pos += length
return ch return ch
def parse_variable(self) -> Variable: def parse_variable(self) -> Variable:
var_name = "" var_name = ""
while ch := self.read(): while ch := self.read(increment_pos=False):
if ch == "}": if ch.isspace() and not var_name:
self._pos += 1
continue
if ch in ["}", ","] or ch.isspace():
break break
var_name += ch
_ = is_valid_source_variable_name(var_name, raise_exception=True) is_lower = ch.isascii() and ch.islower()
if not var_name and not is_lower:
raise StringFormattingException("invalid var name")
if not (is_lower or ch.isnumeric() or ch == "_"):
raise StringFormattingException("invalid var name")
var_name += ch
self._pos += 1
assert is_valid_source_variable_name(var_name, raise_exception=False)
return Variable(var_name) return Variable(var_name)
def parse_function(self) -> Function: def parse_numeric(self) -> NumericType:
parenthesis_counter = 0 numeric_string = ""
func_name = "" while ch := self.read(increment_pos=False):
func_args = "" if not (ch.isnumeric() or ch == "."):
break
self._pos += 1
numeric_string += ch
try:
numeric_float = float(numeric_string)
except ValueError:
raise StringFormattingException(f"Invalid numeric: {numeric_string}")
if (numeric_int := int(numeric_float)) == numeric_float:
return Integer(value=numeric_int)
return Float(value=numeric_float)
def parse_string(self) -> String:
"""
Begin parsing a string, including the quotation value
"""
string_value = ""
open_quotation_char = self.read()
assert open_quotation_char in ["'", '"']
while ch := self.read(): while ch := self.read():
if ch not in ['(', ')']: if ch == open_quotation_char:
if parenthesis_counter > 0: return String(value=string_value)
func_args += ch string_value += ch
raise StringFormattingException("String not closed")
def parse_function_arg(self) -> ArgumentType:
if self.read(increment_pos=False) == "%":
self._pos += 1
return self.parse_function()
if self.read(increment_pos=False).isnumeric():
return self.parse_numeric()
if (self.read(increment_pos=False, length=4) or "").lower() == "true":
self._pos += 4
return Boolean(value=True)
if (self.read(increment_pos=False, length=5) or "").lower() == "false":
self._pos += 5
return Boolean(value=False)
if self.read(increment_pos=False) in ["'", '"']:
return self.parse_string()
if self.read(increment_pos=False).isascii() and self.read(increment_pos=False).islower():
return self.parse_variable()
raise StringFormattingException(
"Invalid function argument, should be either a function, int, float, "
"string, boolean, or variable without brackets"
)
def parse_function_args(self) -> List[ArgumentType]:
"""
Begin parsing function args after the first ``(``, i.e. ``function_name(``
"""
argument_index = 0
comma_count = 0
arguments: List[ArgumentType] = []
while ch := self.read(increment_pos=False):
if ch == ")":
break
if ch.isspace():
self._pos += 1
elif ch == ",":
comma_count += 1
if argument_index != comma_count:
raise StringFormattingException("Comma argument shenanigans")
self._pos += 1
else:
argument_index += 1
arguments.append(self.parse_function_arg())
return arguments
def parse_function(self) -> Function:
"""
Begin parsing a function after reading the first ``%``
"""
function_name: str = ""
function_args: List[String | Variable | "Function"] = []
while ch := self.read():
if ch == ")":
return Function(name=function_name, args=function_args)
if ch != "(":
function_name += ch
else:
function_args = self.parse_function_args()
raise StringFormattingException("Invalid function")
def parse(self) -> "Parser":
bracket_counter = 0
literal_str = ""
while ch := self.read():
if ch == "}":
bracket_counter -= 1
break
if ch == "{":
bracket_counter += 1
if literal_str:
self._stack.put(LiteralString(literal_str))
literal_str = ""
# Allow whitespace after bracket opening
while ch1 := self.read(increment_pos=False):
if not ch1.isspace():
break
self._pos += 1
if ch1 is None:
raise StringFormattingException(
"Open bracket at the end was not properly closed"
)
if ch1 == "%":
self._pos += 1
self._stack.put(self.parse_function())
else: else:
func_name += ch self._stack.put(self.parse_variable())
elif ch == '(': else:
parenthesis_counter += 1 literal_str += ch
elif ch == ')':
parenthesis_counter -= 1 if bracket_counter != 0:
if parenthesis_counter == 0: raise StringFormattingException("Bracket count mismatch")
break
if literal_str:
self._stack.put(LiteralString(literal_str))
return self
def parse(self):
while True:
ch = self.read()
if ch == '{':
self.parse_variable()
if ch == '%':
self.parse_function()
# pylint: enable=invalid-name

View file

View file

@ -0,0 +1,60 @@
import pytest
from ytdl_sub.script.parser import Boolean
from ytdl_sub.script.parser import Float
from ytdl_sub.script.parser import Function
from ytdl_sub.script.parser import Integer
from ytdl_sub.script.parser import LiteralString
from ytdl_sub.script.parser import Parser
from ytdl_sub.script.parser import String
from ytdl_sub.script.parser import Variable
from ytdl_sub.utils.exceptions import StringFormattingException
class TestParser:
def test_simple(self):
parser = Parser("hello world").parse()
assert list(parser._stack.queue) == [LiteralString(value="hello world")]
def test_single_function_one_arg(self):
parser = Parser("hello {%capitalize('hi mom')}").parse()
assert list(parser._stack.queue) == [
LiteralString("hello "),
Function(name="capitalize", args=[String(value="hi mom")]),
]
@pytest.mark.parametrize('whitespace', ["", " ", " ", "\n", " \n "])
def test_single_function_multiple_args(self, whitespace: str):
s = whitespace
parser = Parser(
f"hello{s}{{{s}%concat({s}'string'{s},{s}1{s},{s}2.4{s},"
f"{s}TRUE{s},{s}variable_name{s},{s}%capitalize({s}'hi'{s}){s}){s}}}"
).parse()
assert list(parser._stack.queue) == [
LiteralString(value=f"hello{s}"),
Function(
name="concat",
args=[
String(value="string"),
Integer(value=1),
Float(value=2.4),
Boolean(value=True),
Variable(name="variable_name"),
Function(name="capitalize", args=[String(value="hi")]),
],
),
] + ([LiteralString(value=s)] if s else [])
class TestParserBracketFailures:
def test_bracket_open(self):
with pytest.raises(StringFormattingException):
_ = Parser("{").parse()
def test_bracket_close(self):
with pytest.raises(StringFormattingException):
_ = Parser("}").parse()
def test_bracket_in_function(self):
with pytest.raises(StringFormattingException):
_ = Parser("hello {%capitalize({as_arg)}").parse()