add my.demo for testing out various approaches to configuring
This commit is contained in:
parent
d6f071e3b1
commit
0ac78143f2
4 changed files with 107 additions and 4 deletions
|
@ -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)
|
||||
- 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=
|
||||
|
||||
[[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)
|
||||
|
||||
config = reddit.make_config()
|
||||
config = bluemaestro.make_config()
|
||||
#+end_src
|
||||
|
||||
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~
|
||||
- *(3)*: supports default attributes, at no extra cost
|
||||
- *(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
|
||||
|
||||
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.
|
||||
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
56
my/demo.py
Normal 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
29
tests/demo.py
Normal 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
|
2
tox.ini
2
tox.ini
|
@ -13,7 +13,7 @@ commands =
|
|||
# todo these are probably not necessary anymore?
|
||||
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 -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 run demo.py? just make sure with_my is a bit cleverer?
|
||||
# TODO e.g. under CI, rely on installing
|
||||
|
|
Loading…
Add table
Reference in a new issue