Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.9', 'pypy-3.10', 'pypy-3.11']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.10', 'pypy-3.11']
steps:
- uses: actions/checkout@v5
- uses: pdm-project/setup-pdm@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ dmypy.json

# PyPI configuration file
.pypirc

# PyCharm
.idea/
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changes
development (main)
------------------

- Drop support for Python 3.9.
- Introduce `confidence.Format` with two concrete implementations: `confidence.JSON` and `confidence.YAML`, which can be customized before use (e.g. `format = YAML(suffix='.yml')`).
- **Deprecate** the use of `extension` argument to loading functions and `encoding` argument to dumping functions, both can be controlled with a `confidence.Format`.

Expand Down
6 changes: 2 additions & 4 deletions confidence/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def load(self, fp: typing.TextIO) -> typing.Any:
def loads(self, string: str) -> typing.Any:
raise NotImplementedError

def loadf(self, fpath: typing.Union[str, PathLike], encoding: typing.Optional[str] = None) -> typing.Any:
def loadf(self, fpath: str | PathLike, encoding: str | None = None) -> typing.Any:
with Path(fpath).open('rt', encoding=encoding or self.encoding) as fp:
return self.load(fp)

Expand All @@ -43,9 +43,7 @@ def dump(self, value: typing.Any, fp: typing.TextIO) -> None:
def dumps(self, value: typing.Any) -> str:
raise NotImplementedError

def dumpf(
self, value: typing.Any, fname: typing.Union[str, PathLike], encoding: typing.Optional[str] = None
) -> None:
def dumpf(self, value: typing.Any, fname: str | PathLike, encoding: str | None = None) -> None:
with Path(fname).open('wt', encoding=encoding or self.encoding) as fp:
return self.dump(value, fp)

Expand Down
39 changes: 34 additions & 5 deletions confidence/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import re
import typing
import warnings
from collections.abc import Sequence
from dataclasses import dataclass
from enum import IntEnum
from functools import partial
from itertools import product
Expand Down Expand Up @@ -59,6 +61,33 @@ def read_xdg_config_home(name: str, format: Format = YAML) -> Configuration:
return loadf(config_home / f'{name}{format.suffix}', format=format, default=NotConfigured)


@dataclass(frozen=True)
class PathGlobReader:
path: Path
pattern: str
case_sensitive: bool | None = None
include_hidden: bool = False

def expand(self, name: str, format: Format = YAML) -> Sequence[Path]:
# TODO: maybe formatting al the steps of the path separately is better / safer?
path = Path(str(self.path).format(name=name, suffix=format.suffix))
pattern = self.pattern.format(name=name, suffix=format.suffix)
LOG.debug(f'expanded "{self.path / self.pattern!s}" to "{path / pattern!s}" for {name=} and {format=}')

# TODO: use self.include_hidden
paths = sorted(path.glob(pattern, case_sensitive=self.case_sensitive))
LOG.debug(f'glob pattern "{path / pattern!s}" matched {len(paths)} paths')
return paths

def __call__(self, name: str, format: Format = YAML) -> Configuration:
if paths := self.expand(name, format):
# provide no default here, glob pattern does match files, these should be loadable
return loadf(*paths, format=format)
else:
# no paths, empty configuration
return NotConfigured


def read_envvars(name: str, format: Format = YAML) -> Configuration:
"""
Read environment variables starting with ``NAME_``, where subsequent
Expand Down Expand Up @@ -157,7 +186,7 @@ class Locality(IntEnum):
ENVIRONMENT = 3 #: configuration from environment variables


Loadable = typing.Union[str, typing.Callable[[str, Format], Configuration]]
Loadable = str | typing.Callable[[str, Format], Configuration]


_LOADERS: typing.Mapping[Locality, typing.Iterable[Loadable]] = {
Expand Down Expand Up @@ -190,7 +219,7 @@ class Locality(IntEnum):
}


def loaders(*specifiers: typing.Union[Locality, Loadable]) -> typing.Iterable[Loadable]:
def loaders(*specifiers: Locality | Loadable) -> typing.Iterable[Loadable]:
"""
Generates loaders in the specified order.

Expand Down Expand Up @@ -247,7 +276,7 @@ def load(*fps: typing.TextIO, format: Format = YAML, missing: typing.Any = Missi


def loadf(
*fnames: typing.Union[str, PathLike],
*fnames: str | PathLike,
format: Format = YAML,
default: typing.Any = NoDefault,
missing: typing.Any = Missing.SILENT,
Expand Down Expand Up @@ -343,7 +372,7 @@ def generate_sources() -> typing.Iterable[typing.Mapping[str, typing.Any]]:
return Configuration(*generate_sources(), missing=missing)


def _check_format_encoding(format: Format, encoding: typing.Optional[str]) -> Format:
def _check_format_encoding(format: Format, encoding: str | None) -> Format:
if encoding:
if format is YAML:
warnings.warn(
Expand Down Expand Up @@ -373,7 +402,7 @@ def dump(

def dumpf(
value: typing.Any,
fname: typing.Union[str, PathLike],
fname: str | PathLike,
format: Format = YAML,
encoding: None = None, # NB: parameter is deprecated, see _check_format_encoding
) -> None:
Expand Down
14 changes: 7 additions & 7 deletions confidence/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def get(
path: str,
default: typing.Any = None,
*,
as_type: typing.Optional[typing.Callable] = None,
as_type: typing.Callable | None = None,
resolve_references: bool = True,
) -> typing.Any:
"""
Expand Down Expand Up @@ -194,7 +194,7 @@ def get(
elif isinstance(value, Mapping):
# wrap value in a Configuration
return self._wrap(value)
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
elif isinstance(value, Sequence) and not isinstance(value, str | bytes):
# wrap value in a sequence that retains Configuration functionality
return ConfigurationSequence(value, self._root)
elif resolve_references and isinstance(value, str):
Expand Down Expand Up @@ -338,13 +338,13 @@ def __init__(self, source: typing.Sequence, root: Configuration):
self._source = source
self._root = root

def __getitem__(self, item: typing.Union[int, slice], *, resolve_references: bool = True) -> typing.Any:
def __getitem__(self, item: int | slice, *, resolve_references: bool = True) -> typing.Any:
# retrieve value of interest (NB: item can be a slice, but we'll let _source take care of that)
value = self._source[item]
if isinstance(value, Mapping):
# let root wrap the value
return self._root._wrap(value)
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
if isinstance(value, Sequence) and not isinstance(value, str | bytes):
# wrap a sequence value with an 'instance of self'
return type(self)(value, self._root)
if isinstance(value, str) and resolve_references:
Expand All @@ -359,7 +359,7 @@ def __len__(self) -> int:
return len(self._source)

def __add__(self, other: typing.Sequence[typing.Any]) -> 'ConfigurationSequence':
if not isinstance(other, Sequence) or isinstance(other, (str, bytes)):
if not isinstance(other, Sequence) or isinstance(other, str | bytes):
# incompatible types, let Python resolve an action for this, like calling other.__radd__ or raising a
# TypeError
return NotImplemented
Expand All @@ -369,7 +369,7 @@ def __add__(self, other: typing.Sequence[typing.Any]) -> 'ConfigurationSequence'
return type(self)(list(self._source) + list(other), root=self._root)

def __radd__(self, other: typing.Sequence) -> typing.Sequence:
if not isinstance(other, Sequence) or isinstance(other, (str, bytes)):
if not isinstance(other, Sequence) or isinstance(other, str | bytes):
# incompatible types, let Python resolve an action for this
return NotImplemented

Expand All @@ -394,7 +394,7 @@ def _repr_value(value: typing.Any) -> str:
if isinstance(value, Mapping):
keys = ', '.join(_repr_value(key) for key in value)
return f'mapping(keys=[{keys}])'
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
if isinstance(value, Sequence) and not isinstance(value, str | bytes):
return 'sequence([...])'

# fall back to builtin repr
Expand Down
6 changes: 3 additions & 3 deletions confidence/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Conflict(IntEnum):
def merge_into(
left: typing.MutableMapping[str, typing.Any],
right: typing.Mapping[str, typing.Any],
path: typing.Optional[list[str]] = None,
path: list[str] | None = None,
conflict: Conflict = Conflict.ERROR,
) -> typing.Mapping[str, typing.Any]:
"""
Expand Down Expand Up @@ -60,7 +60,7 @@ def merge_into(

def split_keys(
mapping: typing.Mapping[str, typing.Any],
colliding: typing.Optional[typing.Container] = None,
colliding: typing.Container | None = None,
) -> typing.Mapping[str, typing.Any]:
"""
Recursively walks *mapping* to split keys that contain a dot into nested
Expand Down Expand Up @@ -110,7 +110,7 @@ def split_keys(
def merge(
left: typing.MutableMapping[str, typing.Any],
right: typing.Mapping[str, typing.Any],
path: typing.Optional[list[str]] = None,
path: list[str] | None = None,
conflict: Conflict = Conflict.ERROR,
) -> typing.Mapping[str, typing.Any]:
warnings.warn(
Expand Down
Loading
Loading