[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