diff --git a/pype/lib/profiling.py b/pype/lib/profiling.py new file mode 100644 index 0000000000..f3a3146200 --- /dev/null +++ b/pype/lib/profiling.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +"""Provide profiling decorator.""" +import os +import cProfile + + +def do_profile(fn, to_file=None): + """Wraps function in profiler run and print stat after it is done. + + Args: + to_file (str, optional): If specified, dumps stats into the file + instead of printing. + + """ + if to_file: + to_file = to_file.format(pid=os.getpid()) + + def profiled(*args, **kwargs): + profiler = cProfile.Profile() + try: + profiler.enable() + res = fn(*args, **kwargs) + profiler.disable() + return res + finally: + if to_file: + profiler.dump_stats(to_file) + else: + profiler.print_stats() diff --git a/pype/lib/user_settings.py b/pype/lib/user_settings.py new file mode 100644 index 0000000000..d03d2e3c40 --- /dev/null +++ b/pype/lib/user_settings.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +"""Package to deal with saving and retrieving user specific settings.""" +import os +from datetime import datetime +from abc import ABC, abstractmethod +import configparser +import json +from typing import Any +from functools import lru_cache + +import appdirs +import keyring + +from ..version import __version__ + + +class ASettingRegistry(ABC): + """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: str): + super(ASettingRegistry, self).__init__() + self._name = name + self._items = {} + + def set_item(self, name: str, value: 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: str, value: str) -> None: + # Implement it + pass + + def __setitem__(self, name, value): + self._items[name] = value + self._set_item(name, value) + + def get_item(self, name: str) -> str: + """Get item from settings registry. + + Args: + name (str): Name of the item. + + Returns: + value (str): Value of the item. + + Throws: + ValueError: If item doesn't exist. + + """ + return self._get_item(name) + + @abstractmethod + def _get_item(self, name: str) -> str: + # Implement it + pass + + def __getitem__(self, name): + return self._get_item(name) + + def delete_item(self, name: str): + """Delete item from settings registry. + + Args: + name (str): Name of the item. + + Returns: + value (str): Value of the item. + + """ + self._delete_item(name) + + @abstractmethod + def _delete_item(self, name: str): + """Delete item from settings. + + Note: + see :meth:`pype.lib.user_settings.ARegistrySettings.delete_item` + + """ + pass + + def __delitem__(self, name): + del self._items[name] + self._delete_item(name) + + def set_secure_item(self, name: str, value: 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 + + """ + keyring.set_password(self._name, name, value) + + @lru_cache(maxsize=32) + def get_secure_item(self, name: str) -> str: + """Get value of sensitive item from system's keyring. + + See also `Keyring module`_ + + Args: + name (str): Name of the item. + + Returns: + value (str): Value of the item. + + Throws: + ValueError: If item doesn't exist. + + .. _Keyring module: + https://github.com/jaraco/keyring + + """ + value = keyring.get_password(self._name, name) + if not value: + raise ValueError( + f"Item {self._name}:{name} does not exist in keyring.") + return value + + def delete_secure_item(self, name: 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 + + """ + self.get_secure_item.cache_clear() + keyring.delete_password(self._name, name) + + +class IniSettingRegistry(ASettingRegistry): + """Class implementing ASettingRegistry to use :mod:`configparser`. + + This class is using :mod:`configparser` (ini) files to store items. + + """ + + def __init__(self, name, path: str): + super(IniSettingRegistry, self).__init__(name) + # get registry file + self._registry_file = os.path.join(path, f"{name}.ini") + if not os.path.exists(self._registry_file): + with open(self._registry_file, mode="w") as cfg: + print("# Settings registry", cfg) + print(f"# Generated by Pype {__version__}", cfg) + now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + print(f"# {now} ") + + def set_item_section( + self, section: str, name: str, value: 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.SafeConfigParser() + + 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: str, value: str) -> None: + + self.set_item_section("MAIN", name, value) + + def set_item(self, name: str, value: 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: 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. + + Throws: + ValueError: If value doesn't exist. + + """ + return super(IniSettingRegistry, self).get_item(name) + + @lru_cache(maxsize=32) + def get_item_from_section(self, section: str, name: 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, ValueError is risen. + + Args: + section (str): Name of ini section. + name (str): Name of the item. + + Returns: + str: Item value. + + Throws: + ValueError: If value doesn't exist. + + """ + config = configparser.ConfigParser() + config.read(self._registry_file) + try: + value = config[section][name] + except KeyError: + raise ValueError( + f"Registry doesn't contain value {section}:{name}") + return value + + def _get_item(self, name: str) -> str: + return self.get_item_from_section("MAIN", name) + + def delete_item_from_section(self, section: str, name: str) -> None: + """Delete item from section in ini file. + + Args: + section (str): Section name. + name (str): Name of the item. + + Throws: + 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( + f"Registry doesn't contain value {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:`~pype.lib.IniSettingsRegistry.delete_item_from_section` + + """ + self.delete_item_from_section("MAIN", name) + + +class JSONSettingRegistry(ASettingRegistry): + """Class implementing ASettingRegistry to use json file as storage.""" + + def __init__(self, name, path: str): + super(JSONSettingRegistry, self).__init__(name) + #: str: name of registry file + self._registry_file = os.path.join(path, f"{name}.json") + now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + header = { + "__metadata__": { + "pype-version": __version__, + "generated": now + }, + "registry": {} + } + + 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: str) -> Any: + """Get item value from registry json. + + Note: + See :meth:`pype.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( + f"Registry doesn't contain value {name}") + return value + + def get_item(self, name: str) -> Any: + """Get item value from registry json. + + Args: + name (str): Name of the item. + + Returns: + value of the item + + Throws: + ValueError: If item is not found in registry file. + + """ + return self._get_item(name) + + def _set_item(self, name: str, value: Any) -> None: + """Set item value to registry json. + + Note: + See :meth:`pype.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: str, value: Any) -> 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: str): + 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 PypeSettingsRegistry(JSONSettingRegistry): + """Class handling Pype general settings registry. + + Attributes: + vendor (str): Name used for path construction. + product (str): Additional name used for path construction. + + """ + vendor = "pypeclub" + product = "pype" + + def __init__(self): + path = appdirs.user_data_dir(self.product, self.vendor) + super(PypeSettingsRegistry, self).__init__("pype_settings", path) diff --git a/tests/pype/lib/test_user_settings.py b/tests/pype/lib/test_user_settings.py index 32de58eb07..93c374cea3 100644 --- a/tests/pype/lib/test_user_settings.py +++ b/tests/pype/lib/test_user_settings.py @@ -20,8 +20,7 @@ def ini_registry(tmpdir): yield r -def test_keyring(json_registry, printer): - printer("testing get/set") +def test_keyring(json_registry): service = json_registry._name json_registry.set_secure_item("item1", "foo") json_registry.set_secure_item("item2", "bar") @@ -31,8 +30,6 @@ def test_keyring(json_registry, printer): assert result1 == "foo" assert result2 == "bar" - printer(f"testing delete from {service}") - json_registry.delete_secure_item("item1") json_registry.delete_secure_item("item2") @@ -41,29 +38,28 @@ def test_keyring(json_registry, printer): json_registry.get_secure_item("item2") -def test_ini_registry(ini_registry, printer): - printer("testing get/set") +def test_ini_registry(ini_registry): ini_registry.set_item("test1", "bar") ini_registry.set_item_section("TEST", "test2", "foo") ini_registry.set_item_section("TEST", "test3", "baz") + ini_registry["woo"] = 1 result1 = ini_registry.get_item("test1") result2 = ini_registry.get_item_from_section("TEST", "test2") result3 = ini_registry.get_item_from_section("TEST", "test3") + result4 = ini_registry["woo"] assert result1 == "bar" assert result2 == "foo" assert result3 == "baz" + assert result4 == "1" - printer("test non-existent value") with pytest.raises(ValueError): ini_registry.get_item("xxx") with pytest.raises(ValueError): ini_registry.get_item_from_section("FFF", "yyy") - printer("test deleting") - ini_registry.delete_item("test1") with pytest.raises(ValueError): ini_registry.get_item("test1") @@ -76,6 +72,10 @@ def test_ini_registry(ini_registry, printer): with pytest.raises(ValueError): ini_registry.get_item_from_section("TEST", "test3") + del ini_registry["woo"] + with pytest.raises(ValueError): + ini_registry.get_item("woo") + # ensure TEST section is also deleted cfg = configparser.ConfigParser() cfg.read(ini_registry._registry_file) @@ -86,3 +86,30 @@ def test_ini_registry(ini_registry, printer): with pytest.raises(ValueError): ini_registry.delete_item_from_section("XXX", "UUU") + + +def test_json_registry(json_registry): + json_registry.set_item("foo", "bar") + json_registry.set_item("baz", {"a": 1, "b": "c"}) + json_registry["woo"] = 1 + + result1 = json_registry.get_item("foo") + result2 = json_registry.get_item("baz") + result3 = json_registry["woo"] + + assert result1 == "bar" + assert result2["a"] == 1 + assert result2["b"] == "c" + assert result3 == 1 + + with pytest.raises(ValueError): + json_registry.get_item("zoo") + + json_registry.delete_item("foo") + + with pytest.raises(ValueError): + json_registry.get_item("foo") + + del json_registry["woo"] + with pytest.raises(ValueError): + json_registry.get_item("woo")