Skip to content

Instantly share code, notes, and snippets.

@ChenyangGao
Last active August 30, 2022 01:43
Show Gist options
  • Save ChenyangGao/aca220b5927d9e7d3044d6f1d31baaf6 to your computer and use it in GitHub Desktop.
Save ChenyangGao/aca220b5927d9e7d3044d6f1d31baaf6 to your computer and use it in GitHub Desktop.
Python脚本作为配置文件加载
#!/usr/bin/env python3
# coding: utf-8
__author__ = "ChenyangGao <https://chenyanggao.github.io/>"
__version__ = (0, 1, 1)
__all__ = ["AttrDict", "DictAttr", "Properties"]
import builtins
from pathlib import Path
from types import CodeType
from typing import MutableMapping, Optional, Union
class AttrDict(dict):
"扩展的 dict 类型,它的实例的 __dict__ 属性(命名空间)就是该字典本身,因此执行 __getattr__、__setattr__、__delattr__ 操作的是该字典本身"
def __init__(self, *args, **kwds):
super().__init__(*args, **kwds)
self.__dict__ = self
@MutableMapping.register
class DictAttr:
"""这个类型实现了 collections.abc.MutableMapping 的接口,作为其结构子类型(structural subtyping),即鸭子类型(duck typing),而非名义子类型(nominal subtyping)。
:param init_dict: 如果不为 None,会用于替换实例的 __dict__ 属性
:: doctest
>>> d = dict(foo={})
>>> da = DictAttr(d)
>>> da
DictAttr({'foo': {}})
>>> da.foo
DictAttr({})
>>> da.foo.bar = 1
>>> da
DictAttr({'foo': {'bar': 1}})
>>> d
{'foo': {'bar': 1}}
>>> da.__dict__ is d
True
"""
def __init__(self, init_dict: Optional[dict] = None):
if init_dict is not None:
self.__dict__ = init_dict
def __contains__(self, key):
return key in self.__dict__
def __iter__(self):
return iter(self.__dict__)
def __len__(self):
return len(self.__dict__)
def __repr__(self):
return f"{type(self).__qualname__}({self.__dict__!r})"
def __getattribute__(self, attr):
"如果 attr 是字符串且前后都附缀两个下划线 __,则执行原始行为。否则行为相当于 __getitem__,但在取不到值时,会再执行原始行为。可能抛出 Attribute Error 异常。"
if type(attr) is str and attr[:2] == attr[-2:] == "__":
return super().__getattribute__(attr)
try:
return self[attr]
except KeyError:
return super().__getattribute__(attr)
def __getitem__(self, key):
"从 __dict__ 中取值,当值是 dict 类型时,会被 type(self) 对应的类包装"
val = self.__dict__[key]
if type(val) is dict:
return type(self)(val)
return val
def __setitem__(self, key, val):
"向 __dict__ 中设键值"
self.__dict__[key] = val
def __delitem__(self, key):
"从 __dict__ 中删键"
del self.__dict__[key]
class Properties(DictAttr):
"""这个类型实现了 collections.abc.MutableMapping 的接口,作为其结构子类型(structural subtyping),即鸭子类型(duck typing),而非名义子类型(nominal subtyping)。
:param init_dict: 如果不为 None,会用于替换实例的 __dict__ 属性
:: doctest
>>> d = dict(foo={})
>>> props = Properties(d)
>>> props
Properties({'foo': {}})
>>> props.foo
Properties({})
>>> props.foo.bar = 1
>>> props
Properties({'foo': {'bar': 1}})
>>> props.bar
Properties({})
>>> props
Properties({'foo': {'bar': 1}, 'bar': {}})
>>> props.baz.bay.baz = 1
>>> props
Properties({'foo': {'bar': 1}, 'bar': {}, 'baz': {'bay': {'baz': 1}}})
>>> d
{'foo': {'bar': 1}, 'bar': {}, 'baz': {'bay': {'baz': 1}}}
>>> props.__dict__ is d
True
"""
def __getitem__(self, key):
"从 __dict__ 中取值。首先执行基类 DictAttr 的原始行为;如果取不到值,再从 builtins.__dict__ 中取值;如果取不到,则对于非下划线前缀的字符串属性,把值设为 {},再执行一次基类的原始行为,否则抛出 KeyError。"
try:
return super().__getitem__(key)
except KeyError:
try:
return builtins.__dict__[key]
except KeyError:
pass
if type(key) is not str or key.startswith("_"):
raise
self[key] = {}
return super().__getitem__(key)
def __abs__(self) -> dict:
"创建一个 dict 副本,如果有 __all__ 字段,则筛选出所有在 __all__ 中的键,否则只筛选出键是字符串类型且非下划线前缀的键值对"
d = self.__dict__
if "__all__" in d:
return {
k: abs(v) if isinstance(v, Properties) else v
for k, v in ((k, d[k]) for k in d["__all__"] if k in d)
}
return {
k: abs(v) if isinstance(v, Properties) else v
for k, v in d.items()
if type(k) is str and not k.startswith("_")
}
def __call__(self, source: Union[str, bytes, CodeType, Path]):
"""执行一段 Python 代码,并更新 __dict__ 属性(命名空间)
:param source: Python 代码或者代码文件的路径
:return: 返回实例本身
:: tips
- 请将变量名提前注入 __dict__,否则缺失时自动设为 {}
- 如果代码中有 import 命令,请确保把需要的路径加到 sys.path 中,避免找不到模块
- 属性名有前缀下划线 _,用于说明想要过滤掉
:: doctest
>>> # 构造一段 Python 代码
>>> code = 'from math import nan as _nan, inf as _inf\\nz = _inf\\ny.z = _nan\\nx.y.z = y.z\\nfoo = sum([1,2, 3])\\nbar = abs(1+1j)'
>>> print(code)
from math import nan as _nan, inf as _inf
z = _inf
y.z = _nan
x.y.z = y.z
foo = sum([1,2, 3])
bar = abs(1+1j)
>>> props = Properties()
>>> props
Properties({})
>>> props(code)
Properties({'_nan': nan, '_inf': inf, 'z': inf, 'y': {'z': nan}, 'x': {'y': {'z': nan}}, 'foo': 6, 'bar': 1.4142135623730951})
>>> print(abs(props))
{'z': inf, 'y': {'z': nan}, 'x': {'y': {'z': nan}}, 'foo': 6, 'bar': 1.4142135623730951}
"""
code: Union[str, bytes, CodeType]
if isinstance(source, Path):
code = source.open(encoding="utf_8").read()
else:
code = source
exec(code, None, self) # type: ignore
return self
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=True)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment