add my.demo for testing out various approaches to configuring

This commit is contained in:
Dima Gerasimov 2020-05-10 21:32:48 +01:00
parent d6f071e3b1
commit 0ac78143f2
4 changed files with 107 additions and 4 deletions

View file

@ -187,6 +187,8 @@ I see mypy annotations as the only sane way to support it, because we also get (
- it doesn't support optional attributes (optional as in non-required, not as ~typing.Optional~), so it goes against (3) - it doesn't support optional attributes (optional as in non-required, not as ~typing.Optional~), so it goes against (3)
- prior to python 3.8, it's a part of =typing_extensions= rather than standard =typing=, so using it requires guarding the code with =if typing.TYPE_CHECKING=, which is a bit confusing and bloating. - prior to python 3.8, it's a part of =typing_extensions= rather than standard =typing=, so using it requires guarding the code with =if typing.TYPE_CHECKING=, which is a bit confusing and bloating.
TODO: check out [[https://mypy.readthedocs.io/en/stable/protocols.html#using-isinstance-with-protocols][@runtime_checkable]]?
- =NamedTuple= - =NamedTuple=
[[https://github.com/karlicoss/HPI/pull/45/commits/c877104b90c9d168eaec96e0e770e59048ce4465][Here]] I experimented with using ~NamedTuple~. [[https://github.com/karlicoss/HPI/pull/45/commits/c877104b90c9d168eaec96e0e770e59048ce4465][Here]] I experimented with using ~NamedTuple~.
@ -232,7 +234,7 @@ class bluemaestro(user_config):
} }
return cls(**params) return cls(**params)
config = reddit.make_config() config = bluemaestro.make_config()
#+end_src #+end_src
I claim this solves pretty much everything: I claim this solves pretty much everything:
@ -240,11 +242,27 @@ I claim this solves pretty much everything:
- *(2)*: collaterally, we also solved it, because we can adapt for renames and other legacy config adaptations in ~make_config~ - *(2)*: collaterally, we also solved it, because we can adapt for renames and other legacy config adaptations in ~make_config~
- *(3)*: supports default attributes, at no extra cost - *(3)*: supports default attributes, at no extra cost
- *(4)*: the user config's attributes are available through the base class - *(4)*: the user config's attributes are available through the base class
- *(5)*: everything is transparent to mypy. However, it still lacks runtime checks. - *(5)*: everything is mostly transparent to mypy. There are no runtime type checks yet, but I think possible to integrate with ~@dataclass~
- *(6)*: the dataclass header is easily readable, and it's possible to generate the docs automatically - *(6)*: the dataclass header is easily readable, and it's possible to generate the docs automatically
Downsides: Downsides:
- the =make_config= bit is a little scary and manual, however, it can be extracted in a generic helper method - inheriting from ~user_config~ means early import of =my.config=
Generally it's better to keep everything as lazy as possible and defer loading to the first time the config is used.
This might be annoying at times, e.g. if you have a top-level import of you module, but no config.
But considering that in 99% of cases config is going to be on the disk
and it's possible to do something dynamic like =del sys.modules['my.bluemastro']= to reload the config, I think it's a minor issue.
# TODO demonstrate in a test?
- =make_config= allows for some mypy false negatives in the user config
E.g. if you forgot =export_path= attribute, mypy would miss it. But you'd have a runtime failure, and the downstream code using config is still correctly type checked.
Perhaps it will be better when [[https://github.com/python/mypy/issues/5374][this]] is fixed.
- the =make_config= bit is a little scary and manual
However, it's extracted in a generic helper, and [[https://github.com/karlicoss/HPI/blob/d6f071e3b12ba1cd5a86ad80e3821bec004e6a6d/my/twitter/archive.py#L17][ends up pretty simple]]
My conclusion is that I'm going with this approach for now. My conclusion is that I'm going with this approach for now.
Note that at no stage in required any changes to the user configs, so if I missed something, it would be reversible. Note that at no stage in required any changes to the user configs, so if I missed something, it would be reversible.

56
my/demo.py Normal file
View file

@ -0,0 +1,56 @@
'''
Just a demo module for testing and documentation purposes
'''
from .core.common import Paths
from datetime import tzinfo
import pytz
from my.config import demo as user_config
from dataclasses import dataclass
@dataclass
class demo(user_config):
data_path: Paths
username: str
timezone: tzinfo = pytz.utc
def config() -> demo:
from .core.cfg import make_config
config = make_config(demo)
return config
from pathlib import Path
from typing import Sequence, Iterable
from datetime import datetime
from .core.common import Json, get_files
@dataclass
class Item:
'''
Some completely arbirary artificial stuff, just for testing
'''
username: str
raw: Json
dt: datetime
def inputs() -> Sequence[Path]:
return get_files(config().data_path)
import json
def items() -> Iterable[Item]:
for f in inputs():
dt = datetime.fromtimestamp(f.stat().st_mtime, tz=config().timezone)
j = json.loads(f.read_text())
for raw in j:
yield Item(
username=config().username,
raw=raw,
dt=dt,
)

29
tests/demo.py Normal file
View file

@ -0,0 +1,29 @@
from pathlib import Path
from more_itertools import ilen
# TODO NOTE: this wouldn't work because of an early my.config.demo import
# from my.demo import items
def test_dynamic_config(tmp_path: Path) -> None:
import my.config
class user_config:
username = 'user'
data_path = f'{tmp_path}/*.json'
my.config.demo = user_config # type: ignore[misc, assignment]
from my.demo import items
[item1, item2] = items()
assert item1.username == 'user'
import pytest # type: ignore
@pytest.fixture(autouse=True)
def prepare(tmp_path: Path):
(tmp_path / 'data.json').write_text('''
[
{"key1": 1},
{"key2": 2}
]
''')
yield

View file

@ -13,7 +13,7 @@ commands =
# todo these are probably not necessary anymore? # todo these are probably not necessary anymore?
python3 -c 'from my.config import stub as config; print(config.key)' python3 -c 'from my.config import stub as config; print(config.key)'
python3 -c 'import my.config; import my.config.repos' # shouldn't fail at least python3 -c 'import my.config; import my.config.repos' # shouldn't fail at least
python3 -m pytest tests/misc.py tests/get_files.py tests/config.py::test_set_repo tests/config.py::test_environment_variable python3 -m pytest tests/misc.py tests/get_files.py tests/config.py::test_set_repo tests/config.py::test_environment_variable tests/demo.py
# TODO add; once I figure out porg depdencency?? tests/config.py # TODO add; once I figure out porg depdencency?? tests/config.py
# TODO run demo.py? just make sure with_my is a bit cleverer? # TODO run demo.py? just make sure with_my is a bit cleverer?
# TODO e.g. under CI, rely on installing # TODO e.g. under CI, rely on installing