# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ast import literal_eval
from enum import auto, Enum
from dataclasses import dataclass
from typing import Literal
from .typeinfo import LITERAL_TYPES, TypeInfo
[docs]
class TokenType(Enum):
NAME = auto()
LEFT_SQUARE = auto()
RIGHT_SQUARE = auto()
PIPE = auto()
COMMA = auto()
def __repr__(self):
return str(self)
[docs]
@dataclass
class Token:
type: TokenType
value: str
position: int = -1
[docs]
class TypeInfoTokenizer:
markers = {
'[': TokenType.LEFT_SQUARE,
']': TokenType.RIGHT_SQUARE,
'|': TokenType.PIPE,
',': TokenType.COMMA,
}
def __init__(self, source: str):
self.source = source
self.tokens: 'list[Token]' = []
self.start = 0
self.current = 0
@property
def at_end(self) -> bool:
return self.current >= len(self.source)
[docs]
def tokenize(self) -> 'list[Token]':
while not self.at_end:
self.start = self.current
char = self.advance()
if char in self.markers:
self.add_token(self.markers[char])
elif char.strip():
self.name()
return self.tokens
[docs]
def advance(self) -> str:
char = self.source[self.current]
self.current += 1
return char
[docs]
def peek(self) -> 'str|None':
try:
return self.source[self.current]
except IndexError:
return None
[docs]
def name(self):
end_at = set(self.markers) | {None}
closing_quote = None
char = self.source[self.current-1]
if char in ('"', "'"):
end_at = {None}
closing_quote = char
elif char == 'b' and self.peek() in ('"', "'"):
end_at = {None}
closing_quote = self.advance()
while True:
char = self.peek()
if char in end_at:
break
self.current += 1
if char == closing_quote:
break
self.add_token(TokenType.NAME)
[docs]
def add_token(self, type: TokenType):
value = self.source[self.start:self.current].strip()
self.tokens.append(Token(type, value, self.start))
[docs]
class TypeInfoParser:
def __init__(self, source: str):
self.source = source
self.tokens: 'list[Token]' = []
self.current = 0
@property
def at_end(self) -> bool:
return self.peek() is None
[docs]
def parse(self) -> TypeInfo:
self.tokens = TypeInfoTokenizer(self.source).tokenize()
info = self.type()
if not self.at_end:
self.error(f"Extra content after '{info}'.")
return info
[docs]
def type(self) -> TypeInfo:
if not self.check(TokenType.NAME):
self.error('Type name missing.')
info = TypeInfo(self.advance().value)
if self.match(TokenType.LEFT_SQUARE):
info.nested = self.params(literal=info.type is Literal)
if self.match(TokenType.PIPE):
nested = [info] + self.union()
info = TypeInfo('Union', nested=nested)
return info
[docs]
def params(self, literal: bool = False) -> 'list[TypeInfo]':
params = []
prev = None
while True:
token = self.peek()
if not token:
self.error("Closing ']' missing.")
if token.type is TokenType.RIGHT_SQUARE:
self.advance()
break
if token.type is TokenType.COMMA:
if not prev or prev.type is TokenType.COMMA:
self.error("Type missing before ','.")
self.advance()
prev = token
continue
if token.type is TokenType.NAME:
param = self.type()
elif token.type is TokenType.LEFT_SQUARE:
self.advance()
param = TypeInfo()
param.nested = self.params()
if literal:
param = self._literal_param(param)
params.append(param)
prev = token
if literal and not params:
self.error('Literal cannot be empty.')
return params
def _literal_param(self, param: TypeInfo) -> TypeInfo:
try:
if param.name is None:
raise ValueError
try:
value = literal_eval(param.name)
except ValueError:
if param.name.isidentifier():
return TypeInfo(param.name, None)
raise
if not isinstance(value, LITERAL_TYPES):
raise ValueError
except (ValueError, SyntaxError):
self.error(f"Invalid literal value {str(param)!r}.")
else:
return TypeInfo(repr(value), value)
[docs]
def union(self) -> 'list[TypeInfo]':
types = []
while not types or self.match(TokenType.PIPE):
info = self.type()
if info.is_union:
types.extend(info.nested)
else:
types.append(info)
return types
[docs]
def match(self, *types: TokenType) -> bool:
for typ in types:
if self.check(typ):
self.advance()
return True
return False
[docs]
def check(self, expected: TokenType) -> bool:
peeked = self.peek()
return peeked and peeked.type == expected
[docs]
def advance(self) -> 'Token|None':
token = self.peek()
if token:
self.current += 1
return token
[docs]
def peek(self) -> 'Token|None':
try:
return self.tokens[self.current]
except IndexError:
return None
[docs]
def error(self, message: str, token: 'Token|None' = None):
if not token:
token = self.peek()
position = f'index {token.position}' if token else 'end'
raise ValueError(f"Parsing type {self.source!r} failed: "
f"Error at {position}: {message}")