# -*- coding: utf-8 -*- """Package to deal with saving and retrieving user specific settings.""" import os from datetime import datetime from abc import ABCMeta, abstractmethod import json # disable lru cache in Python 2 try: from functools import lru_cache except ImportError: def lru_cache(maxsize): def max_size(func): def wrapper(*args, **kwargs): value = func(*args, **kwargs) return value return wrapper return max_size # ConfigParser was renamed in python3 to configparser try: import configparser except ImportError: import ConfigParser as configparser import platform import six import appdirs _PLACEHOLDER = object() class OpenPypeSecureRegistry: """Store information using keyring. Registry should be used for private data that should be available only for user. All passed registry names will have added prefix `OpenPype/` to easier identify which data were created by OpenPype. Args: name(str): Name of registry used as identifier for data. """ def __init__(self, name): try: import keyring except Exception: raise NotImplementedError( "Python module `keyring` is not available." ) # hack for cx_freeze and Windows keyring backend if platform.system().lower() == "windows": from keyring.backends import Windows keyring.set_keyring(Windows.WinVaultKeyring()) # Force "OpenPype" prefix self._name = "/".join(("OpenPype", name)) def set_item(self, name, value): # type: (str, str) -> None """Set sensitive item into system's keyring. This uses `Keyring module`_ to save sensitive stuff into system's keyring. Args: name (str): Name of the item. value (str): Value of the item. .. _Keyring module: https://github.com/jaraco/keyring """ import keyring keyring.set_password(self._name, name, value) @lru_cache(maxsize=32) def get_item(self, name, default=_PLACEHOLDER): """Get value of sensitive item from system's keyring. See also `Keyring module`_ Args: name (str): Name of the item. default (Any): Default value if item is not available. Returns: value (str): Value of the item. Raises: ValueError: If item doesn't exist and default is not defined. .. _Keyring module: https://github.com/jaraco/keyring """ import keyring value = keyring.get_password(self._name, name) if value: return value if default is not _PLACEHOLDER: return default # NOTE Should raise `KeyError` raise ValueError( "Item {}:{} does not exist in keyring.".format(self._name, name) ) def delete_item(self, name): # type: (str) -> None """Delete value stored in system's keyring. See also `Keyring module`_ Args: name (str): Name of the item to be deleted. .. _Keyring module: https://github.com/jaraco/keyring """ import keyring self.get_item.cache_clear() keyring.delete_password(self._name, name) @six.add_metaclass(ABCMeta) class ASettingRegistry(): """Abstract class defining structure of **SettingRegistry** 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): # type: (str) -> ASettingRegistry super(ASettingRegistry, self).__init__() 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 def _set_item(self, name, value): # type: (str, str) -> None # Implement it pass def __setitem__(self, name, value): self._items[name] = value self._set_item(name, value) def get_item(self, name): # type: (str) -> str """Get item from settings registry. Args: name (str): Name of the item. Returns: value (str): Value of the item. Raises: ValueError: If item doesn't exist. """ return self._get_item(name) @abstractmethod def _get_item(self, name): # type: (str) -> str # Implement it pass def __getitem__(self, name): return self._get_item(name) def delete_item(self, name): # type: (str) -> None """Delete item from settings registry. Args: name (str): Name of the item. """ self._delete_item(name) @abstractmethod def _delete_item(self, name): # type: (str) -> None """Delete item from settings. Note: see :meth:`openpype.lib.user_settings.ARegistrySettings.delete_item` """ pass def __delitem__(self, name): del self._items[name] self._delete_item(name) class IniSettingRegistry(ASettingRegistry): """Class using :mod:`configparser`. This class is using :mod:`configparser` (ini) files to store items. """ def __init__(self, name, path): # type: (str, str) -> IniSettingRegistry super(IniSettingRegistry, self).__init__(name) # get registry file version = os.getenv("OPENPYPE_VERSION", "N/A") self._registry_file = os.path.join(path, "{}.ini".format(name)) if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: print("# Settings registry", cfg) print("# Generated by OpenPype {}".format(version), cfg) now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") print("# {}".format(now), cfg) def set_item_section( self, section, name, value): # type: (str, str, str) -> None """Set item to specific section of ini registry. If section doesn't exists, it is created. Args: section (str): Name of section. name (str): Name of the item. value (str): Value of the item. """ value = str(value) config = configparser.ConfigParser() config.read(self._registry_file) if not config.has_section(section): config.add_section(section) current = config[section] current[name] = value with open(self._registry_file, mode="w") as cfg: config.write(cfg) def _set_item(self, name, value): # type: (str, str) -> None self.set_item_section("MAIN", name, value) def set_item(self, name, value): # type: (str, str) -> None """Set item to settings ini file. This saves item to ``DEFAULT`` section of ini as each item there must reside in some section. Args: name (str): Name of the item. value (str): Value of the item. """ # this does the some, overridden just for different docstring. # we cast value to str as ini options values must be strings. super(IniSettingRegistry, self).set_item(name, str(value)) def get_item(self, name): # type: (str) -> str """Gets item from settings ini file. This gets settings from ``DEFAULT`` section of ini file as each item there must reside in some section. Args: name (str): Name of the item. Returns: str: Value of item. Raises: ValueError: If value doesn't exist. """ return super(IniSettingRegistry, self).get_item(name) @lru_cache(maxsize=32) def get_item_from_section(self, section, name): # type: (str, str) -> str """Get item from section of ini file. This will read ini file and try to get item value from specified section. If that section or item doesn't exist, :exc:`ValueError` is risen. Args: section (str): Name of ini section. name (str): Name of the item. Returns: str: Item value. Raises: ValueError: If value doesn't exist. """ config = configparser.ConfigParser() config.read(self._registry_file) try: value = config[section][name] except KeyError: raise ValueError( "Registry doesn't contain value {}:{}".format(section, name)) return value def _get_item(self, name): # type: (str) -> str return self.get_item_from_section("MAIN", name) def delete_item_from_section(self, section, name): # type: (str, str) -> None """Delete item from section in ini file. Args: section (str): Section name. name (str): Name of the item. Raises: ValueError: If item doesn't exist. """ self.get_item_from_section.cache_clear() config = configparser.ConfigParser() config.read(self._registry_file) try: _ = config[section][name] except KeyError: raise ValueError( "Registry doesn't contain value {}:{}".format(section, name)) config.remove_option(section, name) # if section is empty, delete it if len(config[section].keys()) == 0: config.remove_section(section) with open(self._registry_file, mode="w") as cfg: config.write(cfg) def _delete_item(self, name): """Delete item from default section. Note: See :meth:`~openpype.lib.IniSettingsRegistry.delete_item_from_section` """ self.delete_item_from_section("MAIN", name) class JSONSettingRegistry(ASettingRegistry): """Class using json file as storage.""" def __init__(self, name, path): # type: (str, str) -> JSONSettingRegistry super(JSONSettingRegistry, self).__init__(name) #: 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") header = { "__metadata__": { "openpype-version": os.getenv("OPENPYPE_VERSION", "N/A"), "generated": now }, "registry": {} } if not os.path.exists(os.path.dirname(self._registry_file)): os.makedirs(os.path.dirname(self._registry_file), exist_ok=True) if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: json.dump(header, cfg, indent=4) @lru_cache(maxsize=32) def _get_item(self, name): # type: (str) -> object """Get item value from registry json. Note: See :meth:`openpype.lib.JSONSettingRegistry.get_item` """ with open(self._registry_file, mode="r") as cfg: data = json.load(cfg) try: value = data["registry"][name] except KeyError: raise ValueError( "Registry doesn't contain value {}".format(name)) return value def get_item(self, name): # type: (str) -> object """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: See :meth:`openpype.lib.JSONSettingRegistry.set_item` """ with open(self._registry_file, "r+") as cfg: data = json.load(cfg) data["registry"][name] = value cfg.truncate(0) cfg.seek(0) 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() with open(self._registry_file, "r+") as cfg: data = json.load(cfg) del data["registry"][name] cfg.truncate(0) cfg.seek(0) json.dump(data, cfg, indent=4) class OpenPypeSettingsRegistry(JSONSettingRegistry): """Class handling OpenPype general settings registry. Attributes: vendor (str): Name used for path construction. product (str): Additional name used for path construction. """ def __init__(self, name=None): self.vendor = "pypeclub" self.product = "openpype" if not name: name = "openpype_settings" path = appdirs.user_data_dir(self.product, self.vendor) super(OpenPypeSettingsRegistry, self).__init__(name, path)