From 0c04ab730adf43c8b92ed2e48fd4ec0e118729df Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:03:22 +0200 Subject: [PATCH 1/4] Lift formatting of a source into _format_source, enable source being a Path --- confidence/io.py | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/confidence/io.py b/confidence/io.py index f384786..a09f5e4 100644 --- a/confidence/io.py +++ b/confidence/io.py @@ -158,7 +158,7 @@ class Locality(IntEnum): ENVIRONMENT = 3 #: configuration from environment variables -Loadable = str | typing.Callable[[str, Format], Configuration] +Loadable = str | Path | typing.Callable[[str, Format], Configuration] _LOADERS: typing.Mapping[Locality, typing.Iterable[Loadable]] = { @@ -234,6 +234,29 @@ def loaders(*specifiers: Locality | Loadable) -> typing.Iterable[Loadable]: ) +def _format_source(source: str | Path, name: str, format: Format) -> Path: + match source: + case str(): + # collect the field names in the format string, issue warning if "extension" is among them + if 'extension' in {span[1] for span in Formatter().parse(source)}: + warnings.warn( + 'using "{extension}" in string template loaders has been deprecated, use "{suffix}" instead', + category=DeprecationWarning, + stacklevel=4, # warn about user code calling load_name rather than _resolve_source + ) + return Path(source.format(name=name, extension=format.suffix.lstrip('.'))) + else: + return Path(source.format(name=name, suffix=format.suffix)) + case Path(): + # format every part of the path separately, filling in name and suffix + # NB: checking for use of {extension} is omitted here, use of Path templates was added after the deprecation + # of "extension" over "suffix" + return Path(*(part.format(name=name, suffix=format.suffix) for part in source.parts)) + case _: + # any other type of source is invalid here + raise TypeError(f'cannot format source of type {type(source).__name__}') + + def load(*fps: typing.TextIO, format: Format = YAML, missing: typing.Any = Missing.SILENT) -> Configuration: """ Read a `Configuration` instance from file-like objects. @@ -299,7 +322,7 @@ def load_name( load_order: typing.Iterable[Loadable] = DEFAULT_LOAD_ORDER, format: Format = YAML, missing: typing.Any = Missing.SILENT, - extension: None = None, # NB: parameter is deprecated, see below + extension: None = None, # NB: parameter is deprecated, see _format_source ) -> Configuration: """ Read a `Configuration` instance by name, trying to read from files in @@ -338,18 +361,8 @@ def generate_sources() -> typing.Iterable[typing.Mapping[str, typing.Any]]: if callable(source): yield source(name, format) else: - # collect the field names in the format string, issue warning if "extension" is among them - if 'extension' in {span[1] for span in Formatter().parse(source)}: - warnings.warn( - 'extension name in string template loader has been deprecated, use suffix instead', - category=DeprecationWarning, - stacklevel=3, # warn about user code calling load_name rather than generate_sources - ) - candidate = Path(source.format(name=name, extension=format.suffix.lstrip('.'))) - else: - candidate = Path(source.format(name=name, suffix=format.suffix)) - - yield loadf(candidate, format=format, default=NotConfigured) + source = _format_source(source, name, format) + yield loadf(source, format=format, default=NotConfigured) return Configuration(*generate_sources(), missing=missing) From fc58ea179eb37e0b7a303f36e1b132efeaf36bf4 Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:04:13 +0200 Subject: [PATCH 2/4] Use Path objects in the default load order, add/adapt tests for new behaviour --- confidence/io.py | 14 +++++++------- tests/test_io.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/confidence/io.py b/confidence/io.py index a09f5e4..d45af73 100644 --- a/confidence/io.py +++ b/confidence/io.py @@ -165,23 +165,23 @@ class Locality(IntEnum): Locality.SYSTEM: ( # system-wide locations read_xdg_config_dirs, - '/etc/{name}/{name}{suffix}', - '/etc/{name}{suffix}', - '/Library/Preferences/{name}/{name}{suffix}', - '/Library/Preferences/{name}{suffix}', + Path('/etc/{name}/{name}{suffix}'), + Path('/etc/{name}{suffix}'), + Path('/Library/Preferences/{name}/{name}{suffix}'), + Path('/Library/Preferences/{name}{suffix}'), partial(read_envvar_dir, 'PROGRAMDATA'), ), Locality.USER: ( # user-local locations read_xdg_config_home, - '~/Library/Preferences/{name}{suffix}', + Path('~/Library/Preferences/{name}{suffix}'), partial(read_envvar_dir, 'APPDATA'), partial(read_envvar_dir, 'LOCALAPPDATA'), - '~/.{name}{suffix}', + Path('~/.{name}{suffix}'), ), Locality.APPLICATION: ( # application-local locations - './{name}{suffix}', + Path('./{name}{suffix}'), ), Locality.ENVIRONMENT: ( # application-specific environment variables diff --git a/tests/test_io.py b/tests/test_io.py index e0436c1..e445261 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -208,7 +208,7 @@ def test_load_name_single(test_files): def test_load_name_multiple(test_files): - test_path = path.join(test_files, '{name}{suffix}') + test_path = test_files / '{name}{suffix}' # bar has precedence over foo subject = load_name('foo', 'fake', 'bar', load_order=(test_path,)) @@ -444,7 +444,7 @@ def test_load_name_deprecated_extension(): def test_load_name_deprecated_extension_template(test_files): with ( - pytest.warns(DeprecationWarning, match='extension name in string template'), + pytest.warns(DeprecationWarning, match='using "{extension}" in string template'), patch('confidence.io.loadf', return_value=NotConfigured) as mocked_loadf, ): load_name( @@ -465,6 +465,12 @@ def test_load_name_deprecated_extension_template(test_files): ) +def test_load_name_incompatible_loader_type(test_files): + incompatible = bytes(str(test_files / 'config.yaml'), 'utf-8') + with pytest.raises(TypeError, match='source of type bytes'): + load_name('app', load_order=(test_files / 'bar.yaml', test_files / '{name}{suffix}', incompatible)) + + def test_dumps(): subject = dumps(Configuration({'ns.key': 42})) From 27fc0f88583cf30ed05b9c0b197d75da4efcaddb Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:07:42 +0200 Subject: [PATCH 3/4] Mention change in CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f649d86..fd00b1b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Changes development (main) ------------------ -- +- Enable loaders in `load_name`'s load order to be `pathlib.Path` objects, which get formatted like string template loaders (e.g. `'/path/to/{name}{suffix}'` will be equivalent to `Path('/path/to/{name}{suffix}')`). 0.17.2 (2025-11-25) ------------------- From b90f23c9e83d7915393e3e6479c2a62cdbc80467 Mon Sep 17 00:00:00 2001 From: Mattijs Ugen <144798+akaIDIOT@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:23:56 +0100 Subject: [PATCH 4/4] Use any for use of {extension} in path templates --- confidence/io.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/confidence/io.py b/confidence/io.py index d45af73..2ff3ae3 100644 --- a/confidence/io.py +++ b/confidence/io.py @@ -236,22 +236,22 @@ def loaders(*specifiers: Locality | Loadable) -> typing.Iterable[Loadable]: def _format_source(source: str | Path, name: str, format: Format) -> Path: match source: + case Path(): + # format every part of the path separately, filling in name and suffix + # NB: checking for use of {extension} is omitted here, use of Path templates was added after the deprecation + # of "extension" over "suffix" + return Path(*(part.format(name=name, suffix=format.suffix) for part in source.parts)) case str(): - # collect the field names in the format string, issue warning if "extension" is among them - if 'extension' in {span[1] for span in Formatter().parse(source)}: + # issue warning if "extension" is used as a field name (second element in the parsed spans) in source + if any(span[1] == 'extension' for span in Formatter().parse(source)): warnings.warn( 'using "{extension}" in string template loaders has been deprecated, use "{suffix}" instead', category=DeprecationWarning, - stacklevel=4, # warn about user code calling load_name rather than _resolve_source + stacklevel=4, # warn about user code calling load_name rather than _format_source ) return Path(source.format(name=name, extension=format.suffix.lstrip('.'))) else: return Path(source.format(name=name, suffix=format.suffix)) - case Path(): - # format every part of the path separately, filling in name and suffix - # NB: checking for use of {extension} is omitted here, use of Path templates was added after the deprecation - # of "extension" over "suffix" - return Path(*(part.format(name=name, suffix=format.suffix) for part in source.parts)) case _: # any other type of source is invalid here raise TypeError(f'cannot format source of type {type(source).__name__}')