diff --git a/MANIFEST.in b/MANIFEST.in index a4dfa4f..4003a3e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,6 @@ -include README.md include COPYING +include *.md +include *.txt +recursive-include tests *.py recursive-include examples * +recursive-include testdata * diff --git a/setup.cfg b/setup.cfg index 6afcf47..31c6a0e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ description-file = README.md [aliases] test=pytest + +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index cf1af43..e0fffa4 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,9 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Libraries'], - requires=['six'], packages=['textfsm'], + include_package_data=True, + package_data={'textfsm': ['../testdata/*']}, + install_requires=['six', 'future'], setup_requires=['pytest-runner'], tests_require=['pytest']) diff --git a/tests/clitable_test.py b/tests/clitable_test.py index d67e5b7..1190b71 100755 --- a/tests/clitable_test.py +++ b/tests/clitable_test.py @@ -19,15 +19,16 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import copy import os import re import unittest +from io import StringIO from textfsm import clitable from textfsm import copyable_regex_object -from six.moves import StringIO class UnitTestIndexTable(unittest.TestCase): @@ -88,6 +89,7 @@ class UnitTestCliTable(unittest.TestCase): """Tests the CliTable class.""" def setUp(self): + super(UnitTestCliTable, self).setUp() clitable.CliTable.INDEX = {} self.clitable = clitable.CliTable('default_index', 'testdata') self.input_data = ('a b c\n' diff --git a/tests/copyable_regex_object_test.py b/tests/copyable_regex_object_test.py index 5ac7973..33860a1 100755 --- a/tests/copyable_regex_object_test.py +++ b/tests/copyable_regex_object_test.py @@ -19,6 +19,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals import copy import unittest diff --git a/tests/terminal_test.py b/tests/terminal_test.py index ffa4e25..d9162a3 100755 --- a/tests/terminal_test.py +++ b/tests/terminal_test.py @@ -17,18 +17,23 @@ """Unittest for terminal module.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +from builtins import object import sys import unittest -# pylint: disable=redefined-builtin -from six.moves import range from textfsm import terminal class TerminalTest(unittest.TestCase): def setUp(self): + super(TerminalTest, self).setUp() self.environ_orig = terminal.os.environ self.open_orig = terminal.os.open self.terminal_orig = terminal.TerminalSize @@ -142,6 +147,7 @@ def flush(self): class PagerTest(unittest.TestCase): def setUp(self): + super(PagerTest, self).setUp() sys.stdout = FakeTerminal() self.get_ch_orig = terminal.Pager._GetCh terminal.Pager._GetCh = lambda self: 'q' diff --git a/tests/textfsm_test.py b/tests/textfsm_test.py index b3c85a4..3719b4a 100755 --- a/tests/textfsm_test.py +++ b/tests/textfsm_test.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # # Copyright 2010 Google Inc. All Rights Reserved. # @@ -18,9 +19,14 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals +from builtins import str import unittest -from six import StringIO +from io import StringIO + + + import textfsm @@ -394,43 +400,6 @@ def testTextFSM(self): t = textfsm.TextFSM(f) self.assertEqual(str(t), buf_result) - # Complex template, multiple vars and states with comments (no var options). - buf = r"""# Header -# Header 2 -Value Beer (.*) -Value Wine (\w+) - -# An explanation. -Start - ^hi there ${Wine}. -> Next.Record State1 - -State1 - ^\w - ^$Beer .. -> Start - # Some comments - ^$$ -> Next - ^$$ -> End - -End -# Tail comment. -""" - - buf_result = r"""Value Beer (.*) -Value Wine (\w+) - -Start - ^hi there ${Wine}. -> Next.Record State1 - -State1 - ^\w - ^$Beer .. -> Start - ^$$ -> Next - ^$$ -> End -""" - f = StringIO(buf) - t = textfsm.TextFSM(f) - self.assertEqual(str(t), buf_result) - def testParseText(self): # Trivial FSM, no records produced. @@ -451,13 +420,13 @@ def testParseText(self): # Tests 'Next' & 'Record' actions. data = 'Matching text' result = t.ParseText(data) - self.assertEqual(str(result), "[['Matching text']]") + self.assertListEqual(result, [['Matching text']]) # Matching two lines. Reseting FSM before Parsing. t.Reset() data = 'Matching text\nAnd again' result = t.ParseText(data) - self.assertEqual(str(result), "[['Matching text'], ['And again']]") + self.assertListEqual(result, [['Matching text'], ['And again']]) # Two Variables and singular options. tplt = ('Value Required boo (one)\nValue Filldown hoo (two)\n\n' @@ -469,15 +438,14 @@ def testParseText(self): # Tests 'Filldown' and 'Required' options. data = 'two\none' result = t.ParseText(data) - self.assertEqual(str(result), "[['one', 'two']]") + self.assertListEqual(result, [['one', 'two']]) t = textfsm.TextFSM(StringIO(tplt)) # Matching two lines. Two records returned due to 'Filldown' flag. data = 'two\none\none' t.Reset() result = t.ParseText(data) - self.assertEqual( - str(result), "[['one', 'two'], ['one', 'two']]") + self.assertListEqual(result, [['one', 'two'], ['one', 'two']]) # Multiple Variables and options. tplt = ('Value Required,Filldown boo (one)\n' @@ -487,8 +455,7 @@ def testParseText(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'two\none\none' result = t.ParseText(data) - self.assertEqual( - str(result), "[['one', 'two'], ['one', 'two']]") + self.assertListEqual(result, [['one', 'two'], ['one', 'two']]) def testParseTextToDicts(self): @@ -510,13 +477,14 @@ def testParseTextToDicts(self): # Tests 'Next' & 'Record' actions. data = 'Matching text' result = t.ParseTextToDicts(data) - self.assertEqual(str(result), "[{'boo': 'Matching text'}]") + self.assertListEqual(result, [{'boo': 'Matching text'}]) # Matching two lines. Reseting FSM before Parsing. t.Reset() data = 'Matching text\nAnd again' result = t.ParseTextToDicts(data) - self.assertEqual(str(result), "[{'boo': 'Matching text'}, {'boo': 'And again'}]") + self.assertListEqual(result, + [{'boo': 'Matching text'}, {'boo': 'And again'}]) # Two Variables and singular options. tplt = ('Value Required boo (one)\nValue Filldown hoo (two)\n\n' @@ -528,15 +496,15 @@ def testParseTextToDicts(self): # Tests 'Filldown' and 'Required' options. data = 'two\none' result = t.ParseTextToDicts(data) - self.assertEqual(str(result), "[{'hoo': 'two', 'boo': 'one'}]") + self.assertListEqual(result, [{'hoo': 'two', 'boo': 'one'}]) 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'}]") + self.assertListEqual( + result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]) # Multiple Variables and options. tplt = ('Value Required,Filldown boo (one)\n' @@ -546,8 +514,8 @@ 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'}]") + self.assertListEqual( + result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]) def testParseNullText(self): @@ -558,7 +526,7 @@ def testParseNullText(self): # Null string data = '' result = t.ParseText(data) - self.assertEqual(result, []) + self.assertListEqual(result, []) def testReset(self): @@ -568,7 +536,7 @@ def testReset(self): result1 = t.ParseText(data) t.Reset() result2 = t.ParseText(data) - self.assertEqual(str(result1), str(result2)) + self.assertListEqual(result1, result2) tplt = ('Value boo (one)\nValue hoo (two)\n\n' 'Start\n ^$boo -> State1\n\n' @@ -595,7 +563,7 @@ def testClear(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'one\ntwo\nonE\ntwO' result = t.ParseText(data) - self.assertEqual(str(result), ("[['onE', 'two']]")) + self.assertListEqual(result, [['onE', 'two']]) # Clearall, with Filldown variable. # Tests 'Clearall'. @@ -607,7 +575,7 @@ def testClear(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'one\ntwo' result = t.ParseText(data) - self.assertEqual(str(result), ("[['', 'two']]")) + self.assertListEqual(result, [['', 'two']]) def testContinue(self): @@ -618,8 +586,7 @@ def testContinue(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'one\non0' result = t.ParseText(data) - self.assertEqual( - str(result), ("[['one', 'one'], ['on0', 'on0']]")) + self.assertListEqual(result, [['one', 'one'], ['on0', 'on0']]) def testError(self): @@ -657,9 +624,7 @@ def testList(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'one\ntwo\non0\ntw0' result = t.ParseText(data) - self.assertEqual( - str(result), ("[[['one'], 'two'], " - "[['on0'], 'tw0']]")) + self.assertListEqual(result, [[['one'], 'two'], [['on0'], 'tw0']]) tplt = ('Value List,Filldown boo (on.)\n' 'Value hoo (on.)\n\n' @@ -669,10 +634,9 @@ def testList(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'one\non0\non1' result = t.ParseText(data) - self.assertEqual( - str(result), ("[[['one'], 'one'], " - "[['one', 'on0'], 'on0'], " - "[['one', 'on0', 'on1'], 'on1']]")) + self.assertEqual(result, ([[['one'], 'one'], + [['one', 'on0'], 'on0'], + [['one', 'on0', 'on1'], 'on1']])) tplt = ('Value List,Required boo (on.)\n' 'Value hoo (tw.)\n\n' @@ -682,39 +646,54 @@ def testList(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'one\ntwo\ntw2' result = t.ParseText(data) - self.assertEqual(str(result), ("[[['one'], 'two']]")) + self.assertListEqual(result, [[['one'], 'two']]) def testNestedMatching(self): """ - Ensures that List-type values with nested regex capture groups are parsed correctly - as a list of dictionaries. + Ensures that List-type values with nested regex capture groups are parsed + correctly as a list of dictionaries. - Additionaly, another value is used with the same group-name as one of the nested groups to ensure that - there are no conflicts when the same name is used. + Additionaly, another value is used with the same group-name as one of the + nested groups to ensure that there are no conflicts when the same name is + used. """ tplt = ( - "Value List foo ((?P\w+):\s+(?P\d+)\s+(?P\w{2})\s*)\n" # A nested group is called "name" - "Value name (\w+)\n\n" # A regular value is called "name" - "Start\n ^\s*${foo}\n ^\s*${name}\n ^\s*$$ -> Record" # "${name}" here refers to the Value called "name" + # A nested group is called "name" + r"Value List foo ((?P\w+):\s+(?P\d+)\s+(?P\w{2})\s*)" + "\n" + # A regular value is called "name" + r"Value name (\w+)" + # "${name}" here refers to the Value called "name" + "\n\nStart\n" + r" ^\s*${foo}" + "\n" + r" ^\s*${name}" + "\n" + r" ^\s*$$ -> Record" ) t = textfsm.TextFSM(StringIO(tplt)) - data = " Bob: 32 NC\n Alice: 27 NY\n Jeff: 45 CA\nJulia\n\n" # Julia should be parsed as "name" separately + # Julia should be parsed as "name" separately + data = " Bob: 32 NC\n Alice: 27 NY\n Jeff: 45 CA\nJulia\n\n" result = t.ParseText(data) - self.assertEqual( + self.assertListEqual( result, ( [[[ - {'name': 'Bob', 'age': '32', 'state': 'NC'}, - {'name': 'Alice', 'age': '27', 'state': 'NY'}, - {'name': 'Jeff', 'age': '45', 'state': 'CA'} + {'name': 'Bob', 'age': '32', 'state': 'NC'}, + {'name': 'Alice', 'age': '27', 'state': 'NY'}, + {'name': 'Jeff', 'age': '45', 'state': 'CA'} ], 'Julia']] - )) + ) + ) def testNestedNameConflict(self): tplt = ( # Two nested groups are called "name" - "Value List foo ((?P\w+)\s+(?P\w+):\s+(?P\d+)\s+(?P\w{2})\s*)\n" - "Start\n ^\s*${foo}\n ^\s*$$ -> Record" + r"Value List foo ((?P\w+)\s+(?P\w+):\s+(?P\d+)\s+(?P\w{2})\s*)" + "\nStart\n" + r"^\s*${foo}" + "\n ^" + r"\s*$$ -> Record" ) self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, StringIO(tplt)) @@ -731,7 +710,7 @@ def testGetValuesByAttrib(self): self.assertEqual(t.GetValuesByAttrib('Filldown'), []) result = t.GetValuesByAttrib('Required') result.sort() - self.assertEqual(result, ['boo', 'hoo']) + self.assertListEqual(result, ['boo', 'hoo']) def testStateChange(self): @@ -770,21 +749,21 @@ def testEOF(self): data = 'Matching text' result = t.ParseText(data) - self.assertEqual(str(result), "[['Matching text']]") + self.assertListEqual(result, [['Matching text']]) # EOF explicitly suppressed in template. tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next\n\nEOF\n' t = textfsm.TextFSM(StringIO(tplt)) result = t.ParseText(data) - self.assertEqual(str(result), '[]') + self.assertListEqual(result, []) # Implicit EOF suppressed by argument. tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next\n' t = textfsm.TextFSM(StringIO(tplt)) result = t.ParseText(data, eof=False) - self.assertEqual(str(result), '[]') + self.assertListEqual(result, []) def testEnd(self): @@ -794,21 +773,21 @@ def testEnd(self): data = 'Matching text A\nMatching text B' result = t.ParseText(data) - self.assertEqual(str(result), '[]') + self.assertListEqual(result, []) # End State, with explicit Record. tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Record End\n' t = textfsm.TextFSM(StringIO(tplt)) result = t.ParseText(data) - self.assertEqual(str(result), "[['Matching text A']]") + self.assertListEqual(result, [['Matching text A']]) # EOF state transition is followed by implicit End State. tplt = 'Value boo (.*)\n\nStart\n ^$boo -> EOF\n ^$boo -> Record\n' t = textfsm.TextFSM(StringIO(tplt)) result = t.ParseText(data) - self.assertEqual(str(result), "[['Matching text A']]") + self.assertListEqual(result, [['Matching text A']]) def testInvalidRegexp(self): @@ -823,7 +802,7 @@ def testValidRegexp(self): t = textfsm.TextFSM(StringIO(tplt)) data = 'f\nfo\nfoo\n' result = t.ParseText(data) - self.assertEqual(str(result), "[['f'], ['fo'], ['foo']]") + self.assertListEqual(result, [['f'], ['fo'], ['foo']]) def testReEnteringState(self): """Issue 2. TextFSM should leave file pointer at top of template file.""" @@ -856,9 +835,69 @@ def testFillup(self): """ t = textfsm.TextFSM(StringIO(tplt)) result = t.ParseText(data) - self.assertEqual( - "[['1', 'A2', 'B1'], ['2', 'A2', 'B3'], ['3', '', 'B3']]", - str(result)) + self.assertListEqual( + result, [['1', 'A2', 'B1'], ['2', 'A2', 'B3'], ['3', '', 'B3']]) + + +class UnitTestUnicode(unittest.TestCase): + """Tests the FSM engine.""" + + def testFSMValue(self): + # Check basic line is parsed. + line = 'Value beer (\\S+Δ)' + v = textfsm.TextFSMValue() + v.Parse(line) + self.assertEqual(v.name, 'beer') + self.assertEqual(v.regex, '(\\S+Δ)') + self.assertEqual(v.template, '(?P\\S+Δ)') + self.assertFalse(v.options) + + def testFSMRule(self): + # Basic line, no action + line = ' ^A beer called ${beer}Δ' + r = textfsm.TextFSMRule(line) + self.assertEqual(r.match, '^A beer called ${beer}Δ') + self.assertEqual(r.line_op, '') + self.assertEqual(r.new_state, '') + self.assertEqual(r.record_op, '') + + def testTemplateValue(self): + # Complex template, multiple vars and states with comments (no var options). + buf = """# Header +# Header 2 +Value Beer (.*) +Value Wine (\\w+) + +# An explanation with a unicode character Δ +Start + ^hi there ${Wine}. -> Next.Record State1 + +State1 + ^\\wΔ + ^$Beer .. -> Start + # Some comments + ^$$ -> Next + ^$$ -> End + +End +# Tail comment. +""" + + buf_result = """Value Beer (.*) +Value Wine (\\w+) + +Start + ^hi there ${Wine}. -> Next.Record State1 + +State1 + ^\\wΔ + ^$Beer .. -> Start + ^$$ -> Next + ^$$ -> End +""" + f = StringIO(buf) + t = textfsm.TextFSM(f) + self.assertEqual(str(t), buf_result) if __name__ == '__main__': diff --git a/tests/texttable_test.py b/tests/texttable_test.py index a565bab..57ae384 100755 --- a/tests/texttable_test.py +++ b/tests/texttable_test.py @@ -19,10 +19,11 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals + +from builtins import range import unittest -# pylint: disable=redefined-builtin -from six.moves import range -from six.moves import StringIO +from io import StringIO from textfsm import terminal from textfsm import texttable @@ -35,6 +36,7 @@ class UnitTestRow(unittest.TestCase): """Tests texttable.Row() class.""" def setUp(self): + super(UnitTestRow, self).setUp() self.row = texttable.Row() self.row._keys = ['a', 'b', 'c'] self.row._values = ['1', '2', '3'] diff --git a/textfsm/__init__.py b/textfsm/__init__.py index 5269986..bfd0861 100644 --- a/textfsm/__init__.py +++ b/textfsm/__init__.py @@ -1,3 +1,13 @@ +"""Template based text parser. + +This module implements a parser, intended to be used for converting +human readable text, such as command output from a router CLI, into +a list of records, containing values extracted from the input text. + +A simple template language is used to describe a state machine to +parse a specific type of text input, returning a record of values +for each input entity. +""" from textfsm.parser import * -__version__ = '1.0.0' +__version__ = '1.1.0' diff --git a/textfsm/clitable.py b/textfsm/clitable.py index f1f78cc..9fe7d9b 100755 --- a/textfsm/clitable.py +++ b/textfsm/clitable.py @@ -24,11 +24,17 @@ Is the glue between an automated command scraping program (such as RANCID) and the TextFSM output parser. """ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals import copy import os import re import threading +from builtins import object # pylint: disable=redefined-builtin +from builtins import str # pylint: disable=redefined-builtin import textfsm from textfsm import copyable_regex_object @@ -174,7 +180,6 @@ class CliTable(texttable.TextTable): _lock = threading.Lock() INDEX = {} - # pylint: disable=C6409 def synchronised(func): """Synchronisation decorator.""" @@ -186,7 +191,6 @@ def Wrapper(main_obj, *args, **kwargs): finally: main_obj._lock.release() # pylint: disable=W0212 return Wrapper - # pylint: enable=C6409 @synchronised def __init__(self, index_file=None, template_dir=None): @@ -327,7 +331,6 @@ def _PreCompile(self, key, value): return value def _Completion(self, match): - # pylint: disable=C6114 r"""Replaces double square brackets with variable length completion. Completion cannot be mixed with regexp matching or '\' characters @@ -349,7 +352,7 @@ def LabelValueTable(self, keys=None): # pylint: disable=E1002 return super(CliTable, self).LabelValueTable(keys) - # pylint: disable=W0622,C6409 + # pylint: disable=W0622 def sort(self, cmp=None, key=None, reverse=False): """Overrides sort func to use the KeyValue for the key.""" if not key and self._keys: diff --git a/textfsm/copyable_regex_object.py b/textfsm/copyable_regex_object.py index 00cb442..5ac29da 100755 --- a/textfsm/copyable_regex_object.py +++ b/textfsm/copyable_regex_object.py @@ -16,12 +16,13 @@ """Work around a regression in Python 2.6 that makes RegexObjects uncopyable.""" + import re +from builtins import object # pylint: disable=redefined-builtin class CopyableRegexObject(object): """Like a re.RegexObject, but can be copied.""" - # pylint: disable=C6409 def __init__(self, pattern): self.pattern = pattern diff --git a/textfsm/parser.py b/textfsm/parser.py index 2790d37..ba994a7 100755 --- a/textfsm/parser.py +++ b/textfsm/parser.py @@ -27,12 +27,18 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals + import getopt import inspect import re import string import sys +from builtins import object # pylint: disable=redefined-builtin +from builtins import str # pylint: disable=redefined-builtin +from builtins import zip # pylint: disable=redefined-builtin +import six class Error(Exception): @@ -82,6 +88,7 @@ class OptionBase(object): """Factory methods for option class. Attributes: + name: The name of the option. value: A TextFSMValue, the parent Value. """ @@ -169,14 +176,15 @@ class Key(OptionBase): """Value constitutes part of the Key of the record.""" class List(OptionBase): - """ + r""" Value takes the form of a list. If the value regex contains nested match groups in the form (?Pregex), instead of adding a string to the list, we add a dictionary of the groups. Eg. - Value List ((?P\w+)\s+(?P\d+)) would create results like [{'name': 'Bob', 'age': 32}] + Value List ((?P\w+)\s+(?P\d+)) would create results like: + [{'name': 'Bob', 'age': 32}] Do not give nested groups the same name as other values in the template. """ @@ -187,15 +195,15 @@ def OnCreateOptions(self): def OnAssignVar(self): # Nested matches will have more than one match group if self.value.compiled_regex.groups > 1: - match = self.value.compiled_regex.match(self.value.value) + match = self.value.compiled_regex.match(self.value.value) else: - 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 + 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()) + self._value.append(match.groupdict()) else: - self._value.append(self.value.value) + self._value.append(self.value.value) def OnClearVar(self): if 'Filldown' not in self.value.OptionNames(): @@ -221,6 +229,7 @@ class TextFSMValue(object): '(.*) is the regular expression to match in the input data. Attributes: + compiled_regex: (regexp), Compiled regex for nested matching of List values. max_name_len: (int), maximum character length os a variable name. name: (str), Name of the value. options: (list), A list of current Value Options. @@ -311,12 +320,13 @@ def Parse(self, value): self.template = re.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: - raise TextFSMTemplateError(str(e)) + # Compile and store the regex object only on List-type values for use in + # nested matching + if any([isinstance(x, TextFSMOptions.List) for x in self.options]): + try: + self.compiled_regex = re.compile(self.regex) + except re.error as e: + raise TextFSMTemplateError(str(e)) def _AddOption(self, name): """Add an option to this Value. @@ -359,7 +369,6 @@ def __str__(self): class CopyableRegexObject(object): """Like a re.RegexObject, but can be copied.""" - # pylint: disable=C6409 def __init__(self, pattern): self.pattern = pattern @@ -580,7 +589,7 @@ def __init__(self, template, options_class=_DEFAULT_OPTIONS): self.Reset() def __str__(self): - """Returns the FSM template, mimic the input file.""" + """Returns the FSM template, mimicing the input file.""" result = '\n'.join([str(value) for value in self.values]) result += '\n' @@ -701,7 +710,8 @@ def _ParseFSMVariables(self, template): # Blank line signifies end of Value definitions. if not line: return - + if not isinstance(line, six.string_types): + line = line.decode('utf-8') # Skip commented lines. if self.comment_regex.match(line): continue @@ -769,7 +779,8 @@ def _ParseFSMState(self, template): for line in template: self._line_num += 1 line = line.rstrip() - + if not isinstance(line, six.string_types): + line = line.decode('utf-8') # 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. @@ -796,7 +807,8 @@ def _ParseFSMState(self, template): # Finish rules processing on blank line. if not line: break - + if not isinstance(line, six.string_types): + line = line.decode('utf-8') if self.comment_regex.match(line): continue @@ -963,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.group(value)) def _Operations(self, rule, line): """Operators on the data record. diff --git a/textfsm/terminal.py b/textfsm/terminal.py index 3a39c63..497d7e8 100755 --- a/textfsm/terminal.py +++ b/textfsm/terminal.py @@ -20,10 +20,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function - - -__version__ = '0.1.1' - +from __future__ import unicode_literals import fcntl import getopt @@ -34,7 +31,10 @@ import termios import time import tty +from builtins import object # pylint: disable=redefined-builtin +from builtins import str # pylint: disable=redefined-builtin +__version__ = '0.1.1' # ANSI, ISO/IEC 6429 escape sequences, SGR (Select Graphic Rendition) subset. SGR = { @@ -170,7 +170,7 @@ def EncloseAnsiText(text): def TerminalSize(): """Returns terminal length and width as a tuple.""" try: - with open(os.ctermid(), 'r') as tty_instance: + with open(os.ctermid()) as tty_instance: length_width = struct.unpack( 'hh', fcntl.ioctl(tty_instance.fileno(), termios.TIOCGWINSZ, '1234')) except (IOError, OSError): @@ -477,7 +477,7 @@ def main(argv=None): # Page text supplied in either specified file or stdin. if len(args) == 1: - with open(args[0]) as f: + with open(args[0], 'r') as f: fd = f.read() else: fd = sys.stdin.read() diff --git a/textfsm/texttable.py b/textfsm/texttable.py index 284bb92..1bce5cd 100755 --- a/textfsm/texttable.py +++ b/textfsm/texttable.py @@ -26,11 +26,19 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +from __future__ import unicode_literals + import copy from functools import cmp_to_key import textwrap -# pylint: disable=redefined-builtin -from six.moves import range + +from builtins import next # pylint: disable=redefined-builtin +from builtins import object # pylint: disable=redefined-builtin +from builtins import range # pylint: disable=redefined-builtin +from builtins import str # pylint: disable=redefined-builtin +from builtins import zip # pylint: disable=redefined-builtin +import six + from textfsm import terminal @@ -155,7 +163,7 @@ def get(self, column, default_value=None): except IndexError: return default_value - def index(self, column): # pylint: disable=C6409 + def index(self, column): """Fetches the column number (0 indexed). Args: @@ -342,9 +350,9 @@ def __getitem__(self, row): def __iter__(self): """Iterator that excludes the header row.""" - return self.next() + return next(self) - def next(self): + def __next__(self): # Maintain a counter so a row can know what index it is. # Save the old value to support nested interations. old_iter = self._iterator @@ -378,7 +386,6 @@ def __copy__(self): def Filter(self, function=None): """Construct Textable from the rows of which the function returns true. - Args: function: A function applied to each row which returns a bool. If function is None, all rows with empty column values are @@ -423,7 +430,6 @@ def Map(self, function): new_table.Append(filtered_row) return new_table - # pylint: disable=C6409 # pylint: disable=W0622 def sort(self, cmp=None, key=None, reverse=False): """Sorts rows in the texttable. @@ -507,7 +513,6 @@ def extend(self, table, keys=None): row1[column] = row2[column] break - # pylint: enable=C6409 def Remove(self, row): """Removes a row from the table. @@ -1027,6 +1032,8 @@ def CsvToTable(self, buf, header=True, separator=','): line = buf.readline() header_str = '' while not header_str: + if not isinstance(line, six.string_types): + line = line.decode('utf-8') # Remove comments. header_str = line.split('#')[0].strip() if not header_str: @@ -1046,6 +1053,8 @@ def CsvToTable(self, buf, header=True, separator=','): # xreadlines would be better but not supported by StringIO for testing. for line in buf: + if not isinstance(line, six.string_types): + line = line.decode('utf-8') # Support commented lines, provide '#' is first character of line. if line.startswith('#'): continue @@ -1071,7 +1080,7 @@ def CsvToTable(self, buf, header=True, separator=','): return self.size - def index(self, name=None): # pylint: disable=C6409 + def index(self, name=None): """Returns index number of supplied column name. Args: