Source code for robot.parsing.model.blocks

#  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.

import ast

from robot.utils import file_writer, is_pathlike, is_string

from .statements import Comment, EmptyLine
from .visitor import ModelVisitor
from ..lexer import Token


[docs]class Block(ast.AST): _fields = () _attributes = ('lineno', 'col_offset', 'end_lineno', 'end_col_offset', 'errors') errors = () @property def lineno(self): statement = FirstStatementFinder.find_from(self) return statement.lineno if statement else -1 @property def col_offset(self): statement = FirstStatementFinder.find_from(self) return statement.col_offset if statement else -1 @property def end_lineno(self): statement = LastStatementFinder.find_from(self) return statement.end_lineno if statement else -1 @property def end_col_offset(self): statement = LastStatementFinder.find_from(self) return statement.end_col_offset if statement else -1
[docs] def validate_model(self): ModelValidator().visit(self)
[docs] def validate(self, context): pass
def _body_is_empty(self): for node in self.body: if not isinstance(node, (EmptyLine, Comment)): return False return True
[docs]class HeaderAndBody(Block): _fields = ('header', 'body') def __init__(self, header, body=None, errors=()): self.header = header self.body = body or [] self.errors = errors
[docs]class File(Block): _fields = ('sections',) _attributes = ('source',) + Block._attributes def __init__(self, sections=None, source=None): self.sections = sections or [] self.source = source
[docs] def save(self, output=None): """Save model to the given ``output`` or to the original source file. The ``output`` can be a path to a file or an already opened file object. If ``output`` is not given, the original source file will be overwritten. """ output = output or self.source if output is None: raise TypeError('Saving model requires explicit output ' 'when original source is not path.') ModelWriter(output).write(self)
[docs]class Section(Block): _fields = ('header', 'body') def __init__(self, header=None, body=None): self.header = header self.body = body or []
[docs]class SettingSection(Section): pass
[docs]class VariableSection(Section): pass
[docs]class TestCaseSection(Section): @property def tasks(self): return self.header.name.upper() in ('TASKS', 'TASK')
[docs]class KeywordSection(Section): pass
[docs]class CommentSection(Section): pass
[docs]class TestCase(Block): _fields = ('header', 'body') def __init__(self, header, body=None): self.header = header self.body = body or [] @property def name(self): return self.header.name
[docs]class Keyword(Block): _fields = ('header', 'body') def __init__(self, header, body=None): self.header = header self.body = body or [] @property def name(self): return self.header.name
[docs]class If(Block): """Represents IF structures in the model. Used with IF, Inline IF, ELSE IF and ELSE nodes. The :attr:`type` attribute specifies the type. """ _fields = ('header', 'body', 'orelse', 'end') def __init__(self, header, body=None, orelse=None, end=None, errors=()): self.header = header self.body = body or [] self.orelse = orelse self.end = end self.errors = errors @property def type(self): return self.header.type @property def condition(self): return self.header.condition @property def assign(self): return self.header.assign
[docs] def validate(self, context): self._validate_body() if self.type == Token.IF: self._validate_structure() self._validate_end() if self.type == Token.INLINE_IF: self._validate_structure() self._validate_inline_if()
def _validate_body(self): if self._body_is_empty(): type = self.type if self.type != Token.INLINE_IF else 'IF' self.errors += (f'{type} branch cannot be empty.',) def _validate_structure(self): orelse = self.orelse else_seen = False while orelse: if else_seen: if orelse.type == Token.ELSE: error = 'Only one ELSE allowed.' else: error = 'ELSE IF not allowed after ELSE.' if error not in self.errors: self.errors += (error,) else_seen = else_seen or orelse.type == Token.ELSE orelse = orelse.orelse def _validate_end(self): if not self.end: self.errors += ('IF must have closing END.',) def _validate_inline_if(self): branch = self assign = branch.assign while branch: if branch.body: item = branch.body[0] if assign and item.type != Token.KEYWORD: self.errors += ('Inline IF with assignment can only contain ' 'keyword calls.',) if getattr(item, 'assign', None): self.errors += ('Inline IF branches cannot contain assignments.',) if item.type == Token.INLINE_IF: self.errors += ('Inline IF cannot be nested.',) branch = branch.orelse
[docs]class For(Block): _fields = ('header', 'body', 'end') def __init__(self, header, body=None, end=None, errors=()): self.header = header self.body = body or [] self.end = end self.errors = errors @property def variables(self): return self.header.variables @property def values(self): return self.header.values @property def flavor(self): return self.header.flavor
[docs] def validate(self, context): if self._body_is_empty(): self.errors += ('FOR loop cannot be empty.',) if not self.end: self.errors += ('FOR loop must have closing END.',)
[docs]class Try(Block): _fields = ('header', 'body', 'next', 'end') def __init__(self, header, body=None, next=None, end=None, errors=()): self.header = header self.body = body or [] self.next = next self.end = end self.errors = errors @property def type(self): return self.header.type @property def patterns(self): return getattr(self.header, 'patterns', ()) @property def pattern_type(self): return getattr(self.header, 'pattern_type', None) @property def variable(self): return getattr(self.header, 'variable', None)
[docs] def validate(self, context): self._validate_body() if self.type == Token.TRY: self._validate_structure() self._validate_end()
def _validate_body(self): if self._body_is_empty(): self.errors += (f'{self.type} branch cannot be empty.',) def _validate_structure(self): else_count = 0 finally_count = 0 except_count = 0 empty_except_count = 0 branch = self.next while branch: if branch.type == Token.EXCEPT: if else_count: self.errors += ('EXCEPT not allowed after ELSE.',) if finally_count: self.errors += ('EXCEPT not allowed after FINALLY.',) if branch.patterns and empty_except_count: self.errors += ('EXCEPT without patterns must be last.',) if not branch.patterns: empty_except_count += 1 except_count += 1 if branch.type == Token.ELSE: if finally_count: self.errors += ('ELSE not allowed after FINALLY.',) else_count += 1 if branch.type == Token.FINALLY: finally_count += 1 branch = branch.next if finally_count > 1: self.errors += ('Only one FINALLY allowed.',) if else_count > 1: self.errors += ('Only one ELSE allowed.',) if empty_except_count > 1: self.errors += ('Only one EXCEPT without patterns allowed.',) if not (except_count or finally_count): self.errors += ('TRY structure must have EXCEPT or FINALLY branch.',) def _validate_end(self): if not self.end: self.errors += ('TRY must have closing END.',)
[docs]class While(Block): _fields = ('header', 'body', 'end') def __init__(self, header, body=None, end=None, errors=()): self.header = header self.body = body or [] self.end = end self.errors = errors @property def condition(self): return self.header.condition @property def limit(self): return self.header.limit
[docs] def validate(self, context): if self._body_is_empty(): self.errors += ('WHILE loop cannot be empty.',) if not self.end: self.errors += ('WHILE loop must have closing END.',)
[docs]class ModelWriter(ModelVisitor): def __init__(self, output): if is_string(output) or is_pathlike(output): self.writer = file_writer(output) self.close_writer = True else: self.writer = output self.close_writer = False
[docs] def write(self, model): try: self.visit(model) finally: if self.close_writer: self.writer.close()
[docs] def visit_Statement(self, statement): for token in statement.tokens: self.writer.write(token.value)
[docs]class ModelValidator(ModelVisitor): def __init__(self): self._context = ValidationContext()
[docs] def visit_Block(self, node): self._context.start_block(node) node.validate(self._context) ModelVisitor.generic_visit(self, node) self._context.end_block()
[docs] def visit_Try(self, node): if node.header.type == Token.FINALLY: self._context.in_finally = True self.visit_Block(node) self._context.in_finally = False
[docs] def visit_Statement(self, node): node.validate(self._context) ModelVisitor.generic_visit(self, node)
[docs]class ValidationContext: def __init__(self): self.roots = [] self.in_finally = False
[docs] def start_block(self, node): self.roots.append(node)
[docs] def end_block(self): self.roots.pop()
@property def in_keyword(self): return Keyword in [type(r) for r in self.roots] @property def in_for(self): return For in [type(r) for r in self.roots] @property def in_while(self): return While in [type(r) for r in self.roots]
[docs]class FirstStatementFinder(ModelVisitor): def __init__(self): self.statement = None
[docs] @classmethod def find_from(cls, model): finder = cls() finder.visit(model) return finder.statement
[docs] def visit_Statement(self, statement): if self.statement is None: self.statement = statement
[docs] def generic_visit(self, node): if self.statement is None: ModelVisitor.generic_visit(self, node)
[docs]class LastStatementFinder(ModelVisitor): def __init__(self): self.statement = None
[docs] @classmethod def find_from(cls, model): finder = cls() finder.visit(model) return finder.statement
[docs] def visit_Statement(self, statement): self.statement = statement