Skip to content
85 changes: 61 additions & 24 deletions sublime_lib/settings_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
__all__ = ['SettingsDict', 'NamedSettingsDict']

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
from collections.abc import Iterable, Iterator, Mapping
from sublime_types import Value
from typing import Callable

Expand All @@ -22,33 +22,41 @@ class SettingsDict:
"""Wraps a :class:`sublime.Settings` object `settings`
with a :class:`dict`-like interface.

There is no way to list or iterate over the keys of a
:class:`~sublime.Settings` object. As a result, the following methods are
not implemented:
Since ST build 4075,
:class:`~sublime.Settings` objects directly implement some :class:`dict` methods,
notably :meth:`__getitem__`, :meth:`__setitem__`, and :meth:`__delitem__`,
which is enough for most use cases.
This class still provides polyfills for older builds
and additional functionality on top.

The ability to iterate over keys of
:class:`~sublime.Settings` objects is limited.
Additionally, :class:`~sublime.Settings`
objects behave similar to a :class:`collections.ChainMap`
in that deletions are only applied to the top-most settings source
and it is not possible to determine
whether a key is present in the top-most settings source
Thus, deletions do not necessarily
lead to the items actually getting deleted.

As a result,
method that involve iteration may behave unexpectedly
and the following methods are **not** implemented:

- :meth:`__len__`
- :meth:`__iter__`
- :meth:`clear`
- :meth:`copy`
- :meth:`items`
- :meth:`keys`
- :meth:`popitem`
- :meth:`values`

You can use :class:`collections.ChainMap` to chain a :class:`SettingsDict`
with other dict-like objects. If you do, calling the above unimplemented
methods on the :class:`~collections.ChainMap` will raise an error.
methods on the :class:`~collections.ChainMap` will also raise an error.
"""

NO_DEFAULT: NamedValue = _NO_DEFAULT

def __init__(self, settings: sublime.Settings):
self.settings: sublime.Settings = settings

def __iter__(self) -> None:
"""Raise NotImplementedError."""
raise NotImplementedError()

def __eq__(self, other: object) -> bool:
"""Return ``True`` if `self` and `other` are of the same type
and refer to the same underlying settings data.
Expand Down Expand Up @@ -89,7 +97,7 @@ def __setitem__(self, key: str, value: Value) -> None:
def __delitem__(self, key: str) -> None:
"""Remove `self[key]` from `self`.

:raise KeyError: if there us no setting with the given `key`.
:raise KeyError: if there is no setting with the given `key`.
"""
if key in self:
self.settings.erase(key)
Expand All @@ -110,7 +118,7 @@ def get(self, key: str, default: Value | None = None) -> Value:
def pop(self, key: str, default: Value | NamedValue = _NO_DEFAULT) -> Value:
"""Remove the setting `self[key]` and return its value or `default`.

:raise KeyError: if `key` is not in the dictionary
:raise KeyError: if `key` is not in the top-level settings object
and `default` is :attr:`SettingsDict.NO_DEFAULT`.

.. versionchanged:: 1.2
Expand All @@ -136,8 +144,9 @@ def setdefault(self, key: str, default: Value = None) -> Value:

def update(
self,
other: dict[str, Value] | Iterable[Iterable[str]] = [],
**kwargs: Value
other: dict[str, Value] | Iterable[tuple[str, Value]] = [],
/,
**kwargs: Value,
) -> None:
"""Update the dictionary with the key/value pairs from `other`,
overwriting existing keys.
Expand All @@ -148,14 +157,42 @@ def update(
the dictionary is then updated with those key/value pairs:
``self.update(red=1, blue=2)``.
"""
if isinstance(other, Mapping):
other = other.items() # type: ignore
if isinstance(other, dict):
for key in other.keys():
self[key] = other[key] # type: ignore
else:
for key, value in other:
self[key] = value

for key, value in other:
for key, value in kwargs.items():
self[key] = value

for key, value in kwargs.items(): # type: ignore
self[key] = value
def keys(self) -> Iterable[str]:
"""Return a set-like object providing a view on the setting object's keys.

.. versionadded:: 2.2
"""
return self.settings.to_dict().keys()

def values(self) -> Iterable[Value]:
"""Return a set-like object providing a view on the setting object's values.

.. versionadded:: 2.2
"""
return self.settings.to_dict().values()

def items(self) -> Iterable[tuple[str, Value]]:
"""Return a set-like object providing a view on the setting object's items.

.. versionadded:: 2.2
"""
return self.settings.to_dict().items()

def __iter__(self) -> Iterator[str]:
return iter(self.settings.to_dict())

def __len__(self) -> int:
return len(self.settings.to_dict())

def subscribe(
self,
Expand All @@ -167,7 +204,7 @@ def subscribe(
when the value derived from the settings object changes
and return a function that when invoked will unregister the callback.

Instead of passing the `SettingsDict` to callback,
Instead of passing the :class:`SettingsDict` to `callback`,
a value derived using `selector` is passed.
If `selector` is callable, then ``selector(self)`` is passed.
If `selector` is a :class:`str`,
Expand Down
Loading