mirror of https://github.com/sgoudham/Enso-Bot.git
Installing requirements
parent
18b990c10d
commit
14ea256537
@ -0,0 +1 @@
|
||||
pip
|
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2004-2020, Vinay Sajip
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -0,0 +1,26 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: config
|
||||
Version: 0.5.0.post0
|
||||
Summary: A hierarchical, easy-to-use, powerful configuration module for Python
|
||||
Home-page: http://docs.red-dove.com/cfg/python.html
|
||||
Author: Vinay Sajip
|
||||
Author-email: vinay_sajip@red-dove.com
|
||||
Maintainer: Vinay Sajip
|
||||
Maintainer-email: vinay_sajip@red-dove.com
|
||||
License: Copyright (C) 2004-2020 by Vinay Sajip. All Rights Reserved. See LICENSE for license.
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Topic :: Software Development
|
||||
|
||||
This module allows a hierarchical configuration scheme with support for mappings and sequences, cross-references between one part of the configuration and another, the ability to flexibly access real Python objects without full-blown eval(), an include facility, simple expression evaluation and the ability to change, save, cascade and merge configurations. Interfaces easily with environment variables and command-line options.
|
||||
|
||||
|
@ -0,0 +1,12 @@
|
||||
config-0.5.0.post0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
config-0.5.0.post0.dist-info/LICENSE,sha256=NpAuOJodhJmffkN3U8ThnyJHcn5SL4v4cnRc2Pg1r6w,1524
|
||||
config-0.5.0.post0.dist-info/METADATA,sha256=ckMDJLQkRBs0upCgvHbR1uWMj9o_GEQ0qaexVnRy9WQ,1384
|
||||
config-0.5.0.post0.dist-info/RECORD,,
|
||||
config-0.5.0.post0.dist-info/WHEEL,sha256=h_aVn5OB2IERUjMbi2pucmR_zzWJtk303YXvhh60NJ8,110
|
||||
config-0.5.0.post0.dist-info/top_level.txt,sha256=9hK4m828QBN59kTX5IVy40cPd9zUw5QWQF2AlSrXCJ4,7
|
||||
config/__init__.py,sha256=IfOXd4eAO8BmzKm0_elrZYTFEVWpXEsQKSPrEGqtGtM,24881
|
||||
config/__pycache__/__init__.cpython-36.pyc,,
|
||||
config/__pycache__/parser.cpython-36.pyc,,
|
||||
config/__pycache__/tokens.cpython-36.pyc,,
|
||||
config/parser.py,sha256=i4Y0GOIerLbbnWUhpCC_cQS2Os232XWk7g91xdTJJa0,14313
|
||||
config/tokens.py,sha256=O2OHc8KoWjSlhzoVKETX6HQyMjs5EQM0JnScNbAaL20,34534
|
@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.33.4)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
@ -0,0 +1 @@
|
||||
config
|
@ -0,0 +1,785 @@
|
||||
#
|
||||
# Copyright 2019-2020 by Vinay Sajip. All Rights Reserved.
|
||||
#
|
||||
try:
|
||||
from collections.abc import Mapping
|
||||
except ImportError:
|
||||
from collections import Mapping
|
||||
import datetime
|
||||
import importlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from .tokens import Token, SCALAR_TOKENS, WORD, BACKTICK, DOLLAR
|
||||
from .parser import (
|
||||
Parser,
|
||||
MappingBody,
|
||||
ListBody,
|
||||
ASTNode,
|
||||
ODict,
|
||||
open_file,
|
||||
RecognizerError,
|
||||
ParserError,
|
||||
)
|
||||
|
||||
__all__ = ['Config', 'ConfigFormatError', 'ConfigError']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if sys.version_info[0] < 3:
|
||||
|
||||
class timezone(datetime.tzinfo):
|
||||
def __init__(self, td):
|
||||
self.td = td
|
||||
|
||||
def utcoffset(self, dt):
|
||||
return self.td
|
||||
|
||||
def dst(self, dt): # pragma: no cover
|
||||
return datetime.timedelta(0)
|
||||
|
||||
|
||||
else:
|
||||
from datetime import timezone
|
||||
|
||||
basestring = str
|
||||
|
||||
__version__ = '0.5.0.post0'
|
||||
|
||||
|
||||
class ConfigFormatError(ParserError):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
# This is a marker used in get(key, defaultValue) to catch rather than KeyError
|
||||
class KeyNotFoundError(ConfigError):
|
||||
pass
|
||||
|
||||
|
||||
def _parse_path(path):
|
||||
p = Parser(io.StringIO(path))
|
||||
try:
|
||||
p.advance()
|
||||
if p.token.kind != WORD:
|
||||
raise ConfigError('Invalid path: %s' % path)
|
||||
result = p.primary()
|
||||
if not p.at_end:
|
||||
raise ConfigError('Invalid path: %s' % path)
|
||||
except RecognizerError as e:
|
||||
raise ConfigError('Invalid path: %s: %s' % (path, e))
|
||||
return result
|
||||
|
||||
|
||||
def _path_iterator(path):
|
||||
def visit(node):
|
||||
if isinstance(node, Token):
|
||||
yield node
|
||||
else:
|
||||
op = node['op']
|
||||
if 'operand' in node:
|
||||
for val in visit(node['operand']):
|
||||
yield val
|
||||
else:
|
||||
for val in visit(node['lhs']):
|
||||
yield val
|
||||
if op == '.':
|
||||
yield op, node['rhs'].value
|
||||
else:
|
||||
assert op in ('[', ':')
|
||||
yield op, node['rhs']
|
||||
|
||||
for v in visit(path):
|
||||
yield v
|
||||
|
||||
|
||||
def _to_source(node):
|
||||
if isinstance(node, Token):
|
||||
return str(node.value)
|
||||
pi = _path_iterator(node)
|
||||
parts = [next(pi).value]
|
||||
for op, operand in pi:
|
||||
if op == '.':
|
||||
parts.append('.')
|
||||
parts.append(operand)
|
||||
elif op == ':':
|
||||
parts.append('[')
|
||||
start, stop, step = operand
|
||||
if start is not None:
|
||||
parts.append(_to_source(start))
|
||||
parts.append(':')
|
||||
if stop is not None:
|
||||
parts.append(_to_source(stop))
|
||||
if step is not None:
|
||||
parts.append(':')
|
||||
parts.append(_to_source(step))
|
||||
parts.append(']')
|
||||
elif op == '[':
|
||||
parts.append('[')
|
||||
parts.append(_to_source(operand))
|
||||
parts.append(']')
|
||||
else: # pragma: no cover
|
||||
raise ConfigError('Unable to navigate: %s' % node)
|
||||
return ''.join(parts)
|
||||
|
||||
|
||||
def _unwrap(o):
|
||||
if isinstance(o, DictWrapper):
|
||||
result = o.as_dict()
|
||||
elif isinstance(o, ListWrapper):
|
||||
result = o.as_list()
|
||||
else:
|
||||
result = o
|
||||
return result
|
||||
|
||||
|
||||
# noinspection PyUnboundLocalVariable
|
||||
_SYSTEM_TYPES = (basestring, bool, int, float, datetime.datetime, datetime.date)
|
||||
# noinspection PyTypeChecker
|
||||
_SCALAR_TYPES = _SYSTEM_TYPES + (Token,)
|
||||
|
||||
|
||||
def _merge_dicts(target, source):
|
||||
for k, v in source.items():
|
||||
if k in target and isinstance(target[k], dict) and isinstance(v, Mapping):
|
||||
_merge_dicts(target[k], v)
|
||||
else:
|
||||
target[k] = source[k]
|
||||
|
||||
|
||||
# use negative lookahead to disallow anything starting with a digit.
|
||||
_IDENTIFIER_PATTERN = re.compile(r'^(?!\d)(\w+)$', re.U)
|
||||
|
||||
|
||||
def is_identifier(s):
|
||||
return bool(_IDENTIFIER_PATTERN.match(s))
|
||||
|
||||
|
||||
class DictWrapper(object):
|
||||
def __init__(self, config, data):
|
||||
self.config = config
|
||||
self._data = data
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyNotFoundError:
|
||||
return default
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not isinstance(key, basestring):
|
||||
raise ConfigError('string required, but found %r' % key)
|
||||
data = self._data
|
||||
config = self.config
|
||||
if key in data or is_identifier(key):
|
||||
try:
|
||||
result = config._evaluated(data[key])
|
||||
except KeyError:
|
||||
raise KeyNotFoundError('not found in configuration: %s' % key)
|
||||
else:
|
||||
path = _parse_path(key)
|
||||
result = config._get_from_path(path)
|
||||
# data[key] = result
|
||||
return result
|
||||
|
||||
def __len__(self): # pragma: no cover
|
||||
return len(self._data)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def __or__(self, other):
|
||||
assert isinstance(other, type(self))
|
||||
data = self.as_dict()
|
||||
_merge_dicts(data, other.as_dict())
|
||||
return type(self)(self.config, data)
|
||||
|
||||
__add__ = __or__
|
||||
|
||||
def __sub__(self, other):
|
||||
assert isinstance(other, type(self))
|
||||
data = dict(self._data)
|
||||
for k in other._data:
|
||||
data.pop(k, None)
|
||||
return type(self)(self.config, data)
|
||||
|
||||
def as_dict(self):
|
||||
result = {}
|
||||
for k, v in self._data.items():
|
||||
v = self[k]
|
||||
if isinstance(v, (DictWrapper, Config)):
|
||||
v = v.as_dict()
|
||||
elif isinstance(v, ListWrapper):
|
||||
v = v.as_list()
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
s = str(', '.join(self._data.keys()))
|
||||
if len(s) > 60:
|
||||
s = s[:57] + '...'
|
||||
return '%s(%s)' % (self.__class__.__name__, s)
|
||||
|
||||
|
||||
class ListWrapper(object):
|
||||
def __init__(self, config, data):
|
||||
self.config = config
|
||||
self._data = data
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def __getitem__(self, index):
|
||||
assert isinstance(index, int) # slices handled in Evaluator
|
||||
result = self._data[index]
|
||||
evaluated = self.config._evaluated(result)
|
||||
if evaluated is not result:
|
||||
self._data[index] = result = evaluated
|
||||
return result
|
||||
|
||||
def __add__(self, other):
|
||||
assert isinstance(other, type(self))
|
||||
return type(self)(self.config, self.as_list() + other.as_list())
|
||||
|
||||
def as_list(self):
|
||||
result = []
|
||||
for item in self:
|
||||
if isinstance(item, (DictWrapper, Config)):
|
||||
item = item.as_dict()
|
||||
elif isinstance(item, ListWrapper):
|
||||
item = item.as_list()
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
s = str(self.as_list())
|
||||
if len(s) > 60:
|
||||
s = s[:57] + '...'
|
||||
return '%s(%s)' % (self.__class__.__name__, s)
|
||||
|
||||
|
||||
_ISO_DATETIME_PATTERN = re.compile(
|
||||
r'\d{4}-\d{2}-\d{2}(([ T])'
|
||||
r'((?P<time>\d{2}:\d{2}:\d{2})'
|
||||
r'(?P<ms>\.\d{1,6})?'
|
||||
r'((?P<sign>[+-])(?P<oh>\d{2}):'
|
||||
r'(?P<om>\d{2})(:(?P<os>\d{2})'
|
||||
r'(?P<oms>\.\d{1,6})?)?)?))?$'
|
||||
)
|
||||
_DOTTED_WORDS = r'[A-Za-z_]\w*(\.[A-Za-z_]\w*)*'
|
||||
_COLON_OBJECT_PATTERN = re.compile('%s:(%s)?$' % (_DOTTED_WORDS, _DOTTED_WORDS))
|
||||
_DOTTED_OBJECT_PATTERN = re.compile('%s$' % _DOTTED_WORDS)
|
||||
_ENV_VALUE_PATTERN = re.compile(r'\$(\w+)(\|(.*))?$')
|
||||
|
||||
|
||||
class Evaluator(object):
|
||||
"""
|
||||
This class is used to evaluate AST nodes. A separate class for this (rather
|
||||
than putting the functionality in Config) because an evaluation context is
|
||||
sometimes required. For example, resolving references needs to keep track
|
||||
of references already seen in an evaluation, to catch circular references.
|
||||
That needs to be done per-evaluation.
|
||||
"""
|
||||
|
||||
op_map = {
|
||||
'@': 'eval_at',
|
||||
'$': 'eval_reference',
|
||||
':': 'eval_slice',
|
||||
'+': 'eval_add',
|
||||
'-': 'eval_subtract',
|
||||
'*': 'eval_multiply',
|
||||
'**': 'eval_power',
|
||||
'/': 'eval_divide',
|
||||
'//': 'eval_integer_divide',
|
||||
'%': 'eval_modulo',
|
||||
'<<': 'eval_left_shift',
|
||||
'>>': 'eval_right_shift',
|
||||
'|': 'eval_bitor',
|
||||
'&': 'eval_bitand',
|
||||
'^': 'eval_bitxor',
|
||||
'or': 'eval_logor',
|
||||
'and': 'eval_logand',
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.refs_seen = {}
|
||||
|
||||
def evaluate(self, node):
|
||||
result = node
|
||||
if isinstance(node, Token):
|
||||
if node.kind in SCALAR_TOKENS:
|
||||
result = node.value
|
||||
elif node.kind == WORD:
|
||||
try:
|
||||
result = self.config.context[node.value]
|
||||
except KeyError:
|
||||
msg = 'Unknown variable \'%s\' at %s' % (node.value, node.start)
|
||||
raise ConfigError(msg)
|
||||
elif node.kind == BACKTICK:
|
||||
result = self.config.convert_string(node.value)
|
||||
else: # pragma: no cover
|
||||
raise NotImplementedError('Unable to evaluate %s' % node)
|
||||
elif isinstance(node, ASTNode):
|
||||
op = node['op']
|
||||
meth = self.op_map[op]
|
||||
result = getattr(self, meth)(node)
|
||||
if isinstance(result, (MappingBody, ListBody)):
|
||||
result = self.config._wrap(result)
|
||||
return result
|
||||
|
||||
def eval_at(self, node):
|
||||
operand = node['operand']
|
||||
config = self.config
|
||||
fn = config._evaluated(operand)
|
||||
if not isinstance(fn, basestring):
|
||||
raise ConfigError('@ operand must be a string, but is %s' % fn)
|
||||
found = False
|
||||
if os.path.isabs(fn):
|
||||
if os.path.isfile(fn):
|
||||
p = fn
|
||||
found = True
|
||||
else:
|
||||
p = os.path.join(config.rootdir, fn)
|
||||
if os.path.isfile(p):
|
||||
found = True
|
||||
else:
|
||||
for ip in config.include_path:
|
||||
p = os.path.join(ip, fn)
|
||||
if os.path.isfile(p):
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise ConfigError('Unable to locate %s' % fn)
|
||||
with open_file(p) as f:
|
||||
p = Parser(f)
|
||||
result = p.container()
|
||||
if isinstance(result, MappingBody):
|
||||
cfg = Config(
|
||||
None, context=config.context, cache=self.config._cache is not None
|
||||
)
|
||||
cfg._parent = config
|
||||
cfg._data = cfg._wrap_mapping(result)
|
||||
result = cfg
|
||||
return result
|
||||
|
||||
def _get_from_path(self, path):
|
||||
def is_ref(n):
|
||||
return isinstance(n, ASTNode) and n['op'] == DOLLAR
|
||||
|
||||
pi = _path_iterator(path)
|
||||
first = next(pi)
|
||||
config = self.config
|
||||
node = config._data[first.value]
|
||||
result = self.evaluate(node)
|
||||
# We start the evaluation with the current instance, but a path may
|
||||
# cross sub-configuration boundaries, and references must always be
|
||||
# evaluated in the context of the immediately enclosing configuration,
|
||||
# not the top-level configuration (references are relative to the
|
||||
# root of the enclosing configuration - otherwise configurations would
|
||||
# not be standalone. So whenever we cross a sub-configuration boundary,
|
||||
# the current_evaluator has to be pegged to that sub-configuration.
|
||||
current_evaluator = self
|
||||
for op, operand in pi:
|
||||
need_string = False
|
||||
if not isinstance(result, (ListWrapper, DictWrapper, Config)):
|
||||
container = result
|
||||
else:
|
||||
container = result._data
|
||||
if isinstance(result, Config):
|
||||
current_evaluator = result._evaluator
|
||||
need_string = not isinstance(result, ListWrapper)
|
||||
sliced = False
|
||||
if isinstance(operand, tuple):
|
||||
operand = slice(*[current_evaluator.evaluate(item) for item in operand])
|
||||
sliced = True
|
||||
if not isinstance(result, ListWrapper):
|
||||
raise ConfigError('slices can only operate on lists')
|
||||
elif op != '.':
|
||||
operand = current_evaluator.evaluate(operand)
|
||||
if need_string:
|
||||
if not isinstance(operand, basestring):
|
||||
raise ConfigError('string required, but found %r' % operand)
|
||||
elif not isinstance(operand, (int, slice)):
|
||||
raise ConfigError(
|
||||
'integer or slice required, but found \'%s\'' % operand
|
||||
)
|
||||
try:
|
||||
v = container[operand] # always use indexing, never attr
|
||||
except IndexError:
|
||||
raise ConfigError('index out of range: %s' % operand)
|
||||
except KeyError:
|
||||
raise KeyNotFoundError('not found in configuration: %s' % operand)
|
||||
if is_ref(v):
|
||||
vid = id(v)
|
||||
if vid in current_evaluator.refs_seen:
|
||||
parts = []
|
||||
for v in current_evaluator.refs_seen.values():
|
||||
parts.append(
|
||||
'%s %s' % (_to_source(v), v['operand']['lhs'].start)
|
||||
)
|
||||
s = ', '.join(sorted(parts))
|
||||
raise ConfigError('Circular reference: %s' % s)
|
||||
current_evaluator.refs_seen[vid] = v
|
||||
if sliced:
|
||||
v = ListBody(v) # ListBody gets wrapped, but not list
|
||||
# v = config._wrap(v)
|
||||
evaluated = current_evaluator.evaluate(v)
|
||||
if evaluated is not v:
|
||||
container[operand] = evaluated
|
||||
result = evaluated
|
||||
return result
|
||||
|
||||
def eval_reference(self, node):
|
||||
result = self._get_from_path(node['operand'])
|
||||
return result
|
||||
|
||||
def eval_add(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
result = lhs + rhs
|
||||
if isinstance(result, list):
|
||||
result = ListWrapper(lhs.config, result)
|
||||
return result
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to add %s to %s' % (rhs, lhs))
|
||||
|
||||
def eval_subtract(self, node):
|
||||
if 'operand' in node:
|
||||
# unary
|
||||
operand = self.evaluate(node['operand'])
|
||||
try:
|
||||
return -operand
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to negate %s' % operand)
|
||||
else:
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs - rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to subtract %s from %s' % (rhs, lhs))
|
||||
|
||||
def eval_multiply(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs * rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to multiply %s by %s' % (lhs, rhs))
|
||||
|
||||
def eval_divide(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs / rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to divide %s by %s' % (lhs, rhs))
|
||||
|
||||
def eval_integer_divide(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs // rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to integer-divide %s by %s' % (lhs, rhs))
|
||||
|
||||
def eval_modulo(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs % rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to compute %s modulo %s' % (lhs, rhs))
|
||||
|
||||
def eval_power(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs ** rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to divide %s by %s' % (lhs, rhs))
|
||||
|
||||
def eval_left_shift(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs << rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to left-shift %s by %s' % (lhs, rhs))
|
||||
|
||||
def eval_right_shift(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs >> rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to right-shift %s by %s' % (lhs, rhs))
|
||||
|
||||
def eval_bitor(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs | rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to bitwise-or %s and %s' % (lhs, rhs))
|
||||
|
||||
def eval_bitand(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs & rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to bitwise-and %s and %s' % (lhs, rhs))
|
||||
|
||||
def eval_bitxor(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
rhs = self.evaluate(node['rhs'])
|
||||
try:
|
||||
return lhs ^ rhs
|
||||
except TypeError:
|
||||
raise ConfigError('Unable to bitwise-xor %s and %s' % (lhs, rhs))
|
||||
|
||||
def eval_logor(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
return lhs or self.evaluate(node['rhs'])
|
||||
|
||||
def eval_logand(self, node):
|
||||
lhs = self.evaluate(node['lhs'])
|
||||
return lhs and self.evaluate(node['rhs'])
|
||||
|
||||
|
||||
def _walk_dotted_path(result, dotted_path):
|
||||
if isinstance(dotted_path, basestring):
|
||||
parts = dotted_path.split('.')
|
||||
else:
|
||||
parts = dotted_path
|
||||
for p in parts:
|
||||
result = getattr(result, p)
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_dotted_object(s):
|
||||
parts = s.split('.')
|
||||
modname = parts.pop(0)
|
||||
# first part must be a module/package.
|
||||
result = importlib.import_module(modname)
|
||||
while parts:
|
||||
p = parts[0]
|
||||
s = '%s.%s' % (modname, p)
|
||||
try:
|
||||
result = importlib.import_module(s)
|
||||
parts.pop(0)
|
||||
modname = s
|
||||
except ImportError:
|
||||
result = _walk_dotted_path(result, parts)
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_colon_object(s):
|
||||
module_name, dotted_path = s.split(':')
|
||||
if module_name in sys.modules:
|
||||
mod = sys.modules[module_name]
|
||||
else:
|
||||
mod = importlib.import_module(module_name)
|
||||
if not dotted_path:
|
||||
result = mod
|
||||
else:
|
||||
result = _walk_dotted_path(mod, dotted_path)
|
||||
return result
|
||||
|
||||
|
||||
def _default_convert_string(s):
|
||||
result = s
|
||||
m = _ISO_DATETIME_PATTERN.match(s)
|
||||
if m:
|
||||
gd = m.groupdict()
|
||||
if not gd['time']:
|
||||
result = datetime.datetime.strptime(m.string, '%Y-%m-%d').date()
|
||||
else:
|
||||
s = '%s %s' % (m.string[:10], gd['time'])
|
||||
result = datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
|
||||
if gd['ms']:
|
||||
ms = int(float(gd['ms']) * 1e6)
|
||||
result = result.replace(microsecond=ms)
|
||||
if gd['oh']:
|
||||
oh = int(gd['oh'], 10)
|
||||
om = int(gd['om'], 10)
|
||||
osec = oms = 0
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
if gd['os']:
|
||||
osec = int(gd['os'], 10)
|
||||
if gd['oms']:
|
||||
oms = int(float(gd['oms']) * 1e6)
|
||||
td = datetime.timedelta(
|
||||
hours=oh, minutes=om, seconds=osec, microseconds=oms
|
||||
)
|
||||
if gd['sign'] == '-':
|
||||
td = -td
|
||||
tzinfo = timezone(td)
|
||||
result = result.replace(tzinfo=tzinfo)
|
||||
else:
|
||||
m = _ENV_VALUE_PATTERN.match(s)
|
||||
if m:
|
||||
key, _, default = m.groups()
|
||||
result = os.environ.get(key, default)
|
||||
else:
|
||||
m = _COLON_OBJECT_PATTERN.match(s)
|
||||
try:
|
||||
if m:
|
||||
result = _resolve_colon_object(s)
|
||||
else:
|
||||
m = _DOTTED_OBJECT_PATTERN.match(s)
|
||||
if m:
|
||||
result = _resolve_dotted_object(s)
|
||||
except ImportError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
class Config(object):
|
||||
def __init__(self, stream_or_path, **kwargs):
|
||||
self.context = kwargs.get('context', {})
|
||||
self.include_path = kwargs.get('include_path', [])
|
||||
self.no_duplicates = kwargs.get('no_duplicates', True)
|
||||
self.strict_conversions = kwargs.get('strict_conversions', True)
|
||||
cache = kwargs.get('cache', False)
|
||||
self.path = kwargs.get('path')
|
||||
self.rootdir = kwargs.get('rootdir')
|
||||
self._can_close = False
|
||||
self._parent = self._data = self._stream = None
|
||||
self._cache = {} if cache else None
|
||||
self._string_converter = _default_convert_string
|
||||
self._evaluator = Evaluator(self)
|
||||
if stream_or_path:
|
||||
if isinstance(stream_or_path, basestring):
|
||||
self.load_file(stream_or_path, kwargs.get('encoding'))
|
||||
else:
|
||||
self.load(stream_or_path)
|
||||
|
||||
def _wrap_mapping(self, items):
|
||||
data = ODict()
|
||||
seen = {}
|
||||
result = DictWrapper(self, data)
|
||||
for k, v in items:
|
||||
key = k.value
|
||||
if self.no_duplicates:
|
||||
if key in seen:
|
||||
msg = 'Duplicate key %s seen at %s ' '(previously at %s)' % (
|
||||
key,
|
||||
k.start,
|
||||
seen[key],
|
||||
)
|
||||
raise ConfigError(msg)
|
||||
seen[key] = k.start
|
||||
data[key] = v
|
||||
return result
|
||||
|
||||
def _get_from_path(self, path):
|
||||
# convenience method
|
||||
evaluator = self._evaluator
|
||||
evaluator.refs_seen.clear()
|
||||
return evaluator._get_from_path(path)
|
||||
|
||||
def convert_string(self, s):
|
||||
result = self._string_converter(s)
|
||||
if result is s and self.strict_conversions:
|
||||
raise ConfigError('Unable to convert string \'%s\'' % s)
|
||||
return result
|
||||
|
||||
def _wrap(self, o):
|
||||
if isinstance(o, MappingBody):
|
||||
result = self._wrap_mapping(o)
|
||||
elif isinstance(o, ListBody):
|
||||
result = ListWrapper(self, o)
|
||||
else:
|
||||
result = o
|
||||
return result
|
||||
|
||||
def _evaluated(self, v):
|
||||
return self._evaluator.evaluate(v)
|
||||
|
||||
def _get(self, key, default=None):
|
||||
cached = self._cache is not None
|
||||
if cached and key in self._cache:
|
||||
result = self._cache[key]
|
||||
else:
|
||||
result = self._data.get(key, default)
|
||||
if cached:
|
||||
self._cache[key] = result
|
||||
return result
|
||||
|
||||
def get(self, key, default=None):
|
||||
return _unwrap(self._get(key, default))
|
||||
|
||||
def __getitem__(self, key):
|
||||
cached = self._cache is not None
|
||||
if cached and key in self._cache:
|
||||
result = self._cache[key]
|
||||
else:
|
||||
result = self._data[key]
|
||||
if cached:
|
||||
self._cache[key] = result
|
||||
return _unwrap(result)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def as_dict(self):
|
||||
return self._data.as_dict()
|
||||
|
||||
# for compatibility with old code base (and its tests)
|
||||
|
||||
def __len__(self):
|
||||
result = 0
|
||||
if self._data is not None:
|
||||
result = len(self._data)
|
||||
return result
|
||||
|
||||
def load(self, stream):
|
||||
self._stream = stream
|
||||
path = self.path
|
||||
if path is None:
|
||||
path = getattr(stream, 'name', None)
|
||||
if path is None:
|
||||
rootdir = os.getcwd()
|
||||
else:
|
||||
rootdir = os.path.dirname(os.path.abspath(path))
|
||||
self.rootdir = rootdir
|
||||
self.path = path
|
||||
try:
|
||||
p = Parser(stream)
|
||||
items = p.container()
|
||||
except ParserError as pe:
|
||||
cfe = ConfigFormatError(*pe.args)
|
||||
cfe.location = pe.location
|
||||
raise cfe
|
||||
if not isinstance(items, MappingBody):
|
||||
raise ConfigError('Root configuration must be a mapping')
|
||||
self._data = self._wrap_mapping(items)
|
||||
if self._cache is not None:
|
||||
self._cache.clear()
|
||||
|
||||
def load_file(self, path, encoding=None):
|
||||
with io.open(path, encoding=encoding or 'utf-8') as f:
|
||||
self.load(f)
|
||||
|
||||
def close(self):
|
||||
if self._can_close and self._stream:
|
||||
self._stream.close()
|
||||
|
||||
def __getattr__(self, key):
|
||||
import warnings
|
||||
|
||||
msg = 'Attribute access is deprecated; use indexed access instead.'
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
return self._data[key]
|
@ -0,0 +1,511 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2018-2020 by Vinay Sajip. All Rights Reserved.
|
||||
#
|
||||
|
||||
from collections import OrderedDict
|
||||
import functools
|
||||
import io
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from .tokens import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
open_file = functools.partial(io.open, encoding='utf-8')
|
||||
|
||||
|
||||
class ParserError(RecognizerError):
|
||||
pass
|
||||
|
||||
|
||||
class ODict(OrderedDict):
|
||||
"""
|
||||
This class preserves insertion order for display purposes but is
|
||||
otherwise just a dict.
|
||||
"""
|
||||
|
||||
def __repr__(self): # pragma: no cover
|
||||
result = []
|
||||
for k, v in self.items():
|
||||
result.append('%r: %r' % (k, v))
|
||||
return '{%s}' % ', '.join(result)
|
||||
|
||||
|
||||
class ASTNode(ODict):
|
||||
start = end = None
|
||||
|
||||
|
||||
class MappingBody(list):
|
||||
start = end = None
|
||||
|
||||
|
||||
class ListBody(list):
|
||||
start = end = None
|
||||
|
||||
|
||||
def set_positions(node, start, end):
|
||||
node.start = start
|
||||
node.end = end
|
||||
|
||||
|
||||
def make_unary_expr(op, operand):
|
||||
"""
|
||||
This function makes an AST node for a unary expression
|
||||
"""
|
||||
result = ASTNode()
|
||||
# The str() calls are to avoid repr ugliness in 2.x
|
||||
result[str('op')] = str(op)
|
||||
result[str('operand')] = operand
|
||||
return result
|
||||
|
||||
|
||||
def make_binary_expr(op, lhs, rhs):
|
||||
"""
|
||||
This function makes an AST node for a binary expression
|
||||
"""
|
||||
result = ASTNode()
|
||||
# The str() calls are to avoid repr ugliness in 2.x
|
||||
result[str('op')] = str(op)
|
||||
result[str('lhs')] = lhs
|
||||
result[str('rhs')] = rhs
|
||||
return result
|
||||
|
||||
|
||||
def invalid_index(n, pos):
|
||||
raise ParserError(
|
||||
'Invalid index at %s: expected 1 expression, ' 'found %d' % (pos, n)
|
||||
)
|
||||
|
||||
|
||||
TOKEN_REPRS = {
|
||||
WORD: 'identifier',
|
||||
NEWLINE: 'newline',
|
||||
INTEGER: 'integer',
|
||||
FLOAT: 'floating-point value',
|
||||
COMPLEX: 'complex value',
|
||||
STRING: 'string',
|
||||
BACKTICK: 'backtick-string',
|
||||
EOF: 'EOF',
|
||||
}
|
||||
|
||||
|
||||
def token_repr(kind):
|
||||
if kind in TOKEN_REPRS:
|
||||
return TOKEN_REPRS[kind]
|
||||
if sys.version_info[0] == 2:
|
||||
return "'%s'" % kind
|
||||
return '%r' % kind
|
||||
|
||||
|
||||
VALUE_STARTERS = {WORD, INTEGER, FLOAT, COMPLEX, STRING, BACKTICK, TRUE, FALSE, NONE}
|
||||
|
||||
|
||||
class Parser(object):
|
||||
def __init__(self, stream=None):
|
||||
self.stream = stream
|
||||
if stream:
|
||||
self.tokenizer = Tokenizer(stream)
|
||||
else:
|
||||
self.tokenizer = None
|
||||
self.token = None
|
||||
|
||||
def _make_stream(self, text):
|
||||
if not isinstance(text, text_type):
|
||||
text = text.decode('utf-8')
|
||||
self.stream = io.StringIO(text)
|
||||
self.tokenizer = Tokenizer(self.stream)
|
||||
self.token = None
|
||||
|
||||
@property
|
||||
def at_end(self):
|
||||
return self.token.kind == EOF
|
||||
|
||||
@property
|
||||
def remaining(self): # for debugging
|
||||
return self.tokenizer.remaining
|
||||
|
||||
def advance(self):
|
||||
self.token = self.tokenizer.get_token()
|
||||
return self.token.kind
|
||||
|
||||
def expect(self, tt):
|
||||
if self.token.kind != tt:
|
||||
pe = ParserError(
|
||||
'Expected %s, got %s at %s' % (token_repr(tt), token_repr(self.token.kind), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
result = self.token
|
||||
self.advance()
|
||||
return result
|
||||
|
||||
def consume_newlines(self):
|
||||
tt = self.token.kind
|
||||
while tt == NEWLINE:
|
||||
tt = self.advance()
|
||||
return tt
|
||||
|
||||
def object_key(self):
|
||||
if self.token.kind == STRING:
|
||||
result = self.strings()
|
||||
else:
|
||||
result = self.token
|
||||
self.advance()
|
||||
return result
|
||||
|
||||
def mapping_body(self):
|
||||
result = MappingBody()
|
||||
start = self.token.start
|
||||
tt = self.consume_newlines()
|
||||
if tt in (RCURLY, EOF):
|
||||
end = self.token.end
|
||||
set_positions(result, start, end)
|
||||
return result
|
||||
if tt not in (WORD, STRING):
|
||||
pe = ParserError(
|
||||
'Unexpected %s at %s' % (token_repr(self.token.kind), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
while tt in (WORD, STRING):
|
||||
key = self.object_key()
|
||||
if self.token.kind not in (COLON, ASSIGN):
|
||||
pe = ParserError(
|
||||
'Unexpected %s at %s'
|
||||
% (token_repr(self.token.kind), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
self.advance()
|
||||
self.consume_newlines()
|
||||
value = self.expr()
|
||||
result.append((key, value))
|
||||
tt = self.token.kind
|
||||
if tt in (NEWLINE, COMMA):
|
||||
self.advance()
|
||||
tt = self.consume_newlines()
|
||||
elif tt not in (RCURLY, EOF):
|
||||
pe = ParserError(
|
||||
'Unexpected %s at %s'
|
||||
% (token_repr(self.token.kind), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
return result
|
||||
|
||||
def strings(self):
|
||||
result = self.token
|
||||
start = result.start
|
||||
s = result.value
|
||||
|
||||
if self.advance() == STRING:
|
||||
all_text = []
|
||||
|
||||
while True:
|
||||
all_text.append(s)
|
||||
s = self.token.value
|
||||
end = self.token.end
|
||||
if self.advance() != STRING:
|
||||
break
|
||||
all_text.append(s)
|
||||
s = ''.join(all_text)
|
||||
result = Token(STRING, s, s)
|
||||
result.start = start
|
||||
result.end = end
|
||||
return result
|
||||
|
||||
def list_body(self):
|
||||
result = ListBody()
|
||||
start = self.token.start
|
||||
end = self.token.end
|
||||
tt = self.consume_newlines()
|
||||
while tt in (
|
||||
LCURLY,
|
||||
LBRACK,
|
||||
LPAREN,
|
||||
AT,
|
||||
DOLLAR,
|
||||
BACKTICK,
|
||||
PLUS,
|
||||
MINUS,
|
||||
BITNOT,
|
||||
INTEGER,
|
||||
FLOAT,
|
||||
COMPLEX,
|
||||
TRUE,
|
||||
FALSE,
|
||||
NONE,
|
||||
NOT,
|
||||
STRING,
|
||||
WORD,
|
||||
):
|
||||
value = self.expr()
|
||||
end = self.token.end
|
||||
result.append(value)
|
||||
tt = self.token.kind
|
||||
if tt not in (NEWLINE, COMMA):
|
||||
break
|
||||
self.advance()
|
||||
tt = self.consume_newlines()
|
||||
set_positions(result, start, end)
|
||||
return result
|
||||
|
||||
def list(self):
|
||||
self.expect(LBRACK)
|
||||
result = self.list_body()
|
||||
self.expect(RBRACK)
|
||||
return result
|
||||
|
||||
def value(self):
|
||||
tt = self.token.kind
|
||||
if tt not in VALUE_STARTERS:
|
||||
pe = ParserError(
|
||||
'Unexpected %s when looking for value: %s'
|
||||
% (token_repr(tt), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
if tt == STRING:
|
||||
token = self.strings()
|
||||
else:
|
||||
token = self.token
|
||||
self.advance()
|
||||
return token
|
||||
|
||||
def mapping(self):
|
||||
start = self.expect(LCURLY).start
|
||||
result = self.mapping_body()
|
||||
end = self.expect(RCURLY).end
|
||||
set_positions(result, start, end)
|
||||
return result
|
||||
|
||||
def container(self):
|
||||
self.advance()
|
||||
k = self.consume_newlines()
|
||||
if k == LCURLY:
|
||||
result = self.mapping()
|
||||
elif k == LBRACK:
|
||||
result = self.list()
|
||||
elif k in (WORD, STRING, EOF):
|
||||
result = self.mapping_body()
|
||||
else:
|
||||
pe = ParserError(
|
||||
'Unexpected %s at %s' % (token_repr(self.token.kind), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
self.consume_newlines()
|
||||
return result
|
||||
|
||||
def atom(self):
|
||||
tt = self.token.kind
|
||||
if tt == LCURLY:
|
||||
result = self.mapping()
|
||||
elif tt == LBRACK:
|
||||
result = self.list()
|
||||
elif tt in VALUE_STARTERS:
|
||||
result = self.value()
|
||||
elif tt == DOLLAR:
|
||||
self.advance()
|
||||
self.expect(LCURLY)
|
||||
result = self.primary()
|
||||
self.expect(RCURLY)
|
||||
result = make_unary_expr(tt, result)
|
||||
elif tt == LPAREN:
|
||||
self.advance()
|
||||
result = self.expr()
|
||||
self.expect(RPAREN)
|
||||
else:
|
||||
# import pdb; pdb.set_trace()
|
||||
pe = ParserError(
|
||||
'Unexpected %s at %s' % (token_repr(self.token.kind), self.token.start)
|
||||
)
|
||||
pe.location = self.token.start
|
||||
raise pe
|
||||
return result
|
||||
|
||||
# noinspection PyUnboundLocalVariable
|
||||
def trailer(self):
|
||||
tt = self.token.kind
|
||||
if tt != LBRACK:
|
||||
self.expect(DOT)
|
||||
result = self.expect(WORD)
|
||||
else:
|
||||
|
||||
def get_slice_element():
|
||||
lb = self.list_body()
|
||||
n = len(lb)
|
||||
if n != 1:
|
||||
invalid_index(n, lb.start)
|
||||
return lb[0]
|
||||
|
||||
tt = self.advance()
|
||||
is_slice = False
|
||||
if tt == COLON:
|
||||
# it's a slice like [:xyz:abc]
|
||||
start = None
|
||||
is_slice = True
|
||||
else:
|
||||
elem = get_slice_element()
|
||||
tt = self.token.kind
|
||||
if tt != COLON:
|
||||
result = elem
|
||||
else:
|
||||
start = elem
|
||||
is_slice = True
|
||||
if not is_slice:
|
||||
tt = LBRACK
|
||||
else:
|
||||
step = stop = None
|
||||
# at this point start is either None (if foo[:xyz]) or a
|
||||
# value representing the start. We are pointing at the COLON
|
||||
# after the start value
|
||||
tt = self.advance()
|
||||
if tt == COLON: # no stop, but there might be a step
|
||||
tt = self.advance()
|
||||
if tt != RBRACK:
|
||||
step = get_slice_element()
|
||||
elif tt != RBRACK:
|
||||
stop = get_slice_element()
|
||||
tt = self.token.kind
|
||||
if tt == COLON:
|
||||
tt = self.advance()
|
||||
if tt != RBRACK:
|
||||
step = get_slice_element()
|
||||
result = (start, stop, step)
|
||||
tt = COLON
|
||||
self.expect(RBRACK)
|
||||
return tt, result
|
||||
|
||||
def primary(self):
|
||||
result = self.atom()
|
||||
while self.token.kind in (LBRACK, DOT):
|
||||
op, rhs = self.trailer()
|
||||
result = make_binary_expr(op, result, rhs)
|
||||
return result
|
||||
|
||||
def power(self):
|
||||
result = self.primary()
|
||||
if self.token.kind == POWER:
|
||||
self.advance()
|
||||
rhs = self.u_expr()
|
||||
result = make_binary_expr(POWER, result, rhs)
|
||||
return result
|
||||
|
||||
def u_expr(self):
|
||||
tt = self.token.kind
|
||||
if tt not in (PLUS, MINUS, BITNOT, AT):
|
||||
result = self.power()
|
||||
else:
|
||||
self.advance()
|
||||
result = make_unary_expr(tt, self.u_expr())
|
||||
return result
|
||||
|
||||
def mul_expr(self):
|
||||
result = self.u_expr()
|
||||
while self.token.kind in (STAR, SLASH, SLASHSLASH, MODULO):
|
||||
op = self.token.kind
|
||||
self.advance()
|
||||
rhs = self.u_expr()
|
||||
result = make_binary_expr(op, result, rhs)
|
||||
return result
|
||||
|
||||
def add_expr(self):
|
||||
result = self.mul_expr()
|
||||
while self.token.kind in (PLUS, MINUS):
|
||||
op = self.token.kind
|
||||
self.advance()
|
||||
rhs = self.mul_expr()
|
||||
result = make_binary_expr(op, result, rhs)
|
||||
return result
|
||||
|
||||
def shift_expr(self):
|
||||
result = self.add_expr()
|
||||
while self.token.kind in (LSHIFT, RSHIFT):
|
||||
op = self.token.kind
|
||||
self.advance()
|
||||
rhs = self.add_expr()
|
||||
result = make_binary_expr(op, result, rhs)
|
||||
return result
|
||||
|
||||
def bitand_expr(self):
|
||||
result = self.shift_expr()
|
||||
while self.token.kind == BITAND:
|
||||
self.advance()
|
||||
rhs = self.shift_expr()
|
||||
result = make_binary_expr(BITAND, result, rhs)
|
||||
return result
|
||||
|
||||
def bitxor_expr(self):
|
||||
result = self.bitand_expr()
|
||||
while self.token.kind == BITXOR:
|
||||
self.advance()
|
||||
rhs = self.bitand_expr()
|
||||
result = make_binary_expr(BITXOR, result, rhs)
|
||||
return result
|
||||
|
||||
def bitor_expr(self):
|
||||
result = self.bitxor_expr()
|
||||
while self.token.kind == BITOR:
|
||||
self.advance()
|
||||
rhs = self.bitxor_expr()
|
||||
result = make_binary_expr(BITOR, result, rhs)
|
||||
return result
|
||||
|
||||
def comp_op(self):
|
||||
result = self.token.kind
|
||||
tt = self.advance()
|
||||
advance = False
|
||||
if result == IS and tt == NOT:
|
||||
result = 'is not'
|
||||
advance = True
|
||||
elif result == NOT and tt == IN:
|
||||
result = 'not in'
|
||||
advance = True
|
||||
if advance:
|
||||
self.advance()
|
||||
return result
|
||||
|
||||
def comparison(self):
|
||||
result = self.bitor_expr()
|
||||
while self.token.kind in (LE, LT, GE, GT, EQ, NEQ, ALT_NEQ, IS, IN, NOT):
|
||||
op = self.comp_op()
|
||||
rhs = self.bitor_expr()
|
||||
result = make_binary_expr(op, result, rhs)
|
||||
return result
|
||||
|
||||
def not_expr(self):
|
||||
if self.token.kind != NOT:
|
||||
result = self.comparison()
|
||||
else:
|
||||
self.advance()
|
||||
result = make_unary_expr(NOT, self.not_expr())
|
||||
return result
|
||||
|
||||
def and_expr(self):
|
||||
result = self.not_expr()
|
||||
while self.token.kind == AND:
|
||||
self.advance()
|
||||
rhs = self.not_expr()
|
||||
result = make_binary_expr(AND, result, rhs)
|
||||
return result
|
||||
|
||||
def or_expr(self):
|
||||
result = self.and_expr()
|
||||
while self.token.kind == OR:
|
||||
self.advance()
|
||||
rhs = self.and_expr()
|
||||
result = make_binary_expr(OR, result, rhs)
|
||||
return result
|
||||
|
||||
expr = or_expr
|
||||
|
||||
def parse(self, text, rule='mapping_body'):
|
||||
self._make_stream(text)
|
||||
self.advance()
|
||||
method = getattr(self, rule, None)
|
||||
if not method:
|
||||
raise ValueError('no such rule: %s' % rule)
|
||||
return method()
|
File diff suppressed because it is too large
Load Diff
@ -1,418 +0,0 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: python-decouple
|
||||
Version: 3.3
|
||||
Summary: Strict separation of settings from code.
|
||||
Home-page: http://github.com/henriquebastos/python-decouple/
|
||||
Author: Henrique Bastos
|
||||
Author-email: henrique@bastos.net
|
||||
License: MIT
|
||||
Description: Python Decouple: Strict separation of settings from code
|
||||
========================================================
|
||||
|
||||
*Decouple* helps you to organize your settings so that you can
|
||||
change parameters without having to redeploy your app.
|
||||
|
||||
It also makes it easy for you to:
|
||||
|
||||
#. store parameters in *ini* or *.env* files;
|
||||
#. define comprehensive default values;
|
||||
#. properly convert values to the correct data type;
|
||||
#. have **only one** configuration module to rule all your instances.
|
||||
|
||||
It was originally designed for Django, but became an independent generic tool
|
||||
for separating settings from code.
|
||||
|
||||
.. image:: https://img.shields.io/travis/henriquebastos/python-decouple.svg
|
||||
:target: https://travis-ci.org/henriquebastos/python-decouple
|
||||
:alt: Build Status
|
||||
|
||||
.. image:: https://landscape.io/github/henriquebastos/python-decouple/master/landscape.png
|
||||
:target: https://landscape.io/github/henriquebastos/python-decouple/master
|
||||
:alt: Code Health
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/python-decouple.svg
|
||||
:target: https://pypi.python.org/pypi/python-decouple/
|
||||
:alt: Latest PyPI version
|
||||
|
||||
|
||||
|
||||
.. contents:: Summary
|
||||
|
||||
|
||||
Why?
|
||||
====
|
||||
|
||||
Web framework's settings stores many different kinds of parameters:
|
||||
|
||||
* Locale and i18n;
|
||||
* Middlewares and Installed Apps;
|
||||
* Resource handles to the database, Memcached, and other backing services;
|
||||
* Credentials to external services such as Amazon S3 or Twitter;
|
||||
* Per-deploy values such as the canonical hostname for the instance.
|
||||
|
||||
The first 2 are *project settings* the last 3 are *instance settings*.
|
||||
|
||||
You should be able to change *instance settings* without redeploying your app.
|
||||
|
||||
Why not just use environment variables?
|
||||
---------------------------------------
|
||||
|
||||
*Envvars* works, but since ``os.environ`` only returns strings, it's tricky.
|
||||
|
||||
Let's say you have an *envvar* ``DEBUG=False``. If you run:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if os.environ['DEBUG']:
|
||||
print True
|
||||
else:
|
||||
print False
|
||||
|
||||
It will print **True**, because ``os.environ['DEBUG']`` returns the **string** ``"False"``.
|
||||
Since it's a non-empty string, it will be evaluated as True.
|
||||
|
||||
*Decouple* provides a solution that doesn't look like a workaround: ``config('DEBUG', cast=bool)``.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Install:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install python-decouple
|
||||
|
||||
|
||||
Then use it on your ``settings.py``.
|
||||
|
||||
#. Import the ``config`` object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from decouple import config
|
||||
|
||||
#. Retrieve the configuration parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||
|
||||
Encodings
|
||||
---------
|
||||
Decouple's default encoding is `UTF-8`.
|
||||
|
||||
But you can specify your preferred encoding.
|
||||
|
||||
Since `config` is lazy and only opens the configuration file when it's first needed, you have the chance to change
|
||||
it's encoding right after import.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from decouple import config
|
||||
config.encoding = 'cp1251'
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
If you wish to fallback to your system's default encoding do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import locale
|
||||
from decouple import config
|
||||
config.encoding = locale.getpreferredencoding(False)
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
Where the settings data are stored?
|
||||
-----------------------------------
|
||||
|
||||
*Decouple* supports both *.ini* and *.env* files.
|
||||
|
||||
Ini file
|
||||
~~~~~~~~
|
||||
|
||||
Simply create a ``settings.ini`` next to your configuration module in the form:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[settings]
|
||||
DEBUG=True
|
||||
TEMPLATE_DEBUG=%(DEBUG)s
|
||||
SECRET_KEY=ARANDOMSECRETKEY
|
||||
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
|
||||
PERCENTILE=90%%
|
||||
#COMMENTED=42
|
||||
|
||||
*Note*: Since ``ConfigParser`` supports *string interpolation*, to represent the character ``%`` you need to escape it as ``%%``.
|
||||
|
||||
Env file
|
||||
~~~~~~~~
|
||||
|
||||
Simply create a ``.env`` text file on your repository's root directory in the form:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
DEBUG=True
|
||||
TEMPLATE_DEBUG=True
|
||||
SECRET_KEY=ARANDOMSECRETKEY
|
||||
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
|
||||
PERCENTILE=90%
|
||||
#COMMENTED=42
|
||||
|
||||
Example: How do I use it with Django?
|
||||
-------------------------------------
|
||||
|
||||
Given that I have a ``.env`` file at my repository root directory, here is a snippet of my ``settings.py``.
|
||||
|
||||
I also recommend using `pathlib <https://docs.python.org/3/library/pathlib.html>`_
|
||||
and `dj-database-url <https://pypi.python.org/pypi/dj-database-url/>`_.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# coding: utf-8
|
||||
from decouple import config
|
||||
from unipath import Path
|
||||
from dj_database_url import parse as db_url
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
DATABASES = {
|
||||
'default': config(
|
||||
'DATABASE_URL',
|
||||
default='sqlite:///' + BASE_DIR.child('db.sqlite3'),
|
||||
cast=db_url
|
||||
)
|
||||
}
|
||||
|
||||
TIME_ZONE = 'America/Sao_Paulo'
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||
|
||||
# ...
|
||||
|
||||
Attention with *undefined* parameters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
On the above example, all configuration parameters except ``SECRET_KEY = config('SECRET_KEY')``
|
||||
have a default value to fallback if it does not exist on the ``.env`` file.
|
||||
|
||||
If ``SECRET_KEY`` is not present in the ``.env``, *decouple* will raise an ``UndefinedValueError``.
|
||||
|
||||
This *fail fast* policy helps you avoid chasing misbehaviors when you eventually forget a parameter.
|
||||
|
||||
Overriding config files with environment variables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sometimes you may want to change a parameter value without having to edit the ``.ini`` or ``.env`` files.
|
||||
|
||||
Since version 3.0, *decouple* respects the *unix way*.
|
||||
Therefore environment variables have precedence over config files.
|
||||
|
||||
To override a config parameter you can simply do:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
DEBUG=True python manage.py
|
||||
|
||||
|
||||
How it works?
|
||||
=============
|
||||
|
||||
*Decouple* always searches for *Options* in this order:
|
||||
|
||||
#. Environment variables;
|
||||
#. Repository: ini or .env file;
|
||||
#. default argument passed to config.
|
||||
|
||||
There are 4 classes doing the magic:
|
||||
|
||||
|
||||
- ``Config``
|
||||
|
||||
Coordinates all the configuration retrieval.
|
||||
|
||||
- ``RepositoryIni``
|
||||
|
||||
Can read values from ``os.environ`` and ini files, in that order.
|
||||
|
||||
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
|
||||
|
||||
- ``RepositoryEnv``
|
||||
|
||||
Can read values from ``os.environ`` and ``.env`` files.
|
||||
|
||||
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
|
||||
|
||||
- ``AutoConfig``
|
||||
|
||||
This is a *lazy* ``Config`` factory that detects which configuration repository you're using.
|
||||
|
||||
It recursively searches up your configuration module path looking for a
|
||||
``settings.ini`` or a ``.env`` file.
|
||||
|
||||
Optionally, it accepts ``search_path`` argument to explicitly define
|
||||
where the search starts.
|
||||
|
||||
The **config** object is an instance of ``AutoConfig`` that instantiates a ``Config`` with the proper ``Repository``
|
||||
on the first time it is used.
|
||||
|
||||
|
||||
Understanding the CAST argument
|
||||
-------------------------------
|
||||
|
||||
By default, all values returned by ``decouple`` are ``strings``, after all they are
|
||||
read from ``text files`` or the ``envvars``.
|
||||
|
||||
However, your Python code may expect some other value type, for example:
|
||||
|
||||
* Django's ``DEBUG`` expects a boolean ``True`` or ``False``.
|
||||
* Django's ``EMAIL_PORT`` expects an ``integer``.
|
||||
* Django's ``ALLOWED_HOSTS`` expects a ``list`` of hostnames.
|
||||
* Django's ``SECURE_PROXY_SSL_HEADER`` expects a ``tuple`` with two elements, the name of the header to look for and the required value.
|
||||
|
||||
To meet this need, the ``config`` function accepts a ``cast`` argument which
|
||||
receives any *callable*, that will be used to *transform* the string value
|
||||
into something else.
|
||||
|
||||
Let's see some examples for the above mentioned cases:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> os.environ['DEBUG'] = 'False'
|
||||
>>> config('DEBUG', cast=bool)
|
||||
False
|
||||
|
||||
>>> os.environ['EMAIL_PORT'] = '42'
|
||||
>>> config('EMAIL_PORT', cast=int)
|
||||
42
|
||||
|
||||
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
|
||||
>>> config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
['.localhost', '.herokuapp.com']
|
||||
|
||||
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
|
||||
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
|
||||
('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
As you can see, ``cast`` is very flexible. But the last example got a bit complex.
|
||||
|
||||
Built in Csv Helper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To address the complexity of the last example, *Decouple* comes with an extensible *Csv helper*.
|
||||
|
||||
Let's improve the last example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from decouple import Csv
|
||||
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
|
||||
>>> config('ALLOWED_HOSTS', cast=Csv())
|
||||
['.localhost', '.herokuapp.com']
|
||||
|
||||
You can also have a `default` value that must be a string to be processed by `Csv`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from decouple import Csv
|
||||
>>> config('ALLOWED_HOSTS', default='127.0.0.1', cast=Csv())
|
||||
['127.0.0.1']
|
||||
|
||||
You can also parametrize the *Csv Helper* to return other types of data.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> os.environ['LIST_OF_INTEGERS'] = '1,2,3,4,5'
|
||||
>>> config('LIST_OF_INTEGERS', cast=Csv(int))
|
||||
[1, 2, 3, 4, 5]
|
||||
|
||||
>>> os.environ['COMPLEX_STRING'] = '%virtual_env%\t *important stuff*\t trailing spaces '
|
||||
>>> csv = Csv(cast=lambda s: s.upper(), delimiter='\t', strip=' %*')
|
||||
>>> csv(os.environ['COMPLEX_STRING'])
|
||||
['VIRTUAL_ENV', 'IMPORTANT STUFF', 'TRAILING SPACES']
|
||||
|
||||
By default *Csv* returns a ``list``, but you can get a ``tuple`` or whatever you want using the ``post_process`` argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
|
||||
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
|
||||
('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
|
||||
Contribute
|
||||
==========
|
||||
|
||||
Your contribution is welcome.
|
||||
|
||||
Setup your development environment:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
git clone git@github.com:henriquebastos/python-decouple.git
|
||||
cd python-decouple
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
tox
|
||||
|
||||
*Decouple* supports both Python 2.7 and 3.6. Make sure you have both installed.
|
||||
|
||||
I use `pyenv <https://github.com/pyenv/pyenv#simple-python-version-management-pyenv>`_ to
|
||||
manage multiple Python versions and I described my workspace setup on this article:
|
||||
`The definitive guide to setup my Python workspace
|
||||
<https://medium.com/@henriquebastos/the-definitive-guide-to-setup-my-python-workspace-628d68552e14>`_
|
||||
|
||||
You can submit pull requests and issues for discussion. However I only
|
||||
consider merging tested code.
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Henrique Bastos <henrique at bastos dot net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Framework :: Django
|
||||
Classifier: Framework :: Flask
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Topic :: Software Development :: Libraries
|
@ -1,11 +0,0 @@
|
||||
LICENSE
|
||||
MANIFEST.in
|
||||
README.rst
|
||||
decouple.py
|
||||
setup.cfg
|
||||
setup.py
|
||||
python_decouple.egg-info/PKG-INFO
|
||||
python_decouple.egg-info/SOURCES.txt
|
||||
python_decouple.egg-info/dependency_links.txt
|
||||
python_decouple.egg-info/not-zip-safe
|
||||
python_decouple.egg-info/top_level.txt
|
@ -1 +0,0 @@
|
||||
|
@ -1,7 +0,0 @@
|
||||
..\__pycache__\decouple.cpython-36.pyc
|
||||
..\decouple.py
|
||||
PKG-INFO
|
||||
SOURCES.txt
|
||||
dependency_links.txt
|
||||
not-zip-safe
|
||||
top_level.txt
|
@ -1 +0,0 @@
|
||||
|
@ -0,0 +1 @@
|
||||
pip
|
@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2013 Henrique Bastos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,420 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: python-decouple
|
||||
Version: 3.3
|
||||
Summary: Strict separation of settings from code.
|
||||
Home-page: http://github.com/henriquebastos/python-decouple/
|
||||
Author: Henrique Bastos
|
||||
Author-email: henrique@bastos.net
|
||||
License: MIT
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Framework :: Django
|
||||
Classifier: Framework :: Flask
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
|
||||
Python Decouple: Strict separation of settings from code
|
||||
========================================================
|
||||
|
||||
*Decouple* helps you to organize your settings so that you can
|
||||
change parameters without having to redeploy your app.
|
||||
|
||||
It also makes it easy for you to:
|
||||
|
||||
#. store parameters in *ini* or *.env* files;
|
||||
#. define comprehensive default values;
|
||||
#. properly convert values to the correct data type;
|
||||
#. have **only one** configuration module to rule all your instances.
|
||||
|
||||
It was originally designed for Django, but became an independent generic tool
|
||||
for separating settings from code.
|
||||
|
||||
.. image:: https://img.shields.io/travis/henriquebastos/python-decouple.svg
|
||||
:target: https://travis-ci.org/henriquebastos/python-decouple
|
||||
:alt: Build Status
|
||||
|
||||
.. image:: https://landscape.io/github/henriquebastos/python-decouple/master/landscape.png
|
||||
:target: https://landscape.io/github/henriquebastos/python-decouple/master
|
||||
:alt: Code Health
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/python-decouple.svg
|
||||
:target: https://pypi.python.org/pypi/python-decouple/
|
||||
:alt: Latest PyPI version
|
||||
|
||||
|
||||
|
||||
.. contents:: Summary
|
||||
|
||||
|
||||
Why?
|
||||
====
|
||||
|
||||
Web framework's settings stores many different kinds of parameters:
|
||||
|
||||
* Locale and i18n;
|
||||
* Middlewares and Installed Apps;
|
||||
* Resource handles to the database, Memcached, and other backing services;
|
||||
* Credentials to external services such as Amazon S3 or Twitter;
|
||||
* Per-deploy values such as the canonical hostname for the instance.
|
||||
|
||||
The first 2 are *project settings* the last 3 are *instance settings*.
|
||||
|
||||
You should be able to change *instance settings* without redeploying your app.
|
||||
|
||||
Why not just use environment variables?
|
||||
---------------------------------------
|
||||
|
||||
*Envvars* works, but since ``os.environ`` only returns strings, it's tricky.
|
||||
|
||||
Let's say you have an *envvar* ``DEBUG=False``. If you run:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
if os.environ['DEBUG']:
|
||||
print True
|
||||
else:
|
||||
print False
|
||||
|
||||
It will print **True**, because ``os.environ['DEBUG']`` returns the **string** ``"False"``.
|
||||
Since it's a non-empty string, it will be evaluated as True.
|
||||
|
||||
*Decouple* provides a solution that doesn't look like a workaround: ``config('DEBUG', cast=bool)``.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Install:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install python-decouple
|
||||
|
||||
|
||||
Then use it on your ``settings.py``.
|
||||
|
||||
#. Import the ``config`` object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from decouple import config
|
||||
|
||||
#. Retrieve the configuration parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||
|
||||
Encodings
|
||||
---------
|
||||
Decouple's default encoding is `UTF-8`.
|
||||
|
||||
But you can specify your preferred encoding.
|
||||
|
||||
Since `config` is lazy and only opens the configuration file when it's first needed, you have the chance to change
|
||||
it's encoding right after import.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from decouple import config
|
||||
config.encoding = 'cp1251'
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
If you wish to fallback to your system's default encoding do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import locale
|
||||
from decouple import config
|
||||
config.encoding = locale.getpreferredencoding(False)
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
Where the settings data are stored?
|
||||
-----------------------------------
|
||||
|
||||
*Decouple* supports both *.ini* and *.env* files.
|
||||
|
||||
Ini file
|
||||
~~~~~~~~
|
||||
|
||||
Simply create a ``settings.ini`` next to your configuration module in the form:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[settings]
|
||||
DEBUG=True
|
||||
TEMPLATE_DEBUG=%(DEBUG)s
|
||||
SECRET_KEY=ARANDOMSECRETKEY
|
||||
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
|
||||
PERCENTILE=90%%
|
||||
#COMMENTED=42
|
||||
|
||||
*Note*: Since ``ConfigParser`` supports *string interpolation*, to represent the character ``%`` you need to escape it as ``%%``.
|
||||
|
||||
Env file
|
||||
~~~~~~~~
|
||||
|
||||
Simply create a ``.env`` text file on your repository's root directory in the form:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
DEBUG=True
|
||||
TEMPLATE_DEBUG=True
|
||||
SECRET_KEY=ARANDOMSECRETKEY
|
||||
DATABASE_URL=mysql://myuser:mypassword@myhost/mydatabase
|
||||
PERCENTILE=90%
|
||||
#COMMENTED=42
|
||||
|
||||
Example: How do I use it with Django?
|
||||
-------------------------------------
|
||||
|
||||
Given that I have a ``.env`` file at my repository root directory, here is a snippet of my ``settings.py``.
|
||||
|
||||
I also recommend using `pathlib <https://docs.python.org/3/library/pathlib.html>`_
|
||||
and `dj-database-url <https://pypi.python.org/pypi/dj-database-url/>`_.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# coding: utf-8
|
||||
from decouple import config
|
||||
from unipath import Path
|
||||
from dj_database_url import parse as db_url
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
DATABASES = {
|
||||
'default': config(
|
||||
'DATABASE_URL',
|
||||
default='sqlite:///' + BASE_DIR.child('db.sqlite3'),
|
||||
cast=db_url
|
||||
)
|
||||
}
|
||||
|
||||
TIME_ZONE = 'America/Sao_Paulo'
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
|
||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
|
||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||
|
||||
# ...
|
||||
|
||||
Attention with *undefined* parameters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
On the above example, all configuration parameters except ``SECRET_KEY = config('SECRET_KEY')``
|
||||
have a default value to fallback if it does not exist on the ``.env`` file.
|
||||
|
||||
If ``SECRET_KEY`` is not present in the ``.env``, *decouple* will raise an ``UndefinedValueError``.
|
||||
|
||||
This *fail fast* policy helps you avoid chasing misbehaviors when you eventually forget a parameter.
|
||||
|
||||
Overriding config files with environment variables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sometimes you may want to change a parameter value without having to edit the ``.ini`` or ``.env`` files.
|
||||
|
||||
Since version 3.0, *decouple* respects the *unix way*.
|
||||
Therefore environment variables have precedence over config files.
|
||||
|
||||
To override a config parameter you can simply do:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
DEBUG=True python manage.py
|
||||
|
||||
|
||||
How it works?
|
||||
=============
|
||||
|
||||
*Decouple* always searches for *Options* in this order:
|
||||
|
||||
#. Environment variables;
|
||||
#. Repository: ini or .env file;
|
||||
#. default argument passed to config.
|
||||
|
||||
There are 4 classes doing the magic:
|
||||
|
||||
|
||||
- ``Config``
|
||||
|
||||
Coordinates all the configuration retrieval.
|
||||
|
||||
- ``RepositoryIni``
|
||||
|
||||
Can read values from ``os.environ`` and ini files, in that order.
|
||||
|
||||
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
|
||||
|
||||
- ``RepositoryEnv``
|
||||
|
||||
Can read values from ``os.environ`` and ``.env`` files.
|
||||
|
||||
**Note:** Since version 3.0 *decouple* respects unix precedence of environment variables *over* config files.
|
||||
|
||||
- ``AutoConfig``
|
||||
|
||||
This is a *lazy* ``Config`` factory that detects which configuration repository you're using.
|
||||
|
||||
It recursively searches up your configuration module path looking for a
|
||||
``settings.ini`` or a ``.env`` file.
|
||||
|
||||
Optionally, it accepts ``search_path`` argument to explicitly define
|
||||
where the search starts.
|
||||
|
||||
The **config** object is an instance of ``AutoConfig`` that instantiates a ``Config`` with the proper ``Repository``
|
||||
on the first time it is used.
|
||||
|
||||
|
||||
Understanding the CAST argument
|
||||
-------------------------------
|
||||
|
||||
By default, all values returned by ``decouple`` are ``strings``, after all they are
|
||||
read from ``text files`` or the ``envvars``.
|
||||
|
||||
However, your Python code may expect some other value type, for example:
|
||||
|
||||
* Django's ``DEBUG`` expects a boolean ``True`` or ``False``.
|
||||
* Django's ``EMAIL_PORT`` expects an ``integer``.
|
||||
* Django's ``ALLOWED_HOSTS`` expects a ``list`` of hostnames.
|
||||
* Django's ``SECURE_PROXY_SSL_HEADER`` expects a ``tuple`` with two elements, the name of the header to look for and the required value.
|
||||
|
||||
To meet this need, the ``config`` function accepts a ``cast`` argument which
|
||||
receives any *callable*, that will be used to *transform* the string value
|
||||
into something else.
|
||||
|
||||
Let's see some examples for the above mentioned cases:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> os.environ['DEBUG'] = 'False'
|
||||
>>> config('DEBUG', cast=bool)
|
||||
False
|
||||
|
||||
>>> os.environ['EMAIL_PORT'] = '42'
|
||||
>>> config('EMAIL_PORT', cast=int)
|
||||
42
|
||||
|
||||
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
|
||||
>>> config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
['.localhost', '.herokuapp.com']
|
||||
|
||||
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
|
||||
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
|
||||
('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
As you can see, ``cast`` is very flexible. But the last example got a bit complex.
|
||||
|
||||
Built in Csv Helper
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To address the complexity of the last example, *Decouple* comes with an extensible *Csv helper*.
|
||||
|
||||
Let's improve the last example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from decouple import Csv
|
||||
>>> os.environ['ALLOWED_HOSTS'] = '.localhost, .herokuapp.com'
|
||||
>>> config('ALLOWED_HOSTS', cast=Csv())
|
||||
['.localhost', '.herokuapp.com']
|
||||
|
||||
You can also have a `default` value that must be a string to be processed by `Csv`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> from decouple import Csv
|
||||
>>> config('ALLOWED_HOSTS', default='127.0.0.1', cast=Csv())
|
||||
['127.0.0.1']
|
||||
|
||||
You can also parametrize the *Csv Helper* to return other types of data.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> os.environ['LIST_OF_INTEGERS'] = '1,2,3,4,5'
|
||||
>>> config('LIST_OF_INTEGERS', cast=Csv(int))
|
||||
[1, 2, 3, 4, 5]
|
||||
|
||||
>>> os.environ['COMPLEX_STRING'] = '%virtual_env%\t *important stuff*\t trailing spaces '
|
||||
>>> csv = Csv(cast=lambda s: s.upper(), delimiter='\t', strip=' %*')
|
||||
>>> csv(os.environ['COMPLEX_STRING'])
|
||||
['VIRTUAL_ENV', 'IMPORTANT STUFF', 'TRAILING SPACES']
|
||||
|
||||
By default *Csv* returns a ``list``, but you can get a ``tuple`` or whatever you want using the ``post_process`` argument:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> os.environ['SECURE_PROXY_SSL_HEADER'] = 'HTTP_X_FORWARDED_PROTO, https'
|
||||
>>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple))
|
||||
('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
|
||||
Contribute
|
||||
==========
|
||||
|
||||
Your contribution is welcome.
|
||||
|
||||
Setup your development environment:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
git clone git@github.com:henriquebastos/python-decouple.git
|
||||
cd python-decouple
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
tox
|
||||
|
||||
*Decouple* supports both Python 2.7 and 3.6. Make sure you have both installed.
|
||||
|
||||
I use `pyenv <https://github.com/pyenv/pyenv#simple-python-version-management-pyenv>`_ to
|
||||
manage multiple Python versions and I described my workspace setup on this article:
|
||||
`The definitive guide to setup my Python workspace
|
||||
<https://medium.com/@henriquebastos/the-definitive-guide-to-setup-my-python-workspace-628d68552e14>`_
|
||||
|
||||
You can submit pull requests and issues for discussion. However I only
|
||||
consider merging tested code.
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Henrique Bastos <henrique at bastos dot net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
__pycache__/decouple.cpython-36.pyc,,
|
||||
decouple.py,sha256=wwJojLoN8vLIeHVQJWhRT3vz3Bm4jUeNc-vIFqoYeM8,6548
|
||||
python_decouple-3.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
python_decouple-3.3.dist-info/LICENSE,sha256=TW48LvIp7DwaSG9SyddiNenrfJTbjscai_8K9g5PCqQ,1076
|
||||
python_decouple-3.3.dist-info/METADATA,sha256=XATX3mnYM7Gjv69Y-2501UskD1sH46rFppirjLd0JHs,13053
|
||||
python_decouple-3.3.dist-info/RECORD,,
|
||||
python_decouple-3.3.dist-info/WHEEL,sha256=YUYzQ6UQdoqxXjimOitTqynltBCkwY6qlTfTh2IzqQU,97
|
||||
python_decouple-3.3.dist-info/top_level.txt,sha256=4Z_ufgotqDCUtAtgtampXl_P-jKS9H5ifDfU0J3hD5U,9
|
@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.34.2)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
Loading…
Reference in New Issue