e282cfeb by Gruel

simple config

0 parents
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/
1 [nosetests]
2 verbosity=3
3 with-doctest=1
4 with-coverage=1
1 [style]
2 based_on_style = pep8
3 spaces_before_comment = 4
4 split_before_logical_operator = true
5 coalesce_brackets = true
6 ALLOW_SPLIT_BEFORE_DICT_VALUE=false
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 ```
1 coverage==4.5.2
2 nose==1.3.7
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 )
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'
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)
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))
1 class BaseError(Exception):
2 pass
3
4
5 class ConvertError(BaseError):
6 pass
7
8
9 class FileDoesNotExist(BaseError):
10 pass
11
12
13 class VariableNotExist(BaseError):
14 pass
15
16
17 class NotConfigured(BaseError):
18 pass
19
20
21 class InvalidConfig(BaseError):
22 pass
1 import sys
2
3 from simple_config import Const
4
5
6 A = 1
7 A = 2
8 # b = 1
9
10
11 sys.modules[__name__] = Const.from_current_module(__name__)
1 [settings]
2 ETCD_HOST=1.1.1.1
3 ETCD_PORT=80
4 ETCD_PASSWD=0hyxdryq_CZ
5 ETCD_USER=test
6
7 HTTP_SERVER=http://test.com
8
9 DEBUG=true
10
11 ALLOWED_HOSTS=*.test.com, api.test.com, *
12
13 USER_NAME=root
14 root
15 root
16 root
17
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'
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')
1 ETCD_HOST=1.1.1.1
2 ETCD_PORT=80
3 ETCD_PASSWD=0hyxdryq_CZ
4 ETCD_USER=test
5
6 HTTP_SERVER=http://test.com
7
8 DEBUG=true
9
10 ALLOWED_HOSTS=*.test.com, api.test.com, *
11
12 USER_NAME=root
13
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!