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)
|
- 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
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?
|
# 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
|
||||||
|
|
Loading…
Add table
Reference in a new issue