Skip to content

Commit

Permalink
Create initial framework for configuration files
Browse files Browse the repository at this point in the history
  • Loading branch information
purefunctor committed Jun 9, 2021
1 parent 452885d commit 5a0b0c4
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 0 deletions.
79 changes: 79 additions & 0 deletions stlt/config.py
@@ -0,0 +1,79 @@
"""Reading and writing user configuration and secrets."""
from __future__ import annotations

from pathlib import Path
import typing as t

import attr
import toml

from stlt.errors import ConfigError


EMPTY_CONFIG_FILE = {
"oauth": {
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"redirect_uri": "REDIRECT_URI",
"scope": "SCOPE",
}
}


@attr.s(slots=True)
class OAuthConfig:
"""Configuration data class for `SpotifyOAuth`."""

client_id: str = attr.ib()
client_secret: str = attr.ib()
redirect_uri: str = attr.ib()
scope: str = attr.ib()

@classmethod
def from_dict(cls, data: t.Mapping) -> OAuthConfig:
"""Create an `OAuthConfig` from a `toml`-based mapping."""
fields = {}
for field in attr.fields(cls):
name = field.name
try:
fields[name] = data[name]
except KeyError:
raise ConfigError(f"Missing required key '{name}'") from None
return cls(**fields)


@attr.s(slots=True)
class Config:
"""Configuration data class for the project."""

oauth: OAuthConfig = attr.ib()

@classmethod
def from_dict(cls, data: t.Mapping) -> Config:
"""Create a `Config` from a `toml`-based mapping."""
builders = {
"oauth": OAuthConfig.from_dict,
}
fields = {}
for field in attr.fields(cls):
name = field.name
try:
fields[name] = builders[name](data[name])
except KeyError:
raise ConfigError(f"Missing required section '{name}'") from None
return cls(**fields)


def ensure_config(config: Path) -> None:
"""Ensure that the `config` file exists and is valid."""
config.parent.mkdir(parents=True, exist_ok=True)
config.touch(exist_ok=True)

with config.open("w") as f:
toml.dump(EMPTY_CONFIG_FILE, f)


def load_config(config: Path) -> t.Mapping:
"""Deserialize the `config` file into a `Mapping`."""
ensure_config(config)
return toml.load(config)
9 changes: 9 additions & 0 deletions stlt/errors.py
@@ -0,0 +1,9 @@
"""Error types that appear within the library."""


class StltError(Exception):
"""Base error type for the library."""


class ConfigError(Exception):
"""Error type raised on configuration."""
55 changes: 55 additions & 0 deletions tests/test_config.py
@@ -0,0 +1,55 @@
import pytest
import toml

from stlt.config import (
Config,
EMPTY_CONFIG_FILE,
OAuthConfig,
ensure_config,
load_config,
)
from stlt.errors import ConfigError


@pytest.fixture
def path(tmp_path):
return tmp_path / ".config" / "config.toml"


class TestEnsureConfig:
def test_creates_a_placeholder_configuration_file(self, path):
ensure_config(path)

assert toml.load(path) == EMPTY_CONFIG_FILE

def test_creates_parent_folders(self, path):
ensure_config(path)

assert path.parent.exists()


class TestLoadConfig:
def test_loads_a_placeholder_configuration_file(self, path):
assert load_config(path) == EMPTY_CONFIG_FILE


class TestOAuthConfig:
def test_from_dict_parses_a_valid_mapping(self, path):
expected = OAuthConfig(**EMPTY_CONFIG_FILE["oauth"])

assert OAuthConfig.from_dict(EMPTY_CONFIG_FILE["oauth"]) == expected

def test_from_dict_fails_on_missing_keys(self, path):
with pytest.raises(ConfigError, match="Missing required key"):
OAuthConfig.from_dict({})


class TestConfig:
def test_from_dict_parses_a_valid_mapping(self, path):
expected = Config(oauth=OAuthConfig.from_dict(EMPTY_CONFIG_FILE["oauth"]))

assert Config.from_dict(EMPTY_CONFIG_FILE) == expected

def test_from_dict_fails_on_missing_sections(self, path):
with pytest.raises(ConfigError, match="Missing required section"):
Config.from_dict({})

0 comments on commit 5a0b0c4

Please sign in to comment.