diff --git a/CHANGELOG.md b/CHANGELOG.md index fe44557..62e2163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changelog ========= +3.7 (2023-01-09) +---------------- + +- Fix `Csv` cast hanging with `default=None`, now returning an empty list. (#149) + 3.6 (2022-02-02) ---------------- diff --git a/README.rst b/README.rst index 6e36272..4184536 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,4 @@ +======================================================== Python Decouple: Strict separation of settings from code ======================================================== @@ -22,8 +23,6 @@ for separating settings from code. :target: https://pypi.python.org/pypi/python-decouple/ :alt: Latest PyPI version - - .. contents:: Summary @@ -42,6 +41,7 @@ The first 2 are *project settings* and the last 3 are *instance settings*. You should be able to change *instance settings* without redeploying your app. + Why not just use environment variables? --------------------------------------- @@ -61,6 +61,7 @@ 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 ===== @@ -88,8 +89,10 @@ Then use it on your ``settings.py``. 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. @@ -112,11 +115,13 @@ If you wish to fall back to your system's default encoding use: config.encoding = locale.getpreferredencoding(False) SECRET_KEY = config('SECRET_KEY') + Where is the settings data stored? ------------------------------------ +---------------------------------- *Decouple* supports both *.ini* and *.env* files. + Ini file ~~~~~~~~ @@ -134,6 +139,7 @@ Simply create a ``settings.ini`` next to your configuration module in the form: *Note*: Since ``ConfigParser`` supports *string interpolation*, to represent the character ``%`` you need to escape it as ``%%``. + Env file ~~~~~~~~ @@ -148,6 +154,7 @@ Simply create a ``.env`` text file in your repository's root directory in the fo PERCENTILE=90% #COMMENTED=42 + Example: How do I use it with Django? ------------------------------------- @@ -191,6 +198,7 @@ and `dj-database-url `_. # ... + Attention with *undefined* parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -201,6 +209,7 @@ If ``SECRET_KEY`` is not present in the ``.env``, *decouple* will raise an ``Und This *fail fast* policy helps you avoid chasing misbehaviours when you eventually forget a parameter. + Overriding config files with environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -297,6 +306,7 @@ Let's see some examples for the above mentioned cases: As you can see, ``cast`` is very flexible. But the last example got a bit complex. + Built in Csv Helper ~~~~~~~~~~~~~~~~~~~ @@ -340,6 +350,7 @@ By default *Csv* returns a ``list``, but you can get a ``tuple`` or whatever you >>> config('SECURE_PROXY_SSL_HEADER', cast=Csv(post_process=tuple)) ('HTTP_X_FORWARDED_PROTO', 'https') + Built in Choices helper ~~~~~~~~~~~~~~~~~~~~~~~ @@ -383,6 +394,72 @@ You can also use a Django-like choices tuple: >>> config('CONNECTION_TYPE', cast=Choices(choices=CONNECTION_OPTIONS)) 'bluetooth' + +Frequently Asked Questions +========================== + + +1) How to specify the `.env` path? +---------------------------------- + +.. code-block:: python + + import os + from decouple import Config, RepositoryEnv + + + config = Config(RepositoryEnv("path/to/.env")) + + +2) How to use python-decouple with Jupyter? +------------------------------------------- + +.. code-block:: python + + import os + from decouple import Config, RepositoryEnv + + + config = Config(RepositoryEnv("path/to/.env")) + + +3) How to specify a file with another name instead of `.env`? +---------------------------------------------------------------- + +.. code-block:: python + + import os + from decouple import Config, RepositoryEnv + + + config = Config(RepositoryEnv("path/to/somefile-like-env")) + + +4) How to define the path to my env file on a env var? +-------------------------------------------------------- + +.. code-block:: python + + import os + from decouple import Config, RepositoryEnv + + + DOTENV_FILE = os.environ.get("DOTENV_FILE", ".env") # only place using os.environ + config = Config(RepositoryEnv(DOTENV_FILE)) + + +5) How can I have multiple *env* files working together? +-------------------------------------------------------- + +.. code-block:: python + + from collections import ChainMap + from decouple import Config, RepositoryEnv + + + config = Config(ChainMap(RepositoryEnv(".private.env"), RepositoryEnv(".env"))) + + Contribute ========== diff --git a/decouple.py b/decouple.py index b19ec86..9873fc9 100644 --- a/decouple.py +++ b/decouple.py @@ -11,10 +11,10 @@ if PYVERSION >= (3, 0, 0): - from configparser import ConfigParser + from configparser import ConfigParser, NoOptionError text_type = str else: - from ConfigParser import SafeConfigParser as ConfigParser + from ConfigParser import SafeConfigParser as ConfigParser, NoOptionError text_type = unicode if PYVERSION >= (3, 2, 0): @@ -134,7 +134,10 @@ def __contains__(self, key): self.parser.has_option(self.SECTION, key)) def __getitem__(self, key): - return self.parser.get(self.SECTION, key) + try: + return self.parser.get(self.SECTION, key) + except NoOptionError: + raise KeyError(key) class RepositoryEnv(RepositoryEmpty): @@ -216,7 +219,7 @@ def _find_file(self, path): # search the parent parent = os.path.dirname(path) - if parent and parent != os.path.abspath(os.sep): + if parent and os.path.normcase(parent) != os.path.normcase(os.path.abspath(os.sep)): return self._find_file(parent) # reached root without finding any files. @@ -271,6 +274,9 @@ def __init__(self, cast=text_type, delimiter=',', strip=string.whitespace, post_ def __call__(self, value): """The actual transformation""" + if value is None: + return self.post_process() + transform = lambda s: self.cast(s.strip(self.strip)) splitter = shlex(value, posix=True) diff --git a/setup.py b/setup.py index e447851..e972783 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ README = os.path.join(os.path.dirname(__file__), 'README.rst') setup(name='python-decouple', - version='3.6', + version='3.8', description='Strict separation of settings from code.', long_description=open(README).read(), author="Henrique Bastos", author_email="henrique@bastos.net", diff --git a/tests/test_autoconfig.py b/tests/test_autoconfig.py index 75d97c3..94af7cc 100644 --- a/tests/test_autoconfig.py +++ b/tests/test_autoconfig.py @@ -97,3 +97,13 @@ def test_autoconfig_env_default_encoding(): assert config.encoding == DEFAULT_ENCODING assert 'ENV' == config('KEY', default='ENV') mopen.assert_called_once_with(filename, encoding=DEFAULT_ENCODING) + + +def test_autoconfig_no_repository(): + path = os.path.join(os.path.dirname(__file__), 'autoconfig', 'ini', 'no_repository') + config = AutoConfig(path) + + with pytest.raises(UndefinedValueError): + config('KeyNotInEnvAndNotInRepository') + + assert isinstance(config.config.repository, RepositoryEmpty) diff --git a/tests/test_env.py b/tests/test_env.py index a259af3..a91c95c 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -136,3 +136,7 @@ def test_env_with_quote(config): assert '"Y"' == config('KeyHasTwoDoubleQuote') assert '''"Y\'''' == config('KeyHasMixedQuotesAsData1') assert '''\'Y"''' == config('KeyHasMixedQuotesAsData2') + +def test_env_repo_keyerror(config): + with pytest.raises(KeyError): + config.repository['UndefinedKey'] diff --git a/tests/test_helper_csv.py b/tests/test_helper_csv.py index 8d55a66..c5db287 100644 --- a/tests/test_helper_csv.py +++ b/tests/test_helper_csv.py @@ -29,3 +29,8 @@ def test_csv_quoted_parse(): assert ['foo', "'bar, baz'", "'qux"] == csv(''' foo ,"'bar, baz'", "'qux"''') assert ['foo', '"bar, baz"', '"qux'] == csv(""" foo ,'"bar, baz"', '"qux'""") + + +def test_csv_none(): + csv = Csv() + assert [] == csv(None) diff --git a/tests/test_ini.py b/tests/test_ini.py index 6ff0523..f610078 100644 --- a/tests/test_ini.py +++ b/tests/test_ini.py @@ -121,3 +121,8 @@ def test_ini_undefined_but_present_in_os_environ(config): def test_ini_empty_string_means_false(config): assert False is config('KeyEmpty', cast=bool) + + +def test_ini_repo_keyerror(config): + with pytest.raises(KeyError): + config.repository['UndefinedKey'] diff --git a/tests/test_secrets.py b/tests/test_secrets.py index 481fdeb..7d6185b 100644 --- a/tests/test_secrets.py +++ b/tests/test_secrets.py @@ -1,5 +1,6 @@ # coding: utf-8 import os +import pytest from decouple import Config, RepositorySecret @@ -28,3 +29,10 @@ def test_secret_overriden_by_environ(): os.environ['db_user'] = 'hi' assert 'hi' == config('db_user') del os.environ['db_user'] + +def test_secret_repo_keyerror(): + path = os.path.join(os.path.dirname(__file__), 'secrets') + repo = RepositorySecret(path) + + with pytest.raises(KeyError): + repo['UndefinedKey']