Compare commits

...

19 Commits

Author SHA1 Message Date
40d58d5488 Merge pull request 'Add TODO keywords programmatically' (#2) from feat/add-todo-keywords-programmatically into develop
All checks were successful
Testing / pytest (push) Successful in 33s
Testing / mypy (push) Successful in 44s
Testing / stability-extra-test (push) Successful in 34s
Reviewed-on: kenkeiras/org-rw#2
Reviewed-by: lyz <lyz@riseup.net>
2024-07-29 14:34:18 +00:00
Sergio Martínez Portela
09f9030818 tests: fix typings to match mypy expectations. 2024-07-29 15:31:39 +01:00
Sergio Martínez Portela
7e44bce145 Merge remote-tracking branch 'origin/develop' into feat/add-todo-keywords-programmatically 2024-07-29 15:19:58 +01:00
ef893a72a0 Merge pull request 'feat/improvements' (#1) from lyz/org-rw:feat/improvements into develop
Reviewed-on: kenkeiras/org-rw#1
Reviewed-by: kenkeiras <kenkeiras@codigoparallevar.com>
2024-07-29 14:17:16 +00:00
Lyz
9fea315115
merge upstream 2024-07-26 13:36:04 +02:00
Lyz
191bb753c4
tests: fix repetition string 2024-07-26 13:34:38 +02:00
Sergio Martínez Portela
b174405c90 Refactor headline state parsing.
- Add separate function to parse states.
- Handle edge case when no `|` is used to split TODO and DONE states.
- Add typing to the states to future-proof for handling keyboard shortcuts and actions on state changes.
2024-07-20 18:10:05 +02:00
cca2a9ec1c Merge branch 'develop' into feat/improvements 2024-07-20 15:53:55 +00:00
Sergio Martínez Portela
da2d8c8c6d Add org-todo-keywords environment to programatically set states. 2024-07-20 14:42:41 +02:00
Sergio Martínez Portela
4c169f5d47 Add (passing) test to read TODO/DONE states from file. 2024-07-20 14:42:20 +02:00
Sergio Martínez Portela
f4d63c2f93 Add (failing) test. 2024-07-20 14:41:09 +02:00
Lyz
ff841f82f0
feat: Set the default Timestamp active to True
That way you don't need to specify it if you don't want
2024-07-20 11:41:15 +02:00
Lyz
be68d10d7a
feat: initialise a Timestamp from a datetime object 2024-07-20 11:38:19 +02:00
Lyz
f640521b56
feat: add the scheduled, deadline and closed arguments to Headline init
style: Improve the type hints of Time

When reading them it's more natural to read Optional[Time] than to
assume that None is part of the Union in Time
2024-07-20 11:14:15 +02:00
Lyz
694f3f59e2
Merge branch 'develop' into feat/improvements 2024-07-20 10:49:06 +02:00
Lyz
921495fca8
Merge branch 'develop' of ssh://code.codigoparallevar.com:2022/lyz/org-rw into develop 2024-07-20 10:48:19 +02:00
Lyz
c5cc14f65c
feat(Timestamp): add the from_datetime method
To update the current Timestamp instance based on a datetime or date object.

I've also included a set_datetime method to OrgTime

feat: add activate and deactivate methods to TimeRange and OrgTime

I need it in a program I'm making
refactor: Create the Time type hint

I had to move the parse_time and parse_org_time_range below OrgTime
because it used the Time type hint and the Time type hint needed the
other two

style: reformat the code following black

style: Add some type hints and docstrings

style: remove unused imports

tests: Correct some mypy errors
2024-07-19 21:36:00 +02:00
Sergio Martínez Portela
a56ac018a8 Prepare for PyPI pushising, bumb version. 2024-07-19 20:01:27 +02:00
Sergio Martínez Portela
4fd29819ea Fix implicit link parsing. 2024-02-21 23:00:59 +01:00
7 changed files with 884 additions and 341 deletions

3
.gitignore vendored
View File

@ -139,3 +139,6 @@ dmypy.json
# Cython debug symbols
cython_debug/
# Files for PyPI publishing
README.md

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import re
from typing import List, TypedDict
class HeadlineDict(TypedDict):
linenum: int
orig: re.Match

View File

@ -5,6 +5,8 @@ set -eu
cd "`dirname $0`"
cd ..
pandoc README.org -o README.md # PyPI doesn't accept Org files
python setup.py sdist
twine upload --verbose dist/*

View File

@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="org-rw",
version="0.0.1.dev1",
version="0.0.2",
description="Library to de/serialize org-files and manipulate them.",
author="kenkeiras",
author_email="kenkeiras@codigoparallevar.com",

View File

@ -1,14 +1,23 @@
import logging
import os
import unittest
from datetime import date
from datetime import datetime as DT
from org_rw import MarkerToken, MarkerType, Timestamp, dumps, load, loads, dom
import org_rw
from utils.assertions import (BOLD, CODE, HL, ITALIC, SPAN, STRIKE, UNDERLINED,
VERBATIM, WEB_LINK, Doc, Tokens)
from utils.assertions import (
BOLD,
CODE,
HL,
ITALIC,
SPAN,
STRIKE,
UNDERLINED,
VERBATIM,
WEB_LINK,
Doc,
Tokens,
)
DIR = os.path.dirname(os.path.abspath(__file__))
@ -283,13 +292,19 @@ class TestSerde(unittest.TestCase):
SPAN("\n"),
SPAN(
" This is a ",
WEB_LINK("[tricky web link]\u200b", "https://codigoparallevar.com/4"),
WEB_LINK(
"[tricky web link]\u200b",
"https://codigoparallevar.com/4",
),
" followed up with some text.\n",
),
SPAN("\n"),
SPAN(
" This is [",
WEB_LINK("another tricky web link", "https://codigoparallevar.com/5"),
WEB_LINK(
"another tricky web link",
"https://codigoparallevar.com/5",
),
"] followed up with some text.\n",
),
],
@ -306,7 +321,7 @@ class TestSerde(unittest.TestCase):
),
],
),
)
),
)
ex.assert_matches(self, doc)
@ -471,7 +486,9 @@ class TestSerde(unittest.TestCase):
+ 'echo "with two lines"\n'
+ "exit 0 # Exit successfully",
)
self.assertEqual(snippets[0].arguments.split(), ['shell', ':results', 'verbatim'])
self.assertEqual(
snippets[0].arguments.split(), ["shell", ":results", "verbatim"]
)
self.assertEqual(
snippets[0].result,
"This is a test\n" + "with two lines",
@ -489,10 +506,10 @@ class TestSerde(unittest.TestCase):
self.assertEqual(
snippets[2].content,
'/* This code has to be escaped to\n'
+ ' * avoid confusion with new headlines.\n'
+ ' */\n'
+ 'main(){}',
"/* This code has to be escaped to\n"
+ " * avoid confusion with new headlines.\n"
+ " */\n"
+ "main(){}",
)
def test_mimic_write_file_05(self):
@ -530,7 +547,7 @@ class TestSerde(unittest.TestCase):
hl_schedule_range = hl.children[1]
self.assertEqual(
hl_schedule_range.scheduled.time,
Timestamp(True, 2020, 12, 15, "Mar", 0, 5, '++1w')
Timestamp(True, 2020, 12, 15, "Mar", 0, 5, "++1w"),
)
self.assertEqual(
hl_schedule_range.scheduled.end_time,
@ -538,7 +555,7 @@ class TestSerde(unittest.TestCase):
)
self.assertEqual(
hl_schedule_range.scheduled.repetition,
'++1w',
"++1w",
)
def test_update_info_file_05(self):
@ -591,7 +608,8 @@ class TestSerde(unittest.TestCase):
MarkerToken(closing=False, tok_type=MarkerType.UNDERLINED_MODE),
"markup",
MarkerToken(closing=True, tok_type=MarkerType.UNDERLINED_MODE),
".", "\n"
".",
"\n",
],
)
@ -625,12 +643,24 @@ class TestSerde(unittest.TestCase):
print(lists4)
self.assertEqual(len(lists4), 2)
self.assertEqual(lists4[0][0].content, ["This is a list item...", "\n that spans multiple lines", "\n"])
self.assertEqual(
lists4[0][0].content,
["This is a list item...", "\n that spans multiple lines", "\n"],
)
self.assertEqual(lists4[0][0].bullet, "-")
self.assertEqual(lists4[0][1].content, ["This is another list item...", "\n that has content on multiple lines", "\n"])
self.assertEqual(
lists4[0][1].content,
[
"This is another list item...",
"\n that has content on multiple lines",
"\n",
],
)
self.assertEqual(lists4[0][1].bullet, "-")
self.assertEqual(lists4[1][0].content, ["This is another", "\n multiline list", "\n"])
self.assertEqual(
lists4[1][0].content, ["This is another", "\n multiline list", "\n"]
)
self.assertEqual(lists4[1][0].bullet, "-")
def test_org_roam_07(self):
@ -674,20 +704,22 @@ class TestSerde(unittest.TestCase):
""".strip(),
)
def test_markup_file_09(self):
with open(os.path.join(DIR, "09-markup-on-headline.org")) as f:
doc = load(f)
hl = doc.getTopHeadlines()[0]
print(hl.title)
self.assertEqual(hl.title.contents, [
'Headline ',
MarkerToken(closing=False, tok_type=MarkerType.UNDERLINED_MODE),
'with',
MarkerToken(closing=True, tok_type=MarkerType.UNDERLINED_MODE),
' markup',
])
self.assertEqual(
hl.title.contents,
[
"Headline ",
MarkerToken(closing=False, tok_type=MarkerType.UNDERLINED_MODE),
"with",
MarkerToken(closing=True, tok_type=MarkerType.UNDERLINED_MODE),
" markup",
],
)
def test_mimic_write_file_10(self):
with open(os.path.join(DIR, "10-tables.org")) as f:
@ -708,9 +740,9 @@ class TestSerde(unittest.TestCase):
print(first_table[0])
self.assertEqual(len(first_table[0].cells), 3)
self.assertEqual(first_table[0].cells[0].strip(), 'Header1')
self.assertEqual(first_table[0].cells[1].strip(), 'Header2')
self.assertEqual(first_table[0].cells[2].strip(), 'Header3')
self.assertEqual(first_table[0].cells[0].strip(), "Header1")
self.assertEqual(first_table[0].cells[1].strip(), "Header2")
self.assertEqual(first_table[0].cells[2].strip(), "Header3")
hl = hl.children[0]
@ -720,9 +752,9 @@ class TestSerde(unittest.TestCase):
print(first_table[0])
self.assertEqual(len(first_table[0].cells), 3)
self.assertEqual(first_table[0].cells[0].strip(), 'Header1')
self.assertEqual(first_table[0].cells[1].strip(), 'Header2')
self.assertEqual(first_table[0].cells[2].strip(), 'Header3')
self.assertEqual(first_table[0].cells[0].strip(), "Header1")
self.assertEqual(first_table[0].cells[1].strip(), "Header2")
self.assertEqual(first_table[0].cells[2].strip(), "Header3")
def test_tables_html_file_10(self):
with open(os.path.join(DIR, "10-tables.org")) as f:
@ -732,27 +764,26 @@ class TestSerde(unittest.TestCase):
tree = hl.as_dom()
non_props = [
item
for item in tree
if not isinstance(item, dom.PropertyDrawerNode)
item for item in tree if not isinstance(item, dom.PropertyDrawerNode)
]
self.assertTrue(isinstance(non_props[0], dom.Text)
and isinstance(non_props[1], dom.TableNode)
and isinstance(non_props[2], dom.Text),
'Expected <Text><Table><Text>')
self.assertTrue(
isinstance(non_props[0], dom.Text)
and isinstance(non_props[1], dom.TableNode)
and isinstance(non_props[2], dom.Text),
"Expected <Text><Table><Text>",
)
hl = hl.children[0]
tree = hl.as_dom()
non_props = [
item
for item in tree
if not (isinstance(item, dom.PropertyDrawerNode)
or isinstance(item, dom.Text))
if not (
isinstance(item, dom.PropertyDrawerNode) or isinstance(item, dom.Text)
)
]
print_tree(non_props)
self.assertTrue(len(non_props) == 1,
'Expected <List>, with only (1) element')
self.assertTrue(len(non_props) == 1, "Expected <List>, with only (1) element")
def test_nested_lists_html_file_11(self):
with open(os.path.join(DIR, "11-nested-lists.org")) as f:
@ -762,30 +793,38 @@ class TestSerde(unittest.TestCase):
tree = hl.as_dom()
non_props = [
item
for item in tree
if not isinstance(item, dom.PropertyDrawerNode)
item for item in tree if not isinstance(item, dom.PropertyDrawerNode)
]
print_tree(non_props)
self.assertTrue((len(non_props) == 1) and (isinstance(non_props[0], dom.ListGroupNode)),
'Expected only <List> as top level')
self.assertTrue(
(len(non_props) == 1) and (isinstance(non_props[0], dom.ListGroupNode)),
"Expected only <List> as top level",
)
dom_list = non_props[0]
children = dom_list.children
self.assertTrue(len(children) == 5, 'Expected 5 items inside <List>, 3 texts and 2 sublists')
self.assertTrue(
len(children) == 5, "Expected 5 items inside <List>, 3 texts and 2 sublists"
)
# Assert texts
self.assertEqual(children[0].content, ['1'])
self.assertEqual(children[2].content, ['2'])
self.assertEqual(children[4].content[0], '3') # Might be ['3', '\n'] but shouldn't be a breaking change
self.assertEqual(children[0].content, ["1"])
self.assertEqual(children[2].content, ["2"])
self.assertEqual(
children[4].content[0], "3"
) # Might be ['3', '\n'] but shouldn't be a breaking change
# Assert lists
self.assertTrue(isinstance(children[1], dom.ListGroupNode), 'Expected sublist inside "1"')
self.assertEqual(children[1].children[0].content, ['1.1'])
self.assertEqual(children[1].children[1].content, ['1.2'])
self.assertTrue(isinstance(children[3], dom.ListGroupNode), 'Expected sublist inside "2"')
self.assertEqual(children[3].children[0].content, ['2.1'])
self.assertEqual(children[3].children[1].content, ['2.2'])
self.assertTrue(
isinstance(children[1], dom.ListGroupNode), 'Expected sublist inside "1"'
)
self.assertEqual(children[1].children[0].content, ["1.1"])
self.assertEqual(children[1].children[1].content, ["1.2"])
self.assertTrue(
isinstance(children[3], dom.ListGroupNode), 'Expected sublist inside "2"'
)
self.assertEqual(children[3].children[0].content, ["2.1"])
self.assertEqual(children[3].children[1].content, ["2.2"])
def test_mimic_write_file_12(self):
with open(os.path.join(DIR, "12-headlines-with-skip-levels.org")) as f:
@ -794,6 +833,38 @@ class TestSerde(unittest.TestCase):
self.assertEqual(dumps(doc), orig)
def test_add_todo_keywords_programatically(self):
orig = '''* NEW_TODO_STATE First entry
* NEW_DONE_STATE Second entry'''
doc = loads(orig, environment={
'org-todo-keywords': "NEW_TODO_STATE | NEW_DONE_STATE"
})
self.assertEqual(doc.headlines[0].is_todo, True)
self.assertEqual(doc.headlines[0].is_done, False)
self.assertEqual(doc.headlines[1].is_todo, False)
self.assertEqual(doc.headlines[1].is_done, True)
self.assertEqual(dumps(doc), orig)
def test_add_todo_keywords_in_file(self):
orig = '''#+TODO: NEW_TODO_STATE | NEW_DONE_STATE
* NEW_TODO_STATE First entry
* NEW_DONE_STATE Second entry'''
doc = loads(orig, environment={
'org-todo-keywords': "NEW_TODO_STATE | NEW_DONE_STATE"
})
self.assertEqual(doc.headlines[0].is_todo, True)
self.assertEqual(doc.headlines[0].is_done, False)
self.assertEqual(doc.headlines[1].is_todo, False)
self.assertEqual(doc.headlines[1].is_done, True)
self.assertEqual(dumps(doc), orig)
def print_tree(tree, indentation=0, headline=None):
for element in tree:
@ -812,6 +883,10 @@ def print_element(element, indentation, headline):
if isinstance(element, org_rw.Link):
print(" " * indentation * 2, "Link:", element.get_raw())
elif isinstance(element, str):
print(" " * indentation * 2, "Str[" + element.replace('\n', '<NL>') + "]", type(element))
print(
" " * indentation * 2,
"Str[" + element.replace("\n", "<NL>") + "]",
type(element),
)
else:
print_tree(element, indentation, headline)

84
tests/test_timestamp.py Normal file
View File

@ -0,0 +1,84 @@
"""Test the Timestamp object."""
import pytest
from datetime import date, datetime
from org_rw import Timestamp
def test_init_with_datetime() -> None:
datetime_obj: datetime = datetime(2024, 7, 20, 15, 45)
ts: Timestamp = Timestamp(active=True, datetime_=datetime_obj)
assert ts.active is True
assert ts._year == 2024
assert ts._month == 7
assert ts._day == 20
assert ts.hour == 15
assert ts.minute == 45
assert ts.dow is None
assert ts.repetition is None
def test_init_with_date() -> None:
date_obj: date = date(2024, 7, 20)
ts: Timestamp = Timestamp(active=True, datetime_=date_obj)
assert ts.active is True
assert ts._year == 2024
assert ts._month == 7
assert ts._day == 20
assert ts.hour is None
assert ts.minute is None
assert ts.dow is None
assert ts.repetition is None
def test_init_with_year_month_day() -> None:
ts: Timestamp = Timestamp(
active=True,
year=2024,
month=7,
day=20,
hour=15,
minute=45,
dow="Saturday",
repetition=".+1d",
)
assert ts.active is True
assert ts._year == 2024
assert ts._month == 7
assert ts._day == 20
assert ts.hour == 15
assert ts.minute == 45
assert ts.dow == "Saturday"
assert ts.repetition == ".+1d"
def test_init_without_required_arguments() -> None:
with pytest.raises(ValueError):
Timestamp(active=True)
def test_init_with_partial_date_info() -> None:
with pytest.raises(ValueError):
Timestamp(active=True, year=2024, month=7)
def test_init_with_datetime_overrides_date_info() -> None:
datetime_obj: datetime = datetime(2024, 7, 20, 15, 45)
ts: Timestamp = Timestamp(
active=True, year=2020, month=1, day=1, datetime_=datetime_obj
)
assert ts.active is True
assert ts._year == 2024
assert ts._month == 7
assert ts._day == 20
assert ts.hour == 15
assert ts.minute == 45
assert ts.dow is None
assert ts.repetition is None