From cb471dd533b2fe76f3daaf1eef6b069a27dc43af Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:57:38 +0200 Subject: [PATCH 1/4] Add test for loading singular values References #143 --- tests/test_formats.py | 47 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/test_formats.py b/tests/test_formats.py index 85f19f5..7139f3e 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -7,7 +7,42 @@ from confidence.formats import JSON, TOML, YAML -@pytest.mark.parametrize('format', (JSON, TOML, YAML)) +@pytest.mark.parametrize( + 'format', + ( + pytest.param(JSON, id='json'), + pytest.param(TOML, id='toml'), + pytest.param(YAML, id='yaml'), + ), +) +@pytest.mark.parametrize( + ('string', 'value'), + [ + ('null', None), + ('true', True), + ('1', 1), + ('42.0', 42.0), + ('a string', 'a string'), + ("single'quote", "single'quote"), + ('double"quote', 'double"quote'), + ], +) +def test_singular_value_from_string(format, string, value): + if (format, value) == (TOML, None): + # None / null / nil is not supported by the TOML spec, see https://github.com/toml-lang/toml/issues/30 + pytest.skip('None is unsupported for TOML format') + + assert format.loads(string) == value + + +@pytest.mark.parametrize( + 'format', + ( + pytest.param(JSON, id='json'), + pytest.param(TOML, id='toml'), + pytest.param(YAML, id='yaml'), + ), +) @pytest.mark.parametrize('value', (None, True, 1, 42.0, 'a string')) def test_singular_value_roundtrip(format, value): if (format, value) == (TOML, None): @@ -17,7 +52,15 @@ def test_singular_value_roundtrip(format, value): assert format.loads(format.dumps(value)) == value -@pytest.mark.parametrize('format', (JSON, TOML, YAML, YAML(suffix='.conf', encoding='utf-32'))) +@pytest.mark.parametrize( + 'format', + ( + pytest.param(JSON, id='json'), + pytest.param(TOML, id='toml'), + pytest.param(YAML, id='yaml'), + pytest.param(YAML(suffix='.conf', encoding='utf-32'), id='yaml-conf-32'), + ), +) @pytest.mark.parametrize( 'value', ( From f9c2dc468d139c0c4909eb874e8235e5284d2702 Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:10:54 +0200 Subject: [PATCH 2/4] Add test for loading typed values from environment variables References #143 --- tests/test_io.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_io.py b/tests/test_io.py index 8de5db9..95664ba 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -370,6 +370,35 @@ def test_load_name_envvars(): assert not load_name('foo', 'bar', load_order=(read_envvars,)) +@pytest.mark.parametrize( + 'format', + ( + pytest.param(JSON, id='json'), + pytest.param(TOML, id='toml'), + pytest.param(YAML, id='yaml'), + ), +) +def test_load_name_envvars_value_types(format): + env = { + 'FOO_KEY': 'foo', + 'FOO_TYPES_NUM': '42', + 'FOO_TYPES_FLT': '42.0', + 'FOO_TYPES_BOL': 'true', + 'FOO_TYPES_ST1': 'str', + 'FOO_TYPES_ST2': '"str"', + } + + with patch('confidence.io.environ', env): + config = load_name('foo', load_order=(read_envvars,), format=format) + + assert config.key == 'foo' + assert config.types.num == 42 + assert config.types.flt == 42.0 + assert config.types.bol is True + assert config.types.st1 == 'str' + assert config.types.st2 == 'str' + + def test_load_name_envvar_file(test_files): env = { 'FOO_CONFIG_FILE': path.join(test_files, 'foo.yaml'), From b9476bc44909520ab5f74f534a2b56f7391df8af Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:55:08 +0200 Subject: [PATCH 3/4] Define Format.loadv to load a single value, using str as the default fallback --- confidence/formats.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/confidence/formats.py b/confidence/formats.py index 7566f79..4c9a2fa 100644 --- a/confidence/formats.py +++ b/confidence/formats.py @@ -1,7 +1,7 @@ import json import typing from abc import ABC, abstractmethod -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass, replace from os import PathLike from pathlib import Path @@ -26,6 +26,7 @@ class Format(ABC): suffix: str = '' #: the default file path suffix for a configuration file of this Format encoding: str = 'utf-8' #: the default text encoding for reading from binary I/O + value_fallback: Callable[[str], typing.Any] = str #: the fallback 'factory' for unparseable single values def load(self, fp: typing.TextIO) -> typing.Any: return self.loads(fp.read()) @@ -38,6 +39,14 @@ def loadf(self, fpath: str | PathLike, encoding: str | None = None) -> typing.An with Path(fpath).open('rt', encoding=encoding or self.encoding) as fp: return self.load(fp) + def loadv(self, string: str) -> typing.Any: + try: + # hope the format implementation will be able to read string as a value formatted value + return self.loads(string) + except ValueError: + # use the fallback otherwise + return self.value_fallback(string) + def dump(self, value: typing.Any, fp: typing.TextIO) -> None: fp.write(self.dumps(value)) From 657074789991ae1147e8a3713b44449a8b1bf2de Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:55:32 +0200 Subject: [PATCH 4/4] Call format.loadv in load_envvars and applicable test --- confidence/io.py | 4 ++-- tests/test_formats.py | 2 +- tests/test_io.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/confidence/io.py b/confidence/io.py index f10a1b6..a6f4abe 100644 --- a/confidence/io.py +++ b/confidence/io.py @@ -101,8 +101,8 @@ def dotted(name: str) -> str: # include the number of variables matched for debugging purposes LOG.info(f'reading configuration from {len(values)} {prefix}* environment variables') - # pass value to yaml.safe_load to align data type transformation with reading values from files - return Configuration({dotted(name): format.loads(value) for name, value in values.items()}) + # pass value to format.loadv to align data type transformation with reading values from files + return Configuration({dotted(name): format.loadv(value) for name, value in values.items()}) def read_envvar_file(name: str, format: Format = YAML) -> Configuration: diff --git a/tests/test_formats.py b/tests/test_formats.py index 7139f3e..fc5507a 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -32,7 +32,7 @@ def test_singular_value_from_string(format, string, value): # None / null / nil is not supported by the TOML spec, see https://github.com/toml-lang/toml/issues/30 pytest.skip('None is unsupported for TOML format') - assert format.loads(string) == value + assert format.loadv(string) == value @pytest.mark.parametrize( diff --git a/tests/test_io.py b/tests/test_io.py index 95664ba..c26f347 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -394,7 +394,7 @@ def test_load_name_envvars_value_types(format): assert config.key == 'foo' assert config.types.num == 42 assert config.types.flt == 42.0 - assert config.types.bol is True + assert config.types.bol == True # noqa: E712 (tomlkit uses a customized type that not is True but == True) assert config.types.st1 == 'str' assert config.types.st2 == 'str'