Compare commits

..

No commits in common. "0e5636d2f50737455cdb322160a265ea64c61346" and "1d3b4c187ba679e3eb21c387ed1e2a033235511d" have entirely different histories.

5 changed files with 55 additions and 121 deletions

View File

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

View File

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

View File

@ -409,15 +409,6 @@ class TestSerde(unittest.TestCase):
hl.deadline.time, Timestamp(True, 2020, 12, 14, "Lun", None, None) 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): def test_update_info_file_05(self):
with open(os.path.join(DIR, "05-dates.org")) as f: with open(os.path.join(DIR, "05-dates.org")) as f:
orig = f.read() 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): def timestamp_to_datetime(ts):
return ts.time.to_datetime() return datetime(ts.year, ts.month, ts.day, ts.hour, ts.minute)
def get_raw(doc): def get_raw(doc):