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
16 changes: 6 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v1
Expand Down Expand Up @@ -58,27 +58,23 @@ jobs:
path: junit/pytest-results-${{ matrix.python-version }}.xml
if: always()

- name: Codecov
run: |
bash <(curl -s https://codecov.io/bash)

- name: Install distribution dependencies
run: |
rm -rf junit
rm -rf deps
pip install build
if: matrix.python-version == 3.12
if: matrix.python-version == 3.13

- name: Create distribution package
run: python -m build
if: matrix.python-version == 3.12
if: matrix.python-version == 3.13

- name: Upload distribution package
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
if: matrix.python-version == 3.12
if: matrix.python-version == 3.13

publish:
runs-on: ubuntu-latest
Expand All @@ -91,10 +87,10 @@ jobs:
name: dist
path: dist

- name: Use Python 3.12
- name: Use Python 3.13
uses: actions/setup-python@v1
with:
python-version: '3.12'
python-version: '3.13'

- name: Install dependencies
run: |
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.5] - 2025-11-24

- Upgrade Python versions in workflow.
- Remove support for Python <= 3.10.
- Remove Codecov from README and workflow.
- Upgrade type annotations.

## [2.0.4] - 2023-12-28 :snowman_with_snow:

- Improves the library to deep-merge dictionaries of values instead of replacing
objects entirely (fix [#10](https://github.com/Neoteroi/essentials-configuration/issues/10)), by @StummeJ.
- Adds Python 3.12 to the build matrix, upgrades dev dependencies and fix tests
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
![Build](https://github.com/Neoteroi/essentials-configuration/workflows/Build/badge.svg)
[![pypi](https://img.shields.io/pypi/v/essentials-configuration.svg)](https://pypi.python.org/pypi/essentials-configuration)
[![versions](https://img.shields.io/pypi/pyversions/essentials-configuration.svg)](https://github.com/Neoteroi/essentials-configuration)
[![codecov](https://codecov.io/gh/Neoteroi/essentials-configuration/branch/main/graph/badge.svg?token=VzAnusWIZt)](https://codecov.io/gh/Neoteroi/essentials-configuration)
[![license](https://img.shields.io/github/license/Neoteroi/essentials-configuration.svg)](https://github.com/Neoteroi/essentials-configuration/blob/main/LICENSE)

# Python configuration utilities

Implementation of key-value pair based configuration for Python applications.

**Features:**

- support for most common sources of application settings
- support for overriding settings in sequence
- support for nested structures and lists, using attribute notation
Expand All @@ -31,6 +32,7 @@ enabling different scenarios like configuration by environment and system
instance.

## Supported sources:

- **toml** files
- **yaml** files
- **json** files
Expand Down
16 changes: 8 additions & 8 deletions config/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC, abstractmethod
from collections import abc
from typing import Any, Dict, List, Mapping, Optional, Type, TypeVar
from typing import Any, Mapping, Type, TypeVar

from deepmerge import Merger

Expand Down Expand Up @@ -97,7 +97,7 @@ def merge_values(destination: Mapping[str, Any], source: Mapping[str, Any]) -> N

class ConfigurationSource(ABC):
@abstractmethod
def get_values(self) -> Dict[str, Any]:
def get_values(self) -> dict[str, Any]:
"""Returns the values read from this source."""

def __repr__(self) -> str:
Expand All @@ -112,7 +112,7 @@ def __init__(self, values: Mapping[str, Any]) -> None:
super().__init__()
self._values = dict(values.items())

def get_values(self) -> Dict[str, Any]:
def get_values(self) -> dict[str, Any]:
return self._values


Expand All @@ -136,11 +136,11 @@ def __new__(cls, arg=None):
return [cls(item) for item in arg]
return arg

def __init__(self, mapping: Optional[Mapping[str, Any]] = None):
def __init__(self, mapping: Mapping[str, Any] | None = None):
"""
Creates a new instance of Configuration object with the given values.
"""
self._data: Dict[str, Any] = dict(mapping.items()) if mapping else {}
self._data: dict[str, Any] = dict(mapping.items()) if mapping else {}

def __contains__(self, item: str) -> bool:
return item in self._data
Expand All @@ -166,7 +166,7 @@ def __repr__(self) -> str:
return f"<Configuration {repr(hidden_values)}>"

@property
def values(self) -> Dict[str, Any]:
def values(self) -> dict[str, Any]:
"""
Returns a copy of the dictionary of current settings.
"""
Expand All @@ -190,13 +190,13 @@ def __init__(self, *sources: ConfigurationSource) -> None:
object from different sources. Sources are applied in the given order and can
override each other's settings.
"""
self._sources: List[ConfigurationSource] = list(sources) if sources else []
self._sources: list[ConfigurationSource] = list(sources) if sources else []

def __repr__(self) -> str:
return f"<ConfigurationBuilder {self._sources}>"

@property
def sources(self) -> List[ConfigurationSource]:
def sources(self) -> list[ConfigurationSource]:
return self._sources

def add_source(self, source: ConfigurationSource):
Expand Down
8 changes: 4 additions & 4 deletions config/common/files.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from abc import abstractmethod
from pathlib import Path
from typing import Any, Dict, Union
from typing import Any

from config.common import ConfigurationSource
from config.errors import MissingConfigurationFileError

PathType = Union[Path, str]
PathType = Path | str


class FileConfigurationSource(ConfigurationSource):
Expand All @@ -15,13 +15,13 @@ def __init__(self, file_path: PathType, optional: bool = False) -> None:
self.optional = optional

@abstractmethod
def read_source(self) -> Dict[str, Any]:
def read_source(self) -> dict[str, Any]:
"""
Reads values from the source file path. This method is not
used if the file does not exist.
"""

def get_values(self) -> Dict[str, Any]:
def get_values(self) -> dict[str, Any]:
if not self.file_path.exists():
if self.optional:
return {}
Expand Down
8 changes: 4 additions & 4 deletions config/env/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Any, Dict, Optional
from typing import Any

from dotenv import load_dotenv

Expand All @@ -10,16 +10,16 @@
class EnvironmentVariables(ConfigurationSource):
def __init__(
self,
prefix: Optional[str] = None,
prefix: str | None = None,
strip_prefix: bool = True,
file: Optional[PathType] = None,
file: PathType | None = None,
) -> None:
super().__init__()
self.prefix = prefix
self.strip_prefix = strip_prefix
self._file = file

def get_values(self) -> Dict[str, Any]:
def get_values(self) -> dict[str, Any]:
if self._file:
load_dotenv(self._file)

Expand Down
4 changes: 2 additions & 2 deletions config/ini/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import configparser
from collections import abc
from typing import Any, Dict
from typing import Any

from config.common.files import FileConfigurationSource

Expand All @@ -20,7 +20,7 @@ def _develop_configparser_values(parser):


class INIFile(FileConfigurationSource):
def read_source(self) -> Dict[str, Any]:
def read_source(self) -> dict[str, Any]:
parser = configparser.ConfigParser()
parser.read(self.file_path, encoding="utf8")
return _develop_configparser_values(parser)
4 changes: 2 additions & 2 deletions config/json/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import json
from typing import Any, Dict
from typing import Any

from config.common.files import FileConfigurationSource


class JSONFile(FileConfigurationSource):
def read_source(self) -> Dict[str, Any]:
def read_source(self) -> dict[str, Any]:
with open(self.file_path, "rt", encoding="utf-8") as source:
return json.load(source)
4 changes: 2 additions & 2 deletions config/toml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable, Dict
from typing import Any, Callable

from config.common.files import FileConfigurationSource, PathType

Expand All @@ -20,6 +20,6 @@ def __init__(
super().__init__(file_path, optional)
self.parse_float = parse_float

def read_source(self) -> Dict[str, Any]:
def read_source(self) -> dict[str, Any]:
with open(self.file_path, "rb") as source:
return tomllib.load(source, parse_float=self.parse_float)
9 changes: 4 additions & 5 deletions config/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
This module provides support for user settings stored locally, for development, or for
CLIs.
"""

from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any
from uuid import uuid4

from config.common import ConfigurationSource
Expand Down Expand Up @@ -36,9 +37,7 @@ class UserSettings(ConfigurationSource):
This class reads settings stored in a file inside the user's folder.
"""

def __init__(
self, project_name: Optional[str] = None, optional: bool = True
) -> None:
def __init__(self, project_name: str | None = None, optional: bool = True) -> None:
"""
Configures an instance of UserSettings that obtains values from a project file
stored in the user's folder. If a project name is not provided, it is
Expand All @@ -60,6 +59,6 @@ def get_base_folder(self) -> Path:
def settings_file_path(self) -> Path:
return self._settings_file_path

def get_values(self) -> Dict[str, Any]:
def get_values(self) -> dict[str, Any]:
"""Returns the values read from this source."""
return self._source.get_values()
17 changes: 8 additions & 9 deletions config/user/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import os
import sys
from pathlib import Path
from typing import Optional

import rich_click as click

Expand All @@ -22,7 +21,7 @@ def debug(self, message):
class UserSettingsManager(UserSettings):
def __init__(
self,
project_name: Optional[str] = None,
project_name: str | None = None,
) -> None:
super().__init__(project_name, True)
self.logger = ClickLogger()
Expand Down Expand Up @@ -120,7 +119,7 @@ def settings():

@click.command(name="init")
@click.option("--project", "-p", required=False)
def init_settings(project: Optional[str]):
def init_settings(project: str | None):
"""
Initialize user settings for the current folder.
If a project name is specified, it is used, otherwise a value is obtained
Expand All @@ -133,7 +132,7 @@ def init_settings(project: Optional[str]):
@click.argument("key")
@click.argument("value")
@click.option("--project", "-p", required=False)
def set_value(key: str, value: str, project: Optional[str]):
def set_value(key: str, value: str, project: str | None):
"""
Set a setting in a user file by key and value.
If a project name is specified, it is used, otherwise a value is obtained
Expand All @@ -151,7 +150,7 @@ def set_value(key: str, value: str, project: Optional[str]):
@click.command(name="get")
@click.argument("key")
@click.option("--project", "-p", required=False)
def get_value(key: str, project: Optional[str]):
def get_value(key: str, project: str | None):
"""
Get a setting in a user file by key.
If a project name is specified, it is used, otherwise a value is obtained
Expand All @@ -167,7 +166,7 @@ def get_value(key: str, project: Optional[str]):
@click.command(name="set-many")
@click.option("--file", help="Input file", type=click.File("r"), default=sys.stdin)
@click.option("--project", "-p", required=False)
def set_many_values(file, project: Optional[str]):
def set_many_values(file, project: str | None):
"""
Set many settings, read from a JSON file passed through stdin.
If a project name is specified, it is used, otherwise a value is obtained
Expand All @@ -188,7 +187,7 @@ def set_many_values(file, project: Optional[str]):
@click.command(name="del")
@click.argument("key")
@click.option("--project", "-p", required=False)
def del_value(key: str, project: Optional[str]):
def del_value(key: str, project: str | None):
"""
Delete a setting for a project, by key.
"""
Expand All @@ -197,7 +196,7 @@ def del_value(key: str, project: Optional[str]):

@click.command(name="show")
@click.option("--project", "-p", required=False)
def show_settings(project: Optional[str]):
def show_settings(project: str | None):
"""
Show the local settings for a project.
"""
Expand All @@ -214,7 +213,7 @@ def list_groups():

@click.command(name="info")
@click.option("--project", "-p", required=False)
def show_info(project: Optional[str]):
def show_info(project: str | None):
"""
Show information about settings for a project.
"""
Expand Down
4 changes: 2 additions & 2 deletions config/yaml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any

import yaml

Expand All @@ -12,7 +12,7 @@ def __init__(
super().__init__(file_path, optional)
self.safe_load = safe_load

def read_source(self) -> Dict[str, Any]:
def read_source(self) -> dict[str, Any]:
with open(self.file_path, "rt", encoding="utf-8") as source:
if self.safe_load:
return yaml.safe_load(source)
Expand Down
1 change: 1 addition & 0 deletions examples/example-02.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This example shows how to load app settings from a TOML file, and from
environment variables, filtered by "APP_" prefix.
"""

from config.common import ConfigurationBuilder
from config.env import EnvVars
from config.toml import TOMLFile
Expand Down
1 change: 1 addition & 0 deletions examples/example-03.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
environment variables, filtered by "APP_" prefix and obtained from a .env file
(this is optional!), and how values can be overridden.
"""

from config.common import ConfigurationBuilder
from config.env import EnvVars
from config.json import JSONFile
Expand Down
Loading