json registry settings, tests and profiling helper

This commit is contained in:
Ondřej Samohel 2020-09-29 12:19:48 +02:00 committed by Ondrej Samohel
parent 39acfa76c5
commit 9535948c05
No known key found for this signature in database
GPG key ID: 8A29C663C672C2B7
3 changed files with 470 additions and 9 deletions

29
pype/lib/profiling.py Normal file
View file

@ -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()

405
pype/lib/user_settings.py Normal file
View file

@ -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)

View file

@ -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")