forked from pool/python-textfsm
- do not require six
- deleted patches
- remove-future-requirement.patch (part of python-textfsm-no-python2.patch)
- added patches
fix c8843d69da
+ python-textfsm-no-python2.patch
OBS-URL: https://build.opensuse.org/request/show/1142217
OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-textfsm?expand=0&rev=25
2667 lines
93 KiB
Diff
2667 lines
93 KiB
Diff
From c8843d69daa9b565fea99a0283ad13c324d5b563 Mon Sep 17 00:00:00 2001
|
|
From: Daniel Harrison <harro@users.noreply.github.com>
|
|
Date: Mon, 29 Jan 2024 12:48:00 +1100
|
|
Subject: [PATCH] Remove python2 support. (#121)
|
|
|
|
Change triggers release renumber to v2.0.0 (which is to be done when we pull into the stable branch)
|
|
---
|
|
setup.py | 60 ++-
|
|
tests/clitable_test.py | 21 +-
|
|
tests/copyable_regex_object_test.py | 42 --
|
|
tests/terminal_test.py | 11 +-
|
|
tests/textfsm_test.py | 641 ++++++++++++++++------------
|
|
tests/texttable_test.py | 21 +-
|
|
textfsm/clitable.py | 40 +-
|
|
textfsm/copyable_regex_object.py | 41 --
|
|
textfsm/parser.py | 188 ++++----
|
|
textfsm/terminal.py | 159 +++----
|
|
textfsm/texttable.py | 152 +++----
|
|
11 files changed, 689 insertions(+), 687 deletions(-)
|
|
delete mode 100755 tests/copyable_regex_object_test.py
|
|
delete mode 100755 textfsm/copyable_regex_object.py
|
|
|
|
Index: textfsm-1.1.3/setup.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/setup.py
|
|
+++ textfsm-1.1.3/setup.py
|
|
@@ -16,42 +16,40 @@
|
|
|
|
"""Setup script."""
|
|
|
|
-from setuptools import setup, find_packages
|
|
-import textfsm
|
|
# To use a consistent encoding
|
|
from codecs import open
|
|
from os import path
|
|
+from setuptools import find_packages, setup
|
|
+import textfsm
|
|
|
|
here = path.abspath(path.dirname(__file__))
|
|
|
|
# Get the long description from the README file
|
|
-with open(path.join(here, 'README.md'), encoding="utf8") as f:
|
|
- long_description = f.read()
|
|
+with open(path.join(here, 'README.md'), encoding='utf8') as f:
|
|
+ long_description = f.read()
|
|
|
|
-setup(name='textfsm',
|
|
- maintainer='Google',
|
|
- maintainer_email='textfsm-dev@googlegroups.com',
|
|
- version=textfsm.__version__,
|
|
- description='Python module for parsing semi-structured text into python tables.',
|
|
- long_description=long_description,
|
|
- long_description_content_type='text/markdown',
|
|
- url='https://github.com/google/textfsm',
|
|
- license='Apache License, Version 2.0',
|
|
- classifiers=[
|
|
- 'Development Status :: 5 - Production/Stable',
|
|
- 'Intended Audience :: Developers',
|
|
- 'License :: OSI Approved :: Apache Software License',
|
|
- 'Operating System :: OS Independent',
|
|
- 'Programming Language :: Python :: 2',
|
|
- 'Programming Language :: Python :: 3',
|
|
- 'Topic :: Software Development :: Libraries'],
|
|
- packages=['textfsm'],
|
|
- entry_points={
|
|
- 'console_scripts': [
|
|
- 'textfsm=textfsm.parser:main'
|
|
- ]
|
|
- },
|
|
- include_package_data=True,
|
|
- package_data={'textfsm': ['../testdata/*']},
|
|
- install_requires=['six', 'future'],
|
|
- )
|
|
+setup(
|
|
+ name='textfsm',
|
|
+ maintainer='Google',
|
|
+ maintainer_email='textfsm-dev@googlegroups.com',
|
|
+ version=textfsm.__version__,
|
|
+ description=(
|
|
+ 'Python module for parsing semi-structured text into python tables.'
|
|
+ ),
|
|
+ long_description=long_description,
|
|
+ long_description_content_type='text/markdown',
|
|
+ url='https://github.com/google/textfsm',
|
|
+ license='Apache License, Version 2.0',
|
|
+ classifiers=[
|
|
+ 'Development Status :: 5 - Production/Stable',
|
|
+ 'Intended Audience :: Developers',
|
|
+ 'License :: OSI Approved :: Apache Software License',
|
|
+ 'Operating System :: OS Independent',
|
|
+ 'Programming Language :: Python :: 3',
|
|
+ 'Topic :: Software Development :: Libraries',
|
|
+ ],
|
|
+ packages=['textfsm'],
|
|
+ entry_points={'console_scripts': ['textfsm=textfsm.parser:main']},
|
|
+ include_package_data=True,
|
|
+ package_data={'textfsm': ['../testdata/*']},
|
|
+)
|
|
Index: textfsm-1.1.3/tests/clitable_test.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/tests/clitable_test.py
|
|
+++ textfsm-1.1.3/tests/clitable_test.py
|
|
@@ -16,19 +16,12 @@
|
|
|
|
"""Unittest for clitable script."""
|
|
|
|
-from __future__ import absolute_import
|
|
-from __future__ import division
|
|
-from __future__ import print_function
|
|
-from __future__ import unicode_literals
|
|
-
|
|
import copy
|
|
+import io
|
|
import os
|
|
import re
|
|
import unittest
|
|
-
|
|
-from io import StringIO
|
|
from textfsm import clitable
|
|
-from textfsm import copyable_regex_object
|
|
|
|
|
|
class UnitTestIndexTable(unittest.TestCase):
|
|
@@ -47,8 +40,7 @@ class UnitTestIndexTable(unittest.TestCa
|
|
|
|
self.assertEqual(indx.compiled.size, 3)
|
|
for col in ('Command', 'Vendor', 'Template', 'Hostname'):
|
|
- self.assertTrue(isinstance(indx.compiled[1][col],
|
|
- copyable_regex_object.CopyableRegexObject))
|
|
+ self.assertIsInstance(indx.compiled[1][col], re.Pattern)
|
|
|
|
self.assertTrue(indx.compiled[1]['Hostname'].match('random string'))
|
|
|
|
@@ -66,8 +58,7 @@ class UnitTestIndexTable(unittest.TestCa
|
|
indx = clitable.IndexTable(_PreParse, _PreCompile, file_path)
|
|
self.assertEqual(indx.index[2]['Template'], 'CLITABLE_TEMPLATEC')
|
|
self.assertEqual(indx.index[1]['Command'], 'sh[[ow]] ve[[rsion]]')
|
|
- self.assertTrue(isinstance(indx.compiled[1]['Hostname'],
|
|
- copyable_regex_object.CopyableRegexObject))
|
|
+ self.assertIsInstance(indx.compiled[1]['Hostname'], re.Pattern)
|
|
self.assertFalse(indx.compiled[1]['Command'])
|
|
|
|
def testGetRowMatch(self):
|
|
@@ -101,7 +92,7 @@ class UnitTestCliTable(unittest.TestCase
|
|
'Start\n'
|
|
' ^${Col1} ${Col2} ${Col3} -> Record\n'
|
|
'\n')
|
|
- self.template_file = StringIO(self.template)
|
|
+ self.template_file = io.StringIO(self.template)
|
|
|
|
def testCompletion(self):
|
|
"""Tests '[[]]' syntax replacement."""
|
|
@@ -123,7 +114,7 @@ class UnitTestCliTable(unittest.TestCase
|
|
|
|
self.assertEqual('sh(o(w)?)? ve(r(s(i(o(n)?)?)?)?)?',
|
|
self.clitable.index.index[1]['Command'])
|
|
- self.assertEqual(None, self.clitable.index.compiled[1]['Template'])
|
|
+ self.assertIsNone(self.clitable.index.compiled[1]['Template'])
|
|
self.assertTrue(
|
|
self.clitable.index.compiled[1]['Command'].match('sho vers'))
|
|
|
|
@@ -267,7 +258,7 @@ class UnitTestCliTable(unittest.TestCase
|
|
'Start\n'
|
|
' ^${Col1} ${Col2} ${Col3} -> Record\n'
|
|
'\n')
|
|
- self.template_file = StringIO(self.template)
|
|
+ self.template_file = io.StringIO(self.template)
|
|
self.clitable._TemplateNamesToFiles = lambda t: [self.template_file]
|
|
self.clitable.ParseCmd(self.input_data + input_data2,
|
|
attributes={'Command': 'sh ver'})
|
|
Index: textfsm-1.1.3/tests/textfsm_test.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/tests/textfsm_test.py
|
|
+++ textfsm-1.1.3/tests/textfsm_test.py
|
|
@@ -16,16 +16,9 @@
|
|
# permissions and limitations under the License.
|
|
|
|
"""Unittest for textfsm module."""
|
|
-from __future__ import absolute_import
|
|
-from __future__ import division
|
|
-from __future__ import print_function
|
|
-from __future__ import unicode_literals
|
|
|
|
-from builtins import str
|
|
+import io
|
|
import unittest
|
|
-from io import StringIO
|
|
-
|
|
-
|
|
|
|
import textfsm
|
|
|
|
@@ -60,27 +53,27 @@ class UnitTestFSM(unittest.TestCase):
|
|
self.assertEqual(v.OptionNames(), ['Required'])
|
|
|
|
# regex must be bounded by parenthesis.
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- v.Parse,
|
|
- 'Value beer (boo(hoo)))boo')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- v.Parse,
|
|
- 'Value beer boo(boo(hoo)))')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- v.Parse,
|
|
- 'Value beer (boo)hoo)')
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, v.Parse, 'Value beer (boo(hoo)))boo'
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, v.Parse, 'Value beer boo(boo(hoo)))'
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, v.Parse, 'Value beer (boo)hoo)'
|
|
+ )
|
|
|
|
# Escaped parentheses don't count.
|
|
v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
|
|
v.Parse(r'Value beer (boo\)hoo)')
|
|
self.assertEqual(v.name, 'beer')
|
|
self.assertEqual(v.regex, r'(boo\)hoo)')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- v.Parse,
|
|
- r'Value beer (boohoo\)')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- v.Parse,
|
|
- r'Value beer (boo)hoo\)')
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, v.Parse, r'Value beer (boohoo\)'
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, v.Parse, r'Value beer (boo)hoo\)'
|
|
+ )
|
|
|
|
# Unbalanced parenthesis can exist if within square "[]" braces.
|
|
v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
|
|
@@ -89,17 +82,16 @@ class UnitTestFSM(unittest.TestCase):
|
|
self.assertEqual(v.regex, '(boo[(]hoo)')
|
|
|
|
# Escaped braces don't count.
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- v.Parse,
|
|
- r'Value beer (boo\[)\]hoo)')
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, v.Parse, r'Value beer (boo\[)\]hoo)'
|
|
+ )
|
|
|
|
# String function.
|
|
v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
|
|
v.Parse('Value Required beer (boo(hoo))')
|
|
self.assertEqual(str(v), 'Value Required beer (boo(hoo))')
|
|
v = textfsm.TextFSMValue(options_class=textfsm.TextFSMOptions)
|
|
- v.Parse(
|
|
- r'Value Required,Filldown beer (bo\S+(hoo))')
|
|
+ v.Parse(r'Value Required,Filldown beer (bo\S+(hoo))')
|
|
self.assertEqual(str(v), r'Value Required,Filldown beer (bo\S+(hoo))')
|
|
|
|
def testFSMRule(self):
|
|
@@ -144,144 +136,174 @@ class UnitTestFSM(unittest.TestCase):
|
|
self.assertEqual(r.record_op, 'NoRecord')
|
|
|
|
# Bad syntax tests.
|
|
- self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
|
|
- ' ^A beer called ${beer} -> Next Next Next')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
|
|
- ' ^A beer called ${beer} -> Boo.hoo')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
|
|
- ' ^A beer called ${beer} -> Continue.Record $Hi')
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError,
|
|
+ textfsm.TextFSMRule,
|
|
+ ' ^A beer called ${beer} -> Next Next Next',
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError,
|
|
+ textfsm.TextFSMRule,
|
|
+ ' ^A beer called ${beer} -> Boo.hoo',
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError,
|
|
+ textfsm.TextFSMRule,
|
|
+ ' ^A beer called ${beer} -> Continue.Record $Hi',
|
|
+ )
|
|
|
|
def testRulePrefixes(self):
|
|
"""Test valid and invalid rule prefixes."""
|
|
|
|
# Bad syntax tests.
|
|
for prefix in (' ', '.^', ' \t', ''):
|
|
- f = StringIO('Value unused (.)\n\nStart\n' + prefix + 'A simple string.')
|
|
+ f = io.StringIO(
|
|
+ 'Value unused (.)\n\nStart\n' + prefix + 'A simple string.'
|
|
+ )
|
|
self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
|
|
|
|
# Good syntax tests.
|
|
for prefix in (' ^', ' ^', '\t^'):
|
|
- f = StringIO('Value unused (.)\n\nStart\n' + prefix + 'A simple string.')
|
|
+ f = io.StringIO(
|
|
+ 'Value unused (.)\n\nStart\n' + prefix + 'A simple string.'
|
|
+ )
|
|
self.assertIsNotNone(textfsm.TextFSM(f))
|
|
|
|
def testImplicitDefaultRules(self):
|
|
|
|
- for line in (' ^A beer called ${beer} -> Record End',
|
|
- ' ^A beer called ${beer} -> End',
|
|
- ' ^A beer called ${beer} -> Next.NoRecord End',
|
|
- ' ^A beer called ${beer} -> Clear End',
|
|
- ' ^A beer called ${beer} -> Error "Hello World"'):
|
|
+ for line in (
|
|
+ ' ^A beer called ${beer} -> Record End',
|
|
+ ' ^A beer called ${beer} -> End',
|
|
+ ' ^A beer called ${beer} -> Next.NoRecord End',
|
|
+ ' ^A beer called ${beer} -> Clear End',
|
|
+ ' ^A beer called ${beer} -> Error "Hello World"',
|
|
+ ):
|
|
r = textfsm.TextFSMRule(line)
|
|
self.assertEqual(str(r), line)
|
|
|
|
- for line in (' ^A beer called ${beer} -> Next "Hello World"',
|
|
- ' ^A beer called ${beer} -> Record.Next',
|
|
- ' ^A beer called ${beer} -> Continue End',
|
|
- ' ^A beer called ${beer} -> Beer End'):
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- textfsm.TextFSMRule, line)
|
|
+ for line in (
|
|
+ ' ^A beer called ${beer} -> Next "Hello World"',
|
|
+ ' ^A beer called ${beer} -> Record.Next',
|
|
+ ' ^A beer called ${beer} -> Continue End',
|
|
+ ' ^A beer called ${beer} -> Beer End',
|
|
+ ):
|
|
+ self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule, line)
|
|
|
|
def testSpacesAroundAction(self):
|
|
- for line in (' ^Hello World -> Boo',
|
|
- ' ^Hello World -> Boo',
|
|
- ' ^Hello World -> Boo'):
|
|
- self.assertEqual(
|
|
- str(textfsm.TextFSMRule(line)), ' ^Hello World -> Boo')
|
|
+ for line in (
|
|
+ ' ^Hello World -> Boo',
|
|
+ ' ^Hello World -> Boo',
|
|
+ ' ^Hello World -> Boo',
|
|
+ ):
|
|
+ self.assertEqual(str(textfsm.TextFSMRule(line)), ' ^Hello World -> Boo')
|
|
|
|
# A '->' without a leading space is considered part of the matching line.
|
|
- self.assertEqual(' A simple line-> Boo -> Next',
|
|
- str(textfsm.TextFSMRule(' A simple line-> Boo -> Next')))
|
|
+ self.assertEqual(
|
|
+ ' A simple line-> Boo -> Next',
|
|
+ str(textfsm.TextFSMRule(' A simple line-> Boo -> Next')),
|
|
+ )
|
|
|
|
def testParseFSMVariables(self):
|
|
# Trivial template to initiate object.
|
|
- f = StringIO('Value unused (.)\n\nStart\n')
|
|
+ f = io.StringIO('Value unused (.)\n\nStart\n')
|
|
t = textfsm.TextFSM(f)
|
|
|
|
# Trivial entry
|
|
buf = 'Value Filldown Beer (beer)\n\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t._ParseFSMVariables(f)
|
|
|
|
# Single variable with commented header.
|
|
buf = '# Headline\nValue Filldown Beer (beer)\n\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t._ParseFSMVariables(f)
|
|
self.assertEqual(str(t._GetValue('Beer')), 'Value Filldown Beer (beer)')
|
|
|
|
# Multiple variables.
|
|
- buf = ('# Headline\n'
|
|
- 'Value Filldown Beer (beer)\n'
|
|
- 'Value Required Spirits (whiskey)\n'
|
|
- 'Value Filldown Wine (claret)\n'
|
|
- '\n')
|
|
+ buf = (
|
|
+ '# Headline\n'
|
|
+ 'Value Filldown Beer (beer)\n'
|
|
+ 'Value Required Spirits (whiskey)\n'
|
|
+ 'Value Filldown Wine (claret)\n'
|
|
+ '\n'
|
|
+ )
|
|
t._line_num = 0
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t._ParseFSMVariables(f)
|
|
self.assertEqual(str(t._GetValue('Beer')), 'Value Filldown Beer (beer)')
|
|
self.assertEqual(
|
|
- str(t._GetValue('Spirits')), 'Value Required Spirits (whiskey)')
|
|
+ str(t._GetValue('Spirits')), 'Value Required Spirits (whiskey)'
|
|
+ )
|
|
self.assertEqual(str(t._GetValue('Wine')), 'Value Filldown Wine (claret)')
|
|
|
|
# Multiple variables.
|
|
- buf = ('# Headline\n'
|
|
- 'Value Filldown Beer (beer)\n'
|
|
- ' # A comment\n'
|
|
- 'Value Spirits ()\n'
|
|
- 'Value Filldown,Required Wine ((c|C)laret)\n'
|
|
- '\n')
|
|
+ buf = (
|
|
+ '# Headline\n'
|
|
+ 'Value Filldown Beer (beer)\n'
|
|
+ ' # A comment\n'
|
|
+ 'Value Spirits ()\n'
|
|
+ 'Value Filldown,Required Wine ((c|C)laret)\n'
|
|
+ '\n'
|
|
+ )
|
|
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t._ParseFSMVariables(f)
|
|
self.assertEqual(str(t._GetValue('Beer')), 'Value Filldown Beer (beer)')
|
|
+ self.assertEqual(str(t._GetValue('Spirits')), 'Value Spirits ()')
|
|
self.assertEqual(
|
|
- str(t._GetValue('Spirits')), 'Value Spirits ()')
|
|
- self.assertEqual(str(t._GetValue('Wine')),
|
|
- 'Value Filldown,Required Wine ((c|C)laret)')
|
|
+ str(t._GetValue('Wine')), 'Value Filldown,Required Wine ((c|C)laret)'
|
|
+ )
|
|
|
|
# Malformed variables.
|
|
buf = 'Value Beer (beer) beer'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
|
|
|
|
buf = 'Value Filldown, Required Spirits ()'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
|
|
buf = 'Value filldown,Required Wine ((c|C)laret)'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
|
|
|
|
# Values that look bad but are OK.
|
|
- buf = ('# Headline\n'
|
|
- 'Value Filldown Beer (bee(r), (and) (M)ead$)\n'
|
|
- '# A comment\n'
|
|
- 'Value Spirits,and,some ()\n'
|
|
- 'Value Filldown,Required Wine ((c|C)laret)\n'
|
|
- '\n')
|
|
- f = StringIO(buf)
|
|
+ buf = (
|
|
+ '# Headline\n'
|
|
+ 'Value Filldown Beer (bee(r), (and) (M)ead$)\n'
|
|
+ '# A comment\n'
|
|
+ 'Value Spirits,and,some ()\n'
|
|
+ 'Value Filldown,Required Wine ((c|C)laret)\n'
|
|
+ '\n'
|
|
+ )
|
|
+ f = io.StringIO(buf)
|
|
t._ParseFSMVariables(f)
|
|
- self.assertEqual(str(t._GetValue('Beer')),
|
|
- 'Value Filldown Beer (bee(r), (and) (M)ead$)')
|
|
self.assertEqual(
|
|
- str(t._GetValue('Spirits,and,some')), 'Value Spirits,and,some ()')
|
|
- self.assertEqual(str(t._GetValue('Wine')),
|
|
- 'Value Filldown,Required Wine ((c|C)laret)')
|
|
+ str(t._GetValue('Beer')), 'Value Filldown Beer (bee(r), (and) (M)ead$)'
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ str(t._GetValue('Spirits,and,some')), 'Value Spirits,and,some ()'
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ str(t._GetValue('Wine')), 'Value Filldown,Required Wine ((c|C)laret)'
|
|
+ )
|
|
|
|
# Variable name too long.
|
|
- buf = ('Value Filldown '
|
|
- 'nametoolong_nametoolong_nametoolo_nametoolong_nametoolong '
|
|
- '(beer)\n\n')
|
|
- f = StringIO(buf)
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- t._ParseFSMVariables, f)
|
|
+ buf = (
|
|
+ 'Value Filldown '
|
|
+ 'nametoolong_nametoolong_nametoolo_nametoolong_nametoolong '
|
|
+ '(beer)\n\n'
|
|
+ )
|
|
+ f = io.StringIO(buf)
|
|
+ self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMVariables, f)
|
|
|
|
def testParseFSMState(self):
|
|
|
|
- f = StringIO('Value Beer (.)\nValue Wine (\\w)\n\nStart\n')
|
|
+ f = io.StringIO('Value Beer (.)\nValue Wine (\\w)\n\nStart\n')
|
|
t = textfsm.TextFSM(f)
|
|
|
|
# Fails as we already have 'Start' state.
|
|
buf = 'Start\n ^.\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
|
|
|
|
# Remove start so we can test new Start state.
|
|
@@ -289,7 +311,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# Single state.
|
|
buf = '# Headline\nStart\n ^.\n\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t._ParseFSMState(f)
|
|
self.assertEqual(str(t.states['Start'][0]), ' ^.')
|
|
try:
|
|
@@ -299,7 +321,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# Multiple states.
|
|
buf = '# Headline\nStart\n ^.\n ^Hello World\n ^Last-[Cc]ha$$nge\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t._line_num = 0
|
|
t.states = {}
|
|
t._ParseFSMState(f)
|
|
@@ -315,21 +337,23 @@ class UnitTestFSM(unittest.TestCase):
|
|
t.states = {}
|
|
# Malformed states.
|
|
buf = 'St%art\n ^.\n ^Hello World\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
|
|
|
|
buf = 'Start\n^.\n ^Hello World\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
|
|
|
|
buf = ' Start\n ^.\n ^Hello World\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
|
|
|
|
# Multiple variables and substitution (depends on _ParseFSMVariables).
|
|
- buf = ('# Headline\nStart\n ^.${Beer}${Wine}.\n'
|
|
- ' ^Hello $Beer\n ^Last-[Cc]ha$$nge\n')
|
|
- f = StringIO(buf)
|
|
+ buf = (
|
|
+ '# Headline\nStart\n ^.${Beer}${Wine}.\n'
|
|
+ ' ^Hello $Beer\n ^Last-[Cc]ha$$nge\n'
|
|
+ )
|
|
+ f = io.StringIO(buf)
|
|
t.states = {}
|
|
t._ParseFSMState(f)
|
|
self.assertEqual(str(t.states['Start'][0]), ' ^.${Beer}${Wine}.')
|
|
@@ -344,43 +368,52 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# State name too long (>32 char).
|
|
buf = 'rnametoolong_nametoolong_nametoolong_nametoolong_nametoolo\n ^.\n\n'
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
self.assertRaises(textfsm.TextFSMTemplateError, t._ParseFSMState, f)
|
|
|
|
def testInvalidStates(self):
|
|
|
|
# 'Continue' should not accept a destination.
|
|
- self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
|
|
- '^.* -> Continue Start')
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError,
|
|
+ textfsm.TextFSMRule,
|
|
+ '^.* -> Continue Start',
|
|
+ )
|
|
|
|
# 'Error' accepts a text string but "next' state does not.
|
|
- self.assertEqual(str(textfsm.TextFSMRule(' ^ -> Error "hi there"')),
|
|
- ' ^ -> Error "hi there"')
|
|
- self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSMRule,
|
|
- '^.* -> Next "Hello World"')
|
|
+ self.assertEqual(
|
|
+ str(textfsm.TextFSMRule(' ^ -> Error "hi there"')),
|
|
+ ' ^ -> Error "hi there"',
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError,
|
|
+ textfsm.TextFSMRule,
|
|
+ '^.* -> Next "Hello World"',
|
|
+ )
|
|
|
|
def testRuleStartsWithCarrot(self):
|
|
|
|
- f = StringIO(
|
|
- 'Value Beer (.)\nValue Wine (\\w)\n\nStart\n A Simple line')
|
|
+ f = io.StringIO(
|
|
+ 'Value Beer (.)\nValue Wine (\\w)\n\nStart\n A Simple line'
|
|
+ )
|
|
self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
|
|
|
|
def testValidateFSM(self):
|
|
|
|
# No Values.
|
|
- f = StringIO('\nNotStart\n')
|
|
+ f = io.StringIO('\nNotStart\n')
|
|
self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
|
|
|
|
# No states.
|
|
- f = StringIO('Value unused (.)\n\n')
|
|
+ f = io.StringIO('Value unused (.)\n\n')
|
|
self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
|
|
|
|
# No 'Start' state.
|
|
- f = StringIO('Value unused (.)\n\nNotStart\n')
|
|
+ f = io.StringIO('Value unused (.)\n\nNotStart\n')
|
|
self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, f)
|
|
|
|
# Has 'Start' state with valid destination
|
|
- f = StringIO('Value unused (.)\n\nStart\n')
|
|
+ f = io.StringIO('Value unused (.)\n\nStart\n')
|
|
t = textfsm.TextFSM(f)
|
|
t.states['Start'] = []
|
|
t.states['Start'].append(textfsm.TextFSMRule('^.* -> Start'))
|
|
@@ -412,14 +445,14 @@ class UnitTestFSM(unittest.TestCase):
|
|
# Trivial template
|
|
buf = 'Value Beer (.*)\n\nStart\n ^\\w\n'
|
|
buf_result = buf
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t = textfsm.TextFSM(f)
|
|
self.assertEqual(str(t), buf_result)
|
|
|
|
# Slightly more complex, multple vars.
|
|
buf = 'Value A (.*)\nValue B (.*)\n\nStart\n ^\\w\n\nState1\n ^.\n'
|
|
buf_result = buf
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t = textfsm.TextFSM(f)
|
|
self.assertEqual(str(t), buf_result)
|
|
|
|
@@ -427,7 +460,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# Trivial FSM, no records produced.
|
|
tplt = 'Value unused (.)\n\nStart\n ^Trivial SFM\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
data = 'Non-matching text\nline1\nline 2\n'
|
|
self.assertFalse(t.ParseText(data))
|
|
@@ -437,7 +470,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# Simple FSM, One Variable no options.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next.Record\n\nEOF\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
# Matching one line.
|
|
# Tests 'Next' & 'Record' actions.
|
|
@@ -452,10 +485,12 @@ class UnitTestFSM(unittest.TestCase):
|
|
self.assertListEqual(result, [['Matching text'], ['And again']])
|
|
|
|
# Two Variables and singular options.
|
|
- tplt = ('Value Required boo (one)\nValue Filldown hoo (two)\n\n'
|
|
- 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF\n')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value Required boo (one)\nValue Filldown hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF\n'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
# Matching two lines. Only one records returned due to 'Required' flag.
|
|
# Tests 'Filldown' and 'Required' options.
|
|
@@ -463,7 +498,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['one', 'two']])
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
# Matching two lines. Two records returned due to 'Filldown' flag.
|
|
data = 'two\none\none'
|
|
t.Reset()
|
|
@@ -471,11 +506,13 @@ class UnitTestFSM(unittest.TestCase):
|
|
self.assertListEqual(result, [['one', 'two'], ['one', 'two']])
|
|
|
|
# Multiple Variables and options.
|
|
- tplt = ('Value Required,Filldown boo (one)\n'
|
|
- 'Value Filldown,Required hoo (two)\n\n'
|
|
- 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF\n')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value Required,Filldown boo (one)\n'
|
|
+ 'Value Filldown,Required hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF\n'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'two\none\none'
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['one', 'two'], ['one', 'two']])
|
|
@@ -484,7 +521,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# Trivial FSM, no records produced.
|
|
tplt = 'Value unused (.)\n\nStart\n ^Trivial SFM\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
data = 'Non-matching text\nline1\nline 2\n'
|
|
self.assertFalse(t.ParseText(data))
|
|
@@ -494,7 +531,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# Simple FSM, One Variable no options.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next.Record\n\nEOF\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
# Matching one line.
|
|
# Tests 'Next' & 'Record' actions.
|
|
@@ -506,14 +543,17 @@ class UnitTestFSM(unittest.TestCase):
|
|
t.Reset()
|
|
data = 'Matching text\nAnd again'
|
|
result = t.ParseTextToDicts(data)
|
|
- self.assertListEqual(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'
|
|
- 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF\n')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value Required boo (one)\nValue Filldown hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF\n'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
# Matching two lines. Only one records returned due to 'Required' flag.
|
|
# Tests 'Filldown' and 'Required' options.
|
|
@@ -521,30 +561,34 @@ class UnitTestFSM(unittest.TestCase):
|
|
result = t.ParseTextToDicts(data)
|
|
self.assertListEqual(result, [{'hoo': 'two', 'boo': 'one'}])
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
# Matching two lines. Two records returned due to 'Filldown' flag.
|
|
data = 'two\none\none'
|
|
t.Reset()
|
|
result = t.ParseTextToDicts(data)
|
|
self.assertListEqual(
|
|
- result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}])
|
|
+ result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]
|
|
+ )
|
|
|
|
# Multiple Variables and options.
|
|
- tplt = ('Value Required,Filldown boo (one)\n'
|
|
- 'Value Filldown,Required hoo (two)\n\n'
|
|
- 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF\n')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value Required,Filldown boo (one)\n'
|
|
+ 'Value Filldown,Required hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF\n'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'two\none\none'
|
|
result = t.ParseTextToDicts(data)
|
|
self.assertListEqual(
|
|
- result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}])
|
|
+ result, [{'hoo': 'two', 'boo': 'one'}, {'hoo': 'two', 'boo': 'one'}]
|
|
+ )
|
|
|
|
def testParseNullText(self):
|
|
|
|
# Simple FSM, One Variable no options.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next.Record\n\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
# Null string
|
|
data = ''
|
|
@@ -554,181 +598,210 @@ class UnitTestFSM(unittest.TestCase):
|
|
def testReset(self):
|
|
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next.Record\n\nEOF\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'Matching text'
|
|
result1 = t.ParseText(data)
|
|
t.Reset()
|
|
result2 = t.ParseText(data)
|
|
self.assertListEqual(result1, result2)
|
|
|
|
- tplt = ('Value boo (one)\nValue hoo (two)\n\n'
|
|
- 'Start\n ^$boo -> State1\n\n'
|
|
- 'State1\n ^$hoo -> Start\n\n'
|
|
- 'EOF')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value boo (one)\nValue hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> State1\n\n'
|
|
+ 'State1\n ^$hoo -> Start\n\n'
|
|
+ 'EOF'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
data = 'one'
|
|
t.ParseText(data)
|
|
t.Reset()
|
|
self.assertEqual(t._cur_state[0].match, '^$boo')
|
|
- self.assertEqual(t._GetValue('boo').value, None)
|
|
- self.assertEqual(t._GetValue('hoo').value, None)
|
|
+ self.assertIsNone(None, t._GetValue('boo').value)
|
|
+ self.assertIsNone(t._GetValue('hoo').value)
|
|
self.assertEqual(t._result, [])
|
|
|
|
def testClear(self):
|
|
|
|
# Clear Filldown variable.
|
|
# Tests 'Clear'.
|
|
- tplt = ('Value Required boo (on.)\n'
|
|
- 'Value Filldown,Required hoo (tw.)\n\n'
|
|
- 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Clear')
|
|
+ tplt = (
|
|
+ 'Value Required boo (on.)\n'
|
|
+ 'Value Filldown,Required hoo (tw.)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Record\n ^$hoo -> Next.Clear'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one\ntwo\nonE\ntwO'
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['onE', 'two']])
|
|
|
|
# Clearall, with Filldown variable.
|
|
# Tests 'Clearall'.
|
|
- tplt = ('Value Filldown boo (on.)\n'
|
|
- 'Value Filldown hoo (tw.)\n\n'
|
|
- 'Start\n ^$boo -> Next.Clearall\n'
|
|
- ' ^$hoo')
|
|
+ tplt = (
|
|
+ 'Value Filldown boo (on.)\n'
|
|
+ 'Value Filldown hoo (tw.)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Clearall\n'
|
|
+ ' ^$hoo'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one\ntwo'
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['', 'two']])
|
|
|
|
def testContinue(self):
|
|
|
|
- tplt = ('Value Required boo (on.)\n'
|
|
- 'Value Filldown,Required hoo (on.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Continue.Record')
|
|
+ tplt = (
|
|
+ 'Value Required boo (on.)\n'
|
|
+ 'Value Filldown,Required hoo (on.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Continue.Record'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one\non0'
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['one', 'one'], ['on0', 'on0']])
|
|
|
|
def testError(self):
|
|
|
|
- tplt = ('Value Required boo (on.)\n'
|
|
- 'Value Filldown,Required hoo (on.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Error')
|
|
+ tplt = (
|
|
+ 'Value Required boo (on.)\n'
|
|
+ 'Value Filldown,Required hoo (on.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Error'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one'
|
|
self.assertRaises(textfsm.TextFSMError, t.ParseText, data)
|
|
|
|
- tplt = ('Value Required boo (on.)\n'
|
|
- 'Value Filldown,Required hoo (on.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Error "Hello World"')
|
|
+ tplt = (
|
|
+ 'Value Required boo (on.)\n'
|
|
+ 'Value Filldown,Required hoo (on.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Error "Hello World"'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
self.assertRaises(textfsm.TextFSMError, t.ParseText, data)
|
|
|
|
def testKey(self):
|
|
- tplt = ('Value Required boo (on.)\n'
|
|
- 'Value Required,Key hoo (on.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Record')
|
|
-
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
- self.assertTrue('Key' in t._GetValue('hoo').OptionNames())
|
|
- self.assertTrue('Key' not in t._GetValue('boo').OptionNames())
|
|
+ tplt = (
|
|
+ 'Value Required boo (on.)\n'
|
|
+ 'Value Required,Key hoo (on.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Record'
|
|
+ )
|
|
+
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
+ self.assertIn('Key', t._GetValue('hoo').OptionNames())
|
|
+ self.assertNotIn('Key', t._GetValue('boo').OptionNames())
|
|
|
|
def testList(self):
|
|
|
|
- tplt = ('Value List boo (on.)\n'
|
|
- 'Value hoo (tw.)\n\n'
|
|
- 'Start\n ^$boo\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF')
|
|
+ tplt = (
|
|
+ 'Value List boo (on.)\n'
|
|
+ 'Value hoo (tw.)\n\n'
|
|
+ 'Start\n ^$boo\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one\ntwo\non0\ntw0'
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [[['one'], 'two'], [['on0'], 'tw0']])
|
|
|
|
- tplt = ('Value List,Filldown boo (on.)\n'
|
|
- 'Value hoo (on.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF')
|
|
+ tplt = (
|
|
+ 'Value List,Filldown boo (on.)\n'
|
|
+ 'Value hoo (on.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one\non0\non1'
|
|
result = t.ParseText(data)
|
|
- self.assertEqual(result, ([[['one'], 'one'],
|
|
- [['one', 'on0'], 'on0'],
|
|
- [['one', 'on0', 'on1'], 'on1']]))
|
|
-
|
|
- tplt = ('Value List,Required boo (on.)\n'
|
|
- 'Value hoo (tw.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Next.Record\n\n'
|
|
- 'EOF')
|
|
+ self.assertEqual(
|
|
+ result,
|
|
+ ([
|
|
+ [['one'], 'one'],
|
|
+ [['one', 'on0'], 'on0'],
|
|
+ [['one', 'on0', 'on1'], 'on1'],
|
|
+ ]),
|
|
+ )
|
|
+
|
|
+ tplt = (
|
|
+ 'Value List,Required boo (on.)\n'
|
|
+ 'Value hoo (tw.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Next.Record\n\n'
|
|
+ 'EOF'
|
|
+ )
|
|
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'one\ntwo\ntw2'
|
|
result = t.ParseText(data)
|
|
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.
|
|
-
|
|
- 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 = (
|
|
- # A nested group is called "name"
|
|
- r"Value List foo ((?P<name>\w+):\s+(?P<age>\d+)\s+(?P<state>\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))
|
|
- # 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.assertListEqual(
|
|
- result, (
|
|
- [[[
|
|
- {'name': 'Bob', 'age': '32', 'state': 'NC'},
|
|
- {'name': 'Alice', 'age': '27', 'state': 'NY'},
|
|
- {'name': 'Jeff', 'age': '45', 'state': 'CA'}
|
|
- ], 'Julia']]
|
|
- )
|
|
- )
|
|
+ """List-type values with nested regex capture groups are parsed correctly.
|
|
|
|
- def testNestedNameConflict(self):
|
|
- tplt = (
|
|
- # Two nested groups are called "name"
|
|
- r"Value List foo ((?P<name>\w+)\s+(?P<name>\w+):\s+(?P<age>\d+)\s+(?P<state>\w{2})\s*)"
|
|
- "\nStart\n"
|
|
- r"^\s*${foo}"
|
|
- "\n ^"
|
|
- r"\s*$$ -> Record"
|
|
- )
|
|
- self.assertRaises(textfsm.TextFSMTemplateError, textfsm.TextFSM, StringIO(tplt))
|
|
+ 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 = (
|
|
+ # A nested group is called "name"
|
|
+ r'Value List foo ((?P<name>\w+):\s+(?P<age>\d+)\s+(?P<state>\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(io.StringIO(tplt))
|
|
+ # 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.assertListEqual(
|
|
+ result,
|
|
+ ([[
|
|
+ [
|
|
+ {'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"
|
|
+ r'Value List foo'
|
|
+ r' ((?P<name>\w+)\s+(?P<name>\w+):\s+(?P<age>\d+)\s+(?P<state>\w{2})\s*)'
|
|
+ '\nStart\n'
|
|
+ r'^\s*${foo}'
|
|
+ '\n ^'
|
|
+ r'\s*$$ -> Record'
|
|
+ )
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, textfsm.TextFSM, io.StringIO(tplt)
|
|
+ )
|
|
|
|
def testGetValuesByAttrib(self):
|
|
|
|
- tplt = ('Value Required boo (on.)\n'
|
|
- 'Value Required,List hoo (on.)\n\n'
|
|
- 'Start\n ^$boo -> Continue\n ^$hoo -> Record')
|
|
+ tplt = (
|
|
+ 'Value Required boo (on.)\n'
|
|
+ 'Value Required,List hoo (on.)\n\n'
|
|
+ 'Start\n ^$boo -> Continue\n ^$hoo -> Record'
|
|
+ )
|
|
|
|
# Explicit default.
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
self.assertEqual(t.GetValuesByAttrib('List'), ['hoo'])
|
|
self.assertEqual(t.GetValuesByAttrib('Filldown'), [])
|
|
result = t.GetValuesByAttrib('Required')
|
|
@@ -738,37 +811,41 @@ class UnitTestFSM(unittest.TestCase):
|
|
def testStateChange(self):
|
|
|
|
# Sinple state change, no actions
|
|
- tplt = ('Value boo (one)\nValue hoo (two)\n\n'
|
|
- 'Start\n ^$boo -> State1\n\nState1\n ^$hoo -> Start\n\n'
|
|
- 'EOF')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value boo (one)\nValue hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> State1\n\nState1\n ^$hoo -> Start\n\n'
|
|
+ 'EOF'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
data = 'one'
|
|
t.ParseText(data)
|
|
self.assertEqual(t._cur_state[0].match, '^$hoo')
|
|
self.assertEqual('one', t._GetValue('boo').value)
|
|
- self.assertEqual(None, t._GetValue('hoo').value)
|
|
+ self.assertIsNone(t._GetValue('hoo').value)
|
|
self.assertEqual(t._result, [])
|
|
|
|
# State change with actions.
|
|
- tplt = ('Value boo (one)\nValue hoo (two)\n\n'
|
|
- 'Start\n ^$boo -> Next.Record State1\n\n'
|
|
- 'State1\n ^$hoo -> Start\n\n'
|
|
- 'EOF')
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ tplt = (
|
|
+ 'Value boo (one)\nValue hoo (two)\n\n'
|
|
+ 'Start\n ^$boo -> Next.Record State1\n\n'
|
|
+ 'State1\n ^$hoo -> Start\n\n'
|
|
+ 'EOF'
|
|
+ )
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
data = 'one'
|
|
t.ParseText(data)
|
|
self.assertEqual(t._cur_state[0].match, '^$hoo')
|
|
- self.assertEqual(None, t._GetValue('boo').value)
|
|
- self.assertEqual(None, t._GetValue('hoo').value)
|
|
+ self.assertIsNone(t._GetValue('boo').value)
|
|
+ self.assertIsNone(t._GetValue('hoo').value)
|
|
self.assertEqual(t._result, [['one', '']])
|
|
|
|
def testEOF(self):
|
|
|
|
# Implicit EOF.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
data = 'Matching text'
|
|
result = t.ParseText(data)
|
|
@@ -776,14 +853,14 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# EOF explicitly suppressed in template.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next\n\nEOF\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [])
|
|
|
|
# Implicit EOF suppressed by argument.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
result = t.ParseText(data, eof=False)
|
|
self.assertListEqual(result, [])
|
|
@@ -792,7 +869,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# End State, EOF is skipped.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> End\n ^$boo -> Record\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'Matching text A\nMatching text B'
|
|
|
|
result = t.ParseText(data)
|
|
@@ -800,14 +877,14 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
# End State, with explicit Record.
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Record End\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
result = t.ParseText(data)
|
|
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))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['Matching text A']])
|
|
@@ -815,14 +892,15 @@ class UnitTestFSM(unittest.TestCase):
|
|
def testInvalidRegexp(self):
|
|
|
|
tplt = 'Value boo (.$*)\n\nStart\n ^$boo -> Next\n'
|
|
- self.assertRaises(textfsm.TextFSMTemplateError,
|
|
- textfsm.TextFSM, StringIO(tplt))
|
|
+ self.assertRaises(
|
|
+ textfsm.TextFSMTemplateError, textfsm.TextFSM, io.StringIO(tplt)
|
|
+ )
|
|
|
|
def testValidRegexp(self):
|
|
"""RegexObjects uncopyable in Python 2.6."""
|
|
|
|
tplt = 'Value boo (fo*)\n\nStart\n ^$boo -> Record\n'
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
data = 'f\nfo\nfoo\n'
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(result, [['f'], ['fo'], ['foo']])
|
|
@@ -832,7 +910,7 @@ class UnitTestFSM(unittest.TestCase):
|
|
|
|
tplt = 'Value boo (.*)\n\nStart\n ^$boo -> Next Stop\n\nStop\n ^abc\n'
|
|
output_text = 'one\ntwo'
|
|
- tmpl_file = StringIO(tplt)
|
|
+ tmpl_file = io.StringIO(tplt)
|
|
|
|
t = textfsm.TextFSM(tmpl_file)
|
|
t.ParseText(output_text)
|
|
@@ -856,10 +934,11 @@ Start
|
|
2 A2 --
|
|
3 -- B3
|
|
"""
|
|
- t = textfsm.TextFSM(StringIO(tplt))
|
|
+ t = textfsm.TextFSM(io.StringIO(tplt))
|
|
result = t.ParseText(data)
|
|
self.assertListEqual(
|
|
- result, [['1', 'A2', 'B1'], ['2', 'A2', 'B3'], ['3', '', 'B3']])
|
|
+ result, [['1', 'A2', 'B1'], ['2', 'A2', 'B3'], ['3', '', 'B3']]
|
|
+ )
|
|
|
|
|
|
class UnitTestUnicode(unittest.TestCase):
|
|
@@ -918,7 +997,7 @@ State1
|
|
^$$ -> Next
|
|
^$$ -> End
|
|
"""
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t = textfsm.TextFSM(f)
|
|
self.assertEqual(str(t), buf_result)
|
|
|
|
Index: textfsm-1.1.3/tests/texttable_test.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/tests/texttable_test.py
|
|
+++ textfsm-1.1.3/tests/texttable_test.py
|
|
@@ -16,14 +16,8 @@
|
|
|
|
"""Unittest for text table."""
|
|
|
|
-from __future__ import absolute_import
|
|
-from __future__ import division
|
|
-from __future__ import print_function
|
|
-from __future__ import unicode_literals
|
|
-
|
|
-from builtins import range
|
|
+import io
|
|
import unittest
|
|
-from io import StringIO
|
|
from textfsm import terminal
|
|
from textfsm import texttable
|
|
|
|
@@ -84,8 +78,8 @@ class UnitTestRow(unittest.TestCase):
|
|
self.assertEqual(3, len(row))
|
|
|
|
# Contains.
|
|
- self.assertTrue('two' not in row)
|
|
- self.assertTrue('Two' in row)
|
|
+ self.assertNotIn('two', row)
|
|
+ self.assertIn('Two', row)
|
|
|
|
# Iteration.
|
|
self.assertEqual(['one', 'Two', 'three'], list(row))
|
|
@@ -253,8 +247,8 @@ class UnitTestTextTable(unittest.TestCas
|
|
|
|
def testContains(self):
|
|
t = self.BasicTable()
|
|
- self.assertTrue('a' in t)
|
|
- self.assertFalse('x' in t)
|
|
+ self.assertIn('a', t)
|
|
+ self.assertNotIn('x', t)
|
|
|
|
def testIteration(self):
|
|
t = self.BasicTable()
|
|
@@ -271,6 +265,7 @@ class UnitTestTextTable(unittest.TestCas
|
|
|
|
# Can we iterate repeatedly.
|
|
index = 0
|
|
+ index2 = 0
|
|
for r in t:
|
|
index += 1
|
|
self.assertEqual(r, t[index])
|
|
@@ -312,7 +307,7 @@ a,b, c, d # Trim comment
|
|
10, 11
|
|
# More comments.
|
|
"""
|
|
- f = StringIO(buf)
|
|
+ f = io.StringIO(buf)
|
|
t = texttable.TextTable()
|
|
self.assertEqual(2, t.CsvToTable(f))
|
|
# pylint: disable=E1101
|
|
@@ -514,7 +509,7 @@ a,b, c, d # Trim comment
|
|
3, t._SmallestColSize('bbb ' + terminal.AnsiText('bb', ['red'])))
|
|
|
|
def testFormattedTableColor(self):
|
|
- # Test to sepcify the color defined in terminal.FG_COLOR_WORDS
|
|
+ # Test to specify the color defined in terminal.FG_COLOR_WORDS
|
|
t = texttable.TextTable()
|
|
t.header = ('LSP', 'Name')
|
|
t.Append(('col1', 'col2'))
|
|
Index: textfsm-1.1.3/textfsm/clitable.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/textfsm/clitable.py
|
|
+++ textfsm-1.1.3/textfsm/clitable.py
|
|
@@ -23,20 +23,12 @@ output combinations and store the data i
|
|
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
|
|
from textfsm import texttable
|
|
|
|
|
|
@@ -48,7 +40,7 @@ class IndexTableError(Error):
|
|
"""General INdexTable error."""
|
|
|
|
|
|
-class CliTableError(Error):
|
|
+class CliTableError(Error): # pylint: disable=g-bad-exception-name
|
|
"""General CliTable error."""
|
|
|
|
|
|
@@ -139,7 +131,7 @@ class IndexTable(object):
|
|
if precompile:
|
|
row[col] = precompile(col, row[col])
|
|
if row[col]:
|
|
- row[col] = copyable_regex_object.CopyableRegexObject(row[col])
|
|
+ row[col] = re.compile(row[col])
|
|
|
|
def GetRowMatch(self, attributes):
|
|
"""Returns the row number that matches the supplied attributes."""
|
|
@@ -148,8 +140,11 @@ class IndexTable(object):
|
|
for key in attributes:
|
|
# Silently skip attributes not present in the index file.
|
|
# pylint: disable=E1103
|
|
- if (key in row.header and row[key] and
|
|
- not row[key].match(attributes[key])):
|
|
+ if (
|
|
+ key in row.header
|
|
+ and row[key]
|
|
+ and not row[key].match(attributes[key])
|
|
+ ):
|
|
# This line does not match, so break and try next row.
|
|
raise StopIteration()
|
|
return row.row
|
|
@@ -184,11 +179,12 @@ class CliTable(texttable.TextTable):
|
|
|
|
# pylint: disable=E0213
|
|
def Wrapper(main_obj, *args, **kwargs):
|
|
- main_obj._lock.acquire() # pylint: disable=W0212
|
|
+ main_obj._lock.acquire() # pylint: disable=W0212
|
|
try:
|
|
return func(main_obj, *args, **kwargs) # pylint: disable=E1102
|
|
finally:
|
|
- main_obj._lock.release() # pylint: disable=W0212
|
|
+ main_obj._lock.release() # pylint: disable=W0212
|
|
+
|
|
return Wrapper
|
|
|
|
@synchronised
|
|
@@ -227,7 +223,7 @@ class CliTable(texttable.TextTable):
|
|
self.index = self.INDEX[fullpath]
|
|
|
|
# Does the IndexTable have the right columns.
|
|
- if 'Template' not in self.index.index.header: # pylint: disable=E1103
|
|
+ if 'Template' not in self.index.index.header: # pylint: disable=E1103
|
|
raise CliTableError("Index file does not have 'Template' column.")
|
|
|
|
def _TemplateNamesToFiles(self, template_str):
|
|
@@ -237,8 +233,7 @@ class CliTable(texttable.TextTable):
|
|
template_files = []
|
|
try:
|
|
for tmplt in template_list:
|
|
- template_files.append(
|
|
- open(os.path.join(self.template_dir, tmplt), 'r'))
|
|
+ template_files.append(open(os.path.join(self.template_dir, tmplt), 'r'))
|
|
except:
|
|
for tmplt in template_files:
|
|
tmplt.close()
|
|
@@ -269,8 +264,9 @@ class CliTable(texttable.TextTable):
|
|
if row_idx:
|
|
templates = self.index.index[row_idx]['Template']
|
|
else:
|
|
- raise CliTableError('No template found for attributes: "%s"' %
|
|
- attributes)
|
|
+ raise CliTableError(
|
|
+ 'No template found for attributes: "%s"' % attributes
|
|
+ )
|
|
|
|
template_files = self._TemplateNamesToFiles(templates)
|
|
|
|
@@ -282,8 +278,9 @@ class CliTable(texttable.TextTable):
|
|
|
|
# Add additional columns from any additional tables.
|
|
for tmplt in template_files[1:]:
|
|
- self.extend(self._ParseCmdItem(self.raw, template_file=tmplt),
|
|
- set(self._keys))
|
|
+ self.extend(
|
|
+ self._ParseCmdItem(self.raw, template_file=tmplt), set(self._keys)
|
|
+ )
|
|
finally:
|
|
for f in template_files:
|
|
f.close()
|
|
@@ -357,6 +354,7 @@ class CliTable(texttable.TextTable):
|
|
if not key and self._keys:
|
|
key = self.KeyValue
|
|
super(CliTable, self).sort(cmp=cmp, key=key, reverse=reverse)
|
|
+
|
|
# pylint: enable=W0622
|
|
|
|
def AddKeys(self, key_list):
|
|
Index: textfsm-1.1.3/textfsm/parser.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/textfsm/parser.py
|
|
+++ textfsm-1.1.3/textfsm/parser.py
|
|
@@ -23,28 +23,19 @@ A simple template language is used to de
|
|
parse a specific type of text input, returning a record of values
|
|
for each input entity.
|
|
"""
|
|
-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):
|
|
"""Base class for errors."""
|
|
|
|
|
|
-class Usage(Exception):
|
|
+class UsageError(Exception):
|
|
"""Error in command line execution."""
|
|
|
|
|
|
@@ -58,15 +49,15 @@ class TextFSMTemplateError(Error):
|
|
|
|
# The below exceptions are internal state change triggers
|
|
# and not used as Errors.
|
|
-class FSMAction(Exception):
|
|
+class FSMAction(Exception): # pylint: disable=g-bad-exception-name
|
|
"""Base class for actions raised with the FSM."""
|
|
|
|
|
|
-class SkipRecord(FSMAction):
|
|
+class SkipRecord(FSMAction): # pylint: disable=g-bad-exception-name
|
|
"""Indicate a record is to be skipped."""
|
|
|
|
|
|
-class SkipValue(FSMAction):
|
|
+class SkipValue(FSMAction): # pylint: disable=g-bad-exception-name
|
|
"""Indicate a value is to be skipped."""
|
|
|
|
|
|
@@ -175,8 +166,8 @@ class TextFSMOptions(object):
|
|
"""Value constitutes part of the Key of the record."""
|
|
|
|
class List(OptionBase):
|
|
- r"""
|
|
- Value takes the form of a list.
|
|
+ # pylint: disable=g-space-before-docstring-summary
|
|
+ r"""Value takes the form of a list.
|
|
|
|
If the value regex contains nested match groups in the form (?P<name>regex),
|
|
instead of adding a string to the list, we add a dictionary of the groups.
|
|
@@ -237,6 +228,7 @@ class TextFSMValue(object):
|
|
fsm: A TextFSMBase(), the containing FSM.
|
|
value: (str), the current value.
|
|
"""
|
|
+
|
|
# The class which contains valid options.
|
|
|
|
def __init__(self, fsm=None, max_name_len=48, options_class=None):
|
|
@@ -285,7 +277,6 @@ class TextFSMValue(object):
|
|
|
|
Raises:
|
|
TextFSMTemplateError: Value declaration contains an error.
|
|
-
|
|
"""
|
|
|
|
value_line = value.split(' ')
|
|
@@ -310,15 +301,17 @@ class TextFSMValue(object):
|
|
|
|
if len(self.name) > self.max_name_len:
|
|
raise TextFSMTemplateError(
|
|
- "Invalid Value name '%s' or name too long." % self.name)
|
|
+ "Invalid Value name '%s' or name too long." % self.name
|
|
+ )
|
|
|
|
- if self.regex[0]!='(' or self.regex[-1]!=')' or self.regex[-2]=='\\':
|
|
+ if self.regex[0] != '(' or self.regex[-1] != ')' or self.regex[-2] == '\\':
|
|
raise TextFSMTemplateError(
|
|
- "Value '%s' must be contained within a '()' pair." % self.regex)
|
|
+ "Value '%s' must be contained within a '()' pair." % self.regex
|
|
+ )
|
|
try:
|
|
compiled_regex = re.compile(self.regex)
|
|
- except re.error as e:
|
|
- raise TextFSMTemplateError(str(e))
|
|
+ except re.error as exc:
|
|
+ raise TextFSMTemplateError(str(exc)) from exc
|
|
|
|
self.template = re.sub(r'^\(', '(?P<%s>' % self.name, self.regex)
|
|
|
|
@@ -345,8 +338,8 @@ class TextFSMValue(object):
|
|
# Create the option object
|
|
try:
|
|
option = self._options_cls.GetOption(name)(self)
|
|
- except AttributeError:
|
|
- raise TextFSMTemplateError('Unknown option "%s"' % name)
|
|
+ except AttributeError as exc:
|
|
+ raise TextFSMTemplateError('Unknown option "%s"' % name) from exc
|
|
|
|
self.options.append(option)
|
|
|
|
@@ -361,7 +354,8 @@ class TextFSMValue(object):
|
|
return 'Value %s %s %s' % (
|
|
','.join(self.OptionNames()),
|
|
self.name,
|
|
- self.regex)
|
|
+ self.regex,
|
|
+ )
|
|
else:
|
|
return 'Value %s %s' % (self.name, self.regex)
|
|
|
|
@@ -373,10 +367,10 @@ class CopyableRegexObject(object):
|
|
self.pattern = pattern
|
|
self.regex = re.compile(pattern)
|
|
|
|
- def match(self, *args, **kwargs):
|
|
+ def match(self, *args, **kwargs): # pylint: disable=invalid-name
|
|
return self.regex.match(*args, **kwargs)
|
|
|
|
- def sub(self, *args, **kwargs):
|
|
+ def sub(self, *args, **kwargs): # pylint: disable=invalid-name
|
|
return self.regex.sub(*args, **kwargs)
|
|
|
|
def __copy__(self):
|
|
@@ -407,6 +401,7 @@ class TextFSMRule(object):
|
|
regex_obj: Compiled regex for which the rule matches.
|
|
line_num: Integer row number of Value.
|
|
"""
|
|
+
|
|
# Implicit default is '(regexp) -> Next.NoRecord'
|
|
MATCH_ACTION = re.compile(r'(?P<match>.*)(\s->(?P<action>.*))')
|
|
|
|
@@ -444,15 +439,16 @@ class TextFSMRule(object):
|
|
self.match = ''
|
|
self.regex = ''
|
|
self.regex_obj = None
|
|
- self.line_op = '' # Equivalent to 'Next'.
|
|
- self.record_op = '' # Equivalent to 'NoRecord'.
|
|
- self.new_state = '' # Equivalent to current state.
|
|
+ self.line_op = '' # Equivalent to 'Next'.
|
|
+ self.record_op = '' # Equivalent to 'NoRecord'.
|
|
+ self.new_state = '' # Equivalent to current state.
|
|
self.line_num = line_num
|
|
|
|
line = line.strip()
|
|
if not line:
|
|
- raise TextFSMTemplateError('Null data in FSMRule. Line: %s'
|
|
- % self.line_num)
|
|
+ raise TextFSMTemplateError(
|
|
+ 'Null data in FSMRule. Line: %s' % self.line_num
|
|
+ )
|
|
|
|
# Is there '->' action present.
|
|
match_action = self.MATCH_ACTION.match(line)
|
|
@@ -466,18 +462,20 @@ class TextFSMRule(object):
|
|
if var_map:
|
|
try:
|
|
self.regex = string.Template(self.match).substitute(var_map)
|
|
- except (ValueError, KeyError):
|
|
+ except (ValueError, KeyError) as exc:
|
|
raise TextFSMTemplateError(
|
|
- "Duplicate or invalid variable substitution: '%s'. Line: %s." %
|
|
- (self.match, self.line_num))
|
|
+ "Duplicate or invalid variable substitution: '%s'. Line: %s."
|
|
+ % (self.match, self.line_num)
|
|
+ ) from exc
|
|
|
|
try:
|
|
# Work around a regression in Python 2.6 that makes RE Objects uncopyable.
|
|
self.regex_obj = CopyableRegexObject(self.regex)
|
|
- except re.error:
|
|
+ except re.error as exc:
|
|
raise TextFSMTemplateError(
|
|
- "Invalid regular expression: '%s'. Line: %s." %
|
|
- (self.regex, self.line_num))
|
|
+ "Invalid regular expression: '%s'. Line: %s."
|
|
+ % (self.regex, self.line_num)
|
|
+ ) from exc
|
|
|
|
# No '->' present, so done.
|
|
if not match_action:
|
|
@@ -493,8 +491,9 @@ class TextFSMRule(object):
|
|
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))
|
|
+ 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'):
|
|
@@ -514,14 +513,16 @@ class TextFSMRule(object):
|
|
if self.line_op == 'Continue' and self.new_state:
|
|
raise TextFSMTemplateError(
|
|
"Action '%s' with new state %s specified. Line: %s."
|
|
- % (self.line_op, self.new_state, self.line_num))
|
|
+ % (self.line_op, self.new_state, self.line_num)
|
|
+ )
|
|
|
|
# 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):
|
|
raise TextFSMTemplateError(
|
|
'Alphanumeric characters only in state names. Line: %s.'
|
|
- % (self.line_num))
|
|
+ % (self.line_num)
|
|
+ )
|
|
|
|
def __str__(self):
|
|
"""Prints out the FSM Rule, mimic the input file."""
|
|
@@ -555,6 +556,7 @@ class TextFSM(object):
|
|
header: Ordered list of values.
|
|
state_list: Ordered list of valid states.
|
|
"""
|
|
+
|
|
# Variable and State name length.
|
|
MAX_NAME_LEN = 48
|
|
comment_regex = re.compile(r'^\s*#')
|
|
@@ -709,7 +711,7 @@ class TextFSM(object):
|
|
# Blank line signifies end of Value definitions.
|
|
if not line:
|
|
return
|
|
- if not isinstance(line, six.string_types):
|
|
+ if not isinstance(line, str):
|
|
line = line.decode('utf-8')
|
|
# Skip commented lines.
|
|
if self.comment_regex.match(line):
|
|
@@ -718,21 +720,28 @@ class TextFSM(object):
|
|
if line.startswith('Value '):
|
|
try:
|
|
value = TextFSMValue(
|
|
- fsm=self, max_name_len=self.MAX_NAME_LEN,
|
|
- options_class=self._options_cls)
|
|
+ fsm=self,
|
|
+ max_name_len=self.MAX_NAME_LEN,
|
|
+ options_class=self._options_cls,
|
|
+ )
|
|
value.Parse(line)
|
|
- except TextFSMTemplateError as error:
|
|
- raise TextFSMTemplateError('%s Line %s.' % (error, self._line_num))
|
|
+ except TextFSMTemplateError as exc:
|
|
+ raise TextFSMTemplateError(
|
|
+ '%s Line %s.' % (exc, self._line_num)
|
|
+ ) from exc
|
|
|
|
if value.name in self.header:
|
|
raise TextFSMTemplateError(
|
|
"Duplicate declarations for Value '%s'. Line: %s."
|
|
- % (value.name, self._line_num))
|
|
+ % (value.name, self._line_num)
|
|
+ )
|
|
|
|
try:
|
|
self._ValidateOptions(value)
|
|
- except TextFSMTemplateError as error:
|
|
- raise TextFSMTemplateError('%s Line %s.' % (error, self._line_num))
|
|
+ except TextFSMTemplateError as exc:
|
|
+ raise TextFSMTemplateError(
|
|
+ '%s Line %s.' % (exc, self._line_num)
|
|
+ ) from exc
|
|
|
|
self.values.append(value)
|
|
self.value_map[value.name] = value.template
|
|
@@ -742,7 +751,8 @@ class TextFSM(object):
|
|
else:
|
|
raise TextFSMTemplateError(
|
|
'Expected blank line after last Value entry. Line: %s.'
|
|
- % (self._line_num))
|
|
+ % (self._line_num)
|
|
+ )
|
|
|
|
def _ValidateOptions(self, value):
|
|
"""Checks that combination of Options is valid."""
|
|
@@ -760,8 +770,8 @@ class TextFSM(object):
|
|
not clash with reserved names and are unique.
|
|
|
|
Args:
|
|
- template: Valid template file after Value definitions
|
|
- have already been read.
|
|
+ template: Valid template file after Value definitions have already been
|
|
+ read.
|
|
|
|
Returns:
|
|
Name of the state parsed from file. None otherwise.
|
|
@@ -778,22 +788,26 @@ class TextFSM(object):
|
|
for line in template:
|
|
self._line_num += 1
|
|
line = line.rstrip()
|
|
- if not isinstance(line, six.string_types):
|
|
+ if not isinstance(line, str):
|
|
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.
|
|
- 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):
|
|
- raise TextFSMTemplateError("Invalid state name: '%s'. Line: %s"
|
|
- % (line, self._line_num))
|
|
+ # Ensure statename has valid syntax and is not a reserved word.
|
|
+ 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
|
|
+ ):
|
|
+ raise TextFSMTemplateError(
|
|
+ "Invalid state name: '%s'. Line: %s" % (line, self._line_num)
|
|
+ )
|
|
|
|
state_name = line
|
|
if state_name in self.states:
|
|
- raise TextFSMTemplateError("Duplicate state name: '%s'. Line: %s"
|
|
- % (line, self._line_num))
|
|
+ raise TextFSMTemplateError(
|
|
+ "Duplicate state name: '%s'. Line: %s" % (line, self._line_num)
|
|
+ )
|
|
self.states[state_name] = []
|
|
self.state_list.append(state_name)
|
|
break
|
|
@@ -806,7 +820,7 @@ class TextFSM(object):
|
|
# Finish rules processing on blank line.
|
|
if not line:
|
|
break
|
|
- if not isinstance(line, six.string_types):
|
|
+ if not isinstance(line, str):
|
|
line = line.decode('utf-8')
|
|
if self.comment_regex.match(line):
|
|
continue
|
|
@@ -814,11 +828,13 @@ class TextFSM(object):
|
|
# A rule within a state, starts with 1 or 2 spaces, or a tab.
|
|
if not line.startswith((' ^', ' ^', '\t^')):
|
|
raise TextFSMTemplateError(
|
|
- "Missing white space or carat ('^') before rule. Line: %s" %
|
|
- self._line_num)
|
|
+ "Missing white space or carat ('^') before rule. Line: %s"
|
|
+ % self._line_num
|
|
+ )
|
|
|
|
self.states[state_name].append(
|
|
- TextFSMRule(line, self._line_num, self.value_map))
|
|
+ TextFSMRule(line, self._line_num, self.value_map)
|
|
+ )
|
|
|
|
return state_name
|
|
|
|
@@ -864,8 +880,9 @@ class TextFSM(object):
|
|
|
|
if rule.new_state not in self.states:
|
|
raise TextFSMTemplateError(
|
|
- "State '%s' not found, referenced in state '%s'" %
|
|
- (rule.new_state, state))
|
|
+ "State '%s' not found, referenced in state '%s'"
|
|
+ % (rule.new_state, state)
|
|
+ )
|
|
|
|
return True
|
|
|
|
@@ -877,7 +894,7 @@ class TextFSM(object):
|
|
Args:
|
|
text: (str), Text to parse with embedded newlines.
|
|
eof: (boolean), Set to False if we are parsing only part of the file.
|
|
- Suppresses triggering EOF state.
|
|
+ Suppresses triggering EOF state.
|
|
|
|
Raises:
|
|
TextFSMError: An error occurred within the FSM.
|
|
@@ -902,7 +919,7 @@ class TextFSM(object):
|
|
|
|
return self._result
|
|
|
|
- def ParseTextToDicts(self, *args, **kwargs):
|
|
+ def ParseTextToDicts(self, text, eof=True):
|
|
"""Calls ParseText and turns the result into list of dicts.
|
|
|
|
List items are dicts of rows, dict key is column header and value is column
|
|
@@ -911,7 +928,7 @@ class TextFSM(object):
|
|
Args:
|
|
text: (str), Text to parse with embedded newlines.
|
|
eof: (boolean), Set to False if we are parsing only part of the file.
|
|
- Suppresses triggering EOF state.
|
|
+ Suppresses triggering EOF state.
|
|
|
|
Raises:
|
|
TextFSMError: An error occurred within the FSM.
|
|
@@ -920,7 +937,7 @@ class TextFSM(object):
|
|
List of dicts.
|
|
"""
|
|
|
|
- result_lists = self.ParseText(*args, **kwargs)
|
|
+ result_lists = self.ParseText(text, eof)
|
|
result_dicts = []
|
|
|
|
for row in result_lists:
|
|
@@ -972,9 +989,9 @@ class TextFSM(object):
|
|
matched: (regexp.match) Named group for each matched value.
|
|
value: (str) The matched value.
|
|
"""
|
|
- _value = self._GetValue(value)
|
|
- if _value is not None:
|
|
- _value.AssignVar(matched.group(value))
|
|
+ self._value = self._GetValue(value)
|
|
+ if self._value is not None:
|
|
+ self._value.AssignVar(matched.group(value))
|
|
|
|
def _Operations(self, rule, line):
|
|
"""Operators on the data record.
|
|
@@ -1017,11 +1034,15 @@ class TextFSM(object):
|
|
# Lastly process line operators.
|
|
if rule.line_op == 'Error':
|
|
if rule.new_state:
|
|
- raise TextFSMError('Error: %s. Rule Line: %s. Input Line: %s.'
|
|
- % (rule.new_state, rule.line_num, line))
|
|
-
|
|
- raise TextFSMError('State Error raised. Rule Line: %s. Input Line: %s'
|
|
- % (rule.line_num, line))
|
|
+ raise TextFSMError(
|
|
+ 'Error: %s. Rule Line: %s. Input Line: %s.'
|
|
+ % (rule.new_state, rule.line_num, line)
|
|
+ )
|
|
+
|
|
+ raise TextFSMError(
|
|
+ 'State Error raised. Rule Line: %s. Input Line: %s'
|
|
+ % (rule.line_num, line)
|
|
+ )
|
|
|
|
elif rule.line_op == 'Continue':
|
|
# Continue with current line without returning to the start of the state.
|
|
@@ -1060,8 +1081,8 @@ def main(argv=None):
|
|
|
|
try:
|
|
opts, args = getopt.getopt(argv[1:], 'h', ['help'])
|
|
- except getopt.error as msg:
|
|
- raise Usage(msg)
|
|
+ except getopt.error as exc:
|
|
+ raise UsageError(exc) from exc
|
|
|
|
for opt, _ in opts:
|
|
if opt in ('-h', '--help'):
|
|
@@ -1070,10 +1091,11 @@ def main(argv=None):
|
|
return 0
|
|
|
|
if not args or len(args) > 4:
|
|
- raise Usage('Invalid arguments.')
|
|
+ raise UsageError('Invalid arguments.')
|
|
|
|
# If we have an argument, parse content of file and display as a template.
|
|
# Template displayed will match input template, minus any comment lines.
|
|
+ result = ''
|
|
with open(args[0], 'r') as template:
|
|
fsm = TextFSM(template)
|
|
print('FSM Template:\n%s\n' % fsm)
|
|
@@ -1108,7 +1130,7 @@ if __name__ == '__main__':
|
|
help_msg = '%s [--help] template [input_file [output_file]]\n' % sys.argv[0]
|
|
try:
|
|
sys.exit(main())
|
|
- except Usage as err:
|
|
+ except UsageError as err:
|
|
print(err, file=sys.stderr)
|
|
print('For help use --help', file=sys.stderr)
|
|
sys.exit(2)
|
|
Index: textfsm-1.1.3/textfsm/terminal.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/textfsm/terminal.py
|
|
+++ textfsm-1.1.3/textfsm/terminal.py
|
|
@@ -16,26 +16,20 @@
|
|
|
|
"""Simple terminal related routines."""
|
|
|
|
-from __future__ import absolute_import
|
|
-from __future__ import division
|
|
-from __future__ import print_function
|
|
-from __future__ import unicode_literals
|
|
-
|
|
-try:
|
|
- # Import fails on Windows machines.
|
|
- import fcntl
|
|
- import termios
|
|
- import tty
|
|
-except (ImportError, ModuleNotFoundError):
|
|
- pass
|
|
import getopt
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
import time
|
|
-from builtins import object # pylint: disable=redefined-builtin
|
|
-from builtins import str # pylint: disable=redefined-builtin
|
|
+
|
|
+try:
|
|
+ # Import fails on Windows machines.
|
|
+ import fcntl # pylint: disable=g-import-not-at-top
|
|
+ import termios # pylint: disable=g-import-not-at-top
|
|
+ import tty # pylint: disable=g-import-not-at-top
|
|
+except (ImportError, ModuleNotFoundError):
|
|
+ pass
|
|
|
|
__version__ = '0.1.1'
|
|
|
|
@@ -67,34 +61,38 @@ SGR = {
|
|
'bg_cyan': 46,
|
|
'bg_white': 47,
|
|
'bg_reset': 49,
|
|
- }
|
|
+}
|
|
|
|
# Provide a familar descriptive word for some ansi sequences.
|
|
-FG_COLOR_WORDS = {'black': ['black'],
|
|
- 'dark_gray': ['bold', 'black'],
|
|
- 'blue': ['blue'],
|
|
- 'light_blue': ['bold', 'blue'],
|
|
- 'green': ['green'],
|
|
- 'light_green': ['bold', 'green'],
|
|
- 'cyan': ['cyan'],
|
|
- 'light_cyan': ['bold', 'cyan'],
|
|
- 'red': ['red'],
|
|
- 'light_red': ['bold', 'red'],
|
|
- 'purple': ['magenta'],
|
|
- 'light_purple': ['bold', 'magenta'],
|
|
- 'brown': ['yellow'],
|
|
- 'yellow': ['bold', 'yellow'],
|
|
- 'light_gray': ['white'],
|
|
- 'white': ['bold', 'white']}
|
|
-
|
|
-BG_COLOR_WORDS = {'black': ['bg_black'],
|
|
- 'red': ['bg_red'],
|
|
- 'green': ['bg_green'],
|
|
- 'yellow': ['bg_yellow'],
|
|
- 'dark_blue': ['bg_blue'],
|
|
- 'purple': ['bg_magenta'],
|
|
- 'light_blue': ['bg_cyan'],
|
|
- 'grey': ['bg_white']}
|
|
+FG_COLOR_WORDS = {
|
|
+ 'black': ['black'],
|
|
+ 'dark_gray': ['bold', 'black'],
|
|
+ 'blue': ['blue'],
|
|
+ 'light_blue': ['bold', 'blue'],
|
|
+ 'green': ['green'],
|
|
+ 'light_green': ['bold', 'green'],
|
|
+ 'cyan': ['cyan'],
|
|
+ 'light_cyan': ['bold', 'cyan'],
|
|
+ 'red': ['red'],
|
|
+ 'light_red': ['bold', 'red'],
|
|
+ 'purple': ['magenta'],
|
|
+ 'light_purple': ['bold', 'magenta'],
|
|
+ 'brown': ['yellow'],
|
|
+ 'yellow': ['bold', 'yellow'],
|
|
+ 'light_gray': ['white'],
|
|
+ 'white': ['bold', 'white'],
|
|
+}
|
|
+
|
|
+BG_COLOR_WORDS = {
|
|
+ 'black': ['bg_black'],
|
|
+ 'red': ['bg_red'],
|
|
+ 'green': ['bg_green'],
|
|
+ 'yellow': ['bg_yellow'],
|
|
+ 'dark_blue': ['bg_blue'],
|
|
+ 'purple': ['bg_magenta'],
|
|
+ 'light_blue': ['bg_cyan'],
|
|
+ 'grey': ['bg_white'],
|
|
+}
|
|
|
|
|
|
# Characters inserted at the start and end of ANSI strings
|
|
@@ -103,15 +101,14 @@ ANSI_START = '\001'
|
|
ANSI_END = '\002'
|
|
|
|
|
|
-sgr_re = re.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % (
|
|
- ANSI_START, ANSI_END))
|
|
+sgr_re = re.compile(r'(%s?\033\[\d+(?:;\d+)*m%s?)' % (ANSI_START, ANSI_END))
|
|
|
|
|
|
class Error(Exception):
|
|
"""The base error class."""
|
|
|
|
|
|
-class Usage(Error):
|
|
+class UsageError(Error):
|
|
"""Command line format error."""
|
|
|
|
|
|
@@ -119,8 +116,8 @@ def _AnsiCmd(command_list):
|
|
"""Takes a list of SGR values and formats them as an ANSI escape sequence.
|
|
|
|
Args:
|
|
- command_list: List of strings, each string represents an SGR value.
|
|
- e.g. 'fg_blue', 'bg_yellow'
|
|
+ command_list: List of strings, each string represents an SGR value. e.g.
|
|
+ 'fg_blue', 'bg_yellow'
|
|
|
|
Returns:
|
|
The ANSI escape sequence.
|
|
@@ -138,7 +135,7 @@ def _AnsiCmd(command_list):
|
|
# Convert to numerical strings.
|
|
command_str = [str(SGR[x.lower()]) for x in command_list]
|
|
# Wrap values in Ansi escape sequence (CSI prefix & SGR suffix).
|
|
- return '\033[%sm' % (';'.join(command_str))
|
|
+ return '\033[%sm' % ';'.join(command_str)
|
|
|
|
|
|
def AnsiText(text, command_list=None, reset=True):
|
|
@@ -146,8 +143,8 @@ def AnsiText(text, command_list=None, re
|
|
|
|
Args:
|
|
text: String to encase in sgr escape sequence.
|
|
- command_list: List of strings, each string represents an sgr value.
|
|
- e.g. 'fg_blue', 'bg_yellow'
|
|
+ command_list: List of strings, each string represents an sgr value. e.g.
|
|
+ 'fg_blue', 'bg_yellow'
|
|
reset: Boolean, if to add a reset sequence to the suffix of the text.
|
|
|
|
Returns:
|
|
@@ -175,11 +172,11 @@ def TerminalSize():
|
|
try:
|
|
with open(os.ctermid()) as tty_instance:
|
|
length_width = struct.unpack(
|
|
- 'hh', fcntl.ioctl(tty_instance.fileno(), termios.TIOCGWINSZ, '1234'))
|
|
+ 'hh', fcntl.ioctl(tty_instance.fileno(), termios.TIOCGWINSZ, '1234')
|
|
+ )
|
|
except (IOError, OSError, NameError):
|
|
try:
|
|
- length_width = (int(os.environ['LINES']),
|
|
- int(os.environ['COLUMNS']))
|
|
+ length_width = (int(os.environ['LINES']), int(os.environ['COLUMNS']))
|
|
except (ValueError, KeyError):
|
|
length_width = (24, 80)
|
|
return length_width
|
|
@@ -201,27 +198,27 @@ def LineWrap(text, omit_sgr=False):
|
|
token_list = sgr_re.split(text_line)
|
|
text_line_list = []
|
|
line_length = 0
|
|
- for (index, token) in enumerate(token_list):
|
|
+ for index, token in enumerate(token_list):
|
|
# Skip null tokens.
|
|
- if token == '':
|
|
+ if not token:
|
|
continue
|
|
|
|
if sgr_re.match(token):
|
|
# Add sgr escape sequences without splitting or counting length.
|
|
text_line_list.append(token)
|
|
- text_line = ''.join(token_list[index +1:])
|
|
+ text_line = ''.join(token_list[index + 1 :])
|
|
else:
|
|
if line_length + len(token) <= width:
|
|
# Token fits in line and we count it towards overall length.
|
|
text_line_list.append(token)
|
|
line_length += len(token)
|
|
- text_line = ''.join(token_list[index +1:])
|
|
+ text_line = ''.join(token_list[index + 1 :])
|
|
else:
|
|
# Line splits part way through this token.
|
|
# So split the token, form a new line and carry the remainder.
|
|
- text_line_list.append(token[:width - line_length])
|
|
- text_line = token[width - line_length:]
|
|
- text_line += ''.join(token_list[index +1:])
|
|
+ text_line_list.append(token[: width - line_length])
|
|
+ text_line = token[width - line_length :]
|
|
+ text_line += ''.join(token_list[index + 1 :])
|
|
break
|
|
|
|
return (''.join(text_line_list), text_line)
|
|
@@ -233,8 +230,9 @@ def LineWrap(text, omit_sgr=False):
|
|
text_multiline = []
|
|
for text_line in text.splitlines():
|
|
# Is this a line that needs splitting?
|
|
- while ((omit_sgr and (len(StripAnsiText(text_line)) > width)) or
|
|
- (len(text_line) > width)):
|
|
+ while (omit_sgr and (len(StripAnsiText(text_line)) > width)) or (
|
|
+ len(text_line) > width
|
|
+ ):
|
|
# If there are no sgr escape characters then do a straight split.
|
|
if not omit_sgr:
|
|
text_multiline.append(text_line[:width])
|
|
@@ -284,8 +282,8 @@ class Pager(object):
|
|
|
|
Args:
|
|
text: A string, the text that will be paged through.
|
|
- delay: A boolean, if True will cause a slight delay
|
|
- between line printing for more obvious scrolling.
|
|
+ delay: A boolean, if True will cause a slight delay between line printing
|
|
+ for more obvious scrolling.
|
|
"""
|
|
self._text = text or ''
|
|
self._delay = delay
|
|
@@ -356,7 +354,9 @@ class Pager(object):
|
|
text = LineWrap(self._text).splitlines()
|
|
while True:
|
|
# Get a list of new lines to display.
|
|
- self._newlines = text[self._displayed:self._displayed+self._lines_to_show]
|
|
+ self._newlines = text[
|
|
+ self._displayed : self._displayed + self._lines_to_show
|
|
+ ]
|
|
for line in self._newlines:
|
|
sys.stdout.write(line + '\n')
|
|
if self._delay and self._lastscroll > 0:
|
|
@@ -366,19 +366,19 @@ class Pager(object):
|
|
if self._currentpagelines >= self._lines_to_show:
|
|
self._currentpagelines = 0
|
|
wish = self._AskUser()
|
|
- if wish == 'q': # Quit pager.
|
|
+ if wish == 'q': # Quit pager.
|
|
return False
|
|
- elif wish == 'g': # Display till the end.
|
|
+ elif wish == 'g': # Display till the end.
|
|
self._Scroll(len(text) - self._displayed + 1)
|
|
- elif wish == '\r': # Enter, down a line.
|
|
+ elif wish == '\r': # Enter, down a line.
|
|
self._Scroll(1)
|
|
elif wish == '\033[B': # Down arrow, down a line.
|
|
self._Scroll(1)
|
|
elif wish == '\033[A': # Up arrow, up a line.
|
|
self._Scroll(-1)
|
|
- elif wish == 'b': # Up a page.
|
|
+ elif wish == 'b': # Up a page.
|
|
self._Scroll(0 - self._cli_lines)
|
|
- else: # Next page.
|
|
+ else: # Next page.
|
|
self._Scroll()
|
|
if self._displayed >= len(text):
|
|
break
|
|
@@ -389,8 +389,8 @@ class Pager(object):
|
|
"""Set attributes to scroll the buffer correctly.
|
|
|
|
Args:
|
|
- lines: An int, number of lines to scroll. If None, scrolls
|
|
- by the terminal length.
|
|
+ lines: An int, number of lines to scroll. If None, scrolls by the terminal
|
|
+ length.
|
|
"""
|
|
if lines is None:
|
|
lines = self._cli_lines
|
|
@@ -413,18 +413,19 @@ class Pager(object):
|
|
A string, the character entered by the user.
|
|
"""
|
|
if self._show_percent:
|
|
- progress = int(self._displayed*100 / (len(self._text.splitlines())))
|
|
+ progress = int(self._displayed * 100 / (len(self._text.splitlines())))
|
|
progress_text = ' (%d%%)' % progress
|
|
else:
|
|
progress_text = ''
|
|
question = AnsiText(
|
|
- 'Enter: next line, Space: next page, '
|
|
- 'b: prev page, q: quit.%s' %
|
|
- progress_text, ['green'])
|
|
+ 'Enter: next line, Space: next page, b: prev page, q: quit.%s'
|
|
+ % progress_text,
|
|
+ ['green'],
|
|
+ )
|
|
sys.stdout.write(question)
|
|
sys.stdout.flush()
|
|
ch = self._GetCh()
|
|
- sys.stdout.write('\r%s\r' % (' '*len(question)))
|
|
+ sys.stdout.write('\r%s\r' % (' ' * len(question)))
|
|
sys.stdout.flush()
|
|
return ch
|
|
|
|
@@ -455,8 +456,8 @@ def main(argv=None):
|
|
|
|
try:
|
|
opts, args = getopt.getopt(argv[1:], 'dhs', ['nodelay', 'help', 'size'])
|
|
- except getopt.error as msg:
|
|
- raise Usage(msg)
|
|
+ except getopt.error as exc:
|
|
+ raise UsageError(exc) from exc
|
|
|
|
# Print usage and return, regardless of presence of other args.
|
|
for opt, _ in opts:
|
|
@@ -475,7 +476,7 @@ def main(argv=None):
|
|
elif opt in ('-d', '--delay'):
|
|
isdelay = True
|
|
else:
|
|
- raise Usage('Invalid arguments.')
|
|
+ raise UsageError('Invalid arguments.')
|
|
|
|
# Page text supplied in either specified file or stdin.
|
|
|
|
@@ -491,7 +492,7 @@ if __name__ == '__main__':
|
|
help_msg = '%s [--help] [--size] [--nodelay] [input_file]\n' % sys.argv[0]
|
|
try:
|
|
sys.exit(main())
|
|
- except Usage as err:
|
|
+ except UsageError as err:
|
|
print(err, file=sys.stderr)
|
|
print('For help use --help', file=sys.stderr)
|
|
sys.exit(2)
|
|
Index: textfsm-1.1.3/textfsm/texttable.py
|
|
===================================================================
|
|
--- textfsm-1.1.3.orig/textfsm/texttable.py
|
|
+++ textfsm-1.1.3/textfsm/texttable.py
|
|
@@ -22,22 +22,10 @@ Tables can be created from CSV input and
|
|
formats such as CSV and variable sized and justified rows.
|
|
"""
|
|
|
|
-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 functools
|
|
import textwrap
|
|
|
|
-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
|
|
|
|
|
|
@@ -56,8 +44,11 @@ class Row(dict):
|
|
to make it behave like a regular dict() and list().
|
|
|
|
Attributes:
|
|
+ color: Colour spec of this row.
|
|
+ header: List of row's headers.
|
|
row: int, the row number in the container table. 0 is the header row.
|
|
table: A TextTable(), the associated container table.
|
|
+ values: List of row's values.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
@@ -162,7 +153,7 @@ class Row(dict):
|
|
except IndexError:
|
|
return default_value
|
|
|
|
- def index(self, column):
|
|
+ def index(self, column): # pylint: disable=invalid-name
|
|
"""Fetches the column number (0 indexed).
|
|
|
|
Args:
|
|
@@ -174,12 +165,12 @@ class Row(dict):
|
|
Raises:
|
|
ValueError: The specified column was not found.
|
|
"""
|
|
- for i, key in enumerate(self._keys):
|
|
- if key == column:
|
|
- return i
|
|
- raise ValueError('Column "%s" not found.' % column)
|
|
+ try:
|
|
+ return self._keys.index(column)
|
|
+ except ValueError as exc:
|
|
+ raise ValueError('Column "%s" not found.' % column) from exc
|
|
|
|
- def iterkeys(self):
|
|
+ def iterkeys(self): # pylint: disable=invalid-name
|
|
return iter(self._keys)
|
|
|
|
def items(self):
|
|
@@ -263,12 +254,13 @@ class Row(dict):
|
|
elif isinstance(values, list) or isinstance(values, tuple):
|
|
if len(values) != len(self._values):
|
|
raise TypeError('Supplied list length != row length')
|
|
- for (index, value) in enumerate(values):
|
|
+ for index, value in enumerate(values):
|
|
self._values[index] = _ToStr(value)
|
|
|
|
else:
|
|
- raise TypeError('Supplied argument must be Row, dict or list, not %s',
|
|
- type(values))
|
|
+ raise TypeError(
|
|
+ 'Supplied argument must be Row, dict or list, not %s' % type(values)
|
|
+ )
|
|
|
|
def Insert(self, key, value, row_index):
|
|
"""Inserts new values at a specified offset.
|
|
@@ -317,8 +309,8 @@ class TextTable(object):
|
|
"""Initialises a new table.
|
|
|
|
Args:
|
|
- row_class: A class to use as the row object. This should be a
|
|
- subclass of this module's Row() class.
|
|
+ row_class: A class to use as the row object. This should be a subclass of
|
|
+ this module's Row() class.
|
|
"""
|
|
self.row_class = row_class
|
|
self.separator = ', '
|
|
@@ -327,7 +319,7 @@ class TextTable(object):
|
|
def Reset(self):
|
|
self._row_index = 1
|
|
self._table = [[]]
|
|
- self._iterator = 0 # While loop row index
|
|
+ self._iterator = 0 # While loop row index
|
|
|
|
def __repr__(self):
|
|
return '%s(%r)' % (self.__class__.__name__, str(self))
|
|
@@ -337,7 +329,7 @@ class TextTable(object):
|
|
return self.table
|
|
|
|
def __incr__(self, incr=1):
|
|
- self._SetRowIndex(self._row_index +incr)
|
|
+ self._SetRowIndex(self._row_index + incr)
|
|
|
|
def __contains__(self, name):
|
|
"""Whether the given column header name exists."""
|
|
@@ -386,9 +378,9 @@ class TextTable(object):
|
|
"""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
|
|
- removed.
|
|
+ function: A function applied to each row which returns a bool. If function
|
|
+ is None, all rows with empty column values are removed.
|
|
+
|
|
Returns:
|
|
A new TextTable()
|
|
|
|
@@ -403,7 +395,7 @@ class TextTable(object):
|
|
# pylint: disable=protected-access
|
|
new_table._table = [self.header]
|
|
for row in self:
|
|
- if function(row) is True:
|
|
+ if function(row):
|
|
new_table.Append(row)
|
|
return new_table
|
|
|
|
@@ -430,6 +422,7 @@ class TextTable(object):
|
|
return new_table
|
|
|
|
# pylint: disable=W0622
|
|
+ # pylint: disable=invalid-name
|
|
def sort(self, cmp=None, key=None, reverse=False):
|
|
"""Sorts rows in the texttable.
|
|
|
|
@@ -455,7 +448,7 @@ class TextTable(object):
|
|
new_table = self._table[1:]
|
|
|
|
if cmp is not None:
|
|
- key = cmp_to_key(cmp)
|
|
+ key = functools.cmp_to_key(cmp)
|
|
|
|
new_table.sort(key=key, reverse=reverse)
|
|
|
|
@@ -465,17 +458,19 @@ class TextTable(object):
|
|
# Re-write the 'row' attribute of each row
|
|
for index, row in enumerate(self._table):
|
|
row.row = index
|
|
+
|
|
# pylint: enable=W0622
|
|
+ # pylint: enable=invalid-name
|
|
|
|
- def extend(self, table, keys=None):
|
|
+ def extend(self, table, keys=None): # pylint: disable=invalid-name
|
|
"""Extends all rows in the texttable.
|
|
|
|
The rows are extended with the new columns from the table.
|
|
|
|
Args:
|
|
table: A texttable, the table to extend this table by.
|
|
- keys: A set, the set of columns to use as the key. If None, the
|
|
- row index is used.
|
|
+ keys: A set, the set of columns to use as the key. If None, the row index
|
|
+ is used.
|
|
|
|
Raises:
|
|
IndexError: If key is not a valid column name.
|
|
@@ -483,7 +478,7 @@ class TextTable(object):
|
|
if keys:
|
|
for k in keys:
|
|
if k not in self._Header():
|
|
- raise IndexError("Unknown key: '%s'", k)
|
|
+ raise IndexError("Unknown key: '%s'" % k)
|
|
|
|
extend_with = []
|
|
for column in table.header:
|
|
@@ -516,8 +511,8 @@ class TextTable(object):
|
|
"""Removes a row from the table.
|
|
|
|
Args:
|
|
- row: int, the row number to delete. Must be >= 1, as the header
|
|
- cannot be removed.
|
|
+ row: int, the row number to delete. Must be >= 1, as the header cannot be
|
|
+ removed.
|
|
|
|
Raises:
|
|
TableError: Attempt to remove nonexistent or header row.
|
|
@@ -607,9 +602,7 @@ class TextTable(object):
|
|
# Avoid the global lookup cost on each iteration.
|
|
lstr = str
|
|
for row in self._table:
|
|
- result.append(
|
|
- '%s\n' %
|
|
- self.separator.join(lstr(v) for v in row))
|
|
+ result.append('%s\n' % self.separator.join(lstr(v) for v in row))
|
|
|
|
return ''.join(result)
|
|
|
|
@@ -618,7 +611,7 @@ class TextTable(object):
|
|
if not isinstance(table, TextTable):
|
|
raise TypeError('Not an instance of TextTable.')
|
|
self.Reset()
|
|
- self._table = copy.deepcopy(table._table) # pylint: disable=W0212
|
|
+ self._table = copy.deepcopy(table._table) # pylint: disable=W0212
|
|
# Point parent table of each row back ourselves.
|
|
for row in self:
|
|
row.table = self
|
|
@@ -666,15 +659,16 @@ class TextTable(object):
|
|
result.extend(self._TextJustify(paragraph, col_size))
|
|
return result
|
|
|
|
- wrapper = textwrap.TextWrapper(width=col_size-2, break_long_words=False,
|
|
- expand_tabs=False)
|
|
+ wrapper = textwrap.TextWrapper(
|
|
+ width=col_size - 2, break_long_words=False, expand_tabs=False
|
|
+ )
|
|
try:
|
|
text_list = wrapper.wrap(text)
|
|
- except ValueError:
|
|
- raise TableError('Field too small (minimum width: 3)')
|
|
+ except ValueError as exc:
|
|
+ raise TableError('Field too small (minimum width: 3)') from exc
|
|
|
|
if not text_list:
|
|
- return [' '*col_size]
|
|
+ return [' ' * col_size]
|
|
|
|
for current_line in text_list:
|
|
stripped_len = len(terminal.StripAnsiText(current_line))
|
|
@@ -687,16 +681,23 @@ class TextTable(object):
|
|
|
|
return result
|
|
|
|
- def FormattedTable(self, width=80, force_display=False, ml_delimiter=True,
|
|
- color=True, display_header=True, columns=None):
|
|
+ def FormattedTable(
|
|
+ self,
|
|
+ width=80,
|
|
+ force_display=False,
|
|
+ ml_delimiter=True,
|
|
+ color=True,
|
|
+ display_header=True,
|
|
+ columns=None,
|
|
+ ):
|
|
"""Returns whole table, with whitespace padding and row delimiters.
|
|
|
|
Args:
|
|
width: An int, the max width we want the table to fit in.
|
|
force_display: A bool, if set to True will display table when the table
|
|
- can't be made to fit to the width.
|
|
+ can't be made to fit to the width.
|
|
ml_delimiter: A bool, if set to False will not display the multi-line
|
|
- delimiter.
|
|
+ delimiter.
|
|
color: A bool. If true, display any colours in row.colour.
|
|
display_header: A bool. If true, display header.
|
|
columns: A list of str, show only columns with these names.
|
|
@@ -780,8 +781,9 @@ class TextTable(object):
|
|
for key in multi_word:
|
|
# If we scale past the desired width for this particular column,
|
|
# then give it its desired width and remove it from the wrapped list.
|
|
- if (largest[key] <=
|
|
- round((largest[key] / float(desired_width)) * spare_width)):
|
|
+ if largest[key] <= round(
|
|
+ (largest[key] / float(desired_width)) * spare_width
|
|
+ ):
|
|
smallest[key] = largest[key]
|
|
multi_word.remove(key)
|
|
spare_width -= smallest[key]
|
|
@@ -789,8 +791,9 @@ class TextTable(object):
|
|
done = False
|
|
# If we scale below the minimum width for this particular column,
|
|
# then leave it at its minimum and remove it from the wrapped list.
|
|
- elif (smallest[key] >=
|
|
- round((largest[key] / float(desired_width)) * spare_width)):
|
|
+ elif smallest[key] >= round(
|
|
+ (largest[key] / float(desired_width)) * spare_width
|
|
+ ):
|
|
multi_word.remove(key)
|
|
spare_width -= smallest[key]
|
|
desired_width -= largest[key]
|
|
@@ -799,8 +802,9 @@ class TextTable(object):
|
|
# Repeat the scaling algorithm with the final wrap list.
|
|
# This time we assign the extra column space by increasing 'smallest'.
|
|
for key in multi_word:
|
|
- smallest[key] = int(round((largest[key] / float(desired_width))
|
|
- * spare_width))
|
|
+ smallest[key] = int(
|
|
+ round((largest[key] / float(desired_width)) * spare_width)
|
|
+ )
|
|
|
|
total_width = 0
|
|
row_count = 0
|
|
@@ -822,7 +826,7 @@ class TextTable(object):
|
|
header_list.append(result_dict[key][row_idx])
|
|
except IndexError:
|
|
# If no value than use whitespace of equal size.
|
|
- header_list.append(' '*smallest[key])
|
|
+ header_list.append(' ' * smallest[key])
|
|
header_list.append('\n')
|
|
|
|
# Format and store the body lines
|
|
@@ -849,7 +853,7 @@ class TextTable(object):
|
|
prev_muli_line = True
|
|
# If current or prior line was multi-line then include delimiter.
|
|
if not first_line and prev_muli_line and ml_delimiter:
|
|
- body_list.append('-'*total_width + '\n')
|
|
+ body_list.append('-' * total_width + '\n')
|
|
if row_count == 1:
|
|
# Our current line was not wrapped, so clear flag.
|
|
prev_muli_line = False
|
|
@@ -861,20 +865,20 @@ class TextTable(object):
|
|
row_list.append(result_dict[key][row_idx])
|
|
except IndexError:
|
|
# If no value than use whitespace of equal size.
|
|
- row_list.append(' '*smallest[key])
|
|
+ row_list.append(' ' * smallest[key])
|
|
row_list.append('\n')
|
|
|
|
if color and row.color is not None:
|
|
body_list.append(
|
|
- terminal.AnsiText(''.join(row_list)[:-1],
|
|
- command_list=row.color))
|
|
+ terminal.AnsiText(''.join(row_list)[:-1], command_list=row.color)
|
|
+ )
|
|
body_list.append('\n')
|
|
else:
|
|
body_list.append(''.join(row_list))
|
|
|
|
first_line = False
|
|
|
|
- header = ''.join(header_list) + '='*total_width
|
|
+ header = ''.join(header_list) + '=' * total_width
|
|
if color and self._Header().color is not None:
|
|
header = terminal.AnsiText(header, command_list=self._Header().color)
|
|
# Add double line delimiter between header and main body.
|
|
@@ -915,7 +919,7 @@ class TextTable(object):
|
|
|
|
body = []
|
|
for row in self:
|
|
- # Some of the row values are pulled into the label, stored in label_prefix.
|
|
+ # Some row values are pulled into the label, stored in label_prefix.
|
|
label_prefix = []
|
|
value_list = []
|
|
for key, value in row.items():
|
|
@@ -925,8 +929,9 @@ class TextTable(object):
|
|
else:
|
|
value_list.append('%s %s' % (key, value))
|
|
|
|
- body.append(''.join(
|
|
- ['%s.%s\n' % ('.'.join(label_prefix), v) for v in value_list]))
|
|
+ body.append(
|
|
+ ''.join(['%s.%s\n' % ('.'.join(label_prefix), v) for v in value_list])
|
|
+ )
|
|
|
|
return '%s%s' % (label_str, ''.join(body))
|
|
|
|
@@ -964,7 +969,6 @@ class TextTable(object):
|
|
|
|
Raises:
|
|
TableError: Column name already exists.
|
|
-
|
|
"""
|
|
if column in self.table:
|
|
raise TableError('Column %r already in table.' % column)
|
|
@@ -1027,11 +1031,12 @@ class TextTable(object):
|
|
self.Reset()
|
|
|
|
header_row = self.row_class()
|
|
+ header_length = 0
|
|
if header:
|
|
line = buf.readline()
|
|
header_str = ''
|
|
while not header_str:
|
|
- if not isinstance(line, six.string_types):
|
|
+ if not isinstance(line, str):
|
|
line = line.decode('utf-8')
|
|
# Remove comments.
|
|
header_str = line.split('#')[0].strip()
|
|
@@ -1052,7 +1057,7 @@ class TextTable(object):
|
|
|
|
# xreadlines would be better but not supported by StringIO for testing.
|
|
for line in buf:
|
|
- if not isinstance(line, six.string_types):
|
|
+ if not isinstance(line, str):
|
|
line = line.decode('utf-8')
|
|
# Support commented lines, provide '#' is first character of line.
|
|
if line.startswith('#'):
|
|
@@ -1066,8 +1071,9 @@ class TextTable(object):
|
|
if not header:
|
|
header_row = self.row_class()
|
|
header_length = len(lst)
|
|
- header_row.values = dict(zip(range(header_length),
|
|
- range(header_length)))
|
|
+ header_row.values = dict(
|
|
+ zip(range(header_length), range(header_length))
|
|
+ )
|
|
self._table[0] = header_row
|
|
header = True
|
|
continue
|
|
@@ -1079,7 +1085,7 @@ class TextTable(object):
|
|
|
|
return self.size
|
|
|
|
- def index(self, name=None):
|
|
+ def index(self, name=None): # pylint: disable=invalid-name
|
|
"""Returns index number of supplied column name.
|
|
|
|
Args:
|
|
@@ -1093,5 +1099,5 @@ class TextTable(object):
|
|
"""
|
|
try:
|
|
return self.header.index(name)
|
|
- except ValueError:
|
|
- raise TableError('Unknown index name %s.' % name)
|
|
+ except ValueError as exc:
|
|
+ raise TableError('Unknown index name %s.' % name) from exc
|