Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 790b836bd8 | |||
| c6bcb05ac6 | |||
| f48f76d9f8 | |||
| c35c88244f | |||
| 7a55ee0777 | |||
| fb81eb84a7 | |||
| 2278a16ca0 | |||
| ab9a7bb34e | |||
| fbd62512d1 | |||
| 4098c0e25a | |||
| 58b45e6cac |
45
.woodpecker.yml
Normal file
45
.woodpecker.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
tags: true
|
||||
|
||||
steps:
|
||||
test:
|
||||
image: python
|
||||
commands:
|
||||
- python -m unittest
|
||||
when:
|
||||
event: [tag, pull_request]
|
||||
|
||||
build:
|
||||
image: python
|
||||
commands:
|
||||
- python -m pip install build
|
||||
- python -m build
|
||||
when:
|
||||
event: tag
|
||||
|
||||
publish:
|
||||
image: gitea.elara.ws/music-kraken/plugin-twine
|
||||
settings:
|
||||
username: __token__
|
||||
password:
|
||||
from_secret: pypi_token
|
||||
when:
|
||||
event: tag
|
||||
|
||||
release:
|
||||
image: woodpeckerci/plugin-release
|
||||
settings:
|
||||
title: "Version ${CI_COMMIT_TAG}"
|
||||
files:
|
||||
- 'dist/hisscl-*.whl'
|
||||
- 'dist/hisscl-*.tar.gz'
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
when:
|
||||
event: tag
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
A [HashiCorp Config Language](https://github.com/hashicorp/hcl) parser for Python
|
||||
|
||||
[](https://ci.elara.ws/repos/63)
|
||||
|
||||
## Usage
|
||||
|
||||
For most simple use-cases, you can use the `load*` convenience functions:
|
||||
@@ -48,5 +50,6 @@ Currently, this parser supports all HCL features except:
|
||||
|
||||
- [For Expressions](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#for-expressions)
|
||||
- [Templates](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#templates)
|
||||
- [Splat Operators](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#splat-operators)
|
||||
|
||||
Support for these features is planned.
|
||||
@@ -40,7 +40,7 @@ class VariableRef:
|
||||
@dataclasses.dataclass
|
||||
class FunctionCall:
|
||||
pos: Position
|
||||
name: str
|
||||
value: 'Value'
|
||||
args: list['Value']
|
||||
|
||||
Ref = VariableRef | FunctionCall
|
||||
@@ -80,7 +80,19 @@ class Expansion:
|
||||
pos: Position
|
||||
value: 'Value'
|
||||
|
||||
Expression = BinaryExpression | UnaryExpression | Expansion
|
||||
@dataclasses.dataclass
|
||||
class Index:
|
||||
pos: Position
|
||||
value: 'Value'
|
||||
index: 'Value'
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GetAttr:
|
||||
pos: Position
|
||||
value: 'Value'
|
||||
attr: str
|
||||
|
||||
Expression = BinaryExpression | UnaryExpression | Expansion | Index | GetAttr
|
||||
Value = Literal | Collection | Expression | Ref
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
||||
@@ -4,9 +4,9 @@ from . import parser
|
||||
import typing
|
||||
import io
|
||||
|
||||
__all__ = ['TypeError', 'Block', 'Interp']
|
||||
__all__ = ['OperandError', 'Block', 'Interp']
|
||||
|
||||
class TypeError(Exception):
|
||||
class OperandError(Exception):
|
||||
def __init__(self, pos: ast.Position, action: str, issue: str, val: typing.Any):
|
||||
super().__init__(f'{pos}: cannot perform {action} on {issue} operand ({type(val).__name__})')
|
||||
|
||||
@@ -15,10 +15,9 @@ class Block(dict):
|
||||
self.labels = labels
|
||||
super().__init__()
|
||||
|
||||
class Interp:
|
||||
vars: dict[str, typing.Any] = {}
|
||||
|
||||
class Interp:
|
||||
def __init__(self, stream: typing.TextIO, name: str):
|
||||
self.vars: dict[str, typing.Any] = {}
|
||||
self.parser = parser.Parser(stream, name)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
@@ -33,6 +32,20 @@ class Interp:
|
||||
def update(self, vars: dict[str, typing.Any]):
|
||||
self.vars.update(vars)
|
||||
|
||||
def _eval_index(self, index: ast.Index) -> typing.Any:
|
||||
val = self._convert_value(index.value)
|
||||
if not hasattr(val, '__getitem__'):
|
||||
raise ValueError(f'{index.value.pos}: value is not indexable ({type(val).__name__})')
|
||||
index_val = self._convert_value(index.index)
|
||||
if type(index_val) is int and hasattr(val, '__len__') and index_val >= len(val):
|
||||
raise IndexError(f'{index.index.pos}: index out of range ({index_val} with length {len(val)})')
|
||||
elif type(index_val) is not int:
|
||||
if isinstance(val, list) or isinstance(val, tuple) or isinstance(val, str):
|
||||
raise TypeError(f'{index.index.pos}: {type(val).__name__} indices must be integers, not {type(index_val).__name__}')
|
||||
elif hasattr(val, '__contains__') and index_val not in val:
|
||||
raise IndexError(f'{index.index.pos}: index {repr(index_val)} does not exist in value')
|
||||
return val[index_val]
|
||||
|
||||
def _convert_value(self, val: ast.Value) -> typing.Any:
|
||||
if isinstance(val, ast.VariableRef):
|
||||
if val.name not in self.vars:
|
||||
@@ -50,6 +63,13 @@ class Interp:
|
||||
return self._eval_binary_expr(val)
|
||||
elif isinstance(val, ast.UnaryExpression):
|
||||
return self._eval_unary_expr(val)
|
||||
elif isinstance(val, ast.Index):
|
||||
return self._eval_index(val)
|
||||
elif isinstance(val, ast.GetAttr):
|
||||
obj = self._convert_value(val.value)
|
||||
if not hasattr(obj, val.attr):
|
||||
raise AttributeError(f'{val.pos}: no such attribute {repr(val.attr)} in object of type {type(obj).__name__}')
|
||||
return getattr(obj, val.attr)
|
||||
elif isinstance(val, ast.Expansion):
|
||||
raise ValueError(f'{val.pos}: cannot use expansion operator outside of a function call')
|
||||
|
||||
@@ -60,31 +80,30 @@ class Interp:
|
||||
return self._is_numerical(val) or isinstance(val, str)
|
||||
|
||||
def _exec_func_call(self, call: ast.FunctionCall) -> typing.Any:
|
||||
if call.name not in self.vars:
|
||||
raise KeyError(f'{call.pos}: no such function: {repr(call.name)}')
|
||||
elif not callable(self.vars[call.name]):
|
||||
val = self._convert_value(call.value)
|
||||
if not callable(val):
|
||||
raise ValueError(f'{call.pos}: cannot call non-callable object')
|
||||
args = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Expansion):
|
||||
val = self._convert_value(arg.value)
|
||||
if not isinstance(val, typing.Iterable):
|
||||
raise ValueError(f"{arg.pos}: cannot perform expansion on non-iterable value ({type(val).__name__})")
|
||||
args.extend(val)
|
||||
arg_val = self._convert_value(arg.value)
|
||||
if not isinstance(arg_val, typing.Iterable):
|
||||
raise ValueError(f"{arg.pos}: cannot perform expansion on non-iterable value ({type(arg_val).__name__})")
|
||||
args.extend(arg_val)
|
||||
else:
|
||||
args.append(self._convert_value(arg))
|
||||
return self.vars[call.name](*args)
|
||||
return val(*args)
|
||||
|
||||
def _eval_unary_expr(self, expr: ast.UnaryExpression) -> float | int | bool:
|
||||
val = self._convert_value(expr.value)
|
||||
match expr.op.value:
|
||||
case '!':
|
||||
if type(val) is not bool:
|
||||
raise TypeError(expr.value.pos, 'NOT operation', 'non-boolean', val)
|
||||
raise OperandError(expr.value.pos, 'NOT operation', 'non-boolean', val)
|
||||
return not val
|
||||
case '-':
|
||||
if not self._is_numerical(val):
|
||||
raise TypeError(expr.value.pos, 'negation', 'non-numerical', val)
|
||||
raise OperandError(expr.value.pos, 'negation', 'non-numerical', val)
|
||||
return -val
|
||||
case _:
|
||||
raise ValueError(f'{expr.op.pos}: unknown unary operation: {repr(expr.op.value)}')
|
||||
@@ -100,69 +119,69 @@ class Interp:
|
||||
return left != right
|
||||
case '+':
|
||||
if not self._is_numerical(left):
|
||||
raise TypeError(expr.left.pos, 'addition operation', 'non-numerical', left)
|
||||
raise OperandError(expr.left.pos, 'addition operation', 'non-numerical', left)
|
||||
elif not self._is_numerical(right):
|
||||
raise TypeError(expr.right.pos, 'addition operation', 'non-numerical', right)
|
||||
raise OperandError(expr.right.pos, 'addition operation', 'non-numerical', right)
|
||||
return left + right
|
||||
case '-':
|
||||
if not self._is_numerical(left):
|
||||
raise TypeError(expr.left.pos, 'subtraction operation', 'non-numerical', left)
|
||||
raise OperandError(expr.left.pos, 'subtraction operation', 'non-numerical', left)
|
||||
elif not self._is_numerical(right):
|
||||
raise TypeError(expr.right.pos, 'subtraction operation', 'non-numerical', right)
|
||||
raise OperandError(expr.right.pos, 'subtraction operation', 'non-numerical', right)
|
||||
return left - right
|
||||
case '*':
|
||||
if not self._is_numerical(left):
|
||||
raise TypeError(expr.left.pos, 'multiplication operation', 'non-numerical', left)
|
||||
raise OperandError(expr.left.pos, 'multiplication operation', 'non-numerical', left)
|
||||
elif not self._is_numerical(right):
|
||||
raise TypeError(expr.right.pos, 'multiplication operation', 'non-numerical', right)
|
||||
raise OperandError(expr.right.pos, 'multiplication operation', 'non-numerical', right)
|
||||
return left * right
|
||||
case '/':
|
||||
if not self._is_numerical(left):
|
||||
raise TypeError(expr.left.pos, 'division operation', 'non-numerical', left)
|
||||
raise OperandError(expr.left.pos, 'division operation', 'non-numerical', left)
|
||||
elif not self._is_numerical(right):
|
||||
raise TypeError(expr.right.pos, 'division operation', 'non-numerical', right)
|
||||
raise OperandError(expr.right.pos, 'division operation', 'non-numerical', right)
|
||||
return left / right
|
||||
case '%':
|
||||
if not self._is_numerical(left):
|
||||
raise TypeError(expr.left.pos, 'modulo operation', 'non-numerical', left)
|
||||
raise OperandError(expr.left.pos, 'modulo operation', 'non-numerical', left)
|
||||
elif not self._is_numerical(right):
|
||||
raise TypeError(expr.right.pos, 'modulo operation', 'non-numerical', right)
|
||||
raise OperandError(expr.right.pos, 'modulo operation', 'non-numerical', right)
|
||||
return left % right
|
||||
case '>':
|
||||
if not self._is_comparable(left):
|
||||
raise TypeError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
raise OperandError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
elif not self._is_comparable(right):
|
||||
raise TypeError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
raise OperandError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
return left > right
|
||||
case '<':
|
||||
if not self._is_comparable(left):
|
||||
raise TypeError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
raise OperandError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
elif not self._is_comparable(right):
|
||||
raise TypeError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
raise OperandError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
return left < right
|
||||
case '<=':
|
||||
if not self._is_comparable(left):
|
||||
raise TypeError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
raise OperandError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
elif not self._is_comparable(right):
|
||||
raise TypeError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
raise OperandError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
return left <= right
|
||||
case '>=':
|
||||
if not self._is_comparable(left):
|
||||
raise TypeError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
raise OperandError(expr.left.pos, 'comparison', 'non-comparable', left)
|
||||
elif not self._is_comparable(right):
|
||||
raise TypeError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
raise OperandError(expr.right.pos, 'comparison', 'non-comparable', right)
|
||||
return left >= right
|
||||
case '||':
|
||||
if type(left) is not bool:
|
||||
raise TypeError(expr.left.pos, 'OR operation', 'non-boolean', left)
|
||||
raise OperandError(expr.left.pos, 'OR operation', 'non-boolean', left)
|
||||
elif type(right) is not bool:
|
||||
raise TypeError(expr.right.pos, 'OR operation', 'non-boolean', right)
|
||||
raise OperandError(expr.right.pos, 'OR operation', 'non-boolean', right)
|
||||
return left or right
|
||||
case '&&':
|
||||
if type(left) is not bool:
|
||||
raise TypeError(expr.left.pos, 'AND operation', 'non-boolean', left)
|
||||
raise OperandError(expr.left.pos, 'AND operation', 'non-boolean', left)
|
||||
elif type(right) is not bool:
|
||||
raise TypeError(expr.right.pos, 'AND operation', 'non-boolean', right)
|
||||
raise OperandError(expr.right.pos, 'AND operation', 'non-boolean', right)
|
||||
return left and right
|
||||
case _:
|
||||
raise ValueError(f'{expr.op.pos}: unknown binary operation: {repr(expr.op.value)}')
|
||||
|
||||
@@ -25,6 +25,7 @@ class Token(enum.Enum):
|
||||
COLON = 12
|
||||
OPERATOR = 13
|
||||
ELLIPSIS = 14
|
||||
DOT = 15
|
||||
|
||||
class ExpectedError(Exception):
|
||||
def __init__(self, pos: ast.Position, expected: str, got: str):
|
||||
@@ -34,13 +35,15 @@ class ExpectedError(Exception):
|
||||
self.expected = expected
|
||||
|
||||
class Lexer:
|
||||
pos = ast.Position()
|
||||
prev_pos = ast.Position()
|
||||
unread = ''
|
||||
|
||||
def __init__(self, stream: typing.TextIO, name: str):
|
||||
self.pos = pos = ast.Position()
|
||||
self.prev_pos = ast.Position()
|
||||
self.unread = ''
|
||||
self.stream = stream
|
||||
self.pos.name = name
|
||||
|
||||
def _pos(self) -> ast.Position:
|
||||
return dataclasses.replace(self.pos)
|
||||
|
||||
def _peek(self, n: int) -> str:
|
||||
if self.unread != '':
|
||||
@@ -175,7 +178,6 @@ class Lexer:
|
||||
|
||||
return Token.HEREDOC, pos, out.getvalue()
|
||||
|
||||
# TODO: scan multi-char operators like ==
|
||||
def _scan_operator(self, char) -> tuple[Token, ast.Position, str]:
|
||||
pos = dataclasses.replace(self.pos)
|
||||
with io.StringIO() as out:
|
||||
@@ -183,32 +185,31 @@ class Lexer:
|
||||
out.write(char)
|
||||
char = self._read()
|
||||
self._unread(char)
|
||||
val = out.getvalue()
|
||||
return Token.OPERATOR, pos, out.getvalue()
|
||||
|
||||
def scan(self) -> tuple[Token, ast.Position, str]:
|
||||
def scan(self) -> tuple[Token, ast.Position, str]:
|
||||
char = self._read()
|
||||
while is_whitespace(char):
|
||||
char = self._read()
|
||||
|
||||
match char:
|
||||
case '{' | '}':
|
||||
return Token.CURLY, self.pos, char
|
||||
return Token.CURLY, self._pos(), char
|
||||
case '[' | ']':
|
||||
return Token.SQUARE, self.pos, char
|
||||
return Token.SQUARE, self._pos(), char
|
||||
case '(' | ')':
|
||||
return Token.PAREN, self.pos, char
|
||||
return Token.PAREN, self._pos(), char
|
||||
case ',':
|
||||
return Token.COMMA, self.pos, char
|
||||
return Token.COMMA, self._pos(), char
|
||||
case ':':
|
||||
return Token.COLON, self.pos, char
|
||||
return Token.COLON, self._pos(), char
|
||||
case '"':
|
||||
return self._scan_str()
|
||||
case '<':
|
||||
# If the next character is not another less than symbol,
|
||||
# this is probably a less than operator.
|
||||
if self._peek(1) != '<':
|
||||
return Token.OPERATOR, self.pos, char
|
||||
return self._scan_operator(char)
|
||||
return self._scan_heredoc(char)
|
||||
case '/':
|
||||
next = self._peek(1)
|
||||
@@ -224,19 +225,20 @@ class Lexer:
|
||||
# If the next character is not another slash
|
||||
# or an asterisk, this is probably a division
|
||||
# operator.
|
||||
return Token.OPERATOR, self.pos, char
|
||||
return self._scan_operator(char)
|
||||
case '#':
|
||||
# Ignore comments and return next token
|
||||
self._scan_comment(char)
|
||||
return self.scan()
|
||||
case '.':
|
||||
if (next := self._read()) != '.':
|
||||
self._unread(next)
|
||||
return Token.DOT, self._pos(), next
|
||||
elif (next := self._read()) != '.':
|
||||
raise ExpectedError(self.pos, '.', next)
|
||||
if (next := self._read()) != '.':
|
||||
raise ExpectedError(self.pos, '.', next)
|
||||
return Token.ELLIPSIS, self.pos, "..."
|
||||
return Token.ELLIPSIS, self._pos(), "..."
|
||||
case '':
|
||||
return Token.EOF, self.pos, char
|
||||
return Token.EOF, self._pos(), char
|
||||
|
||||
if is_numeric(char):
|
||||
return self._scan_number(char)
|
||||
@@ -245,7 +247,7 @@ class Lexer:
|
||||
elif is_operator(char):
|
||||
return self._scan_operator(char)
|
||||
|
||||
return Token.ILLEGAL, self.pos, char
|
||||
return Token.ILLEGAL, self._pos(), char
|
||||
|
||||
def is_whitespace(char: str) -> bool:
|
||||
return char in (' ', '\t', '\r', '\n')
|
||||
|
||||
@@ -15,10 +15,9 @@ class ExpectedError(Exception):
|
||||
self.expected = expected
|
||||
|
||||
class Parser:
|
||||
_prev: tuple[lexer.Token, ast.Position, str] | None = None
|
||||
|
||||
def __init__(self, stream: TextIO, name: str):
|
||||
self.lexer = lexer.Lexer(stream, name)
|
||||
self._prev: tuple[lexer.Token, ast.Position, str] | None = None
|
||||
|
||||
def _scan(self) -> tuple[lexer.Token, ast.Position, str]:
|
||||
if self._prev is not None:
|
||||
@@ -29,7 +28,29 @@ class Parser:
|
||||
|
||||
def _unscan(self, tok: lexer.Token, pos: ast.Position, lit: str):
|
||||
self._prev = tok, pos, lit
|
||||
|
||||
def _parse_index(self, val: ast.Value, start_pos: ast.Position) -> ast.Index:
|
||||
index = ast.Index(pos=start_pos, value=val, index=self._parse_expr())
|
||||
tok, start_pos, lit = self._scan()
|
||||
if tok != lexer.Token.SQUARE or lit != ']':
|
||||
raise ExpectedError(start_pos, 'closing square bracket', lit)
|
||||
while self.lexer._peek(1) == '[':
|
||||
_, start_pos, _ = self._scan()
|
||||
index = self._parse_index(index, start_pos)
|
||||
return index
|
||||
|
||||
def _parse_getattr(self, val: ast.Value, start_pos: ast.Position) -> ast.Index | ast.GetAttr:
|
||||
tok, pos, lit = self._scan()
|
||||
if tok == lexer.Token.INTEGER:
|
||||
return ast.Index(pos=start_pos, value=val, index=ast.Integer(pos=pos, value=int(lit)))
|
||||
elif tok == lexer.Token.IDENT:
|
||||
return ast.GetAttr(pos=start_pos, value=val, attr=lit)
|
||||
else:
|
||||
raise ExpectedError(pos, 'integer or identifier', lit)
|
||||
while self.lexer._peek(1) == '.':
|
||||
_, start_pos, _ = self._scan()
|
||||
index = self._parse_getattr(index, start_pos)
|
||||
|
||||
def _parse_expr(self) -> ast.Value:
|
||||
left = self._parse_value()
|
||||
tok, pos, lit = self._scan()
|
||||
@@ -77,15 +98,14 @@ class Parser:
|
||||
|
||||
return ast.Object(start_pos, items)
|
||||
|
||||
def _parse_func_call(self) -> ast.FunctionCall:
|
||||
id_tok, id_pos, id_lit = self._scan()
|
||||
tok, pos, lit = self._scan()
|
||||
if tok != lexer.Token.PAREN or lit != '(':
|
||||
raise ExpectedError(pos, 'opening parentheses', lit)
|
||||
|
||||
tok, pos, lit = self._scan()
|
||||
def _parse_func_call(self, val: ast.Value, start_pos: ast.Position) -> ast.FunctionCall:
|
||||
tok, pos, lit = self._scan()
|
||||
if tok == lexer.Token.PAREN and lit == ')':
|
||||
return ast.FunctionCall(pos=id_pos, name=id_lit, args=[])
|
||||
out = ast.FunctionCall(pos=start_pos, value=val, args=[])
|
||||
while self.lexer._peek(1) == '(':
|
||||
_, start_pos, _ = self._scan()
|
||||
out = self._parse_func_call(out, start_pos)
|
||||
return out
|
||||
self._unscan(tok, pos, lit)
|
||||
|
||||
args: list[ast.Value] = []
|
||||
@@ -104,36 +124,39 @@ class Parser:
|
||||
break
|
||||
else:
|
||||
raise ExpectedError(pos, 'comma or closing parentheses', lit)
|
||||
return ast.FunctionCall(pos=id_pos, name=id_lit, args=args)
|
||||
|
||||
out = ast.FunctionCall(pos=start_pos, value=val, args=args)
|
||||
while self.lexer._peek(1) == '(':
|
||||
_, start_pos, _ = self._scan()
|
||||
out = self._parse_func_call(out, start_pos)
|
||||
return out
|
||||
|
||||
def _parse_value(self) -> ast.Value:
|
||||
out = None
|
||||
tok, pos, lit = self._scan()
|
||||
match tok:
|
||||
case lexer.Token.INTEGER:
|
||||
return ast.Integer(pos=pos, value=int(lit))
|
||||
out = ast.Integer(pos=pos, value=int(lit))
|
||||
case lexer.Token.FLOAT:
|
||||
return ast.Float(pos=pos, value=float(lit))
|
||||
out = ast.Float(pos=pos, value=float(lit))
|
||||
case lexer.Token.BOOL:
|
||||
return ast.Bool(pos=pos, value=(lit == 'true'))
|
||||
out = ast.Bool(pos=pos, value=(lit == 'true'))
|
||||
case lexer.Token.STRING:
|
||||
return ast.String(pos=pos, value=pyast.literal_eval(lit))
|
||||
out = ast.String(pos=pos, value=pyast.literal_eval(lit))
|
||||
case lexer.Token.IDENT:
|
||||
if self.lexer._peek(1) == '(':
|
||||
self._unscan(tok, pos, lit)
|
||||
return self._parse_func_call()
|
||||
return ast.VariableRef(pos=pos, name=lit)
|
||||
out = ast.VariableRef(pos=pos, name=lit)
|
||||
case lexer.Token.HEREDOC:
|
||||
return ast.String(pos=pos, value=lit)
|
||||
out = ast.String(pos=pos, value=lit)
|
||||
case lexer.Token.OPERATOR:
|
||||
return ast.UnaryExpression(pos=pos, op=ast.Operator(pos=pos, value=lit), value=self._parse_value())
|
||||
out = ast.UnaryExpression(pos=pos, op=ast.Operator(pos=pos, value=lit), value=self._parse_value())
|
||||
case lexer.Token.SQUARE:
|
||||
if lit != '[':
|
||||
raise ExpectedError(pos, repr('['), lit)
|
||||
return self._parse_tuple(pos)
|
||||
out = self._parse_tuple(pos)
|
||||
case lexer.Token.CURLY:
|
||||
if lit != '{':
|
||||
raise ExpectedError(pos, repr('{'), lit)
|
||||
return self._parse_object(pos)
|
||||
out = self._parse_object(pos)
|
||||
case lexer.Token.PAREN:
|
||||
if lit != '(':
|
||||
raise ExpectedError(pos, repr('('), lit)
|
||||
@@ -141,9 +164,21 @@ class Parser:
|
||||
tok, pos, lit = self._scan()
|
||||
if tok != lexer.Token.PAREN or lit != ')':
|
||||
raise ExpectedError(pos, repr(')'), lit)
|
||||
return expr
|
||||
|
||||
raise ExpectedError(pos, 'value', lit)
|
||||
out = expr
|
||||
case _:
|
||||
raise ExpectedError(pos, 'value', lit)
|
||||
|
||||
tok, pos, lit = self._scan()
|
||||
if tok == lexer.Token.SQUARE and lit == '[':
|
||||
out = self._parse_index(out, pos)
|
||||
elif tok == lexer.Token.PAREN and lit == '(':
|
||||
out = self._parse_func_call(out, pos)
|
||||
elif tok == lexer.Token.DOT:
|
||||
out = self._parse_getattr(out, pos)
|
||||
else:
|
||||
self._unscan(tok, pos, lit)
|
||||
|
||||
return out
|
||||
|
||||
def parse(self, until: tuple[lexer.Token, str] = (lexer.Token.EOF, '')) -> ast.AST:
|
||||
tree = []
|
||||
|
||||
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
158
test/test_interp.py
Normal file
158
test/test_interp.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from hisscl import interp
|
||||
import unittest
|
||||
import io
|
||||
|
||||
class TestBasic(unittest.TestCase):
|
||||
def test_assignment(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 26.04"), "TestBasic.test_assignment").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 26.04)
|
||||
|
||||
def test_block(self):
|
||||
cfg = interp.Interp(io.StringIO("x { y = 26.04 }"), "TestBasic.test_block").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], [{'y': 26.04}])
|
||||
|
||||
def test_block_labels(self):
|
||||
cfg = interp.Interp(io.StringIO("x y { z = 26.04 }"), "TestBasic.test_block_labels").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], [{'z': 26.04}])
|
||||
self.assertEqual(cfg['x'][0].labels, ['y'])
|
||||
|
||||
class TestExpressions(unittest.TestCase):
|
||||
def test_add(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 123 + 333"), "TestExpressions.test_add").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 456)
|
||||
|
||||
def test_sub(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 456 - 333"), "TestExpressions.test_sub").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 123)
|
||||
|
||||
def test_mul(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 128 * 2"), "TestExpressions.test_mul").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 256)
|
||||
|
||||
def test_div(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 256 / 2"), "TestExpressions.test_div").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 128)
|
||||
|
||||
def test_mod(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 256 % 3"), "TestExpressions.test_mod").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 1)
|
||||
|
||||
def test_eq(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 123 == 456"), "TestExpressions.test_eq").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], False)
|
||||
|
||||
def test_ne(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 123 != 456"), "TestExpressions.test_ne").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], True)
|
||||
|
||||
def test_lt(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 123 < 456"), "TestExpressions.test_lt").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], True)
|
||||
|
||||
def test_gt(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 123 > 456"), "TestExpressions.test_gt").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], False)
|
||||
|
||||
def test_le(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 123 <= 123"), "TestExpressions.test_le").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], True)
|
||||
|
||||
def test_ge(self):
|
||||
cfg = interp.Interp(io.StringIO("x = 1234 >= 123"), "TestExpressions.test_ge").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], True)
|
||||
|
||||
def test_or(self):
|
||||
cfg = interp.Interp(io.StringIO("x = true || false"), "TestExpressions.test_or").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], True)
|
||||
|
||||
def test_and(self):
|
||||
cfg = interp.Interp(io.StringIO("x = true && false"), "TestExpressions.test_and").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], False)
|
||||
|
||||
def test_not(self):
|
||||
cfg = interp.Interp(io.StringIO("x = !true"), "TestExpressions.test_not").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], False)
|
||||
|
||||
def test_neg(self):
|
||||
cfg = interp.Interp(io.StringIO("x = -1"), "TestExpressions.test_neg").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], -1)
|
||||
|
||||
class TestRefs(unittest.TestCase):
|
||||
def test_var(self):
|
||||
i = interp.Interp(io.StringIO("x = 123 + y"), "TestRefs.test_var")
|
||||
i['y'] = 333
|
||||
cfg = i.run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 456)
|
||||
|
||||
def test_index(self):
|
||||
i = interp.Interp(io.StringIO('x = y[1]'), "TestRefs.test_index")
|
||||
i['y'] = [123, 456, 789]
|
||||
cfg = i.run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 456)
|
||||
|
||||
def test_multi_index(self):
|
||||
cfg = interp.Interp(io.StringIO('x = ["123", "456", "789"][1][2]'), "TestRefs.test_multi_index").run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], '6')
|
||||
|
||||
def test_index_legacy(self):
|
||||
i = interp.Interp(io.StringIO('x = y.1'), "TestRefs.test_index_legacy")
|
||||
i['y'] = [123, 456, 789]
|
||||
cfg = i.run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 456)
|
||||
|
||||
def test_getattr(self):
|
||||
class Y:
|
||||
z = 123
|
||||
i = interp.Interp(io.StringIO('x = Y.z'), "TestRefs.test_getattr")
|
||||
i['Y'] = Y()
|
||||
cfg = i.run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 123)
|
||||
|
||||
def test_func(self):
|
||||
def y(a, b):
|
||||
return a + b
|
||||
i = interp.Interp(io.StringIO("x = y(123, 333)"), "TestRefs.test_func")
|
||||
i['y'] = y
|
||||
cfg = i.run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 456)
|
||||
|
||||
def test_func_expansion(self):
|
||||
def y(a, b):
|
||||
return a + b
|
||||
i = interp.Interp(io.StringIO("x = y(z...)"), "TestRefs.test_func_expansion")
|
||||
i['y'] = y
|
||||
i['z'] = (123, 333)
|
||||
cfg = i.run()
|
||||
self.assertIn('x', cfg)
|
||||
self.assertEqual(cfg['x'], 456)
|
||||
|
||||
def test_call_uncallable(self):
|
||||
i = interp.Interp(io.StringIO("x = y(123, 333)"), "TestRefs.test_call_uncallable")
|
||||
i['y'] = 0
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
cfg = i.run()
|
||||
self.assertIn('cannot call non-callable object', str(ctx.exception))
|
||||
86
test/test_lexer.py
Normal file
86
test/test_lexer.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from hisscl import lexer
|
||||
import unittest
|
||||
import io
|
||||
|
||||
class TestComments(unittest.TestCase):
|
||||
def test_double_slash(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('// this should be ignored'), 'TestComments.test_double_slash').scan()
|
||||
self.assertEqual(tok, lexer.Token.EOF)
|
||||
self.assertEqual(lit, '')
|
||||
|
||||
def test_pound(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('# this should be ignored'), 'TestComments.test_pound').scan()
|
||||
self.assertEqual(tok, lexer.Token.EOF)
|
||||
self.assertEqual(lit, '')
|
||||
|
||||
def test_multiline(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('/* this\nshould\nbe\nignored */'), 'TestComments.test_multiline').scan()
|
||||
self.assertEqual(tok, lexer.Token.EOF)
|
||||
self.assertEqual(lit, '')
|
||||
|
||||
class TestOperators(unittest.TestCase):
|
||||
def test_lt(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('<'), 'TestOperators.test_lt').scan()
|
||||
self.assertEqual(tok, lexer.Token.OPERATOR)
|
||||
self.assertEqual(lit, '<')
|
||||
|
||||
def test_div(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('/'), 'TestOperators.test_div').scan()
|
||||
self.assertEqual(tok, lexer.Token.OPERATOR)
|
||||
self.assertEqual(lit, '/')
|
||||
|
||||
def test_multichar(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('<='), 'TestOperators.test_multichar').scan()
|
||||
self.assertEqual(tok, lexer.Token.OPERATOR)
|
||||
self.assertEqual(lit, '<=')
|
||||
|
||||
def test_ellipsis(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('...'), 'TestOperators.test_ellipsis').scan()
|
||||
self.assertEqual(tok, lexer.Token.ELLIPSIS)
|
||||
self.assertEqual(lit, '...')
|
||||
|
||||
def test_invalid_ellipsis(self):
|
||||
with self.assertRaises(lexer.ExpectedError) as ctx:
|
||||
lexer.Lexer(io.StringIO('..'), 'TestOperators.test_invalid_ellipsis').scan()
|
||||
self.assertIn("expected .", str(ctx.exception))
|
||||
self.assertIn("got EOF", str(ctx.exception))
|
||||
|
||||
class TestHeredoc(unittest.TestCase):
|
||||
def test_heredoc(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('<<EOT\nthis\nis\na\nmultiline\nstring\nEOT'), 'TestHeredoc.test_heredoc').scan()
|
||||
self.assertEqual(tok, lexer.Token.HEREDOC)
|
||||
self.assertEqual(lit, '\nthis\nis\na\nmultiline\nstring\n')
|
||||
|
||||
class TestNumber(unittest.TestCase):
|
||||
def test_integer(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('1234'), 'TestNumber.test_integer').scan()
|
||||
self.assertEqual(tok, lexer.Token.INTEGER)
|
||||
self.assertEqual(lit, "1234")
|
||||
|
||||
def test_float(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('1234.5678'), 'TestNumber.test_float').scan()
|
||||
self.assertEqual(tok, lexer.Token.FLOAT)
|
||||
self.assertEqual(lit, "1234.5678")
|
||||
|
||||
def test_invalid_float(self):
|
||||
with self.assertRaises(lexer.ExpectedError) as ctx:
|
||||
lexer.Lexer(io.StringIO('1.0.0'), 'TestNumber.test_invalid_float').scan()
|
||||
self.assertIn("expected number", str(ctx.exception))
|
||||
self.assertIn("got '.'", str(ctx.exception))
|
||||
|
||||
class TestString(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('""'), 'TestString.test_basic').scan()
|
||||
self.assertEqual(tok, lexer.Token.STRING)
|
||||
self.assertEqual(lit, '""')
|
||||
|
||||
def test_escape(self):
|
||||
tok, pos, lit = lexer.Lexer(io.StringIO('"\\""'), 'TestString.test_escape').scan()
|
||||
self.assertEqual(tok, lexer.Token.STRING)
|
||||
self.assertEqual(lit, '"\\""')
|
||||
|
||||
def test_newline(self):
|
||||
with self.assertRaises(lexer.ExpectedError) as ctx:
|
||||
lexer.Lexer(io.StringIO('"\n"'), 'TestString.test_newline').scan()
|
||||
self.assertIn("expected '\"'", str(ctx.exception))
|
||||
self.assertIn("got '\\n'", str(ctx.exception))
|
||||
243
test/test_parser.py
Normal file
243
test/test_parser.py
Normal file
@@ -0,0 +1,243 @@
|
||||
from hisscl import parser, ast
|
||||
import unittest
|
||||
import io
|
||||
|
||||
class TestLiterals(unittest.TestCase):
|
||||
def test_integer(self):
|
||||
val = parser.Parser(io.StringIO('1234'), 'TestLiterals.test_integer')._parse_value()
|
||||
self.assertIsInstance(val, ast.Integer)
|
||||
assert type(val) is ast.Integer
|
||||
self.assertEqual(val.value, 1234)
|
||||
|
||||
def test_float(self):
|
||||
val = parser.Parser(io.StringIO('1234.5678'), 'TestLiterals.test_float')._parse_value()
|
||||
self.assertIsInstance(val, ast.Float)
|
||||
assert type(val) is ast.Float
|
||||
self.assertEqual(val.value, 1234.5678)
|
||||
|
||||
def test_string(self):
|
||||
val = parser.Parser(io.StringIO('"test \\" \\u26a7"'), 'TestLiterals.test_string')._parse_value()
|
||||
self.assertIsInstance(val, ast.String)
|
||||
assert type(val) is ast.String
|
||||
self.assertEqual(val.value, 'test " \u26a7')
|
||||
|
||||
def test_bool(self):
|
||||
val = parser.Parser(io.StringIO('true'), 'TestLiterals.test_bool')._parse_value()
|
||||
self.assertIsInstance(val, ast.Bool)
|
||||
assert type(val) is ast.Bool
|
||||
self.assertEqual(val.value, True)
|
||||
|
||||
def test_heredoc(self):
|
||||
val = parser.Parser(io.StringIO('<<EOT\nthis\nis\na\nmultiline\nstring\nEOT'), 'TestLiterals.test_heredoc')._parse_value()
|
||||
self.assertIsInstance(val, ast.String)
|
||||
assert type(val) is ast.String
|
||||
self.assertEqual(val.value, '\nthis\nis\na\nmultiline\nstring\n')
|
||||
|
||||
class TestCollections(unittest.TestCase):
|
||||
def test_tuple(self):
|
||||
val = parser.Parser(io.StringIO('[1, 2.0, "3", true]'), 'TestCollections.test_tuple')._parse_value()
|
||||
self.assertIsInstance(val, ast.Tuple)
|
||||
assert type(val) is ast.Tuple
|
||||
self.assertEqual(val.items, [
|
||||
ast.Integer(
|
||||
pos = ast.Position(name="TestCollections.test_tuple", line=1, col=2),
|
||||
value = 1,
|
||||
),
|
||||
ast.Float(
|
||||
pos = ast.Position(name="TestCollections.test_tuple", line=1, col=5),
|
||||
value = 2.0,
|
||||
),
|
||||
ast.String(
|
||||
pos = ast.Position(name="TestCollections.test_tuple", line=1, col=10),
|
||||
value = "3",
|
||||
),
|
||||
ast.Bool(
|
||||
pos = ast.Position(name="TestCollections.test_tuple", line=1, col=15),
|
||||
value = True,
|
||||
),
|
||||
])
|
||||
|
||||
def test_object(self):
|
||||
val = parser.Parser(io.StringIO('{true: 2.0, "3": 4, 5.0: "6"}'), 'TestCollections.test_object')._parse_value()
|
||||
self.assertIsInstance(val, ast.Object)
|
||||
assert type(val) is ast.Object
|
||||
self.assertEqual(val.items, [
|
||||
(
|
||||
ast.Bool(
|
||||
pos = ast.Position(name="TestCollections.test_object", line=1, col=2),
|
||||
value = True,
|
||||
),
|
||||
ast.Float(
|
||||
pos = ast.Position(name="TestCollections.test_object", line=1, col=8),
|
||||
value = 2.0,
|
||||
),
|
||||
),
|
||||
(
|
||||
ast.String(
|
||||
pos = ast.Position(name="TestCollections.test_object", line=1, col=13),
|
||||
value = "3",
|
||||
),
|
||||
ast.Integer(
|
||||
pos = ast.Position(name="TestCollections.test_object", line=1, col=18),
|
||||
value = 4,
|
||||
),
|
||||
),
|
||||
(
|
||||
ast.Float(
|
||||
pos = ast.Position(name="TestCollections.test_object", line=1, col=21),
|
||||
value = 5.0,
|
||||
),
|
||||
ast.String(
|
||||
pos = ast.Position(name="TestCollections.test_object", line=1, col=26),
|
||||
value = "6",
|
||||
),
|
||||
)
|
||||
])
|
||||
|
||||
class TestExpressions(unittest.TestCase):
|
||||
def test_bare_value(self):
|
||||
val = parser.Parser(io.StringIO('1234'), 'TestExpressions.test_bare_value')._parse_expr()
|
||||
self.assertIsInstance(val, ast.Integer)
|
||||
assert type(val) is ast.Integer
|
||||
self.assertEqual(val.value, 1234)
|
||||
|
||||
def test_binary(self):
|
||||
val = parser.Parser(io.StringIO('1234 == 5678'), 'TestExpressions.test_binary')._parse_expr()
|
||||
self.assertEqual(val, ast.BinaryExpression(
|
||||
pos = ast.Position(name='TestExpressions.test_binary', line=1, col=1),
|
||||
left = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary', line=1, col=1),
|
||||
value = 1234,
|
||||
),
|
||||
op = ast.Operator(
|
||||
pos = ast.Position(name='TestExpressions.test_binary', line=1, col=6),
|
||||
value = '==',
|
||||
),
|
||||
right = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary', line=1, col=9),
|
||||
value = 5678
|
||||
),
|
||||
))
|
||||
|
||||
def test_binary_nested(self):
|
||||
val = parser.Parser(io.StringIO('(1234 - 5) == 5678'), 'TestExpressions.test_binary_nested')._parse_expr()
|
||||
self.assertEqual(val, ast.BinaryExpression(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=2),
|
||||
left = ast.BinaryExpression(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=2),
|
||||
left = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=2),
|
||||
value = 1234,
|
||||
),
|
||||
op = ast.Operator(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=7),
|
||||
value = '-',
|
||||
),
|
||||
right = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=9),
|
||||
value = 5,
|
||||
),
|
||||
),
|
||||
op = ast.Operator(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=12),
|
||||
value = '==',
|
||||
),
|
||||
right = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=15),
|
||||
value = 5678
|
||||
),
|
||||
))
|
||||
|
||||
def test_binary_multi(self):
|
||||
val = parser.Parser(io.StringIO('1234 == 5678 - 4444'), 'TestExpressions.test_binary_nested')._parse_expr()
|
||||
self.assertEqual(val, ast.BinaryExpression(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=1),
|
||||
left = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=1),
|
||||
value = 1234,
|
||||
),
|
||||
op = ast.Operator(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=6),
|
||||
value = '==',
|
||||
),
|
||||
right = ast.BinaryExpression(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=9),
|
||||
left = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=9),
|
||||
value = 5678
|
||||
),
|
||||
op = ast.Operator(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=14),
|
||||
value = '-',
|
||||
),
|
||||
right = ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_binary_nested', line=1, col=16),
|
||||
value = 4444,
|
||||
),
|
||||
),
|
||||
))
|
||||
|
||||
def test_expansion(self):
|
||||
val = parser.Parser(io.StringIO('x(y...)'), 'TestExpressions.test_expansion')._parse_expr()
|
||||
self.assertEqual(val, ast.FunctionCall(
|
||||
pos = ast.Position(name='TestExpressions.test_expansion', line=1, col=2),
|
||||
value = ast.VariableRef(
|
||||
pos = ast.Position(name='TestExpressions.test_expansion', line=1, col=1),
|
||||
name = 'x',
|
||||
),
|
||||
args = [
|
||||
ast.Expansion(
|
||||
pos = ast.Position(name='TestExpressions.test_expansion', line=1, col=3),
|
||||
value = ast.VariableRef(
|
||||
pos = ast.Position(name='TestExpressions.test_expansion', line=1, col=3),
|
||||
name = 'y',
|
||||
),
|
||||
),
|
||||
],
|
||||
))
|
||||
|
||||
def test_index(self):
|
||||
val = parser.Parser(io.StringIO('x[0]'), 'TestExpressions.test_index')._parse_expr()
|
||||
self.assertIsInstance(val, ast.Index)
|
||||
assert type(val) is ast.Index
|
||||
self.assertEqual(val.value, ast.VariableRef(
|
||||
pos = ast.Position(name='TestExpressions.test_index', line=1, col=1),
|
||||
name = 'x',
|
||||
))
|
||||
self.assertEqual(val.index, ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_index', line=1, col=3),
|
||||
value = 0,
|
||||
))
|
||||
|
||||
def test_index_legacy(self):
|
||||
val = parser.Parser(io.StringIO('x.0'), 'TestExpressions.test_index_legacy')._parse_expr()
|
||||
self.assertIsInstance(val, ast.Index)
|
||||
assert type(val) is ast.Index
|
||||
self.assertEqual(val.value, ast.VariableRef(
|
||||
pos = ast.Position(name='TestExpressions.test_index_legacy', line=1, col=1),
|
||||
name = 'x',
|
||||
))
|
||||
self.assertEqual(val.index, ast.Integer(
|
||||
pos = ast.Position(name='TestExpressions.test_index_legacy', line=1, col=3),
|
||||
value = 0,
|
||||
))
|
||||
|
||||
def test_getattr(self):
|
||||
val = parser.Parser(io.StringIO('x.y'), 'TestExpressions.test_getattr')._parse_expr()
|
||||
self.assertIsInstance(val, ast.GetAttr)
|
||||
assert type(val) is ast.GetAttr
|
||||
self.assertEqual(val.value, ast.VariableRef(
|
||||
pos = ast.Position(name='TestExpressions.test_getattr', line=1, col=1),
|
||||
name = 'x',
|
||||
))
|
||||
self.assertEqual(val.attr, 'y')
|
||||
|
||||
def test_unary(self):
|
||||
val = parser.Parser(io.StringIO('!true'), 'TestExpressions.test_unary')._parse_value()
|
||||
self.assertIsInstance(val, ast.UnaryExpression)
|
||||
assert type(val) is ast.UnaryExpression
|
||||
self.assertEqual(val.op.value, '!')
|
||||
self.assertEqual(val.value, ast.Bool(
|
||||
pos = ast.Position(name="TestExpressions.test_unary", line=1, col=2),
|
||||
value = True,
|
||||
))
|
||||
Reference in New Issue
Block a user