Merge pull request #1397 from ynput/enhancement/local-settings-code-zen

Lib: Local settings code refactor
This commit is contained in:
Jakub Trllo 2025-07-31 14:43:58 +02:00 committed by GitHub
commit 978f6d4205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,6 +8,7 @@ import warnings
from datetime import datetime from datetime import datetime
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from functools import lru_cache from functools import lru_cache
from typing import Optional, Any
import platformdirs import platformdirs
import ayon_api import ayon_api
@ -15,26 +16,31 @@ import ayon_api
_PLACEHOLDER = object() _PLACEHOLDER = object()
# TODO should use 'KeyError' or 'Exception' as base
class RegistryItemNotFound(ValueError):
"""Raised when the item is not found in the keyring."""
class _Cache: class _Cache:
username = None username = None
def _get_ayon_appdirs(*args): def _get_ayon_appdirs(*args: str) -> str:
return os.path.join( return os.path.join(
platformdirs.user_data_dir("AYON", "Ynput"), platformdirs.user_data_dir("AYON", "Ynput"),
*args *args
) )
def get_ayon_appdirs(*args): def get_ayon_appdirs(*args: str) -> str:
"""Local app data directory of AYON client. """Local app data directory of AYON client.
Deprecated: Deprecated:
Use 'get_launcher_local_dir' or 'get_launcher_storage_dir' based on Use 'get_launcher_local_dir' or 'get_launcher_storage_dir' based on
use-case. Deprecation added 24/08/09 (0.4.4-dev.1). a use-case. Deprecation added 24/08/09 (0.4.4-dev.1).
Args: Args:
*args (Iterable[str]): Subdirectories/files in local app data dir. *args (Iterable[str]): Subdirectories/files in the local app data dir.
Returns: Returns:
str: Path to directory/file in local app data dir. str: Path to directory/file in local app data dir.
@ -52,7 +58,7 @@ def get_ayon_appdirs(*args):
def get_launcher_storage_dir(*subdirs: str) -> str: def get_launcher_storage_dir(*subdirs: str) -> str:
"""Get storage directory for launcher. """Get a storage directory for launcher.
Storage directory is used for storing shims, addons, dependencies, etc. Storage directory is used for storing shims, addons, dependencies, etc.
@ -77,14 +83,14 @@ def get_launcher_storage_dir(*subdirs: str) -> str:
def get_launcher_local_dir(*subdirs: str) -> str: def get_launcher_local_dir(*subdirs: str) -> str:
"""Get local directory for launcher. """Get a local directory for launcher.
Local directory is used for storing machine or user specific data. Local directory is used for storing machine or user-specific data.
The location is user specific. The location is user-specific.
Note: Note:
This function should be called at least once on bootstrap. This function should be called at least once on the bootstrap.
Args: Args:
*subdirs (str): Subdirectories relative to local dir. *subdirs (str): Subdirectories relative to local dir.
@ -101,7 +107,7 @@ def get_launcher_local_dir(*subdirs: str) -> str:
def get_addons_resources_dir(addon_name: str, *args) -> str: def get_addons_resources_dir(addon_name: str, *args) -> str:
"""Get directory for storing resources for addons. """Get a directory for storing resources for addons.
Some addons might need to store ad-hoc resources that are not part of Some addons might need to store ad-hoc resources that are not part of
addon client package (e.g. because of size). Studio might define addon client package (e.g. because of size). Studio might define
@ -111,7 +117,7 @@ def get_addons_resources_dir(addon_name: str, *args) -> str:
Args: Args:
addon_name (str): Addon name. addon_name (str): Addon name.
*args (str): Subfolders in resources directory. *args (str): Subfolders in the resources directory.
Returns: Returns:
str: Path to resources directory. str: Path to resources directory.
@ -134,9 +140,10 @@ class AYONSecureRegistry:
identify which data were created by AYON. identify which data were created by AYON.
Args: Args:
name(str): Name of registry used as identifier for data. name(str): Name of registry used as the identifier for data.
""" """
def __init__(self, name): def __init__(self, name: str) -> None:
try: try:
import keyring import keyring
@ -152,13 +159,12 @@ class AYONSecureRegistry:
keyring.set_keyring(Windows.WinVaultKeyring()) keyring.set_keyring(Windows.WinVaultKeyring())
# Force "AYON" prefix # Force "AYON" prefix
self._name = "/".join(("AYON", name)) self._name = f"AYON/{name}"
def set_item(self, name, value): def set_item(self, name: str, value: str) -> None:
# type: (str, str) -> None """Set sensitive item into the system's keyring.
"""Set sensitive item into system's keyring.
This uses `Keyring module`_ to save sensitive stuff into system's This uses `Keyring module`_ to save sensitive stuff into the system's
keyring. keyring.
Args: Args:
@ -172,22 +178,26 @@ class AYONSecureRegistry:
import keyring import keyring
keyring.set_password(self._name, name, value) keyring.set_password(self._name, name, value)
self.get_item.cache_clear()
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def get_item(self, name, default=_PLACEHOLDER): def get_item(
"""Get value of sensitive item from system's keyring. self, name: str, default: Any = _PLACEHOLDER
) -> Optional[str]:
"""Get value of sensitive item from the system's keyring.
See also `Keyring module`_ See also `Keyring module`_
Args: Args:
name (str): Name of the item. name (str): Name of the item.
default (Any): Default value if item is not available. default (Any): Default value if the item is not available.
Returns: Returns:
value (str): Value of the item. value (str): Value of the item.
Raises: Raises:
ValueError: If item doesn't exist and default is not defined. RegistryItemNotFound: If the item doesn't exist and default
is not defined.
.. _Keyring module: .. _Keyring module:
https://github.com/jaraco/keyring https://github.com/jaraco/keyring
@ -202,14 +212,12 @@ class AYONSecureRegistry:
if default is not _PLACEHOLDER: if default is not _PLACEHOLDER:
return default return default
# NOTE Should raise `KeyError` raise RegistryItemNotFound(
raise ValueError( f"Item {self._name}:{name} not found in keyring."
"Item {}:{} does not exist in keyring.".format(self._name, name)
) )
def delete_item(self, name): def delete_item(self, name: str) -> None:
# type: (str) -> None """Delete value stored in the system's keyring.
"""Delete value stored in system's keyring.
See also `Keyring module`_ See also `Keyring module`_
@ -227,47 +235,38 @@ class AYONSecureRegistry:
class ASettingRegistry(ABC): class ASettingRegistry(ABC):
"""Abstract class defining structure of **SettingRegistry** class. """Abstract class to defining structure of registry class.
It is implementing methods to store secure items into keyring, otherwise
mechanism for storing common items must be implemented in abstract
methods.
Attributes:
_name (str): Registry names.
""" """
def __init__(self, name: str) -> None:
def __init__(self, name):
# type: (str) -> ASettingRegistry
super(ASettingRegistry, self).__init__()
self._name = name self._name = name
self._items = {}
def set_item(self, name, value):
# type: (str, str) -> None
"""Set item to settings registry.
Args:
name (str): Name of the item.
value (str): Value of the item.
"""
self._set_item(name, value)
@abstractmethod @abstractmethod
def _set_item(self, name, value): def _get_item(self, name: str) -> Any:
# type: (str, str) -> None """Get item value from registry."""
# Implement it
pass
def __setitem__(self, name, value): @abstractmethod
self._items[name] = value def _set_item(self, name: str, value: str) -> None:
"""Set item value to registry."""
@abstractmethod
def _delete_item(self, name: str) -> None:
"""Delete item from registry."""
def __getitem__(self, name: str) -> Any:
return self._get_item(name)
def __setitem__(self, name: str, value: str) -> None:
self._set_item(name, value) self._set_item(name, value)
def get_item(self, name): def __delitem__(self, name: str) -> None:
# type: (str) -> str self._delete_item(name)
@property
def name(self) -> str:
return self._name
def get_item(self, name: str) -> str:
"""Get item from settings registry. """Get item from settings registry.
Args: Args:
@ -277,22 +276,22 @@ class ASettingRegistry(ABC):
value (str): Value of the item. value (str): Value of the item.
Raises: Raises:
ValueError: If item doesn't exist. RegistryItemNotFound: If the item doesn't exist.
""" """
return self._get_item(name) return self._get_item(name)
@abstractmethod def set_item(self, name: str, value: str) -> None:
def _get_item(self, name): """Set item to settings registry.
# type: (str) -> str
# Implement it
pass
def __getitem__(self, name): Args:
return self._get_item(name) name (str): Name of the item.
value (str): Value of the item.
def delete_item(self, name): """
# type: (str) -> None self._set_item(name, value)
def delete_item(self, name: str) -> None:
"""Delete item from settings registry. """Delete item from settings registry.
Args: Args:
@ -301,16 +300,6 @@ class ASettingRegistry(ABC):
""" """
self._delete_item(name) self._delete_item(name)
@abstractmethod
def _delete_item(self, name):
# type: (str) -> None
"""Delete item from settings."""
pass
def __delitem__(self, name):
del self._items[name]
self._delete_item(name)
class IniSettingRegistry(ASettingRegistry): class IniSettingRegistry(ASettingRegistry):
"""Class using :mod:`configparser`. """Class using :mod:`configparser`.
@ -318,20 +307,17 @@ class IniSettingRegistry(ASettingRegistry):
This class is using :mod:`configparser` (ini) files to store items. This class is using :mod:`configparser` (ini) files to store items.
""" """
def __init__(self, name: str, path: str) -> None:
def __init__(self, name, path): super().__init__(name)
# type: (str, str) -> IniSettingRegistry
super(IniSettingRegistry, self).__init__(name)
# get registry file # get registry file
self._registry_file = os.path.join(path, "{}.ini".format(name)) self._registry_file = os.path.join(path, f"{name}.ini")
if not os.path.exists(self._registry_file): if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg: with open(self._registry_file, mode="w") as cfg:
print("# Settings registry", cfg) print("# Settings registry", cfg)
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
print("# {}".format(now), cfg) print(f"# {now}", cfg)
def set_item_section(self, section, name, value): def set_item_section(self, section: str, name: str, value: str) -> None:
# type: (str, str, str) -> None
"""Set item to specific section of ini registry. """Set item to specific section of ini registry.
If section doesn't exists, it is created. If section doesn't exists, it is created.
@ -354,12 +340,10 @@ class IniSettingRegistry(ASettingRegistry):
with open(self._registry_file, mode="w") as cfg: with open(self._registry_file, mode="w") as cfg:
config.write(cfg) config.write(cfg)
def _set_item(self, name, value): def _set_item(self, name: str, value: str) -> None:
# type: (str, str) -> None
self.set_item_section("MAIN", name, value) self.set_item_section("MAIN", name, value)
def set_item(self, name, value): def set_item(self, name: str, value: str) -> None:
# type: (str, str) -> None
"""Set item to settings ini file. """Set item to settings ini file.
This saves item to ``DEFAULT`` section of ini as each item there This saves item to ``DEFAULT`` section of ini as each item there
@ -372,10 +356,9 @@ class IniSettingRegistry(ASettingRegistry):
""" """
# this does the some, overridden just for different docstring. # this does the some, overridden just for different docstring.
# we cast value to str as ini options values must be strings. # we cast value to str as ini options values must be strings.
super(IniSettingRegistry, self).set_item(name, str(value)) super().set_item(name, str(value))
def get_item(self, name): def get_item(self, name: str) -> str:
# type: (str) -> str
"""Gets item from settings ini file. """Gets item from settings ini file.
This gets settings from ``DEFAULT`` section of ini file as each item This gets settings from ``DEFAULT`` section of ini file as each item
@ -388,19 +371,18 @@ class IniSettingRegistry(ASettingRegistry):
str: Value of item. str: Value of item.
Raises: Raises:
ValueError: If value doesn't exist. RegistryItemNotFound: If value doesn't exist.
""" """
return super(IniSettingRegistry, self).get_item(name) return super().get_item(name)
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def get_item_from_section(self, section, name): def get_item_from_section(self, section: str, name: str) -> str:
# type: (str, str) -> str
"""Get item from section of ini file. """Get item from section of ini file.
This will read ini file and try to get item value from specified This will read ini file and try to get item value from specified
section. If that section or item doesn't exist, :exc:`ValueError` section. If that section or item doesn't exist,
is risen. :exc:`RegistryItemNotFound` is risen.
Args: Args:
section (str): Name of ini section. section (str): Name of ini section.
@ -410,7 +392,7 @@ class IniSettingRegistry(ASettingRegistry):
str: Item value. str: Item value.
Raises: Raises:
ValueError: If value doesn't exist. RegistryItemNotFound: If value doesn't exist.
""" """
config = configparser.ConfigParser() config = configparser.ConfigParser()
@ -418,16 +400,15 @@ class IniSettingRegistry(ASettingRegistry):
try: try:
value = config[section][name] value = config[section][name]
except KeyError: except KeyError:
raise ValueError( raise RegistryItemNotFound(
"Registry doesn't contain value {}:{}".format(section, name)) f"Registry doesn't contain value {section}:{name}"
)
return value return value
def _get_item(self, name): def _get_item(self, name: str) -> str:
# type: (str) -> str
return self.get_item_from_section("MAIN", name) return self.get_item_from_section("MAIN", name)
def delete_item_from_section(self, section, name): def delete_item_from_section(self, section: str, name: str) -> None:
# type: (str, str) -> None
"""Delete item from section in ini file. """Delete item from section in ini file.
Args: Args:
@ -435,7 +416,7 @@ class IniSettingRegistry(ASettingRegistry):
name (str): Name of the item. name (str): Name of the item.
Raises: Raises:
ValueError: If item doesn't exist. RegistryItemNotFound: If the item doesn't exist.
""" """
self.get_item_from_section.cache_clear() self.get_item_from_section.cache_clear()
@ -444,8 +425,9 @@ class IniSettingRegistry(ASettingRegistry):
try: try:
_ = config[section][name] _ = config[section][name]
except KeyError: except KeyError:
raise ValueError( raise RegistryItemNotFound(
"Registry doesn't contain value {}:{}".format(section, name)) f"Registry doesn't contain value {section}:{name}"
)
config.remove_option(section, name) config.remove_option(section, name)
# if section is empty, delete it # if section is empty, delete it
@ -461,29 +443,28 @@ class IniSettingRegistry(ASettingRegistry):
class JSONSettingRegistry(ASettingRegistry): class JSONSettingRegistry(ASettingRegistry):
"""Class using json file as storage.""" """Class using a json file as storage."""
def __init__(self, name, path): def __init__(self, name: str, path: str) -> None:
# type: (str, str) -> JSONSettingRegistry super().__init__(name)
super(JSONSettingRegistry, self).__init__(name) self._registry_file = os.path.join(path, f"{name}.json")
#: str: name of registry file
self._registry_file = os.path.join(path, "{}.json".format(name))
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
header = { header = {
"__metadata__": {"generated": now}, "__metadata__": {"generated": now},
"registry": {} "registry": {}
} }
if not os.path.exists(os.path.dirname(self._registry_file)): # Use 'os.path.dirname' in case someone uses slashes in 'name'
os.makedirs(os.path.dirname(self._registry_file), exist_ok=True) dirpath = os.path.dirname(self._registry_file)
if not os.path.exists(dirpath):
os.makedirs(dirpath, exist_ok=True)
if not os.path.exists(self._registry_file): if not os.path.exists(self._registry_file):
with open(self._registry_file, mode="w") as cfg: with open(self._registry_file, mode="w") as cfg:
json.dump(header, cfg, indent=4) json.dump(header, cfg, indent=4)
@lru_cache(maxsize=32) @lru_cache(maxsize=32)
def _get_item(self, name): def _get_item(self, name: str) -> str:
# type: (str) -> object """Get item value from the registry.
"""Get item value from registry json.
Note: Note:
See :meth:`ayon_core.lib.JSONSettingRegistry.get_item` See :meth:`ayon_core.lib.JSONSettingRegistry.get_item`
@ -494,29 +475,13 @@ class JSONSettingRegistry(ASettingRegistry):
try: try:
value = data["registry"][name] value = data["registry"][name]
except KeyError: except KeyError:
raise ValueError( raise RegistryItemNotFound(
"Registry doesn't contain value {}".format(name)) f"Registry doesn't contain value {name}"
)
return value return value
def get_item(self, name): def _set_item(self, name: str, value: str) -> None:
# type: (str) -> object """Set item value to the registry.
"""Get item value from registry json.
Args:
name (str): Name of the item.
Returns:
value of the item
Raises:
ValueError: If item is not found in registry file.
"""
return self._get_item(name)
def _set_item(self, name, value):
# type: (str, object) -> None
"""Set item value to registry json.
Note: Note:
See :meth:`ayon_core.lib.JSONSettingRegistry.set_item` See :meth:`ayon_core.lib.JSONSettingRegistry.set_item`
@ -528,41 +493,39 @@ class JSONSettingRegistry(ASettingRegistry):
cfg.truncate(0) cfg.truncate(0)
cfg.seek(0) cfg.seek(0)
json.dump(data, cfg, indent=4) json.dump(data, cfg, indent=4)
def set_item(self, name, value):
# type: (str, object) -> None
"""Set item and its value into json registry file.
Args:
name (str): name of the item.
value (Any): value of the item.
"""
self._set_item(name, value)
def _delete_item(self, name):
# type: (str) -> None
self._get_item.cache_clear() self._get_item.cache_clear()
def _delete_item(self, name: str) -> None:
with open(self._registry_file, "r+") as cfg: with open(self._registry_file, "r+") as cfg:
data = json.load(cfg) data = json.load(cfg)
del data["registry"][name] del data["registry"][name]
cfg.truncate(0) cfg.truncate(0)
cfg.seek(0) cfg.seek(0)
json.dump(data, cfg, indent=4) json.dump(data, cfg, indent=4)
self._get_item.cache_clear()
class AYONSettingsRegistry(JSONSettingRegistry): class AYONSettingsRegistry(JSONSettingRegistry):
"""Class handling AYON general settings registry. """Class handling AYON general settings registry.
Args: Args:
name (Optional[str]): Name of the registry. name (Optional[str]): Name of the registry. Using 'None' or not
""" passing name is deprecated.
def __init__(self, name=None): """
def __init__(self, name: Optional[str] = None) -> None:
if not name: if not name:
name = "AYON_settings" name = "AYON_settings"
warnings.warn(
(
"Used 'AYONSettingsRegistry' without 'name' argument."
" The argument will be required in future versions."
),
DeprecationWarning,
stacklevel=2,
)
path = get_launcher_storage_dir() path = get_launcher_storage_dir()
super(AYONSettingsRegistry, self).__init__(name, path) super().__init__(name, path)
def get_local_site_id(): def get_local_site_id():