Skip to content

Commit

Permalink
feat: Add config loading (#11)
Browse files Browse the repository at this point in the history
* build: Update dependencies and ruff settings

* feat: Add io functions for reading config files

* feat: Add function for loading a config

* test: Add test cases for io functions

* feat: Add safe loading of config

* feat Add test cases for config module

* docs: Add section on config files to README

* chore: Add ruff cache to .gitignore

* refactor: Rename get and set function in config.py

* style: Use pattern matching instead of if-statements

* docs: Update package docstring

* refactor: Remove file-existence check

* fix: Fix pyright issues
  • Loading branch information
leoschleier authored Jan 1, 2024
1 parent 2c9bbcc commit 7f24587
Show file tree
Hide file tree
Showing 12 changed files with 592 additions and 15 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,7 @@ cython_debug/
#.idea/

# Test data
!tests/data/.env
!tests/data/.env

# Ruff
.ruff_cache
24 changes: 24 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ from pit import viper
env = viper.auto_env()
```

### Config Files

Config files are another config source for `pit-viper`. The package supports
loading, accessing, and setting defaults for a config. Supported file formats
are JSON, TOML, and YAML.

```python
from pathlib import Path
from pit import viper

MY_CONFIG_DIR = Path() / "config"

viper.set_config_path(MY_CONFIG_DIR)
viper.set_config_name("my_config")
viper.set_config_type("toml")

viper.set("foo", "default-value")

viper.load_config()

bar = viper.get("foo")
nested_parameter = viper.get("my.nested.parameter")
```

## Development

We use [Poetry](https://github.com/python-poetry/poetry) for packaging and
Expand Down
190 changes: 183 additions & 7 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ packages = [{include = "pit", from = "src"}]

[tool.poetry.dependencies]
python = "^3.11"
pyyaml = "^6.0.1"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.2"
black = "^23.10.1"
ruff = "^0.1.3"

[build-system]
requires = ["poetry-core"]
Expand All @@ -47,6 +50,7 @@ pythonVersion = "3.11"

[tool.ruff]
select = ["ALL"]
ignore = ["ANN401"] # Use of typing.Any
fixable = ["ALL"]
fix = true
line-length = 79
Expand Down
37 changes: 30 additions & 7 deletions src/pit/viper/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
"""Pit-viper offers configuration management capabilities.
This package currently provides one main function, `auto_env`,
which reads a `.env` file and sets the corresponding environment
variables in the current process. This can be useful for configuring
applications or scripts that rely on environment variables.
This package supports accessing parameters from environment variables,
and configuration files.
Usage:
>>> from pit import viper
>>> from pathlib import Path
>>>
>>> viper.auto_env()
>>>
>>> viper.set_config_path(Path() / "config")
>>> viper.set_config_name("my_config")
>>> viper.set_config_type("toml")
>>> viper.set("foo", "default-value")
>>>
>>> viper.load_config()
>>> foo = viper.get("foo")
For more information, see the documentation for the `auto_env`
function.
For more information, see the https://github.com/leoschleier/pit-viper.
"""
from pit.viper.config import get_conf as get
from pit.viper.config import (
load_config,
set_config_name,
set_config_path,
set_config_type,
)
from pit.viper.config import set_conf as set # noqa: A001
from pit.viper.env import auto_env

__all__ = ["auto_env"]
__all__ = [
"auto_env",
"get",
"load_config",
"set",
"set_config_name",
"set_config_path",
"set_config_type",
]
94 changes: 94 additions & 0 deletions src/pit/viper/_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Internal IO functions for viper."""
import json
import tomllib
from pathlib import Path
from typing import Any

import yaml


def read_config(path: Path) -> dict[str, Any]:
"""Read a config file.
This function supports configs from JSON, TOML, and YAML files.
Parameters
----------
path : Path
Path to the config file.
Returns
-------
dict[str, Any]
Config
Raises
------
ValueError
Raised if the config file extension is not supported.
"""
file_extension = path.suffix if path.suffix else path.name

match file_extension:
case ".json":
return _read_json(path)
case ".toml":
return _read_toml(path)
case ".yml" | ".yaml":
return _read_yaml(path)
case _:
msg = f"Config file extension {file_extension} not supported."
raise ValueError(msg)




def _read_json(path: Path) -> dict[str, Any]:
"""Read a JSON file.
Parameters
----------
path : Path
Path to the JSON file.
Returns
-------
dict[str, Any]
JSON file contents.
"""
with path.open("rb") as stream:
return json.load(stream)


def _read_toml(path: Path) -> dict[str, Any]:
"""Read a TOML file.
Parameters
----------
path : Path
Path to the TOML file.
Returns
-------
dict[str, Any]
TOML file contents.
"""
with path.open("rb") as stream:
return tomllib.load(stream)


def _read_yaml(path: Path) -> dict[str, Any]:
"""Read a YAML file.
Parameters
----------
path : Path
Path to the YAML file.
Returns
-------
dict[str, Any]
YAML file contents.
"""
with path.open("rb") as stream:
return yaml.safe_load(stream)
135 changes: 135 additions & 0 deletions src/pit/viper/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Load config from file."""
from pathlib import Path
from typing import Any

from pit.viper import _io

_config_path: Path | None = None
_config_name: str = ""
_config_type: str = ""
_config: dict[str, Any] = {}


def set_config_name(name: str) -> None:
"""Set the config name.
Parameters
----------
name : str
Name of the config file.
"""
global _config_name # noqa: PLW0603
_config_name = name


def set_config_path(path: Path) -> None:
"""Set the config path.
Parameters
----------
path : Path
Path to the config file or the directory containing the config
file.
"""
global _config_path # noqa: PLW0603
_config_path = path


def set_config_type(type_: str) -> None:
"""Set the config type.
Supported config formats: json, toml, yaml, yml.
Parameters
----------
type_ : str
Config format.
"""
global _config_type # noqa: PLW0603
_config_type = type_.lower()


def get_conf(key: str, default: Any = None) -> Any:
"""Get a value from the config.
Parameters
----------
key : str
Key to get the value for.
default : Any, optional
Default value to return if the key is not found, by default None
Returns
-------
Any
Value for the key.
"""
keys = key.split(".")
value = _config

for k in keys:
if isinstance(value, dict):
value = value.get(k, default)

return value


def set_conf(key: str, value: Any) -> None:
"""Set a value in the config.
Parameters
----------
key : str
Key to set the value for.
value : Any
Value to set.
"""
keys = key.split(".")
config = _config

for k in keys[:-1]:
if isinstance(config, dict):
config = config.setdefault(k, {})

config[keys[-1]] = value


def load_config() -> None:
"""Load the config from the config file.
Raises
------
FileNotFoundError
Raised if the config path is not set or the path does not exist.
"""
if not _config_path:
msg = "Config path not set."
raise FileNotFoundError(msg)

config_file = (
f"{_config_name}.{_config_type}" if _config_type else _config_name
)
config_path = _config_path / config_file

config = _io.read_config(config_path)
for key, value in config.items():
_safe_set(key, value)


def _safe_set(key: str, value: Any) -> None:
"""Set a value in the config without overwriting nested defaults.
Parameters
----------
key : str
Key to set the value for.
value : Any
Value to set.
"""
if isinstance(value, dict):
for k, v in value.items(): # pyright: ignore [reportUnknownVariableType]
_safe_set(f"{key}.{k}", v)
else:
set_conf(key, value)


11 changes: 11 additions & 0 deletions tests/data/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"foo": "bar",
"test": {
"nested": {
"a": 0,
"b": 1.1,
"c": true,
"d": "test-string"
}
}
}
8 changes: 8 additions & 0 deletions tests/data/config/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
foo = "bar"

[test]
[test.nested]
a = 0
b = 1.1
c = true
d = "test-string"
8 changes: 8 additions & 0 deletions tests/data/config/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
foo: bar

test:
nested:
a: 0
b: 1.1
c: true
d: test-string
Loading

0 comments on commit 7f24587

Please sign in to comment.