[Python-modules-commits] [gtextfsm] 01/05: Imported Upstream version 0.2.1
Vincent Bernat
bernat at moszumanska.debian.org
Wed May 25 09:59:20 UTC 2016
This is an automated email from the git hooks/post-receive script.
bernat pushed a commit to branch master
in repository gtextfsm.
commit 91e72f15b50dc73e01604b17d99e12ec4aeab7c2
Author: Vincent Bernat <bernat at debian.org>
Date: Wed May 25 11:45:08 2016 +0200
Imported Upstream version 0.2.1
---
PKG-INFO | 16 +
setup.py | 36 ++
textfsm/__init__.py | 2 +
textfsm/clitable.py | 366 +++++++++++++
textfsm/copyable_regex_object.py | 40 ++
textfsm/textfsm.py | 1045 ++++++++++++++++++++++++++++++++++++++
textfsm/texttable.py | 978 +++++++++++++++++++++++++++++++++++
7 files changed, 2483 insertions(+)
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..b2f09b2
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,16 @@
+Metadata-Version: 1.1
+Name: gtextfsm
+Version: 0.2.1
+Summary: UNKNOWN
+Home-page: https://code.google.com/p/textfsm/
+Author: Google
+Author-email: textfsm-dev at googlegroups.com
+License: Apache License, Version 2.0
+Description: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries
+Requires: terminal
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..db29087
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+#
+# Copyright 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from distutils.core import setup
+
+import textfsm
+
+
+setup(name='gtextfsm',
+ maintainer='Google',
+ maintainer_email='textfsm-dev at googlegroups.com',
+ version=textfsm.__version__,
+ url='https://code.google.com/p/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',
+ 'Topic :: Software Development :: Libraries'],
+ requires=['terminal'],
+ packages = ["textfsm"],)
+ #py_modules=['clitable', 'textfsm', 'copyable_regex_object', 'texttable'])
diff --git a/textfsm/__init__.py b/textfsm/__init__.py
new file mode 100644
index 0000000..99caf84
--- /dev/null
+++ b/textfsm/__init__.py
@@ -0,0 +1,2 @@
+from textfsm import *
+__version__ = textfsm.__version__
diff --git a/textfsm/clitable.py b/textfsm/clitable.py
new file mode 100755
index 0000000..2be2a91
--- /dev/null
+++ b/textfsm/clitable.py
@@ -0,0 +1,366 @@
+#!/usr/bin/python2.6
+#
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+"""GCLI Table - CLI data in TextTable format.
+
+Class that reads CLI output and parses into tabular format.
+
+Supports the use of index files to map TextFSM templates to device/command
+output combinations and store the data in a TextTable.
+
+Is the glue between an automated command scraping program (such as RANCID) and
+the TextFSM output parser.
+"""
+
+import copy
+import os
+import re
+import threading
+import copyable_regex_object
+import textfsm
+import texttable
+
+
+class Error(Exception):
+ """Base class for errors."""
+
+
+class IndexTableError(Error):
+ """General INdexTable error."""
+
+
+class CliTableError(Error):
+ """General CliTable error."""
+
+
+class IndexTable(object):
+ """Class that reads and stores comma-separated values as a TextTable.
+
+ Stores a compiled regexp of the value for efficient matching.
+
+ Includes functions to preprocess Columns (both compiled and uncompiled).
+
+ Attributes:
+ index: TextTable, the index file parsed into a texttable.
+ compiled: TextTable, the table but with compiled regexp for each field.
+ """
+
+ def __init__(self, preread=None, precompile=None, file_path=None):
+ """Create new IndexTable object.
+
+ Args:
+ preread: func, Pre-processing, applied to each field as it is read.
+ precompile: func, Pre-compilation, applied to each field before compiling.
+ file_path: String, Location of file to use as input.
+ """
+ self.index = None
+ self.compiled = None
+ if file_path:
+ self._index_file = file_path
+ self._index_handle = open(self._index_file, 'r')
+ self._ParseIndex(preread, precompile)
+
+ def __len__(self):
+ """Returns number of rows in table."""
+ return self.index.size
+
+ def _ParseIndex(self, preread, precompile):
+ """Reads index file and stores entries in TextTable.
+
+ For optimisation reasons, a second table is created with compiled entries.
+
+ Args:
+ preread: func, Pre-processing, applied to each field as it is read.
+ precompile: func, Pre-compilation, applied to each field before compiling.
+
+ Raises:
+ IndexTableError: If the column headers has illegal column labels.
+ """
+ self.index = texttable.TextTable()
+ self.index.CsvToTable(self._index_handle)
+
+ if preread:
+ for row in self.index:
+ for col in row.header:
+ row[col] = preread(col, row[col])
+
+ self.compiled = copy.deepcopy(self.index)
+
+ for row in self.compiled:
+ for col in row.header:
+ if precompile:
+ row[col] = precompile(col, row[col])
+ if row[col]:
+ row[col] = copyable_regex_object.CopyableRegexObject(row[col])
+
+ def GetRowMatch(self, attributes):
+ """Returns the row number that matches the supplied attributes."""
+ for row in self.compiled:
+ try:
+ for key in attributes:
+ # Silently skip attributes not present in the index file.
+ # pylint: disable-msg=E1103
+ 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
+ except StopIteration:
+ pass
+ return 0
+
+
+class CliTable(texttable.TextTable):
+ """Class that reads CLI output and parses into tabular format.
+
+ Reads an index file and uses it to map command strings to templates. It then
+ uses TextFSM to parse the command output (raw) into a tabular format.
+
+ The superkey is the set of columns that contain data that uniquely defines the
+ row, the key is the row number otherwise. This is typically gathered from the
+ templates 'Key' value but is extensible.
+
+ Attributes:
+ raw: String, Unparsed command string from device/command.
+ index_file: String, file where template/command mappings reside.
+ template_dir: String, directory where index file and templates reside.
+ """
+
+ # Parse each template index only once across all instances.
+ # Without this, the regexes are parsed at every call to CliTable().
+ _lock = threading.Lock()
+ INDEX = {}
+
+ # pylint: disable-msg=C6409
+ def synchronised(func):
+ """Synchronisation decorator."""
+
+ # pylint: disable-msg=E0213
+ def Wrapper(main_obj, *args, **kwargs):
+ main_obj._lock.acquire() # pylint: disable-msg=W0212
+ try:
+ return func(main_obj, *args, **kwargs) # pylint: disable-msg=E1102
+ finally:
+ main_obj._lock.release() # pylint: disable-msg=W0212
+ return Wrapper
+ # pylint: enable-msg=C6409
+
+ @synchronised
+ def __init__(self, index_file=None, template_dir=None):
+ """Create new CLiTable object.
+
+ Args:
+ index_file: String, file where template/command mappings reside.
+ template_dir: String, directory where index file and templates reside.
+ """
+ # pylint: disable-msg=E1002
+ super(CliTable, self).__init__()
+ self._keys = set()
+ self.raw = None
+ self.index_file = index_file
+ self.template_dir = template_dir
+ if index_file:
+ self.ReadIndex(index_file)
+
+ def ReadIndex(self, index_file=None):
+ """Reads the IndexTable index file of commands and templates.
+
+ Args:
+ index_file: String, file where template/command mappings reside.
+
+ Raises:
+ CliTableError: A template column was not found in the table.
+ """
+
+ self.index_file = index_file or self.index_file
+ fullpath = os.path.join(self.template_dir, self.index_file)
+ if self.index_file and fullpath not in self.INDEX:
+ self.index = IndexTable(self._PreParse, self._PreCompile, fullpath)
+ self.INDEX[fullpath] = self.index
+ else:
+ self.index = self.INDEX[fullpath]
+
+ # Does the IndexTable have the right columns.
+ if 'Template' not in self.index.index.header: # pylint: disable-msg=E1103
+ raise CliTableError("Index file does not have 'Template' column.")
+
+ def _TemplateNamesToFiles(self, template_str):
+ """Parses a string of templates into a list of file handles."""
+
+ template_list = template_str.split(':')
+ template_files = []
+ for tmplt in template_list:
+ template_files.append(
+ open(os.path.join(self.template_dir, tmplt), 'r'))
+
+ return template_files
+
+ def ParseCmd(self, cmd_input, attributes=None, templates=None):
+ """Creates a TextTable table of values from cmd_input string.
+
+ Parses command output with template/s. If more than one template is found
+ subsequent tables are merged if keys match (dropped otherwise).
+
+ Args:
+ cmd_input: String, Device/command response.
+ attributes: Dict, attribute that further refine matching template.
+ templates: String list of templates to parse with. If None, uses index
+
+ Raises:
+ CliTableError: A template was not found for the given command.
+ """
+ # Store raw command data within the object.
+ self.raw = cmd_input
+
+ if not templates:
+ # Find template in template index.
+ row_idx = self.index.GetRowMatch(attributes)
+ if row_idx:
+ templates = self.index.index[row_idx]['Template']
+ else:
+ raise CliTableError('No template found for attributes: "%s"' %
+ attributes)
+
+ template_files = self._TemplateNamesToFiles(templates)
+ # Re-initialise the table.
+ self.Reset()
+ self._keys = set()
+ self.table = self._ParseCmdItem(self.raw, template_file=template_files[0])
+
+ # 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))
+
+ def _ParseCmdItem(self, cmd_input, template_file=None):
+ """Creates Texttable with output of command.
+
+ Args:
+ cmd_input: String, Device response.
+ template_file: File object, template to parse with.
+
+ Returns:
+ TextTable containing command output.
+
+ Raises:
+ CliTableError: A template was not found for the given command.
+ """
+ # Build FSM machine from the template.
+ fsm = textfsm.TextFSM(template_file)
+ if not self._keys:
+ self._keys = set(fsm.GetValuesByAttrib('Key'))
+
+ # Pass raw data through FSM.
+ table = texttable.TextTable()
+ table.header = fsm.header
+
+ # Fill TextTable from record entries.
+ for record in fsm.ParseText(cmd_input):
+ table.Append(record)
+ return table
+
+ def _PreParse(self, key, value):
+ """Executed against each field of each row read from index table."""
+ if key == 'Command':
+ return re.sub('(\[\[.+?\]\])', self._Completion, value)
+ else:
+ return value
+
+ def _PreCompile(self, key, value):
+ """Executed against each field of each row before compiling as regexp."""
+ if key == 'Template':
+ return
+ else:
+ return value
+
+ def _Completion(self, match):
+ # pylint: disable-msg=C6114
+ """Replaces double square brackets with variable length completion.
+
+ Completion cannot be mixed with regexp matching or '\' characters
+ i.e. '[[(\n)]] would become (\(n)?)?.'
+
+ Args:
+ match: A regex Match() object.
+
+ Returns:
+ String of the format '(a(b(c(d)?)?)?)?'.
+ """
+ # Strip the outer '[[' & ']]' and replace with ()? regexp pattern.
+ word = str(match.group())[2:-2]
+ return '(' + ('(').join(word) + ')?' * len(word)
+
+ def LabelValueTable(self, keys=None):
+ """Return LabelValue with FSM derived keys."""
+ keys = keys or self.superkey
+ # pylint: disable-msg=E1002
+ return super(CliTable, self).LabelValueTable(keys)
+
+ # pylint: disable-msg=W0622,C6409
+ def sort(self, cmp=None, key=None, reverse=False):
+ """Overrides sort func to use the KeyValue for the key."""
+ if not key and self._keys:
+ key = self.KeyValue
+ super(CliTable, self).sort(cmp=cmp, key=key, reverse=reverse)
+ # pylint: enable-msg=W0622
+
+ def AddKeys(self, key_list):
+ """Mark additional columns as being part of the superkey.
+
+ Supplements the Keys already extracted from the FSM template.
+ Useful when adding new columns to existing tables.
+ Note: This will impact attempts to further 'extend' the table as the
+ superkey must be common between tables for successful extension.
+
+ Args:
+ key_list: list of header entries to be included in the superkey.
+
+ Raises:
+ KeyError: If any entry in list is not a valid header entry.
+ """
+
+ for keyname in key_list:
+ if keyname not in self.header:
+ raise KeyError("'%s'" % keyname)
+
+ self._keys = self._keys.union(set(key_list))
+
+ @property
+ def superkey(self):
+ """Returns a set of column names that together constitute the superkey."""
+ sorted_list = []
+ for header in self.header:
+ if header in self._keys:
+ sorted_list.append(header)
+ return sorted_list
+
+ def KeyValue(self, row=None):
+ """Returns the super key value for the row."""
+ if not row:
+ if self._iterator:
+ # If we are inside an iterator use current row iteration.
+ row = self[self._iterator]
+ else:
+ row = self.row
+ # If no superkey then use row number.
+ if not self.superkey:
+ return ['%s' % row.row]
+
+ sorted_list = []
+ for header in self.header:
+ if header in self.superkey:
+ sorted_list.append(row[header])
+ return sorted_list
diff --git a/textfsm/copyable_regex_object.py b/textfsm/copyable_regex_object.py
new file mode 100755
index 0000000..6b82ea3
--- /dev/null
+++ b/textfsm/copyable_regex_object.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python2.6
+#
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+"""Work around a regression in Python 2.6 that makes RegexObjects uncopyable."""
+
+import re
+
+
+class CopyableRegexObject(object):
+ """Like a re.RegexObject, but can be copied."""
+ # pylint: disable-msg=C6409
+
+ def __init__(self, pattern):
+ self.pattern = pattern
+ self.regex = re.compile(pattern)
+
+ def match(self, *args, **kwargs):
+ return self.regex.match(*args, **kwargs)
+
+ def sub(self, *args, **kwargs):
+ return self.regex.sub(*args, **kwargs)
+
+ def __copy__(self):
+ return CopyableRegexObject(self.pattern)
+
+ def __deepcopy__(self, unused_memo):
+ return self.__copy__()
diff --git a/textfsm/textfsm.py b/textfsm/textfsm.py
new file mode 100755
index 0000000..cec9bbc
--- /dev/null
+++ b/textfsm/textfsm.py
@@ -0,0 +1,1045 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2010 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+"""Template based text parser.
+
+This module implements a parser, intended to be used for converting
+human readable text, such as command output from a router CLI, into
+a list of records, containing values extracted from the input text.
+
+A simple template language is used to describe a state machine to
+parse a specific type of text input, returning a record of values
+for each input entity.
+"""
+
+__version__ = '0.2.1'
+
+import getopt
+import inspect
+import re
+import string
+import sys
+
+
+class Error(Exception):
+ """Base class for errors."""
+
+
+class Usage(Exception):
+ """Error in command line execution."""
+
+
+class TextFSMError(Error):
+ """Error in the FSM state execution."""
+
+
+class TextFSMTemplateError(Error):
+ """Errors while parsing templates."""
+
+
+# The below exceptions are internal state change triggers
+# and not used as Errors.
+class FSMAction(Exception):
+ """Base class for actions raised with the FSM."""
+
+
+class SkipRecord(FSMAction):
+ """Indicate a record is to be skipped."""
+
+
+class SkipValue(FSMAction):
+ """Indicate a value is to be skipped."""
+
+
+class TextFSMOptions(object):
+ """Class containing all valid TextFSMValue options.
+
+ Each nested class here represents a TextFSM option. The format
+ is "option<name>".
+ Each class may override any of the methods inside the OptionBase class.
+
+ A user of this module can extend options by subclassing
+ TextFSMOptionsBase, adding the new option class(es), then passing
+ that new class to the TextFSM constructor with the 'option_class'
+ argument.
+ """
+
+ class OptionBase(object):
+ """Factory methods for option class.
+
+ Attributes:
+ value: A TextFSMValue, the parent Value.
+ """
+
+ def __init__(self, value):
+ self.value = value
+
+ @property
+ def name(self):
+ return self.__class__.__name__.replace('option', '')
+
+ def OnCreateOptions(self):
+ """Called after all options have been parsed for a Value."""
+
+ def OnClearVar(self):
+ """Called when value has been cleared."""
+
+ def OnClearAllVar(self):
+ """Called when a value has clearalled."""
+
+ def OnAssignVar(self):
+ """Called when a matched value is being assigned."""
+
+ def OnGetValue(self):
+ """Called when the value name is being requested."""
+
+ def OnSaveRecord(self):
+ """Called just prior to a record being committed."""
+
+ @classmethod
+ def ValidOptions(cls):
+ """Returns a list of valid option names."""
+ valid_options = []
+ for obj_name in dir(cls):
+ obj = getattr(cls, obj_name)
+ if inspect.isclass(obj) and issubclass(obj, cls.OptionBase):
+ valid_options.append(obj_name)
+ return valid_options
+
+ @classmethod
+ def GetOption(cls, name):
+ """Returns the class of the requested option name."""
+ return getattr(cls, name)
+
+ class Required(OptionBase):
+ """The Value must be non-empty for the row to be recorded."""
+
+ def OnSaveRecord(self):
+ if not self.value.value:
+ raise SkipRecord
+
+ class Filldown(OptionBase):
+ """Value defaults to the previous line's value."""
+
+ def OnCreateOptions(self):
+ self._myvar = None
+
+ def OnAssignVar(self):
+ self._myvar = self.value.value
+
+ def OnClearVar(self):
+ self.value.value = self._myvar
+
+ def OnClearAllVar(self):
+ self._myvar = None
+
+ class Fillup(OptionBase):
+ """Like Filldown, but upwards until it finds a non-empty entry."""
+
+ def OnAssignVar(self):
+ # If value is set, copy up the results table, until we
+ # see a set item.
+ if self.value.value:
+ # Get index of relevant result column.
+ value_idx = self.value.fsm.values.index(self.value)
+ # Go up the list from the end until we see a filled value.
+ for result in reversed(self.value.fsm._result):
+ if result[value_idx]:
+ # Stop when a record has this column already.
+ break
+ # Otherwise set the column value.
+ result[value_idx] = self.value.value
+
+ class Key(OptionBase):
+ """Value constitutes part of the Key of the record."""
+
+ class List(OptionBase):
+ """Value takes the form of a list."""
+
+ def OnCreateOptions(self):
+ self.OnClearAllVar()
+
+ def OnAssignVar(self):
+ self._value.append(self.value.value)
+
+ def OnClearVar(self):
+ if 'Filldown' not in self.value.OptionNames():
+ self._value = []
+
+ def OnClearAllVar(self):
+ self._value = []
+
+ def OnSaveRecord(self):
+ self.value.value = list(self._value)
+
+
+class TextFSMValue(object):
+ """A TextFSM value.
+
+ A value has syntax like:
+
+ 'Value Filldown,Required helloworld (.*)'
+
+ Where 'Value' is a keyword.
+ 'Filldown' and 'Required' are options.
+ 'helloworld' is the value name.
+ '(.*) is the regular expression to match in the input data.
+
+ Attributes:
+ max_name_len: (int), maximum character length os a variable name.
+ name: (str), Name of the value.
+ options: (list), A list of current Value Options.
+ regex: (str), Regex which the value is matched on.
+ template: (str), regexp with named groups added.
+ 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):
+ """Initialise a new TextFSMValue."""
+ self.max_name_len = max_name_len
+ self.name = None
+ self.options = []
+ self.regex = None
+ self.value = None
+ self.fsm = fsm
+ self._options_cls = options_class
+
+ def AssignVar(self, value):
+ """Assign a value to this Value."""
+ self.value = value
+ # Call OnAssignVar on options.
+ [option.OnAssignVar() for option in self.options]
+
+ def ClearVar(self):
+ """Clear this Value."""
+ self.value = None
+ # Call OnClearVar on options.
+ [option.OnClearVar() for option in self.options]
+
+ def ClearAllVar(self):
+ """Clear this Value."""
+ self.value = None
+ # Call OnClearAllVar on options.
+ [option.OnClearAllVar() for option in self.options]
+
+ def Header(self):
+ """Fetch the header name of this Value."""
+ # Call OnGetValue on options.
+ [option.OnGetValue() for option in self.options]
+ return self.name
+
+ def OptionNames(self):
+ """Returns a list of option names for this Value."""
+ return [option.name for option in self.options]
+
+ def Parse(self, value):
+ """Parse a 'Value' declaration.
+
+ Args:
+ value: String line from a template file, must begin with 'Value '.
+
+ Raises:
+ TextFSMTemplateError: Value declaration contains an error.
+
+ """
+
+ value_line = value.split(' ')
+ if len(value_line) < 3:
+ raise TextFSMTemplateError('Expect at least 3 tokens on line.')
+
+ if not value_line[2].startswith('('):
+ # Options are present
+ options = value_line[1]
+ for option in options.split(','):
+ self._AddOption(option)
+ # Call option OnCreateOptions callbacks
+ [option.OnCreateOptions() for option in self.options]
+
+ self.name = value_line[2]
+ self.regex = ' '.join(value_line[3:])
+ else:
+ # There were no valid options, so there are no options.
+ # Treat this argument as the name.
+ self.name = value_line[1]
+ self.regex = ' '.join(value_line[2:])
+
+ if len(self.name) > self.max_name_len:
+ raise TextFSMTemplateError(
+ "Invalid Value name '%s' or name too long." % self.name)
+
+ if (not re.match(r'^\(.*\)$', self.regex) or
+ self.regex.count('(') != self.regex.count(')')):
+ raise TextFSMTemplateError(
+ "Value '%s' must be contained within a '()' pair." % self.regex)
+
+ self.template = re.sub(r'^\(', '(?P<%s>' % self.name, self.regex)
+
+ def _AddOption(self, name):
+ """Add an option to this Value.
+
+ Args:
+ name: (str), the name of the Option to add.
+
+ Raises:
+ TextFSMTemplateError: If option is already present or
+ the option does not exist.
+ """
+
+ # Check for duplicate option declaration
+ if name in [option.name for option in self.options]:
+ raise TextFSMTemplateError('Duplicate option "%s"' % name)
+
+ # Create the option object
+ try:
+ option = self._options_cls.GetOption(name)(self)
+ except AttributeError:
+ raise TextFSMTemplateError('Unknown option "%s"' % name)
+
+ self.options.append(option)
+
+ def OnSaveRecord(self):
+ """Called just prior to a record being committed."""
+ [option.OnSaveRecord() for option in self.options]
+
+ def __str__(self):
+ """Prints out the FSM Value, mimic the input file."""
+
+ if self.options:
+ return 'Value %s %s %s' % (
+ ','.join(self.OptionNames()),
+ self.name,
+ self.regex)
+ else:
+ return 'Value %s %s' % (self.name, self.regex)
+
+
+class CopyableRegexObject(object):
+ """Like a re.RegexObject, but can be copied."""
+ # pylint: disable-msg=C6409
+
+ def __init__(self, pattern):
+ self.pattern = pattern
+ self.regex = re.compile(pattern)
+
+ def match(self, *args, **kwargs):
+ return self.regex.match(*args, **kwargs)
+
+ def sub(self, *args, **kwargs):
+ return self.regex.sub(*args, **kwargs)
+
+ def __copy__(self):
+ return CopyableRegexObject(self.pattern)
+
+ def __deepcopy__(self, unused_memo):
+ return self.__copy__()
+
+
+class TextFSMRule(object):
+ """A rule in each FSM state.
+
+ A value has syntax like:
+
+ ^<regexp> -> Next.Record State2
+
+ Where '<regexp>' is a regular expression.
+ 'Next' is a Line operator.
+ 'Record' is a Record operator.
+ 'State2' is the next State.
+
+ Attributes:
+ match: Regex to match this rule.
+ regex: match after template substitution.
+ line_op: Operator on input line on match.
+ record_op: Operator on output record on match.
+ new_state: Label to jump to on action
+ 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('(?P<match>.*)(\s->(?P<action>.*))')
+
+ # The structure to the right of the '->'.
+ LINE_OP = ('Continue', 'Next', 'Error')
+ RECORD_OP = ('Clear', 'Clearall', 'Record', 'NoRecord')
+
+ # Line operators.
+ LINE_OP_RE = '(?P<ln_op>%s)' % '|'.join(LINE_OP)
+ # Record operators.
+ RECORD_OP_RE = '(?P<rec_op>%s)' % '|'.join(RECORD_OP)
+ # Line operator with optional record operator.
+ OPERATOR_RE = '(%s(\.%s)?)' % (LINE_OP_RE, RECORD_OP_RE)
+ # New State or 'Error' string.
+ NEWSTATE_RE = '(?P<new_state>\w+|\".*\")'
+
+ # Compound operator (line and record) with optional new state.
+ ACTION_RE = re.compile('\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE))
+ # Record operator with optional new state.
+ ACTION2_RE = re.compile('\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE))
+ # Default operators with optional new state.
+ ACTION3_RE = re.compile('(\s+%s)?$' % (NEWSTATE_RE))
+
+ def __init__(self, line, line_num=-1, var_map=None):
+ """Initialise a new rule object.
+
+ Args:
+ line: (str), a template rule line to parse.
+ line_num: (int), Optional line reference included in error reporting.
+ var_map: Map for template (${var}) substitutions.
+
+ Raises:
+ TextFSMTemplateError: If 'line' is not a valid format for a Value entry.
+ """
+ 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_num = line_num
+
+ line = line.strip()
+ if not line:
+ raise TextFSMTemplateError('Null data in FSMRule. Line: %s'
+ % self.line_num)
+
+ # Is there '->' action present.
+ match_action = self.MATCH_ACTION.match(line)
+ if match_action:
+ self.match = match_action.group('match')
+ else:
+ self.match = line
+
+ # Replace ${varname} entries.
+ self.regex = self.match
+ if var_map:
+ try:
+ self.regex = string.Template(self.match).substitute(var_map)
+ except (ValueError, KeyError):
+ raise TextFSMTemplateError(
+ "Duplicate or invalid variable substitution: '%s'. Line: %s." %
+ (self.match, self.line_num))
+
+ try:
+ # Work around a regression in Python 2.6 that makes RE Objects uncopyable.
+ self.regex_obj = CopyableRegexObject(self.regex)
+ except re.error:
+ raise TextFSMTemplateError(
+ "Invalid regular expression: '%s'. Line: %s." %
+ (self.regex, self.line_num))
+
+ # No '->' present, so done.
+ if not match_action:
+ return
+
+ # Attempt to match line.record operation.
+ action_re = self.ACTION_RE.match(match_action.group('action'))
+ if not action_re:
+ # Attempt to match record operation.
+ action_re = self.ACTION2_RE.match(match_action.group('action'))
+ if not action_re:
+ # Math implicit defaults with an optional new state.
+ action_re = self.ACTION3_RE.match(match_action.group('action'))
+ if not action_re:
+ # Last attempt, match an optional new state only.
+ raise TextFSMTemplateError("Badly formatted rule '%s'. Line: %s." %
+ (line, self.line_num))
+
+ # We have an Line operator.
+ if 'ln_op' in action_re.groupdict() and action_re.group('ln_op'):
+ self.line_op = action_re.group('ln_op')
+
+ # We have a record operator.
+ if 'rec_op' in action_re.groupdict() and action_re.group('rec_op'):
+ self.record_op = action_re.group('rec_op')
+
+ # A new state was specified.
+ if 'new_state' in action_re.groupdict() and action_re.group('new_state'):
+ self.new_state = action_re.group('new_state')
+
+ # Only 'Next' (or implicit 'Next') line operator can have a new_state.
+ # But we allow error to have one as a warning message so we are left
+ # checking that Continue does not.
+ 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))
+
+ # 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('\w+', self.new_state):
+ raise TextFSMTemplateError(
+ 'Alphanumeric characters only in state names. Line: %s.'
+ % (self.line_num))
+
+ def __str__(self):
... 1540 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/gtextfsm.git
More information about the Python-modules-commits
mailing list