another attempt to make the configs more self-documenting: via NamedTuple
This commit is contained in:
parent
4b8c2d4be4
commit
c877104b90
2 changed files with 66 additions and 47 deletions
100
my/reddit.py
100
my/reddit.py
|
@ -1,61 +1,55 @@
|
||||||
"""
|
"""
|
||||||
Reddit data: saved items/comments/upvotes/etc.
|
Reddit data: saved items/comments/upvotes/etc.
|
||||||
"""
|
"""
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Protocol
|
|
||||||
|
|
||||||
# todo extract this for documentation...
|
from typing import NamedTuple, Optional
|
||||||
class reddit(Protocol):
|
from .core.common import PathIsh
|
||||||
'''
|
|
||||||
Reddit module uses [[rexport][https://github.com/karlicoss/rexport]] output
|
|
||||||
'''
|
|
||||||
|
|
||||||
export_path: PathIsh # path to the exported data
|
class reddit(NamedTuple):
|
||||||
rexport : Optional[PathIsh] # path to a local clone of rexport
|
'''
|
||||||
|
Reddit module uses [[rexport][https://github.com/karlicoss/rexport]] output
|
||||||
# TODO hmm, I need something like an overlay/delegate, which:
|
'''
|
||||||
# - checks for required attributes (configurable?)
|
export_path: PathIsh # path to the exported data
|
||||||
# - fills optional
|
rexport : Optional[PathIsh] = None # path to a local clone of rexport
|
||||||
# - doesn't modify the config user has passed otherwise
|
|
||||||
# supports existing python code, ideally uses inheritance
|
|
||||||
#
|
|
||||||
# I really want loose coupling, so the config wouldn't have to import anything
|
|
||||||
# this looks promising, but it uses toml/yaml I think.
|
|
||||||
# https://github.com/karlicoss/HPI/issues/12#issuecomment-610038961
|
|
||||||
# maybe just use dataclasses or something?
|
|
||||||
|
|
||||||
cfg = reddit
|
|
||||||
else:
|
|
||||||
from my.config import reddit as cfg
|
|
||||||
|
|
||||||
###
|
###
|
||||||
|
# hmm, I need something like an overlay/delegate, which:
|
||||||
|
# - checks for required attributes (configurable?)
|
||||||
|
# - fills optional
|
||||||
|
# - doesn't modify the config user has passed otherwise
|
||||||
|
# supports existing python code, ideally uses inheritance
|
||||||
|
#
|
||||||
|
# I really want loose coupling, so the config wouldn't have to import anything
|
||||||
|
# this looks promising, but it uses toml/yaml I think.
|
||||||
|
# https://github.com/karlicoss/HPI/issues/12#issuecomment-610038961
|
||||||
|
# so far seems like a tweaked namedtuple suits well for it?
|
||||||
|
# need to test though
|
||||||
|
###
|
||||||
|
cfg = reddit
|
||||||
|
from my.config import reddit as uconfig
|
||||||
|
|
||||||
# TODO hmm, optional attribute and Optional type are quite different...
|
|
||||||
|
|
||||||
from typing import Optional, cast
|
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from .core.common import classproperty, PathIsh
|
|
||||||
|
|
||||||
# todo would be nice to inherit from cfg to get defaults.. but mypy says it's incompatible -- because of classproperty??
|
# TODO can we make this generic?
|
||||||
class config(cfg):
|
class Config(cfg, uconfig):
|
||||||
if not TYPE_CHECKING: # TODO ugh. interferes with typing? not ideal as easy to miss.
|
def __new__(cls) -> 'Config':
|
||||||
if 'rexport' not in vars(cfg):
|
from typing import Dict, Any
|
||||||
rexport = None
|
props: Dict[str, Any] = {k: v for k, v in vars(uconfig).items()}
|
||||||
|
|
||||||
# experimenting on
|
if 'export_dir' in props:
|
||||||
if 'export_path' not in vars(cfg):
|
# legacy name
|
||||||
@classproperty
|
props['export_path'] = props['export_dir']
|
||||||
def export_path(self) -> PathIsh: # type: ignore[override]
|
|
||||||
legacy_path: Optional[PathIsh] = getattr(cfg, 'export_dir', None)
|
|
||||||
assert legacy_path is not None # todo warn?
|
|
||||||
return legacy_path
|
|
||||||
|
|
||||||
@classproperty
|
fields = cfg._fields
|
||||||
def rexport_module(cls) -> ModuleType:
|
props = {k: v for k, v in props.items() if k in fields}
|
||||||
|
inst = super(Config, cls).__new__(cls, **props)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rexport_module(self) -> ModuleType:
|
||||||
# todo return Type[rexport]??
|
# todo return Type[rexport]??
|
||||||
# todo ModuleIsh?
|
# todo ModuleIsh?
|
||||||
rpath = cls.rexport
|
rpath = self.rexport
|
||||||
if rpath is not None:
|
if rpath is not None:
|
||||||
from my.cfg import set_repo
|
from my.cfg import set_repo
|
||||||
set_repo('rexport', rpath)
|
set_repo('rexport', rpath)
|
||||||
|
@ -63,15 +57,29 @@ class config(cfg):
|
||||||
import my.config.repos.rexport.dal as m
|
import my.config.repos.rexport.dal as m
|
||||||
return m
|
return m
|
||||||
|
|
||||||
# TODO maybe make the whole thing lazy?
|
# ok, so this suits me:
|
||||||
|
# - checks for required attributes (thanks, NamedTuple)
|
||||||
|
# - fills optional (thanks, NamedTuple)
|
||||||
|
# - passes the rest through (thanks, multiple inheritance)
|
||||||
|
# - allows adding extensions/accessors
|
||||||
|
# - we can still use from my.reddit import reddit as config in the simplest scenario?
|
||||||
|
# the only downside is the laziness?
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# TODO not sure about the laziness...
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# TODO not sure what is the right way to handle this..
|
# TODO not sure what is the right way to handle this..
|
||||||
import my.config.repos.rexport.dal as rexport
|
import my.config.repos.rexport.dal as rexport
|
||||||
else:
|
else:
|
||||||
# TODO ugh. this would import too early
|
# TODO ugh. this would import too early
|
||||||
|
# but on the other hand we do want to bring the objects into the scope for easier imports, etc. ugh!
|
||||||
|
# ok, fair enough I suppose. It makes sense to configure something before using it. can always figure it out later..
|
||||||
|
# maybe, the config could dynamically detect change and reimport itself? dunno.
|
||||||
rexport = config.rexport_module
|
rexport = config.rexport_module
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from my.reddit import events, inputs, saved
|
|
||||||
from my.common import make_dict
|
from my.common import make_dict
|
||||||
|
|
||||||
|
|
||||||
def test() -> None:
|
def test() -> None:
|
||||||
|
from my.reddit import events, inputs, saved
|
||||||
list(events())
|
list(events())
|
||||||
list(saved())
|
list(saved())
|
||||||
|
|
||||||
|
|
||||||
def test_unfav() -> None:
|
def test_unfav() -> None:
|
||||||
|
from my.reddit import events, inputs, saved
|
||||||
ev = events()
|
ev = events()
|
||||||
url = 'https://reddit.com/r/QuantifiedSelf/comments/acxy1v/personal_dashboard/'
|
url = 'https://reddit.com/r/QuantifiedSelf/comments/acxy1v/personal_dashboard/'
|
||||||
uev = [e for e in ev if e.url == url]
|
uev = [e for e in ev if e.url == url]
|
||||||
|
@ -23,6 +24,7 @@ def test_unfav() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_saves() -> None:
|
def test_saves() -> None:
|
||||||
|
from my.reddit import events, inputs, saved
|
||||||
# TODO not sure if this is necesasry anymore?
|
# TODO not sure if this is necesasry anymore?
|
||||||
saves = list(saved())
|
saves = list(saved())
|
||||||
# just check that they are unique..
|
# just check that they are unique..
|
||||||
|
@ -30,6 +32,7 @@ def test_saves() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_disappearing() -> None:
|
def test_disappearing() -> None:
|
||||||
|
from my.reddit import events, inputs, saved
|
||||||
# eh. so for instance, 'metro line colors' is missing from reddit-20190402005024.json for no reason
|
# eh. so for instance, 'metro line colors' is missing from reddit-20190402005024.json for no reason
|
||||||
# but I guess it was just a short glitch... so whatever
|
# but I guess it was just a short glitch... so whatever
|
||||||
saves = events()
|
saves = events()
|
||||||
|
@ -39,12 +42,18 @@ def test_disappearing() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_unfavorite() -> None:
|
def test_unfavorite() -> None:
|
||||||
|
from my.reddit import events, inputs, saved
|
||||||
evs = events()
|
evs = events()
|
||||||
unfavs = [s for s in evs if s.text == 'unfavorited']
|
unfavs = [s for s in evs if s.text == 'unfavorited']
|
||||||
[xxx] = [u for u in unfavs if u.eid == 'unf-19ifop']
|
[xxx] = [u for u in unfavs if u.eid == 'unf-19ifop']
|
||||||
assert xxx.dt == datetime(2019, 1, 28, 8, 10, 20, tzinfo=pytz.utc)
|
assert xxx.dt == datetime(2019, 1, 28, 8, 10, 20, tzinfo=pytz.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extra_attr() -> None:
|
||||||
|
from my.reddit import config
|
||||||
|
assert isinstance(getattr(config, 'passthrough'), str)
|
||||||
|
|
||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
@pytest.fixture(autouse=True, scope='module')
|
@pytest.fixture(autouse=True, scope='module')
|
||||||
def prepare():
|
def prepare():
|
||||||
|
@ -55,3 +64,5 @@ def prepare():
|
||||||
# first bit is for 'test_unfavorite, the second is for test_disappearing
|
# first bit is for 'test_unfavorite, the second is for test_disappearing
|
||||||
files = files[300:330] + files[500:520]
|
files = files[300:330] + files[500:520]
|
||||||
config.export_dir = files # type: ignore
|
config.export_dir = files # type: ignore
|
||||||
|
|
||||||
|
setattr(config, 'passthrough', "isn't handled, but available dynamically nevertheless")
|
||||||
|
|
Loading…
Add table
Reference in a new issue