[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