From c877104b90c9d168eaec96e0e770e59048ce4465 Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Sat, 9 May 2020 23:17:44 +0100 Subject: [PATCH] another attempt to make the configs more self-documenting: via NamedTuple --- my/reddit.py | 100 ++++++++++++++++++++++++++---------------------- tests/reddit.py | 13 ++++++- 2 files changed, 66 insertions(+), 47 deletions(-) diff --git a/my/reddit.py b/my/reddit.py index 00c678b..a9951e9 100755 --- a/my/reddit.py +++ b/my/reddit.py @@ -1,61 +1,55 @@ """ 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... - class reddit(Protocol): - ''' - Reddit module uses [[rexport][https://github.com/karlicoss/rexport]] output - ''' +from typing import NamedTuple, Optional +from .core.common import PathIsh - export_path: PathIsh # path to the exported data - rexport : Optional[PathIsh] # path to a local clone of rexport - - # TODO 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 - # maybe just use dataclasses or something? - - cfg = reddit -else: - from my.config import reddit as cfg +class reddit(NamedTuple): + ''' + Reddit module uses [[rexport][https://github.com/karlicoss/rexport]] output + ''' + export_path: PathIsh # path to the exported data + rexport : Optional[PathIsh] = None # path to a local clone of rexport ### +# 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 .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?? -class config(cfg): - if not TYPE_CHECKING: # TODO ugh. interferes with typing? not ideal as easy to miss. - if 'rexport' not in vars(cfg): - rexport = None +# TODO can we make this generic? +class Config(cfg, uconfig): + def __new__(cls) -> 'Config': + from typing import Dict, Any + props: Dict[str, Any] = {k: v for k, v in vars(uconfig).items()} - # experimenting on - if 'export_path' not in vars(cfg): - @classproperty - 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 + if 'export_dir' in props: + # legacy name + props['export_path'] = props['export_dir'] - @classproperty - def rexport_module(cls) -> ModuleType: + fields = cfg._fields + 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 ModuleIsh? - rpath = cls.rexport + rpath = self.rexport if rpath is not None: from my.cfg import set_repo set_repo('rexport', rpath) @@ -63,15 +57,29 @@ class config(cfg): import my.config.repos.rexport.dal as 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: # TODO not sure what is the right way to handle this.. import my.config.repos.rexport.dal as rexport else: # 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 - ### diff --git a/tests/reddit.py b/tests/reddit.py index 1068038..4f2c1bd 100644 --- a/tests/reddit.py +++ b/tests/reddit.py @@ -1,16 +1,17 @@ from datetime import datetime import pytz -from my.reddit import events, inputs, saved from my.common import make_dict def test() -> None: + from my.reddit import events, inputs, saved list(events()) list(saved()) def test_unfav() -> None: + from my.reddit import events, inputs, saved ev = events() url = 'https://reddit.com/r/QuantifiedSelf/comments/acxy1v/personal_dashboard/' uev = [e for e in ev if e.url == url] @@ -23,6 +24,7 @@ def test_unfav() -> None: def test_saves() -> None: + from my.reddit import events, inputs, saved # TODO not sure if this is necesasry anymore? saves = list(saved()) # just check that they are unique.. @@ -30,6 +32,7 @@ def test_saves() -> 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 # but I guess it was just a short glitch... so whatever saves = events() @@ -39,12 +42,18 @@ def test_disappearing() -> None: def test_unfavorite() -> None: + from my.reddit import events, inputs, saved evs = events() unfavs = [s for s in evs if s.text == 'unfavorited'] [xxx] = [u for u in unfavs if u.eid == 'unf-19ifop'] 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 @pytest.fixture(autouse=True, scope='module') def prepare(): @@ -55,3 +64,5 @@ def prepare(): # first bit is for 'test_unfavorite, the second is for test_disappearing files = files[300:330] + files[500:520] config.export_dir = files # type: ignore + + setattr(config, 'passthrough', "isn't handled, but available dynamically nevertheless")