Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #88 from ActivityWatch/dev/config-toml
  • Loading branch information
ErikBjare committed Jun 15, 2021
2 parents aa14c8b + 067bb69 commit 811712c
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 12 deletions.
2 changes: 1 addition & 1 deletion aw_core/__about__.py
Expand Up @@ -16,7 +16,7 @@
__summary__ = "Core library for ActivityWatch"
__uri__ = "https://github.com/ActivityWatch/aw-core"

__version__ = "0.4.1"
__version__ = "0.4.2"

__author__ = "Erik Bjäreholt, Johan Bjäreholt"
__email__ = "erik@bjareho.lt, johan@bjareho.lt"
Expand Down
78 changes: 77 additions & 1 deletion aw_core/config.py
@@ -1,12 +1,83 @@
import os
import logging
from typing import Any, Dict, Union
from configparser import ConfigParser

from deprecation import deprecated
import tomlkit

from aw_core import dirs
from aw_core.__about__ import __version__

logger = logging.getLogger(__name__)


def _merge(a: dict, b: dict, path=None):
"""
Recursively merges b into a, with b taking precedence.
From: https://stackoverflow.com/a/7205107/965332
"""
if path is None:
path = []
for key in b:
if key in a:
if isinstance(a[key], dict) and isinstance(b[key], dict):
_merge(a[key], b[key], path + [str(key)])
elif a[key] == b[key]:
pass # same leaf value
else:
a[key] = b[key]
else:
a[key] = b[key]
return a


def _comment_out_toml(s: str):
return "\n".join(["#" + line for line in s.split("\n")])


def load_config_toml(
appname: str, default_config: str
) -> Union[dict, tomlkit.container.Container]:
config_dir = dirs.get_config_dir(appname)
config_file_path = os.path.join(config_dir, "{}.toml".format(appname))

# Run early to ensure input is valid toml before writing
default_config_toml = tomlkit.parse(default_config)

# Override defaults from existing config file
if os.path.isfile(config_file_path):
with open(config_file_path, "r") as f:
config = f.read()
config_toml = tomlkit.parse(config)
else:
# If file doesn't exist, write with commented-out default config
with open(config_file_path, "w") as f:
f.write(_comment_out_toml(default_config))
config_toml = dict()

config = _merge(default_config_toml, config_toml)

return config


def save_config_toml(appname: str, config: str) -> None:
# Check that passed config string is valid toml
assert tomlkit.parse(config)

config_dir = dirs.get_config_dir(appname)
config_file_path = os.path.join(config_dir, "{}.toml".format(appname))

with open(config_file_path, "w") as f:
f.write(config)


@deprecated(
details="Use the load_config_toml function instead",
deprecated_in="0.4.2",
current_version=__version__,
)
def load_config(appname, default_config):
"""
Take the defaults, and if a config file exists, use the settings specified
Expand All @@ -15,7 +86,7 @@ def load_config(appname, default_config):
config = default_config

config_dir = dirs.get_config_dir(appname)
config_file_path = os.path.join(config_dir, "{}.ini".format(appname))
config_file_path = os.path.join(config_dir, "{}.toml".format(appname))

# Override defaults from existing config file
if os.path.isfile(config_file_path):
Expand All @@ -28,6 +99,11 @@ def load_config(appname, default_config):
return config


@deprecated(
details="Use the save_config_toml function instead",
deprecated_in="0.4.2",
current_version=__version__,
)
def save_config(appname, config):
config_dir = dirs.get_config_dir(appname)
config_file_path = os.path.join(config_dir, "{}.ini".format(appname))
Expand Down
33 changes: 30 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pyproject.toml
Expand Up @@ -25,6 +25,8 @@ python-json-logger = "^0.1.11"
TakeTheTime = "^0.3.1"
pymongo = {version = "^3.10.0", optional = true}
strict-rfc3339 = "^0.7"
tomlkit = "^0.6.0"
deprecation = "^2.0.7"
timeslot = "*"

[tool.poetry.dev-dependencies]
Expand Down
69 changes: 62 additions & 7 deletions tests/test_config.py
@@ -1,19 +1,77 @@
import unittest
import shutil
from configparser import ConfigParser

import pytest
import deprecation

from aw_core import dirs
from aw_core.config import load_config, save_config
from aw_core.config import load_config, save_config, load_config_toml, save_config_toml

appname = "aw-core-test"
section = "section"
config_dir = dirs.get_config_dir(appname)

default_config_str = f"""# A default config file, with comments!
[{section}]
somestring = "Hello World!" # A comment
somevalue = 12.3 # Another comment
somearray = ["asd", 123]"""


@pytest.fixture(autouse=True)
def clean_config():
# Remove test config file if it already exists
shutil.rmtree(config_dir, ignore_errors=True)

# Rerun get_config dir to create config directory
dirs.get_config_dir(appname)

yield

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


def test_create():
appname = "aw-core-test"
section = "section"
config_dir = dirs.get_config_dir(appname)

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

def test_config_defaults():
# Load non-existing config (will create a out-commented default config file)
config = load_config_toml(appname, default_config_str)

# Check that load_config used defaults
assert config[section]["somestring"] == "Hello World!"
assert config[section]["somevalue"] == 12.3
assert config[section]["somearray"] == ["asd", 123]


def test_config_no_defaults():
# Write defaults to file
save_config_toml(appname, default_config_str)

# Load written defaults without defaults
config = load_config_toml(appname, "")
assert config[section]["somestring"] == "Hello World!"
assert config[section]["somevalue"] == 12.3
assert config[section]["somearray"] == ["asd", 123]


def test_config_override():
# Create a minimal config file with one overridden value
config = """[section]
somevalue = 1000.1"""
save_config_toml(appname, config)

# Open non-default config file and verify that values are correct
config = load_config_toml(appname, default_config_str)
assert config[section]["somevalue"] == 1000.1


@deprecation.fail_if_not_removed
def test_config_ini():
# Create default config
default_config = ConfigParser()
default_config[section] = {"somestring": "Hello World!", "somevalue": 12.3}
Expand All @@ -37,6 +95,3 @@ def test_create():
assert new_config[section].getfloat("somevalue") == config[section].getfloat(
"somevalue"
)

# Remove test config file
shutil.rmtree(config_dir)

0 comments on commit 811712c

Please sign in to comment.