Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support POSIX parameter expansion #30

Merged
merged 1 commit into from
Sep 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ ignored.
# I am a comment and that is OK
FOO="BAR"

``.env`` can interpolate variables using POSIX variable expansion, variables
are replaced from the environment first or from other values in the ``.env``
file if the variable is not present in the environment.

.. code:: shell

CONFIG_PATH=${HOME}/.config/foo
DOMAIN=example.org
EMAIL=admin@${DOMAIN}


Django
------

Expand Down Expand Up @@ -209,6 +220,13 @@ us a pull request.
This project is currently maintained by `Saurabh Kumar <https://saurabh-kumar.com>`__ and
would not have been possible without the support of these `awesome people <https://github.com/theskumar/python-dotenv/graphs/contributors>`__.

Executing the tests:

::

$ flake8
$ pytest

Changelog
=========

Expand All @@ -217,6 +235,7 @@ dev
- Drop support for Python 2.6
- Handle escaped charaters and newlines in quoted values. (Thanks `@iameugenejo`_)
- Remove any spaces around unquoted key/value. (Thanks `@paulochf`_)
- Added POSIX variable expansion.

0.5.1
----------
Expand Down
4 changes: 2 additions & 2 deletions dotenv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import click

from .main import get_key, parse_dotenv, set_key, unset_key
from .main import get_key, dotenv_values, set_key, unset_key


@click.group()
Expand Down Expand Up @@ -35,7 +35,7 @@ def cli(ctx, file, quote):
def list(ctx):
'''Display all the stored key/value.'''
file = ctx.obj['FILE']
dotenv_as_dict = parse_dotenv(file)
dotenv_as_dict = dotenv_values(file)
for k, v in dotenv_as_dict:
click.echo('%s="%s"' % (k, v))

Expand Down
37 changes: 34 additions & 3 deletions dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import os
import sys
import warnings
import re
from collections import OrderedDict

__escape_decoder = codecs.getdecoder('unicode_escape')
__posix_variable = re.compile('\$\{[^\}]*\}')


def decode_escaped(escaped):
Expand All @@ -21,7 +23,7 @@ def load_dotenv(dotenv_path):
if not os.path.exists(dotenv_path):
warnings.warn("Not loading %s - it doesn't exist." % dotenv_path)
return None
for k, v in parse_dotenv(dotenv_path):
for k, v in dotenv_values(dotenv_path).items():
os.environ.setdefault(k, v)
return True

Expand All @@ -36,7 +38,7 @@ def get_key(dotenv_path, key_to_get):
if not os.path.exists(dotenv_path):
warnings.warn("can't read %s - it doesn't exist." % dotenv_path)
return None
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
dotenv_as_dict = dotenv_values(dotenv_path)
if key_to_get in dotenv_as_dict:
return dotenv_as_dict[key_to_get]
else:
Expand Down Expand Up @@ -73,7 +75,7 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
if not os.path.exists(dotenv_path):
warnings.warn("can't delete from %s - it doesn't exist." % dotenv_path)
return None, key_to_unset
dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path))
dotenv_as_dict = dotenv_values(dotenv_path)
if key_to_unset in dotenv_as_dict:
dotenv_as_dict.pop(key_to_unset, None)
else:
Expand All @@ -83,6 +85,12 @@ def unset_key(dotenv_path, key_to_unset, quote_mode="always"):
return success, key_to_unset


def dotenv_values(dotenv_path):
values = OrderedDict(parse_dotenv(dotenv_path))
values = resolve_nested_variables(values)
return values


def parse_dotenv(dotenv_path):
with open(dotenv_path) as f:
for line in f:
Expand All @@ -103,6 +111,29 @@ def parse_dotenv(dotenv_path):
yield k, v


def resolve_nested_variables(values):
def _replacement(name):
"""
get appropiate value for a variable name.
first search in environ, if not found,
then look into the dotenv variables
"""
ret = os.getenv(name, values.get(name, ""))
return ret

def _re_sub_callback(match_object):
"""
From a match object gets the variable name and returns
the correct replacement
"""
return _replacement(match_object.group()[2:-1])

for k, v in values.items():
values[k] = __posix_variable.sub(_re_sub_callback, v)

return values


def flatten_and_write(dotenv_path, dotenv_as_dict, quote_mode="always"):
with open(dotenv_path, "w") as f:
for k, v in dotenv_as_dict.items():
Expand Down
35 changes: 35 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from os import environ
from os.path import dirname, join

import dotenv
Expand Down Expand Up @@ -83,3 +84,37 @@ def test_default_path(cli):
output = sh.dotenv('get', 'HELLO')
assert output == 'HELLO="WORLD"\n'
sh.rm(dotenv_path)


def test_get_key_with_interpolation(cli):
with cli.isolated_filesystem():
sh.touch(dotenv_path)
dotenv.set_key(dotenv_path, 'HELLO', 'WORLD')
dotenv.set_key(dotenv_path, 'FOO', '${HELLO}')
dotenv.set_key(dotenv_path, 'BAR', 'CONCATENATED_${HELLO}_POSIX_VAR')

# test replace from variable in file
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == 'WORLD'
stored_value = dotenv.get_key(dotenv_path, 'BAR')
assert stored_value == 'CONCATENATED_WORLD_POSIX_VAR'
# test replace from environ taking precedence over file
environ["HELLO"] = "TAKES_PRECEDENCE"
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == "TAKES_PRECEDENCE"
sh.rm(dotenv_path)


def test_get_key_with_interpolation_of_unset_variable(cli):
with cli.isolated_filesystem():
sh.touch(dotenv_path)
dotenv.set_key(dotenv_path, 'FOO', '${NOT_SET}')
# test unavailable replacement returns empty string
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == ''
# unless present in environment
environ['NOT_SET'] = 'BAR'
stored_value = dotenv.get_key(dotenv_path, 'FOO')
assert stored_value == 'BAR'
del(environ['NOT_SET'])
sh.rm(dotenv_path)
16 changes: 15 additions & 1 deletion tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import pytest
import tempfile
import warnings
import sh

from dotenv import load_dotenv, find_dotenv
from dotenv import load_dotenv, find_dotenv, set_key


def test_warns_if_file_does_not_exist():
Expand Down Expand Up @@ -55,3 +56,16 @@ def test_find_dotenv():
with open(filename, 'w') as f:
f.write("TEST=test\n")
assert find_dotenv(usecwd=True) == filename


def test_load_dotenv(cli):
dotenv_path = '.test_load_dotenv'
with cli.isolated_filesystem():
sh.touch(dotenv_path)
set_key(dotenv_path, 'DOTENV', 'WORKS')
assert 'DOTENV' not in os.environ
success = load_dotenv(dotenv_path)
assert success
assert 'DOTENV' in os.environ
assert os.environ['DOTENV'] == 'WORKS'
sh.rm(dotenv_path)