Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Dima Gerasimov
0e0ab5fe24 try using Protocol for my.whatsapp.android 2024-08-22 01:44:32 +01:00
Dima Gerasimov
675bb66e52 some notes on rethinking mixning in the user config 2024-08-19 01:08:10 +01:00
7 changed files with 204 additions and 5 deletions

12
doc/experiments_with_config/run Executable file
View 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"

View 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'

View 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=}')

View 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]

View 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]

View file

@ -7,9 +7,9 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
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.error import echain, notnone
from my.core.sqlite import sqlite_connection
@ -19,17 +19,23 @@ import my.config
logger = make_logger(__name__)
@dataclass
class Config(my.config.whatsapp.android):
class Config(Protocol):
# paths[s]/glob to the exported sqlite databases
export_path: Paths
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]:
config = make_config()
return get_files(config.export_path)
@ -62,6 +68,8 @@ Entity = Union[Chat, Sender, Message]
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
config = make_config()
chats = {}
for r in db.execute(
'''