From aac2d6140c85635d4744934e3f8486bccec633e3 Mon Sep 17 00:00:00 2001 From: gachteme Date: Mon, 24 Sep 2018 16:11:28 -0400 Subject: [PATCH 01/16] changed re to regex module --- clitable.py | 4 ++-- clitable_test.py | 8 +++---- copyable_regex_object.py | 6 ++--- terminal.py | 12 +++++----- textfsm.py | 52 ++++++++++++++++++++-------------------- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/clitable.py b/clitable.py index fa0125b..9a3fe1f 100755 --- a/clitable.py +++ b/clitable.py @@ -27,7 +27,7 @@ import copy import os -import re +import regex import threading import copyable_regex_object import textfsm @@ -314,7 +314,7 @@ def _ParseCmdItem(self, cmd_input, template_file=None): def _PreParse(self, key, value): """Executed against each field of each row read from index table.""" if key == 'Command': - return re.sub(r'(\[\[.+?\]\])', self._Completion, value) + return regex.sub(r'(\[\[.+?\]\])', self._Completion, value) else: return value diff --git a/clitable_test.py b/clitable_test.py index fabb62b..d6efffc 100755 --- a/clitable_test.py +++ b/clitable_test.py @@ -22,7 +22,7 @@ import copy import os -import re +import regex import unittest import clitable @@ -104,11 +104,11 @@ def setUp(self): def testCompletion(self): """Tests '[[]]' syntax replacement.""" indx = clitable.CliTable() - self.assertEqual('abc', re.sub(r'(\[\[.+?\]\])', indx._Completion, 'abc')) + self.assertEqual('abc', regex.sub(r'(\[\[.+?\]\])', indx._Completion, 'abc')) self.assertEqual('a(b(c)?)?', - re.sub(r'(\[\[.+?\]\])', indx._Completion, 'a[[bc]]')) + regex.sub(r'(\[\[.+?\]\])', indx._Completion, 'a[[bc]]')) self.assertEqual('a(b(c)?)? de(f)?', - re.sub(r'(\[\[.+?\]\])', indx._Completion, + regex.sub(r'(\[\[.+?\]\])', indx._Completion, 'a[[bc]] de[[f]]')) def testRepeatRead(self): diff --git a/copyable_regex_object.py b/copyable_regex_object.py index 00cb442..237337d 100755 --- a/copyable_regex_object.py +++ b/copyable_regex_object.py @@ -16,16 +16,16 @@ """Work around a regression in Python 2.6 that makes RegexObjects uncopyable.""" -import re +import regex class CopyableRegexObject(object): - """Like a re.RegexObject, but can be copied.""" + """Like a regex.RegexObject, but can be copied.""" # pylint: disable=C6409 def __init__(self, pattern): self.pattern = pattern - self.regex = re.compile(pattern) + self.regex = regex.compile(pattern) def match(self, *args, **kwargs): return self.regex.match(*args, **kwargs) diff --git a/terminal.py b/terminal.py index 3a39c63..9d07941 100755 --- a/terminal.py +++ b/terminal.py @@ -28,7 +28,7 @@ import fcntl import getopt import os -import re +import regex import struct import sys import termios @@ -100,7 +100,7 @@ ANSI_END = '\002' -sgr_re = re.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % ( +sgr_re = regex.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % ( ANSI_START, ANSI_END)) @@ -159,12 +159,12 @@ def AnsiText(text, command_list=None, reset=True): def StripAnsiText(text): """Strip ANSI/SGR escape sequences from text.""" - return sgr_re.sub('', text) + return sgr_regex.sub('', text) def EncloseAnsiText(text): """Enclose ANSI/SGR escape sequences with ANSI_START and ANSI_END.""" - return sgr_re.sub(lambda x: ANSI_START + x.group(1) + ANSI_END, text) + return sgr_regex.sub(lambda x: ANSI_START + x.group(1) + ANSI_END, text) def TerminalSize(): @@ -195,7 +195,7 @@ def LineWrap(text, omit_sgr=False): def _SplitWithSgr(text_line): """Tokenise the line so that the sgr sequences can be omitted.""" - token_list = sgr_re.split(text_line) + token_list = sgr_regex.split(text_line) text_line_list = [] line_length = 0 for (index, token) in enumerate(token_list): @@ -203,7 +203,7 @@ def _SplitWithSgr(text_line): if token is '': continue - if sgr_re.match(token): + if sgr_regex.match(token): # Add sgr escape sequences without splitting or counting length. text_line_list.append(token) text_line = ''.join(token_list[index +1:]) diff --git a/textfsm.py b/textfsm.py index 746e2db..589f7f6 100755 --- a/textfsm.py +++ b/textfsm.py @@ -32,7 +32,7 @@ import getopt import inspect -import re +import regex import string import sys @@ -306,18 +306,18 @@ def Parse(self, value): raise TextFSMTemplateError( "Invalid Value name '%s' or name too long." % self.name) - if (not re.match(r'^\(.*\)$', self.regex) or + if (not regex.match(r'^\(.*\)$', self.regex) or self.regex.count('(') != self.regex.count(')')): raise TextFSMTemplateError( "Value '%s' must be contained within a '()' pair." % self.regex) - self.template = re.sub(r'^\(', '(?P<%s>' % self.name, self.regex) + self.template = regex.sub(r'^\(', '(?P<%s>' % self.name, self.regex) # Compile and store the regex object only on List-type values for use in nested matching if any(map(lambda x: isinstance(x, TextFSMOptions.List), self.options)): try: - self.compiled_regex = re.compile(self.regex) - except re.error as e: + self.compiled_regex = regex.compile(self.regex) + except regex.error as e: raise TextFSMTemplateError(str(e)) def _AddOption(self, name): @@ -360,12 +360,12 @@ def __str__(self): class CopyableRegexObject(object): - """Like a re.RegexObject, but can be copied.""" + """Like a regex.RegexObject, but can be copied.""" # pylint: disable=C6409 def __init__(self, pattern): self.pattern = pattern - self.regex = re.compile(pattern) + self.regex = regex.compile(pattern) def match(self, *args, **kwargs): return self.regex.match(*args, **kwargs) @@ -402,7 +402,7 @@ class TextFSMRule(object): line_num: Integer row number of Value. """ # Implicit default is '(regexp) -> Next.NoRecord' - MATCH_ACTION = re.compile(r'(?P.*)(\s->(?P.*))') + MATCH_ACTION = regex.compile(r'(?P.*)(\s->(?P.*))') # The structure to the right of the '->'. LINE_OP = ('Continue', 'Next', 'Error') @@ -418,11 +418,11 @@ class TextFSMRule(object): NEWSTATE_RE = r'(?P\w+|\".*\")' # Compound operator (line and record) with optional new state. - ACTION_RE = re.compile(r'\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE)) + ACTION_RE = regex.compile(r'\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE)) # Record operator with optional new state. - ACTION2_RE = re.compile(r'\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE)) + ACTION2_RE = regex.compile(r'\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE)) # Default operators with optional new state. - ACTION3_RE = re.compile(r'(\s+%s)?$' % (NEWSTATE_RE)) + ACTION3_RE = regex.compile(r'(\s+%s)?$' % (NEWSTATE_RE)) def __init__(self, line, line_num=-1, var_map=None): """Initialise a new rule object. @@ -468,7 +468,7 @@ def __init__(self, line, line_num=-1, var_map=None): try: # Work around a regression in Python 2.6 that makes RE Objects uncopyable. self.regex_obj = CopyableRegexObject(self.regex) - except re.error: + except regex.error: raise TextFSMTemplateError( "Invalid regular expression: '%s'. Line: %s." % (self.regex, self.line_num)) @@ -478,29 +478,29 @@ def __init__(self, line, line_num=-1, var_map=None): return # Attempt to match line.record operation. - action_re = self.ACTION_RE.match(match_action.group('action')) + action_re = self.ACTION_regex.match(match_action.group('action')) if not action_re: # Attempt to match record operation. - action_re = self.ACTION2_RE.match(match_action.group('action')) + action_re = self.ACTION2_regex.match(match_action.group('action')) if not action_re: # Math implicit defaults with an optional new state. - action_re = self.ACTION3_RE.match(match_action.group('action')) + action_re = self.ACTION3_regex.match(match_action.group('action')) if not action_re: # Last attempt, match an optional new state only. raise TextFSMTemplateError("Badly formatted rule '%s'. Line: %s." % (line, self.line_num)) # We have an Line operator. - if 'ln_op' in action_re.groupdict() and action_re.group('ln_op'): - self.line_op = action_re.group('ln_op') + if 'ln_op' in action_regex.groupdict() and action_regex.group('ln_op'): + self.line_op = action_regex.group('ln_op') # We have a record operator. - if 'rec_op' in action_re.groupdict() and action_re.group('rec_op'): - self.record_op = action_re.group('rec_op') + if 'rec_op' in action_regex.groupdict() and action_regex.group('rec_op'): + self.record_op = action_regex.group('rec_op') # A new state was specified. - if 'new_state' in action_re.groupdict() and action_re.group('new_state'): - self.new_state = action_re.group('new_state') + if 'new_state' in action_regex.groupdict() and action_regex.group('new_state'): + self.new_state = action_regex.group('new_state') # Only 'Next' (or implicit 'Next') line operator can have a new_state. # But we allow error to have one as a warning message so we are left @@ -512,7 +512,7 @@ def __init__(self, line, line_num=-1, var_map=None): # Check that an error message is present only with the 'Error' operator. if self.line_op != 'Error' and self.new_state: - if not re.match(r'\w+', self.new_state): + if not regex.match(r'\w+', self.new_state): raise TextFSMTemplateError( 'Alphanumeric characters only in state names. Line: %s.' % (self.line_num)) @@ -551,8 +551,8 @@ class TextFSM(object): """ # Variable and State name length. MAX_NAME_LEN = 48 - comment_regex = re.compile(r'^\s*#') - state_name_re = re.compile(r'^(\w+)$') + comment_regex = regex.compile(r'^\s*#') + state_name_re = regex.compile(r'^(\w+)$') _DEFAULT_OPTIONS = TextFSMOptions def __init__(self, template, options_class=_DEFAULT_OPTIONS): @@ -659,7 +659,7 @@ def _AppendRecord(self): self._ClearRecord() def _Parse(self, template): - """Parses template file for FSM structure. + """Parses template file for FSM structuregex. Args: template: Valid template file. @@ -775,7 +775,7 @@ def _ParseFSMState(self, template): # First line is state definition if line and not self.comment_regex.match(line): # Ensure statename has valid syntax and is not a reserved word. - if (not self.state_name_re.match(line) or + if (not self.state_name_regex.match(line) or len(line) > self.MAX_NAME_LEN or line in TextFSMRule.LINE_OP or line in TextFSMRule.RECORD_OP): From d9aa5e544742b992cc7a4819b474083a3567741a Mon Sep 17 00:00:00 2001 From: gachteme Date: Mon, 24 Sep 2018 16:45:01 -0400 Subject: [PATCH 02/16] fixed overly greedy replacements that were made in last commit --- textfsm.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/textfsm.py b/textfsm.py index 589f7f6..614c115 100755 --- a/textfsm.py +++ b/textfsm.py @@ -478,29 +478,29 @@ def __init__(self, line, line_num=-1, var_map=None): return # Attempt to match line.record operation. - action_re = self.ACTION_regex.match(match_action.group('action')) + action_re = self.ACTION_RE.match(match_action.group('action')) if not action_re: # Attempt to match record operation. - action_re = self.ACTION2_regex.match(match_action.group('action')) + action_re = self.ACTION2_RE.match(match_action.group('action')) if not action_re: # Math implicit defaults with an optional new state. - action_re = self.ACTION3_regex.match(match_action.group('action')) + action_re = self.ACTION3_RE.match(match_action.group('action')) if not action_re: # Last attempt, match an optional new state only. raise TextFSMTemplateError("Badly formatted rule '%s'. Line: %s." % (line, self.line_num)) # We have an Line operator. - if 'ln_op' in action_regex.groupdict() and action_regex.group('ln_op'): - self.line_op = action_regex.group('ln_op') + if 'ln_op' in action_re.groupdict() and action_re.group('ln_op'): + self.line_op = action_re.group('ln_op') # We have a record operator. - if 'rec_op' in action_regex.groupdict() and action_regex.group('rec_op'): - self.record_op = action_regex.group('rec_op') + if 'rec_op' in action_re.groupdict() and action_re.group('rec_op'): + self.record_op = action_re.group('rec_op') # A new state was specified. - if 'new_state' in action_regex.groupdict() and action_regex.group('new_state'): - self.new_state = action_regex.group('new_state') + if 'new_state' in action_re.groupdict() and action_re.group('new_state'): + self.new_state = action_re.group('new_state') # Only 'Next' (or implicit 'Next') line operator can have a new_state. # But we allow error to have one as a warning message so we are left From ff984dbde7902edd31dfc109959d81dbf6a3ca91 Mon Sep 17 00:00:00 2001 From: gachteme Date: Fri, 28 Sep 2018 10:35:54 -0400 Subject: [PATCH 03/16] finished porting over to regex from re. Runs. --- textfsm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/textfsm.py b/textfsm.py index 614c115..0625daa 100755 --- a/textfsm.py +++ b/textfsm.py @@ -775,7 +775,7 @@ def _ParseFSMState(self, template): # First line is state definition if line and not self.comment_regex.match(line): # Ensure statename has valid syntax and is not a reserved word. - if (not self.state_name_regex.match(line) or + if (not self.state_name_re.match(line) or len(line) > self.MAX_NAME_LEN or line in TextFSMRule.LINE_OP or line in TextFSMRule.RECORD_OP): From 1c5bdf7eccfe5a1793f5b2f2168db3ec0ce96295 Mon Sep 17 00:00:00 2001 From: gachteme Date: Sun, 30 Sep 2018 22:36:02 -0400 Subject: [PATCH 04/16] First commit with basic functionality. --- textfsm.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/textfsm.py b/textfsm.py index 0625daa..deab76a 100755 --- a/textfsm.py +++ b/textfsm.py @@ -106,6 +106,7 @@ def OnClearAllVar(self): def OnAssignVar(self): """Called when a matched value is being assigned.""" + def OnGetValue(self): """Called when the value name is being requested.""" @@ -127,6 +128,13 @@ def GetOption(cls, name): """Returns the class of the requested option name.""" return getattr(cls, name) + class Repeated(OptionBase): + """Will use regex module's 'captures' behavior to get all repeated + values instead of just the last value as re would.""" + + def OnAssignVar(self): + self.value.value = self.value.values_list + class Required(OptionBase): """The Value must be non-empty for the row to be recorded.""" @@ -194,8 +202,8 @@ def OnAssignVar(self): match = None # If the List-value regex has match-groups defined, add the resulting dict to the list # Otherwise, add the string that was matched - if match and match.groupdict(): - self._value.append(match.groupdict()) + if match and match.capturesdict(): + self._value.append(match.capturesdict()) else: self._value.append(self.value.value) @@ -240,12 +248,14 @@ def __init__(self, fsm=None, max_name_len=48, options_class=None): self.options = [] self.regex = None self.value = None + self.values_list = None self.fsm = fsm self._options_cls = options_class def AssignVar(self, value): """Assign a value to this Value.""" - self.value = value + self.value = value[-1] + self.values_list = value # Call OnAssignVar on options. _ = [option.OnAssignVar() for option in self.options] @@ -928,7 +938,7 @@ def _CheckLine(self, line): for rule in self._cur_state: matched = self._CheckRule(rule, line) if matched: - for value in matched.groupdict(): + for value in matched.capturesdict(): self._AssignVar(matched, value) if self._Operations(rule, line): @@ -965,7 +975,7 @@ def _AssignVar(self, matched, value): """ _value = self._GetValue(value) if _value is not None: - _value.AssignVar(matched.group(value)) + _value.AssignVar(matched.captures(value)) def _Operations(self, rule, line): """Operators on the data record. From c5d17dd1b06fdb5201ad71b570142183fd553429 Mon Sep 17 00:00:00 2001 From: gachteme Date: Mon, 15 Oct 2018 14:50:02 -0400 Subject: [PATCH 05/16] Fix to bug where repeateddata without a match returned an empty string. Added examples. --- examples/repeated_basic_example | 7 +++++++ examples/repeated_basic_template | 12 ++++++++++++ textfsm.py | 16 ++++++++++++++-- textfsm_test.py | 21 ++++++++++++++++----- 4 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 examples/repeated_basic_example create mode 100644 examples/repeated_basic_template diff --git a/examples/repeated_basic_example b/examples/repeated_basic_example new file mode 100644 index 0000000..848edbf --- /dev/null +++ b/examples/repeated_basic_example @@ -0,0 +1,7 @@ +This is an example to demonstrate the usage of the 'repeated' keyword, which enables one variable to have multiple captures on one line. + + +normaldata1.1 normaldata1.2 key1.1:data1.1, key1.2:data1.2, key1.3:data1.3, normaldata1.3 normaldata1.3 +normaldata2.1 normaldata2.2 key2.1:data2.1, key2.2:data2.2, normaldata2.3 normaldata2.4 +normaldata3.1 normaldata3.2 normaldata3.3 normaldata3.4 +normaldata4.1 normaldata4.2 key4.1:data4.1, key4.2:data4.2, key4.3:data4.3, normaldata4.3 normaldata4.3 diff --git a/examples/repeated_basic_template b/examples/repeated_basic_template new file mode 100644 index 0000000..9af6cb5 --- /dev/null +++ b/examples/repeated_basic_template @@ -0,0 +1,12 @@ +Value normaldata1 (\S+) +Value normaldata2 (\S+) +Value normaldata3 (\S+) +Value normaldata4 (\S+) +Value Repeated keything (\S+) +Value Repeated valuedata (\S+) +Value Repeated unusedRepeated (\S+) +Value List unused (\S+) + + +Start + ^${normaldata1}\s+${normaldata2} (${keything}:${valuedata},? )*${normaldata3}\s+${normaldata4} -> Record diff --git a/textfsm.py b/textfsm.py index deab76a..5edb1a3 100755 --- a/textfsm.py +++ b/textfsm.py @@ -135,6 +135,15 @@ class Repeated(OptionBase): def OnAssignVar(self): self.value.value = self.value.values_list + def OnCreateOptions(self): + self.value.value = [] + + def OnClearVar(self): + self.value.value = [] + + def OnClearAllVar(self): + self.value.value = [] + class Required(OptionBase): """The Value must be non-empty for the row to be recorded.""" @@ -203,7 +212,7 @@ def OnAssignVar(self): # If the List-value regex has match-groups defined, add the resulting dict to the list # Otherwise, add the string that was matched if match and match.capturesdict(): - self._value.append(match.capturesdict()) + self._value.append({x: match.capturesdict()[x][-1] for x in match.capturesdict()}) else: self._value.append(self.value.value) @@ -254,7 +263,10 @@ def __init__(self, fsm=None, max_name_len=48, options_class=None): def AssignVar(self, value): """Assign a value to this Value.""" - self.value = value[-1] + try: + self.value = value[-1] + except IndexError: + self.value = '' self.values_list = value # Call OnAssignVar on options. _ = [option.OnAssignVar() for option in self.options] diff --git a/textfsm_test.py b/textfsm_test.py index 232aa95..a303f50 100755 --- a/textfsm_test.py +++ b/textfsm_test.py @@ -513,15 +513,22 @@ def testParseTextToDicts(self): # Tests 'Filldown' and 'Required' options. data = 'two\none' result = t.ParseTextToDicts(data) - self.assertEqual(str(result), "[{'hoo': 'two', 'boo': 'one'}]") + try: + self.assertEqual(str(result), "[{'hoo': 'two', 'boo': 'one'}]") + except AssertionError: + self.assertEqual(str(result), "[{'boo': 'one', 'hoo': 'two'}]") t = textfsm.TextFSM(StringIO(tplt)) # Matching two lines. Two records returned due to 'Filldown' flag. data = 'two\none\none' t.Reset() result = t.ParseTextToDicts(data) - self.assertEqual( - str(result), "[{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]") + try: + self.assertEqual( + str(result), "[{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]") + except AssertionError: + self.assertEqual( + str(result), "[{'boo': 'one', 'hoo': 'two'}, {'boo': 'one', 'hoo': 'two'}]") # Multiple Variables and options. tplt = ('Value Required,Filldown boo (one)\n' @@ -531,8 +538,12 @@ def testParseTextToDicts(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'two\none\none' result = t.ParseTextToDicts(data) - self.assertEqual( - str(result), "[{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]") + try: + self.assertEqual( + str(result), "[{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]") + except AssertionError: + self.assertEqual( + str(result), "[{'boo': 'one', 'hoo': 'two'}, {'boo': 'one', 'hoo': 'two'}]") def testParseNullText(self): From 2f92cfdb6ec1038d0d86c6765abf0a162832d83a Mon Sep 17 00:00:00 2001 From: gachteme Date: Mon, 15 Oct 2018 16:04:27 -0400 Subject: [PATCH 06/16] Added basic test case for Repeated option. Changed invalid regex test case to fail with new regex module rules. --- textfsm_test.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/textfsm_test.py b/textfsm_test.py index a303f50..fa216b5 100755 --- a/textfsm_test.py +++ b/textfsm_test.py @@ -808,7 +808,7 @@ def testEnd(self): def testInvalidRegexp(self): - tplt = 'Value boo (.$*)\n\nStart\n ^$boo -> Next\n' + tplt = 'Value boo ([(\S+]))\n\nStart\n ^$boo -> Next\n' self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, StringIO(tplt)) @@ -857,5 +857,27 @@ def testFillup(self): str(result)) + def testRepeated(self): + """Repeated option should work ok.""" + tplt = """Value Repeated repeatedKey (\S+) +Value Repeated repeatedValue (\S+) +Value normalData (\S+) +Value normalData2 (\S+) +Value Repeated repeatedUnused (\S+) + +Start + ^${normalData} (${repeatedKey}:${repeatedValue} )*${normalData2} -> Record""" + + data = """ +normal1 key1:value1 key2:value2 key3:value3 normal2 \n +normal1 normal2 """ + + t = textfsm.TextFSM(StringIO(tplt)) + result = t.ParseText(data) + self.assertEqual( + "[[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'normal1', 'normal2', []]," + + " [[], [], 'normal1', 'normal2', []]]", + str(result)) + if __name__ == '__main__': unittest.main() From fd1d4dccf22f667b21ed6cdfd37114c7aa9d7b96 Mon Sep 17 00:00:00 2001 From: gachteme Date: Mon, 15 Oct 2018 16:09:29 -0400 Subject: [PATCH 07/16] Update to requirements. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cc8feff..46bbf18 100755 --- a/setup.py +++ b/setup.py @@ -45,6 +45,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries'], - requires=['six'], + requires=['six', 'regex'], py_modules=['clitable', 'textfsm', 'copyable_regex_object', 'texttable', 'terminal']) From 80ef2ddc67b801ea8181485d94dd5baebbd7eb26 Mon Sep 17 00:00:00 2001 From: gachteme Date: Mon, 15 Oct 2018 16:18:12 -0400 Subject: [PATCH 08/16] Cleanup. --- textfsm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/textfsm.py b/textfsm.py index 5edb1a3..3b88447 100755 --- a/textfsm.py +++ b/textfsm.py @@ -106,7 +106,6 @@ def OnClearAllVar(self): def OnAssignVar(self): """Called when a matched value is being assigned.""" - def OnGetValue(self): """Called when the value name is being requested.""" From 81a043a45be9550bfcf20caa22afc7cb56a0c79e Mon Sep 17 00:00:00 2001 From: gachteme Date: Wed, 17 Jul 2019 12:58:47 -0600 Subject: [PATCH 09/16] Allowed fallback to re if regex module cannot be imported. Tested for both re and regex in python 3.6 --- setup.py | 2 +- textfsm/parser.py | 59 +++++++++++++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 3263b14..cc12908 100755 --- a/setup.py +++ b/setup.py @@ -45,5 +45,5 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries'], - requires=['six'], + requires=['six', 'regex'], packages=['textfsm']) diff --git a/textfsm/parser.py b/textfsm/parser.py index 620efc4..eb3ee1e 100755 --- a/textfsm/parser.py +++ b/textfsm/parser.py @@ -30,9 +30,16 @@ import getopt import inspect -import regex import string import sys +import warnings +try: + import regex as regexModule + useRegex = True +except ImportError: + import re as regexModule + warnings.warn("Could not locate regex module. Defaulting to re. Repeated keyword disabled.", ImportWarning) + useRegex = False class Error(Exception): @@ -118,6 +125,8 @@ def ValidOptions(cls): obj = getattr(cls, obj_name) if inspect.isclass(obj) and issubclass(obj, cls.OptionBase): valid_options.append(obj_name) + if useRegex is not True: + valid_options.remove("Repeated") return valid_options @classmethod @@ -130,6 +139,8 @@ class Repeated(OptionBase): values instead of just the last value as re would.""" def OnAssignVar(self): + if useRegex is not True: + raise ModuleNotFoundError("Cannot use Repeated option without installing the regex module.") self.value.value = self.value.values_list def OnCreateOptions(self): @@ -208,8 +219,8 @@ def OnAssignVar(self): match = None # If the List-value regex has match-groups defined, add the resulting dict to the list # Otherwise, add the string that was matched - if match and match.capturesdict(): - self._value.append({x: match.capturesdict()[x][-1] for x in match.capturesdict()}) + if match and match.groupdict(): + self._value.append(match.groupdict()) else: self._value.append(self.value.value) @@ -263,7 +274,7 @@ def AssignVar(self, value): try: self.value = value[-1] except IndexError: - self.value = '' + self.value = "" self.values_list = value # Call OnAssignVar on options. _ = [option.OnAssignVar() for option in self.options] @@ -325,18 +336,18 @@ def Parse(self, value): raise TextFSMTemplateError( "Invalid Value name '%s' or name too long." % self.name) - if (not regex.match(r'^\(.*\)$', self.regex) or + if (not regexModule.match(r'^\(.*\)$', self.regex) or self.regex.count('(') != self.regex.count(')')): raise TextFSMTemplateError( "Value '%s' must be contained within a '()' pair." % self.regex) - self.template = regex.sub(r'^\(', '(?P<%s>' % self.name, self.regex) + self.template = regexModule.sub(r'^\(', '(?P<%s>' % self.name, self.regex) # Compile and store the regex object only on List-type values for use in nested matching if any(map(lambda x: isinstance(x, TextFSMOptions.List), self.options)): try: - self.compiled_regex = regex.compile(self.regex) - except regex.error as e: + self.compiled_regex = regexModule.compile(self.regex) + except regexModule.error as e: raise TextFSMTemplateError(str(e)) def _AddOption(self, name): @@ -379,12 +390,12 @@ def __str__(self): class CopyableRegexObject(object): - """Like a regex.RegexObject, but can be copied.""" + """Like a re.RegexObject, but can be copied.""" # pylint: disable=C6409 def __init__(self, pattern): self.pattern = pattern - self.regex = regex.compile(pattern) + self.regex = regexModule.compile(pattern) def match(self, *args, **kwargs): return self.regex.match(*args, **kwargs) @@ -421,7 +432,7 @@ class TextFSMRule(object): line_num: Integer row number of Value. """ # Implicit default is '(regexp) -> Next.NoRecord' - MATCH_ACTION = regex.compile(r'(?P.*)(\s->(?P.*))') + MATCH_ACTION = regexModule.compile(r'(?P.*)(\s->(?P.*))') # The structure to the right of the '->'. LINE_OP = ('Continue', 'Next', 'Error') @@ -437,11 +448,11 @@ class TextFSMRule(object): NEWSTATE_RE = r'(?P\w+|\".*\")' # Compound operator (line and record) with optional new state. - ACTION_RE = regex.compile(r'\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE)) + ACTION_RE = regexModule.compile(r'\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE)) # Record operator with optional new state. - ACTION2_RE = regex.compile(r'\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE)) + ACTION2_RE = regexModule.compile(r'\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE)) # Default operators with optional new state. - ACTION3_RE = regex.compile(r'(\s+%s)?$' % (NEWSTATE_RE)) + ACTION3_RE = regexModule.compile(r'(\s+%s)?$' % (NEWSTATE_RE)) def __init__(self, line, line_num=-1, var_map=None): """Initialise a new rule object. @@ -487,7 +498,7 @@ def __init__(self, line, line_num=-1, var_map=None): try: # Work around a regression in Python 2.6 that makes RE Objects uncopyable. self.regex_obj = CopyableRegexObject(self.regex) - except regex.error: + except regexModule.error: raise TextFSMTemplateError( "Invalid regular expression: '%s'. Line: %s." % (self.regex, self.line_num)) @@ -531,7 +542,7 @@ def __init__(self, line, line_num=-1, var_map=None): # Check that an error message is present only with the 'Error' operator. if self.line_op != 'Error' and self.new_state: - if not regex.match(r'\w+', self.new_state): + if not regexModule.match(r'\w+', self.new_state): raise TextFSMTemplateError( 'Alphanumeric characters only in state names. Line: %s.' % (self.line_num)) @@ -570,8 +581,8 @@ class TextFSM(object): """ # Variable and State name length. MAX_NAME_LEN = 48 - comment_regex = regex.compile(r'^\s*#') - state_name_re = regex.compile(r'^(\w+)$') + comment_regex = regexModule.compile(r'^\s*#') + state_name_re = regexModule.compile(r'^(\w+)$') _DEFAULT_OPTIONS = TextFSMOptions def __init__(self, template, options_class=_DEFAULT_OPTIONS): @@ -947,8 +958,13 @@ def _CheckLine(self, line): for rule in self._cur_state: matched = self._CheckRule(rule, line) if matched: - for value in matched.capturesdict(): - self._AssignVar(matched, value) + if useRegex is True: + for value in matched.capturesdict(): + self._AssignVar(matched, value) + else: + # workaround to fallback on re module if regex not imported + for value in matched.groupdict(): + self._AssignVar(matched, value) if self._Operations(rule, line): # Not a Continue so check for state transition. @@ -984,7 +1000,10 @@ def _AssignVar(self, matched, value): """ _value = self._GetValue(value) if _value is not None: + if useRegex: _value.AssignVar(matched.captures(value)) + else: + _value.AssignVar([matched.group(value)]) def _Operations(self, rule, line): """Operators on the data record. From 33149e987d33c9c2573ff9130e53fa33ebd5dd2e Mon Sep 17 00:00:00 2001 From: gachteme Date: Thu, 18 Jul 2019 11:53:27 -0600 Subject: [PATCH 10/16] removed regex module where unnecessary. Passes regression in python 3 with and without regex module. --- textfsm/clitable.py | 4 ++-- textfsm/copyable_regex_object.py | 7 +++++-- textfsm/terminal.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/textfsm/clitable.py b/textfsm/clitable.py index f7af766..f1f78cc 100755 --- a/textfsm/clitable.py +++ b/textfsm/clitable.py @@ -27,7 +27,7 @@ import copy import os -import regex +import re import threading import textfsm @@ -315,7 +315,7 @@ def _ParseCmdItem(self, cmd_input, template_file=None): def _PreParse(self, key, value): """Executed against each field of each row read from index table.""" if key == 'Command': - return regex.sub(r'(\[\[.+?\]\])', self._Completion, value) + return re.sub(r'(\[\[.+?\]\])', self._Completion, value) else: return value diff --git a/textfsm/copyable_regex_object.py b/textfsm/copyable_regex_object.py index 237337d..9dfbaf2 100755 --- a/textfsm/copyable_regex_object.py +++ b/textfsm/copyable_regex_object.py @@ -16,7 +16,10 @@ """Work around a regression in Python 2.6 that makes RegexObjects uncopyable.""" -import regex +try: + import regex as regexModule +except ImportError: + import re as regexModule class CopyableRegexObject(object): @@ -25,7 +28,7 @@ class CopyableRegexObject(object): def __init__(self, pattern): self.pattern = pattern - self.regex = regex.compile(pattern) + self.regex = regexModule.compile(pattern) def match(self, *args, **kwargs): return self.regex.match(*args, **kwargs) diff --git a/textfsm/terminal.py b/textfsm/terminal.py index 9d07941..449c7d3 100755 --- a/textfsm/terminal.py +++ b/textfsm/terminal.py @@ -28,7 +28,7 @@ import fcntl import getopt import os -import regex +import re import struct import sys import termios @@ -100,7 +100,7 @@ ANSI_END = '\002' -sgr_re = regex.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % ( +sgr_re = re.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % ( ANSI_START, ANSI_END)) From 2f61b2aabb23df42ce9012dccd65f6457cbfd874 Mon Sep 17 00:00:00 2001 From: gachteme Date: Tue, 23 Jul 2019 11:14:21 -0600 Subject: [PATCH 11/16] Made repeated keyword tests not fail when falling back on re module. Ensured error is raised when using Repeated keyword without regex module. --- tests/textfsm_test.py | 50 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/tests/textfsm_test.py b/tests/textfsm_test.py index 8166475..4241701 100755 --- a/tests/textfsm_test.py +++ b/tests/textfsm_test.py @@ -22,6 +22,11 @@ import unittest from six import StringIO import textfsm +try: + import regex as regexModule + useRegex = True +except ImportError: + useRegex = False class UnitTestFSM(unittest.TestCase): @@ -871,7 +876,6 @@ def testFillup(self): "[['1', 'A2', 'B1'], ['2', 'A2', 'B3'], ['3', '', 'B3']]", str(result)) - def testRepeated(self): """Repeated option should work ok.""" tplt = """Value Repeated repeatedKey (\S+) @@ -888,11 +892,45 @@ def testRepeated(self): normal1 normal2 """ t = textfsm.TextFSM(StringIO(tplt)) - result = t.ParseText(data) - self.assertEqual( - "[[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'normal1', 'normal2', []]," - + " [[], [], 'normal1', 'normal2', []]]", - str(result)) + if useRegex is True: + result = t.ParseText(data) + self.assertEqual( + "[[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'normal1', 'normal2', []]," + + " [[], [], 'normal1', 'normal2', []]]", + str(result)) + else: + # test proper failure when falling back on re module + try: + result = t.ParseText(data) # TODO: ensure this is not optimized out if tests are bytecode files + raise AssertionError("Expected a ModuleNotFoundError when using keyword 'Repeated' without 'regex' module") + except ModuleNotFoundError as e: + self.assertEqual(e.args[0], "Cannot use Repeated option without installing the regex module.") + + def testTemp(self): + tplt = """Value Repeated repeatedKey (\S+) +Value Repeated repeatedValue (\S+) +Value Repeated repeatedUnused (\S+) + +Start + ^(${repeatedKey}:${repeatedValue} )+ + ^record -> Record""" + + data = """ +key1:value1 key2:value2 key3:value3 \n +key4:value4 key5:value5 key6:value6 \n +record""" + + t = textfsm.TextFSM(StringIO(tplt)) + try: + result = t.ParseText(data) + except ModuleNotFoundError as e: + if useRegex is True: + raise e + else: + return + + for i in range(len(result)): + print(result[i]) if __name__ == '__main__': unittest.main() From 0707470eac8f21ba46d1dc1a0245aff27e8cb6b1 Mon Sep 17 00:00:00 2001 From: gachteme Date: Tue, 23 Jul 2019 12:18:03 -0600 Subject: [PATCH 12/16] Allowed use of Repeated and List together. Added tests for correct behavior. Refactored test for proper error functionality when re and Repeated are used. --- tests/textfsm_test.py | 34 ++++++++++++++++------------------ textfsm/parser.py | 4 ++++ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/textfsm_test.py b/tests/textfsm_test.py index 4241701..77c3bc5 100755 --- a/tests/textfsm_test.py +++ b/tests/textfsm_test.py @@ -900,16 +900,14 @@ def testRepeated(self): str(result)) else: # test proper failure when falling back on re module - try: - result = t.ParseText(data) # TODO: ensure this is not optimized out if tests are bytecode files - raise AssertionError("Expected a ModuleNotFoundError when using keyword 'Repeated' without 'regex' module") - except ModuleNotFoundError as e: - self.assertEqual(e.args[0], "Cannot use Repeated option without installing the regex module.") + with self.assertRaises(ModuleNotFoundError, + msg="Expected a ModuleNotFoundError when using keyword 'Repeated' without 'regex' module"): + result = t.ParseText(data) - def testTemp(self): - tplt = """Value Repeated repeatedKey (\S+) -Value Repeated repeatedValue (\S+) -Value Repeated repeatedUnused (\S+) + def testRepeatedList(self): + tplt = """Value List,Repeated repeatedKey (\S+) +Value Repeated,List repeatedValue (\S+) +Value Repeated,List repeatedUnused (\S+) Start ^(${repeatedKey}:${repeatedValue} )+ @@ -921,16 +919,16 @@ def testTemp(self): record""" t = textfsm.TextFSM(StringIO(tplt)) - try: + if useRegex is True: result = t.ParseText(data) - except ModuleNotFoundError as e: - if useRegex is True: - raise e - else: - return - - for i in range(len(result)): - print(result[i]) + else: + return + + self.assertEqual("[[[['key1', 'key2', 'key3'], ['key4', 'key5', 'key6']], [['value1', 'value2', 'value3']," + + " ['value4', 'value5', 'value6']], []]]", + str(result) + ) + if __name__ == '__main__': unittest.main() diff --git a/textfsm/parser.py b/textfsm/parser.py index eb3ee1e..022e6ae 100755 --- a/textfsm/parser.py +++ b/textfsm/parser.py @@ -33,6 +33,7 @@ import string import sys import warnings +warnings.simplefilter("always") try: import regex as regexModule useRegex = True @@ -222,6 +223,9 @@ def OnAssignVar(self): if match and match.groupdict(): self._value.append(match.groupdict()) else: + if "Repeated" in (optionClass.name for optionClass in self.value.options): + self._value.append(self.value.values_list) + else: self._value.append(self.value.value) def OnClearVar(self): From 5aa7071c00842c3f0bcf5b101daa1cd454a40c55 Mon Sep 17 00:00:00 2001 From: gachteme Date: Wed, 24 Jul 2019 11:42:29 -0600 Subject: [PATCH 13/16] Allowed use of Repeated and Fillup or Filldown together. Added tests for correct behavior. --- tests/textfsm_test.py | 64 +++++++++++++++++++++++++++++++++++++++++++ textfsm/parser.py | 11 ++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/tests/textfsm_test.py b/tests/textfsm_test.py index 77c3bc5..b90c002 100755 --- a/tests/textfsm_test.py +++ b/tests/textfsm_test.py @@ -905,6 +905,7 @@ def testRepeated(self): result = t.ParseText(data) def testRepeatedList(self): + """Keywords Repeated and List should work together""" tplt = """Value List,Repeated repeatedKey (\S+) Value Repeated,List repeatedValue (\S+) Value Repeated,List repeatedUnused (\S+) @@ -929,6 +930,69 @@ def testRepeatedList(self): str(result) ) + def testRepeatedFilldown(self): + """Keywords Repeated and Filldown should work together""" + tplt = """Value Filldown,Repeated repeatedKey (\S+) +Value Repeated,Filldown repeatedValue (\S+) +Value Required otherMatch (\S+) + +Start + ^record -> Record + ^(${repeatedKey}:${repeatedValue} )+ + ^${otherMatch} + """ + + data = """ +key1:value1 key2:value2 key3:value3 \n +key4:value4 key5:value5 key6:value6 \n +foo \n +bar \n +record \n +foobar \n +record""" + + t = textfsm.TextFSM(StringIO(tplt)) + if useRegex is True: + result = t.ParseText(data) + else: + return + + self.assertEqual("[[['key4', 'key5', 'key6'], ['value4', 'value5', 'value6'], 'bar'], [['key4', 'key5', 'key6']," + " ['value4', 'value5', 'value6'], 'foobar']]", + str(result) + ) + + def testRepeatedFillup(self): + """Keywords Repeated and Fillup should work together""" + tplt = """Value Fillup,Repeated repeatedKey (\S+) +Value Repeated,Fillup repeatedValue (\S+) +Value Required otherMatch (\S+) + +Start + ^record -> Record + ^(${repeatedKey}:${repeatedValue} )+ + ^${otherMatch} + """ + + data = """ +foo \n +bar \n +record \n +foobar \n +key1:value1 key2:value2 key3:value3 \n +record""" + + t = textfsm.TextFSM(StringIO(tplt)) + if useRegex is True: + result = t.ParseText(data) + else: + return + + self.assertEqual("[[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'bar']," + " [['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'foobar']]", + str(result) + ) + if __name__ == '__main__': unittest.main() diff --git a/textfsm/parser.py b/textfsm/parser.py index 022e6ae..0ab52bd 100755 --- a/textfsm/parser.py +++ b/textfsm/parser.py @@ -148,7 +148,8 @@ def OnCreateOptions(self): self.value.value = [] def OnClearVar(self): - self.value.value = [] + if 'Filldown' not in self.value.OptionNames(): + self.value.value = [] def OnClearAllVar(self): self.value.value = [] @@ -168,6 +169,8 @@ def OnCreateOptions(self): def OnAssignVar(self): self._myvar = self.value.value + if "Repeated" in self.value.OptionNames(): + self._myvar = self.value.values_list def OnClearVar(self): self.value.value = self._myvar @@ -179,6 +182,10 @@ class Fillup(OptionBase): """Like Filldown, but upwards until it finds a non-empty entry.""" def OnAssignVar(self): + # make sure repeated OnAssignVar runs first so value.value is set + for option in self.value.options: + if option.name is "Repeated": + option.OnAssignVar() # If value is set, copy up the results table, until we # see a set item. if self.value.value: @@ -223,7 +230,7 @@ def OnAssignVar(self): if match and match.groupdict(): self._value.append(match.groupdict()) else: - if "Repeated" in (optionClass.name for optionClass in self.value.options): + if "Repeated" in self.value.OptionNames(): self._value.append(self.value.values_list) else: self._value.append(self.value.value) From 19a922ebfe06ef840267ae43d61ed5a7a5901f4f Mon Sep 17 00:00:00 2001 From: gachteme Date: Wed, 24 Jul 2019 12:21:30 -0600 Subject: [PATCH 14/16] Changes to ensure python 2.7 compatibility. Tests pass with and without regex module in python 3.6 and python 2.7 --- tests/textfsm_test.py | 3 ++- textfsm/parser.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/textfsm_test.py b/tests/textfsm_test.py index b90c002..cb7c527 100755 --- a/tests/textfsm_test.py +++ b/tests/textfsm_test.py @@ -22,6 +22,7 @@ import unittest from six import StringIO import textfsm +from textfsm.parser import TextFSMTemplateError try: import regex as regexModule useRegex = True @@ -900,7 +901,7 @@ def testRepeated(self): str(result)) else: # test proper failure when falling back on re module - with self.assertRaises(ModuleNotFoundError, + with self.assertRaises(TextFSMTemplateError, msg="Expected a ModuleNotFoundError when using keyword 'Repeated' without 'regex' module"): result = t.ParseText(data) diff --git a/textfsm/parser.py b/textfsm/parser.py index 0ab52bd..dafd3c2 100755 --- a/textfsm/parser.py +++ b/textfsm/parser.py @@ -141,7 +141,7 @@ class Repeated(OptionBase): def OnAssignVar(self): if useRegex is not True: - raise ModuleNotFoundError("Cannot use Repeated option without installing the regex module.") + raise TextFSMTemplateError("Cannot use Repeated option without installing the regex module.") self.value.value = self.value.values_list def OnCreateOptions(self): From 1b5f21aee2e2413c0e186b3d7fd830014bd50121 Mon Sep 17 00:00:00 2001 From: gachteme Date: Wed, 24 Jul 2019 22:00:37 -0600 Subject: [PATCH 15/16] moved Repeated tests to assertListEqual. Bugfix for new dependencies. Works in 2.7 and 3.6 --- tests/textfsm_test.py | 26 +++++++++++++------------- textfsm/parser.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/textfsm_test.py b/tests/textfsm_test.py index 68e9ada..9c6815c 100755 --- a/tests/textfsm_test.py +++ b/tests/textfsm_test.py @@ -920,10 +920,10 @@ def testRepeated(self): t = textfsm.TextFSM(StringIO(tplt)) if useRegex is True: result = t.ParseText(data) - self.assertEqual( - "[[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'normal1', 'normal2', []]," - + " [[], [], 'normal1', 'normal2', []]]", - str(result)) + self.assertListEqual( + [[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'normal1', 'normal2', []], + [[], [], 'normal1', 'normal2', []]], + result) else: # test proper failure when falling back on re module with self.assertRaises(TextFSMTemplateError, @@ -951,9 +951,9 @@ def testRepeatedList(self): else: return - self.assertEqual("[[[['key1', 'key2', 'key3'], ['key4', 'key5', 'key6']], [['value1', 'value2', 'value3']," + - " ['value4', 'value5', 'value6']], []]]", - str(result) + self.assertListEqual([[[['key1', 'key2', 'key3'], ['key4', 'key5', 'key6']], [['value1', 'value2', 'value3'], + ['value4', 'value5', 'value6']], []]], + result ) def testRepeatedFilldown(self): @@ -983,9 +983,9 @@ def testRepeatedFilldown(self): else: return - self.assertEqual("[[['key4', 'key5', 'key6'], ['value4', 'value5', 'value6'], 'bar'], [['key4', 'key5', 'key6']," - " ['value4', 'value5', 'value6'], 'foobar']]", - str(result) + self.assertListEqual([[['key4', 'key5', 'key6'], ['value4', 'value5', 'value6'], 'bar'], [['key4', 'key5', 'key6'], + ['value4', 'value5', 'value6'], 'foobar']], + result ) def testRepeatedFillup(self): @@ -1014,9 +1014,9 @@ def testRepeatedFillup(self): else: return - self.assertEqual("[[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'bar']," - " [['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'foobar']]", - str(result) + self.assertListEqual([[['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'bar'], + [['key1', 'key2', 'key3'], ['value1', 'value2', 'value3'], 'foobar']], + result ) diff --git a/textfsm/parser.py b/textfsm/parser.py index 03b07a6..577d93b 100755 --- a/textfsm/parser.py +++ b/textfsm/parser.py @@ -191,7 +191,7 @@ class Fillup(OptionBase): def OnAssignVar(self): # make sure repeated OnAssignVar runs first so value.value is set for option in self.value.options: - if option.name is "Repeated": + if option.name == "Repeated": option.OnAssignVar() # If value is set, copy up the results table, until we # see a set item. From 47452544ed5fe6eb080c221bbecea46344570223 Mon Sep 17 00:00:00 2001 From: gachteme Date: Sat, 27 Jul 2019 12:05:37 -0600 Subject: [PATCH 16/16] moved clitable_test back to re where regex is not needed. --- tests/clitable_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/clitable_test.py b/tests/clitable_test.py index 1359bd0..1190b71 100755 --- a/tests/clitable_test.py +++ b/tests/clitable_test.py @@ -23,7 +23,7 @@ import copy import os -import regex +import re import unittest from io import StringIO @@ -106,11 +106,11 @@ def setUp(self): def testCompletion(self): """Tests '[[]]' syntax replacement.""" indx = clitable.CliTable() - self.assertEqual('abc', regex.sub(r'(\[\[.+?\]\])', indx._Completion, 'abc')) + self.assertEqual('abc', re.sub(r'(\[\[.+?\]\])', indx._Completion, 'abc')) self.assertEqual('a(b(c)?)?', - regex.sub(r'(\[\[.+?\]\])', indx._Completion, 'a[[bc]]')) + re.sub(r'(\[\[.+?\]\])', indx._Completion, 'a[[bc]]')) self.assertEqual('a(b(c)?)? de(f)?', - regex.sub(r'(\[\[.+?\]\])', indx._Completion, + re.sub(r'(\[\[.+?\]\])', indx._Completion, 'a[[bc]] de[[f]]')) def testRepeatRead(self):