Skip to content

Commit f4b1bb8

Browse files
committed
feat: added support for toml config
1 parent b2d9179 commit f4b1bb8

File tree

5 files changed

+186
-24
lines changed

5 files changed

+186
-24
lines changed

aw_core/__about__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
# https://github.com/pypa/pipfile/blob/master/pipfile/__about__.py
33

44
__all__ = [
5-
"__title__", "__summary__", "__uri__", "__version__", "__author__",
6-
"__email__", "__license__", "__copyright__",
5+
"__title__",
6+
"__summary__",
7+
"__uri__",
8+
"__version__",
9+
"__author__",
10+
"__email__",
11+
"__license__",
12+
"__copyright__",
713
]
814

915
__title__ = "aw-core"
1016
__summary__ = "Core library for ActivityWatch"
1117
__uri__ = "https://github.com/ActivityWatch/aw-core"
1218

13-
__version__ = "0.4.1"
19+
__version__ = "0.4.2"
1420

1521
__author__ = "Erik Bjäreholt, Johan Bjäreholt"
1622
__email__ = "erik@bjareho.lt, johan@bjareho.lt"

aw_core/config.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,83 @@
11
import os
22
import logging
3+
from typing import Any, Dict, Union
34
from configparser import ConfigParser
45

6+
from deprecation import deprecated
7+
import tomlkit
8+
59
from aw_core import dirs
10+
from aw_core.__about__ import __version__
611

712
logger = logging.getLogger(__name__)
813

914

15+
def _merge(a: dict, b: dict, path=None):
16+
"""
17+
Recursively merges b into a, with b taking precedence.
18+
19+
From: https://stackoverflow.com/a/7205107/965332
20+
"""
21+
if path is None:
22+
path = []
23+
for key in b:
24+
if key in a:
25+
if isinstance(a[key], dict) and isinstance(b[key], dict):
26+
_merge(a[key], b[key], path + [str(key)])
27+
elif a[key] == b[key]:
28+
pass # same leaf value
29+
else:
30+
a[key] = b[key]
31+
else:
32+
a[key] = b[key]
33+
return a
34+
35+
36+
def _comment_out_toml(s: str):
37+
return "\n".join(["#" + line for line in s.split("\n")])
38+
39+
40+
def load_config_toml(
41+
appname: str, default_config: str
42+
) -> Union[dict, tomlkit.container.Container]:
43+
config_dir = dirs.get_config_dir(appname)
44+
config_file_path = os.path.join(config_dir, "{}.toml".format(appname))
45+
46+
# Run early to ensure input is valid toml before writing
47+
default_config_toml = tomlkit.parse(default_config)
48+
49+
# Override defaults from existing config file
50+
if os.path.isfile(config_file_path):
51+
with open(config_file_path, "r") as f:
52+
config = f.read()
53+
config_toml = tomlkit.parse(config)
54+
else:
55+
# TODO: If file doesn't exist, write with commented-out default config
56+
with open(config_file_path, "w") as f:
57+
f.write(_comment_out_toml(default_config))
58+
config_toml = dict()
59+
60+
config = _merge(default_config_toml, config_toml)
61+
62+
return config
63+
64+
65+
def save_config_toml(appname: str, config: str) -> None:
66+
# Check that passed config string is valid toml
67+
assert tomlkit.parse(config)
68+
69+
config_dir = dirs.get_config_dir(appname)
70+
config_file_path = os.path.join(config_dir, "{}.toml".format(appname))
71+
72+
with open(config_file_path, "w") as f:
73+
f.write(config)
74+
75+
76+
@deprecated(
77+
details="Use the load_config_toml function instead",
78+
deprecated_in="0.4.2",
79+
current_version=__version__,
80+
)
1081
def load_config(appname, default_config):
1182
"""
1283
Take the defaults, and if a config file exists, use the settings specified
@@ -15,11 +86,11 @@ def load_config(appname, default_config):
1586
config = default_config
1687

1788
config_dir = dirs.get_config_dir(appname)
18-
config_file_path = os.path.join(config_dir, "{}.ini".format(appname))
89+
config_file_path = os.path.join(config_dir, "{}.toml".format(appname))
1990

2091
# Override defaults from existing config file
2192
if os.path.isfile(config_file_path):
22-
with open(config_file_path, 'r') as f:
93+
with open(config_file_path, "r") as f:
2394
config.read_file(f)
2495

2596
# Overwrite current config file (necessary in case new default would be added)
@@ -28,8 +99,13 @@ def load_config(appname, default_config):
2899
return config
29100

30101

102+
@deprecated(
103+
details="Use the save_config_toml function instead",
104+
deprecated_in="0.4.2",
105+
current_version=__version__,
106+
)
31107
def save_config(appname, config):
32108
config_dir = dirs.get_config_dir(appname)
33109
config_file_path = os.path.join(config_dir, "{}.ini".format(appname))
34-
with open(config_file_path, 'w') as f:
110+
with open(config_file_path, "w") as f:
35111
config.write(f)

poetry.lock

Lines changed: 30 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ python-json-logger = "^0.1.11"
2525
TakeTheTime = "^0.3.1"
2626
pymongo = {version = "^3.10.0", optional = true}
2727
strict-rfc3339 = "^0.7"
28+
tomlkit = "^0.6.0"
29+
deprecation = "^2.0.7"
2830

2931
[tool.poetry.dev-dependencies]
3032
pytest = "^5.3.2"

tests/test_config.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,83 @@
1-
import unittest
21
import shutil
32
from configparser import ConfigParser
43

4+
import pytest
5+
import deprecation
6+
57
from aw_core import dirs
6-
from aw_core.config import load_config, save_config
8+
from aw_core.config import load_config, save_config, load_config_toml, save_config_toml
9+
10+
appname = "aw-core-test"
11+
section = "section"
12+
config_dir = dirs.get_config_dir(appname)
13+
14+
default_config_str = f"""# A default config file, with comments!
15+
[{section}]
16+
somestring = "Hello World!" # A comment
17+
somevalue = 12.3 # Another comment
18+
somearray = ["asd", 123]"""
19+
20+
21+
@pytest.fixture(autouse=True)
22+
def clean_config():
23+
# Remove test config file if it already exists
24+
shutil.rmtree(config_dir, ignore_errors=True)
25+
26+
# Rerun get_config dir to create config directory
27+
dirs.get_config_dir(appname)
728

8-
def test_create():
9-
appname = "aw-core-test"
10-
section = "section"
11-
config_dir = dirs.get_config_dir(appname)
29+
yield
1230

1331
# Remove test config file if it already exists
1432
shutil.rmtree(config_dir)
1533

34+
35+
def test_config_defaults():
36+
# Load non-existing config (will create a out-commented default config file)
37+
config = load_config_toml(appname, default_config_str)
38+
39+
# Check that load_config used defaults
40+
assert config[section]["somestring"] == "Hello World!"
41+
assert config[section]["somevalue"] == 12.3
42+
assert config[section]["somearray"] == ["asd", 123]
43+
44+
45+
def test_config_no_defaults():
46+
# Write defaults to file
47+
save_config_toml(appname, default_config_str)
48+
49+
# Load written defaults without defaults
50+
config = load_config_toml(appname, "")
51+
assert config[section]["somestring"] == "Hello World!"
52+
assert config[section]["somevalue"] == 12.3
53+
assert config[section]["somearray"] == ["asd", 123]
54+
55+
56+
def test_config_override():
57+
# Create a minimal config file with one overridden value
58+
config = """[section]
59+
somevalue = 1000.1"""
60+
save_config_toml(appname, config)
61+
62+
# Open non-default config file and verify that values are correct
63+
config = load_config_toml(appname, default_config_str)
64+
assert config[section]["somevalue"] == 1000.1
65+
66+
67+
@deprecation.fail_if_not_removed
68+
def test_config_ini():
1669
# Create default config
1770
default_config = ConfigParser()
18-
default_config[section] = {
19-
"somestring": "Hello World!",
20-
"somevalue": 12.3
21-
}
71+
default_config[section] = {"somestring": "Hello World!", "somevalue": 12.3}
2272

2373
# Load non-existing config (will create a default config file)
2474
config = load_config(appname, default_config)
2575

2676
# Check that current config file is same as default config file
2777
assert config[section]["somestring"] == default_config[section]["somestring"]
28-
assert config[section].getfloat("somevalue") == default_config[section].getfloat("somevalue")
78+
assert config[section].getfloat("somevalue") == default_config[section].getfloat(
79+
"somevalue"
80+
)
2981

3082
# Modify and save config file
3183
config[section]["somevalue"] = "1000.1"
@@ -34,7 +86,6 @@ def test_create():
3486
# Open non-default config file and verify that values are correct
3587
new_config = load_config(appname, default_config)
3688
assert new_config[section]["somestring"] == config[section]["somestring"]
37-
assert new_config[section].getfloat("somevalue") == config[section].getfloat("somevalue")
38-
39-
# Remove test config file
40-
shutil.rmtree(config_dir)
89+
assert new_config[section].getfloat("somevalue") == config[section].getfloat(
90+
"somevalue"
91+
)

0 commit comments

Comments
 (0)