Compare commits
2 commits
master
...
wip-config
Author | SHA1 | Date | |
---|---|---|---|
|
0e0ab5fe24 | ||
|
675bb66e52 |
7 changed files with 204 additions and 5 deletions
12
doc/experiments_with_config/run
Executable file
12
doc/experiments_with_config/run
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
cd "$(dirname "0")"
|
||||||
|
|
||||||
|
WHAT="$1"
|
||||||
|
|
||||||
|
export PYTHONPATH=src
|
||||||
|
|
||||||
|
ERROR=0
|
||||||
|
python3 -m mypy -p "pkg.$WHAT" || ERROR=1
|
||||||
|
python3 -c "import pkg.$WHAT as M; M.run()" || ERROR=1
|
||||||
|
exit "$ERROR"
|
74
doc/experiments_with_config/src/pkg/config.py
Normal file
74
doc/experiments_with_config/src/pkg/config.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
# 'bare' config, no typing annotations even
|
||||||
|
# current_impl : works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy DOES NOT fail (bad!)
|
||||||
|
# via_dataclass : FAILS both mypy and runtime
|
||||||
|
# via_properties: works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy fails (good!)
|
||||||
|
# src/pkg/via_properties.py:32:12:32:28: error: Cannot instantiate abstract class "combined_config" with abstract attribute "export_path" [abstract]
|
||||||
|
# return combined_config()
|
||||||
|
class module_config_1:
|
||||||
|
custom_setting = 'adhoc setting'
|
||||||
|
export_path = '/path/to/data'
|
||||||
|
|
||||||
|
|
||||||
|
# config defined with @dataclass annotation
|
||||||
|
# current_impl : works in runtime
|
||||||
|
# mypy DOES NOT pass
|
||||||
|
# seems like it doesn't like that non-default attributes (export_path: str) in module config
|
||||||
|
# are following default attributes (export_path in this config)
|
||||||
|
# via_dataclass : works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy fails (good!)
|
||||||
|
# src/pkg/via_dataclass.py:56:12:56:28: error: Missing positional argument "export_path" in call to "combined_config" [call-arg]
|
||||||
|
# return combined_config()
|
||||||
|
# via_properties: works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy fails (good!)
|
||||||
|
# same error as above
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class module_config_2:
|
||||||
|
custom_setting: str = 'adhoc setting'
|
||||||
|
export_path: str = '/path/to/data'
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: ok, if a config attrubute happened to be a classproperty, then it fails mypy
|
||||||
|
# but it still works in runtime, which is good, easy to migrate if necessary
|
||||||
|
|
||||||
|
|
||||||
|
# mixed style config, some attributes are defined via property
|
||||||
|
# this is quite useful if you want to defer some computations from config import time
|
||||||
|
# current_impl : works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy DOES NOT fail (bad!)
|
||||||
|
# via_dataclass : FAILS both mypy and runtime
|
||||||
|
# via_properties: works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy fails (good!)
|
||||||
|
# same error as above
|
||||||
|
class module_config_3:
|
||||||
|
custom_setting: str = 'adhoc setting'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def export_path(self) -> str:
|
||||||
|
return '/path/to/data'
|
||||||
|
|
||||||
|
|
||||||
|
# same mixed style as above, but also a @dataclass annotation
|
||||||
|
# via_dataclass: FAILS both mypy and runtime
|
||||||
|
# src/pkg/via_dataclass.py: note: In function "make_config":
|
||||||
|
# src/pkg/via_dataclass.py:53:5:54:12: error: Definition of "export_path" in base class "module_config" is incompatible with definition in base class "config" [misc]
|
||||||
|
# class combined_config(user_config, config):
|
||||||
|
# ^
|
||||||
|
# src/pkg/via_dataclass.py:56:12:56:28: error: Missing positional argument "export_path" in call to "combined_config" [call-arg]
|
||||||
|
# return combined_config()
|
||||||
|
# ^~~~~~~~~~~~~~~~~
|
||||||
|
# via_properties: works both mypy and runtime
|
||||||
|
# if we comment out export_path, mypy fails (good!)
|
||||||
|
# same error as above
|
||||||
|
@dataclass
|
||||||
|
class module_config_4:
|
||||||
|
custom_setting: str = 'adhoc setting'
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def export_path(self) -> str:
|
||||||
|
return '/path/to/data'
|
29
doc/experiments_with_config/src/pkg/current_impl.py
Normal file
29
doc/experiments_with_config/src/pkg/current_impl.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
"""
|
||||||
|
Currently 'preferred' way of defining configs as of 20240818
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from pkg.config import module_config as user_config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class config(user_config):
|
||||||
|
export_path: str
|
||||||
|
|
||||||
|
cache_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
print('hello from', __name__)
|
||||||
|
|
||||||
|
cfg = config
|
||||||
|
|
||||||
|
# check a required attribute
|
||||||
|
print(f'{cfg.export_path=}')
|
||||||
|
|
||||||
|
# check a non-required attribute with default value
|
||||||
|
print(f'{cfg.cache_path=}')
|
||||||
|
|
||||||
|
# check a 'dynamically' defined attribute in user config
|
||||||
|
print(f'{cfg.custom_setting=}')
|
||||||
|
|
0
doc/experiments_with_config/src/pkg/py.typed
Normal file
0
doc/experiments_with_config/src/pkg/py.typed
Normal file
38
doc/experiments_with_config/src/pkg/via_dataclass.py
Normal file
38
doc/experiments_with_config/src/pkg/via_dataclass.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class config:
|
||||||
|
export_path: str
|
||||||
|
cache_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def make_config() -> config:
|
||||||
|
from pkg.config import module_config as user_config
|
||||||
|
|
||||||
|
# NOTE: order is important -- attributes would be added in reverse order
|
||||||
|
# e.g. first from config, then from user_config -- just what we want
|
||||||
|
# NOTE: in theory, this works without @dataclass annotation on combined_config
|
||||||
|
# however, having @dataclass adds extra type checks about missing required attributes
|
||||||
|
# when we instantiate combined_config
|
||||||
|
@dataclass
|
||||||
|
class combined_config(user_config, config): ...
|
||||||
|
|
||||||
|
return combined_config()
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
print('hello from', __name__)
|
||||||
|
|
||||||
|
cfg = make_config()
|
||||||
|
|
||||||
|
# check a required attribute
|
||||||
|
print(f'{cfg.export_path=}')
|
||||||
|
|
||||||
|
# check a non-required attribute with default value
|
||||||
|
print(f'{cfg.cache_path=}')
|
||||||
|
|
||||||
|
# check a 'dynamically' defined attribute in user config
|
||||||
|
# NOTE: mypy fails as it has no static knowledge of the attribute
|
||||||
|
# but kinda expected, not much we can do
|
||||||
|
print(f'{cfg.custom_setting=}') # type: ignore[attr-defined]
|
38
doc/experiments_with_config/src/pkg/via_properties.py
Normal file
38
doc/experiments_with_config/src/pkg/via_properties.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
class config:
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def export_path(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_path(self) -> str | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def make_config() -> config:
|
||||||
|
from pkg.config import module_config as user_config
|
||||||
|
|
||||||
|
# NOTE: order is important -- attributes would be added in reverse order
|
||||||
|
# e.g. first from config, then from user_config -- just what we want
|
||||||
|
class combined_config(user_config, config): ...
|
||||||
|
|
||||||
|
return combined_config()
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
print('hello from', __name__)
|
||||||
|
|
||||||
|
cfg = make_config()
|
||||||
|
|
||||||
|
# check a required attribute
|
||||||
|
print(f'{cfg.export_path=}')
|
||||||
|
|
||||||
|
# check a non-required attribute with default value
|
||||||
|
print(f'{cfg.cache_path=}')
|
||||||
|
|
||||||
|
# check a 'dynamically' defined attribute in user config
|
||||||
|
# NOTE: mypy fails as it has no static knowledge of the attribute
|
||||||
|
# but kinda expected, not much we can do
|
||||||
|
print(f'{cfg.custom_setting=}') # type: ignore[attr-defined]
|
|
@ -7,9 +7,9 @@ from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from typing import Union, Sequence, Iterator, Optional
|
from typing import Union, Sequence, Iterator, Optional, Protocol
|
||||||
|
|
||||||
from my.core import get_files, Paths, datetime_aware, Res, make_logger, make_config
|
from my.core import get_files, Paths, datetime_aware, Res, make_logger
|
||||||
from my.core.common import unique_everseen
|
from my.core.common import unique_everseen
|
||||||
from my.core.error import echain, notnone
|
from my.core.error import echain, notnone
|
||||||
from my.core.sqlite import sqlite_connection
|
from my.core.sqlite import sqlite_connection
|
||||||
|
@ -19,17 +19,23 @@ import my.config
|
||||||
logger = make_logger(__name__)
|
logger = make_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class Config(Protocol):
|
||||||
class Config(my.config.whatsapp.android):
|
|
||||||
# paths[s]/glob to the exported sqlite databases
|
# paths[s]/glob to the exported sqlite databases
|
||||||
export_path: Paths
|
export_path: Paths
|
||||||
|
|
||||||
my_user_id: Optional[str] = None
|
my_user_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
config = make_config(Config)
|
def make_config() -> Config:
|
||||||
|
import my.config as user_config
|
||||||
|
|
||||||
|
class combined_config(user_config.whatsapp.android, Config): ...
|
||||||
|
|
||||||
|
return combined_config()
|
||||||
|
|
||||||
|
|
||||||
def inputs() -> Sequence[Path]:
|
def inputs() -> Sequence[Path]:
|
||||||
|
config = make_config()
|
||||||
return get_files(config.export_path)
|
return get_files(config.export_path)
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +68,8 @@ Entity = Union[Chat, Sender, Message]
|
||||||
def _process_db(db: sqlite3.Connection) -> Iterator[Entity]:
|
def _process_db(db: sqlite3.Connection) -> Iterator[Entity]:
|
||||||
# TODO later, split out Chat/Sender objects separately to safe on object creation, similar to other android data sources
|
# TODO later, split out Chat/Sender objects separately to safe on object creation, similar to other android data sources
|
||||||
|
|
||||||
|
config = make_config()
|
||||||
|
|
||||||
chats = {}
|
chats = {}
|
||||||
for r in db.execute(
|
for r in db.execute(
|
||||||
'''
|
'''
|
||||||
|
|
Loading…
Add table
Reference in a new issue