388 lines
11 KiB
Python
388 lines
11 KiB
Python
# coding: utf-8
|
|
# -------------------------------------------------------------------
|
|
# YakPanel
|
|
# -------------------------------------------------------------------
|
|
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
|
# -------------------------------------------------------------------
|
|
# Author: yakpanel
|
|
# -------------------------------------------------------------------
|
|
|
|
# ------------------------------
|
|
# config app
|
|
# ------------------------------
|
|
|
|
import copy
|
|
import os
|
|
import threading
|
|
|
|
try:
|
|
import ujson as json
|
|
except ImportError:
|
|
import json
|
|
|
|
__all__ = [
|
|
"DictConfig",
|
|
"ListConfig",
|
|
]
|
|
|
|
|
|
class _Ctx:
|
|
"""轻量锁+加载"""
|
|
__slots__ = ("_mgr", "_save")
|
|
|
|
def __init__(self, mgr: "SimpleConfig", save: bool = False):
|
|
self._mgr = mgr
|
|
self._save = save
|
|
|
|
def __enter__(self):
|
|
mgr = self._mgr
|
|
mgr._lock.acquire()
|
|
try:
|
|
if not mgr._loaded:
|
|
mgr._do_load()
|
|
except:
|
|
mgr._lock.release()
|
|
raise
|
|
return mgr._cache
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
try:
|
|
if self._save and exc_type is None:
|
|
self._mgr._save()
|
|
finally:
|
|
self._mgr._lock.release()
|
|
|
|
|
|
class SimpleConfig(object):
|
|
"""json"""
|
|
__slots__ = ("_path", "_tmp_path", "_lock", "_cache", "_loaded", "_default")
|
|
|
|
def __init__(self, path: str, default=None):
|
|
"""配置文件的绝对路径"""
|
|
self._path: str = path
|
|
self._default = default
|
|
self._tmp_path: str = path + ".tmp"
|
|
self._lock = threading.RLock()
|
|
self._cache = None
|
|
self._loaded: bool = False
|
|
|
|
if default is not None and not os.path.exists(path):
|
|
with self._lock:
|
|
self._cache = copy.deepcopy(default)
|
|
self._loaded = True
|
|
self._save()
|
|
|
|
def __bool__(self) -> bool:
|
|
"""判断当前数据是否非空"""
|
|
with self._ctx() as c:
|
|
return bool(c)
|
|
|
|
def _ctx(self, save: bool = False) -> _Ctx:
|
|
return _Ctx(self, save=save)
|
|
|
|
def _do_load(self):
|
|
"""需持锁"""
|
|
if os.path.exists(self._path):
|
|
try:
|
|
with open(self._path, "r", encoding="utf-8") as f:
|
|
self._cache = json.load(f)
|
|
self._loaded = True
|
|
return
|
|
except (ValueError, OSError):
|
|
pass
|
|
self._cache = copy.deepcopy(self._default) if self._default is not None else self._default_data()
|
|
self._loaded = True
|
|
|
|
def _save(self):
|
|
"""需持锁"""
|
|
dirname = os.path.dirname(self._path)
|
|
if dirname:
|
|
os.makedirs(dirname, exist_ok=True)
|
|
try:
|
|
with open(self._tmp_path, "w", encoding="utf-8") as f:
|
|
json.dump(self._cache, f, ensure_ascii=False)
|
|
f.flush()
|
|
os.replace(self._tmp_path, self._path)
|
|
except OSError:
|
|
self._loaded = False
|
|
try:
|
|
os.remove(self._tmp_path)
|
|
except FileNotFoundError:
|
|
pass
|
|
raise
|
|
|
|
def _default_data(self):
|
|
raise NotImplementedError
|
|
|
|
# ----------- public -----------
|
|
|
|
def reload(self):
|
|
"""强制重新加载"""
|
|
with self._lock:
|
|
self._loaded = False
|
|
self._do_load()
|
|
|
|
def clear(self):
|
|
"""清空并持久化"""
|
|
with self._ctx(save=True):
|
|
self._cache = copy.deepcopy(self._default) if self._default is not None else self._default_data()
|
|
|
|
def save(self):
|
|
"""手动持久化"""
|
|
with self._ctx():
|
|
if self._loaded:
|
|
self._save()
|
|
|
|
def atomic(self) -> _Ctx:
|
|
"""原子操作上下文, 事务"""
|
|
return self._ctx(save=True)
|
|
|
|
@property
|
|
def path(self) -> str:
|
|
return self._path
|
|
|
|
def exists(self) -> bool:
|
|
"""配置文件是否存在于磁盘"""
|
|
return os.path.exists(self._path)
|
|
|
|
def delete_config(self):
|
|
with self._lock:
|
|
for p in (self._path, self._tmp_path):
|
|
try:
|
|
os.remove(p)
|
|
except FileNotFoundError:
|
|
pass
|
|
self._cache = copy.deepcopy(self._default) if self._default is not None else self._default_data()
|
|
self._loaded = True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class DictConfig(SimpleConfig):
|
|
"""
|
|
cfg = DictConfig("/path/to/config.json", default={"a": 1})
|
|
cfg.set("key", "value")
|
|
cfg.get("key") # "value"
|
|
cfg.get("missing", 0) # 0
|
|
cfg["key"] = "new_value"
|
|
del cfg["key"]
|
|
cfg.update({"a": 1, "b": 2}) # 浅合并,整体覆盖同名 key
|
|
cfg.merge({"a": {"x": 1}}) # 深合并,更新嵌套中若干字段而非整体覆盖
|
|
cfg.keys() / cfg.values() / cfg.items()
|
|
"key" in cfg
|
|
len(cfg)
|
|
cfg.pop("key", None)
|
|
cfg.setdefault("key", default_val)
|
|
cfg.as_dict()
|
|
"""
|
|
|
|
def _default_data(self) -> dict:
|
|
return {}
|
|
|
|
def get(self, key: str, default=None):
|
|
with self._ctx() as c:
|
|
return c.get(key, default)
|
|
|
|
def __getitem__(self, key: str):
|
|
with self._ctx() as c:
|
|
return c[key]
|
|
|
|
def __contains__(self, key: str) -> bool:
|
|
with self._ctx() as c:
|
|
return key in c
|
|
|
|
def __len__(self) -> int:
|
|
with self._ctx() as c:
|
|
return len(c)
|
|
|
|
def __iter__(self):
|
|
with self._ctx() as c:
|
|
return iter(list(c.keys()))
|
|
|
|
def keys(self):
|
|
with self._ctx() as c:
|
|
return list(c.keys())
|
|
|
|
def values(self):
|
|
with self._ctx() as c:
|
|
return list(c.values())
|
|
|
|
def items(self):
|
|
with self._ctx() as c:
|
|
return list(c.items())
|
|
|
|
def as_dict(self) -> dict:
|
|
with self._ctx() as c:
|
|
return dict(c)
|
|
|
|
def __repr__(self) -> str:
|
|
with self._ctx() as c:
|
|
return f"DictConfig({self._path!r}, {c!r})"
|
|
|
|
def set(self, key: str, value) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c[key] = value
|
|
|
|
def __setitem__(self, key: str, value) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c[key] = value
|
|
|
|
def update(self, data: dict) -> None:
|
|
if not data:
|
|
return
|
|
with self._ctx(save=True) as c:
|
|
c.update(data)
|
|
|
|
def merge(self, data: dict) -> None:
|
|
"""合并data到配置, 对嵌套dict递归合并, 非update覆盖"""
|
|
if not data:
|
|
return
|
|
|
|
def _deep_merge(base: dict, patch: dict) -> None:
|
|
for k, v in patch.items():
|
|
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
|
|
_deep_merge(base[k], v)
|
|
else:
|
|
base[k] = v
|
|
|
|
with self._ctx(save=True) as c:
|
|
_deep_merge(c, data)
|
|
|
|
def setdefault(self, key: str, default=None):
|
|
with self._ctx(save=True) as c:
|
|
if key not in c:
|
|
c[key] = default
|
|
return c[key]
|
|
|
|
def delete(self, key: str) -> None:
|
|
with self._ctx(save=True) as c:
|
|
if key in c:
|
|
del c[key]
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
with self._ctx(save=True) as c:
|
|
del c[key]
|
|
|
|
def pop(self, key: str, *args):
|
|
with self._ctx(save=True) as c:
|
|
return c.pop(key, *args) if args else c.pop(key)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ListConfig(SimpleConfig):
|
|
"""
|
|
cfg = ListConfig("/path/to/list.json", default=[1,2,3])
|
|
cfg.append("item") / cfg.insert(0, "x") / cfg.extend([...])
|
|
cfg.get(0) / cfg[0] / cfg[0] = "v"
|
|
cfg.remove("x") / cfg.pop(0) / del cfg[0]
|
|
cfg.index("x") / cfg.count("x")
|
|
"x" in cfg / len(cfg) / iter(cfg)
|
|
cfg.sort() / cfg.reverse()
|
|
cfg.unique()
|
|
cfg.as_list()
|
|
"""
|
|
|
|
def _default_data(self) -> list:
|
|
return []
|
|
|
|
def get(self, index: int, default=None):
|
|
with self._ctx() as c:
|
|
try:
|
|
return c[index]
|
|
except IndexError:
|
|
return default
|
|
|
|
def __getitem__(self, index):
|
|
with self._ctx() as c:
|
|
return c[index]
|
|
|
|
def __contains__(self, item) -> bool:
|
|
with self._ctx() as c:
|
|
return item in c
|
|
|
|
def __len__(self) -> int:
|
|
with self._ctx() as c:
|
|
return len(c)
|
|
|
|
def __iter__(self):
|
|
with self._ctx() as c:
|
|
return iter(list(c))
|
|
|
|
def count(self, item) -> int:
|
|
with self._ctx() as c:
|
|
return c.count(item)
|
|
|
|
def index(self, item, *args) -> int:
|
|
with self._ctx() as c:
|
|
return c.index(item, *args)
|
|
|
|
def as_list(self) -> list:
|
|
with self._ctx() as c:
|
|
return list(c)
|
|
|
|
def __repr__(self) -> str:
|
|
with self._ctx() as c:
|
|
return f"ListConfig({self._path!r}, {c!r})"
|
|
|
|
def set(self, index: int, value) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c[index] = value
|
|
|
|
def __setitem__(self, index, value) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c[index] = value
|
|
|
|
def __delitem__(self, index) -> None:
|
|
with self._ctx(save=True) as c:
|
|
del c[index]
|
|
|
|
def append(self, item) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c.append(item)
|
|
|
|
def insert(self, index: int, item) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c.insert(index, item)
|
|
|
|
def extend(self, items) -> None:
|
|
items = list(items)
|
|
if not items:
|
|
return
|
|
with self._ctx(save=True) as c:
|
|
c.extend(items)
|
|
|
|
def remove(self, item) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c.remove(item)
|
|
|
|
def pop(self, index: int = -1):
|
|
with self._ctx(save=True) as c:
|
|
return c.pop(index)
|
|
|
|
def sort(self, *, key=None, reverse: bool = False) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c.sort(key=key, reverse=reverse)
|
|
|
|
def reverse(self) -> None:
|
|
with self._ctx(save=True) as c:
|
|
c.reverse()
|
|
|
|
def unique(self) -> None:
|
|
"""保序去重"""
|
|
with self._ctx(save=True) as c:
|
|
seen: set = set()
|
|
result = []
|
|
for item in c:
|
|
try:
|
|
key = item
|
|
hash(key)
|
|
except TypeError:
|
|
key = id(item)
|
|
if key not in seen:
|
|
seen.add(key)
|
|
result.append(item)
|
|
c[:] = result
|