polar: minor improvements, konsume: more type annotations

This commit is contained in:
Dima Gerasimov 2020-05-15 08:40:12 +01:00
parent f3d5064ff2
commit 0f27071dcc
4 changed files with 50 additions and 40 deletions

View file

@ -134,7 +134,8 @@ def get_files(pp: Paths, glob: str=DEFAULT_GLOB, sort: bool=True) -> Tuple[Path,
warnings.warn(f"Treating {ss} as glob path. Explicit glob={glob} argument is ignored!") warnings.warn(f"Treating {ss} as glob path. Explicit glob={glob} argument is ignored!")
paths.extend(map(Path, do_glob(ss))) paths.extend(map(Path, do_glob(ss)))
else: else:
assert src.is_file(), src if not src.is_file():
raise RuntimeError(f"Expected '{src}' to exist")
# todo assert matches glob?? # todo assert matches glob??
paths.append(src) paths.append(src)

View file

@ -11,7 +11,7 @@ def zoom(w, *keys):
# TODO need to support lists # TODO need to support lists
class Zoomable: class Zoomable:
def __init__(self, parent, *args, **kwargs): def __init__(self, parent, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) # type: ignore super().__init__(*args, **kwargs) # type: ignore
self.parent = parent self.parent = parent
@ -21,19 +21,19 @@ class Zoomable:
def dependants(self): def dependants(self):
raise NotImplementedError raise NotImplementedError
def ignore(self): def ignore(self) -> None:
self.consume_all() self.consume_all()
def consume_all(self): def consume_all(self) -> None:
for d in self.dependants: for d in self.dependants:
d.consume_all() d.consume_all()
self.consume() self.consume()
def consume(self): def consume(self) -> None:
assert self.parent is not None assert self.parent is not None
self.parent._remove(self) self.parent._remove(self)
def zoom(self): def zoom(self) -> 'Zoomable':
self.consume() self.consume()
return self return self
@ -56,6 +56,8 @@ class Wdict(Zoomable, OrderedDict):
def this_consumed(self): def this_consumed(self):
return len(self) == 0 return len(self) == 0
# TODO specify mypy type for the index special method?
class Wlist(Zoomable, list): class Wlist(Zoomable, list):
def _remove(self, xx): def _remove(self, xx):
@ -83,7 +85,8 @@ class Wvalue(Zoomable):
def __repr__(self): def __repr__(self):
return 'WValue{' + repr(self.value) + '}' return 'WValue{' + repr(self.value) + '}'
def _wrap(j, parent=None): from typing import Tuple
def _wrap(j, parent=None) -> Tuple[Zoomable, List[Zoomable]]:
res: Zoomable res: Zoomable
cc: List[Zoomable] cc: List[Zoomable]
if isinstance(j, dict): if isinstance(j, dict):
@ -109,13 +112,14 @@ def _wrap(j, parent=None):
raise RuntimeError(f'Unexpected type: {type(j)} {j}') raise RuntimeError(f'Unexpected type: {type(j)} {j}')
from contextlib import contextmanager from contextlib import contextmanager
from typing import Iterator
class UnconsumedError(Exception): class UnconsumedError(Exception):
pass pass
# TODO think about error policy later... # TODO think about error policy later...
@contextmanager @contextmanager
def wrap(j, throw=True): def wrap(j, throw=True) -> Iterator[Zoomable]:
w, children = _wrap(j) w, children = _wrap(j)
yield w yield w
@ -128,28 +132,33 @@ def wrap(j, throw=True):
# TODO log? # TODO log?
pass pass
from typing import cast
def test_unconsumed(): def test_unconsumed():
import pytest # type: ignore import pytest # type: ignore
with pytest.raises(UnconsumedError): with pytest.raises(UnconsumedError):
with wrap({'a': 1234}) as w: with wrap({'a': 1234}) as w:
w = cast(Wdict, w)
pass pass
with pytest.raises(UnconsumedError): with pytest.raises(UnconsumedError):
with wrap({'c': {'d': 2222}}) as w: with wrap({'c': {'d': 2222}}) as w:
w = cast(Wdict, w)
d = w['c']['d'].zoom() d = w['c']['d'].zoom()
def test_consumed(): def test_consumed():
with wrap({'a': 1234}) as w: with wrap({'a': 1234}) as w:
w = cast(Wdict, w)
a = w['a'].zoom() a = w['a'].zoom()
with wrap({'c': {'d': 2222}}) as w: with wrap({'c': {'d': 2222}}) as w:
w = cast(Wdict, w)
c = w['c'].zoom() c = w['c'].zoom()
d = c['d'].zoom() d = c['d'].zoom()
def test_types(): def test_types():
# (string, number, object, array, boolean or nul # (string, number, object, array, boolean or nul
with wrap({'string': 'string', 'number': 3.14, 'boolean': True, 'null': None, 'list': [1, 2, 3]}) as w: with wrap({'string': 'string', 'number': 3.14, 'boolean': True, 'null': None, 'list': [1, 2, 3]}) as w:
w = cast(Wdict, w)
w['string'].zoom() w['string'].zoom()
w['number'].consume() w['number'].consume()
w['boolean'].zoom() w['boolean'].zoom()
@ -159,5 +168,8 @@ def test_types():
def test_consume_all(): def test_consume_all():
with wrap({'aaa': {'bbb': {'hi': 123}}}) as w: with wrap({'aaa': {'bbb': {'hi': 123}}}) as w:
w = cast(Wdict, w)
aaa = w['aaa'].zoom() aaa = w['aaa'].zoom()
aaa['bbb'].consume_all() aaa['bbb'].consume_all()
# TODO type check this...

View file

@ -35,27 +35,20 @@ config = make_config(polar)
# https://github.com/burtonator/polar-bookshelf/issues/296 # https://github.com/burtonator/polar-bookshelf/issues/296
from datetime import datetime from datetime import datetime
from typing import List, Dict, Iterator, NamedTuple, Sequence, Optional from typing import List, Dict, Iterable, NamedTuple, Sequence, Optional
import json import json
import pytz import pytz
from ..core import get_files, LazyLogger from ..core import LazyLogger, Json
from ..core.common import isoparse
from ..error import Res, echain, unwrap, sort_res_by from ..error import Res, echain, sort_res_by
from ..kython.konsume import wrap, zoom, ignore from ..kython.konsume import wrap, zoom, ignore, Zoomable, Wdict
logger = LazyLogger(__name__) logger = LazyLogger(__name__)
# TODO use core.isoparse
def parse_dt(s: str) -> datetime:
return pytz.utc.localize(datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ'))
Uid = str
# Ok I guess handling comment-level errors is a bit too much.. # Ok I guess handling comment-level errors is a bit too much..
Cid = str Cid = str
class Comment(NamedTuple): class Comment(NamedTuple):
@ -71,6 +64,8 @@ class Highlight(NamedTuple):
comments: Sequence[Comment] comments: Sequence[Comment]
Uid = str
class Book(NamedTuple): class Book(NamedTuple):
uid: Uid uid: Uid
created: datetime created: datetime
@ -80,8 +75,6 @@ class Book(NamedTuple):
# think about it later. # think about it later.
items: Sequence[Highlight] items: Sequence[Highlight]
Error = Exception # for backwards compat with Orger; can remove later
Result = Res[Book] Result = Res[Book]
class Loader: class Loader:
@ -89,12 +82,13 @@ class Loader:
self.path = p self.path = p
self.uid = self.path.parent.name self.uid = self.path.parent.name
def error(self, cause, extra='') -> Exception: def error(self, cause: Exception, extra: str ='') -> Exception:
if len(extra) > 0: if len(extra) > 0:
extra = '\n' + extra extra = '\n' + extra
return echain(Exception(f'while processing {self.path}{extra}'), cause) return echain(Exception(f'while processing {self.path}{extra}'), cause)
def load_item(self, meta) -> Iterator[Highlight]: def load_item(self, meta: Zoomable) -> Iterable[Highlight]:
meta = cast(Wdict, meta)
# TODO this should be destructive zoom? # TODO this should be destructive zoom?
meta['notes'].zoom() meta['notes'].zoom()
meta['pagemarks'].zoom() meta['pagemarks'].zoom()
@ -134,7 +128,7 @@ class Loader:
cmap[hlid] = ccs cmap[hlid] = ccs
ccs.append(Comment( ccs.append(Comment(
cid=cid.value, cid=cid.value,
created=parse_dt(crt.value), created=isoparse(crt.value),
text=html.value, # TODO perhaps coonvert from html to text or org? text=html.value, # TODO perhaps coonvert from html to text or org?
)) ))
v.consume() v.consume()
@ -162,7 +156,7 @@ class Loader:
yield Highlight( yield Highlight(
hid=hid, hid=hid,
created=parse_dt(crt), created=isoparse(crt),
selection=text, selection=text,
comments=tuple(comments), comments=tuple(comments),
) )
@ -174,12 +168,12 @@ class Loader:
# TODO sort by date? # TODO sort by date?
def load_items(self, metas) -> Iterator[Highlight]: def load_items(self, metas: Json) -> Iterable[Highlight]:
for p, meta in metas.items(): for p, meta in metas.items():
with wrap(meta, throw=False) as meta: with wrap(meta, throw=False) as meta:
yield from self.load_item(meta) yield from self.load_item(meta)
def load(self) -> Iterator[Result]: def load(self) -> Iterable[Result]:
logger.info('processing %s', self.path) logger.info('processing %s', self.path)
j = json.loads(self.path.read_text()) j = json.loads(self.path.read_text())
@ -193,14 +187,15 @@ class Loader:
yield Book( yield Book(
uid=self.uid, uid=self.uid,
created=parse_dt(added), created=isoparse(added),
filename=filename, filename=filename,
title=title, title=title,
items=list(self.load_items(pm)), items=list(self.load_items(pm)),
) )
def iter_entries() -> Iterator[Result]: def iter_entries() -> Iterable[Result]:
from ..core import get_files
for d in get_files(config.polar_dir, glob='*/state.json'): for d in get_files(config.polar_dir, glob='*/state.json'):
loader = Loader(d) loader = Loader(d)
try: try:
@ -213,16 +208,18 @@ def iter_entries() -> Iterator[Result]:
def get_entries() -> List[Result]: def get_entries() -> List[Result]:
# sorting by first annotation is reasonable I guess??? # sorting by first annotation is reasonable I guess???
# todo perhaps worth making it a pattern? X() returns iterable, get_X returns reasonably sorted list?
return list(sort_res_by(iter_entries(), key=lambda e: e.created)) return list(sort_res_by(iter_entries(), key=lambda e: e.created))
def main(): def main():
for entry in iter_entries(): for e in iter_entries():
try: if isinstance(e, Exception):
ee = unwrap(entry)
except Error as e:
logger.exception(e) logger.exception(e)
else: else:
logger.info('processed %s', ee.uid) logger.info('processed %s', e.uid)
for i in ee.items: for i in e.items:
logger.info(i) logger.info(i)
Error = Exception # for backwards compat with Orger; can remove later

View file

@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
ROOT = Path(__file__).parent.parent.absolute() ROOT = Path(__file__).parent.absolute()
import pytest # type: ignore import pytest # type: ignore
@ -17,7 +17,7 @@ def test_hpi(dotpolar: str):
if dotpolar != '': if dotpolar != '':
pdir = Path(ROOT / dotpolar) pdir = Path(ROOT / dotpolar)
class user_config: class user_config:
export_dir = pdir polar_dir = pdir
import my.config import my.config
setattr(my.config, 'polar', user_config) setattr(my.config, 'polar', user_config)
@ -30,4 +30,4 @@ def test_hpi(dotpolar: str):
import my.reading.polar as polar import my.reading.polar as polar
from my.reading.polar import get_entries from my.reading.polar import get_entries
assert len(list(get_entries())) > 10 assert len(list(get_entries())) > 1