functions kinda working
This commit is contained in:
parent
67364b29e5
commit
03c90b36f2
4 changed files with 257 additions and 45 deletions
|
|
@ -1,7 +1,4 @@
|
|||
|
||||
|
||||
class Functions:
|
||||
|
||||
@staticmethod
|
||||
def lower(string: str) -> str:
|
||||
"""
|
||||
|
|
@ -28,4 +25,3 @@ class Functions:
|
|||
Capitalized string
|
||||
"""
|
||||
return string.capitalize()
|
||||
|
||||
|
|
@ -1,75 +1,231 @@
|
|||
from dataclasses import dataclass
|
||||
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
|
||||
|
||||
@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:
|
||||
name: str
|
||||
|
||||
@dataclass
|
||||
|
||||
NumericType = Union[Integer, Float]
|
||||
ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Function:
|
||||
name: str
|
||||
args: List[str]
|
||||
args: List[ArgumentType]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LiteralString:
|
||||
value: str
|
||||
|
||||
|
||||
class Parser:
|
||||
|
||||
def __init__(self, text: str):
|
||||
self._text = text
|
||||
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:
|
||||
ch = self._text[self._pos]
|
||||
ch = self._text[self._pos : (self._pos + length)]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
self._pos += 1
|
||||
if increment_pos:
|
||||
self._pos += length
|
||||
return ch
|
||||
|
||||
def parse_variable(self) -> Variable:
|
||||
var_name = ""
|
||||
while ch := self.read():
|
||||
if ch == "}":
|
||||
while ch := self.read(increment_pos=False):
|
||||
if ch.isspace() and not var_name:
|
||||
self._pos += 1
|
||||
continue
|
||||
if ch in ["}", ","] or ch.isspace():
|
||||
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)
|
||||
|
||||
def parse_function(self) -> Function:
|
||||
parenthesis_counter = 0
|
||||
func_name = ""
|
||||
func_args = ""
|
||||
def parse_numeric(self) -> NumericType:
|
||||
numeric_string = ""
|
||||
while ch := self.read(increment_pos=False):
|
||||
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():
|
||||
if ch not in ['(', ')']:
|
||||
if parenthesis_counter > 0:
|
||||
func_args += ch
|
||||
if ch == open_quotation_char:
|
||||
return String(value=string_value)
|
||||
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:
|
||||
func_name += ch
|
||||
elif ch == '(':
|
||||
parenthesis_counter += 1
|
||||
elif ch == ')':
|
||||
parenthesis_counter -= 1
|
||||
if parenthesis_counter == 0:
|
||||
break
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def parse(self):
|
||||
while True:
|
||||
ch = self.read()
|
||||
if ch == '{':
|
||||
self.parse_variable()
|
||||
if ch == '%':
|
||||
self.parse_function()
|
||||
self._stack.put(self.parse_variable())
|
||||
else:
|
||||
literal_str += ch
|
||||
|
||||
if bracket_counter != 0:
|
||||
raise StringFormattingException("Bracket count mismatch")
|
||||
|
||||
if literal_str:
|
||||
self._stack.put(LiteralString(literal_str))
|
||||
|
||||
return self
|
||||
|
||||
|
||||
# pylint: enable=invalid-name
|
||||
|
|
|
|||
0
tests/unit/script/__init__.py
Normal file
0
tests/unit/script/__init__.py
Normal file
60
tests/unit/script/test_parser.py
Normal file
60
tests/unit/script/test_parser.py
Normal 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()
|
||||
Loading…
Reference in a new issue