def test_iter(self): root = NestedList() # breakpoint() self.__test_iter_helper(root, [root]) child = root.insert_child() self.__test_iter_helper(root, [root, child]) child2 = child.insert_sibling() self.__test_iter_helper(root, [root, child, child2]) grandchild = child2.insert_child() self.__test_iter_helper(root, [root, child, child2, grandchild]) child3 = child2.insert_sibling() self.__test_iter_helper(root, [root, child, child2, grandchild, child3]) greatgrandchild = grandchild.insert_child() self.__test_iter_helper( root, [root, child, child2, grandchild, greatgrandchild, child3]) greatgreatgrandchild = greatgrandchild.insert_child() self.__test_iter_helper(root, [ root, child, child2, grandchild, greatgrandchild, greatgreatgrandchild, child3 ]) sibling2 = root.insert_sibling() sibling = root.insert_sibling() self.__test_iter_helper(root, [ root, child, child2, grandchild, greatgrandchild, greatgreatgrandchild, child3, sibling, sibling2 ])
def test_instantiation(self): root = NestedList() self.assertTrue(isinstance(root, NestedList)) child = root.insert_child() self.assertTrue(isinstance(child, NestedList)) sibling = root.insert_sibling() self.assertTrue(isinstance(sibling, NestedList))
def test_new_node(self): root = NestedList() child = root.insert_child() grandchild = child.insert_child() child2 = child.insert_sibling() self.assertIs(root.child, child) self.assertIs(child.sibling, child2) self.assertIs(root.child.child, grandchild)
def test_unindent_complex(self): one = NestedList(fields=["one"]) two = one.insert_child(texts=["two"]) three = two.insert_sibling(["three"]) three.insert_child(["four"]) five = three.insert_sibling(["five"]) five.insert_child(["six"]) target = "one" \ + "\n two" \ + "\n three" \ + "\n four" \ + "\n five" \ + "\n six" \ + "\n" self.__comp_str_to_node(one, target) two.unindent(parent=one) target = "one" \ + "\ntwo" \ + "\n three" \ + "\n four" \ + "\n five" \ + "\n six" \ + "\n" self.__comp_str_to_node(one, target) # cleanup old references del two del three del five two = one.sibling three = two.child three.unindent(parent=two) target = "one" \ + "\ntwo" \ + "\nthree" \ + "\n four" \ + "\n five" \ + "\n six" \ + "\n" self.__comp_str_to_node(one, target) # cleanup del two del three three = one.sibling.sibling five = three.child.sibling five.unindent(parent=three) target = "one" \ + "\ntwo" \ + "\nthree" \ + "\n four" \ + "\nfive" \ + "\n six" \ + "\n" self.__comp_str_to_node(one, target)
def test_get_node(self): root = NestedList() child = root.insert_child() grandchild = child.insert_child() child2 = child.insert_sibling() """ root child grandchild child2 """ self.assertIs(root.get_node(0), root) self.assertIs(root.get_node(1), child) self.assertIs(root.get_node(2), grandchild) self.assertIs(root.get_node(3), child2) grandchild2 = grandchild.insert_sibling() child3 = child2.insert_sibling() greatgrandchild = grandchild.insert_child() """ root child grandchild greatgrandchild grandchild2 child2 child3 """ self.assertIs(root.get_node(0), root) self.assertIs(root.get_node(1), child) self.assertIs(root.get_node(2), grandchild) self.assertIs(root.get_node(3), greatgrandchild) self.assertIs(root.get_node(4), grandchild2) self.assertIs(root.get_node(5), child2) self.assertIs(root.get_node(6), child3)
def test_columns(self): root = NestedList(["123", "1"]) target = "123 1\n" self.__comp_str_to_node(root, target) root.insert_sibling(["1234"]) target = "123 1\n" target += "1234\n" self.__comp_str_to_node(root, target) del root.sibling target = "123 1\n" self.__comp_str_to_node(root, target)
def test_str_complex(self): root = NestedList(fields=["root"]) child = root.insert_child(texts=["child"]) child2 = child.insert_sibling(["child2"]) grandchild = child2.insert_child(["grandchild"]) child3 = child2.insert_sibling(["child3"]) child3.insert_child(["grandchild2"]) target = "root" \ + "\n child" \ + "\n child2" \ + "\n grandchild" \ + "\n child3" \ + "\n grandchild2" \ + "\n" self.__comp_str_to_node(root, target)
def test_row_iter(self): root = NestedList(["1", "2", "3"]) result = '' for field in root.row_iter: result += field target = "1 2 3" self.assertEqual(result, target)
def test_get_count(self): root = NestedList() self.assertEqual(root.count(), 1) sibling = root.insert_sibling() self.assertEqual(root.count(), 2) child = sibling.insert_child() self.assertEqual(sibling.count(), 2) self.assertEqual(root.count(), 3) grandchild = child.insert_child() self.assertEqual(root.count(), 4) grandchild2 = grandchild.insert_sibling() self.assertEqual(root.count(), 5) grandchild.insert_sibling() self.assertEqual(root.count(), 6) child.insert_sibling() self.assertEqual(root.count(), 7)
def test_unindent_simple(self): root = NestedList(["root"]) child = root.insert_child(["child"]) child.unindent(root) target = NestedList(["root"]) target.insert_sibling(["child"]) self.assertEqual(root, target)
def __init__(self, view: View, file_path: str = None, root: NestedList = None): """ Attributes max_lines: Maximum visible line count for `result_window` __top: Available __top line position for current page (used on scrolling) __num_lines(): Available __num_lines() line position for whole pages (as length of items) current: Current highlighted line number (as window cursor) page: Total page count which being changed corresponding to result of a query (starts from 0) ┌--------------------------------------┐ |1. Item | |--------------------------------------| <- __top = 1 |2. Item | |3. Item | |4./Item///////////////////////////////| <- __cursor_y = 3, __abs_cursor_y = 4 |5. Item | |6. Item | |7. Item | |8. Item | <- window_rows = 7, __bottom() = 8 |--------------------------------------| |9. Item | |10. Item | <- __len(lines) = 10 | | | | <- page = 1 (0 and 1) └--------------------------------------┘ """ self.__banner = OneTimeBanner() self.__view = view # The y coordinate that the top of the window starts at self.__top = 0 # The x coordinate that the left of the window starts at self.__left = 0 # current cursor position on window self.__cursor_y = 0 self.__cursor_x = 0 # Start of Nested List self.__root = NestedList() if file_path is not None: self.__file_path = file_path if os.path.exists(file_path): self.__root = self.load(file_path) else: self.save(file_path) elif root is not None: self.__root = root
def test_delete_sibling(self): one = NestedList(["one"]) two = one.insert_sibling(["two"]) three = two.insert_child(["three"]) four = three.insert_sibling(["four"]) five = two.insert_sibling(["five"]) target = "one\n" \ + "two\n" \ + " three\n" \ + " four\n" \ + "five\n" actual = str(one) self.assertEqual(target, actual) del one.sibling target = "one\n" \ + " three\n" \ + " four\n" \ + "five\n" actual = str(one) self.assertEqual(target, actual)
def test_eq(self): root = NestedList(fields=["01234"]) child = root.insert_child(texts=["012", "0", "0"]) child.insert_child(["012"]) child.insert_sibling(["0", "0", "0123", ""]) root_copy = NestedList(fields=["01234"]) child_copy = root_copy.insert_child(texts=["012", "0", "0"]) child_copy.insert_child(["012"]) child_copy.insert_sibling(["0", "0", "0123", ""]) self.assertEqual(root, root_copy) root_dif = NestedList(fields=["01234"]) child_dif = root_dif.insert_child(texts=["012", "0", "0"]) child_dif.insert_child(["012"]) child_dif.insert_sibling(["0", "0", "0123", "9"]) self.assertNotEqual(root, root_dif)
def test_str(self): root = NestedList(["123", "1"]) target = "123 1\n" self.assertEqual(str(root), target) root.insert_child(["1234"]) target += " 1234\n" self.assertEqual(str(root), target) root.insert_sibling(["12"]) target += "12\n"
def test_serialization_complex(self): root = NestedList(["one", "two", "three"]) child = root.insert_child(["child1", "child2"]) grandchild = child.insert_child(["gc1"]) grandchild.insert_sibling(["gc2", "gc2"]) sibling = root.insert_sibling() sibling.insert_sibling(["sib2", "sib2"]) pickle = root.serialize() copy = NestedList.deserialize(pickle) self.assertEqual(str(root), str(copy)) self.assertEqual(root, copy)
class Model(object): __tab: str = " " __file_extension = '.nnn' def __init__(self, view: View, file_path: str = None, root: NestedList = None): """ Attributes max_lines: Maximum visible line count for `result_window` __top: Available __top line position for current page (used on scrolling) __num_lines(): Available __num_lines() line position for whole pages (as length of items) current: Current highlighted line number (as window cursor) page: Total page count which being changed corresponding to result of a query (starts from 0) ┌--------------------------------------┐ |1. Item | |--------------------------------------| <- __top = 1 |2. Item | |3. Item | |4./Item///////////////////////////////| <- __cursor_y = 3, __abs_cursor_y = 4 |5. Item | |6. Item | |7. Item | |8. Item | <- window_rows = 7, __bottom() = 8 |--------------------------------------| |9. Item | |10. Item | <- __len(lines) = 10 | | | | <- page = 1 (0 and 1) └--------------------------------------┘ """ self.__banner = OneTimeBanner() self.__view = view # The y coordinate that the top of the window starts at self.__top = 0 # The x coordinate that the left of the window starts at self.__left = 0 # current cursor position on window self.__cursor_y = 0 self.__cursor_x = 0 # Start of Nested List self.__root = NestedList() if file_path is not None: self.__file_path = file_path if os.path.exists(file_path): self.__root = self.load(file_path) else: self.save(file_path) elif root is not None: self.__root = root @property def __window_rows(self): return self.__view.num_rows @property def __window_columns(self): return self.__view.num_columns @property def __page(self): return self.__root.count() // self.__window_rows @property def __bottom(self) -> int: """Index of last line currently on screen""" return self.__top + self.__window_rows @property def __right(self) -> int: """x axis Index of last character currently on screen""" return self.__left + self.__window_columns @property def __abs_cursor_x(self): """x axis index of cursor within text (not screen)""" return self.__cursor_x + self.__left @property def __abs_cursor_y(self): """y axis index of cursor within lines (not screen)""" return self.__cursor_y + self.__top def __get_node(self, offset: int = 0, start: int = None): if start is None: start = self.__abs_cursor_y return self.__root.get_node(start + offset) @property def input_char(self) -> int: return self.__view.input_char def __correct_lateral_bounds(self): """ Puts cursor back in x-axis limits if outside """ node: NestedList = self.__get_node() left_limit: int = len(node.indent_padding) right_limit: int = node.width # put within node if self.__abs_cursor_x < left_limit: self.__abs_cursor_x = left_limit elif self.__abs_cursor_x > right_limit: self.__abs_cursor_x = right_limit else: # cursor is within bounds field_end = node.get_selected_field_end(self.__abs_cursor_x, LateralDirection.RIGHT) if field_end < self.__abs_cursor_x: # if cursor in padding of field, put it at end self.__abs_cursor_x = field_end def move(self, direction: Direction, num_spaces: int = 1): if isinstance(direction, LateralDirection): if self.__cursor_x == 0 and direction == LateralDirection.LEFT \ or self.__cursor_x == self.__window_columns - 1 and direction == LateralDirection.RIGHT: #if moving past right or left limits of screen self.scroll(direction) self.__abs_cursor_x += direction * num_spaces else: # VerticalDirection if self.__cursor_y == 0 and direction == VerticalDirection.UP \ or self.__cursor_y == self.__window_rows - 1 and direction == VerticalDirection.DOWN: # if moving past top or bottom of screen self.scroll(direction) elif direction == VerticalDirection.UP or self.__abs_cursor_y < self.__root.count() - 1: # not moving past bottom of buffer self.__cursor_y += direction * num_spaces self.__correct_lateral_bounds() def move_end(self, direction: LateralDirection): spaces = self.__get_node().width self.move(direction, spaces) def move_field_end(self, direction: LateralDirection): node = self.__get_node() self.__abs_cursor_x = node.get_selected_field_end(self.__abs_cursor_x, direction) def scroll(self, direction: Direction): """Moves the screen Prevents the screen moving past the top or bottom of its text """ if isinstance(direction, LateralDirection): # if not at absolute right or left if direction == LateralDirection.LEFT and self.__left > 0 \ or direction == LateralDirection.RIGHT and self.__get_node().width > self.__right: self.__left += direction else: # Vertical Direction # if not at absolute top or bottom of lines if direction == VerticalDirection.UP and self.__top > 0 \ or direction == VerticalDirection.DOWN and self.__root.count() > self.__bottom: self.__top += direction def page(self, direction: VerticalDirection): """Paging the window when pressing PgUp/PgDn keys""" current_page = (self.__top + self.__cursor_y) // self.__window_rows next_page = current_page + direction # The last page may have fewer items than max lines, # so we should adjust the current cursor position as maximum item count on last page if next_page == self.__page: self.__cursor_y = min(self.__cursor_y, self.__root.count() % self.__window_rows - 1) # Page up # if current page is not a first page, page up is possible # __top position can not be negative, so if __top position is going to be negative, we should set it as 0 if (direction == VerticalDirection.UP) and (current_page > 0): self.__top = max(0, self.__top - self.__window_rows) return # Page down # if current page is not a last page, page down is possible if (direction == VerticalDirection.DOWN) and (current_page < self.__page): self.__top += self.__window_rows return def display(self): """Display the items on window""" self.__view.clear() for abs_row_index, node in enumerate(self.__root): if abs_row_index < self.__top: # lines before the top of the screen continue row_index = abs_row_index - self.__top if row_index >= self.__window_rows: break # stop at end of window # Lines within visible screen indent_padding = node.indent_padding chars_before_start = self.__left - len(indent_padding) # padding within visible screen indent_padding = indent_padding[self.__left:] self.__view.addstr(row_index, 0, indent_padding, Styles.EVEN) printed_chars = len(indent_padding) for field_index, text in enumerate(node.row_iter): """ style changes between field indent precedes first field All but last field has trailing tab """ if field_index == 0: if node.collapsed: style = Styles.COLLAPSED_HEADER else: style = Styles.HEADER elif field_index % 2: style = Styles.EVEN else: style = Styles.ODD if chars_before_start > 0: # If we haven't crossed the lateral start of the line text = text[chars_before_start:] chars_before_start -= len(text) self.__view.addstr(row_index, printed_chars, text, style) printed_chars += len(text) # banner if self.__banner.has_message: self.__view.addstr(self.__window_rows - 1, 0, self.__banner.message, Styles.BANNER) self.__view.move_cursor(self.__cursor_y, self.__abs_cursor_x) def at_root(self): return self.__get_node() is self.__root def at_line_start(self) -> bool: """ :return: whether the cursor is at the start of the line (past the indent) """ return self.__abs_cursor_x == len(self.__get_node().indent_padding) def at_line_end(self): """ :return: whether the cursor is at the start of the line (past the indent) """ return self.__abs_cursor_x == self.__get_node().width def at_field_end(self, direction: LateralDirection) -> bool: return self.__get_node().get_selected_field_end(self.__abs_cursor_x, direction) == self.__abs_cursor_x def insert(self, insertion: str): """ Insert a string into the current field of the current node :param insertion: The string to insert """ node: NestedList = self.__get_node() node.insert(self.__abs_cursor_x, insertion) self.__abs_cursor_x += len(insertion) def delete(self, x_coord_offset: int): """ Delete the character at the given position :param x_coord_offset: the offset from the x_coord of the cursor where the character to be deleted is """ node: NestedList = self.__get_node() node.delete_char_at(self.__abs_cursor_x + x_coord_offset) # self.__abs_cursor_x += x_coord_offset self.move(LateralDirection.LEFT, x_coord_offset) def is_first_child(self) -> bool: """ :return: whether the cursor's current node is the first child of its parent node Vacuously false if current is root. """ current: NestedList = self.__get_node() if current is self.__root: return False previous: NestedList = self.__get_node(-1) return previous.child is current @property def current_node_has_child(self) -> bool: return self.__get_node().has_child def indent_current_node(self): """ :precondition: current node cannot be first child or root """ assert not self.is_first_child() assert not self.at_root() previous: NestedList = self.get_previous_sibling() node: NestedList = self.__get_node() node.indent(previous) # self.__abs_cursor_x += len(self.__tab) self.move(LateralDirection.RIGHT, len(self.__tab)) def unindent_current_node(self): parent: NestedList = self.get_parent() self.__get_node().unindent(parent) # self.__abs_cursor_x -= len(self.__tab) self.move(LateralDirection.LEFT, len(self.__tab)) def get_previous_sibling(self) -> NestedList: """ :precondition: must not be first child or root :return: The previous sibling of the current node """ level = self.__get_node().level for index in reversed(range(self.__abs_cursor_y)): node = self.__get_node(start=index) if node.level == level: return node if node.level < level: return NullNestedList.get_instance() raise Exception("no previous sibling or parent found. Is this root?") def get_parent(self) -> NestedList: """ Precondition: must not be level 0 node :return: The previous sibling of the current node """ parent_level = self.__get_node().level - 1 for index in reversed(range(self.__abs_cursor_y)): node = self.__get_node(start=index) if node.level == parent_level: return node raise Exception("No parent") def split_field(self): node = self.__get_node() node.split_field(self.__abs_cursor_x) # self.__abs_cursor_x += self.get_padding_len() self.move(LateralDirection.RIGHT, self.get_padding_len()) def split_node(self): if self.at_line_end() or (not self.at_field_end(LateralDirection.LEFT) and not self.at_field_end(LateralDirection.RIGHT)): self.split_field() if self.at_field_end(LateralDirection.RIGHT): # cursor needs to be on the first field to move over for node.split() self.move(LateralDirection.RIGHT, self.get_padding_len()) self.__get_node().split(self.__abs_cursor_x) self.move(VerticalDirection.DOWN) self.move(LateralDirection.LEFT, self.__abs_cursor_x) def combine_nodes(self): """ removes this row and adds its fields onto the previous row """ to_remove = self.__get_node() assert to_remove.level == 0 # prev_row: not necessarily the previous_sibling prev_row: NestedList = self.__get_node(offset=-1) prev_sibling: NestedList = self.get_previous_sibling() self.move(VerticalDirection.UP) self.move_end(LateralDirection.RIGHT) to_remove.combine(prev_row, prev_sibling) self.move(LateralDirection.RIGHT, self.get_padding_len()) def get_column_width(self) -> int: """ :return: The width of the column at this field """ node = self.__get_node() field_index = node.get_field_index(self.__abs_cursor_x) return len(node.get_padded_field(field_index)) def get_field(self) -> str: node = self.__get_node() field_index = node.get_field_index(self.__abs_cursor_x) return node.get_field(field_index) def get_neighbor_field(self, direction: LateralDirection) -> str: """ :precondition: cursor cannot currently be in the first field and direction be left """ node = self.__get_node() field_index = node.get_field_index(self.__abs_cursor_x) return node.get_field(field_index + direction) def get_padding_len(self) -> int: """ :return: The length of the padding on the current field or the padding that would be on the field if it were not the last """ node = self.__get_node() field_index = node.get_field_index(self.__abs_cursor_x) return node.get_padding_len(field_index) def get_neighbor_padding_len(self, direction: LateralDirection) -> int: """ :precondition: cursor cannot currently be in the first field and direction be left :param direction: :return: the number of characters in the padding of the field to the right or left of the field inhabited by the cursor """ node = self.__get_node() field_index = node.get_field_index(self.__abs_cursor_x) return node.get_padding_len(field_index + direction) def get_neighbor_column_width(self, direction: LateralDirection) -> int: """ :precondition: cursor cannot currently be in the first field and direction be left :param direction: :return: the number of characters in the padding of the field to the right or left of the field inhabited by the cursor """ node = self.__get_node() field_index = node.get_field_index(self.__abs_cursor_x) return len(node.get_padded_field(field_index + direction)) def combine_fields(self, direction: LateralDirection): """ :preconditions: If direction is left, the cursor is on the left edge of the current field If right, it is on the right edge of the current field :param direction: The direction of the field that will be combined with the current field """ assert self.at_field_end(direction) node = self.__get_node() # movement must be calculated before node combination, even though only used by left combine movement = self.get_neighbor_padding_len(direction) node.combine_fields(self.__abs_cursor_x, direction) if direction == LateralDirection.LEFT: # self.__abs_cursor_x -= movement self.move(LateralDirection.LEFT, movement) def signal_user_error(self): self.__view.signal_user_error() def get_level(self) -> int: return self.__get_node().level def toggle_current_node_collapsed(self): self.__get_node().toggle_collapsed() @property def collapsed(self) -> bool: return self.__get_node().collapsed def save(self, file_path: str = None): if file_path is None: file_path = self.__file_path if not file_path.endswith(self.__file_extension): file_path += self.__file_extension pickle = self.__root.serialize() with open(file_path, 'w') as file: file.write(json.dumps(pickle, indent=4)) self.__banner.message = 'Changes saved to {}'.format(file_path) def load(self, file_path: str) -> NestedList: assert file_path.endswith(self.__file_extension) with open(file_path, 'r') as file: pickle = json.load(file) return NestedList.deserialize(pickle)
def test_eq_simple(self): root = NestedList(fields=["01234"]) self.assertEqual(root, root) other = NestedList(fields=["01234"]) self.assertEqual(root, other)
def test_serialization_simple(self): one = NestedList(["one", "two", "three"]) pickle = one.serialize() copy = NestedList.deserialize(pickle) self.assertEqual(str(one), str(copy)) self.assertEqual(one, copy)
def test_delete_simple(self): root = NestedList(["root"]) root.insert_sibling(["sib"]) del root.sibling target = NestedList(["root"]) self.assertEqual(root, target) root.insert_child(["child"]) del root.child self.assertEqual(root, target) root.insert_sibling(["sib1"]) root.insert_sibling(["sib2"]) del root.sibling target.insert_sibling(["sib1"]) self.assertEqual(root, target)
def load(self, file_path: str) -> NestedList: assert file_path.endswith(self.__file_extension) with open(file_path, 'r') as file: pickle = json.load(file) return NestedList.deserialize(pickle)