diff --git a/org_rw/README.org b/org_rw/README.org deleted file mode 100644 index 95ec98a..0000000 --- a/org_rw/README.org +++ /dev/null @@ -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. diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index a7949d6..3b110b8 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import collections import difflib import logging @@ -56,8 +54,8 @@ NODE_PROPERTIES_RE = re.compile( r"^(?P\s*):(?P[^+:]+)(?P\+)?:(?P\s*)(?P.+)$" ) RAW_LINE_RE = re.compile(r"^\s*([^\s#:*]|$)") -BASE_TIME_STAMP_RE = r"(?P\d{4})-(?P\d{2})-(?P\d{2})( ?(?P[^ ]+))?( (?P\d{1,2}):(?P\d{1,2})(-+(?P\d{1,2}):(?P\d{1,2}))?)?(?P (?P(\+|\+\+|\.\+|-|--))(?P\d+)(?P[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\d{4})-(?P\d{2})-(?P\d{2})( ?(?P[^ ]+))?( (?P\d{1,2}):(?P\d{1,2})(--(?P\d{1,2}):(?P\d{1,2}))?)?(?P (?P(\+|\+\+|\.\+|-|--))(?P\d+)(?P[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)) @@ -277,7 +275,7 @@ class Headline: as_time_range = parse_org_time_range(start, end) parsed = as_time_range else: - parsed = OrgTime.parse(time_seg) + parsed = parse_org_time(time_seg) times.append(parsed) return times @@ -453,9 +451,6 @@ 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 @@ -555,7 +550,7 @@ def token_from_type(tok_type): class TimeRange: - def __init__(self, start_time: OrgTime, end_time: OrgTime): + def __init__(self, start_time, end_time): self.start_time = start_time self.end_time = end_time @@ -575,76 +570,69 @@ 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) -> TimeRange: - return TimeRange(OrgTime.parse(start), OrgTime.parse(end)) +def parse_org_time_range(start, end): + return TimeRange(parse_org_time(start), parse_org_time(end)) -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 parse_org_time(value): + if m := ACTIVE_TIME_STAMP_RE.match(value): + active = True + elif m := INACTIVE_TIME_STAMP_RE.match(value): + active = False + else: + return None - 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): - active = False - else: - return None - - if m.group("end_hour"): - return OrgTime( - Timestamp( - active, - int(m.group("year")), - int(m.group("month")), - int(m.group("day")), - m.group("dow"), - int(m.group("start_hour")), - int(m.group("start_minute")), - ), - Timestamp( - active, - int(m.group("year")), - int(m.group("month")), - int(m.group("day")), - m.group("dow"), - int(m.group("end_hour")), - int(m.group("end_minute")), - ), - ) - - return OrgTime( + if m.group("end_hour"): + return TimeRange( Timestamp( active, int(m.group("year")), int(m.group("month")), int(m.group("day")), m.group("dow"), - int(m.group("start_hour")) if m.group("start_hour") else None, - int(m.group("start_minute")) if m.group("start_minute") else None, - m.group("repetition").strip() if m.group("repetition") else None, - ) + int(m.group("start_hour")), + int(m.group("start_minute")), + ), + Timestamp( + active, + int(m.group("year")), + int(m.group("month")), + int(m.group("day")), + m.group("dow"), + int(m.group("end_hour")), + int(m.group("end_minute")), + ), ) + return Timestamp( + active, + int(m.group("year")), + int(m.group("month")), + int(m.group("day")), + m.group("dow"), + int(m.group("start_hour")) if m.group("start_hour") else None, + int(m.group("start_minute")) if m.group("start_minute") else None, + m.group("repetition").strip() if m.group("repetition") else None, + ) -def time_from_str(s: str) -> OrgTime: - return OrgTime.parse(s) +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 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( 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: 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 @@ -1183,10 +1164,10 @@ class OrgDoc: if plus is None: 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) - elif isinstance(prop.value, OrgTime): - value = prop.value.to_raw() else: value = prop.value @@ -1411,7 +1392,7 @@ class OrgDocReader: value = as_time_range else: raise Exception("Unknown time range format: {}".format(value)) - elif as_time := OrgTime.parse(value): + elif as_time := parse_org_time(value): value = as_time try: diff --git a/tests/05-dates.org b/tests/05-dates.org index 8b1de23..d837035 100644 --- a/tests/05-dates.org +++ b/tests/05-dates.org @@ -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] :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] @@ -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_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> diff --git a/tests/test_org.py b/tests/test_org.py index 0f8c90e..6490641 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -409,15 +409,6 @@ 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() diff --git a/tests/utils/assertions.py b/tests/utils/assertions.py index 8e4f2d8..0e21424 100644 --- a/tests/utils/assertions.py +++ b/tests/utils/assertions.py @@ -7,7 +7,7 @@ from org_rw import (Bold, Code, Italic, Line, Strike, Text, Underlined, 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):