generated from datalad/datalad-extension-template
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: sketch of a new configuration manager
This will eventually fix #397
- Loading branch information
Showing
16 changed files
with
918 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import ( | ||
Any, | ||
Callable, | ||
) | ||
|
||
# momentarily needed for the legacy_register_config() | ||
# implementation. | ||
from datalad.interface.common_cfg import definitions # type: ignore | ||
from datalad.support.extensions import ( # type: ignore | ||
register_config as _legacy_register_config, | ||
) | ||
from datasalad.settings import Defaults | ||
|
||
from datalad_next.config.dialog import get_dialog_class_from_legacy_ui_label | ||
from datalad_next.config.item import ( | ||
ConfigurationItem, | ||
UnsetValue, | ||
) | ||
from datalad_next.constraints import ( | ||
Constraint, | ||
EnsureNone, | ||
) | ||
|
||
lgr = logging.getLogger('datalad.config') | ||
|
||
|
||
class ImplementationDefault(Defaults): | ||
def __str__(self): | ||
return 'ImplementationDefaults' | ||
|
||
|
||
# | ||
# legacy support tooling from here. | ||
# non of this is executed by the code above. It has to be triggered manually | ||
# and pointed to an instance of ImplementationDefaults | ||
# | ||
|
||
def load_legacy_defaults(source: ImplementationDefault) -> None: | ||
for name, cfg in definitions.items(): | ||
if 'default' not in cfg: | ||
lgr.debug( | ||
'Configuration %r has no default(_fn), not registering', | ||
name | ||
) | ||
continue | ||
|
||
cfg_props = cfg._props | ||
ui = cfg_props.get('ui', None) | ||
if ui is not None: | ||
dialog = get_dialog_class_from_legacy_ui_label(ui[0])( | ||
title=ui[1]['title'], | ||
text=ui[1].get('text', ''), | ||
) | ||
else: | ||
dialog = None | ||
|
||
coercer = cfg_props.get('type') | ||
if name == 'datalad.tests.temp.dir': | ||
# https://github.com/datalad/datalad/issues/7662 | ||
coercer = coercer | EnsureNone() | ||
|
||
default = cfg_props.get('default', UnsetValue) | ||
default_fn = cfg_props.get('default_fn') | ||
|
||
source[name] = ConfigurationItem( | ||
default_fn if default_fn else default, | ||
validator=coercer, | ||
lazy=default_fn is not None, | ||
dialog=dialog, | ||
store_target=get_store_target_from_destination_label( | ||
cfg_props.get('destination'), | ||
), | ||
) | ||
|
||
|
||
def legacy_register_config( | ||
source: ImplementationDefault, | ||
name: str, | ||
title: str, | ||
*, | ||
default: Any = UnsetValue, | ||
default_fn: Callable | type[UnsetValue] = UnsetValue, | ||
description: str | None = None, | ||
type: Constraint | None = None, # noqa: A002 | ||
dialog: str | None = None, | ||
scope: str | type[UnsetValue] = UnsetValue, | ||
): | ||
source[name] = ConfigurationItem( | ||
default_fn if default_fn else default, | ||
validator=type, | ||
lazy=default_fn is not None, | ||
dialog=None if dialog is None | ||
else get_dialog_class_from_legacy_ui_label(dialog)( | ||
title=title, | ||
text=description or '', | ||
), | ||
store_target=get_store_target_from_destination_label(scope), | ||
) | ||
|
||
# lastly trigger legacy registration | ||
_legacy_register_config( | ||
name=name, | ||
title=title, | ||
default=default, | ||
default_fn=default_fn, | ||
description=description, | ||
type=type, | ||
dialog=dialog, | ||
scope=scope, | ||
) | ||
|
||
|
||
def get_store_target_from_destination_label( | ||
label: str | UnsetValue | None, | ||
) -> str | None: | ||
if label in (None, UnsetValue): | ||
return None | ||
if label == 'global': | ||
return 'GlobalGitConfig' | ||
if label == 'local': | ||
return 'LocalGitConfig' | ||
if label == 'dataset': | ||
return 'DatasetBranchConfig' | ||
msg = f'unsupported configuration destination label {label!r}' | ||
raise ValueError(msg) | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from __future__ import annotations | ||
|
||
from dataclasses import dataclass | ||
|
||
__all__ = [ | ||
'Dialog', | ||
'Question', | ||
'YesNo', | ||
'Choice', | ||
] | ||
|
||
|
||
# only from PY3.10 | ||
# @dataclass(kw_only=True) | ||
@dataclass | ||
class Dialog: | ||
title: str | ||
text: str | ||
|
||
|
||
@dataclass | ||
class Question(Dialog): | ||
pass | ||
|
||
|
||
@dataclass | ||
class YesNo(Dialog): | ||
pass | ||
|
||
|
||
@dataclass | ||
class Choice(Dialog): | ||
pass | ||
|
||
|
||
def get_dialog_class_from_legacy_ui_label(label: str) -> type[Dialog]: | ||
"""Recode legacy `datalad.interface.common_cfg` UI type label""" | ||
if label == 'yesno': | ||
return YesNo | ||
elif label == 'question': | ||
return Question | ||
else: | ||
msg = f'unknown UI type label {label!r}' | ||
raise ValueError(msg) | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
from __future__ import annotations | ||
|
||
import json | ||
import logging | ||
from os import environ | ||
from typing import Any | ||
|
||
from datasalad.settings import CachingSource | ||
|
||
from datalad_next.config.item import ConfigurationItem | ||
|
||
lgr = logging.getLogger('datalad.config') | ||
|
||
|
||
class LegacyEnvironment(CachingSource): | ||
""" | ||
All loaded items have a ``store_target`` of ``Environment``, assuming | ||
that if they are loaded from the environment, a modification can | ||
also target the environment again. | ||
""" | ||
is_writable = False | ||
|
||
def load(self) -> None: | ||
# not resetting here, incremental load | ||
for k, v in self._load_legacy_overrides().items(): | ||
self._items[k] = ConfigurationItem(value=v) | ||
for k in environ: | ||
if not k.startswith('DATALAD_'): | ||
continue | ||
# translate variable name to config item key | ||
item_key = k.replace('__', '-').replace('_', '.').lower() | ||
self._items[item_key] = ConfigurationItem(value=environ[k]) | ||
|
||
def _load_legacy_overrides(self) -> dict[str, Any]: | ||
try: | ||
return { | ||
str(k): v | ||
for k, v in json.loads( | ||
environ.get("DATALAD_CONFIG_OVERRIDES_JSON", '{}') | ||
).items() | ||
} | ||
except json.decoder.JSONDecodeError as exc: | ||
lgr.warning( | ||
"Failed to load DATALAD_CONFIG_OVERRIDES_JSON: %s", | ||
exc, | ||
) | ||
return {} | ||
|
||
def __str__(self): | ||
return 'LegacyEnvironment' | ||
|
||
def __repr__(self): | ||
return 'LegacyEnvironment()' |
Oops, something went wrong.