diff --git a/ciscoconfparse/ccp_abc.py b/ciscoconfparse/ccp_abc.py index ee0abb1..6dd4649 100644 --- a/ciscoconfparse/ccp_abc.py +++ b/ciscoconfparse/ccp_abc.py @@ -41,7 +41,7 @@ class BaseCfgLine(metaclass=ABCMeta): parent = None child_indent = 0 is_comment = None - children = [] + _children = [] indent = 0 # assign indent in the self.text setter method confobj = None # Reference to the list object which owns it blank_line_keep = False # CiscoConfParse() uses blank_line_keep @@ -72,11 +72,11 @@ def __init__(self, all_lines=None, line=DEFAULT_TEXT, comment_delimiter="!", **k self.comment_delimiter = comment_delimiter self._uncfgtext_to_be_deprecated = "" self._text = DEFAULT_TEXT + self._children = [] self.linenum = -1 self.parent = self # by default, assign parent as itself self.child_indent = 0 self.is_comment = None - self.children = [] self.indent = 0 # assign indent in the self.text setter method self.confobj = None # Reference to the list object which owns it self.blank_line_keep = False # CiscoConfParse() uses blank_line_keep @@ -172,6 +172,27 @@ def __lt__(self, val): return True return False + @property + @logger.catch(reraise=True) + def children(self): + if isinstance(self._children, list): + return self._children + else: + error = f"Fatal: {type(self._children)} found as BaseCfgLine().children; it should be a list." + logger.critical(error) + raise NotImplementedError(error) + + @children.setter + @logger.catch(reraise=True) + def children(self, arg): + if isinstance(arg, list): + self._children = arg + return self._children + else: + error = f"{type(arg)} cannot be assigned to BaseCfgLine().children" + logger.critical(error) + raise NotImplementedError(error) + # On BaseCfgLine() @property @logger.catch(reraise=True) @@ -397,6 +418,10 @@ def text(self): @logger.catch(reraise=True) def text(self, newtext=None): """Set self.text, self.indent, self.line_id (and all comments' self.parent)""" + # Convert a config line to a string if this is a BaseCfgLine()... + if isinstance(newtext, BaseCfgLine): + newtext = newtext.text + # FIXME - children do not associate correctly if this is used as-is... if not isinstance(newtext, str): error = f"text=`{newtext}` is an invalid config line" @@ -852,11 +877,23 @@ def insert_after(self, insertstr=None): # On BaseCfgLine() @junos_unsupported + @logger.catch(reraise=True) def append_to_family( self, insertstr, indent=-1, auto_indent_width=1, auto_indent=False ): """Append an :class:`~models_cisco.IOSCfgLine` object with ``insertstr`` - as a child at the bottom of the current configuration family. + as a child at the top of the current configuration family. + + ``insertstr`` is inserted at the top of the family to ensure there are no + unintended object relationships created during the change. As an example, + it is possible that the last child is a grandchild (instead of a child) and + a simple append after that grandchild is risky to always get ``insertstr`` + indent level correct. However, we always know the correct indent for a + child of this object. + + If auto_indent is True, add ``insertstr`` with the correct left-indent + level automatically. + Parameters ---------- insertstr : str @@ -909,38 +946,103 @@ def append_to_family( ! >>> """ + + if auto_indent is True and indent > 0: + error = "indent and auto_indent are not supported together." + logger.error(error) + raise NotImplementedError(error) + + if self.confobj is None: + error = "Cannot insert on a None BaseCfgLine().confobj" + logger.critical(error) + raise NotImplementedError(error) + + insertstr_parent_indent = self.indent + # Build the string to insert with proper indentation... if auto_indent: - insertstr = (" " * (self.indent + auto_indent_width)) + insertstr.lstrip() + insertstr = (" " * (insertstr_parent_indent + auto_indent_width)) + insertstr.lstrip() elif indent > 0: insertstr = (" " * (self.indent + indent)) + insertstr.lstrip() else: - insertstr = insertstr.lstrip() + # do not modify insertstr + pass # BaseCfgLine.append_to_family(), insert a single line after this - # object's children + # object... + this_obj = type(self) + newobj = this_obj(line=insertstr) + newobj_parent = self.find_parent_for(insertstr) + if isinstance(newobj_parent, BaseCfgLine): + try: + # find index of the new parent and return... + _idx = self.confobj.ConfigObjs.index(newobj_parent) - 1 + retval = self.confobj.ConfigObjs.insert(_idx, newobj) + return retval + except BaseException as eee: + raise eee + else: + retval = self.confobj.ConfigObjs.insert(self.linenum + 1, newobj) + return retval + error = f"""Cannot append to family since this line has no parent with a lower indent: '''{insertstr}'''""" + logger.critical(error) + raise NotImplementedError(error) + + # On BaseCfgLine() + @junos_unsupported + @logger.catch(reraise=True) + def find_parent_for(self, val): + """Search all children of this object and return the most appropriate parent object for the indent associated with ``val``""" + if isinstance(val, str): + pass + elif isinstance(val, BaseCfgLine): + pass + else: + error = f"val must be a str or instance of BaseCfgLine(); however, BaseCfgLine().find_parent_for() got {type(val)}" + logger.critical(error) + raise NotImplementedError(error) + + indent_dict = {} + for obj in sorted(self.all_children): + indent_dict[obj.indent] = obj + indent_dict[self.indent] = self + try: - last_child = self.all_children[-1] - retval = self.confobj.insert_after(last_child, insertstr, atomic=False) - except IndexError: - # The object has no children - retval = self.confobj.insert_after(self, insertstr, atomic=False) - return retval + val_indent = len(val) - len(val.lstrip()) + if isinstance(val_indent, int): + for obj_indent in sorted(indent_dict.keys(), reverse=False): + if obj_indent > val_indent: + return indent_dict[obj_indent] + else: + error = f"Could not find indent for {val}" + logger.critical(error) + raise TypeError(error) + except Exception as eee: + logger.critical(str(eee)) + raise eee + + return self + # On BaseCfgLine() @logger.catch(reraise=True) def rstrip(self): - """Implement rstrip() on the BaseCfgLine().text""" + """Implement rstrip() on the BaseCfgLine().text; manually call CiscoConfParse().commit() after the rstrip()""" self._text = self._text.rstrip() + return self._text + # On BaseCfgLine() @logger.catch(reraise=True) def lstrip(self): - """Implement lstrip() on the BaseCfgLine().text""" + """Implement lstrip() on the BaseCfgLine().text; manually call CiscoConfParse().commit() after the lstrip()""" self._text = self._text.lstrip() + return self._text + # On BaseCfgLine() @logger.catch(reraise=True) def strip(self): - """Implement strip() on the BaseCfgLine().text""" + """Implement strip() on the BaseCfgLine().text; manually call CiscoConfParse().commit() after the strip()""" self._text = self._text.strip() + return self._text # On BaseCfgLine() @junos_unsupported diff --git a/ciscoconfparse/ciscoconfparse.py b/ciscoconfparse/ciscoconfparse.py index 2073ca3..5ab09e1 100644 --- a/ciscoconfparse/ciscoconfparse.py +++ b/ciscoconfparse/ciscoconfparse.py @@ -945,6 +945,12 @@ def openargs(self): retval = {"mode": "rU", "encoding": self.encoding} return retval + # This method is on CiscoConfParse() + @property + @logger.catch(reraise=True) + def text(self): + return self.ioscfg + # This method is on CiscoConfParse() @property @logger.catch(reraise=True) @@ -3290,7 +3296,14 @@ def count(self, val): # This method is on ConfigList() @ logger.catch(reraise=True) def index(self, val, *args): - return self._list.index(val, *args) + try: + return self._list.index(val, *args) + except ValueError: + error = f"{val} is not in this ConfigList()" + logger.error(error) + raise ValueError(error) + except BaseException as eee: + raise eee # This method is on ConfigList() @ logger.catch(reraise=True) @@ -3550,7 +3563,7 @@ def insert_after(self, exist_val=None, new_val=None, atomic=False, new_val_inden #self._bootstrap_from_text() self._list = self.bootstrap_obj_init_ng(self.ioscfg) else: - ## Just renumber lines... + # Just renumber lines... self.reassign_linenums() # This method is on ConfigList() @@ -3558,10 +3571,16 @@ def insert_after(self, exist_val=None, new_val=None, atomic=False, new_val_inden @ logger.catch(reraise=True) def insert(self, ii, val): if not isinstance(ii, int): - raise ValueError + error = f"The ConfigList() index must be an integer, but ConfigList().insert() got {type(ii)}" + logger.critical(error) + raise ValueError(error) + + # Get the configuration line text if val is a BaseCfgLine() instance + if isinstance(val, BaseCfgLine): + val = val.text # Coerce a string into the appropriate object - if getattr(val, "capitalize", False): + if isinstance(val, str): if self.factory: obj = config_line_factory( text=val, @@ -3576,18 +3595,18 @@ def insert(self, ii, val): ) else: - err_txt = 'insert() cannot insert "{}"'.format(val) - logger.error(err_txt) - raise ValueError(err_txt) + error = f'''insert() cannot insert {type(val)} "{val}" with factory={self.factory}''' + logger.critical(error) + raise ValueError(error) else: - err_txt = 'insert() cannot insert "{}"'.format(val) - logger.error(err_txt) - raise ValueError(err_txt) + error = f'''insert() cannot insert {type(val)} "{val}"''' + logger.critical(error) + raise TypeError(error) - ## Insert something at index ii + # Insert the object at index ii self._list.insert(ii, obj) - ## Just renumber lines... + # Just renumber lines... self.reassign_linenums() # This method is on ConfigList() diff --git a/tests/test_Ccp_Abc.py b/tests/test_Ccp_Abc.py index 2f299c8..f83999a 100755 --- a/tests/test_Ccp_Abc.py +++ b/tests/test_Ccp_Abc.py @@ -177,6 +177,7 @@ def testVal_BaseCfgLine_has_child_with_01(): def testVal_BaseCfgLine_has_child_with_02(): """Test BaseCfgLine().has_child_with()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -189,6 +190,7 @@ def testVal_BaseCfgLine_has_child_with_02(): def testVal_BaseCfgLine_has_child_with_03(): """Test BaseCfgLine().has_child_with()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -201,6 +203,7 @@ def testVal_BaseCfgLine_has_child_with_03(): def testVal_BaseCfgLine_insert_before_01(): """Test BaseCfgLine().insert_before()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -215,6 +218,7 @@ def testVal_BaseCfgLine_insert_before_01(): def testVal_BaseCfgLine_insert_before_02(): """Test BaseCfgLine().insert_before()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -229,6 +233,7 @@ def testVal_BaseCfgLine_insert_before_02(): def testVal_BaseCfgLine_insert_before_03(): """Test BaseCfgLine().insert_before() raises TypeError""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -241,6 +246,7 @@ def testVal_BaseCfgLine_insert_before_03(): def testVal_BaseCfgLine_insert_after_01(): """Test BaseCfgLine().insert_after()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -255,6 +261,7 @@ def testVal_BaseCfgLine_insert_after_01(): def testVal_BaseCfgLine_insert_after_02(): """Test BaseCfgLine().insert_after()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -269,6 +276,7 @@ def testVal_BaseCfgLine_insert_after_02(): def testVal_BaseCfgLine_insert_after_03(): """Test BaseCfgLine().insert_after() raises TypeError""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -281,36 +289,42 @@ def testVal_BaseCfgLine_insert_after_03(): def testVal_BaseCfgLine_append_to_family_01(): """Test BaseCfgLine().append_to_family()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] ) obj = parse.find_objects('interface')[0] obj.append_to_family(' description This or that') - parse.commit() - uut = parse.objs[-1] - assert uut.text == " description This or that" - assert parse.objs[0].children[-1].text == " description This or that" + assert parse.text == [ + 'interface Ethernet0/0', + ' description This or that', + ' ip address 192.0.2.1 255.255.255.0', + ' no ip proxy-arp', + ] def testVal_BaseCfgLine_append_to_family_02(): """Test BaseCfgLine().append_to_family() with a BaseCfgLine()""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] ) obj = parse.find_objects('interface')[0] obj.append_to_family(BaseCfgLine(line=' description This or that')) + # commit is required if appending a BaseCfgLine() parse.commit() - uut = parse.objs[-1] + uut = parse.objs[1] assert uut.text == " description This or that" - assert parse.objs[0].children[-1].text == " description This or that" + assert parse.objs[0].children[0].text == " description This or that" def testVal_BaseCfgLine_append_to_family_03(): """Test BaseCfgLine().append_to_family(auto_indent=False)""" parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' ["interface Ethernet0/0", " ip address 192.0.2.1 255.255.255.0", " no ip proxy-arp",] @@ -318,14 +332,76 @@ def testVal_BaseCfgLine_append_to_family_03(): obj = parse.find_objects('interface')[0] obj.append_to_family('description This or that', auto_indent=False) parse.commit() - uut = parse.objs[-1] + uut = parse.objs[1] assert uut.text == "description This or that" - # This test should be a != because we did not indent... - assert parse.objs[0].children[-1].text != "description This or that" + # Now the children should be empty; this is a new parent line + assert len(uut.children) == 1 + assert uut.children[0].text == " ip address 192.0.2.1 255.255.255.0" + assert len(uut.all_children) == 2 + assert uut.all_children[1].text == " no ip proxy-arp" - # Now the children belong to the description line... + +def testVal_BaseCfgLine_append_to_family_04(): + """Test BaseCfgLine().append_to_family(auto_indent=True) and last line is a grandchild""" + parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' + ["interface Ethernet0/0", + " ip address 192.0.2.1 255.255.255.0", + " no ip proxy-arp",] + ) + obj = parse.find_objects('interface')[0] + obj.append_to_family('description This or that', auto_indent=True) + parse.commit() + uut = parse.objs[1] + # Test that auto_indent worked + assert uut.text == ' description This or that' + assert uut.children == [] + # Ensure this is the first line in the family + assert parse.objs[0].children[0].text == ' description This or that' + + +def testVal_BaseCfgLine_append_to_family_05(): + """Test BaseCfgLine().append_to_family(auto_indent=True) and last line is a grandchild.""" + parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' + ["interface Ethernet0/0", + " ip address 192.0.2.1 255.255.255.0", + " no ip proxy-arp",] + ) + obj = parse.find_objects('interface')[0] + obj.append_to_family('description This or that', auto_indent=True) + parse.commit() + uut = parse.objs[1] + assert uut.children == [] + # Ensure the line is single indented after insert + # below a grandchild + assert uut.text == ' description This or that' + # Ensure this is the last line in the family + assert parse.objs[0].children[0].text == ' description This or that' + assert parse.objs[0].all_children[-1].text == ' no ip proxy-arp' + + +def testVal_BaseCfgLine_append_to_family_06(): + """Test BaseCfgLine().append_to_family(auto_indent=True) appending a great-grandchild of Ethernet0/0 (below ' no ip proxy-arp').""" + parse = CiscoConfParse( + # A fake double-indent on 'no ip proxy-arp' + ["interface Ethernet0/0", + " ip address 192.0.2.1 255.255.255.0", + " no ip proxy-arp",] + ) + obj = parse.find_objects('proxy-arp')[0] + obj.append_to_family('a fake great-grandchild of interface Ethernet0/0', auto_indent=True) + parse.commit() + uut = parse.objs[-1] assert uut.children == [] - assert uut.children[-1].text != " no ip proxy-arp" + # Ensure the line is correctly indented after insert + # below a grandchild + assert uut.text == ' a fake great-grandchild of interface Ethernet0/0' + # Ensure that the number of direct children is still correct + assert len(parse.objs[0].children) == 1 + # Ensure this is the last line in the family + assert parse.objs[0].all_children[-2].text == ' no ip proxy-arp' + assert parse.objs[0].all_children[-1].text == ' a fake great-grandchild of interface Ethernet0/0' def testVal_BaseCfgLine_verbose_01():