Last active
August 30, 2022 01:43
-
-
Save ChenyangGao/aca220b5927d9e7d3044d6f1d31baaf6 to your computer and use it in GitHub Desktop.
Python脚本作为配置文件加载
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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