WIP: Add support for updating a headline whole text contents #4

Draft
kenkeiras wants to merge 11 commits from support-updating-raw-note-contents into develop
4 changed files with 174 additions and 6 deletions

View File

@ -877,6 +877,46 @@ class Headline:
else: else:
raise NotImplementedError() 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): def get_element_in_line(self, linenum):
for line in self.contents: for line in self.contents:
if linenum == line.linenum: if linenum == line.linenum:
@ -1074,6 +1114,7 @@ Keyword = collections.namedtuple(
Property = collections.namedtuple( Property = collections.namedtuple(
"Property", ("linenum", "match", "key", "value", "options") "Property", ("linenum", "match", "key", "value", "options")
) )
Structural = collections.namedtuple("Structural", ("linenum", "line"))
class ListItem: 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 How are [YYYY-MM-DD HH:mm--HH:mm] and ([... HH:mm]--[... HH:mm]) differentiated ?
# @TODO Consider recurrence annotations # @TODO Consider recurrence annotations
@ -2340,6 +2394,7 @@ class OrgDoc:
self.headlines: List[Headline] = list( self.headlines: List[Headline] = list(
map(lambda hl: parse_headline(hl, self, self), headlines) map(lambda hl: parse_headline(hl, self, self), headlines)
) )
self.environment = environment
@property @property
def id(self): def id(self):
@ -2524,8 +2579,8 @@ class OrgDocReader:
self.delimiters: List[DelimiterLine] = [] self.delimiters: List[DelimiterLine] = []
self.list_items: List[ListItem] = [] self.list_items: List[ListItem] = []
self.table_rows: List[TableRow] = [] self.table_rows: List[TableRow] = []
self.structural: List = [] self.structural: List[Structural] = []
self.properties: List = [] self.properties: List[Property] = []
self.current_drawer: Optional[List] = None self.current_drawer: Optional[List] = None
self.environment = environment self.environment = environment
@ -2707,7 +2762,7 @@ class OrgDocReader:
def add_property_drawer_line(self, linenum: int, line: str, match: re.Match): def add_property_drawer_line(self, linenum: int, line: str, match: re.Match):
if len(self.headline_hierarchy) == 0: if len(self.headline_hierarchy) == 0:
self.current_drawer = self.properties self.current_drawer = self.properties
self.structural.append((linenum, line)) self.structural.append(Structural(linenum, line))
else: else:
assert self.headline_hierarchy[-1] is not None assert self.headline_hierarchy[-1] is not None
self.current_drawer = self.headline_hierarchy[-1]["properties"] 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): def add_drawer_end_line(self, linenum: int, line: str, match: re.Match):
self.current_drawer = None self.current_drawer = None
if len(self.headline_hierarchy) == 0: if len(self.headline_hierarchy) == 0:
self.structural.append((linenum, line)) self.structural.append(Structural(linenum, line))
else: else:
assert self.headline_hierarchy[-1] is not None assert self.headline_hierarchy[-1] is not None
self.headline_hierarchy[-1]["structural"].append((linenum, line)) self.headline_hierarchy[-1]["structural"].append((linenum, line))

View File

@ -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

View File

@ -1,4 +1,5 @@
import os import os
import tempfile
import unittest import unittest
from datetime import datetime as DT from datetime import datetime as DT
@ -869,6 +870,86 @@ class TestSerde(unittest.TestCase):
self.assertEqual(dumps(doc), orig) 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): def test_mimic_write_file_13(self):
with open(os.path.join(DIR, "13-tags.org")) as f: with open(os.path.join(DIR, "13-tags.org")) as f:
orig = f.read() orig = f.read()

View File

@ -67,7 +67,12 @@ class HL:
self.content = content self.content = content
self.children = children 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)) test_case.assertEqual(self.title, get_raw(doc.title))
# Check properties # Check properties
@ -84,7 +89,12 @@ class HL:
timestamp_to_datetime(doc_props[i].value), prop[1] 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 # Check children
if self.children is None: if self.children is None: