From 134d872ca6f81fafed3a8f880d6e05877c9680ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Mon, 22 Jul 2024 23:31:19 +0100 Subject: [PATCH 01/17] wip: First approach to get going. --- org_rw/org_rw.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index c0a1244..2120011 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -772,6 +772,20 @@ class Headline: else: raise NotImplementedError() + def update_raw_contents(self, new_contents): + # @TODO: Properly re-parse elements + self.keywords = [] + self.contents = [] + self.list_items = [] + self.table_rows = [] + self.properties = [] + self.structural = [] + self.delimiters = [] + for line in new_contents.split('\n'): + self.contents.append( + RawLine(linenum=0, line=line) + ) + def get_element_in_line(self, linenum): for line in self.contents: if linenum == line.linenum: From 071004ea7a7f816733000b5a7de27b6faf02835a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Mon, 29 Jul 2024 15:36:57 +0100 Subject: [PATCH 02/17] Quick fix for handling date data. --- org_rw/org_rw.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 86b233f..1d21c98 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -829,6 +829,10 @@ class Headline: self.properties = [] self.structural = [] self.delimiters = [] + self.scheduled = None + self.deadline = None + self.closed = None + for line in new_contents.split('\n'): self.contents.append( RawLine(linenum=0, line=line) From 5de339570d675b1c7812483981205004ea582895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Tue, 30 Jul 2024 10:21:44 +0200 Subject: [PATCH 03/17] Add (failing) test for reparse on headline update. --- tests/test_org.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/test_org.py b/tests/test_org.py index e49c6cf..c7cfd6e 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -865,6 +865,46 @@ class TestSerde(unittest.TestCase): self.assertEqual(dumps(doc), orig) + def test_update_reparse(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) + def print_tree(tree, indentation=0, headline=None): for element in tree: From d0498d2f5ba476a18955ae0979caaaa0d7718867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Tue, 30 Jul 2024 10:52:06 +0200 Subject: [PATCH 04/17] Add support for data parse on headline content update. --- org_rw/org_rw.py | 48 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 1d21c98..acca21d 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -1,5 +1,4 @@ from __future__ import annotations -from typing import Optional from datetime import timedelta import collections import difflib @@ -9,7 +8,7 @@ import re import sys from datetime import date, datetime, timedelta from enum import Enum -from typing import cast, Iterator, List, Literal, Optional, Tuple, TypedDict, Union +from typing import Any, cast, Iterator, List, Literal, Optional, Tuple, TypedDict, TypeVar, Union from .types import HeadlineDict @@ -821,7 +820,7 @@ class Headline: raise NotImplementedError() def update_raw_contents(self, new_contents): - # @TODO: Properly re-parse elements + # Clear elements self.keywords = [] self.contents = [] self.list_items = [] @@ -833,10 +832,31 @@ class Headline: self.deadline = None self.closed = None - for line in new_contents.split('\n'): - self.contents.append( - RawLine(linenum=0, line=line) - ) + 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: @@ -1054,6 +1074,19 @@ TableRow = collections.namedtuple( ), ) +ItemWithLineNum = Union[Keyword, RawLine, Property, ListItem, tuple[int, Any]] +def offset_linenum(offset: int, item: ItemWithLineNum) -> ItemWithLineNum: + if isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], int): + return item + + if isinstance(item, ListItem): + item.linenum += offset + return item + + assert isinstance(item, (Keyword, RawLine, Property)), \ + "Expected (Keyword|RawLine|Property), 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 @@ -2258,6 +2291,7 @@ class OrgDoc: self.headlines: List[Headline] = list( map(lambda hl: parse_headline(hl, self, self), headlines) ) + self.environment = environment @property def id(self): From ef615feac5e7fc178d3a6888d4e817a1891a62f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Tue, 30 Jul 2024 10:52:37 +0200 Subject: [PATCH 05/17] Allow trailing whitespace changes on raw content update. --- tests/test_org.py | 2 +- tests/utils/assertions.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_org.py b/tests/test_org.py index c7cfd6e..e66e338 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -903,7 +903,7 @@ class TestSerde(unittest.TestCase): hl.update_raw_contents(content) # Check after update - ex.assert_matches(self, hl) + ex.assert_matches(self, hl, accept_trailing_whitespace_changes=True) def print_tree(tree, indentation=0, headline=None): diff --git a/tests/utils/assertions.py b/tests/utils/assertions.py index 59dc658..732bc37 100644 --- a/tests/utils/assertions.py +++ b/tests/utils/assertions.py @@ -58,7 +58,7 @@ 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 @@ -75,7 +75,10 @@ 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: From c7f78e0a6cc7b25ddbe7bb7555b72cdda70db7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Tue, 30 Jul 2024 15:33:49 +0200 Subject: [PATCH 06/17] Add reparse test line ordering of reparsing. --- tests/13-update-reparse-test.org | 22 +++++++++++++++++ tests/test_org.py | 42 +++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tests/13-update-reparse-test.org 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 e66e338..96e0181 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 @@ -865,7 +866,7 @@ class TestSerde(unittest.TestCase): self.assertEqual(dumps(doc), orig) - def test_update_reparse(self): + def test_update_reparse_same_structure(self): with open(os.path.join(DIR, "01-simple.org")) as f: doc = load(f) @@ -905,6 +906,45 @@ class TestSerde(unittest.TestCase): # 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 print_tree(tree, indentation=0, headline=None): for element in tree: From 97db5686d80ad4fe9164560e6ab5ea255c19e83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Tue, 30 Jul 2024 17:43:46 +0200 Subject: [PATCH 07/17] Tag structural elements. --- org_rw/org_rw.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index acca21d..c8836e1 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -1026,6 +1026,9 @@ Keyword = collections.namedtuple( Property = collections.namedtuple( "Property", ("linenum", "match", "key", "value", "options") ) +Structural = collections.namedtuple( + "Structural", ("linenum", "line") +) class ListItem: @@ -1074,17 +1077,14 @@ TableRow = collections.namedtuple( ), ) -ItemWithLineNum = Union[Keyword, RawLine, Property, ListItem, tuple[int, Any]] +ItemWithLineNum = Union[Keyword, RawLine, Property, ListItem, Structural] def offset_linenum(offset: int, item: ItemWithLineNum) -> ItemWithLineNum: - if isinstance(item, tuple) and len(item) == 2 and isinstance(item[0], int): - return item - if isinstance(item, ListItem): item.linenum += offset return item - assert isinstance(item, (Keyword, RawLine, Property)), \ - "Expected (Keyword|RawLine|Property), found {}".format(item) + assert isinstance(item, (Keyword, RawLine, Property, Structural)), \ + "Expected (Keyword|RawLine|Property|Structural), found {}".format(item) return item._replace(linenum=item.linenum + offset) @@ -2465,8 +2465,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 @@ -2648,7 +2648,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"] @@ -2667,7 +2667,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)) From 15af4212aea4bc0b27d984adf444801712d9a015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Mon, 7 Oct 2024 19:48:16 +0200 Subject: [PATCH 08/17] Apply formatting scripts. --- org_rw/org_rw.py | 18 ++++++++++-------- tests/test_org.py | 20 ++++++++++---------- tests/utils/assertions.py | 11 +++++++++-- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 7133ad9..ddb88ec 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -1,4 +1,5 @@ from __future__ import annotations + import collections import difflib import logging @@ -7,7 +8,6 @@ import re import sys from datetime import date, datetime, timedelta from enum import Enum - from typing import ( Dict, Iterator, @@ -860,7 +860,9 @@ class Headline: # 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.') + 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)) @@ -879,7 +881,6 @@ class Headline: # Environment is not used, as it's known - def get_element_in_line(self, linenum): for line in self.contents: if linenum == line.linenum: @@ -1077,9 +1078,7 @@ Keyword = collections.namedtuple( Property = collections.namedtuple( "Property", ("linenum", "match", "key", "value", "options") ) -Structural = collections.namedtuple( - "Structural", ("linenum", "line") -) +Structural = collections.namedtuple("Structural", ("linenum", "line")) class ListItem: @@ -1129,13 +1128,16 @@ 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) + assert isinstance( + item, (Keyword, RawLine, Property, Structural) + ), "Expected (Keyword|RawLine|Property|Structural), found {}".format(item) return item._replace(linenum=item.linenum + offset) diff --git a/tests/test_org.py b/tests/test_org.py index 959d968..cb204f6 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -903,8 +903,8 @@ class TestSerde(unittest.TestCase): # Update lines = list(doc.dump_headline(hl, recursive=False)) - assert lines[0].startswith('* ') # Title, skip it - content = '\n'.join(lines[1:]) + assert lines[0].startswith("* ") # Title, skip it + content = "\n".join(lines[1:]) hl.update_raw_contents(content) # Check after update @@ -914,7 +914,7 @@ class TestSerde(unittest.TestCase): with open(os.path.join(DIR, "13-update-reparse-test.org")) as f: doc = load(f) - expected_hl_contents = ''' :PROPERTIES: + expected_hl_contents = """ :PROPERTIES: :ID: 13-update-reparse-first-level-id :CREATED: [2020-01-01 Wed 01:01] :END: @@ -924,12 +924,12 @@ class TestSerde(unittest.TestCase): - 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:]) + assert lines[0].startswith("* ") # Title, skip it + content = "\n".join(lines[1:]) self.assertEqual(content, expected_hl_contents) # Check after update @@ -937,17 +937,17 @@ class TestSerde(unittest.TestCase): self.assertEqual(content, expected_hl_contents) # Check after dump and reload - with tempfile.NamedTemporaryFile('wt') as f: + with tempfile.NamedTemporaryFile("wt") as f: save = org_rw.dumps(doc) f.write(save) f.flush() - with open(f.name, 'rt') as reader: + 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:]) + 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): diff --git a/tests/utils/assertions.py b/tests/utils/assertions.py index 011c255..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, accept_trailing_whitespace_changes=False): + 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 @@ -85,7 +90,9 @@ class HL: ) if accept_trailing_whitespace_changes: - test_case.assertEqual(get_raw_contents(doc).rstrip(), self.get_raw().rstrip()) + test_case.assertEqual( + get_raw_contents(doc).rstrip(), self.get_raw().rstrip() + ) else: test_case.assertEqual(get_raw_contents(doc), self.get_raw()) From 9c04717a1239182acda2af1de516a6e25d3dd079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 13:49:09 +0100 Subject: [PATCH 09/17] Fix support of code blocks outside headlines. --- org_rw/org_rw.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 31b904c..ed1cf2c 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -2307,6 +2307,7 @@ class OrgDoc: list_items, structural, properties, + delimiters, environment=BASE_ENVIRONMENT, ): self.todo_keywords = [HeadlineState(name=kw) for kw in DEFAULT_TODO_KEYWORDS] @@ -2336,6 +2337,7 @@ class OrgDoc: self.list_items: List[ListItem] = list_items self.structural: List = structural self.properties: List = properties + self.delimiters: List = delimiters self._path = None self.headlines: List[Headline] = list( map(lambda hl: parse_headline(hl, self, self), headlines) @@ -2500,6 +2502,9 @@ class OrgDoc: for struct in self.structural: lines.append(dump_structural(struct)) + for content in self.delimiters: + lines.append(dump_delimiters(content)) + for kw in self.keywords: lines.append(dump_kw(kw)) @@ -2537,6 +2542,7 @@ class OrgDocReader: self.list_items, self.structural, self.properties, + self.delimiters, self.environment, ) From c0fc78fe331df6e5859020fbbfc61e16410ffbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 14:11:32 +0100 Subject: [PATCH 10/17] fix(gitea): Fix build with newer images. --- .gitea/workflows/tests.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index f56a490..a3adf0a 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -9,8 +9,8 @@ jobs: - name: Check out repository code uses: actions/checkout@v3 - run: apt-get update && apt-get install -y python3-pip - - run: pip install -e . - - run: pip install pytest + - run: pip install --break-system-package -e . + - run: pip install --break-system-package pytest - run: pytest mypy: @@ -19,8 +19,8 @@ jobs: - name: Check out repository code uses: actions/checkout@v3 - run: apt-get update && apt-get install -y python3-pip - - run: pip install -e . - - run: pip install mypy + - run: pip install --break-system-package -e . + - run: pip install --break-system-package mypy - run: mypy org_rw --check-untyped-defs style-formatting: @@ -29,8 +29,8 @@ jobs: - name: Check out repository code uses: actions/checkout@v3 - run: apt-get update && apt-get install -y python3-pip - - run: pip install -e . - - run: pip install black + - run: pip install --break-system-package -e . + - run: pip install --break-system-package black - run: black --check . style-sorted-imports: @@ -39,8 +39,8 @@ jobs: - name: Check out repository code uses: actions/checkout@v3 - run: apt-get update && apt-get install -y python3-pip - - run: pip install -e . - - run: pip install isort + - run: pip install --break-system-package -e . + - run: pip install --break-system-package isort - run: isort --profile black --check . stability-extra-test: @@ -49,5 +49,5 @@ jobs: - name: Check out repository code uses: actions/checkout@v3 - run: apt-get update && apt-get install -y git-core python3-pip - - run: pip install -e . + - run: pip install --break-system-package -e . - run: bash extra-tests/check_all.sh From dbac8b2d6e13942881bf55e07d42560274adec7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 14:11:52 +0100 Subject: [PATCH 11/17] feat(dom): Add support for generic drawer outputs. --- org_rw/dom.py | 8 ++++++++ org_rw/org_rw.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/org_rw/dom.py b/org_rw/dom.py index f9ed40f..0b779b5 100644 --- a/org_rw/dom.py +++ b/org_rw/dom.py @@ -24,6 +24,14 @@ class ResultsDrawerNode(DrawerNode): return "".format(len(self.children)) +class GenericDrawerNode(DrawerNode): + def __init__(self, drawer_name): + self.drawer_name = drawer_name + + def __repr__(self): + return "".format(self.drawer_name, len(self.children)) + + class PropertyNode: def __init__(self, key, value): self.key = key diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index ed1cf2c..2208f3c 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -122,6 +122,7 @@ NON_FINISHED_GROUPS = ( dom.ListGroupNode, dom.ResultsDrawerNode, dom.PropertyDrawerNode, + dom.GenericDrawerNode, ) FREE_GROUPS = (dom.CodeBlock,) @@ -636,6 +637,13 @@ class Headline: assert current_node is None current_node = dom.ResultsDrawerNode() + # TODO: Allow indentation of these blocks inside others + indentation_tree = [current_node] + tree.append(current_node) + elif content.strip().startswith(':') and content.strip().endswith(':'): + assert current_node is None + current_node = dom.GenericDrawerNode(content.strip().strip(':')) + # TODO: Allow indentation of these blocks inside others indentation_tree = [current_node] tree.append(current_node) From 8b4e12ea2eb8ff754792492454be7a48fb2c1ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 16:25:39 +0100 Subject: [PATCH 12/17] Add `dom.TableRow.get_raw()` support. --- org_rw/dom.py | 7 +++++++ org_rw/utils.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/org_rw/dom.py b/org_rw/dom.py index 0b779b5..61e0882 100644 --- a/org_rw/dom.py +++ b/org_rw/dom.py @@ -70,12 +70,19 @@ class TableSeparatorRow: def __init__(self, orig=None): self.orig = orig + def get_raw(self): + return get_raw_contents(self.orig) + class TableRow: def __init__(self, cells, orig=None): self.cells = cells self.orig = orig + def get_raw(self): + return get_raw_contents(self.orig) + + class Text: def __init__(self, content): diff --git a/org_rw/utils.py b/org_rw/utils.py index 5b8b4e5..146a942 100644 --- a/org_rw/utils.py +++ b/org_rw/utils.py @@ -7,6 +7,7 @@ from .org_rw import ( Italic, Line, ListItem, + TableRow, RawLine, Strike, Text, @@ -50,6 +51,8 @@ def get_raw_contents(doc) -> str: return doc.get_raw() if isinstance(doc, ListItem): return dump_contents(doc)[1] + if isinstance(doc, TableRow): + return dump_contents(doc)[1] print("Unhandled type: " + str(doc)) raise NotImplementedError("Unhandled type: " + str(doc)) From 0bdb29a2783a76898dd709b6c23ce25fbe3a498c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 16:49:06 +0100 Subject: [PATCH 13/17] Don't cut delimiter lines out of `get_lines_between()`. --- org_rw/org_rw.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 2208f3c..8839342 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -872,9 +872,24 @@ class Headline: yield from get_links_from_content(item.content) def get_lines_between(self, start, end): - for line in self.contents: + # @TODO: Generalize for other line types too. + everything = ( + [] + # + self.keywords + + self.contents + # + self.list_items + # + self.table_rows + # + self.properties + # + self.structural + + self.delimiters + ) + + for line in everything: if start <= line.linenum < end: - yield "".join(line.get_raw()) + if 'get_raw' in dir(line): + yield "".join(line.get_raw()) + else: + yield line.line def get_contents(self, format): if format == "raw": From 506a17dc5cc233d7f971db1a0765747e724610bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 16:49:25 +0100 Subject: [PATCH 14/17] fix(org_rw): Ensure closing delimiters are same subtype as openers. --- org_rw/org_rw.py | 1 + 1 file changed, 1 insertion(+) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 8839342..f26ef94 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -415,6 +415,7 @@ class Headline: if ( isinstance(line, DelimiterLine) and line.delimiter_type == DelimiterLineType.END_BLOCK + and line.type_data.subtype == current_node.header.type_data.subtype ): start = current_node.header.linenum From 3b90723250dd7bde121eafbc5ed2bdf8d9dc125b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Sun, 9 Feb 2025 16:50:22 +0100 Subject: [PATCH 15/17] format: Automatic formatting fixes. --- org_rw/dom.py | 1 - org_rw/org_rw.py | 6 +++--- org_rw/utils.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/org_rw/dom.py b/org_rw/dom.py index 61e0882..baf0092 100644 --- a/org_rw/dom.py +++ b/org_rw/dom.py @@ -83,7 +83,6 @@ class TableRow: return get_raw_contents(self.orig) - class Text: def __init__(self, content): self.content = content diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index f26ef94..6baadd1 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -641,9 +641,9 @@ class Headline: # TODO: Allow indentation of these blocks inside others indentation_tree = [current_node] tree.append(current_node) - elif content.strip().startswith(':') and content.strip().endswith(':'): + elif content.strip().startswith(":") and content.strip().endswith(":"): assert current_node is None - current_node = dom.GenericDrawerNode(content.strip().strip(':')) + current_node = dom.GenericDrawerNode(content.strip().strip(":")) # TODO: Allow indentation of these blocks inside others indentation_tree = [current_node] @@ -887,7 +887,7 @@ class Headline: for line in everything: if start <= line.linenum < end: - if 'get_raw' in dir(line): + if "get_raw" in dir(line): yield "".join(line.get_raw()) else: yield line.line diff --git a/org_rw/utils.py b/org_rw/utils.py index 146a942..87f6712 100644 --- a/org_rw/utils.py +++ b/org_rw/utils.py @@ -7,9 +7,9 @@ from .org_rw import ( Italic, Line, ListItem, - TableRow, RawLine, Strike, + TableRow, Text, Underlined, Verbatim, From f936bccf7f668d356bdf1cee596d94a6a2e567a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Wed, 16 Apr 2025 00:46:52 +0200 Subject: [PATCH 16/17] doc: Add a small "Principles" section to README. --- README.org | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.org b/README.org index 95ec98a..253c8f6 100644 --- a/README.org +++ b/README.org @@ -7,6 +7,10 @@ A python library to parse, modify and save Org-mode files. - Modify these data and write it back to disk. - Keep the original structure intact (indentation, spaces, format, ...). +** Principles +- Data structures should be exposed as it's read on Emacs's org-mode or when in doubt as raw as possible. +- Data in the objects should be modificable, as a way to update the document itself. *Consider this a Object-oriented design.* +- *Modification of the original text if there's no change is considered a bug (see [[id:7363ba38-1662-4d3c-9e83-0999824975b7][Known issues]]).* ** Safety mechanism As this library is still in early development. Running it over files might produce unexpected changes on them. For this reason it's heavily recommended to @@ -21,6 +25,9 @@ Also, see [[id:76e77f7f-c9e0-4c83-ad2f-39a5a8894a83][Known issues:Structure modi not properly stored and can trigger this safety mechanism on a false-positive. * Known issues +:PROPERTIES: +:ID: 7363ba38-1662-4d3c-9e83-0999824975b7 +:END: ** Structure modifications :PROPERTIES: :ID: 76e77f7f-c9e0-4c83-ad2f-39a5a8894a83 From 55fc87cfdcef23eea402148c0a237976988107cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mart=C3=ADnez=20Portela?= Date: Wed, 16 Apr 2025 01:00:09 +0200 Subject: [PATCH 17/17] Add absence of dependencies as principle. --- README.org | 6 ++++-- requirements.txt | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 requirements.txt diff --git a/README.org b/README.org index 253c8f6..6f03720 100644 --- a/README.org +++ b/README.org @@ -8,9 +8,11 @@ A python library to parse, modify and save Org-mode files. - Keep the original structure intact (indentation, spaces, format, ...). ** Principles -- Data structures should be exposed as it's read on Emacs's org-mode or when in doubt as raw as possible. -- Data in the objects should be modificable, as a way to update the document itself. *Consider this a Object-oriented design.* +- Avoid any dependency outside of Python's standard library. +- Don't do anything outside of the scope of parsing/re-serializing Org-mode files. - *Modification of the original text if there's no change is considered a bug (see [[id:7363ba38-1662-4d3c-9e83-0999824975b7][Known issues]]).* +- Data structures should be exposed as it's read on Emacs's org-mode or when in doubt as raw as possible. +- Data in the objects should be modificable as a way to update the document itself. *Consider this a Object-oriented design.* ** Safety mechanism As this library is still in early development. Running it over files might produce unexpected changes on them. For this reason it's heavily recommended to diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1c51c66..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -# No external requirements at this point