Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aw_core/__about__.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same way that aw-server-rust does it.
Whether it's the best solution though I'm not sure as the default config might become outdated if it gets updated in the future. It's a decent solution anyway. The other possible solutions I have in mind are neither better or worse just different, so might as well just be consistent with how aw-server-rust does it.

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a schema to test against here and/or in the tests?

Also does that assert result in a good runtime error?

Copy link
Member Author

@ErikBjare ErikBjare Apr 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since each user (server, watchers) of the API decides itself what config options to use I'm not sure if a schema makes sense.

But maybe it could check against the default config or something to make sure things are of the same type etc? Sounds like a lot of work though, and it will probably work just fine and errors should be rare and clearly user-caused (since the user will know that they edited the config).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice with a schema, but considering the work needed I don't think it's worth it.
I don't think the configs are large enough as of now that making a schema would add much value, if we find in the future that we would like that we can add that.


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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)