[Python-modules-commits] [python-envparse] 01/02: New upstream version 0.2.0
Sophie Brun
sbrun-guest at moszumanska.debian.org
Wed Oct 25 09:17:50 UTC 2017
This is an automated email from the git hooks/post-receive script.
sbrun-guest pushed a commit to branch debian/master
in repository python-envparse.
commit d1ece48a28b58107195460eec2ebd1c07f327fff
Author: Sophie Brun <sophie at freexian.com>
Date: Wed Oct 25 10:29:57 2017 +0200
New upstream version 0.2.0
---
.gitignore | 12 +++
.travis.yml | 17 ++++
CHANGELOG.rst | 17 ++++
LICENSE | 21 +++++
MANIFEST.in | 1 +
Makefile | 15 ++++
README.rst | 196 +++++++++++++++++++++++++++++++++++++++++++++++
envparse.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++
setup.cfg | 2 +
setup.py | 75 ++++++++++++++++++
tests/envfile | 15 ++++
tests/test_casts.py | 159 ++++++++++++++++++++++++++++++++++++++
tox.ini | 22 ++++++
13 files changed, 769 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0f3232d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+*.py[cod]
+
+build
+dist
+*.egg-info
+.tox
+.cache
+.eggs
+
+# coverage
+.coverage
+htmlcov
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..80c38c6
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,17 @@
+language: python
+python:
+ - "3.5"
+env:
+ matrix:
+ - TOXENV=py27
+ - TOXENV=py32
+ - TOXENV=py33
+ - TOXENV=py34
+ - TOXENV=py35
+ - TOXENV=pypy
+ - TOXENV=pypy3
+ - TOXENV=flake8
+install:
+ - travis_retry pip install tox
+script:
+ - make test
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..54a8c37
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,17 @@
+CHANGELOG
+=========
+
+v0.2.0
+------
+
+- Major rewrite, based on django-environ but made agnostic.
+ - Tox support for running tests with different Python types.
+ - Use pytest for unit tests.
+
+
+v0.1.6
+------
+
+- Use curly-braces for proxied values since shells will attempt to resolve
+dollar-sign values themselves. Dollar-sign style is still supported, but
+deprecated and will be removed in a 1.0 release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..38b21a3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2012 Rick Harris
+Copyright (c) 2013 Daniele Faraglia
+Copyright (c) 2015 Russell Davies
+
+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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0c73842
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.rst LICENSE
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c0e4ef4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,15 @@
+SHELL := /bin/bash
+
+help:
+ @echo 'Makefile for envparse'
+ @echo ''
+ @echo 'Usage:'
+ @echo ' make release push to the PyPI'
+ @echo ' make test run the test suite'
+ @echo ''
+
+release:
+ python setup.py register sdist bdist_wheel upload
+
+test:
+ tox
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..4c05a05
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,196 @@
+envparse
+========
+``envparse`` is a simple utility to parse environment variables.
+
+If you use Heroku and/or subscribe to the tenets of the
+`12 Factor App <http://www.12factor.net/>`_
+you'll be using a lot of environment variable-based configuration in your app.
+``os.environ`` is a great choice to start off with but over time you'll find
+yourself duplicating quite a bit of code around handling raw environment
+variables.
+
+``envparse`` aims to eliminate this duplicated, often inconsistent parsing
+code and instead provide a single, easy-to-use wrapper.
+
+Ideas, and code portions, have been taken from `django-environ
+<https://github.com/joke2k/django-environ>`_ project but made framework
+agnostic.
+
+
+Installing
+----------
+Through PyPI::
+
+ $ pip install envparse
+
+Manually::
+
+ $ pip install git+https://github.com/rconradharris/envparse.git
+ OR
+ $ git clone https://github.com/rconradharris/envparse && cd envparse
+ $ python setup.py install
+
+
+Usage
+-----
+In your settings or configuration module, first either import the standard
+parser or one with a schema:
+
+.. code-block:: python
+
+ # Standard
+ from envparse import env
+
+ # Schema
+ from envparse import Env
+ env = Env(BOOLEAN_VAR=bool, LIST_VAR=dict(type=list, subtype=int))
+
+
+``env`` can then be called in two ways:
+
+* Type explicit: ``env('ENV_VAR_NAME', type=TYPE, ...)``
+* Type implicit (for Python builtin types only): ``env.TYPE('ENV_VAR_NAME', ...)``
+ If type is not specified, explicitly or implicitly, then the default
+ type is ``str``.
+
+
+Casting to a specified type:
+
+.. code-block:: python
+
+ # Environment variable: MAIL_ENABLED=1
+
+ mail_enabled = env('MAIL_ENABLED', type=bool)
+ # OR mail_enabled = env.bool('MAIL_ENABLED')
+ assert mail_enabled is True
+
+Casting nested types:
+
+.. code-block:: python
+
+ # Environment variable: FOO=1,2,3
+ foo = env('FOO'), subtype=int)
+ # OR: foo = env('FOO', type=list, subtype=int)
+ # Note that there is no way to implicitly call subtypes.
+ assert foo == [1, 2, 3]
+
+Specifying defaults:
+
+.. code-block:: python
+
+ # Environment variable MAX_ROWS has not been defined
+
+ max_rows = env.int('MAX_ROWS', default=100)
+ assert max_rows == 100
+
+Proxying values, useful in Heroku for wiring up the environment variables they
+provide to the ones that your app actually uses:
+
+.. code-block:: python
+
+ # Environment variables: MAILGUN_SMTP_LOGIN=foo,
+ # SMTP_LOGIN='{{MAILGUN_SMTP_LOGIN}}'
+
+ smtp_login = env('SMTP_LOGIN')
+ assert smtp_login == 'foo'
+
+Now if you switch to using Mandrill as an email provider, instead of having to
+modify your app, you can simply make a configuration change:
+
+.. code-block:: bash
+
+ SMTP_LOGIN='{{MANDRILL_UESRNAME}}'
+
+There are also a few convenience methods:
+
+* ``env.json``: parses JSON and returns a dict.
+* ``env.url``: parses a url and returns a ``urlparse.ParseResult`` object.
+
+
+Type specific notes:
+
+* list: the expected environment variable format is ``FOO=1,2,3`` and may
+ contain spaces between the commas as well as preceding or trailing whitespace.
+* dict: the expected environment variable format is ``FOO='key1=val1,
+ key2=val2``. Spaces are also allowed.
+* json: a regular JSON string such as ``FOO='{"foo": "bar"}'`` is expected.
+
+
+Schemas
+~~~~~~~
+Define a schema so you can only need to provide the type, subtype, and defaults
+once:
+
+.. code-block:: python
+
+ # Environment variables: MAIL_ENABLED=0, LIST_INT='1,2,3'
+
+ # Bind schema to Env object to get schema-based lookups
+ env = Env(MAIL_ENABLED=bool, SMTP_LOGIN=dict(type=str, default='foo'),
+ LIST_INT=dict(type=list, subtype=int))
+ assert env('MAIL_ENABLED') is False
+ assert env('SMTP_LOGIN') == 'foo' # Not defined so uses default
+ assert env('LIST_INT') == [1, 2, 3]
+
+The ``Env`` constructor takes values in the form of either: ``VAR_NAME=type``
+or ``VAR_NAME=dict`` where ``dict`` is a dictionary with either one or more of
+the following keys specified: ``type``, ``subtype``, ``default``.
+
+
+Pre- and Postprocessors
+~~~~~~~~~~~~~~~~~~~~~~~
+Preprocessors are callables that are run on the environment variable string
+before any type casting takes place:
+
+.. code-block:: python
+
+ # Environment variables: FOO=bar
+
+ # Preprocessor to change variable to uppercase
+ to_upper = lambda v: v.upper()
+ foo = env('FOO', preprocessor=to_upper)
+ assert foo == 'BAR'
+
+Postprocessors are callables that are run after the type casting takes place.
+An example of one might be returning a datastructure expected by a framework:
+
+.. code-block:: python
+
+ # Environment variable: REDIS_URL='redis://:redispass@127.0.0.1:6379/0'
+ def django_redis(url):
+ return {'BACKEND': 'django_redis.cache.RedisCache',
+ 'LOCATION': '{}:{}:{}'.format(url.hostname, url.port, url.path.strip('/')),
+ 'OPTIONS': {'PASSWORD': url.password}}
+
+ redis_config = env('REDIS_URL', postprocessor=django_redis)
+ assert redis_config == {'BACKEND': 'django_redis.cache.RedisCache',
+ 'LOCATION': '127.0.0.1:6379:0', 'OPTIONS': {'PASSWORD': 'redispass'}}
+
+
+Environment File
+~~~~~~~~~~~~~~~~
+Read from a .env file (line delimited KEY=VALUE):
+
+.. code-block:: python
+
+ # This recurses up the directory tree until a file called '.env' is found.
+ env.read_env()
+
+ # Manually specifying a path
+ env.read_env('/config/.myenv')
+
+ # Values can be read as normal
+ env.int('FOO')
+
+
+Tests
+-----
+.. image:: https://secure.travis-ci.org/rconradharris/envparse.png?branch=master
+
+To run the tests install tox::
+
+ pip install tox
+
+Then run them with::
+
+ make test
diff --git a/envparse.py b/envparse.py
new file mode 100644
index 0000000..e13b584
--- /dev/null
+++ b/envparse.py
@@ -0,0 +1,217 @@
+"""
+envparse is a simple utility to parse environment variables.
+"""
+from __future__ import unicode_literals
+import inspect
+import json as pyjson
+import logging
+import os
+import re
+import shlex
+import warnings
+try:
+ import urllib.parse as urlparse
+except ImportError:
+ # Python 2
+ import urlparse
+
+
+__version__ = '0.2.0'
+
+
+logger = logging.getLogger(__file__)
+
+
+class ConfigurationError(Exception):
+ pass
+
+
+# Cannot rely on None since it may be desired as a return value.
+NOTSET = type(str('NoValue'), (object,), {})
+
+
+def shortcut(cast):
+ def method(self, var, **kwargs):
+ return self.__call__(var, cast=cast, **kwargs)
+ return method
+
+
+class Env(object):
+ """
+ Lookup and cast environment variables with optional schema.
+
+ Usage:::
+
+ env = Env()
+ env('foo')
+ env.bool('bar')
+
+ # Create env with a schema
+ env = Env(MAIL_ENABLED=bool, SMTP_LOGIN=(str, 'DEFAULT'))
+ if env('MAIL_ENABLED'):
+ ...
+ """
+ BOOLEAN_TRUE_STRINGS = ('true', 'on', 'ok', 'y', 'yes', '1')
+
+ def __init__(self, **schema):
+ self.schema = schema
+
+ def __call__(self, var, default=NOTSET, cast=None, subcast=None,
+ force=False, preprocessor=None, postprocessor=None):
+ """
+ Return value for given environment variable.
+
+ :param var: Name of variable.
+ :param default: If var not present in environ, return this instead.
+ :param cast: Type or callable to cast return value as.
+ :param subcast: Subtype or callable to cast return values as (used for
+ nested structures).
+ :param force: force to cast to type even if default is set.
+ :param preprocessor: callable to run on pre-casted value.
+ :param postprocessor: callable to run on casted value.
+
+ :returns: Value from environment or default (if set).
+ """
+ logger.debug("Get '%s' casted as '%s'/'%s' with default '%s'", var,
+ cast, subcast, default)
+
+ if var in self.schema:
+ params = self.schema[var]
+ if isinstance(params, dict):
+ if cast is None:
+ cast = params.get('cast', cast)
+ if subcast is None:
+ subcast = params.get('subcast', subcast)
+ if default == NOTSET:
+ default = params.get('default', default)
+ else:
+ if cast is None:
+ cast = params
+ # Default cast is `str` if it is not specified. Most types will be
+ # implicitly strings so reduces having to specify.
+ cast = str if cast is None else cast
+
+ try:
+ value = os.environ[var]
+ except KeyError:
+ if default is NOTSET:
+ error_msg = "Environment variable '{}' not set.".format(var)
+ raise ConfigurationError(error_msg)
+ else:
+ value = default
+
+ # Resolve any proxied values
+ if hasattr(value, 'startswith') and value.startswith('{{'):
+ value = self.__call__(value.lstrip('{{}}'), default, cast, subcast,
+ default, force, preprocessor, postprocessor)
+
+ if preprocessor:
+ value = preprocessor(value)
+ if value != default or force:
+ value = self.cast(value, cast, subcast)
+ if postprocessor:
+ value = postprocessor(value)
+ return value
+
+ @classmethod
+ def cast(cls, value, cast=str, subcast=None):
+ """
+ Parse and cast provided value.
+
+ :param value: Stringed value.
+ :param cast: Type or callable to cast return value as.
+ :param subcast: Subtype or callable to cast return values as (used for
+ nested structures).
+
+ :returns: Value of type `cast`.
+ """
+ if cast is bool:
+ value = value.lower() in cls.BOOLEAN_TRUE_STRINGS
+ elif cast is float:
+ # Clean string
+ float_str = re.sub(r'[^\d,\.]', '', value)
+ # Split to handle thousand separator for different locales, i.e.
+ # comma or dot being the placeholder.
+ parts = re.split(r'[,\.]', float_str)
+ if len(parts) == 1:
+ float_str = parts[0]
+ else:
+ float_str = "{0}.{1}".format(''.join(parts[0:-1]), parts[-1])
+ value = float(float_str)
+ elif type(cast) is type and (issubclass(cast, list) or
+ issubclass(cast, tuple)):
+ value = (subcast(i.strip()) if subcast else i.strip() for i in
+ value.split(',') if i)
+ elif cast is dict:
+ value = {k.strip(): subcast(v.strip()) if subcast else v.strip()
+ for k, v in (i.split('=') for i in value.split(',') if
+ value)}
+ try:
+ return cast(value)
+ except ValueError as error:
+ raise ConfigurationError(*error.args)
+
+ # Shortcuts
+ bool = shortcut(bool)
+ dict = shortcut(dict)
+ float = shortcut(float)
+ int = shortcut(int)
+ list = shortcut(list)
+ set = shortcut(set)
+ str = shortcut(str)
+ tuple = shortcut(tuple)
+ json = shortcut(pyjson.loads)
+ url = shortcut(urlparse.urlparse)
+
+ @staticmethod
+ def read_envfile(path=None, **overrides):
+ """
+ Read a .env file (line delimited KEY=VALUE) into os.environ.
+
+ If not given a path to the file, recurses up the directory tree until
+ found.
+
+ Uses code from Honcho (github.com/nickstenning/honcho) for parsing the
+ file.
+ """
+ if path is None:
+ frame = inspect.currentframe().f_back
+ caller_dir = os.path.dirname(frame.f_code.co_filename)
+ path = os.path.join(os.path.abspath(caller_dir), '.env')
+
+ try:
+ with open(path, 'r') as f:
+ content = f.read()
+ except getattr(__builtins__, 'FileNotFoundError', IOError):
+ logger.debug('envfile not found at %s, looking in parent dir.',
+ path)
+ filedir, filename = os.path.split(path)
+ pardir = os.path.abspath(os.path.join(filedir, os.pardir))
+ path = os.path.join(pardir, filename)
+ if filedir != pardir:
+ Env.read_envfile(path, **overrides)
+ else:
+ # Reached top level directory.
+ warnings.warn('Could not any envfile.')
+ return
+
+ logger.debug('Reading environment variables from: %s', path)
+ for line in content.splitlines():
+ tokens = list(shlex.shlex(line, posix=True))
+ # parses the assignment statement
+ if len(tokens) < 3:
+ continue
+ name, op = tokens[:2]
+ value = ''.join(tokens[2:])
+ if op != '=':
+ continue
+ if not re.match(r'[A-Za-z_][A-Za-z_0-9]*', name):
+ continue
+ value = value.replace(r'\n', '\n').replace(r'\t', '\t')
+ os.environ.setdefault(name, value)
+
+ for name, value in overrides.items():
+ os.environ.setdefault(name, value)
+
+# Convenience object if no schema is required.
+env = Env()
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..5e40900
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..898814c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+from setuptools import setup, find_packages
+from setuptools.command.test import test as TestCommand
+import codecs
+import os
+import sys
+import re
+
+
+def read(*parts):
+ here = os.path.abspath(os.path.dirname(__file__))
+ # intentionally *not* adding an encoding option to open
+ return codecs.open(os.path.join(here, *parts), 'r').read()
+
+
+def find_version(*file_paths):
+ version_file = read(*file_paths)
+ version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
+ version_file, re.M)
+ if version_match:
+ return version_match.group(1)
+ raise RuntimeError("Unable to find version string.")
+
+
+class PyTest(TestCommand):
+ user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
+
+ def initialize_options(self):
+ TestCommand.initialize_options(self)
+ self.pytest_args = []
+
+ def finalize_options(self):
+ TestCommand.finalize_options(self)
+ self.test_args = []
+ self.test_suite = True
+
+ def run_tests(self):
+ #import here, cause outside the eggs aren't loaded
+ import pytest
+ errno = pytest.main(self.pytest_args)
+ sys.exit(errno)
+
+setup(
+ name='envparse',
+ version=find_version('.', 'envparse.py'),
+ url='https://github.com/rconradharris/envparse',
+ license='MIT',
+ author='Rick Harris',
+ author_email='rconradharris at gmail.com',
+ tests_require=['pytest'],
+ install_requires=[''],
+ cmdclass={'test': PyTest},
+ description='Simple environment variable parsing',
+ long_description=read('README.rst'),
+ py_modules=['envparse'],
+ platforms='any',
+ zip_safe=False,
+ classifiers = [
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
+ 'Development Status :: 4 - Beta',
+ 'Natural Language :: English',
+ 'Environment :: Web Environment',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ ],
+ extras_require={
+ 'testing': ['pytest'],
+ }
+)
diff --git a/tests/envfile b/tests/envfile
new file mode 100644
index 0000000..8efda9c
--- /dev/null
+++ b/tests/envfile
@@ -0,0 +1,15 @@
+BLANK=''
+STR=foo
+INT=42
+FLOAT=33.3
+BOOL_TRUE=1
+BOOL_FALSE=0
+PROXIED={{STR}}
+LIST_STR='foo,bar'
+LIST_STR_WITH_SPACES=' foo, bar'
+LIST_INT=1,2,3
+LIST_INT_WITH_SPACES=1, 2,3
+DICT_STR=key1=val1, key2=val2
+DICT_INT=key1=1, key2=2
+JSON='{"foo": "bar", "baz": [1, 2, 3]}'
+URL=https://example.com/path?query=1
diff --git a/tests/test_casts.py b/tests/test_casts.py
new file mode 100644
index 0000000..ecb6bd8
--- /dev/null
+++ b/tests/test_casts.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+import pytest
+
+from envparse import Env, env, ConfigurationError, urlparse
+
+
+env_vars = dict(
+ BLANK='',
+ STR='foo',
+ INT='42',
+ FLOAT='33.3',
+ BOOL_TRUE='1',
+ BOOL_FALSE='0',
+ PROXIED='{{STR}}',
+ LIST_STR='foo,bar',
+ LIST_STR_WITH_SPACES=' foo, bar',
+ LIST_INT='1,2,3',
+ LIST_INT_WITH_SPACES=' 1, 2,3',
+ DICT_STR='key1=val1, key2=val2',
+ DICT_INT='key1=1, key2=2',
+ JSON='{"foo": "bar", "baz": [1, 2, 3]}',
+ URL='https://example.com/path?query=1',
+)
+
+
+ at pytest.fixture(autouse=True, params=['environ', 'envfile'])
+def environ(monkeypatch, request):
+ """Setup environment with sample variables."""
+ if request.param == 'environ':
+ for key, val in env_vars.items():
+ monkeypatch.setenv(key, val)
+ elif request.param == 'envfile':
+ env.read_envfile('tests/envfile')
+
+
+# Helper function
+def assert_type_value(cast, expected, result):
+ assert cast == type(result)
+ assert expected == result
+
+
+def test_var_not_present():
+ with pytest.raises(ConfigurationError):
+ env('NOT_PRESENT')
+
+
+def test_var_not_present_with_default():
+ default_val = 'default val'
+ assert default_val, env('NOT_PRESENT', default=default_val)
+
+
+def test_default_none():
+ assert_type_value(type(None), None, env('NOT_PRESENT', default=None))
+
+
+def test_implicit_nonbuiltin_type():
+ with pytest.raises(AttributeError):
+ env.foo('FOO')
+
+
+def test_str():
+ expected = str(env_vars['STR'])
+ assert_type_value(str, expected, env('STR'))
+ assert_type_value(str, expected, env.str('STR'))
+
+
+def test_int():
+ expected = int(env_vars['INT'])
+ assert_type_value(int, expected, env('INT', cast=int))
+ assert_type_value(int, expected, env.int('INT'))
+
+
+def test_float():
+ expected = float(env_vars['FLOAT'])
+ assert_type_value(float, expected, env.float('FLOAT'))
+
+
+def test_bool():
+ assert_type_value(bool, True, env.bool('BOOL_TRUE'))
+ assert_type_value(bool, False, env.bool('BOOL_FALSE'))
+
+
+def test_list():
+ list_str = ['foo', 'bar']
+ assert_type_value(list, list_str, env('LIST_STR', cast=list))
+ assert_type_value(list, list_str, env.list('LIST_STR'))
+ assert_type_value(list, list_str, env.list('LIST_STR_WITH_SPACES'))
+ list_int = [1, 2, 3]
+ assert_type_value(list, list_int, env('LIST_INT', cast=list,
+ subcast=int))
+ assert_type_value(list, list_int, env.list('LIST_INT', subcast=int))
+ assert_type_value(list, list_int, env.list('LIST_INT_WITH_SPACES',
+ subcast=int))
+ assert_type_value(list, [], env.list('BLANK', subcast=int))
+
+
+def test_dict():
+ dict_str = dict(key1='val1', key2='val2')
+ assert_type_value(dict, dict_str, env.dict('DICT_STR'))
+ assert_type_value(dict, dict_str, env('DICT_STR', cast=dict))
+ dict_int = dict(key1=1, key2=2)
+ assert_type_value(dict, dict_int, env('DICT_INT', cast=dict,
+ subcast=int))
+ assert_type_value(dict, dict_int, env.dict('DICT_INT', subcast=int))
+ assert_type_value(dict, {}, env.dict('BLANK'))
+
+
+def test_json():
+ expected = {'foo': 'bar', 'baz': [1, 2, 3]}
+ assert_type_value(dict, expected, env.json('JSON'))
+
+
+def test_url():
+ url = urlparse.urlparse('https://example.com/path?query=1')
+ assert_type_value(url.__class__, url, env.url('URL'))
+
+
+def proxied_value():
+ assert_type_value(str, 'bar', env('PROXIED'))
+
+
+def test_preprocessor():
+ assert_type_value(str, 'FOO', env('STR', preprocessor=lambda
+ v: v.upper()))
+
+
+def test_postprocessor(monkeypatch):
+ """
+ Test a postprocessor which turns a redis url into a Django compatible
+ cache url.
+ """
+ redis_url = 'redis://:redispass@127.0.0.1:6379/0'
+ monkeypatch.setenv('redis_url', redis_url)
+ expected = {'BACKEND': 'django_redis.cache.RedisCache',
+ 'LOCATION': '127.0.0.1:6379:0',
+ 'OPTIONS': {'PASSWORD': 'redispass'}}
+
+ def django_redis(url):
+ return {
+ 'BACKEND': 'django_redis.cache.RedisCache',
+ 'LOCATION': '{}:{}:{}'.format(url.hostname, url.port, url.path.strip('/')),
+ 'OPTIONS': {'PASSWORD': url.password}}
+
+ assert_type_value(dict, expected, env.url('redis_url',
+ postprocessor=django_redis))
+
+
+def test_schema():
+ env = Env(STR=str, STR_DEFAULT=dict(cast=str, default='default'),
+ INT=int, LIST_STR=list, LIST_INT=dict(cast=list, subcast=int))
+ assert_type_value(str, 'foo', env('STR'))
+ assert_type_value(str, 'default', env('STR_DEFAULT'))
+ assert_type_value(int, 42, env('INT'))
+ assert_type_value(list, ['foo', 'bar'], env('LIST_STR'))
+ assert_type_value(list, [1, 2, 3], env('LIST_INT'))
+ # Overrides
+ assert_type_value(str, '42', env('INT', cast=str))
+ assert_type_value(str, 'manual_default', env('STR_DEFAULT',
+ default='manual_default'))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..2405dc3
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,22 @@
+[tox]
+envlist=py27, py32, py33, py34, py35, pypy, pypy3, flake8, coverage
+
+[testenv]
+commands = {envpython} setup.py test
+deps =
+ pytest
+
+[testenv:coverage]
+deps =
+ coverage
+ {[testenv]deps}
+commands =
+ coverage run -m pytest tests --strict {posargs}
+ coverage report --include=envparse.py
+ coverage html --include=envparse.py
+
+[testenv:flake8]
+deps =
+ flake8
+commands =
+ flake8 envparse.py tests --max-line-length=100
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-envparse.git
More information about the Python-modules-commits
mailing list