1# Copyright 2008-2015 Nokia Networks 2# Copyright 2016- Robot Framework Foundation 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import os 17import copy 18import warnings 19 20from robot.errors import DataError 21from robot.variables import is_var 22from robot.output import LOGGER 23from robot.writer import DataFileWriter 24from robot.utils import abspath, is_string, normalize, py2to3, NormalizedDict 25 26from .comments import Comment 27from .populators import FromFilePopulator, FromDirectoryPopulator, NoTestsFound 28from .settings import (Documentation, Fixture, Timeout, Tags, Metadata, 29 Library, Resource, Variables, Arguments, Return, 30 Template, MetadataList, ImportList) 31 32 33def TestData(parent=None, source=None, include_suites=None, 34 warn_on_skipped='DEPRECATED', extensions=None): 35 """Parses a file or directory to a corresponding model object. 36 37 :param parent: Optional parent to be used in creation of the model object. 38 :param source: Path where test data is read from. 39 :param warn_on_skipped: Deprecated. 40 :param extensions: List/set of extensions to parse. If None, all files 41 supported by Robot Framework are parsed when searching test cases. 42 :returns: :class:`~.model.TestDataDirectory` if `source` is a directory, 43 :class:`~.model.TestCaseFile` otherwise. 44 """ 45 # TODO: Remove in RF 3.2. 46 if warn_on_skipped != 'DEPRECATED': 47 warnings.warn("Option 'warn_on_skipped' is deprecated and has no " 48 "effect.", DeprecationWarning) 49 if os.path.isdir(source): 50 return TestDataDirectory(parent, source).populate(include_suites, 51 extensions) 52 return TestCaseFile(parent, source).populate() 53 54 55class _TestData(object): 56 _setting_table_names = 'Setting', 'Settings' 57 _variable_table_names = 'Variable', 'Variables' 58 _testcase_table_names = 'Test Case', 'Test Cases', 'Task', 'Tasks' 59 _keyword_table_names = 'Keyword', 'Keywords' 60 _comment_table_names = 'Comment', 'Comments' 61 62 def __init__(self, parent=None, source=None): 63 self.parent = parent 64 self.source = abspath(source) if source else None 65 self.children = [] 66 self._tables = dict(self._get_tables()) 67 68 def _get_tables(self): 69 for names, table in [(self._setting_table_names, self.setting_table), 70 (self._variable_table_names, self.variable_table), 71 (self._testcase_table_names, self.testcase_table), 72 (self._keyword_table_names, self.keyword_table), 73 (self._comment_table_names, None)]: 74 for name in names: 75 yield name, table 76 77 def start_table(self, header_row): 78 table = self._find_table(header_row) 79 if table is None or not self._table_is_allowed(table): 80 return None 81 table.set_header(header_row) 82 return table 83 84 def _find_table(self, header_row): 85 name = header_row[0] if header_row else '' 86 title = name.title() 87 if title not in self._tables: 88 title = self._resolve_deprecated_table(name) 89 if title is None: 90 self._report_unrecognized_table(name) 91 return None 92 return self._tables[title] 93 94 def _resolve_deprecated_table(self, used_name): 95 normalized = normalize(used_name) 96 for name in (self._setting_table_names + self._variable_table_names + 97 self._testcase_table_names + self._keyword_table_names + 98 self._comment_table_names): 99 if normalize(name) == normalized: 100 self._report_deprecated_table(used_name, name) 101 return name 102 return None 103 104 def _report_deprecated_table(self, deprecated, name): 105 self.report_invalid_syntax( 106 "Section name '%s' is deprecated. Use '%s' instead." 107 % (deprecated, name), level='WARN' 108 ) 109 110 def _report_unrecognized_table(self, name): 111 self.report_invalid_syntax( 112 "Unrecognized table header '%s'. Available headers for data: " 113 "'Setting(s)', 'Variable(s)', 'Test Case(s)', 'Task(s)' and " 114 "'Keyword(s)'. Use 'Comment(s)' to embedded additional data." 115 % name 116 ) 117 118 def _table_is_allowed(self, table): 119 return True 120 121 @property 122 def name(self): 123 return self._format_name(self._get_basename()) if self.source else None 124 125 def _get_basename(self): 126 return os.path.splitext(os.path.basename(self.source))[0] 127 128 def _format_name(self, name): 129 name = self._strip_possible_prefix_from_name(name) 130 name = name.replace('_', ' ').strip() 131 return name.title() if name.islower() else name 132 133 def _strip_possible_prefix_from_name(self, name): 134 return name.split('__', 1)[-1] 135 136 @property 137 def keywords(self): 138 return self.keyword_table.keywords 139 140 @property 141 def imports(self): 142 return self.setting_table.imports 143 144 def report_invalid_syntax(self, message, level='ERROR'): 145 initfile = getattr(self, 'initfile', None) 146 path = os.path.join(self.source, initfile) if initfile else self.source 147 LOGGER.write("Error in file '%s': %s" % (path, message), level) 148 149 def save(self, **options): 150 """Writes this datafile to disk. 151 152 :param options: Configuration for writing. These are passed to 153 :py:class:`~robot.writer.datafilewriter.WritingContext` as 154 keyword arguments. 155 156 See also :py:class:`robot.writer.datafilewriter.DataFileWriter` 157 """ 158 return DataFileWriter(**options).write(self) 159 160 161@py2to3 162class TestCaseFile(_TestData): 163 """The parsed test case file object. 164 165 :param parent: parent object to be used in creation of the model object. 166 :param source: path where test data is read from. 167 """ 168 169 def __init__(self, parent=None, source=None): 170 self.directory = os.path.dirname(source) if source else None 171 self.setting_table = TestCaseFileSettingTable(self) 172 self.variable_table = VariableTable(self) 173 self.testcase_table = TestCaseTable(self) 174 self.keyword_table = KeywordTable(self) 175 _TestData.__init__(self, parent, source) 176 177 def populate(self): 178 FromFilePopulator(self).populate(self.source) 179 self._validate() 180 return self 181 182 def _validate(self): 183 if not self.testcase_table.is_started(): 184 raise NoTestsFound('File has no tests or tasks.') 185 186 def has_tests(self): 187 return True 188 189 def __iter__(self): 190 for table in [self.setting_table, self.variable_table, 191 self.testcase_table, self.keyword_table]: 192 yield table 193 194 def __nonzero__(self): 195 return any(table for table in self) 196 197 198class ResourceFile(_TestData): 199 """The parsed resource file object. 200 201 :param source: path where resource file is read from. 202 """ 203 204 def __init__(self, source=None): 205 self.directory = os.path.dirname(source) if source else None 206 self.setting_table = ResourceFileSettingTable(self) 207 self.variable_table = VariableTable(self) 208 self.testcase_table = TestCaseTable(self) 209 self.keyword_table = KeywordTable(self) 210 _TestData.__init__(self, source=source) 211 212 def populate(self): 213 FromFilePopulator(self).populate(self.source, resource=True) 214 self._report_status() 215 return self 216 217 def _report_status(self): 218 if self.setting_table or self.variable_table or self.keyword_table: 219 LOGGER.info("Imported resource file '%s' (%d keywords)." 220 % (self.source, len(self.keyword_table.keywords))) 221 else: 222 LOGGER.warn("Imported resource file '%s' is empty." % self.source) 223 224 def _table_is_allowed(self, table): 225 if table is self.testcase_table: 226 raise DataError("Resource file '%s' cannot contain tests or " 227 "tasks." % self.source) 228 return True 229 230 def __iter__(self): 231 for table in [self.setting_table, self.variable_table, self.keyword_table]: 232 yield table 233 234 235class TestDataDirectory(_TestData): 236 """The parsed test data directory object. Contains hiearchical structure 237 of other :py:class:`.TestDataDirectory` and :py:class:`.TestCaseFile` 238 objects. 239 240 :param parent: parent object to be used in creation of the model object. 241 :param source: path where test data is read from. 242 """ 243 244 def __init__(self, parent=None, source=None): 245 self.directory = source 246 self.initfile = None 247 self.setting_table = InitFileSettingTable(self) 248 self.variable_table = VariableTable(self) 249 self.testcase_table = TestCaseTable(self) 250 self.keyword_table = KeywordTable(self) 251 _TestData.__init__(self, parent, source) 252 253 def populate(self, include_suites=None, extensions=None, recurse=True): 254 FromDirectoryPopulator().populate(self.source, self, include_suites, 255 extensions, recurse) 256 self.children = [ch for ch in self.children if ch.has_tests()] 257 return self 258 259 def _get_basename(self): 260 return os.path.basename(self.source) 261 262 def _table_is_allowed(self, table): 263 if table is self.testcase_table: 264 LOGGER.error("Test suite initialization file in '%s' cannot " 265 "contain tests or tasks." % self.source) 266 return False 267 return True 268 269 def add_child(self, path, include_suites, extensions=None): 270 self.children.append(TestData(parent=self, 271 source=path, 272 include_suites=include_suites, 273 extensions=extensions)) 274 275 def has_tests(self): 276 return any(ch.has_tests() for ch in self.children) 277 278 def __iter__(self): 279 for table in [self.setting_table, self.variable_table, self.keyword_table]: 280 yield table 281 282 283@py2to3 284class _Table(object): 285 286 def __init__(self, parent): 287 self.parent = parent 288 self._header = None 289 290 def set_header(self, header): 291 self._header = self._prune_old_style_headers(header) 292 293 def _prune_old_style_headers(self, header): 294 if len(header) < 3: 295 return header 296 if self._old_header_matcher.match(header): 297 return [header[0]] 298 return header 299 300 @property 301 def header(self): 302 return self._header or [self.type.title() + 's'] 303 304 @property 305 def name(self): 306 return self.header[0] 307 308 @property 309 def source(self): 310 return self.parent.source 311 312 @property 313 def directory(self): 314 return self.parent.directory 315 316 def report_invalid_syntax(self, message, level='ERROR'): 317 self.parent.report_invalid_syntax(message, level) 318 319 def __nonzero__(self): 320 return bool(self._header or len(self)) 321 322 def __len__(self): 323 return sum(1 for item in self) 324 325 326class _WithSettings(object): 327 _setters = {} 328 _aliases = {} 329 330 def get_setter(self, name): 331 if name[-1:] == ':': 332 name = name[:-1] 333 setter = self._get_setter(name) 334 if setter is not None: 335 return setter 336 setter = self._get_deprecated_setter(name) 337 if setter is not None: 338 return setter 339 self.report_invalid_syntax("Non-existing setting '%s'." % name) 340 return None 341 342 def _get_setter(self, name): 343 title = name.title() 344 if title in self._aliases: 345 title = self._aliases[name] 346 if title in self._setters: 347 return self._setters[title](self) 348 return None 349 350 def _get_deprecated_setter(self, name): 351 normalized = normalize(name) 352 for setting in list(self._setters) + list(self._aliases): 353 if normalize(setting) == normalized: 354 self._report_deprecated_setting(name, setting) 355 return self._get_setter(setting) 356 return None 357 358 def _report_deprecated_setting(self, deprecated, correct): 359 self.report_invalid_syntax( 360 "Setting '%s' is deprecated. Use '%s' instead." 361 % (deprecated, correct), level='WARN' 362 ) 363 364 def report_invalid_syntax(self, message, level='ERROR'): 365 raise NotImplementedError 366 367 368class _SettingTable(_Table, _WithSettings): 369 type = 'setting' 370 371 def __init__(self, parent): 372 _Table.__init__(self, parent) 373 self.doc = Documentation('Documentation', self) 374 self.suite_setup = Fixture('Suite Setup', self) 375 self.suite_teardown = Fixture('Suite Teardown', self) 376 self.test_setup = Fixture('Test Setup', self) 377 self.test_teardown = Fixture('Test Teardown', self) 378 self.force_tags = Tags('Force Tags', self) 379 self.default_tags = Tags('Default Tags', self) 380 self.test_template = Template('Test Template', self) 381 self.test_timeout = Timeout('Test Timeout', self) 382 self.metadata = MetadataList(self) 383 self.imports = ImportList(self) 384 385 @property 386 def _old_header_matcher(self): 387 return OldStyleSettingAndVariableTableHeaderMatcher() 388 389 def add_metadata(self, name, value='', comment=None): 390 self.metadata.add(Metadata(self, name, value, comment)) 391 return self.metadata[-1] 392 393 def add_library(self, name, args=None, comment=None): 394 self.imports.add(Library(self, name, args, comment=comment)) 395 return self.imports[-1] 396 397 def add_resource(self, name, invalid_args=None, comment=None): 398 self.imports.add(Resource(self, name, invalid_args, comment=comment)) 399 return self.imports[-1] 400 401 def add_variables(self, name, args=None, comment=None): 402 self.imports.add(Variables(self, name, args, comment=comment)) 403 return self.imports[-1] 404 405 def __len__(self): 406 return sum(1 for setting in self if setting.is_set()) 407 408 409class TestCaseFileSettingTable(_SettingTable): 410 _setters = {'Documentation': lambda s: s.doc.populate, 411 'Suite Setup': lambda s: s.suite_setup.populate, 412 'Suite Teardown': lambda s: s.suite_teardown.populate, 413 'Test Setup': lambda s: s.test_setup.populate, 414 'Test Teardown': lambda s: s.test_teardown.populate, 415 'Force Tags': lambda s: s.force_tags.populate, 416 'Default Tags': lambda s: s.default_tags.populate, 417 'Test Template': lambda s: s.test_template.populate, 418 'Test Timeout': lambda s: s.test_timeout.populate, 419 'Library': lambda s: s.imports.populate_library, 420 'Resource': lambda s: s.imports.populate_resource, 421 'Variables': lambda s: s.imports.populate_variables, 422 'Metadata': lambda s: s.metadata.populate} 423 _aliases = {'Task Setup': 'Test Setup', 424 'Task Teardown': 'Test Teardown', 425 'Task Template': 'Test Template', 426 'Task Timeout': 'Test Timeout'} 427 428 def __iter__(self): 429 for setting in [self.doc, self.suite_setup, self.suite_teardown, 430 self.test_setup, self.test_teardown, self.force_tags, 431 self.default_tags, self.test_template, self.test_timeout] \ 432 + self.metadata.data + self.imports.data: 433 yield setting 434 435 436class ResourceFileSettingTable(_SettingTable): 437 _setters = {'Documentation': lambda s: s.doc.populate, 438 'Library': lambda s: s.imports.populate_library, 439 'Resource': lambda s: s.imports.populate_resource, 440 'Variables': lambda s: s.imports.populate_variables} 441 442 def __iter__(self): 443 for setting in [self.doc] + self.imports.data: 444 yield setting 445 446 447class InitFileSettingTable(_SettingTable): 448 _setters = {'Documentation': lambda s: s.doc.populate, 449 'Suite Setup': lambda s: s.suite_setup.populate, 450 'Suite Teardown': lambda s: s.suite_teardown.populate, 451 'Test Setup': lambda s: s.test_setup.populate, 452 'Test Teardown': lambda s: s.test_teardown.populate, 453 'Test Timeout': lambda s: s.test_timeout.populate, 454 'Force Tags': lambda s: s.force_tags.populate, 455 'Library': lambda s: s.imports.populate_library, 456 'Resource': lambda s: s.imports.populate_resource, 457 'Variables': lambda s: s.imports.populate_variables, 458 'Metadata': lambda s: s.metadata.populate} 459 460 def __iter__(self): 461 for setting in [self.doc, self.suite_setup, self.suite_teardown, 462 self.test_setup, self.test_teardown, self.force_tags, 463 self.test_timeout] + self.metadata.data + self.imports.data: 464 yield setting 465 466 467class VariableTable(_Table): 468 type = 'variable' 469 470 def __init__(self, parent): 471 _Table.__init__(self, parent) 472 self.variables = [] 473 474 @property 475 def _old_header_matcher(self): 476 return OldStyleSettingAndVariableTableHeaderMatcher() 477 478 def add(self, name, value, comment=None): 479 self.variables.append(Variable(self, name, value, comment)) 480 481 def __iter__(self): 482 return iter(self.variables) 483 484 485class TestCaseTable(_Table): 486 type = 'test case' 487 488 def __init__(self, parent): 489 _Table.__init__(self, parent) 490 self.tests = [] 491 492 def set_header(self, header): 493 if self._header and header: 494 self._validate_mode(self._header[0], header[0]) 495 _Table.set_header(self, header) 496 497 def _validate_mode(self, name1, name2): 498 tasks1 = normalize(name1) in ('task', 'tasks') 499 tasks2 = normalize(name2) in ('task', 'tasks') 500 if tasks1 is not tasks2: 501 raise DataError('One file cannot have both tests and tasks.') 502 503 @property 504 def _old_header_matcher(self): 505 return OldStyleTestAndKeywordTableHeaderMatcher() 506 507 def add(self, name): 508 self.tests.append(TestCase(self, name)) 509 return self.tests[-1] 510 511 def __iter__(self): 512 return iter(self.tests) 513 514 def is_started(self): 515 return bool(self._header) 516 517 518class KeywordTable(_Table): 519 type = 'keyword' 520 521 def __init__(self, parent): 522 _Table.__init__(self, parent) 523 self.keywords = [] 524 525 @property 526 def _old_header_matcher(self): 527 return OldStyleTestAndKeywordTableHeaderMatcher() 528 529 def add(self, name): 530 self.keywords.append(UserKeyword(self, name)) 531 return self.keywords[-1] 532 533 def __iter__(self): 534 return iter(self.keywords) 535 536 537@py2to3 538class Variable(object): 539 540 def __init__(self, parent, name, value, comment=None): 541 self.parent = parent 542 self.name = name.rstrip('= ') 543 if name.startswith('$') and value == []: 544 value = '' 545 if is_string(value): 546 value = [value] 547 self.value = value 548 self.comment = Comment(comment) 549 550 def as_list(self): 551 if self.has_data(): 552 return [self.name] + self.value + self.comment.as_list() 553 return self.comment.as_list() 554 555 def is_set(self): 556 return True 557 558 def is_for_loop(self): 559 return False 560 561 def has_data(self): 562 return bool(self.name or ''.join(self.value)) 563 564 def __nonzero__(self): 565 return self.has_data() 566 567 def report_invalid_syntax(self, message, level='ERROR'): 568 self.parent.report_invalid_syntax("Setting variable '%s' failed: %s" 569 % (self.name, message), level) 570 571 572class _WithSteps(object): 573 574 def add_step(self, content, comment=None): 575 self.steps.append(Step(content, comment)) 576 return self.steps[-1] 577 578 def copy(self, name): 579 new = copy.deepcopy(self) 580 new.name = name 581 self._add_to_parent(new) 582 return new 583 584 585class TestCase(_WithSteps, _WithSettings): 586 587 def __init__(self, parent, name): 588 self.parent = parent 589 self.name = name 590 self.doc = Documentation('[Documentation]', self) 591 self.template = Template('[Template]', self) 592 self.tags = Tags('[Tags]', self) 593 self.setup = Fixture('[Setup]', self) 594 self.teardown = Fixture('[Teardown]', self) 595 self.timeout = Timeout('[Timeout]', self) 596 self.steps = [] 597 if name == '...': 598 self.report_invalid_syntax( 599 "Using '...' as test case name is deprecated. It will be " 600 "considered line continuation in Robot Framework 3.2.", 601 level='WARN' 602 ) 603 604 _setters = {'Documentation': lambda s: s.doc.populate, 605 'Template': lambda s: s.template.populate, 606 'Setup': lambda s: s.setup.populate, 607 'Teardown': lambda s: s.teardown.populate, 608 'Tags': lambda s: s.tags.populate, 609 'Timeout': lambda s: s.timeout.populate} 610 611 @property 612 def source(self): 613 return self.parent.source 614 615 @property 616 def directory(self): 617 return self.parent.directory 618 619 def add_for_loop(self, declaration, comment=None): 620 self.steps.append(ForLoop(self, declaration, comment)) 621 return self.steps[-1] 622 623 def end_for_loop(self): 624 loop, steps = self._find_last_empty_for_and_steps_after() 625 if not loop: 626 return False 627 loop.steps.extend(steps) 628 self.steps[-len(steps):] = [] 629 return True 630 631 def _find_last_empty_for_and_steps_after(self): 632 steps = [] 633 for step in reversed(self.steps): 634 if isinstance(step, ForLoop): 635 if not step.steps: 636 steps.reverse() 637 return step, steps 638 break 639 steps.append(step) 640 return None, [] 641 642 def report_invalid_syntax(self, message, level='ERROR'): 643 type_ = 'test case' if type(self) is TestCase else 'keyword' 644 message = "Invalid syntax in %s '%s': %s" % (type_, self.name, message) 645 self.parent.report_invalid_syntax(message, level) 646 647 def _add_to_parent(self, test): 648 self.parent.tests.append(test) 649 650 @property 651 def settings(self): 652 return [self.doc, self.tags, self.setup, self.template, self.timeout, 653 self.teardown] 654 655 def __iter__(self): 656 for element in [self.doc, self.tags, self.setup, 657 self.template, self.timeout] \ 658 + self.steps + [self.teardown]: 659 yield element 660 661 662class UserKeyword(TestCase): 663 664 def __init__(self, parent, name): 665 self.parent = parent 666 self.name = name 667 self.doc = Documentation('[Documentation]', self) 668 self.args = Arguments('[Arguments]', self) 669 self.return_ = Return('[Return]', self) 670 self.timeout = Timeout('[Timeout]', self) 671 self.teardown = Fixture('[Teardown]', self) 672 self.tags = Tags('[Tags]', self) 673 self.steps = [] 674 if name == '...': 675 self.report_invalid_syntax( 676 "Using '...' as keyword name is deprecated. It will be " 677 "considered line continuation in Robot Framework 3.2.", 678 level='WARN' 679 ) 680 681 _setters = {'Documentation': lambda s: s.doc.populate, 682 'Arguments': lambda s: s.args.populate, 683 'Return': lambda s: s.return_.populate, 684 'Timeout': lambda s: s.timeout.populate, 685 'Teardown': lambda s: s.teardown.populate, 686 'Tags': lambda s: s.tags.populate} 687 688 def _add_to_parent(self, test): 689 self.parent.keywords.append(test) 690 691 @property 692 def settings(self): 693 return [self.args, self.doc, self.tags, self.timeout, self.teardown, self.return_] 694 695 def __iter__(self): 696 for element in [self.args, self.doc, self.tags, self.timeout] \ 697 + self.steps + [self.teardown, self.return_]: 698 yield element 699 700 701class ForLoop(_WithSteps): 702 """The parsed representation of a for-loop. 703 704 :param list declaration: The literal cell values that declare the loop 705 (excluding ":FOR"). 706 :param str comment: A comment, default None. 707 :ivar str flavor: The value of the 'IN' item, uppercased. 708 Typically 'IN', 'IN RANGE', 'IN ZIP', or 'IN ENUMERATE'. 709 :ivar list vars: Variables set per-iteration by this loop. 710 :ivar list items: Loop values that come after the 'IN' item. 711 :ivar str comment: A comment, or None. 712 :ivar list steps: A list of steps in the loop. 713 """ 714 flavors = {'IN', 'IN RANGE', 'IN ZIP', 'IN ENUMERATE'} 715 normalized_flavors = NormalizedDict((f, f) for f in flavors) 716 717 def __init__(self, parent, declaration, comment=None): 718 self.parent = parent 719 self.flavor, index = self._get_flavor_and_index(declaration) 720 self.vars = declaration[:index] 721 self.items = declaration[index+1:] 722 self.comment = Comment(comment) 723 self.steps = [] 724 725 def _get_flavor_and_index(self, declaration): 726 for index, item in enumerate(declaration): 727 if item in self.flavors: 728 return item, index 729 if item in self.normalized_flavors: 730 correct = self.normalized_flavors[item] 731 self._report_deprecated_flavor_syntax(item, correct) 732 return correct, index 733 if normalize(item).startswith('in'): 734 return item.upper(), index 735 return 'IN', len(declaration) 736 737 def _report_deprecated_flavor_syntax(self, deprecated, correct): 738 self.parent.report_invalid_syntax( 739 "Using '%s' as a FOR loop separator is deprecated. " 740 "Use '%s' instead." % (deprecated, correct), level='WARN' 741 ) 742 743 def is_comment(self): 744 return False 745 746 def is_for_loop(self): 747 return True 748 749 def as_list(self, indent=False, include_comment=True): 750 comments = self.comment.as_list() if include_comment else [] 751 return ['FOR'] + self.vars + [self.flavor] + self.items + comments 752 753 def __iter__(self): 754 return iter(self.steps) 755 756 def is_set(self): 757 return True 758 759 760class Step(object): 761 762 def __init__(self, content, comment=None): 763 self.assign = self._get_assign(content) 764 self.name = content.pop(0) if content else None 765 self.args = content 766 self.comment = Comment(comment) 767 768 def _get_assign(self, content): 769 assign = [] 770 while content and is_var(content[0].rstrip('= ')): 771 assign.append(content.pop(0)) 772 return assign 773 774 def is_comment(self): 775 return not (self.assign or self.name or self.args) 776 777 def is_for_loop(self): 778 return False 779 780 def is_set(self): 781 return True 782 783 def as_list(self, indent=False, include_comment=True): 784 kw = [self.name] if self.name is not None else [] 785 comments = self.comment.as_list() if include_comment else [] 786 data = self.assign + kw + self.args + comments 787 if indent: 788 data.insert(0, '') 789 return data 790 791 792class OldStyleSettingAndVariableTableHeaderMatcher(object): 793 794 def match(self, header): 795 return all(value.lower() == 'value' for value in header[1:]) 796 797 798class OldStyleTestAndKeywordTableHeaderMatcher(object): 799 800 def match(self, header): 801 if header[1].lower() != 'action': 802 return False 803 for arg in header[2:]: 804 if not arg.lower().startswith('arg'): 805 return False 806 return True 807