Robot Framework Integrated Development Environment (RIDE)
model.py
Go to the documentation of this file.
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 
16 import os
17 import copy
18 import re
19 import warnings
20 
21 from robotide.lib.robot.errors import DataError
22 from robotide.lib.robot.variables import is_var
23 from robotide.lib.robot.output import LOGGER
24 from robotide.lib.robot.writer import DataFileWriter
25 from robotide.lib.robot.utils import abspath, is_string, normalize, py2to3, NormalizedDict
26 
27 from .comments import Comment
28 from .populators import FromFilePopulator, FromDirectoryPopulator, NoTestsFound
29 from .settings import (Documentation, Fixture, Timeout, Tags, Metadata,
30  Library, Resource, Variables, Arguments, Return,
31  Template, MetadataList, ImportList)
32 
33 re_set_var = re.compile(r"(?i)^set[ ](\S.*)+variable$")
34 
35 
36 
46 def TestData(parent=None, source=None, include_suites=None,
47  warn_on_skipped='DEPRECATED', extensions=None, settings=None):
48  # TODO: Remove in RF 3.2.
49  if warn_on_skipped != 'DEPRECATED':
50  warnings.warn("Option 'warn_on_skipped' is deprecated and has no "
51  "effect.", DeprecationWarning)
52  if os.path.isdir(source):
53  return TestDataDirectory(parent, source, settings).populate(include_suites,
54  extensions)
55  return TestCaseFile(parent, source, settings).populate()
56 
57 
58 class _TestData():
59 
62  _setting_table_names = 'Setting', 'Settings'
63 
66  _variable_table_names = 'Variable', 'Variables'
67 
70  _testcase_table_names = 'Test Case', 'Test Cases', 'Task', 'Tasks'
71 
74  _keyword_table_names = 'Keyword', 'Keywords'
75 
78  _comment_table_names = 'Comment', 'Comments'
79 
80  def __init__(self, parent=None, source=None):
81  self.parentparent = parent
82  self.sourcesource = abspath(source) if source else None
83  self.childrenchildren = []
84  self._preamble_preamble = []
85  self._tables_tables = dict(self._get_tables_get_tables())
86 
87  def _get_tables(self):
88  for names, table in [(self._setting_table_names_setting_table_names, self.setting_table),
89  (self._variable_table_names_variable_table_names, self.variable_table),
90  (self._testcase_table_names_testcase_table_names, self.testcase_table),
91  (self._keyword_table_names_keyword_table_names, self.keyword_table),
92  (self._comment_table_names_comment_table_names, None)]:
93  for name in names:
94  yield name, table
95 
96  def start_table(self, header_row):
97  table = self._find_table_find_table(header_row)
98  if table is None or not self._table_is_allowed_table_is_allowed(table):
99  return None
100  table.set_header(header_row)
101  return table
102 
103  def has_preamble(self):
104  return len(self.preamblepreamblepreamblepreamble) > 0
105 
106  def add_preamble(self, row):
107  self._preamble_preamble.append(row)
108 
109  @property
110  preamble = property
111 
112  def preamble(self):
113  return self._preamble_preamble
114 
115  @preamble.setter
116 
117  def preamble(self, row):
118  self.add_preambleadd_preamble(row)
119 
120  def _find_table(self, header_row):
121  name = header_row[0] if header_row else ''
122  title = name.title()
123  if title not in self._tables_tables:
124  title = self._resolve_deprecated_table_resolve_deprecated_table(name)
125  if title is None:
126  self._report_unrecognized_table_report_unrecognized_table(name)
127  return None
128  return self._tables_tables[title]
129 
130  def _resolve_deprecated_table(self, used_name):
131  normalized = normalize(used_name)
132  for name in (self._setting_table_names_setting_table_names + self._variable_table_names_variable_table_names +
133  self._testcase_table_names_testcase_table_names + self._keyword_table_names_keyword_table_names +
134  self._comment_table_names_comment_table_names):
135  if normalize(name) == normalized:
136  self._report_deprecated_table_report_deprecated_table(used_name, name)
137  return name
138  return None
139 
140  def _report_deprecated_table(self, deprecated, name):
141  self.report_invalid_syntaxreport_invalid_syntax(
142  "Section name '%s' is deprecated. Use '%s' instead."
143  % (deprecated, name), level='WARN'
144  )
145 
146  def _report_unrecognized_table(self, name):
147  self.report_invalid_syntaxreport_invalid_syntax(
148  "Unrecognized table header '%s'. Available headers for data: "
149  "'Setting(s)', 'Variable(s)', 'Test Case(s)', 'Task(s)' and "
150  "'Keyword(s)'. Use 'Comment(s)' to embedded additional data."
151  % name
152  )
153 
154  def _table_is_allowed(self, table):
155  return True
156 
157  @property
158  name = property
159 
160  def name(self):
161  return self._format_name_format_name(self._get_basename_get_basename()) if self.sourcesource else None
162 
163  @property
164  rawname = property
165 
166  def rawname(self):
167  return self._get_basename_get_basename() if self.sourcesource else None
168  # To be used on resource prefixed suggestions
169 
170  def _get_basename(self):
171  return os.path.splitext(os.path.basename(self.sourcesource))[0]
172 
173  def _format_name(self, name):
174  name = self._strip_possible_prefix_from_name_strip_possible_prefix_from_name(name)
175  name = name.replace('_', ' ').strip()
176  return name.title() if name.islower() else name
177 
179  return name.split('__', 1)[-1]
180 
181  @property
182  keywords = property
183 
184  def keywords(self):
185  return self.keyword_table.keywords
186 
187  @property
188  imports = property
189 
190  def imports(self):
191  return self.setting_table.imports
192 
193  def report_invalid_syntax(self, message, level='ERROR'):
194  initfile = getattr(self, 'initfile', None)
195  path = os.path.join(self.sourcesource, initfile) if initfile else self.sourcesource
196  LOGGER.write("Error in file '%s': %s" % (path, message), level)
197 
198 
206  def save(self, **options):
207  return DataFileWriter(**options).write(self)
208 
209 
210 @py2to3
211 
217 
218  def __init__(self, parent=None, source=None, settings=None):
219  self.directorydirectory = os.path.dirname(source) if source else None
220  self.setting_tablesetting_table = TestCaseFileSettingTable(self)
221  self.variable_tablevariable_table = VariableTable(self)
222  self.testcase_tabletestcase_table = TestCaseTable(self)
223  self.keyword_tablekeyword_table = KeywordTable(self)
224  self._settings_settings = settings
225  self._tab_size_tab_size = self._settings_settings.get('txt number of spaces', 2) if self._settings_settings else 2
226  _TestData.__init__(self, parent, source)
227 
228  def populate(self):
229  FromFilePopulator(self, self._tab_size_tab_size).populate(self.sourcesource)
230  self._validate_validate()
231  return self
232 
233  def _validate(self):
234  if not self.testcase_tabletestcase_table.is_started():
235  # print(f"DEBUG: Model TestCaseFile _validate this is where there are no tests")
236  raise NoTestsFound('File has no tests or tasks.')
237 
238  def has_tests(self):
239  return True
240 
241  def __iter__(self):
242  for table in [self.setting_tablesetting_table, self.variable_tablevariable_table,
243  self.testcase_tabletestcase_table, self.keyword_tablekeyword_table]:
244  yield table
245 
246  def __nonzero__(self):
247  return any(table for table in self)
248 
249 
250 
255 
256  def __init__(self, source=None, settings=None):
257  self.directorydirectory = os.path.dirname(source) if source else None
258  self.setting_tablesetting_table = ResourceFileSettingTable(self)
259  self.variable_tablevariable_table = VariableTable(self)
260  self.testcase_tabletestcase_table = TestCaseTable(self)
261  self.keyword_tablekeyword_table = KeywordTable(self)
262  self._settings_settings = settings
263  self._tab_size_tab_size = self._settings_settings.get('txt number of spaces', 2) if self._settings_settings else 2
264  _TestData.__init__(self, source=source)
265 
266  def populate(self):
267  FromFilePopulator(self, self._tab_size_tab_size).populate(self.sourcesource, resource=True)
268  self._report_status_report_status()
269  return self
270 
271  def _report_status(self):
272  if self.setting_tablesetting_table or self.variable_tablevariable_table or self.keyword_tablekeyword_table:
273  LOGGER.info("Imported resource file '%s' (%d keywords)."
274  % (self.sourcesource, len(self.keyword_tablekeyword_table.keywords)))
275  else:
276  LOGGER.warn("Imported resource file '%s' is empty." % self.sourcesource)
277 
278  def _table_is_allowed(self, table):
279  if table is self.testcase_tabletestcase_table:
280  raise DataError("Resource file '%s' cannot contain tests or "
281  "tasks." % self.sourcesource)
282  return True
283 
284  def __iter__(self):
285  for table in [self.setting_tablesetting_table, self.variable_tablevariable_table, self.keyword_tablekeyword_table]:
286  yield table
287 
288 
289 
297 
298  def __init__(self, parent=None, source=None, settings=None):
299  self.directorydirectory = source
300  self.initfileinitfile = None
301  self.setting_tablesetting_table = InitFileSettingTable(self)
302  self.variable_tablevariable_table = VariableTable(self)
303  self.testcase_tabletestcase_table = TestCaseTable(self)
304  self.keyword_tablekeyword_table = KeywordTable(self)
305  self._settings_settings = settings
306  self._tab_size_tab_size = self._settings_settings.get('txt number of spaces', 2) if self._settings_settings else 2
307  _TestData.__init__(self, parent, source)
308 
309  def populate(self, include_suites=None, extensions=None, recurse=True):
310  FromDirectoryPopulator().populate(self.sourcesource, self, include_suites,
311  extensions, recurse, self._tab_size_tab_size)
312  self.childrenchildrenchildren = [ch for ch in self.childrenchildrenchildren if ch.has_tests()]
313  return self
314 
315  def _get_basename(self):
316  return os.path.basename(self.sourcesource)
317 
318  def _table_is_allowed(self, table):
319  if table is self.testcase_tabletestcase_table:
320  LOGGER.error("Test suite initialization file in '%s' cannot "
321  "contain tests or tasks." % self.sourcesource)
322  return False
323  return True
324 
325  def add_child(self, path, include_suites, extensions=None):
326  self.childrenchildrenchildren.append(TestData(parent=self,
327  source=path,
328  include_suites=include_suites,
329  extensions=extensions, settings=self._settings_settings))
330 
331  def has_tests(self):
332  return any(ch.has_tests() for ch in self.childrenchildrenchildren)
333 
334  def __iter__(self):
335  for table in [self.setting_tablesetting_table, self.variable_tablevariable_table, self.keyword_tablekeyword_table]:
336  yield table
337 
338 
339 @py2to3
340 class _Table():
341 
342  def __init__(self, parent):
343  self.parentparent = parent
344  self._header_header = None
345 
346  def set_header(self, header):
347  self._header_header = self._prune_old_style_headers_prune_old_style_headers(header)
348 
349  def _prune_old_style_headers(self, header):
350  if len(header) < 3:
351  return header
352  if self._old_header_matcher.match(header):
353  return [header[0]]
354  return header
355 
356  @property
357  header = property
358 
359  def header(self):
360  return self._header_header or [self.type.title() + 's']
361 
362  @property
363  name = property
364 
365  def name(self):
366  return self.headerheaderheader[0]
367 
368  @property
369  source = property
370 
371  def source(self):
372  return self.parentparent.source
373 
374  @property
375  directory = property
376 
377  def directory(self):
378  return self.parentparent.directory
379 
380  def report_invalid_syntax(self, message, level='ERROR'):
381  self.parentparent.report_invalid_syntax(message, level)
382 
383  def __nonzero__(self):
384  return bool(self._header_header or len(self))
385 
386  def __len__(self):
387  return sum(1 for item in self)
388 
389 
391 
394  _setters = {}
395 
398  _aliases = {}
399 
400  def get_setter(self, name):
401  if name[-1:] == ':':
402  name = name[:-1]
403  setter = self._get_setter_get_setter(name)
404  if setter is not None:
405  return setter
406  setter = self._get_deprecated_setter_get_deprecated_setter(name)
407  if setter is not None:
408  return setter
409  self.report_invalid_syntaxreport_invalid_syntax("Non-existing setting '%s'." % name)
410  return None
411 
412  def _get_setter(self, name):
413  title = name.title()
414  if title in self._aliases_aliases:
415  title = self._aliases_aliases[name]
416  if title in self._setters_setters:
417  return self._setters_setters[title](self)
418  return None
419 
420  def _get_deprecated_setter(self, name):
421  normalized = normalize(name)
422  for setting in list(self._setters_setters) + list(self._aliases_aliases):
423  if normalize(setting) == normalized:
424  self._report_deprecated_setting_report_deprecated_setting(name, setting)
425  return self._get_setter_get_setter(setting)
426  return None
427 
428  def _report_deprecated_setting(self, deprecated, correct):
429  self.report_invalid_syntaxreport_invalid_syntax(
430  "Setting '%s' is deprecated. Use '%s' instead."
431  % (deprecated, correct), level='WARN'
432  )
433 
434  def report_invalid_syntax(self, message, level='ERROR'):
435  raise NotImplementedError
436 
437 
439  type = 'setting'
440 
441  def __init__(self, parent):
442  _Table.__init__(self, parent)
443  self.docdoc = Documentation('Documentation', self)
444  self.suite_setupsuite_setup = Fixture('Suite Setup', self)
445  self.suite_teardownsuite_teardown = Fixture('Suite Teardown', self)
446  self.test_setuptest_setup = Fixture('Test Setup', self)
447  self.test_teardowntest_teardown = Fixture('Test Teardown', self)
448  self.force_tagsforce_tags = Tags('Force Tags', self)
449  self.default_tagsdefault_tags = Tags('Default Tags', self)
450  self.test_templatetest_template = Template('Test Template', self)
451  self.test_timeouttest_timeout = Timeout('Test Timeout', self)
452  self.metadatametadata = MetadataList(self)
453  self.importsimports = ImportList(self)
454 
455  @property
456  _old_header_matcher = property
457 
460 
461  def add_metadata(self, name, value='', comment=None):
462  self.metadatametadata.add(Metadata(self, name, value, comment))
463  return self.metadatametadata[-1]
464 
465  def add_library(self, name, args=None, comment=None):
466  self.importsimports.add(Library(self, name, args, comment=comment))
467  return self.importsimports[-1]
468 
469  def add_resource(self, name, invalid_args=None, comment=None):
470  self.importsimports.add(Resource(self, name, invalid_args, comment=comment))
471  return self.importsimports[-1]
472 
473  def add_variables(self, name, args=None, comment=None):
474  self.importsimports.add(Variables(self, name, args, comment=comment))
475  return self.importsimports[-1]
476 
477  def __len__(self):
478  return sum(1 for setting in self if setting.is_set())
479 
480 
482 
485  _setters = {'Documentation': lambda s: s.doc.populate,
486  'Suite Setup': lambda s: s.suite_setup.populate,
487  'Suite Teardown': lambda s: s.suite_teardown.populate,
488  'Test Setup': lambda s: s.test_setup.populate,
489  'Test Teardown': lambda s: s.test_teardown.populate,
490  'Force Tags': lambda s: s.force_tags.populate,
491  'Default Tags': lambda s: s.default_tags.populate,
492  'Test Template': lambda s: s.test_template.populate,
493  'Test Timeout': lambda s: s.test_timeout.populate,
494  'Library': lambda s: s.imports.populate_library,
495  'Resource': lambda s: s.imports.populate_resource,
496  'Variables': lambda s: s.imports.populate_variables,
497  'Metadata': lambda s: s.metadata.populate}
498 
501  _aliases = {'Task Setup': 'Test Setup',
502  'Task Teardown': 'Test Teardown',
503  'Task Template': 'Test Template',
504  'Task Timeout': 'Test Timeout'}
505 
506  def __iter__(self):
507  for setting in [self.docdoc, self.suite_setupsuite_setup, self.suite_teardownsuite_teardown,
508  self.test_setuptest_setup, self.test_teardowntest_teardown, self.force_tagsforce_tags,
509  self.default_tagsdefault_tags, self.test_templatetest_template, self.test_timeouttest_timeout] \
510  + self.metadatametadata.data + self.importsimports.data:
511  yield setting
512 
513 
515 
518  _setters = {'Documentation': lambda s: s.doc.populate,
519  'Library': lambda s: s.imports.populate_library,
520  'Resource': lambda s: s.imports.populate_resource,
521  'Variables': lambda s: s.imports.populate_variables}
522 
523  def __iter__(self):
524  for setting in [self.docdoc] + self.importsimports.data:
525  yield setting
526 
527 
529 
532  _setters = {'Documentation': lambda s: s.doc.populate,
533  'Suite Setup': lambda s: s.suite_setup.populate,
534  'Suite Teardown': lambda s: s.suite_teardown.populate,
535  'Test Setup': lambda s: s.test_setup.populate,
536  'Test Teardown': lambda s: s.test_teardown.populate,
537  'Test Timeout': lambda s: s.test_timeout.populate,
538  'Force Tags': lambda s: s.force_tags.populate,
539  'Library': lambda s: s.imports.populate_library,
540  'Resource': lambda s: s.imports.populate_resource,
541  'Variables': lambda s: s.imports.populate_variables,
542  'Metadata': lambda s: s.metadata.populate}
543 
544  def __iter__(self):
545  for setting in [self.docdoc, self.suite_setupsuite_setup, self.suite_teardownsuite_teardown,
546  self.test_setuptest_setup, self.test_teardowntest_teardown, self.force_tagsforce_tags,
547  self.test_timeouttest_timeout] + self.metadatametadata.data + self.importsimports.data:
548  yield setting
549 
550 
552  type = 'variable'
553 
554  def __init__(self, parent):
555  _Table.__init__(self, parent)
556  self.variablesvariables = []
557 
558  @property
559  _old_header_matcher = property
560 
563 
564  def add(self, name, value, comment=None):
565  self.variablesvariables.append(Variable(self, name, value, comment))
566 
567  def __iter__(self):
568  return iter(self.variablesvariables)
569 
570 
572  type = 'test case'
573 
574  def __init__(self, parent):
575  _Table.__init__(self, parent)
576  self.teststests = []
577 
578  def set_header(self, header):
579  if self._header_header and header:
580  self._validate_mode_validate_mode(self._header_header[0], header[0])
581  _Table.set_header(self, header)
582 
583  def _validate_mode(self, name1, name2):
584  tasks1 = normalize(name1) in ('task', 'tasks')
585  tasks2 = normalize(name2) in ('task', 'tasks')
586  if tasks1 is not tasks2:
587  raise DataError('One file cannot have both tests and tasks.')
588 
589  @property
590  _old_header_matcher = property
591 
594 
595  def add(self, name):
596  self.teststests.append(TestCase(self, name))
597  return self.teststests[-1]
598 
599  def __iter__(self):
600  return iter(self.teststests)
601 
602  def is_started(self):
603  return bool(self._header_header)
604 
605 
607  type = 'keyword'
608 
609  def __init__(self, parent):
610  _Table.__init__(self, parent)
611  self.keywordskeywords = []
612 
613  @property
614  _old_header_matcher = property
615 
618 
619  def add(self, name):
620  self.keywordskeywords.append(UserKeyword(self, name))
621  return self.keywordskeywords[-1]
622 
623  def __iter__(self):
624  return iter(self.keywordskeywords)
625 
626 
627 @py2to3
628 class Variable():
629 
630  def __init__(self, parent, name, value, comment=None):
631  self.parentparent = parent
632  self.namename = name.rstrip('= ')
633  if name.startswith('$') and value == []:
634  value = ''
635  if is_string(value):
636  value = [value]
637  self.valuevalue = value
638  self.commentcomment = Comment(comment)
639 
640  def as_list(self):
641  if self.has_datahas_data():
642  return [self.namename] + self.valuevalue + self.commentcomment.as_list()
643  return self.commentcomment.as_list()
644 
645  def is_set(self):
646  return True
647 
648  def is_for_loop(self):
649  return False
650 
651  def has_data(self):
652  return bool(self.namename or ''.join(self.valuevalue))
653 
654  def __nonzero__(self):
655  return self.has_datahas_data()
656 
657  def report_invalid_syntax(self, message, level='ERROR'):
658  self.parentparent.report_invalid_syntax("Setting variable '%s' failed: %s"
659  % (self.namename, message), level)
660 
661 
662 class _WithSteps():
663 
664  def add_step(self, content, comment=None):
665  # print(f"DEBUG: model.py Enter _WithSteps content={content[:]} comment={comment}")
666  self.steps.append(Step(content, comment))
667  return self.steps[-1]
668 
669  def copy(self, name):
670  new = copy.deepcopy(self)
671  new.name = name
672  self._add_to_parent(new)
673  return new
674 
675 
677 
678  def __init__(self, parent, name):
679  self.parentparent = parent
680  self.namename = name
681  self.docdoc = Documentation('[Documentation]', self)
682  self.templatetemplate = Template('[Template]', self)
683  self.tagstags = Tags('[Tags]', self)
684  self.setupsetup = Fixture('[Setup]', self)
685  self.teardownteardown = Fixture('[Teardown]', self)
686  self.timeouttimeout = Timeout('[Timeout]', self)
687  self.stepssteps = []
688  if name == '...':
689  self.report_invalid_syntaxreport_invalid_syntaxreport_invalid_syntax(
690  "Using '...' as test case name is deprecated. It will be "
691  "considered line continuation in Robot Framework 3.2.",
692  level='WARN'
693  )
694 
695 
698  _setters = {'Documentation': lambda s: s.doc.populate,
699  'Template': lambda s: s.template.populate,
700  'Setup': lambda s: s.setup.populate,
701  'Teardown': lambda s: s.teardown.populate,
702  'Tags': lambda s: s.tags.populate,
703  'Timeout': lambda s: s.timeout.populate}
704 
705  @property
706  source = property
707 
708  def source(self):
709  return self.parentparent.source
710 
711  @property
712  directory = property
713 
714  def directory(self):
715  return self.parentparent.directory
716 
717  def add_for_loop(self, declaration, comment=None):
718  # DEBUG
719  #self.steps.append(ForLoop(self, ['FOR'] + declaration, comment=comment))
720  # print(f"DEBUG: Model add_for_loop init={['FOR'] + declaration} comment:{comment}")
721  self.stepssteps.append(Step(['FOR'] + declaration, comment))
722  # : Model add_for_loop return steps:{self.steps[-1].as_list()} comment:{comment}")
723  return self.stepssteps[-1]
724 
725  def end_for_loop(self):
726  loop, steps = self._find_last_empty_for_and_steps_after_find_last_empty_for_and_steps_after()
727  if not loop:
728  return False
729  loop.steps.extend(steps)
730  self.stepssteps[-len(steps):] = []
731  return True
732 
734  steps = []
735  for step in reversed(self.stepssteps):
736  if isinstance(step, ForLoop):
737  if not step.steps:
738  steps.reverse()
739  return step, steps
740  break
741  steps.append(step)
742  return None, []
743 
744  def report_invalid_syntax(self, message, level='ERROR'):
745  type_ = 'test case' if type(self) is TestCase else 'keyword'
746  message = "Invalid syntax in %s '%s': %s" % (type_, self.namename, message)
747  self.parentparent.report_invalid_syntax(message, level)
748 
749  def _add_to_parent(self, test):
750  self.parentparent.tests.append(test)
751 
752  @property
753  settings = property
754 
755  def settings(self):
756  return [self.docdoc, self.tagstags, self.setupsetup, self.templatetemplate, self.timeouttimeout,
757  self.teardownteardown]
758 
759  def __iter__(self):
760  for element in [self.docdoc, self.tagstags, self.setupsetup,
761  self.templatetemplate, self.timeouttimeout] \
762  + self.stepssteps + [self.teardownteardown]:
763  yield element
764 
765 
767 
768  def __init__(self, parent, name):
769  self.parentparentparent = parent
770  self.namenamename = name
771  self.docdocdoc = Documentation('[Documentation]', self)
772  self.argsargs = Arguments('[Arguments]', self)
773  self.return_return_ = Return('[Return]', self)
774  self.timeouttimeouttimeout = Timeout('[Timeout]', self)
775  self.teardownteardownteardown = Fixture('[Teardown]', self)
776  self.tagstagstags = Tags('[Tags]', self)
777  self.stepsstepssteps = []
778  if name == '...':
779  self.report_invalid_syntaxreport_invalid_syntaxreport_invalid_syntax(
780  "Using '...' as keyword name is deprecated. It will be "
781  "considered line continuation in Robot Framework 3.2.",
782  level='WARN'
783  )
784 
785 
788  _setters = {'Documentation': lambda s: s.doc.populate,
789  'Arguments': lambda s: s.args.populate,
790  'Return': lambda s: s.return_.populate,
791  'Timeout': lambda s: s.timeout.populate,
792  'Teardown': lambda s: s.teardown.populate,
793  'Tags': lambda s: s.tags.populate}
794 
795  def _add_to_parent(self, test):
796  self.parentparentparent.keywords.append(test)
797 
798  @property
799  settings = property
800 
801  def settings(self):
802  return [self.argsargs, self.docdocdoc, self.tagstagstags, self.timeouttimeouttimeout, self.teardownteardownteardown, self.return_return_]
803 
804  def __iter__(self):
805  for element in [self.argsargs, self.docdocdoc, self.tagstagstags, self.timeouttimeouttimeout] \
806  + self.stepsstepssteps + [self.teardownteardownteardown, self.return_return_]:
807  yield element
808 
809 
810 
823  flavors = {'IN', 'IN RANGE', 'IN ZIP', 'IN ENUMERATE'}
824  normalized_flavors = NormalizedDict((f, f) for f in flavors)
825  inner_kw_pos = None
826 
827  def __init__(self, parent, declaration, indentation=[], comment=None):
828  self.parentparent = parent
829  self.indentindent = indentation if isinstance(indentation, list) else [indentation]
830  isize = idx = 0
831  print(f"\nDEBUG: ForLoop init ENTER declaration={declaration[:]}")
832  if declaration[0] == '':
833  declaration.pop(0)
834  for idx in range(0, len(declaration)):
835  if declaration[idx] == '':
836  if idx >= 0:
837  isize = self.increase_indentincrease_indent()
838  else:
839  self.first_kwfirst_kw = declaration[idx]
840  break
841  self.inner_kw_posinner_kw_pos = idx
842  print(f"\nDEBUG: ForLoop init indent {isize} self.inner_kw_pos={self.inner_kw_pos}\ndeclaration={declaration[:]}")
843  # compensation for double FOR
844  if declaration[self.inner_kw_posinner_kw_pos+1] == declaration[self.inner_kw_posinner_kw_pos] == 'FOR':
845  declaration.pop(self.inner_kw_posinner_kw_pos+1)
846  self.flavor, index = self._get_flavor_and_index_get_flavor_and_index(declaration)
847  self.varsvars = declaration[self.inner_kw_posinner_kw_pos+1:index]
848  self.itemsitems = declaration[index+1:]
849  self.commentcomment = Comment(comment)
850  self.stepssteps = []
851  self.argsargs = []
852 
853  def _get_flavor_and_index(self, declaration):
854  for index, item in enumerate(declaration):
855  if item in self.flavorsflavors:
856  return item, index
857  if item in self.normalized_flavorsnormalized_flavors:
858  correct = self.normalized_flavorsnormalized_flavors[item]
859  self._report_deprecated_flavor_syntax_report_deprecated_flavor_syntax(item, correct)
860  return correct, index
861  if normalize(item).startswith('in'):
862  return item.upper(), index
863  return 'IN', len(declaration)
864 
865  def _report_deprecated_flavor_syntax(self, deprecated, correct):
866  self.parentparent.report_invalid_syntax(
867  "Using '%s' as a FOR loop separator is deprecated. "
868  "Use '%s' instead." % (deprecated, correct), level='WARN'
869  )
870 
871  def is_comment(self):
872  return False
873 
874  def is_for_loop(self):
875  return True
876 
877  def as_list(self, indent=True, include_comment=True):
878  comments = self.commentcomment.as_list() if include_comment else []
879  # print(f"DEBUG: Model ForLoop as_list: indent={self.indent[:]} self.first_kw={self.first_kw}\n"
880  # f"{self.vars} + {self.flavor} + {self.items} + {comments}")
881  return self.indentindent + [self.first_kwfirst_kw] + self.varsvars + [self.flavor] + self.itemsitems + comments
882 
883  def __iter__(self):
884  return iter(self.stepssteps)
885 
886  def is_set(self):
887  return True
888 
889  def increase_indent(self):
890  self.indentindent.append('')
891  return len(self.indentindent)
892 
893  def decrease_indent(self):
894  self.indentindent = self.indentindent[:-1] if len(self.indentindent) > 0 else []
895  return len(self.indentindent)
896 
897 
898 class Step():
899 
900  inner_kw_pos = None
901  name = None
902  indent = []
903  assign = []
904  args = []
905  comment = []
906  normal_assign = None
907  cells = []
908 
909  def __init__(self, content, comment=None):
910  # print(f"DEBUG: RFLib Model enter init Step: ENTER cell content={content} comment={comment}")
911  if isinstance(content, Step):
912  size = len(content)
913  self.cellscellscells = content.cells # .as_list()
914  elif isinstance(content, list):
915  size = len(content)
916  self.cellscellscells = content
917  else:
918  size = len(self.cellscellscells) # size = len(self)
919  # cells = self.as_list()
920  if comment:
921  if isinstance(comment, list):
922  self.cellscellscells.extend(comment)
923  elif isinstance(comment, str):
924  self.cellscellscells.append(comment)
925  # if self.cells:
926  # print(f"DEBUG: model _first_non_empty_cell: {self.cells}")
927  index = self.first_non_empty_cellfirst_non_empty_cell(content) # Called first to set self.inner_kw_pos
928  self.inner_kw_posinner_kw_pos = index
929  self.normal_assignnormal_assign = None
930  self.assignassignassign = self._get_assign_get_assign() # self._get_assign(content)
931  # print(f"DEBUG: RFLib Model enter init Step: 1st cell content={content} comment={comment} index={index}"
932  # f" assign={self.assign} self.normal_assign={self.normal_assign}")
933  self.indentindentindent = []
934  self.argsargsargs = []
935  self.namename = None
936  self.commentcommentcomment = Comment(comment)
937  if index < 0: # This is redundant because index is >= 0
938  return
939  for _ in range(0, index):
940  self.indentindentindent.append('')
941  # print(f"DEBUG: RFLib Model init Step: index={index} inner_kw_pos = {self.inner_kw_pos} indent={self.indent[:]} \ncontent {content}")
942  self.argsargsargs = content[index + 1:] if content and index <= len(content) - 1 else []
943  # print(f"DEBUG: RFLib Model init Step: 1st cell len(content)={len(content)} index {index} indent={self.indent[:]}") # 1st cell: {content[index]}")
944  # TODO: Create setters for Step.name and Step.args, see stepcontrollers.py replace_keyword
945  if index < len(content):
946  self.namename = content[index] if content else None
947  else:
948  self.namename = None
949  # if self.assign:
950  # print(f"DEBUG RFLib init Step: self.assign {self.assign}")
951 
952  @staticmethod
953  def is_kind_of_comment(content):
954  return content.lower() in ['comment', 'builtin.comment'] or content.startswith('#')
955 
956  def _get_assign(self):
957  assign = []
958  idx = 0
959  positional = True
960  cells = self.cellscellscells.copy()
961  if cells and cells != ['']:
962  index = self.inner_kw_posinner_kw_pos # DEBUG avoiding calling self.first_non_empty_cell(content)
963  if index < len(cells) and is_var(cells[index].rstrip('=')):
964  self.normal_assignnormal_assign = True
965  if 0 <= index < len(cells) and self.is_kind_of_commentis_kind_of_comment(cells[index]): # Special case for commented content
966  return []
967  # print(f"DEBUG: RFLib Model _get_assign VAR NORMAL (index={index}) inner_kw_pos={self.inner_kw_pos} content={content[:]}")
968  # first handle non FOR cases
969  idx = 0
970  try:
971  if cells[self.inner_kw_posinner_kw_pos] != 'FOR':
972  while idx < len(cells):
973  if is_var(cells[idx].rstrip('=')):
974  assign.append(cells.pop(idx))
975  # if idx < self.inner_kw_pos:
976  idx -= 1
977  else:
978  break
979  idx += 1
980  # print(f"DEBUG: RFLib Model _get_assign RETURN assign={assign} size of content={len(content)}")
981  return assign
982  except IndexError:
983  pass
984  idx = index
985  while idx < len(cells) and positional:
986  if idx <= self.inner_kw_posinner_kw_pos:
987  positional = True
988  else:
989  positional = False
990  if not positional and self.inner_kw_posinner_kw_pos < idx <= self.inner_kw_posinner_kw_pos + 3 < len(cells) and cells[self.inner_kw_posinner_kw_pos] == 'FOR':
991  # print(f"DEBUG: RFLib Model _get_assign idx={idx} +1{self.inner_kw_pos + 1}:{idx+1} +2{self.inner_kw_pos + 2}:{idx+2}"
992  # f"FOR content1={content[self.inner_kw_pos + 1]}"
993  # f" content2={content[self.inner_kw_pos + 2]} size of content={len(content)}")
994  if idx + 2 < len(cells): # idx < self.inner_kw_pos + 3 and
995  # print(f"DEBUG: RFLib Model _get_assign FOR idx={idx} second IN ENUMERATE"
996  # f" content[idx + 1]={content[idx + 1]} content[idx + 2]={content[idx + 2]}")
997  if cells[idx + 1] == 'IN ENUMERATE' or cells[idx + 2] == 'IN ENUMERATE':
998  positional = True
999  self.normal_assignnormal_assign = False
1000  # print(f"DEBUG: RFLib Model _get_assign FOR idx={idx} second IN ENUMERATE"
1001  # f" size of content={len(content)} VALUE={content[idx]}")
1002  if idx == self.inner_kw_posinner_kw_pos + 1:
1003  positional = True
1004  self.normal_assignnormal_assign = False
1005  # print(f"DEBUG: RFLib Model _get_assign FOR idx={idx} first loop var")
1006  # else:
1007  # positional = False
1008  if not positional and self.inner_kw_posinner_kw_pos < idx <= self.inner_kw_posinner_kw_pos + 1 < len(cells) and re_set_var.match(cells[self.inner_kw_posinner_kw_pos]):
1009  positional = True
1010  self.normal_assignnormal_assign = False
1011  if is_var(cells[idx].rstrip('=')) and positional: # and self.normal_assign:
1012  assign.append(cells.pop(idx))
1013  idx -= 1 # We need to recheck var in case of IN ENUMERATE
1014  idx += 1
1015  # print(f"DEBUG: RFLib Model _get_assign idx={idx} size of content={len(content)}")
1016  return assign
1017 
1018  def is_comment(self):
1019  return self.namename.lower() == 'comment' or not (self.assignassignassign or self.namename or self.argsargsargs)
1020 
1021  def is_for_loop(self):
1022  # TODO: remove steps ForLoop: return self.name == 'FOR'
1023  return False
1024 
1025  def is_set(self):
1026  return True
1027 
1028 
1035  def as_list(self, indent=False, include_comment=True):
1036  # print(f"DEBUG RFLib Model Step: as_list() ENTER assign={self.assign}\n"
1037  # f"cells={self.cells[:]}")
1038  if indent:
1039  return [''] + self.cellscellscells[:]
1040  return self.cellscellscells[:]
1041 
1042  def first_non_empty_cell(self, content=None):
1043  # print(f"DEBUG: model enter _first_non_empty_cell")
1044  size = len(self.cellscellscells)
1045  index = 0
1046  while index < size and self.cellscellscells[index] == '':
1047  index += 1
1048  return index if 0 <= index < size else index - 1 if index - 1 > 0 else 0
1049 
1050  def first_empty_cell(self):
1051  index = self.inner_kw_posinner_kw_pos
1052  if index > 0:
1053  return index - 1
1054  return None
1055 
1056  def increase_indent(self):
1057  self.indentindentindent.append('')
1058  self.cellscellscells.insert(0, '')
1059  return len(self.indentindentindent)
1060 
1061  def decrease_indent(self):
1062  self.indentindentindent = self.indentindentindent[:-1] if len(self.indentindentindent) > 0 else []
1063  self.cellscellscells = self.cellscellscells[1:] if len(self.cellscellscells) >= 1 and self.cellscellscells[0] == '' else self.cellscellscells
1064  return len(self.indentindentindent)
1065 
1066  def add_step(self, content, comment=None):
1067  self.__init____init__(content, comment)
1068  return self
1069 
1070  def __len__(self):
1071  kw = [self.namename] if self.namename is not None and self.namename[0] != '#' else []
1072  cells_len = len(self.cellscellscells)
1073  if self.namename == 'FOR':
1074  seglen = len(self.indentindentindent) + len(kw) + len(self.argsargsargs) + len(self.commentcommentcomment)
1075  else:
1076  seglen = len(self.indentindentindent) + len(self.assignassignassign) + len(kw) + len(self.argsargsargs) + len(self.commentcommentcomment)
1077  # Compensation for args==comment
1078  if self.argsargsargs and self.commentcommentcomment and self.argsargsargs[-1] == self.commentcommentcomment.as_list()[-1]:
1079  seglen -= 1 # len(self.args[:-1])
1080  elif len(self.commentcommentcomment) > 1 and self.argsargsargs == self.commentcommentcomment.as_list()[1:]:
1081  seglen -= len(self.commentcommentcomment)
1082  # Compensation for assign==kw
1083  if self.assignassignassign and kw and self.assignassignassign[0] == kw[0]:
1084  seglen -= len(self.assignassignassign) # len assign because assign may also be in args
1085  # Compensation for kw==comment
1086  if kw and self.commentcommentcomment and kw == self.commentcommentcomment.as_list():
1087  seglen -= 1
1088  # print(f"DEBUG RFLib Model Step: len indent={len(self.indent)} assign= {len(self.assign)}\n"
1089  # f" assign= {self.assign[:]} kw={len(kw)} KW={kw}"
1090  # f" args={len(self.args)} \n args={self.args[:]} commt={len(self.comment)}"
1091  # f" comment={self.comment.as_list()}")
1092  # print(f"DEBUG RFLib Model Step: len computed={seglen} cells_len={cells_len} cells={self.cells}")
1093  # assert seglen == cells_len
1094  return cells_len
1095 
1096 
1098 
1099  def match(self, header):
1100  return all(value.lower() == 'value' for value in header[1:])
1101 
1102 
1104 
1105  def match(self, header):
1106  if header[1].lower() != 'action':
1107  return False
1108  for arg in header[2:]:
1109  if not arg.lower().startswith('arg'):
1110  return False
1111  return True
Used when variable does not exist.
Definition: errors.py:67
The parsed representation of a for-loop.
Definition: model.py:822
def _get_flavor_and_index(self, declaration)
Definition: model.py:853
def _report_deprecated_flavor_syntax(self, deprecated, correct)
Definition: model.py:865
def as_list(self, indent=True, include_comment=True)
Definition: model.py:877
def __init__(self, parent, declaration, indentation=[], comment=None)
Definition: model.py:827
The parsed resource file object.
Definition: model.py:254
def __init__(self, source=None, settings=None)
Definition: model.py:256
def as_list(self, indent=False, include_comment=True)
import inspect stack = inspect.stack() the_class = stack[1][0].f_locals["self"].__class__....
Definition: model.py:1035
def __init__(self, content, comment=None)
Definition: model.py:909
def add_step(self, content, comment=None)
Definition: model.py:1066
def first_non_empty_cell(self, content=None)
Definition: model.py:1042
The parsed test case file object.
Definition: model.py:216
def __init__(self, parent=None, source=None, settings=None)
Definition: model.py:218
def _validate_mode(self, name1, name2)
Definition: model.py:583
def report_invalid_syntax(self, message, level='ERROR')
Definition: model.py:744
def add_for_loop(self, declaration, comment=None)
Definition: model.py:717
def __init__(self, parent, name)
Definition: model.py:678
The parsed test data directory object.
Definition: model.py:296
def __init__(self, parent=None, source=None, settings=None)
Definition: model.py:298
def add_child(self, path, include_suites, extensions=None)
Definition: model.py:325
def populate(self, include_suites=None, extensions=None, recurse=True)
Definition: model.py:309
def __init__(self, parent, name)
Definition: model.py:768
def add(self, name, value, comment=None)
Definition: model.py:564
def report_invalid_syntax(self, message, level='ERROR')
Definition: model.py:657
def __init__(self, parent, name, value, comment=None)
Definition: model.py:630
def add_metadata(self, name, value='', comment=None)
Definition: model.py:461
def add_library(self, name, args=None, comment=None)
Definition: model.py:465
def add_resource(self, name, invalid_args=None, comment=None)
Definition: model.py:469
def add_variables(self, name, args=None, comment=None)
Definition: model.py:473
def report_invalid_syntax(self, message, level='ERROR')
Definition: model.py:380
def _prune_old_style_headers(self, header)
Definition: model.py:349
def _strip_possible_prefix_from_name(self, name)
Definition: model.py:178
def start_table(self, header_row)
Definition: model.py:96
def __init__(self, parent=None, source=None)
Definition: model.py:80
def _find_table(self, header_row)
Definition: model.py:120
def _report_deprecated_table(self, deprecated, name)
Definition: model.py:140
def report_invalid_syntax(self, message, level='ERROR')
Definition: model.py:193
def _resolve_deprecated_table(self, used_name)
Definition: model.py:130
def _report_unrecognized_table(self, name)
Definition: model.py:146
def save(self, **options)
Writes this datafile to disk.
Definition: model.py:206
def report_invalid_syntax(self, message, level='ERROR')
Definition: model.py:434
def _report_deprecated_setting(self, deprecated, correct)
Definition: model.py:428
def add_step(self, content, comment=None)
Definition: model.py:664
def write(msg, level='INFO', html=False)
Writes the message to the log file using the given level.
Definition: logger.py:86
def TestData(parent=None, source=None, include_suites=None, warn_on_skipped='DEPRECATED', extensions=None, settings=None)
Parses a file or directory to a corresponding model object.
Definition: model.py:47
def normalize(string, ignore=(), caseless=True, spaceless=True)
Normalizes given string according to given spec.
Definition: normalizing.py:30
def abspath(path, case_normalize=False)
Replacement for os.path.abspath with some enhancements and bug fixes.
Definition: robotpath.py:87
def is_var(string, identifiers='$@&')
Definition: isvar.py:22