diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 31b904c..f8cf8a8 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -877,6 +877,46 @@ class Headline: else: raise NotImplementedError() + def update_raw_contents(self, new_contents): + # Clear elements + self.keywords = [] + self.contents = [] + self.list_items = [] + self.table_rows = [] + self.properties = [] + self.structural = [] + self.delimiters = [] + self.scheduled = None + self.deadline = None + self.closed = None + + reader = OrgDocReader(environment=self.doc.environment) + reader.read(new_contents) + + # No need to finalize as we can take the data from the reader instead of from a doc + if len(reader.headlines) > 0: + # Probably can be done by just adding the headlines to this one's children + raise NotImplementedError( + "new headlines on raw contents not supported yet. This probably should be simple, see comment on code." + ) + + for kw in reader.keywords: + self.keywords.append(offset_linenum(self.start_line + 1, kw)) + + for content in reader.contents: + self.contents.append(offset_linenum(self.start_line + 1, content)) + + for list_item in reader.list_items: + self.list_items.append(offset_linenum(self.start_line + 1, list_item)) + + for struct_item in reader.structural: + self.structural.append(offset_linenum(self.start_line + 1, struct_item)) + + for prop in reader.properties: + self.properties.append(offset_linenum(self.start_line + 1, prop)) + + # Environment is not used, as it's known + def get_element_in_line(self, linenum): for line in self.contents: if linenum == line.linenum: @@ -1074,6 +1114,7 @@ Keyword = collections.namedtuple( Property = collections.namedtuple( "Property", ("linenum", "match", "key", "value", "options") ) +Structural = collections.namedtuple("Structural", ("linenum", "line")) class ListItem: @@ -1122,6 +1163,19 @@ TableRow = collections.namedtuple( ), ) +ItemWithLineNum = Union[Keyword, RawLine, Property, ListItem, Structural] + + +def offset_linenum(offset: int, item: ItemWithLineNum) -> ItemWithLineNum: + if isinstance(item, ListItem): + item.linenum += offset + return item + + assert isinstance( + item, (Keyword, RawLine, Property, Structural) + ), "Expected (Keyword|RawLine|Property|Structural), found {}".format(item) + return item._replace(linenum=item.linenum + offset) + # @TODO How are [YYYY-MM-DD HH:mm--HH:mm] and ([... HH:mm]--[... HH:mm]) differentiated ? # @TODO Consider recurrence annotations @@ -2340,6 +2394,7 @@ class OrgDoc: self.headlines: List[Headline] = list( map(lambda hl: parse_headline(hl, self, self), headlines) ) + self.environment = environment @property def id(self): @@ -2524,8 +2579,8 @@ class OrgDocReader: self.delimiters: List[DelimiterLine] = [] self.list_items: List[ListItem] = [] self.table_rows: List[TableRow] = [] - self.structural: List = [] - self.properties: List = [] + self.structural: List[Structural] = [] + self.properties: List[Property] = [] self.current_drawer: Optional[List] = None self.environment = environment @@ -2707,7 +2762,7 @@ class OrgDocReader: def add_property_drawer_line(self, linenum: int, line: str, match: re.Match): if len(self.headline_hierarchy) == 0: self.current_drawer = self.properties - self.structural.append((linenum, line)) + self.structural.append(Structural(linenum, line)) else: assert self.headline_hierarchy[-1] is not None self.current_drawer = self.headline_hierarchy[-1]["properties"] @@ -2726,7 +2781,7 @@ class OrgDocReader: def add_drawer_end_line(self, linenum: int, line: str, match: re.Match): self.current_drawer = None if len(self.headline_hierarchy) == 0: - self.structural.append((linenum, line)) + self.structural.append(Structural(linenum, line)) else: assert self.headline_hierarchy[-1] is not None self.headline_hierarchy[-1]["structural"].append((linenum, line)) diff --git a/tests/13-update-reparse-test.org b/tests/13-update-reparse-test.org new file mode 100644 index 0000000..97eee86 --- /dev/null +++ b/tests/13-update-reparse-test.org @@ -0,0 +1,22 @@ +#+TITLE: 13-Update reparse +#+DESCRIPTION: Update-Reparse org file +#+TODO: TODO(t) PAUSED(p) | DONE(d) + + +* First level + :PROPERTIES: + :ID: 13-update-reparse-first-level-id + :CREATED: [2020-01-01 Wed 01:01] + :END: + First level content + + - A list of items :: + - With a sublist + + Something after the list. + +** Second level + :PROPERTIES: + :ID: 13-update-reparse-second-level-id + :END: + Second level content diff --git a/tests/test_org.py b/tests/test_org.py index a1fdff1..6c6180f 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -1,4 +1,5 @@ import os +import tempfile import unittest from datetime import datetime as DT @@ -869,6 +870,86 @@ class TestSerde(unittest.TestCase): self.assertEqual(dumps(doc), orig) + def test_update_reparse_same_structure(self): + with open(os.path.join(DIR, "01-simple.org")) as f: + doc = load(f) + + hl = doc.getTopHeadlines()[0] + ex = HL( + "First level", + props=[ + ("ID", "01-simple-first-level-id"), + ("CREATED", DT(2020, 1, 1, 1, 1)), + ], + content=" First level content\n", + children=[ + HL( + "Second level", + props=[("ID", "01-simple-second-level-id")], + content="\n Second level content\n", + children=[ + HL( + "Third level", + props=[("ID", "01-simple-third-level-id")], + content="\n Third level content\n", + ) + ], + ) + ], + ) + + # Ground check + ex.assert_matches(self, hl) + + # Update + lines = list(doc.dump_headline(hl, recursive=False)) + assert lines[0].startswith("* ") # Title, skip it + content = "\n".join(lines[1:]) + hl.update_raw_contents(content) + + # Check after update + ex.assert_matches(self, hl, accept_trailing_whitespace_changes=True) + + def test_update_reparse_same_values(self): + with open(os.path.join(DIR, "13-update-reparse-test.org")) as f: + doc = load(f) + + expected_hl_contents = """ :PROPERTIES: + :ID: 13-update-reparse-first-level-id + :CREATED: [2020-01-01 Wed 01:01] + :END: + First level content + + - A list of items :: + - With a sublist + + Something after the list. +""" + + hl = doc.getTopHeadlines()[0] + lines = list(doc.dump_headline(hl, recursive=False)) + assert lines[0].startswith("* ") # Title, skip it + content = "\n".join(lines[1:]) + self.assertEqual(content, expected_hl_contents) + + # Check after update + hl.update_raw_contents(content) + self.assertEqual(content, expected_hl_contents) + + # Check after dump and reload + with tempfile.NamedTemporaryFile("wt") as f: + save = org_rw.dumps(doc) + f.write(save) + f.flush() + + with open(f.name, "rt") as reader: + reloaded = org_rw.load(reader) + re_hl = reloaded.getTopHeadlines()[0] + lines = list(doc.dump_headline(hl, recursive=False)) + assert lines[0].startswith("* ") # Title, skip it + content = "\n".join(lines[1:]) + self.assertEqual(content, expected_hl_contents) + def test_mimic_write_file_13(self): with open(os.path.join(DIR, "13-tags.org")) as f: orig = f.read() diff --git a/tests/utils/assertions.py b/tests/utils/assertions.py index 9012d49..47ab637 100644 --- a/tests/utils/assertions.py +++ b/tests/utils/assertions.py @@ -67,7 +67,12 @@ class HL: self.content = content self.children = children - def assert_matches(self, test_case: unittest.TestCase, doc): + def assert_matches( + self, + test_case: unittest.TestCase, + doc, + accept_trailing_whitespace_changes=False, + ): test_case.assertEqual(self.title, get_raw(doc.title)) # Check properties @@ -84,7 +89,12 @@ class HL: timestamp_to_datetime(doc_props[i].value), prop[1] ) - test_case.assertEqual(get_raw_contents(doc), self.get_raw()) + if accept_trailing_whitespace_changes: + test_case.assertEqual( + get_raw_contents(doc).rstrip(), self.get_raw().rstrip() + ) + else: + test_case.assertEqual(get_raw_contents(doc), self.get_raw()) # Check children if self.children is None: