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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ concurrency:
cancel-in-progress: true

jobs:
typecheck:
runs-on: "ubuntu-latest"
steps:
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# hatch-vcs needs the full history to resolve the version when uv
# builds the project environment
fetch-depth: 0
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0

- name: Run mypy
run: uv run --group typecheck mypy donfig

test:
runs-on: ${{ matrix.os }}
strategy:
Expand Down
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ repos:
# Use pyproject.toml if possible instead of adding command line parameters here
args: [ --warn-unused-configs ]
additional_dependencies:
# Keep in sync with the `typecheck` dependency group in pyproject.toml
# Type stubs
- types-docutils
- types-PyYAML
- types-setuptools
# Typed libraries
- pytest
- cloudpickle
- repo: https://github.com/scientific-python/cookie
rev: 2026.04.04
hooks:
Expand Down
23 changes: 12 additions & 11 deletions donfig/_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import uuid
from threading import Lock
from typing import Any
from weakref import WeakValueDictionary


Expand Down Expand Up @@ -42,38 +43,38 @@ class SerializableLock:
The creation of locks is itself not threadsafe.
"""

_locks: WeakValueDictionary = WeakValueDictionary()
_locks: WeakValueDictionary[str, Lock] = WeakValueDictionary()

def __init__(self, token=None):
def __init__(self, token: str | None = None) -> None:
self.token = token or str(uuid.uuid4())
if self.token in SerializableLock._locks:
self.lock = SerializableLock._locks[self.token]
else:
self.lock = Lock()
SerializableLock._locks[self.token] = self.lock

def acquire(self, *args, **kwargs):
def acquire(self, *args: Any, **kwargs: Any) -> bool:
return self.lock.acquire(*args, **kwargs)

def release(self, *args, **kwargs):
def release(self, *args: Any, **kwargs: Any) -> None:
return self.lock.release(*args, **kwargs)

def __enter__(self):
def __enter__(self) -> None:
self.lock.__enter__()

def __exit__(self, *args):
def __exit__(self, *args: Any) -> None:
self.lock.__exit__(*args)

def locked(self):
def locked(self) -> bool:
return self.lock.locked()

def __getstate__(self):
def __getstate__(self) -> str:
return self.token

def __setstate__(self, token):
self.__init__(token)
def __setstate__(self, token: str) -> None:
self.__init__(token) # type: ignore[misc]

def __str__(self):
def __str__(self) -> str:
return f"<{self.__class__.__name__}: {self.token}>"

__repr__ = __str__
82 changes: 44 additions & 38 deletions donfig/config_obj.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from collections.abc import Mapping, MutableMapping, Sequence
from contextlib import nullcontext
from copy import deepcopy
from types import TracebackType
from typing import Any, Literal

import yaml
Expand Down Expand Up @@ -48,11 +49,11 @@ def canonical_name(k: str, config: Mapping[str, Any]) -> str:


def update(
old: MutableMapping[str, Any],
old: dict[str, Any],
new: Mapping[str, Any],
priority: Literal["old", "new", "new-defaults"] = "new",
defaults: Mapping | None = None,
) -> Mapping[str, Any]:
defaults: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Update a nested dictionary with values from another

This is like dict.update except that it smoothly merges nested values
Expand Down Expand Up @@ -114,7 +115,7 @@ def update(
return old


def merge(*dicts: Mapping) -> dict:
def merge(*dicts: Mapping[str, Any]) -> dict[str, Any]:
"""Update a sequence of nested dictionaries

This prefers the values in the latter dictionaries to those in the former
Expand All @@ -131,13 +132,13 @@ def merge(*dicts: Mapping) -> dict:
donfig.config_obj.update

"""
result: dict = {}
result: dict[str, Any] = {}
for d in dicts:
update(result, d)
return result


def collect_yaml(paths: Sequence[str]) -> list[dict]:
def collect_yaml(paths: Sequence[str]) -> list[dict[str, Any]]:
"""Collect configuration from yaml files

This searches through a list of paths, expands to find all yaml or json
Expand Down Expand Up @@ -174,7 +175,7 @@ def collect_yaml(paths: Sequence[str]) -> list[dict]:
return configs


def _load_config_file(path: str) -> dict | None:
def _load_config_file(path: str) -> dict[str, Any] | None:
try:
with open(path) as f:
config = yaml.safe_load(f.read())
Expand All @@ -192,8 +193,8 @@ def _load_config_file(path: str) -> dict | None:


def collect_env(
prefix: str, env: Mapping[str, str] | None = None, deprecations: MutableMapping[str, str | None] | None = None
) -> dict:
prefix: str, env: Mapping[str, str] | None = None, deprecations: Mapping[str, str | None] | None = None
) -> dict[str, Any]:
"""Collect config from environment variables

This grabs environment variables of the form "DASK_FOO__BAR_BAZ=123" and
Expand Down Expand Up @@ -223,7 +224,7 @@ def collect_env(
except (SyntaxError, ValueError):
d[varname] = value

result: dict = {}
result: dict[str, Any] = {}
# fake thread lock to use set functionality
lock = nullcontext()
ConfigSet(result, lock, deprecations or {}, d)
Expand Down Expand Up @@ -252,16 +253,16 @@ class ConfigSet:

def __init__(
self,
config: MutableMapping,
lock: SerializableLock | contextlib.AbstractContextManager,
deprecations: MutableMapping[str, str | None],
arg: Mapping | None = None,
**kwargs,
):
config: MutableMapping[str, Any],
lock: SerializableLock | contextlib.AbstractContextManager[Any],
deprecations: Mapping[str, str | None],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm I wonder why this was a MutableMapping before. Good catch.

arg: Mapping[str, Any] | None = None,
**kwargs: Any,
) -> None:
with lock:
self.config = config
self.deprecations = deprecations
self._record: list[tuple[str, tuple, Any]] = []
self._record: list[tuple[Literal["replace", "insert"], tuple[str, ...], Any]] = []

if arg is not None:
for key, value in arg.items():
Expand All @@ -273,7 +274,7 @@ def __init__(
key = self._check_deprecations(key)
self._assign(key.split("."), value, config)

def _check_deprecations(self, key: str):
def _check_deprecations(self, key: str) -> str:
"""Check if the provided value has been renamed or removed.

Parameters
Expand All @@ -300,10 +301,15 @@ def _check_deprecations(self, key: str):
else:
return key

def __enter__(self):
def __enter__(self) -> MutableMapping[str, Any]:
return self.config

def __exit__(self, type, value, traceback):
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
for op, path, value in reversed(self._record):
d = self.config
if op == "replace":
Expand All @@ -323,7 +329,7 @@ def _assign(
self,
keys: Sequence[str],
value: Any,
d: MutableMapping,
d: MutableMapping[str, Any],
path: tuple[str, ...] = (),
record: bool = True,
) -> None:
Expand Down Expand Up @@ -363,7 +369,7 @@ def _assign(
self._assign(keys[1:], value, d[key], path, record=record)


def expand_environment_variables(config):
def expand_environment_variables(config: Any) -> Any:
"""Expand environment variables in a nested config dictionary

This function will recursively search through any nested dictionaries
Expand Down Expand Up @@ -443,24 +449,24 @@ def __init__(
self.defaults: list[Mapping[str, Any]] = list(defaults) if defaults is not None else []
self.deprecations = deprecations

self.config: MutableMapping[str, Any] = {}
self.config: dict[str, Any] = {}
self.config_lock = SerializableLock()
self.refresh()

def __contains__(self, item):
def __contains__(self, item: Any) -> bool:
try:
self[item]
return True
except (TypeError, IndexError, KeyError):
return False

def __getitem__(self, item):
def __getitem__(self, item: str) -> Any:
return self.get(item)

def pprint(self, **kwargs):
def pprint(self, **kwargs: Any) -> None:
return pprint.pprint(self.config, **kwargs)

def collect(self, paths: list[str] | None = None, env: Mapping[str, str] | None = None) -> dict:
def collect(self, paths: list[str] | None = None, env: Mapping[str, str] | None = None) -> dict[str, Any]:
Comment thread
maxrjones marked this conversation as resolved.
"""Collect configuration from paths and environment variables

Parameters
Expand All @@ -486,16 +492,16 @@ def collect(self, paths: list[str] | None = None, env: Mapping[str, str] | None
paths = self.paths
if env is None:
env = self.env
configs = []
configs: list[Mapping[str, Any]] = []

if yaml:
configs.extend(collect_yaml(paths=paths))
# yaml is a hard dependency, so its loader is always available.
configs.extend(collect_yaml(paths=paths))

configs.append(collect_env(self.env_prefix, env=env))

return merge(*configs)

def refresh(self, **kwargs) -> None:
def refresh(self, **kwargs: Any) -> None:
"""Update configuration by re-reading yaml files and env variables.

This goes through the following stages:
Expand Down Expand Up @@ -559,7 +565,7 @@ def get(self, key: str, default: Any = no_default) -> Any:
raise
return result

def update_defaults(self, new: Mapping) -> None:
def update_defaults(self, new: Mapping[str, Any]) -> None:
"""Add a new set of defaults to the configuration

It does two things:
Expand All @@ -574,7 +580,7 @@ def update_defaults(self, new: Mapping) -> None:
self.defaults.append(new)
update(self.config, new, priority="new-defaults", defaults=current_defaults)

def to_dict(self):
def to_dict(self) -> dict[str, Any]:
"""Return dictionary copy of configuration.

.. warning::
Expand All @@ -586,19 +592,19 @@ def to_dict(self):
"""
return deepcopy(self.config)

def clear(self):
def clear(self) -> None:
"""Clear all existing configuration."""
self.config.clear()

def merge(self, *dicts):
def merge(self, *dicts: Mapping[str, Any]) -> None:
"""Merge this configuration with multiple dictionaries.

See :func:`~donfig.config_obj.merge` for more information.

"""
self.config = merge(self.config, *dicts)

def update(self, new, priority="new"):
def update(self, new: Mapping[str, Any], priority: Literal["old", "new", "new-defaults"] = "new") -> None:
"""Update the internal configuration dictionary with `new`.

See :func:`~donfig.config_obj.update` for more information.
Expand All @@ -614,7 +620,7 @@ def expand_environment_variables(self) -> None:
"""
self.config = expand_environment_variables(self.config)

def rename(self, aliases: Mapping) -> None:
def rename(self, aliases: Mapping[str, str]) -> None:
"""Rename old keys to new keys

This helps migrate older configuration versions over time
Expand All @@ -633,7 +639,7 @@ def rename(self, aliases: Mapping) -> None:

self.set(new)

def set(self, arg=None, **kwargs):
def set(self, arg: Mapping[str, Any] | None = None, **kwargs: Any) -> ConfigSet:
"""Set configuration values within a context manager.

Parameters
Expand Down
Empty file added donfig/py.typed
Empty file.
Loading