Skip to content

Commit

Permalink
Cleanup and document settings API (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
analog-cbarber committed Apr 22, 2024
1 parent 9c3de73 commit 022db41
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 25 deletions.
148 changes: 125 additions & 23 deletions src/whl2conda/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@
__all__ = ["Whl2CondaSettings", "settings"]


class FieldDefault(NamedTuple):
class _FieldDefault(NamedTuple):
factory: Callable


class StringConversionField:
class _SettingsField:
"""
Base class for Whl2CondaSettings dataclass
"""

def __init__(self, *, default):
if callable(default):
self._default = FieldDefault(default)
self._default = _FieldDefault(default)
else:
self._default = default

Expand All @@ -55,7 +59,7 @@ def __get__(self, obj, type):
return getattr(obj, self._name)

def __set__(self, obj, value):
if isinstance(value, FieldDefault):
if isinstance(value, _FieldDefault):
value = value.factory()
elif isinstance(value, str):
value = self._convert_from_str(value)
Expand All @@ -65,10 +69,19 @@ def __delete__(self, obj):
self.__set__(obj, self._default)

def _convert_from_str(self, value: str):
"""
Maybe overridden to support conversion from string (from command line)
"""
return value


class BoolField(StringConversionField):
class _BoolField(_SettingsField):
"""
Boolean valued field.
Supports string conversion from true/false, yes/no, y/n
"""

def __init__(self, *, default: bool = False):
super().__init__(default=default)

Expand All @@ -82,36 +95,66 @@ def _convert_from_str(self, value: str):
raise ValueError(f"Invalid value {value!r} for bool field")


class CondaPackageFormatField(StringConversionField):
class _CondaPackageFormatField(_SettingsField):
"""
CondaPackageFormat valued field
"""

def __init__(self):
super().__init__(default=CondaPackageFormat.V2)

def _convert_from_str(self, value: str):
return CondaPackageFormat.from_string(value)


def toidentifier(name: str) -> str:
def _toidentifier(name: str) -> str:
"""
Convert name to an identifier (dashes to underscores)
"""
return name.replace("-", "_")


def fromidentifier(name: str) -> str:
def _fromidentifier(name: str) -> str:
"""
Convert name from identifier (underscores to dashes)
"""
return name.replace("_", "-")


@dataclasses.dataclass(kw_only=True)
class Whl2CondaSettings:
"""
User settings for whl2conda.
These are accessed through the global [settings][(m).] variable.
"""

SETTINGS_FILENAME: ClassVar[str] = "whl2conda.json"
"""Default base filename for saved settings."""

DEFAULT_SETTINGS_FILE: ClassVar[Path] = user_config_path() / SETTINGS_FILENAME
"""Default filepath for saved settings."""

# TODO:
# - difftool
# - pyproject defaults

auto_update_std_renames: BoolField = BoolField()
auto_update_std_renames: _BoolField = _BoolField()
"""
Whether to automatically update the standard renames for operations
that need them. Default is false.
"""

conda_format: CondaPackageFormatField = CondaPackageFormatField()
conda_format: _CondaPackageFormatField = _CondaPackageFormatField()
"""
The default output conda package format if not specified. Default is V2.
"""

pypi_indexes: StringConversionField = StringConversionField(default=dict)
pypi_indexes: _SettingsField = _SettingsField(default=dict)
"""
Dictionary of aliases for pypi package indexes from which wheels can be
downloaded. Default is empty.
"""

#
# Internal attributes
Expand All @@ -120,8 +163,14 @@ class Whl2CondaSettings:
_settings_file: Path = dataclasses.field(
default=DEFAULT_SETTINGS_FILE, compare=False
)
"""
Location of underlying settings file.
"""

_fieldnames: ClassVar[frozenset[str]] = frozenset()
"""
Set of public field names.
"""

@property
def settings_file(self) -> Path:
Expand All @@ -133,12 +182,27 @@ def settings_file(self) -> Path:
"""
return self._settings_file

#
# Settings access/modification methods
#

def to_dict(self) -> dict[str, Any]:
"""
Return dictionary containing public settings data.
"""
return {
k: v for k, v in dataclasses.asdict(self).items() if not k.startswith("_")
}

def get(self, key: str) -> Any:
"""
Get a value from the settings by string key.
The key may either be just the field name (e.g. 'conda-format')
or can refer to am entry within dictionary-valued field
(e.g. 'pypi-indexes.acme'). Note that the dashes in the first
component of the key will be converted to underscores.
"""
name, subkey = self._split_key(key)
value = getattr(self, name)

Expand All @@ -154,6 +218,13 @@ def get(self, key: str) -> Any:
return value

def set(self, key: str, value: Any) -> None:
"""
Set a value in the settings by string key.
See [get][..] for details on key format.
This does not save the settings file.
"""
name, subkey = self._split_key(key)

if not subkey:
Expand All @@ -172,6 +243,10 @@ def unset(self, key: str) -> None:
Unset attribute with given key.
The setting will revert to its original value.
See [get][..] for details on key format.
This does not save the settings file.
"""
name, subkey = self._split_key(key)

Expand All @@ -189,20 +264,39 @@ def unset(self, key: str) -> None:
except KeyError:
pass

def _split_key(self, key: str) -> tuple[str, str]:
parts = key.split(".", maxsplit=1)
name = toidentifier(parts[0])
if name not in self._fieldnames:
raise KeyError(f"Unknown settings key '{key}'")
return name, parts[1] if len(parts) > 1 else ""
def unset_all(self) -> None:
"""
Unset all settings, and revert to default values.
"""
for k in self._fieldnames:
self.unset(k)

#
# File operations
#

@classmethod
def from_file(cls, filename: Union[Path, str] = "") -> Whl2CondaSettings:
"""
Return settings read from file.
Arguments:
filename: relative path to settings file (may start with '~')
defaults to [DEFAULT_SETTINGS_FILE][(c).] if not specified.
"""
settings = cls()
settings.load(filename or cls.DEFAULT_SETTINGS_FILE)
return settings

def load(self, filename: Union[Path, str], reset_all: bool = False) -> None:
"""
Reload settings from file
Args:
filename: relative path to settings file (may start with '~')
reset_all: if True, then all settings will be unset and reverted
to default value prior to loading.
"""
filepath = Path(Path(filename).expanduser())
self._settings_file = filepath
if reset_all:
Expand All @@ -217,6 +311,7 @@ def load(self, filename: Union[Path, str], reset_all: bool = False) -> None:
def save(self, filename: Union[Path, str] = "") -> None:
"""
Write settings to specified file in JSON format.
Args:
filename: file to write. Defaults to [settings_file][..]
"""
Expand All @@ -226,16 +321,23 @@ def save(self, filename: Union[Path, str] = "") -> None:
json_obj["$created"] = str(dt.datetime.now())
filepath.write_text(json.dumps(json_obj, indent=2))

def unset_all(self) -> None:
"""
Unset all settings, and revert to default values.
"""
for k in self._fieldnames:
self.unset(k)
#
# Internal methods
#

def _split_key(self, key: str) -> tuple[str, str]:
parts = key.split(".", maxsplit=1)
name = _toidentifier(parts[0])
if name not in self._fieldnames:
raise KeyError(f"Unknown settings key '{key}'")
return name, parts[1] if len(parts) > 1 else ""


Whl2CondaSettings._fieldnames = frozenset(
f.name for f in dataclasses.fields(Whl2CondaSettings) if not f.name.startswith("_")
)

settings = Whl2CondaSettings.from_file()
"""
User settings.
"""
4 changes: 2 additions & 2 deletions test/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import pytest

from whl2conda.impl.pyproject import CondaPackageFormat
from whl2conda.settings import Whl2CondaSettings, fromidentifier
from whl2conda.settings import Whl2CondaSettings, _fromidentifier

# ruff: noqa: F811

Expand Down Expand Up @@ -150,7 +150,7 @@ def check_settings(settings: Whl2CondaSettings, tmp_path: Path) -> None:
assert settings == settings2

for name in Whl2CondaSettings._fieldnames:
assert settings.get(fromidentifier(name)) == getattr(settings, name)
assert settings.get(_fromidentifier(name)) == getattr(settings, name)

settings_dict = settings.to_dict()

Expand Down

0 comments on commit 022db41

Please sign in to comment.