Compare commits

...

2 Commits

Author SHA1 Message Date
Sergio Martínez Portela
0e5636d2f5 Add base README. 2021-01-17 12:55:00 +01:00
Sergio Martínez Portela
6476e2a511 Fix support for hour ranges. 2021-01-17 12:40:15 +01:00
5 changed files with 124 additions and 58 deletions

34
org_rw/README.org Normal file
View File

@ -0,0 +1,34 @@
* Org-rw
A python library to parse, modify and save Org-mode files.
* Goals
- Reading org-mode files, with all the relevant information (format, dates, lists, links, metadata, ...).
- Modify these data and write it back to disk.
- Keep the original structure intact (indentation, spaces, format, ...).
** 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
have backup copies before using it on important files.
By default the library checks that the re-serialization of the loaded files will
not produce any change, and throw an error in case it does. But this cannot
guarantee that later changes to the document will not corrupt the output so be
careful.
Also, see [[id:76e77f7f-c9e0-4c83-ad2f-39a5a8894a83][Known issues:Structure modifications]] for cases when the structure is
not properly stored and can trigger this safety mechanism on a false-positive.
* Known issues
** Structure modifications
:PROPERTIES:
:ID: 76e77f7f-c9e0-4c83-ad2f-39a5a8894a83
:END:
- The exact format is not retained when saving dates/times. This might cause problems with the safety mechanism if you have dates that.
Note that in both cases, doing ~C-c C-c~ on the date (from Emacs) will change it to the format that Org-rw serializes it to.
- Use multiple dashes for hour ranges, like =<2020-12-01 10:00----11:00>=. It will get re-serialized as =<2020-12-01 10:00-11:00>=, thus triggering the safety mechanism as unexpected changes have happened.
- Same in case hours are not two digits (with leading 0's if needed), like =<2020-12-01 9:00>=. It will get serialized as =<2020-12-01 9:00>=.
* Other python libraries for org-mode
- [[https://github.com/karlicoss/orgparse][orgparse]] :: More mature, but does not provide format support or writing back to disk.

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import collections
import difflib
import logging
@ -54,8 +56,8 @@ NODE_PROPERTIES_RE = re.compile(
r"^(?P<indentation>\s*):(?P<key>[^+:]+)(?P<plus>\+)?:(?P<spacing>\s*)(?P<value>.+)$"
)
RAW_LINE_RE = re.compile(r"^\s*([^\s#:*]|$)")
BASE_TIME_STAMP_RE = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})( ?(?P<dow>[^ ]+))?( (?P<start_hour>\d{1,2}):(?P<start_minute>\d{1,2})(--(?P<end_hour>\d{1,2}):(?P<end_minute>\d{1,2}))?)?(?P<repetition> (?P<rep_mark>(\+|\+\+|\.\+|-|--))(?P<rep_value>\d+)(?P<rep_unit>[hdwmy]))?"
CLEAN_TIME_STAMP_RE = r"\d{4}-\d{2}-\d{2}( ?([^ ]+))?( (\d{1,2}):(\d{1,2})(--(\d{1,2}):(\d{1,2}))?)?( (\+|\+\+|\.\+|-|--)\d+[hdwmy])?"
BASE_TIME_STAMP_RE = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})( ?(?P<dow>[^ ]+))?( (?P<start_hour>\d{1,2}):(?P<start_minute>\d{1,2})(-+(?P<end_hour>\d{1,2}):(?P<end_minute>\d{1,2}))?)?(?P<repetition> (?P<rep_mark>(\+|\+\+|\.\+|-|--))(?P<rep_value>\d+)(?P<rep_unit>[hdwmy]))?"
CLEAN_TIME_STAMP_RE = r"\d{4}-\d{2}-\d{2}( ?([^ ]+))?( (\d{1,2}):(\d{1,2})(-+(\d{1,2}):(\d{1,2}))?)?( (\+|\+\+|\.\+|-|--)\d+[hdwmy])?"
ACTIVE_TIME_STAMP_RE = re.compile(r"<{}>".format(BASE_TIME_STAMP_RE))
INACTIVE_TIME_STAMP_RE = re.compile(r"\[{}\]".format(BASE_TIME_STAMP_RE))
@ -275,7 +277,7 @@ class Headline:
as_time_range = parse_org_time_range(start, end)
parsed = as_time_range
else:
parsed = parse_org_time(time_seg)
parsed = OrgTime.parse(time_seg)
times.append(parsed)
return times
@ -451,6 +453,9 @@ class Timestamp:
self.minute = minute
self.repetition = repetition
def to_datetime(self) -> datetime:
return datetime(self.year, self.month, self.day, self.hour, self.minute)
def __eq__(self, other):
if not isinstance(other, Timestamp):
return False
@ -550,7 +555,7 @@ def token_from_type(tok_type):
class TimeRange:
def __init__(self, start_time, end_time):
def __init__(self, start_time: OrgTime, end_time: OrgTime):
self.start_time = start_time
self.end_time = end_time
@ -570,11 +575,24 @@ class TimeRange:
return datetime(et.year, et.month, et.day, et.hour or 0, et.minute or 0)
def parse_org_time_range(start, end):
return TimeRange(parse_org_time(start), parse_org_time(end))
def parse_org_time_range(start, end) -> TimeRange:
return TimeRange(OrgTime.parse(start), OrgTime.parse(end))
def parse_org_time(value):
class OrgTime:
def __init__(self, ts: Timestamp, end_time: Union[Timestamp, None] = None):
assert ts is not None
self.time = ts
self.end_time = end_time
def to_raw(self):
return timestamp_to_string(self.time, self.end_time)
def __repr__(self):
return f"OrgTime({self.to_raw()})"
@classmethod
def parse(self, value: str) -> OrgTime:
if m := ACTIVE_TIME_STAMP_RE.match(value):
active = True
elif m := INACTIVE_TIME_STAMP_RE.match(value):
@ -583,7 +601,7 @@ def parse_org_time(value):
return None
if m.group("end_hour"):
return TimeRange(
return OrgTime(
Timestamp(
active,
int(m.group("year")),
@ -603,7 +621,9 @@ def parse_org_time(value):
int(m.group("end_minute")),
),
)
return Timestamp(
return OrgTime(
Timestamp(
active,
int(m.group("year")),
int(m.group("month")),
@ -613,26 +633,18 @@ def parse_org_time(value):
int(m.group("start_minute")) if m.group("start_minute") else None,
m.group("repetition").strip() if m.group("repetition") else None,
)
)
class OrgTime:
def __init__(self, ts: Timestamp):
assert ts is not None
self.time = ts
def to_raw(self):
return timestamp_to_string(self.time)
def time_from_str(s: str):
return OrgTime(parse_org_time(s))
def time_from_str(s: str) -> OrgTime:
return OrgTime.parse(s)
def timerange_to_string(tr: TimeRange):
return timestamp_to_string(tr.start_time) + "--" + timestamp_to_string(tr.end_time)
return tr.start_time.to_raw() + "--" + tr.end_time.to_raw()
def timestamp_to_string(ts: Timestamp):
def timestamp_to_string(ts: Timestamp, end_time: Union[Timestamp, None] = None) -> str:
date = "{year}-{month:02d}-{day:02d}".format(
year=ts.year, month=ts.month, day=ts.day
)
@ -646,6 +658,13 @@ def timestamp_to_string(ts: Timestamp):
else:
base = date
if end_time is not None:
assert end_time.hour is not None
assert end_time.minute is not None
base = "{base}-{hour:02}:{minute:02d}".format(
base=base, hour=end_time.hour, minute=end_time.minute
)
if ts.repetition:
base = base + " " + ts.repetition
@ -1164,10 +1183,10 @@ class OrgDoc:
if plus is None:
plus = ""
if isinstance(prop.value, Timestamp):
value = timestamp_to_string(prop.value)
elif isinstance(prop.value, TimeRange):
if isinstance(prop.value, TimeRange):
value = timerange_to_string(prop.value)
elif isinstance(prop.value, OrgTime):
value = prop.value.to_raw()
else:
value = prop.value
@ -1392,7 +1411,7 @@ class OrgDocReader:
value = as_time_range
else:
raise Exception("Unknown time range format: {}".format(value))
elif as_time := parse_org_time(value):
elif as_time := OrgTime.parse(value):
value = as_time
try:

View File

@ -8,6 +8,7 @@ SCHEDULED: <2020-12-12 Sáb> CLOSED: <2020-12-13 Dom> DEADLINE: <2020-12-14 Lun>
:JUST_DAY: [2020-12-10]
:DAY_AND_WEEKDAY: [2020-12-10 Xov]
:DAY_AND_HOUR: [2020-12-10 Xov 00:02]
:DAY_AND_HOUR_HOUR_RANGE: [2020-12-10 Xov 00:02]
:JUST_DAY_TIME_RANGE: [2020-12-10]--[2020-12-11]
:JUST_DAY_TIME_RANGE_NEGATIVE: [2020-12-11]--[2020-12-10]
:DAY_AND_WEEKDAY_TIME_RANGE: [2020-12-10 Xov]--[2020-12-11 Ven]
@ -15,3 +16,6 @@ SCHEDULED: <2020-12-12 Sáb> CLOSED: <2020-12-13 Dom> DEADLINE: <2020-12-14 Lun>
:DAY_AND_HOUR_TIME_RANGE: [2020-12-10 00:02]--[2020-12-11 00:30]
:DAY_AND_HOUR_TIME_RANGE_NEGATIVE: [2020-12-10 00:30]--[2020-12-11 00:02]
:END:
** Scheduled for time range
SCHEDULED: <2020-12-15 Mar 00:05-00:10>

View File

@ -409,6 +409,15 @@ class TestSerde(unittest.TestCase):
hl.deadline.time, Timestamp(True, 2020, 12, 14, "Lun", None, None)
)
hl_schedule_range = hl.children[0]
self.assertEqual(
hl_schedule_range.scheduled.time, Timestamp(True, 2020, 12, 15, "Mar", 0, 5)
)
self.assertEqual(
hl_schedule_range.scheduled.end_time,
Timestamp(True, 2020, 12, 15, "Mar", 0, 10),
)
def test_update_info_file_05(self):
with open(os.path.join(DIR, "05-dates.org")) as f:
orig = f.read()

View File

@ -7,7 +7,7 @@ from org_rw import (Bold, Code, Italic, Line, Strike, Text, Underlined,
def timestamp_to_datetime(ts):
return datetime(ts.year, ts.month, ts.day, ts.hour, ts.minute)
return ts.time.to_datetime()
def get_raw(doc):