From 72931658eab7bb3e3f9a3a37b5d687487c8a02ee Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 24 Nov 2025 07:59:54 +0100 Subject: [PATCH 1/6] Update Python versions --- .github/workflows/build.yml | 16 ++++++---------- CHANGELOG.md | 7 +++++++ README.md | 4 +++- pyproject.toml | 12 ++++++------ requirements.txt | 29 +++++++++++++++++++++-------- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96f0292..8c68b01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 @@ -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 @@ -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: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cf064..59a70df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ 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. + ## [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 diff --git a/README.md b/README.md index 0850611..6d47002 100644 --- a/README.md +++ b/README.md @@ -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 @@ -31,6 +32,7 @@ enabling different scenarios like configuration by environment and system instance. ## Supported sources: + - **toml** files - **yaml** files - **json** files diff --git a/pyproject.toml b/pyproject.toml index aee7601..903ccc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,28 +4,28 @@ build-backend = "hatchling.build" [project] name = "essentials-configuration" -version = "2.0.4" +version = "2.0.5" authors = [{ name = "Roberto Prevato", email = "roberto.prevato@gmail.com" }] description = "Implementation of key-value pair based configuration for Python applications." readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] keywords = ["configuration", "root", "management", "strategy", "settings"] dependencies = [ - "deepmerge~=1.1.0", + "deepmerge~=2.0", "tomli; python_version < '3.11'", - "python-dotenv~=1.0.0", + "python-dotenv~=1.2.1", ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index 61e3c46..592bc80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,17 @@ +annotated-types==0.7.0 appdirs==1.4.4 attrs==22.1.0 black==22.12.0 -build==0.10.0 +build==1.3.0 +certifi==2025.11.12 +charset-normalizer==3.4.4 click==8.1.7 +commonmark==0.9.1 coverage==7.4.0 -deepmerge==1.1.0 +deepmerge==2.0 +docutils==0.22.3 flake8==5.0.4 +idna==3.11 iniconfig==1.1.1 isort==5.10.1 markdown-it-py==2.2.0 @@ -19,18 +25,25 @@ platformdirs==2.5.2 pluggy==1.0.0 py==1.11.0 pycodestyle==2.9.1 -pydantic==1.10.7 +pydantic==2.12.4 +pydantic_core==2.41.5 pyflakes==2.5.0 Pygments==2.15.0 pyparsing==3.0.9 pyproject_hooks==1.0.0 pytest==7.4.3 pytest-cov==4.1.0 -python-dotenv==1.0.0 -PyYAML==6.0.1 +python-dotenv==1.2.1 +PyYAML==6.0.3 regex==2022.10.31 -rich==13.3.4 +requests==2.32.5 +rich==12.6.0 +rich-cli==1.8.1 rich-click==1.6.1 +rich-rst==1.3.2 +textual==0.1.18 toml==0.10.2 -tomli==2.0.1 -typing_extensions==4.9.0 +tomli==2.3.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 From d87d05c45acfbe27db044c807c089a1a7fc4b7ba Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 24 Nov 2025 08:01:51 +0100 Subject: [PATCH 2/6] Update requirements.txt --- requirements.txt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 592bc80..dbd728b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ annotated-types==0.7.0 appdirs==1.4.4 attrs==22.1.0 -black==22.12.0 +black==25.11.0 build==1.3.0 certifi==2025.11.12 charset-normalizer==3.4.4 @@ -10,30 +10,31 @@ commonmark==0.9.1 coverage==7.4.0 deepmerge==2.0 docutils==0.22.3 -flake8==5.0.4 +flake8==7.3.0 idna==3.11 iniconfig==1.1.1 -isort==5.10.1 +isort==7.0.0 markdown-it-py==2.2.0 mccabe==0.7.0 mdurl==0.1.2 mypy==0.982 mypy-extensions==0.4.3 -packaging==21.3 +packaging==25.0 pathspec==0.10.1 platformdirs==2.5.2 pluggy==1.0.0 py==1.11.0 -pycodestyle==2.9.1 +pycodestyle==2.14.0 pydantic==2.12.4 pydantic_core==2.41.5 -pyflakes==2.5.0 +pyflakes==3.4.0 Pygments==2.15.0 pyparsing==3.0.9 pyproject_hooks==1.0.0 pytest==7.4.3 pytest-cov==4.1.0 python-dotenv==1.2.1 +pytokens==0.3.0 PyYAML==6.0.3 regex==2022.10.31 requests==2.32.5 From a73021ca99214881fad6cd484eb64a5b847bf3bf Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 24 Nov 2025 08:49:51 +0100 Subject: [PATCH 3/6] Upgrade type annotations --- CHANGELOG.md | 1 + config/common/__init__.py | 16 ++++++++-------- config/common/files.py | 8 ++++---- config/env/__init__.py | 8 ++++---- config/ini/__init__.py | 4 ++-- config/json/__init__.py | 4 ++-- config/toml/__init__.py | 4 ++-- config/user/__init__.py | 7 ++++--- config/user/cli.py | 16 ++++++++-------- config/yaml/__init__.py | 4 ++-- tests/test_configuration.py | 4 ++-- 11 files changed, 39 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a70df..0a20d15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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: diff --git a/config/common/__init__.py b/config/common/__init__.py index 7ce3bb3..c8c8857 100644 --- a/config/common/__init__.py +++ b/config/common/__init__.py @@ -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 @@ -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: @@ -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 @@ -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 @@ -166,7 +166,7 @@ def __repr__(self) -> str: return f"" @property - def values(self) -> Dict[str, Any]: + def values(self) -> dict[str, Any]: """ Returns a copy of the dictionary of current settings. """ @@ -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"" @property - def sources(self) -> List[ConfigurationSource]: + def sources(self) -> list[ConfigurationSource]: return self._sources def add_source(self, source: ConfigurationSource): diff --git a/config/common/files.py b/config/common/files.py index dc6c392..f5a7266 100644 --- a/config/common/files.py +++ b/config/common/files.py @@ -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): @@ -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 {} diff --git a/config/env/__init__.py b/config/env/__init__.py index c65cfb2..2f8cb30 100644 --- a/config/env/__init__.py +++ b/config/env/__init__.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict, Optional +from typing import Any from dotenv import load_dotenv @@ -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) diff --git a/config/ini/__init__.py b/config/ini/__init__.py index 8a8ced6..f7408e9 100644 --- a/config/ini/__init__.py +++ b/config/ini/__init__.py @@ -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 @@ -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) diff --git a/config/json/__init__.py b/config/json/__init__.py index 0ed7c9f..7670ded 100644 --- a/config/json/__init__.py +++ b/config/json/__init__.py @@ -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) diff --git a/config/toml/__init__.py b/config/toml/__init__.py index fd35186..1c641b7 100644 --- a/config/toml/__init__.py +++ b/config/toml/__init__.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable from config.common.files import FileConfigurationSource, PathType @@ -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) diff --git a/config/user/__init__.py b/config/user/__init__.py index 902d35b..154c364 100644 --- a/config/user/__init__.py +++ b/config/user/__init__.py @@ -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 @@ -37,7 +38,7 @@ class UserSettings(ConfigurationSource): """ def __init__( - self, project_name: Optional[str] = None, optional: bool = True + self, project_name: str | None = None, optional: bool = True ) -> None: """ Configures an instance of UserSettings that obtains values from a project file @@ -60,6 +61,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() diff --git a/config/user/cli.py b/config/user/cli.py index 1ec9624..6a200b9 100644 --- a/config/user/cli.py +++ b/config/user/cli.py @@ -22,7 +22,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() @@ -120,7 +120,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 @@ -133,7 +133,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 @@ -151,7 +151,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 @@ -167,7 +167,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 @@ -188,7 +188,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. """ @@ -197,7 +197,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. """ @@ -214,7 +214,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. """ diff --git a/config/yaml/__init__.py b/config/yaml/__init__.py index 9db8fa6..fdc6706 100644 --- a/config/yaml/__init__.py +++ b/config/yaml/__init__.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any import yaml @@ -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) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index d604d41..6d63465 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,5 +1,5 @@ import os -from typing import Any, Dict +from typing import Any from uuid import uuid4 import pytest @@ -32,7 +32,7 @@ def _get_file_path(file_name: str) -> str: class FooSource(ConfigurationSource): - def get_values(self) -> Dict[str, Any]: + def get_values(self) -> dict[str, Any]: return {} From 981a17cef1d328ffe0b75877d3eb7221392c80a8 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 24 Nov 2025 08:50:54 +0100 Subject: [PATCH 4/6] Update cli.py --- config/user/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/config/user/cli.py b/config/user/cli.py index 6a200b9..fa43c60 100644 --- a/config/user/cli.py +++ b/config/user/cli.py @@ -2,7 +2,6 @@ import os import sys from pathlib import Path -from typing import Optional import rich_click as click From d05145aa35c6fc90dedfd112b39975531f13bab0 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 24 Nov 2025 08:52:30 +0100 Subject: [PATCH 5/6] Update __init__.py --- config/user/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/user/__init__.py b/config/user/__init__.py index 154c364..90c53b3 100644 --- a/config/user/__init__.py +++ b/config/user/__init__.py @@ -37,9 +37,7 @@ class UserSettings(ConfigurationSource): This class reads settings stored in a file inside the user's folder. """ - def __init__( - self, project_name: str | None = 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 From ea795c3a66cbb59ba3f3b18400fbca90ffdc2e0f Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Mon, 24 Nov 2025 08:53:13 +0100 Subject: [PATCH 6/6] Format examples --- examples/example-02.py | 1 + examples/example-03.py | 1 + examples/example-04.py | 1 + examples/example-05.py | 1 + examples/example-07.py | 1 + 5 files changed, 5 insertions(+) diff --git a/examples/example-02.py b/examples/example-02.py index 608bff4..0b37778 100644 --- a/examples/example-02.py +++ b/examples/example-02.py @@ -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 diff --git a/examples/example-03.py b/examples/example-03.py index 8e90e03..aef8420 100644 --- a/examples/example-03.py +++ b/examples/example-03.py @@ -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 diff --git a/examples/example-04.py b/examples/example-04.py index d35f7f6..d8db595 100644 --- a/examples/example-04.py +++ b/examples/example-04.py @@ -2,6 +2,7 @@ This example illustrates a way to override settings from a common file, using an environment specific settings file. """ + import os from dataclasses import dataclass diff --git a/examples/example-05.py b/examples/example-05.py index f9689cb..da035c4 100644 --- a/examples/example-05.py +++ b/examples/example-05.py @@ -1,6 +1,7 @@ """ This example shows how nested values can be overridden using strings. """ + from config.common import ConfigurationBuilder, MapSource builder = ConfigurationBuilder( diff --git a/examples/example-07.py b/examples/example-07.py index 611faf1..b36bb7c 100644 --- a/examples/example-07.py +++ b/examples/example-07.py @@ -2,6 +2,7 @@ This example shows how to override nested properties using environment variables. """ + import os from config.common import ConfigurationBuilder, MapSource