Skip to content

Commit

Permalink
TMP
Browse files Browse the repository at this point in the history
  • Loading branch information
mih committed Sep 23, 2024
1 parent 6061ad8 commit 98e7949
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 118 deletions.
2 changes: 2 additions & 0 deletions datalad_next/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@
# order reflects precedence rule, first source with a
# key takes precedence
'environment': Environment(),
#'git-local': ...,
'git-global': GlobalGitConfig(),
'git-system': SystemGitConfig(),
#'datalad-branch': ...,
'defaults': defaults,
})

Expand Down
2 changes: 1 addition & 1 deletion datalad_next/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def get_validator_from_legacy_spec(
if default is UnsetValue and default_fn is not UnsetValue:
validator = DynamicDefaultConstraint(
default_fn,
type_ if type_ is not UnsetValue else NoConstraint,
type_ if type_ is not UnsetValue else NoConstraint(),
)
return validator

Expand Down
3 changes: 3 additions & 0 deletions datalad_next/config/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ def _load_legacy_overrides(self) -> dict[str, Any]:

def __str__(self):
return 'Environment'

def __repr__(self):
return 'Environment()'
31 changes: 25 additions & 6 deletions datalad_next/config/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
import re
from abc import abstractmethod
from pathlib import Path
from typing import (
TYPE_CHECKING,
)

if TYPE_CHECKING: # pragma: nocover
from os import PathLike

from datasalad.itertools import (
decode_bytes,
Expand All @@ -13,7 +19,10 @@

from datalad_next.config.item import ConfigurationItem
from datalad_next.config.source import ConfigurationSource
from datalad_next.runners import iter_git_subproc
from datalad_next.runners import (
call_git,
iter_git_subproc,
)

lgr = logging.getLogger('datalad.config')

Expand All @@ -40,7 +49,7 @@ def load(self) -> None:
fileset = set()

with iter_git_subproc(
[*self._get_git_config_cmd(), '--list'],
[*self._get_git_config_cmd(), '--show-origin', '--list', '-z'],
input=None,
cwd=cwd,
) as gitcfg:
Expand All @@ -62,29 +71,39 @@ def load(self) -> None:
self._sources = origin_paths.union(origin_blobs)

for k, v in dct.items():
self[k] = ConfigurationItem(
super().__setitem__(k, ConfigurationItem(
value=v,
store_target=self.__class__,
)
))

def __setitem__(self, key: str, value: ConfigurationItem) -> None:
call_git(
[*self._get_git_config_cmd(), '--replace-all', key, value.value],
)
super().__setitem__(key, value)


class SystemGitConfig(GitConfig):
def _get_git_config_cmd(self) -> list[str]:
return ['config', '--show-origin', '--system', '-z']
return ['config', '--system']

def _get_git_config_cwd(self) -> Path:
return Path.cwd()


class GlobalGitConfig(GitConfig):
def _get_git_config_cmd(self) -> list[str]:
return ['config', '--show-origin', '--global', '-z']
return ['config', '--global']

def _get_git_config_cwd(self) -> Path:
return Path.cwd()


class LocalGitConfig(GitConfig):
def __init__(self, path: PathLike):
super().__init__()
self._path = str(path)

def _get_git_config_cmd(self) -> list[str]:
return ['config', '--show-origin', '--local', '-z']

Expand Down
163 changes: 53 additions & 110 deletions datalad_next/config/legacy.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""`MultiConfiguration` adaptor for `ConfigManager` drop-in replacement"""

from __future__ import annotations

from types import MappingProxyType
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -13,7 +13,11 @@

from datalad_next.config import MultiConfiguration

from datalad_next.config.defaults import ImplementationDefault
from datalad_next.config.default import ImplementationDefault
from datalad_next.config.item import (
ConfigurationItem,
UnsetValue,
)


class ConfigManager:
Expand All @@ -30,11 +34,11 @@ def __init__(
self._mngr = _mngr
self._defaults = [
s for s in self._mngr.sources.values()
if isinstance(ImplementationDefault)
if isinstance(s, ImplementationDefault)
][-1]
# TODO: actually, these need really complex handling, because that
# container is manipulated directly in client code...
self.overrides = overrides
self._overrides = overrides

# TODO: make obsolete
self._repo_dot_git = None
Expand All @@ -48,113 +52,42 @@ def __init__(
self._repo_dot_git = dataset.repo.dot_git
self._repo_pathobj = dataset.repo.pathobj

@property
def overrides(self):
# this is a big hassle. the original class hands out the real dict to do any
# manipulation with it. for a transition we want to keep some control, and
# hand out a proxy only
if self._overrides is None:
self._overrides = {}
return MappingProxyType(self._overrides)

@property
def _stores(self):
# this beast only exists to satisfy a test that reaches into the
# internals (that no longer exists) and verifies that the right
# source files are read
files = set()
# only for tests
for label in ['git-system', 'git-global', 'git-local']:
src = self._mngr.sources.get(label)
if src is None:
continue
src.load()
files.update(src._sources)
return {'git': {'files': files}}

def reload(self, force: bool = False) -> None:
for s in self._mngr.sources.values():
s.load()

def obtain(self, var, default=None, dialog_type=None, valtype=None,
store=False, scope=None, reload=True, **kwargs):
# do local import, as this module is import prominently and the
# could theoretically import all kind of weird things for type
# conversion
from datalad.interface.common_cfg import definitions as cfg_defs

# fetch what we know about this variable
cdef = cfg_defs.get(var, {})
# type conversion setup
if valtype is None and 'type' in cdef:
valtype = cdef['type']
if valtype is None:
valtype = lambda x: x

# any default?
if default is None and 'default' in cdef:
default = cdef['default']

_value = None
if var in self:
# nothing needs to be obtained, it is all here already
_value = self[var]
elif store is False and default is not None:
# nothing will be stored, and we have a default -> no user confirmation
# we cannot use logging, because we want to use the config to configure
# the logging
#lgr.debug('using default {} for config setting {}'.format(default, var))
_value = default

if _value is not None:
# we got everything we need and can exit early
try:
return valtype(_value)
except Exception as e:
raise ValueError(
"value '{}' of existing configuration for '{}' cannot be "
"converted to the desired type '{}' ({})".format(
_value, var, valtype, e)) from e

# now we need to try to obtain something from the user
from datalad.ui import ui

# configure UI
dialog_opts = kwargs
if dialog_type is None: # no override
# check for common knowledge on how to obtain a value
if 'ui' in cdef:
dialog_type = cdef['ui'][0]
# pull standard dialog settings
dialog_opts = cdef['ui'][1]
# update with input
dialog_opts.update(kwargs)

if (not ui.is_interactive or dialog_type is None) and default is None:
raise RuntimeError(
"cannot obtain value for configuration item '{}', "
"not preconfigured, no default, no UI available".format(var))

if not hasattr(ui, dialog_type):
raise ValueError("UI '{}' does not support dialog type '{}'".format(
ui, dialog_type))

# configure storage destination, if needed
if store:
if scope is None and 'destination' in cdef:
scope = cdef['destination']
if scope is None:
raise ValueError(
"request to store configuration item '{}', but no "
"storage destination specified".format(var))

# obtain via UI
dialog = getattr(ui, dialog_type)
_value = dialog(default=default, **dialog_opts)

if _value is None:
# we got nothing
if default is None:
raise RuntimeError(
"could not obtain value for configuration item '{}', "
"not preconfigured, no default".format(var))
# XXX maybe we should return default here, even it was returned
# from the UI -- if that is even possible

# execute type conversion before storing to check that we got
# something that looks like what we want
try:
value = valtype(_value)
except Exception as e:
raise ValueError(
"cannot convert user input `{}` to desired type ({})".format(
_value, e)) from e
# XXX we could consider "looping" until we have a value of proper
# type in case of a user typo...

if store:
# store value as it was before any conversion, needs to be str
# anyway
# needs string conversion nevertheless, because default could come
# in as something else
self.add(var, '{}'.format(_value), scope=scope, reload=reload)
return value
# TODO: here is everything to run self._defaults[var] = ...
# if we happen to not find a default for this config.
# this is a trajectory for making the transition to having registered
# configs
#try:
return self[var]

def __repr__(self):
# give full list of all tracked config sources, plus overrides
Expand All @@ -176,8 +109,8 @@ def __len__(self) -> int:

def __getitem__(self, key: str) -> Any:
# use a custom default to discover unset values
val = self._mngr.getvalue(key, _Unset)
if val is _Unset:
val = self._mngr.getvalue(key, UnsetValue)
if val is UnsetValue:
# we do not actually have it
raise KeyError
return val
Expand Down Expand Up @@ -228,7 +161,13 @@ def add(self, var, value, scope='branch', reload=True):
raise NotImplementedError

def set(self, var, value, scope='branch', reload=True, force=False):
raise NotImplementedError
src_label = scope_label_to_source_label_map[scope]
src = self._mngr.sources[src_label]

breakpoint()
src[var] = ConfigurationItem(
value=value
)

def rename_section(self, old, new, scope='branch', reload=True):
raise NotImplementedError
Expand All @@ -240,5 +179,9 @@ def unset(self, var, scope='branch', reload=True):
raise NotImplementedError


class _Unset:
pass
scope_label_to_source_label_map = {
'branch': 'datalad-branch',
'local': 'git-local',
'global': 'git-global',
'override': 'environment',
}
2 changes: 1 addition & 1 deletion datalad_next/config/multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Any,
)

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: nocover
from datalad_next.config import (
ConfigurationItem,
ConfigurationSource,
Expand Down
File renamed without changes.
28 changes: 28 additions & 0 deletions datalad_next/config/tests/test_env.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
from ..env import Environment
from ..item import ConfigurationItem


def test_environment():
env = Environment()
assert str(env) == 'Environment'
assert repr(env) == 'Environment()'


def test_load_datalad_env(monkeypatch):
target_key = 'datalad.chunky-monkey.feedback'
target_value = 'ohmnomnom'
absurd_must_be_absent_key = 'nobody.would.use.such.a.key'
with monkeypatch.context() as m:
m.setenv('DATALAD_CHUNKY__MONKEY_FEEDBACK', 'ohmnomnom')
env = Environment()
assert target_key in env.keys() # noqa: SIM118
assert target_key in env
assert env.get(target_key).value == target_value
# default is wrapped into ConfigurationItem if needed
assert env.get(
absurd_must_be_absent_key,
target_value
).value is target_value
assert env.get(
absurd_must_be_absent_key,
ConfigurationItem(value=target_value)
).value is target_value
assert env.getvalue(target_key) == target_value
assert env.getvalue(absurd_must_be_absent_key) is None
assert len(env)


def test_load_legacy_overrides(monkeypatch):
Expand All @@ -23,3 +43,11 @@ def test_load_legacy_overrides(monkeypatch):
assert env.getvalue('datalad.key1') == 'evenmoreoverride'
assert env.getvalue('annex.key2') == 'override'

with monkeypatch.context() as m:
m.setenv(
'DATALAD_CONFIG_OVERRIDES_JSON',
'{"datalad.key1":NOJSON, "annex.key2":"override"}',
)
env = Environment()
assert 'datalad.key1' not in env
assert 'annex.key2' not in env
Loading

0 comments on commit 98e7949

Please sign in to comment.