simple config
0 parents
Showing
15 changed files
with
872 additions
and
0 deletions
.gitignore
0 → 100755
1 | # Byte-compiled / optimized / DLL files | ||
2 | __pycache__/ | ||
3 | *.py[cod] | ||
4 | *$py.class | ||
5 | |||
6 | # C extensions | ||
7 | *.so | ||
8 | |||
9 | # Distribution / packaging | ||
10 | .Python | ||
11 | build/ | ||
12 | develop-eggs/ | ||
13 | dist/ | ||
14 | downloads/ | ||
15 | eggs/ | ||
16 | .eggs/ | ||
17 | lib/ | ||
18 | lib64/ | ||
19 | parts/ | ||
20 | sdist/ | ||
21 | var/ | ||
22 | wheels/ | ||
23 | share/python-wheels/ | ||
24 | *.egg-info/ | ||
25 | .installed.cfg | ||
26 | *.egg | ||
27 | MANIFEST | ||
28 | |||
29 | # PyInstaller | ||
30 | # Usually these files are written by a python script from a template | ||
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. | ||
32 | *.manifest | ||
33 | *.spec | ||
34 | |||
35 | # Installer logs | ||
36 | pip-log.txt | ||
37 | pip-delete-this-directory.txt | ||
38 | |||
39 | # Unit test / coverage reports | ||
40 | htmlcov/ | ||
41 | .tox/ | ||
42 | .nox/ | ||
43 | .coverage | ||
44 | .coverage.* | ||
45 | .cache | ||
46 | nosetests.xml | ||
47 | coverage.xml | ||
48 | *.cover | ||
49 | .hypothesis/ | ||
50 | .pytest_cache/ | ||
51 | |||
52 | # Translations | ||
53 | *.mo | ||
54 | *.pot | ||
55 | |||
56 | # Django stuff: | ||
57 | *.log | ||
58 | local_settings.py | ||
59 | db.sqlite3 | ||
60 | |||
61 | # Flask stuff: | ||
62 | instance/ | ||
63 | .webassets-cache | ||
64 | |||
65 | # Scrapy stuff: | ||
66 | .scrapy | ||
67 | |||
68 | # Sphinx documentation | ||
69 | docs/_build/ | ||
70 | |||
71 | # PyBuilder | ||
72 | target/ | ||
73 | |||
74 | # Jupyter Notebook | ||
75 | .ipynb_checkpoints | ||
76 | |||
77 | # IPython | ||
78 | profile_default/ | ||
79 | ipython_config.py | ||
80 | |||
81 | # pyenv | ||
82 | .python-version | ||
83 | |||
84 | # celery beat schedule file | ||
85 | celerybeat-schedule | ||
86 | |||
87 | # SageMath parsed files | ||
88 | *.sage.py | ||
89 | |||
90 | # Environments | ||
91 | .env | ||
92 | .venv | ||
93 | env/ | ||
94 | venv/ | ||
95 | ENV/ | ||
96 | env.bak/ | ||
97 | venv.bak/ | ||
98 | |||
99 | # Spyder project settings | ||
100 | .spyderproject | ||
101 | .spyproject | ||
102 | |||
103 | # Rope project settings | ||
104 | .ropeproject | ||
105 | |||
106 | # mkdocs documentation | ||
107 | /site | ||
108 | |||
109 | # mypy | ||
110 | .mypy_cache/ | ||
111 | .dmypy.json | ||
112 | dmypy.json | ||
113 | |||
114 | # Pyre type checker | ||
115 | .pyre/ | ||
116 | |||
117 | # pycharm file | ||
118 | .idea/ |
.setup.cfg
0 → 100755
.style.yapf
0 → 100755
README.md
0 → 100755
1 | # simple_config | ||
2 | |||
3 | Python项目的配置信息工具,可以从解析配置文件、环境变量。 | ||
4 | |||
5 | ## Getting Started | ||
6 | |||
7 | |||
8 | ### Installing | ||
9 | |||
10 | |||
11 | ``` | ||
12 | $: git clone git@gitlab.situdata.com:zhouweiqi/simple_config.git | ||
13 | $: pip install -e simple_config | ||
14 | ``` | ||
15 | |||
16 | |||
17 | ### QuickStart | ||
18 | |||
19 | |||
20 | **配置文件** | ||
21 | |||
22 | 创建配置文件`test.ini`, 内容如下: | ||
23 | |||
24 | ``` | ||
25 | ETCD_HOST=1.1.1.1 | ||
26 | ETCD_PORT=80 | ||
27 | ETCD_PASSWD=0hyxdryq_CZ | ||
28 | ETCD_USER=test | ||
29 | HTTP_SERVER=http://test.com | ||
30 | DEBUG=true | ||
31 | ALLOWED_HOSTS=*.test.com, api.test.com, * | ||
32 | USER_NAME=root | ||
33 | ``` | ||
34 | |||
35 | 解析配置文件 | ||
36 | |||
37 | ```python | ||
38 | In [2]: from simple_config import Config, ConfigAttribute, converter | ||
39 | In [3]: class ProjectConfig(Config): | ||
40 | ...: ETCD_HOST = ConfigAttribute('ETCD_HOST') | ||
41 | ...: ETCD_PORT = ConfigAttribute('ETCD_PORT', get_converter=int) | ||
42 | ...: ETCD_USER = ConfigAttribute('ETCD_USER') | ||
43 | ...: ETCD_PASSWD = ConfigAttribute('ETCD_PASSWD') | ||
44 | ...: | ||
45 | ...: HTTP_SERVER = ConfigAttribute( | ||
46 | ...: 'HTTP_SERVER', get_converter=converter.server) | ||
47 | ...: | ||
48 | ...: DEBUG = ConfigAttribute('DEBUG', get_converter=converter.boolean) | ||
49 | ...: ALIAS_DEBUG = ConfigAttribute('DEBUG', get_converter=converter.boolean) | ||
50 | ...: | ||
51 | ...: ALLOWED_HOSTS = ConfigAttribute( | ||
52 | ...: 'ALLOWED_HOSTS', get_converter=converter.Csv()) | ||
53 | ...: | ||
54 | ...: | ||
55 | ...: config = ProjectConfig(defaults={'DEBUG': False}) | ||
56 | |||
57 | In [4]: config.DEBUG | ||
58 | Out[4]: False | ||
59 | |||
60 | In [5]: config.from_env_file('test.ini') | ||
61 | |||
62 | In [6]: config.DEBUG | ||
63 | Out[6]: True | ||
64 | |||
65 | In [7]: config.ETCD_PORT | ||
66 | Out[7]: 80 | ||
67 | |||
68 | In [8]: config.get_namespace('ETCD_') | ||
69 | Out[8]: {'host': '1.1.1.1', 'passwd': '0hyxdryq_CZ', 'port': 80, 'user': 'test'} | ||
70 | |||
71 | In [9]: config.ALLOWED_HOSTS | ||
72 | Out[9]: ['*.test.com', 'api.test.com', '*'] | ||
73 | |||
74 | In [10]: config.ALIAS_DEBUG | ||
75 | Out[10]: True | ||
76 | |||
77 | In [11]: config.USER_NAME # 获取配置文件中有,但是Config没有声明的配置 | ||
78 | Out[11]: 'root' | ||
79 | |||
80 | In [12]: config.HTTP_SERVER | ||
81 | Out[12]: Server(scheme='http', host='test.com', port=80) | ||
82 | |||
83 | ``` | ||
84 | |||
85 | |||
86 | **常量** | ||
87 | |||
88 | 创建文件`const.py`, 内容如下 | ||
89 | |||
90 | ```python | ||
91 | import sys | ||
92 | from simple_config import Const | ||
93 | |||
94 | A = 1 | ||
95 | B = 2 | ||
96 | |||
97 | |||
98 | sys.modules[__name__] = Const.from_current_module(__name__) | ||
99 | ``` | ||
100 | |||
101 | 加载常量 | ||
102 | |||
103 | ```python | ||
104 | In [1]: import const | ||
105 | In [2]: const | ||
106 | Out[2]: <simple_config.Const at 0x7fc59c9475c0> | ||
107 | In [3]: const? | ||
108 | Type: Const | ||
109 | String form: <simple_config.Const object at 0x7fc59c9475c0> | ||
110 | Docstring: <no docstring> | ||
111 | In [4]: const.A | ||
112 | Out[4]: 1 | ||
113 | In [5]: const.B | ||
114 | Out[5]: 2 | ||
115 | In [6]: const.A = 3 # 不能修改已有变量值 | ||
116 | --------------------------------------------------------------------------- | ||
117 | ConstError Traceback (most recent call last) | ||
118 | <ipython-input-6-d1e1d5d0fa5a> in <module> | ||
119 | ----> 1 const.A = 3 | ||
120 | |||
121 | ~/code/simple_config/simple_config/__init__.py in __setattr__(self, name, value) | ||
122 | 14 def __setattr__(self, name, value): | ||
123 | 15 if name in self.__dict__: | ||
124 | ---> 16 raise self.ConstError("Can't change const.{}".format(name)) | ||
125 | 17 if not name.isupper(): | ||
126 | 18 raise self.ConstCaseError( | ||
127 | |||
128 | ConstError: Can't change const.A | ||
129 | ``` | ||
130 | |||
131 | |||
132 | |||
133 | ## Test | ||
134 | |||
135 | 1. 安装依赖 | ||
136 | |||
137 | ```bash | ||
138 | $ pip install nose coverage | ||
139 | ``` | ||
140 | |||
141 | 2. 运行测试 | ||
142 | |||
143 | ``` | ||
144 | nosetests -c .setup.cfg | ||
145 | ``` |
requirements/test_requirements.txt
0 → 100755
setup.py
0 → 100755
1 | import setuptools | ||
2 | import simple_config | ||
3 | |||
4 | with open('README.md', 'r', encoding='utf8') as fh: | ||
5 | long_description = fh.read() | ||
6 | |||
7 | setuptools.setup( | ||
8 | name="simple_config", | ||
9 | version=simple_config.__version__, | ||
10 | author="clay", | ||
11 | author_email="zhouweiqi@situdata.com", | ||
12 | description="A small tool to parse and store project config", | ||
13 | long_description=long_description, | ||
14 | long_description_content_type="text/markdown", | ||
15 | packages=setuptools.find_packages(), | ||
16 | classifiers=[ | ||
17 | "Programming Language :: Python :: 3", | ||
18 | "Operating System :: OS Independent", | ||
19 | ], | ||
20 | ) |
simple_config/__init__.py
0 → 100755
1 | import sys | ||
2 | |||
3 | from .core import Config | ||
4 | from .core import Attribute as ConfigAttribute | ||
5 | |||
6 | |||
7 | class Const(object): | ||
8 | class ConstError(TypeError): | ||
9 | pass | ||
10 | |||
11 | class ConstCaseError(ConstError): | ||
12 | pass | ||
13 | |||
14 | def __setattr__(self, name, value): | ||
15 | if name in self.__dict__: | ||
16 | raise self.ConstError("Can't change const.{}".format(name)) | ||
17 | if not name.isupper(): | ||
18 | raise self.ConstCaseError( | ||
19 | "const name {} is not all uppercase".format(name)) | ||
20 | self.__dict__[name] = value | ||
21 | |||
22 | @classmethod | ||
23 | def from_current_module(cls, name): | ||
24 | current_module = sys.modules[name] | ||
25 | _const = cls() | ||
26 | for key in dir(current_module): | ||
27 | if not key.isupper(): | ||
28 | continue | ||
29 | setattr(_const, key, getattr(current_module, key)) | ||
30 | return _const | ||
31 | |||
32 | |||
33 | __name__ = "simple_config" | ||
34 | __all__ = ('Config', 'ConfigAttribute', 'const') | ||
35 | __version__ = '1.2' |
simple_config/converter.py
0 → 100755
1 | from collections import namedtuple | ||
2 | import re | ||
3 | import string | ||
4 | from shlex import shlex | ||
5 | |||
6 | from .exceptions import ConvertError | ||
7 | |||
8 | __all__ = ('host_port', 'server', 'Csv') | ||
9 | |||
10 | Server = namedtuple('Server', 'scheme host port') | ||
11 | |||
12 | host_port_pattern = re.compile( | ||
13 | r'^(?P<host>[0-9a-zA-Z\-.]+)(?:\:(?P<port>\d{1,5}))?$') | ||
14 | |||
15 | |||
16 | def host_port(string): | ||
17 | """ | ||
18 | 解析"host:port"字符串 | ||
19 | |||
20 | >>> host_port('dev.some-host.org:60000') | ||
21 | Server(scheme=None, host='dev.some-host.org', port=60000) | ||
22 | >>> host_port('8.8.8.8:8080') | ||
23 | Server(scheme=None, host='8.8.8.8', port=8080) | ||
24 | >>> host_port('8.8.8.8') | ||
25 | Server(scheme=None, host='8.8.8.8', port=None) | ||
26 | >>> res = host_port('dev.some-host.org:600000') | ||
27 | Traceback (most recent call last): | ||
28 | ... | ||
29 | simple_config.exceptions.ConvertError: invalid host_port \ | ||
30 | string: dev.some-host.org:600000 | ||
31 | |||
32 | """ | ||
33 | match = host_port_pattern.match(string) | ||
34 | if not match: | ||
35 | raise ConvertError('invalid host_port string: %s' % string) | ||
36 | port = match.group('port') and int(match.group('port')) | ||
37 | return Server(None, match.group('host'), port) | ||
38 | |||
39 | |||
40 | http_server_pattern = re.compile( | ||
41 | r'^(?P<schema>https?)://(?P<host>[0-9a-zA-Z\-.]+)(?:\:(?P<port>\d{1,5}))?$' | ||
42 | ) | ||
43 | |||
44 | |||
45 | def server(string): | ||
46 | """ | ||
47 | 解析"http://host:port"字符串 | ||
48 | |||
49 | >>> server('http://dev.some-host.org:60000') | ||
50 | Server(scheme='http', host='dev.some-host.org', port=60000) | ||
51 | >>> server('https://8.8.8.8:8080') | ||
52 | Server(scheme='https', host='8.8.8.8', port=8080) | ||
53 | >>> server('http://8.8.8.8') | ||
54 | Server(scheme='http', host='8.8.8.8', port=80) | ||
55 | >>> res = server('dev.some-host.org:600000') | ||
56 | Traceback (most recent call last): | ||
57 | ... | ||
58 | simple_config.exceptions.ConvertError: invalid http_server \ | ||
59 | string: dev.some-host.org:600000 | ||
60 | |||
61 | |||
62 | """ | ||
63 | match = http_server_pattern.match(string) | ||
64 | if not match: | ||
65 | raise ConvertError('invalid http_server string: %s' % string) | ||
66 | port = match.group('port') and int(match.group('port')) | ||
67 | if not port: | ||
68 | port = 80 if match.group('schema') == 'http' else 443 | ||
69 | return Server(match.group('schema'), match.group('host'), port) | ||
70 | |||
71 | |||
72 | def boolean(instance): | ||
73 | """ | ||
74 | 根据输入,返回是否为True | ||
75 | |||
76 | >>> boolean(True) | ||
77 | True | ||
78 | >>> boolean('True') | ||
79 | True | ||
80 | >>> boolean('TRUE') | ||
81 | True | ||
82 | >>> boolean('false') | ||
83 | False | ||
84 | >>> boolean(None) | ||
85 | Traceback (most recent call last): | ||
86 | ... | ||
87 | simple_config.exceptions.ConvertError: expected boolean or str, \ | ||
88 | but get <class 'type'> | ||
89 | |||
90 | """ | ||
91 | if isinstance(instance, bool): | ||
92 | return instance | ||
93 | elif isinstance(instance, str): | ||
94 | if instance.upper() == 'TRUE': | ||
95 | return True | ||
96 | return False | ||
97 | raise ConvertError('expected boolean or str, but get %s' % type(object)) | ||
98 | |||
99 | |||
100 | class Csv(object): | ||
101 | """ | ||
102 | Produces a csv parser that return a list of transformed elements. | ||
103 | |||
104 | origin code is here | ||
105 | https://github.com/henriquebastos/python-decouple/blob/master/decouple.py | ||
106 | >>> csv = Csv() | ||
107 | >>> hosts = "*,*.test.com, demo.test.com ,test.org" | ||
108 | >>> csv(hosts) | ||
109 | ['*', '*.test.com', 'demo.test.com', 'test.org'] | ||
110 | >>> csv = Csv(delimiter=';;') | ||
111 | >>> string = 'a;b; c;D' | ||
112 | >>> csv(string) | ||
113 | ['a', 'b', 'c', 'D'] | ||
114 | >>> hosts = "*,*.test.com, demo.test.com ,test.org" | ||
115 | >>> csv(hosts) | ||
116 | ['*,*.test.com, demo.test.com ,test.org'] | ||
117 | """ | ||
118 | |||
119 | def __init__(self, | ||
120 | cast=str, | ||
121 | delimiter=',', | ||
122 | strip=string.whitespace, | ||
123 | post_process=list): | ||
124 | """ | ||
125 | Parameters: | ||
126 | cast -- callable that transforms the item just before | ||
127 | it's added to the list. | ||
128 | delimiter -- string of delimiters chars passed to shlex. | ||
129 | strip -- string of non-relevant characters to be passed to | ||
130 | str.strip after the split. | ||
131 | """ | ||
132 | self.cast = cast | ||
133 | self.delimiter = delimiter | ||
134 | self.strip = strip | ||
135 | self.post_process = post_process | ||
136 | |||
137 | def __call__(self, value): | ||
138 | """The actual transformation""" | ||
139 | |||
140 | def transform(s): | ||
141 | return self.cast(s.strip(self.strip)) | ||
142 | |||
143 | splitter = shlex(value, posix=True) | ||
144 | splitter.whitespace = self.delimiter | ||
145 | splitter.whitespace_split = True | ||
146 | |||
147 | return self.post_process(transform(s) for s in splitter) |
simple_config/core.py
0 → 100755
1 | from configparser import ConfigParser | ||
2 | import os | ||
3 | import re | ||
4 | import warnings | ||
5 | |||
6 | from .exceptions import (FileDoesNotExist, VariableNotExist, NotConfigured, | ||
7 | InvalidConfig) | ||
8 | |||
9 | |||
10 | class Attribute(object): | ||
11 | """ | ||
12 | |||
13 | >>> from .converter import boolean | ||
14 | >>> class TestConfig(Config): | ||
15 | ... DEBUG = Attribute('DEBUG', boolean) | ||
16 | ... | ||
17 | >>> config = TestConfig() | ||
18 | >>> config.DEBUG | ||
19 | Traceback (most recent call last): | ||
20 | .... | ||
21 | simple_config.exceptions.NotConfigured: value of 'DEBUG' has not been set | ||
22 | >>> config = TestConfig(defaults={'debug':False}) | ||
23 | >>> config.DEBUG | ||
24 | False | ||
25 | >>> config.DEBUG = False | ||
26 | >>> config._raw_config['debug'] | ||
27 | Traceback (most recent call last): | ||
28 | .... | ||
29 | KeyError: 'debug' | ||
30 | >>> config._raw_config['DEBUG'] | ||
31 | False | ||
32 | """ | ||
33 | |||
34 | def __init__(self, name, get_converter=None): | ||
35 | if name.upper() != name: | ||
36 | warnings.warn('upper case name is expected') | ||
37 | self.__name__ = name.upper() | ||
38 | self.get_converter = get_converter | ||
39 | |||
40 | def __get__(self, obj, type=None): | ||
41 | if obj is None: | ||
42 | return self | ||
43 | try: | ||
44 | rv = obj._raw_config[self.__name__] | ||
45 | except KeyError: | ||
46 | raise NotConfigured( | ||
47 | "value of '%s' has not been set" % self.__name__) | ||
48 | if self.get_converter is not None: | ||
49 | rv = self.get_converter(rv) | ||
50 | return rv | ||
51 | |||
52 | def __set__(self, obj, value): | ||
53 | obj._raw_config[self.__name__] = value | ||
54 | |||
55 | |||
56 | class Environment(dict): | ||
57 | def __init__(self, *args, **kwargs): | ||
58 | super(Environment, self).__init__(*args, **kwargs) | ||
59 | |||
60 | @classmethod | ||
61 | def from_env_file(cls, filename, load_env=False): | ||
62 | """从文件中加载环境变量 | ||
63 | """ | ||
64 | if not os.path.exists(filename): | ||
65 | raise FileDoesNotExist('cannot find file: %s' % filename) | ||
66 | with open(filename, 'r', encoding='utf8') as f: | ||
67 | lines = f.readlines() | ||
68 | dct = dict(os.environ) if load_env else {} | ||
69 | |||
70 | for line in lines: | ||
71 | if line.startswith('#'): | ||
72 | continue | ||
73 | if '=' not in line: | ||
74 | if re.match(r'^\s*$', line): | ||
75 | continue | ||
76 | raise InvalidConfig("config line should contain '='") | ||
77 | key, value = line.strip().split('=', 1) | ||
78 | if value[0] == value[-1] and value[0] in ('\'', '"'): | ||
79 | warnings.warn('There is no need to quote string') | ||
80 | dct[key] = value | ||
81 | return cls(dct) | ||
82 | |||
83 | @classmethod | ||
84 | def from_env(cls): | ||
85 | return cls(dict(os.environ)) | ||
86 | |||
87 | |||
88 | class Config(object): | ||
89 | INI_SECTION_NAME = 'settings' | ||
90 | |||
91 | def __init__(self, defaults=None): | ||
92 | self._raw_config = {} | ||
93 | defaults and self._update_config(**defaults) | ||
94 | |||
95 | def from_ini(self, filename): | ||
96 | """ | ||
97 | |||
98 | >>> config = Config(defaults={'DBEUG': True}) | ||
99 | >>> from tempfile import TemporaryFile | ||
100 | >>> filename = TemporaryFile().name | ||
101 | >>> with open(filename, 'w', encoding='utf8') as f: | ||
102 | ... f.write('[settings]\\ndebug=false\\n') | ||
103 | 23 | ||
104 | >>> config.from_ini(filename) | ||
105 | >>> config._raw_config['DEBUG'] | ||
106 | 'false' | ||
107 | >>> with open(filename, 'w', encoding='utf8') as f: | ||
108 | ... f.write('[settings]\\ndebug=True\\n') | ||
109 | 22 | ||
110 | >>> config.from_ini(filename) | ||
111 | >>> config._raw_config['DEBUG'] | ||
112 | 'True' | ||
113 | >>> os.remove(filename) | ||
114 | """ | ||
115 | config = ConfigParser() | ||
116 | with open(filename, 'r', encoding='utf8') as f: | ||
117 | config.read_file(f) | ||
118 | self._update_config(**config[self.INI_SECTION_NAME]) | ||
119 | |||
120 | def from_env_file(self, filename, load_env=False): | ||
121 | """ | ||
122 | |||
123 | >>> config = Config(defaults={'DBEUG': True}) | ||
124 | >>> from tempfile import TemporaryFile | ||
125 | >>> filename = TemporaryFile().name | ||
126 | >>> with open(filename, 'w', encoding='utf8') as f: | ||
127 | ... f.write('debug=false') | ||
128 | 11 | ||
129 | >>> config.from_env_file(filename) | ||
130 | >>> config._raw_config['DEBUG'] | ||
131 | 'false' | ||
132 | >>> with open(filename, 'w', encoding='utf8') as f: | ||
133 | ... f.write('debug=True') | ||
134 | 10 | ||
135 | >>> config.from_env_file(filename) | ||
136 | >>> config._raw_config['DEBUG'] | ||
137 | 'True' | ||
138 | >>> os.remove(filename) | ||
139 | """ | ||
140 | self._update_config(**Environment.from_env_file( | ||
141 | filename, load_env=load_env)) | ||
142 | |||
143 | def from_env_var(self, variable, errors='strict'): | ||
144 | environ = Environment.from_env() | ||
145 | value = environ.get(variable) | ||
146 | if value is None and errors == 'strict': | ||
147 | raise VariableNotExist('cannot get %s from env' % variable) | ||
148 | self._update_config(**{variable: value}) | ||
149 | |||
150 | def from_object(self, obj): | ||
151 | for key in dir(obj): | ||
152 | if key.isupper(): | ||
153 | self._update_config(**{key: getattr(obj, key)}) | ||
154 | |||
155 | def from_dict(self, dct): | ||
156 | """ | ||
157 | >>> from .converter import boolean | ||
158 | >>> class NewConfig(Config): | ||
159 | ... DEBUG = Attribute('DEBUG', get_converter=boolean) | ||
160 | ... | ||
161 | >>> config = NewConfig() | ||
162 | >>> config.DEBUG | ||
163 | Traceback (most recent call last): | ||
164 | .... | ||
165 | simple_config.exceptions.NotConfigured: \ | ||
166 | value of 'DEBUG' has not been set | ||
167 | >>> config.from_dict({'debug': 'true'}) | ||
168 | >>> config.DEBUG | ||
169 | True | ||
170 | >>> config.from_dict({'Debug': ''}) | ||
171 | >>> config.DEBUG | ||
172 | False | ||
173 | |||
174 | """ | ||
175 | self._update_config(**dct) | ||
176 | |||
177 | def _update_config(self, **kwargs): | ||
178 | for key, value in kwargs.items(): | ||
179 | self._raw_config[key.upper()] = value | ||
180 | |||
181 | def __setattr__(self, key, value): | ||
182 | """ | ||
183 | >>> config = Config() | ||
184 | >>> config._raw_config['DEBUG'] | ||
185 | Traceback (most recent call last): | ||
186 | .... | ||
187 | KeyError: 'DEBUG' | ||
188 | >>> config.DEBUG = False | ||
189 | >>> config._raw_config | ||
190 | {'DEBUG': False} | ||
191 | """ | ||
192 | pre_fields = ('_raw_config', ) | ||
193 | if key in pre_fields: | ||
194 | super().__setattr__(key, value) | ||
195 | else: | ||
196 | if key.upper() != key: | ||
197 | warnings.warn('upper case attribute is expected') | ||
198 | key = key.upper() | ||
199 | self._update_config(**{key: value}) | ||
200 | |||
201 | def get_namespace(self, namespace, lowercase=True, trim_namespace=True): | ||
202 | """ | ||
203 | >>> config = Config( | ||
204 | ... defaults={'A_ID': 1, 'A_NAME': 2, 'A_CITY': 3, 'B_ID': 2}) | ||
205 | >>> config.get_namespace('A') == {'_id': 1, '_name': 2, '_city': 3} | ||
206 | True | ||
207 | >>> config.get_namespace('A_') == {'id': 1, 'name': 2, 'city': 3} | ||
208 | True | ||
209 | >>> config.get_namespace('A_', lowercase=False) \\ | ||
210 | ... == {'ID': 1, 'NAME': 2, 'CITY': 3} | ||
211 | True | ||
212 | >>> config.get_namespace( | ||
213 | ... 'A_', lowercase=False, trim_namespace=False) \\ | ||
214 | ... == {'A_ID': 1, 'A_NAME': 2, 'A_CITY': 3} | ||
215 | True | ||
216 | """ | ||
217 | dct = {} | ||
218 | for key in self._raw_config.keys(): | ||
219 | origin_key = key | ||
220 | if not key.startswith(namespace): | ||
221 | continue | ||
222 | if trim_namespace: | ||
223 | key = key[len(namespace):] | ||
224 | # if lowercase: | ||
225 | # key = key.lower() | ||
226 | dct[key] = getattr(self, origin_key) | ||
227 | return dct | ||
228 | |||
229 | def __getattr__(self, key): | ||
230 | try: | ||
231 | return self._raw_config[key] | ||
232 | except KeyError: | ||
233 | raise AttributeError("'%s' object has no attribute '%s'" % | ||
234 | (self.__class__, key)) |
simple_config/exceptions.py
0 → 100755
test/_const_demo.py
0 → 100755
test/test.ini
0 → 100755
test/test_config.py
0 → 100755
1 | import os | ||
2 | |||
3 | from simple_config import Config, ConfigAttribute, converter | ||
4 | from simple_config import exceptions | ||
5 | |||
6 | file_dir = os.path.dirname(os.path.abspath(__file__)) | ||
7 | |||
8 | |||
9 | class ProjectConfig(Config): | ||
10 | ETCD_HOST = ConfigAttribute('ETCD_HOST') | ||
11 | ETCD_PORT = ConfigAttribute('ETCD_PORT', get_converter=int) | ||
12 | ETCD_USER = ConfigAttribute('ETCD_USER') | ||
13 | ETCD_PASSWD = ConfigAttribute('ETCD_PASSWD') | ||
14 | |||
15 | HTTP_SERVER = ConfigAttribute( | ||
16 | 'HTTP_SERVER', get_converter=converter.server) | ||
17 | |||
18 | DEBUG = ConfigAttribute('DEBUG', get_converter=converter.boolean) | ||
19 | ALIAS_DEBUG = ConfigAttribute('DEBUG', get_converter=converter.boolean) | ||
20 | |||
21 | ALLOWED_HOSTS = ConfigAttribute( | ||
22 | 'ALLOWED_HOSTS', get_converter=converter.Csv()) | ||
23 | |||
24 | |||
25 | def test_defaults(): | ||
26 | config = ProjectConfig(defaults={'DEBUG': False, 'ETCD_HOST': 'test'}) | ||
27 | assert config.DEBUG is False | ||
28 | assert config.ALIAS_DEBUG is False | ||
29 | assert config.ETCD_HOST == 'test' | ||
30 | try: | ||
31 | config.ETCD_PORT | ||
32 | except exceptions.NotConfigured: | ||
33 | pass | ||
34 | else: | ||
35 | raise Exception('exception is expected') | ||
36 | |||
37 | |||
38 | def test_env_file(): | ||
39 | config = ProjectConfig(defaults={'DEBUG': False}) | ||
40 | assert config.DEBUG is False | ||
41 | config.from_env_file(os.path.join(file_dir, 'test_env')) | ||
42 | assert config.DEBUG is True | ||
43 | assert config.ETCD_PORT == 80 | ||
44 | assert config.get_namespace('ETCD_') == { | ||
45 | 'host': '1.1.1.1', | ||
46 | 'passwd': '0hyxdryq_CZ', | ||
47 | 'port': 80, | ||
48 | 'user': 'test' | ||
49 | } | ||
50 | assert config.ALLOWED_HOSTS == ['*.test.com', 'api.test.com', '*'] | ||
51 | assert config.ALIAS_DEBUG is True | ||
52 | assert config.USER_NAME == 'root' | ||
53 | |||
54 | |||
55 | def test_ini(): | ||
56 | config = ProjectConfig(defaults={'DEBUG': False}) | ||
57 | assert config.DEBUG is False | ||
58 | config.from_ini(os.path.join(file_dir, 'test.ini')) | ||
59 | assert config.DEBUG is True | ||
60 | assert config.ETCD_PORT == 80 | ||
61 | assert config.get_namespace('ETCD_') == { | ||
62 | 'host': '1.1.1.1', | ||
63 | 'passwd': '0hyxdryq_CZ', | ||
64 | 'port': 80, | ||
65 | 'user': 'test' | ||
66 | } | ||
67 | assert config.ALLOWED_HOSTS == ['*.test.com', 'api.test.com', '*'] | ||
68 | assert config.ALIAS_DEBUG is True | ||
69 | assert config.USER_NAME == 'root\nroot\nroot\nroot' |
test/test_const.py
0 → 100755
1 | import importlib.machinery | ||
2 | import os | ||
3 | |||
4 | filename = os.path.join( | ||
5 | os.path.dirname(os.path.abspath(__file__)), '_const_demo.py') | ||
6 | |||
7 | loader = importlib.machinery.SourceFileLoader('const_demo', filename) | ||
8 | const_demo = loader.load_module() | ||
9 | |||
10 | |||
11 | def test_const(): | ||
12 | # 变量只能被赋值一次 | ||
13 | const_demo.B = 2 | ||
14 | try: | ||
15 | const_demo.B = 3 | ||
16 | except const_demo.ConstError: | ||
17 | pass | ||
18 | else: | ||
19 | raise Exception('ConstError is expected') | ||
20 | |||
21 | # module中赋值的变量能够被自动加载 | ||
22 | assert const_demo.A == 2 | ||
23 | try: | ||
24 | const_demo.A = 2 | ||
25 | except const_demo.ConstError: | ||
26 | pass | ||
27 | else: | ||
28 | raise Exception('ConstError is expected') | ||
29 | assert not hasattr(const_demo, 'b') |
-
Please register or sign in to post a comment