Compare commits

..

10 Commits

Author SHA1 Message Date
Sergio Martínez Portela
55fc87cfdc Add absence of dependencies as principle.
All checks were successful
Testing / pytest (push) Successful in 20s
Testing / mypy (push) Successful in 29s
Testing / style-formatting (push) Successful in 19s
Testing / style-sorted-imports (push) Successful in 16s
Testing / stability-extra-test (push) Successful in 22s
2025-04-16 01:00:09 +02:00
Sergio Martínez Portela
f936bccf7f doc: Add a small "Principles" section to README.
All checks were successful
Testing / pytest (push) Successful in 17s
Testing / mypy (push) Successful in 22s
Testing / style-formatting (push) Successful in 20s
Testing / style-sorted-imports (push) Successful in 19s
Testing / stability-extra-test (push) Successful in 21s
2025-04-16 00:46:52 +02:00
78bd091e61 Merge pull request 'Multiple fixes on loader due to extended tests.' (#15) from fixes/loading into develop
All checks were successful
Testing / pytest (push) Successful in 20s
Testing / mypy (push) Successful in 27s
Testing / style-formatting (push) Successful in 23s
Testing / style-sorted-imports (push) Successful in 19s
Testing / stability-extra-test (push) Successful in 23s
Reviewed-on: kenkeiras/org-rw#15
2025-04-15 21:56:51 +00:00
Sergio Martínez Portela
3b90723250 format: Automatic formatting fixes.
All checks were successful
Testing / pytest (push) Successful in 23s
Testing / mypy (push) Successful in 28s
Testing / style-formatting (push) Successful in 23s
Testing / style-sorted-imports (push) Successful in 19s
Testing / stability-extra-test (push) Successful in 26s
2025-02-09 16:50:52 +01:00
Sergio Martínez Portela
506a17dc5c fix(org_rw): Ensure closing delimiters are same subtype as openers. 2025-02-09 16:50:52 +01:00
Sergio Martínez Portela
0bdb29a278 Don't cut delimiter lines out of get_lines_between(). 2025-02-09 16:50:52 +01:00
Sergio Martínez Portela
8b4e12ea2e Add dom.TableRow.get_raw() support. 2025-02-09 16:50:52 +01:00
Sergio Martínez Portela
dbac8b2d6e feat(dom): Add support for generic drawer outputs. 2025-02-09 16:50:52 +01:00
Sergio Martínez Portela
c0fc78fe33 fix(gitea): Fix build with newer images. 2025-02-09 14:13:28 +01:00
Sergio Martínez Portela
9c04717a12 Fix support of code blocks outside headlines.
Some checks failed
Testing / pytest (push) Failing after 1m11s
Testing / mypy (push) Failing after 17s
Testing / style-formatting (push) Failing after 15s
Testing / style-sorted-imports (push) Failing after 16s
Testing / stability-extra-test (push) Failing after 20s
2025-02-09 13:49:09 +01:00
7 changed files with 68 additions and 238 deletions

View File

@ -9,8 +9,8 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v3 uses: actions/checkout@v3
- run: apt-get update && apt-get install -y python3-pip - run: apt-get update && apt-get install -y python3-pip
- run: pip install -e . - run: pip install --break-system-package -e .
- run: pip install pytest - run: pip install --break-system-package pytest
- run: pytest - run: pytest
mypy: mypy:
@ -19,8 +19,8 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v3 uses: actions/checkout@v3
- run: apt-get update && apt-get install -y python3-pip - run: apt-get update && apt-get install -y python3-pip
- run: pip install -e . - run: pip install --break-system-package -e .
- run: pip install mypy - run: pip install --break-system-package mypy
- run: mypy org_rw --check-untyped-defs - run: mypy org_rw --check-untyped-defs
style-formatting: style-formatting:
@ -29,8 +29,8 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v3 uses: actions/checkout@v3
- run: apt-get update && apt-get install -y python3-pip - run: apt-get update && apt-get install -y python3-pip
- run: pip install -e . - run: pip install --break-system-package -e .
- run: pip install black - run: pip install --break-system-package black
- run: black --check . - run: black --check .
style-sorted-imports: style-sorted-imports:
@ -39,8 +39,8 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v3 uses: actions/checkout@v3
- run: apt-get update && apt-get install -y python3-pip - run: apt-get update && apt-get install -y python3-pip
- run: pip install -e . - run: pip install --break-system-package -e .
- run: pip install isort - run: pip install --break-system-package isort
- run: isort --profile black --check . - run: isort --profile black --check .
stability-extra-test: stability-extra-test:
@ -49,5 +49,5 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v3 uses: actions/checkout@v3
- run: apt-get update && apt-get install -y git-core python3-pip - 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 - run: bash extra-tests/check_all.sh

View File

@ -7,6 +7,12 @@ A python library to parse, modify and save Org-mode files.
- Modify these data and write it back to disk. - Modify these data and write it back to disk.
- Keep the original structure intact (indentation, spaces, format, ...). - Keep the original structure intact (indentation, spaces, format, ...).
** Principles
- 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 ** Safety mechanism
As this library is still in early development. Running it over files might 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 produce unexpected changes on them. For this reason it's heavily recommended to
@ -21,6 +27,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. not properly stored and can trigger this safety mechanism on a false-positive.
* Known issues * Known issues
:PROPERTIES:
:ID: 7363ba38-1662-4d3c-9e83-0999824975b7
:END:
** Structure modifications ** Structure modifications
:PROPERTIES: :PROPERTIES:
:ID: 76e77f7f-c9e0-4c83-ad2f-39a5a8894a83 :ID: 76e77f7f-c9e0-4c83-ad2f-39a5a8894a83

View File

@ -24,6 +24,14 @@ class ResultsDrawerNode(DrawerNode):
return "<Results: {}>".format(len(self.children)) return "<Results: {}>".format(len(self.children))
class GenericDrawerNode(DrawerNode):
def __init__(self, drawer_name):
self.drawer_name = drawer_name
def __repr__(self):
return "<Drawer{}: {}>".format(self.drawer_name, len(self.children))
class PropertyNode: class PropertyNode:
def __init__(self, key, value): def __init__(self, key, value):
self.key = key self.key = key
@ -62,12 +70,18 @@ class TableSeparatorRow:
def __init__(self, orig=None): def __init__(self, orig=None):
self.orig = orig self.orig = orig
def get_raw(self):
return get_raw_contents(self.orig)
class TableRow: class TableRow:
def __init__(self, cells, orig=None): def __init__(self, cells, orig=None):
self.cells = cells self.cells = cells
self.orig = orig self.orig = orig
def get_raw(self):
return get_raw_contents(self.orig)
class Text: class Text:
def __init__(self, content): def __init__(self, content):

View File

@ -122,6 +122,7 @@ NON_FINISHED_GROUPS = (
dom.ListGroupNode, dom.ListGroupNode,
dom.ResultsDrawerNode, dom.ResultsDrawerNode,
dom.PropertyDrawerNode, dom.PropertyDrawerNode,
dom.GenericDrawerNode,
) )
FREE_GROUPS = (dom.CodeBlock,) FREE_GROUPS = (dom.CodeBlock,)
@ -329,7 +330,7 @@ class Headline:
closed: Optional[Time] = None, closed: Optional[Time] = None,
): ):
self.start_line = start_line self.start_line = start_line
self._depth = depth self.depth = depth
self.orig = orig self.orig = orig
self.properties = properties self.properties = properties
self.keywords = keywords self.keywords = keywords
@ -414,6 +415,7 @@ class Headline:
if ( if (
isinstance(line, DelimiterLine) isinstance(line, DelimiterLine)
and line.delimiter_type == DelimiterLineType.END_BLOCK and line.delimiter_type == DelimiterLineType.END_BLOCK
and line.type_data.subtype == current_node.header.type_data.subtype
): ):
start = current_node.header.linenum start = current_node.header.linenum
@ -636,6 +638,13 @@ class Headline:
assert current_node is None assert current_node is None
current_node = dom.ResultsDrawerNode() 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 # TODO: Allow indentation of these blocks inside others
indentation_tree = [current_node] indentation_tree = [current_node]
tree.append(current_node) tree.append(current_node)
@ -762,16 +771,6 @@ class Headline:
pass pass
self._state = new_state self._state = new_state
@property
def depth(self):
return self._depth
@depth.setter
def depth(self, value):
self._depth = value
for child in self.children:
child.depth = value + 1
@property @property
def clock(self): def clock(self):
times = [] times = []
@ -874,9 +873,24 @@ class Headline:
yield from get_links_from_content(item.content) yield from get_links_from_content(item.content)
def get_lines_between(self, start, end): 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: if start <= line.linenum < end:
if "get_raw" in dir(line):
yield "".join(line.get_raw()) yield "".join(line.get_raw())
else:
yield line.line
def get_contents(self, format): def get_contents(self, format):
if format == "raw": if format == "raw":
@ -1076,38 +1090,6 @@ class Headline:
self.children.append(headline) self.children.append(headline)
return headline return headline
def refile(
self, destination: Union["Headline", OrgDoc], top: bool = False
) -> Union["Headline", OrgDoc]:
"""Refile this headline to a new destination.
Args:
destination: The headline to which this headline will be moved
top: Whether to append to bottom or insert at top of destination's children
Returns:
The destination headline
"""
# Remove from the parent
if self.parent:
if isinstance(self.parent, Headline):
self.parent.children.remove(self)
else:
self.parent.headlines.remove(self)
# Add ourselves to the destination
if top:
destination.children.insert(0, self)
else:
destination.children.append(self)
# Adjust the depth
self.depth = destination.depth + 1
# Adjust our parent
self.parent = destination
return destination
RawLine = collections.namedtuple("RawLine", ("linenum", "line")) RawLine = collections.namedtuple("RawLine", ("linenum", "line"))
Keyword = collections.namedtuple( Keyword = collections.namedtuple(
@ -2349,6 +2331,7 @@ class OrgDoc:
list_items, list_items,
structural, structural,
properties, properties,
delimiters,
environment=BASE_ENVIRONMENT, environment=BASE_ENVIRONMENT,
): ):
self.todo_keywords = [HeadlineState(name=kw) for kw in DEFAULT_TODO_KEYWORDS] self.todo_keywords = [HeadlineState(name=kw) for kw in DEFAULT_TODO_KEYWORDS]
@ -2378,6 +2361,7 @@ class OrgDoc:
self.list_items: List[ListItem] = list_items self.list_items: List[ListItem] = list_items
self.structural: List = structural self.structural: List = structural
self.properties: List = properties self.properties: List = properties
self.delimiters: List = delimiters
self._path = None self._path = None
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)
@ -2408,24 +2392,6 @@ class OrgDoc:
def shallow_tags(self) -> list[str]: def shallow_tags(self) -> list[str]:
return self.tags return self.tags
@property
def depth(self):
"""
Attribute to be compatible with the signature of the Headlines.
Useful when doing operations across the headline hierarchy
"""
return 0
@property
def children(self):
"""
Attribute to be compatible with the signature of the Headlines.
Useful when doing operations across the headline hierarchy
"""
return self.headlines
## Querying ## Querying
def get_links(self): def get_links(self):
for headline in self.headlines: for headline in self.headlines:
@ -2560,6 +2526,9 @@ class OrgDoc:
for struct in self.structural: for struct in self.structural:
lines.append(dump_structural(struct)) lines.append(dump_structural(struct))
for content in self.delimiters:
lines.append(dump_delimiters(content))
for kw in self.keywords: for kw in self.keywords:
lines.append(dump_kw(kw)) lines.append(dump_kw(kw))
@ -2597,6 +2566,7 @@ class OrgDocReader:
self.list_items, self.list_items,
self.structural, self.structural,
self.properties, self.properties,
self.delimiters,
self.environment, self.environment,
) )

View File

@ -9,6 +9,7 @@ from .org_rw import (
ListItem, ListItem,
RawLine, RawLine,
Strike, Strike,
TableRow,
Text, Text,
Underlined, Underlined,
Verbatim, Verbatim,
@ -50,6 +51,8 @@ def get_raw_contents(doc) -> str:
return doc.get_raw() return doc.get_raw()
if isinstance(doc, ListItem): if isinstance(doc, ListItem):
return dump_contents(doc)[1] return dump_contents(doc)[1]
if isinstance(doc, TableRow):
return dump_contents(doc)[1]
print("Unhandled type: " + str(doc)) print("Unhandled type: " + str(doc))
raise NotImplementedError("Unhandled type: " + str(doc)) raise NotImplementedError("Unhandled type: " + str(doc))

View File

@ -1 +0,0 @@
# No external requirements at this point

View File

@ -1024,171 +1024,6 @@ class TestSerde(unittest.TestCase):
self.assertEqual(dumps(doc), "* TODO First entry") self.assertEqual(dumps(doc), "* TODO First entry")
def test_refile_headline_down_to_bottom(self) -> None:
orig = """* Source Headline
** Child of Source
* Destination Headline
** Existing Child"""
doc = loads(orig)
source_headline = doc.headlines[0]
destination_headline = doc.headlines[1]
result = source_headline.refile(destination_headline)
assert result == destination_headline
assert source_headline.parent == destination_headline
assert source_headline in destination_headline.children
assert destination_headline.children[-1] == source_headline
assert (
dumps(doc)
== """* Destination Headline
** Existing Child
** Source Headline
*** Child of Source"""
)
def test_refile_headline_down_to_top(self) -> None:
orig = """* Source Headline
** Child of Source
* Destination Headline
** Existing Child"""
doc = loads(orig)
source_headline = doc.headlines[0]
destination_headline = doc.headlines[1]
result = source_headline.refile(destination_headline, top=True)
assert result == destination_headline
assert source_headline.parent == destination_headline
assert source_headline in destination_headline.children
assert destination_headline.children[0] == source_headline
assert (
dumps(doc)
== """* Destination Headline
** Source Headline
*** Child of Source
** Existing Child"""
)
def test_refile_headline_down_to_existing_child(self) -> None:
orig = """* Source Headline
** Child of Source
* Destination Parent
** Destination Headline"""
doc = loads(orig)
source_headline = doc.headlines[0]
destination_headline = doc.headlines[1]
destination_child = destination_headline.children[0]
result = source_headline.refile(destination_child)
assert result == destination_child
assert source_headline.parent == destination_child
assert source_headline in destination_child.children
assert destination_child.children[-1] == source_headline
assert (
dumps(doc)
== """* Destination Parent
** Destination Headline
*** Source Headline
**** Child of Source"""
)
def test_refile_headline_from_child_to_parent_bottom(self) -> None:
orig = """* Destination Headline
** Existing Child
*** Source Headline
**** Source Child"""
doc = loads(orig)
source_headline = doc.headlines[0].children[0].children[0]
destination_headline = doc.headlines[0]
result = source_headline.refile(destination_headline)
assert result == destination_headline
assert source_headline.parent == destination_headline
assert source_headline in destination_headline.children
assert destination_headline.children[-1] == source_headline
assert (
dumps(doc)
== """* Destination Headline
** Existing Child
** Source Headline
*** Source Child"""
)
def test_refile_headline_from_child_to_parent_top(self) -> None:
orig = """* Destination Headline
** Existing Child
*** Source Headline
**** Source Child"""
doc = loads(orig)
source_headline = doc.headlines[0].children[0].children[0]
destination_headline = doc.headlines[0]
result = source_headline.refile(destination_headline, top=True)
assert result == destination_headline
assert source_headline.parent == destination_headline
assert source_headline in destination_headline.children
assert destination_headline.children[0] == source_headline
assert (
dumps(doc)
== """* Destination Headline
** Source Headline
*** Source Child
** Existing Child"""
)
def test_refile_headline_from_child_to_first_level_at_bottom(self) -> None:
orig = """* Destination Headline
** Existing Child
*** Source Headline
**** Source Child"""
doc = loads(orig)
source_headline = doc.headlines[0].children[0].children[0]
destination_headline = doc.headlines[0].parent
result = source_headline.refile(destination_headline)
assert result == destination_headline
assert source_headline.parent == destination_headline
assert source_headline in destination_headline.children
assert destination_headline.children[-1] == source_headline
assert (
dumps(doc)
== """* Destination Headline
** Existing Child
* Source Headline
** Source Child"""
)
def test_refile_headline_from_child_to_first_level_at_top(self) -> None:
orig = """* Destination Headline
** Existing Child
*** Source Headline
**** Source Child"""
doc = loads(orig)
source_headline = doc.headlines[0].children[0].children[0]
destination_headline = doc.headlines[0].parent
result = source_headline.refile(destination_headline, top=True)
assert result == destination_headline
assert source_headline.parent == destination_headline
assert source_headline in destination_headline.children
assert destination_headline.children[0] == source_headline
assert (
dumps(doc)
== """* Source Headline
** Source Child
* Destination Headline
** Existing Child"""
)
def print_tree(tree, indentation=0, headline=None): def print_tree(tree, indentation=0, headline=None):
for element in tree: for element in tree: