feat: enhance type annotations and formatting #7

Merged
kenkeiras merged 1 commits from lyz/org-rw:feat/small-improvements into develop 2024-08-18 20:16:17 +00:00
2 changed files with 96 additions and 31 deletions
Showing only changes of commit 75055f5e08 - Show all commits

View File

@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Dict, Optional, TextIO
from datetime import timedelta from datetime import timedelta
import collections import collections
import difflib import difflib
@ -23,7 +23,9 @@ DEFAULT_DONE_KEYWORDS = ["DONE"]
BASE_ENVIRONMENT = { BASE_ENVIRONMENT = {
"org-footnote-section": "Footnotes", "org-footnote-section": "Footnotes",
"org-todo-keywords": ' '.join(DEFAULT_TODO_KEYWORDS) + ' | ' + ' '.join(DEFAULT_DONE_KEYWORDS), "org-todo-keywords": " ".join(DEFAULT_TODO_KEYWORDS)
+ " | "
+ " ".join(DEFAULT_DONE_KEYWORDS),
"org-options-keywords": ( "org-options-keywords": (
"ARCHIVE:", "ARCHIVE:",
"AUTHOR:", "AUTHOR:",
@ -115,11 +117,13 @@ NON_FINISHED_GROUPS = (
) )
FREE_GROUPS = (dom.CodeBlock,) FREE_GROUPS = (dom.CodeBlock,)
# States # States
class HeadlineState(TypedDict): class HeadlineState(TypedDict):
# To be extended to handle keyboard shortcuts # To be extended to handle keyboard shortcuts
name: str name: str
class OrgDocDeclaredStates(TypedDict): class OrgDocDeclaredStates(TypedDict):
not_completed: List[HeadlineState] not_completed: List[HeadlineState]
completed: List[HeadlineState] completed: List[HeadlineState]
@ -1094,7 +1098,9 @@ class Timestamp:
datetime: The corresponding datetime object. datetime: The corresponding datetime object.
""" """
if self.hour is not None: if self.hour is not None:
return datetime(self.year, self.month, self.day, self.hour, self.minute or 0) return datetime(
self.year, self.month, self.day, self.hour, self.minute or 0
)
else: else:
return datetime(self.year, self.month, self.day, 0, 0) return datetime(self.year, self.month, self.day, 0, 0)
@ -1493,7 +1499,6 @@ class OrgTime:
""" """
return self.time.active return self.time.active
@active.setter @active.setter
def active(self, value: bool) -> None: def active(self, value: bool) -> None:
""" """
@ -1668,7 +1673,7 @@ class Text:
def __repr__(self): def __repr__(self):
return "{{Text line: {}; content: {} }}".format(self.linenum, self.contents) return "{{Text line: {}; content: {} }}".format(self.linenum, self.contents)
def get_text(self): def get_text(self) -> str:
return token_list_to_plaintext(self.contents) return token_list_to_plaintext(self.contents)
def get_raw(self): def get_raw(self):
@ -1991,7 +1996,7 @@ def parse_contents(raw_contents: List[RawLine]):
return [parse_content_block(block) for block in blocks] return [parse_content_block(block) for block in blocks]
def parse_content_block(raw_contents: Union[List[RawLine], str]): def parse_content_block(raw_contents: Union[List[RawLine], str]) -> Text:
contents_buff = [] contents_buff = []
if isinstance(raw_contents, str): if isinstance(raw_contents, str):
contents_buff.append(raw_contents) contents_buff.append(raw_contents)
@ -2077,16 +2082,16 @@ def parse_headline(hl, doc, parent) -> Headline:
title = line title = line
is_done = is_todo = False is_done = is_todo = False
for state in doc.todo_keywords or []: for state in doc.todo_keywords or []:
if title.startswith(state['name'] + " "): if title.startswith(state["name"] + " "):
hl_state = state hl_state = state
title = title[len(state['name'] + " ") :] title = title[len(state["name"] + " ") :]
is_todo = True is_todo = True
break break
else: else:
for state in doc.done_keywords or []: for state in doc.done_keywords or []:
if title.startswith(state['name'] + " "): if title.startswith(state["name"] + " "):
hl_state = state hl_state = state
title = title[len(state['name'] + " ") :] title = title[len(state["name"] + " ") :]
is_done = True is_done = True
break break
@ -2185,7 +2190,7 @@ def dump_delimiters(line: DelimiterLine):
def parse_todo_done_keywords(line: str) -> OrgDocDeclaredStates: def parse_todo_done_keywords(line: str) -> OrgDocDeclaredStates:
clean_line = re.sub(r"\([^)]+\)", "", line) clean_line = re.sub(r"\([^)]+\)", "", line)
if '|' in clean_line: if "|" in clean_line:
todo_kws, done_kws = clean_line.split("|", 1) todo_kws, done_kws = clean_line.split("|", 1)
has_split = True has_split = True
else: else:
@ -2200,20 +2205,20 @@ def parse_todo_done_keywords(line: str) -> OrgDocDeclaredStates:
todo_keywords = todo_keywords[:-1] todo_keywords = todo_keywords[:-1]
return { return {
"not_completed": [ "not_completed": [HeadlineState(name=keyword) for keyword in todo_keywords],
HeadlineState(name=keyword) "completed": [HeadlineState(name=keyword) for keyword in done_keywords],
for keyword in todo_keywords
],
"completed": [
HeadlineState(name=keyword)
for keyword in done_keywords
],
} }
class OrgDoc: class OrgDoc:
def __init__( def __init__(
self, headlines, keywords, contents, list_items, structural, properties, self,
headlines,
keywords,
contents,
list_items,
structural,
properties,
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]
@ -2223,13 +2228,19 @@ class OrgDoc:
for keyword in keywords: for keyword in keywords:
if keyword.key in ("TODO", "SEQ_TODO"): if keyword.key in ("TODO", "SEQ_TODO"):
states = parse_todo_done_keywords(keyword.value) states = parse_todo_done_keywords(keyword.value)
self.todo_keywords, self.done_keywords = states['not_completed'], states['completed'] self.todo_keywords, self.done_keywords = (
states["not_completed"],
states["completed"],
)
keywords_set_in_file = True keywords_set_in_file = True
if not keywords_set_in_file and 'org-todo-keywords' in environment: if not keywords_set_in_file and "org-todo-keywords" in environment:
# Read keywords from environment # Read keywords from environment
states = parse_todo_done_keywords(environment['org-todo-keywords']) states = parse_todo_done_keywords(environment["org-todo-keywords"])
self.todo_keywords, self.done_keywords = states['not_completed'], states['completed'] self.todo_keywords, self.done_keywords = (
states["not_completed"],
states["completed"],
)
self.keywords: List[Property] = keywords self.keywords: List[Property] = keywords
self.contents: List[RawLine] = contents self.contents: List[RawLine] = contents
@ -2304,7 +2315,7 @@ class OrgDoc:
state = "" state = ""
if headline.state: if headline.state:
state = headline.state['name'] + " " state = headline.state["name"] + " "
raw_title = token_list_to_raw(headline.title.contents) raw_title = token_list_to_raw(headline.title.contents)
tags_padding = "" tags_padding = ""
@ -2418,7 +2429,7 @@ class OrgDocReader:
self.current_drawer: Optional[List] = None self.current_drawer: Optional[List] = None
self.environment = environment self.environment = environment
def finalize(self): def finalize(self) -> OrgDoc:
return OrgDoc( return OrgDoc(
self.headlines, self.headlines,
self.keywords, self.keywords,
@ -2724,7 +2735,26 @@ class OrgDocReader:
raise raise
def loads(s, environment=BASE_ENVIRONMENT, extra_cautious=True): def loads(
s: str, environment: Optional[Dict] = BASE_ENVIRONMENT, extra_cautious: bool = True
) -> OrgDoc:
"""
Load an Org-mode document from a string.
Args:
s (str): The string representation of the Org-mode document.
environment (Optional[dict]): The environment for parsing. Defaults to
`BASE_ENVIRONMENT`.
extra_cautious (bool): If True, perform an extra check to ensure that
the document can be re-serialized to the original string. Defaults to True.
Returns:
OrgDoc: The loaded Org-mode document.
Raises:
NonReproducibleDocument: If `extra_cautious` is True and there is a
difference between the original string and the re-serialized document.
"""
reader = OrgDocReader(environment) reader = OrgDocReader(environment)
reader.read(s) reader.read(s)
doc = reader.finalize() doc = reader.finalize()
@ -2764,20 +2794,55 @@ def loads(s, environment=BASE_ENVIRONMENT, extra_cautious=True):
return doc return doc
def load(f, environment=BASE_ENVIRONMENT, extra_cautious=False): def load(
f: TextIO,
environment: Optional[dict] = BASE_ENVIRONMENT,
extra_cautious: bool = False,
) -> OrgDoc:
"""
Load an Org-mode document from a file object.
Args:
f (TextIO): The file object containing the Org-mode document.
environment (Optional[dict]): The environment for parsing. Defaults to
`BASE_ENVIRONMENT`.
extra_cautious (bool): If True, perform an extra check to ensure that
the document can be re-serialized to the original string. Defaults to False.
Returns:
OrgDoc: The loaded Org-mode document.
"""
doc = loads(f.read(), environment, extra_cautious) doc = loads(f.read(), environment, extra_cautious)
doc._path = os.path.abspath(f.name) doc._path = os.path.abspath(f.name)
return doc return doc
def dumps(doc): def dumps(doc: OrgDoc) -> str:
"""
Serialize an OrgDoc object to a string.
Args:
doc (OrgDoc): The OrgDoc object to serialize.
Returns:
str: The serialized string representation of the OrgDoc object.
"""
dump = list(doc.dump()) dump = list(doc.dump())
result = "\n".join(dump) result = "\n".join(dump)
# print(result)
return result return result
def dump(doc, fp): def dump(doc: OrgDoc, fp: TextIO) -> None:
"""
Serialize an OrgDoc object to a file.
Args:
doc (OrgDoc): The OrgDoc object to serialize.
fp (TextIO): The file-like object to write the serialized data to.
Returns:
None
"""
it = doc.dump() it = doc.dump()
# Write first line separately # Write first line separately

0
org_rw/py.typed Normal file
View File