some notes on rethinking mixning in the user config

This commit is contained in:
Dima Gerasimov 2024-08-19 01:08:10 +01:00
parent 5ec357915b
commit 675bb66e52
7 changed files with 211 additions and 6 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]