no more usage of parser
This commit is contained in:
parent
03c90b36f2
commit
4a5eb8cd42
4 changed files with 178 additions and 114 deletions
|
|
@ -1,62 +1,40 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
from queue import LifoQueue
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
|
from ytdl_sub.script.types import ArgumentType
|
||||||
|
from ytdl_sub.script.types import Boolean
|
||||||
|
from ytdl_sub.script.types import Float
|
||||||
|
from ytdl_sub.script.types import Function
|
||||||
|
from ytdl_sub.script.types import Integer
|
||||||
|
from ytdl_sub.script.types import LiteralString
|
||||||
|
from ytdl_sub.script.types import NumericType
|
||||||
|
from ytdl_sub.script.types import String
|
||||||
|
from ytdl_sub.script.types import SyntaxTree
|
||||||
|
from ytdl_sub.script.types import Variable
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
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
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
class _Parser:
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
NumericType = Union[Integer, Float]
|
|
||||||
ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function"]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Function:
|
|
||||||
name: str
|
|
||||||
args: List[ArgumentType]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class LiteralString:
|
|
||||||
value: str
|
|
||||||
|
|
||||||
|
|
||||||
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[LiteralString | Variable | Function] = LifoQueue()
|
self._ast: List[LiteralString | Variable | Function] = []
|
||||||
|
|
||||||
def read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]:
|
self._syntax_tree = self._parse()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ast(self) -> SyntaxTree:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Abstract syntax tree of the parsed text
|
||||||
|
"""
|
||||||
|
return self._syntax_tree
|
||||||
|
|
||||||
|
def _read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
ch = self._text[self._pos : (self._pos + length)]
|
ch = self._text[self._pos : (self._pos + length)]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
|
@ -66,9 +44,9 @@ class Parser:
|
||||||
self._pos += length
|
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(increment_pos=False):
|
while ch := self._read(increment_pos=False):
|
||||||
if ch.isspace() and not var_name:
|
if ch.isspace() and not var_name:
|
||||||
self._pos += 1
|
self._pos += 1
|
||||||
continue
|
continue
|
||||||
|
|
@ -85,12 +63,15 @@ class Parser:
|
||||||
var_name += ch
|
var_name += ch
|
||||||
self._pos += 1
|
self._pos += 1
|
||||||
|
|
||||||
|
if not var_name:
|
||||||
|
raise StringFormattingException("invalid var name")
|
||||||
|
|
||||||
assert is_valid_source_variable_name(var_name, raise_exception=False)
|
assert is_valid_source_variable_name(var_name, raise_exception=False)
|
||||||
return Variable(var_name)
|
return Variable(var_name)
|
||||||
|
|
||||||
def parse_numeric(self) -> NumericType:
|
def _parse_numeric(self) -> NumericType:
|
||||||
numeric_string = ""
|
numeric_string = ""
|
||||||
while ch := self.read(increment_pos=False):
|
while ch := self._read(increment_pos=False):
|
||||||
if not (ch.isnumeric() or ch == "."):
|
if not (ch.isnumeric() or ch == "."):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -107,43 +88,43 @@ class Parser:
|
||||||
|
|
||||||
return Float(value=numeric_float)
|
return Float(value=numeric_float)
|
||||||
|
|
||||||
def parse_string(self) -> String:
|
def _parse_string(self) -> String:
|
||||||
"""
|
"""
|
||||||
Begin parsing a string, including the quotation value
|
Begin parsing a string, including the quotation value
|
||||||
"""
|
"""
|
||||||
string_value = ""
|
string_value = ""
|
||||||
open_quotation_char = self.read()
|
open_quotation_char = self._read()
|
||||||
assert open_quotation_char in ["'", '"']
|
assert open_quotation_char in ["'", '"']
|
||||||
|
|
||||||
while ch := self.read():
|
while ch := self._read():
|
||||||
if ch == open_quotation_char:
|
if ch == open_quotation_char:
|
||||||
return String(value=string_value)
|
return String(value=string_value)
|
||||||
string_value += ch
|
string_value += ch
|
||||||
|
|
||||||
raise StringFormattingException("String not closed")
|
raise StringFormattingException("String not closed")
|
||||||
|
|
||||||
def parse_function_arg(self) -> ArgumentType:
|
def _parse_function_arg(self) -> ArgumentType:
|
||||||
if self.read(increment_pos=False) == "%":
|
if self._read(increment_pos=False) == "%":
|
||||||
self._pos += 1
|
self._pos += 1
|
||||||
return self.parse_function()
|
return self._parse_function()
|
||||||
if self.read(increment_pos=False).isnumeric():
|
if self._read(increment_pos=False).isnumeric():
|
||||||
return self.parse_numeric()
|
return self._parse_numeric()
|
||||||
if (self.read(increment_pos=False, length=4) or "").lower() == "true":
|
if (self._read(increment_pos=False, length=4) or "").lower() == "true":
|
||||||
self._pos += 4
|
self._pos += 4
|
||||||
return Boolean(value=True)
|
return Boolean(value=True)
|
||||||
if (self.read(increment_pos=False, length=5) or "").lower() == "false":
|
if (self._read(increment_pos=False, length=5) or "").lower() == "false":
|
||||||
self._pos += 5
|
self._pos += 5
|
||||||
return Boolean(value=False)
|
return Boolean(value=False)
|
||||||
if self.read(increment_pos=False) in ["'", '"']:
|
if self._read(increment_pos=False) in ["'", '"']:
|
||||||
return self.parse_string()
|
return self._parse_string()
|
||||||
if self.read(increment_pos=False).isascii() and self.read(increment_pos=False).islower():
|
if self._read(increment_pos=False).isascii() and self._read(increment_pos=False).islower():
|
||||||
return self.parse_variable()
|
return self._parse_variable()
|
||||||
raise StringFormattingException(
|
raise StringFormattingException(
|
||||||
"Invalid function argument, should be either a function, int, float, "
|
"Invalid function argument, should be either a function, int, float, "
|
||||||
"string, boolean, or variable without brackets"
|
"string, boolean, or variable without brackets"
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_function_args(self) -> List[ArgumentType]:
|
def _parse_function_args(self) -> List[ArgumentType]:
|
||||||
"""
|
"""
|
||||||
Begin parsing function args after the first ``(``, i.e. ``function_name(``
|
Begin parsing function args after the first ``(``, i.e. ``function_name(``
|
||||||
"""
|
"""
|
||||||
|
|
@ -151,7 +132,7 @@ class Parser:
|
||||||
comma_count = 0
|
comma_count = 0
|
||||||
|
|
||||||
arguments: List[ArgumentType] = []
|
arguments: List[ArgumentType] = []
|
||||||
while ch := self.read(increment_pos=False):
|
while ch := self._read(increment_pos=False):
|
||||||
if ch == ")":
|
if ch == ")":
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -165,43 +146,43 @@ class Parser:
|
||||||
self._pos += 1
|
self._pos += 1
|
||||||
else:
|
else:
|
||||||
argument_index += 1
|
argument_index += 1
|
||||||
arguments.append(self.parse_function_arg())
|
arguments.append(self._parse_function_arg())
|
||||||
|
|
||||||
return arguments
|
return arguments
|
||||||
|
|
||||||
def parse_function(self) -> Function:
|
def _parse_function(self) -> Function:
|
||||||
"""
|
"""
|
||||||
Begin parsing a function after reading the first ``%``
|
Begin parsing a function after reading the first ``%``
|
||||||
"""
|
"""
|
||||||
function_name: str = ""
|
function_name: str = ""
|
||||||
function_args: List[String | Variable | "Function"] = []
|
function_args: List[String | Variable | "Function"] = []
|
||||||
|
|
||||||
while ch := self.read():
|
while ch := self._read():
|
||||||
if ch == ")":
|
if ch == ")":
|
||||||
return Function(name=function_name, args=function_args)
|
return Function(name=function_name, args=function_args)
|
||||||
|
|
||||||
if ch != "(":
|
if ch != "(":
|
||||||
function_name += ch
|
function_name += ch
|
||||||
else:
|
else:
|
||||||
function_args = self.parse_function_args()
|
function_args = self._parse_function_args()
|
||||||
|
|
||||||
raise StringFormattingException("Invalid function")
|
raise StringFormattingException("Invalid function")
|
||||||
|
|
||||||
def parse(self) -> "Parser":
|
def _parse(self) -> SyntaxTree:
|
||||||
bracket_counter = 0
|
bracket_counter = 0
|
||||||
literal_str = ""
|
literal_str = ""
|
||||||
while ch := self.read():
|
while ch := self._read():
|
||||||
if ch == "}":
|
if ch == "}":
|
||||||
bracket_counter -= 1
|
bracket_counter -= 1
|
||||||
break
|
break
|
||||||
if ch == "{":
|
if ch == "{":
|
||||||
bracket_counter += 1
|
bracket_counter += 1
|
||||||
if literal_str:
|
if literal_str:
|
||||||
self._stack.put(LiteralString(literal_str))
|
self._ast.append(LiteralString(literal_str))
|
||||||
literal_str = ""
|
literal_str = ""
|
||||||
|
|
||||||
# Allow whitespace after bracket opening
|
# Allow whitespace after bracket opening
|
||||||
while ch1 := self.read(increment_pos=False):
|
while ch1 := self._read(increment_pos=False):
|
||||||
if not ch1.isspace():
|
if not ch1.isspace():
|
||||||
break
|
break
|
||||||
self._pos += 1
|
self._pos += 1
|
||||||
|
|
@ -213,9 +194,9 @@ class Parser:
|
||||||
|
|
||||||
if ch1 == "%":
|
if ch1 == "%":
|
||||||
self._pos += 1
|
self._pos += 1
|
||||||
self._stack.put(self.parse_function())
|
self._ast.append(self._parse_function())
|
||||||
else:
|
else:
|
||||||
self._stack.put(self.parse_variable())
|
self._ast.append(self._parse_variable())
|
||||||
else:
|
else:
|
||||||
literal_str += ch
|
literal_str += ch
|
||||||
|
|
||||||
|
|
@ -223,9 +204,13 @@ class Parser:
|
||||||
raise StringFormattingException("Bracket count mismatch")
|
raise StringFormattingException("Bracket count mismatch")
|
||||||
|
|
||||||
if literal_str:
|
if literal_str:
|
||||||
self._stack.put(LiteralString(literal_str))
|
self._ast.append(LiteralString(literal_str))
|
||||||
|
|
||||||
return self
|
return SyntaxTree(ast=self._ast)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(text: str) -> SyntaxTree:
|
||||||
|
return _Parser(text).ast
|
||||||
|
|
||||||
|
|
||||||
# pylint: enable=invalid-name
|
# pylint: enable=invalid-name
|
||||||
|
|
|
||||||
76
src/ytdl_sub/script/types.py
Normal file
76
src/ytdl_sub/script/types.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
from typing import Set
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
|
||||||
|
NumericType = Union[Integer, Float]
|
||||||
|
ArgumentType = Union[Integer, Float, String, Boolean, Variable, "Function"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Function:
|
||||||
|
name: str
|
||||||
|
args: List[ArgumentType]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def variables(self) -> Set[Variable]:
|
||||||
|
"""
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
All variables used within the function
|
||||||
|
"""
|
||||||
|
variables: Set[Variable] = set()
|
||||||
|
for arg in self.args:
|
||||||
|
if isinstance(arg, Variable):
|
||||||
|
variables.add(arg)
|
||||||
|
elif isinstance(arg, Function):
|
||||||
|
variables.union(arg.variables)
|
||||||
|
|
||||||
|
return variables
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class LiteralString:
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SyntaxTree:
|
||||||
|
ast: List[LiteralString | Variable | Function]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def variables(self) -> Set[Variable]:
|
||||||
|
variables: Set[Variable] = set()
|
||||||
|
for token in self.ast:
|
||||||
|
if isinstance(token, Variable):
|
||||||
|
variables.add(token)
|
||||||
|
elif isinstance(token, Function):
|
||||||
|
variables.union(token.variables)
|
||||||
|
|
||||||
|
return variables
|
||||||
|
|
@ -75,7 +75,7 @@ def download_and_convert_url_thumbnail(
|
||||||
# timeout after 8 seconds
|
# timeout after 8 seconds
|
||||||
with urlopen(thumbnail_url, timeout=1.0) as file:
|
with urlopen(thumbnail_url, timeout=1.0) as file:
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as thumbnail:
|
with tempfile.NamedTemporaryFile(delete=False) as thumbnail:
|
||||||
thumbnail.write(file.read())
|
thumbnail.write(file._read())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(output_thumbnail_path), exist_ok=True)
|
os.makedirs(os.path.dirname(output_thumbnail_path), exist_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,63 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ytdl_sub.script.parser import Boolean
|
from ytdl_sub.script.parser import parse
|
||||||
from ytdl_sub.script.parser import Float
|
from ytdl_sub.script.types import Boolean
|
||||||
from ytdl_sub.script.parser import Function
|
from ytdl_sub.script.types import Float
|
||||||
from ytdl_sub.script.parser import Integer
|
from ytdl_sub.script.types import Function
|
||||||
from ytdl_sub.script.parser import LiteralString
|
from ytdl_sub.script.types import Integer
|
||||||
from ytdl_sub.script.parser import Parser
|
from ytdl_sub.script.types import LiteralString
|
||||||
from ytdl_sub.script.parser import String
|
from ytdl_sub.script.types import String
|
||||||
from ytdl_sub.script.parser import Variable
|
from ytdl_sub.script.types import SyntaxTree
|
||||||
|
from ytdl_sub.script.types import Variable
|
||||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||||
|
|
||||||
|
|
||||||
class TestParser:
|
class TestParser:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
parser = Parser("hello world").parse()
|
assert parse("hello world") == SyntaxTree([LiteralString(value="hello world")])
|
||||||
assert list(parser._stack.queue) == [LiteralString(value="hello world")]
|
|
||||||
|
|
||||||
def test_single_function_one_arg(self):
|
def test_single_function_one_arg(self):
|
||||||
parser = Parser("hello {%capitalize('hi mom')}").parse()
|
assert parse("hello {%capitalize('hi mom')}") == SyntaxTree(
|
||||||
assert list(parser._stack.queue) == [
|
[
|
||||||
LiteralString("hello "),
|
LiteralString("hello "),
|
||||||
Function(name="capitalize", args=[String(value="hi mom")]),
|
Function(name="capitalize", args=[String(value="hi mom")]),
|
||||||
]
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize('whitespace', ["", " ", " ", "\n", " \n "])
|
@pytest.mark.parametrize("whitespace", ["", " ", " ", "\n", " \n "])
|
||||||
def test_single_function_multiple_args(self, whitespace: str):
|
def test_single_function_multiple_args(self, whitespace: str):
|
||||||
s = whitespace
|
s = whitespace
|
||||||
parser = Parser(
|
assert parse(
|
||||||
f"hello{s}{{{s}%concat({s}'string'{s},{s}1{s},{s}2.4{s},"
|
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}}}"
|
f"{s}TRUE{s},{s}variable_name{s},{s}%capitalize({s}'hi'{s}){s}){s}}}"
|
||||||
).parse()
|
) == SyntaxTree(
|
||||||
assert list(parser._stack.queue) == [
|
[
|
||||||
LiteralString(value=f"hello{s}"),
|
LiteralString(value=f"hello{s}"),
|
||||||
Function(
|
Function(
|
||||||
name="concat",
|
name="concat",
|
||||||
args=[
|
args=[
|
||||||
String(value="string"),
|
String(value="string"),
|
||||||
Integer(value=1),
|
Integer(value=1),
|
||||||
Float(value=2.4),
|
Float(value=2.4),
|
||||||
Boolean(value=True),
|
Boolean(value=True),
|
||||||
Variable(name="variable_name"),
|
Variable(name="variable_name"),
|
||||||
Function(name="capitalize", args=[String(value="hi")]),
|
Function(name="capitalize", args=[String(value="hi")]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
] + ([LiteralString(value=s)] if s else [])
|
]
|
||||||
|
+ ([LiteralString(value=s)] if s else [])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestParserBracketFailures:
|
class TestParserBracketFailures:
|
||||||
def test_bracket_open(self):
|
def test_bracket_open(self):
|
||||||
with pytest.raises(StringFormattingException):
|
with pytest.raises(StringFormattingException):
|
||||||
_ = Parser("{").parse()
|
parse("{")
|
||||||
|
|
||||||
def test_bracket_close(self):
|
def test_bracket_close(self):
|
||||||
with pytest.raises(StringFormattingException):
|
with pytest.raises(StringFormattingException):
|
||||||
_ = Parser("}").parse()
|
parse("}")
|
||||||
|
|
||||||
def test_bracket_in_function(self):
|
def test_bracket_in_function(self):
|
||||||
with pytest.raises(StringFormattingException):
|
with pytest.raises(StringFormattingException):
|
||||||
_ = Parser("hello {%capitalize({as_arg)}").parse()
|
parse("hello {%capitalize({as_arg)}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue