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

feat: support reading/loading multiple .env files #424

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
71f1730
accept multiple files in dotenv run
duarte-pompeu Sep 10, 2022
5f7d1f8
allow multiple files for other cli targets
duarte-pompeu Sep 10, 2022
6718b16
fix behavior or adapt tests
duarte-pompeu Sep 10, 2022
b4f335b
improve readability
duarte-pompeu Sep 10, 2022
0660ded
remove f-string
duarte-pompeu Sep 10, 2022
d46b1e6
improve formatting
duarte-pompeu Sep 10, 2022
bc474ca
restore code to test for warning
duarte-pompeu Sep 10, 2022
2b3a28b
format code
duarte-pompeu Oct 1, 2022
f730c6d
add flag to allow suppressing warning in get_key
duarte-pompeu Oct 1, 2022
d6c1385
fix warning and test in cli
duarte-pompeu Oct 1, 2022
630c0bd
Merge branch 'main' into multiple-files
duarte-pompeu Dec 4, 2023
c585a7b
fix lint warnings
duarte-pompeu Dec 4, 2023
9b1b4af
fix some problems
duarte-pompeu Dec 4, 2023
a2d9365
fix tests
duarte-pompeu Dec 4, 2023
f5ed3e9
fix type hint
duarte-pompeu Dec 4, 2023
49c34a5
add some tests
duarte-pompeu Dec 4, 2023
437a921
remove support for unsetting multiple files
duarte-pompeu Dec 8, 2023
a10e462
improve backward compatibility
duarte-pompeu Dec 8, 2023
186dc81
remove unused logger
duarte-pompeu Dec 8, 2023
dde1654
remove support for set on multiple files
duarte-pompeu Dec 8, 2023
ec23819
print errors to stderr
duarte-pompeu Dec 8, 2023
febc153
improve params for test_lit_multi_file
duarte-pompeu Dec 8, 2023
c52dd9e
test get for multiple files
duarte-pompeu Dec 8, 2023
9e3df30
test run with multiple envs
duarte-pompeu Dec 8, 2023
6090d64
update docs
duarte-pompeu Dec 8, 2023
d61c1ed
remove unecessary param
duarte-pompeu Dec 8, 2023
46f1b75
remove format param for test_list_multi_file
duarte-pompeu Dec 8, 2023
363aab5
document loading order of multiple .envs
duarte-pompeu Dec 28, 2023
1d33b6a
revert change in description
duarte-pompeu Dec 28, 2023
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
97 changes: 57 additions & 40 deletions src/dotenv/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
import os
import shlex
import sys
Expand All @@ -15,9 +16,12 @@
from .main import dotenv_values, get_key, set_key, unset_key
from .version import __version__

logger = logging.getLogger(__name__)


@click.group()
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
@click.option('-f', '--file', default=[os.path.join(os.getcwd(), '.env')],
multiple=True,
type=click.Path(file_okay=True),
help="Location of the .env file, defaults to .env file in current working directory.")
@click.option('-q', '--quote', default='always',
Expand All @@ -33,7 +37,7 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
ctx.obj = {}
ctx.obj['QUOTE'] = quote
ctx.obj['EXPORT'] = export
ctx.obj['FILE'] = file
ctx.obj['FILES'] = file


@cli.command()
Expand All @@ -44,13 +48,14 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
"which displays name=value without quotes.")
def list(ctx: click.Context, format: bool) -> None:
'''Display all the stored key/value.'''
file = ctx.obj['FILE']
if not os.path.isfile(file):
raise click.BadParameter(
'Path "%s" does not exist.' % (file),
ctx=ctx
)
dotenv_as_dict = dotenv_values(file)
dotenv_as_dict = {}
for file in ctx.obj['FILES']:
if not os.path.isfile(file):
raise click.BadParameter(
'Path "%s" does not exist.' % (file),
ctx=ctx
)
dotenv_as_dict.update(dotenv_values(file))
if format == 'json':
click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True))
else:
Expand All @@ -69,31 +74,37 @@ def list(ctx: click.Context, format: bool) -> None:
@click.argument('value', required=True)
def set(ctx: click.Context, key: Any, value: Any) -> None:
'''Store the given key/value.'''
file = ctx.obj['FILE']
file = ctx.obj['FILES']
quote = ctx.obj['QUOTE']
export = ctx.obj['EXPORT']
success, key, value = set_key(file, key, value, quote, export)
if success:
click.echo('%s=%s' % (key, value))
else:
exit(1)
for file in ctx.obj['FILES']:
success, key, value = set_key(file, key, value, quote, export)
if success:
click.echo('%s=%s' % (key, value))
else:
exit(1)


@cli.command()
@click.pass_context
@click.argument('key', required=True)
def get(ctx: click.Context, key: Any) -> None:
'''Retrieve the value for the given key.'''
file = ctx.obj['FILE']
if not os.path.isfile(file):
raise click.BadParameter(
'Path "%s" does not exist.' % (file),
ctx=ctx
)
stored_value = get_key(file, key)
if stored_value:
click.echo(stored_value)
value, set = None, False
files = ctx.obj['FILES']
for file in files:
if not os.path.isfile(file):
raise click.BadParameter(
'Path "%s" does not exist.' % (file),
ctx=ctx
)
stored_value = get_key(file, key)
if stored_value:
value, set = stored_value, True
if set:
click.echo(value)
else:
logger.warning(f"Key {key} not found in {files[0] if len(files) == 1 else files}.")
duarte-pompeu marked this conversation as resolved.
Show resolved Hide resolved
exit(1)


Expand All @@ -102,11 +113,17 @@ def get(ctx: click.Context, key: Any) -> None:
@click.argument('key', required=True)
def unset(ctx: click.Context, key: Any) -> None:
'''Removes the given key.'''
file = ctx.obj['FILE']
quote = ctx.obj['QUOTE']
success, key = unset_key(file, key, quote)
if success:
click.echo("Successfully removed %s" % key)

global_success = False
success_files = []
for file in ctx.obj['FILES']:
success, key = unset_key(file, key, quote)
if success:
global_success = True
success_files.append(file)
if global_success:
click.echo("Successfully removed %s from %s" % (key, success_files[0] if len(success_files) == 1 else success_files))
duarte-pompeu marked this conversation as resolved.
Show resolved Hide resolved
else:
exit(1)

Expand All @@ -121,18 +138,18 @@ def unset(ctx: click.Context, key: Any) -> None:
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
"""Run command with environment variables present."""
file = ctx.obj['FILE']
if not os.path.isfile(file):
raise click.BadParameter(
'Invalid value for \'-f\' "%s" does not exist.' % (file),
ctx=ctx
)
dotenv_as_dict = {
k: v
for (k, v) in dotenv_values(file).items()
if v is not None and (override or k not in os.environ)
}

dotenv_as_dict = {}
for file in ctx.obj['FILES']:
if not os.path.isfile(file):
raise click.BadParameter(
'Invalid value for \'-f\' "%s" does not exist.' % (file),
ctx=ctx
)
dotenv_as_dict.update({
k: v
for (k, v) in dotenv_values(file).items()
if v is not None and (override or k not in os.environ)
})
if not commandline:
click.echo('No command given.')
exit(1)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def test_unset_existing_value(cli, dotenv_file):

result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'unset', 'a'])

assert (result.exit_code, result.output) == (0, "Successfully removed a\n")
assert (result.exit_code, result.output) == (0, "Successfully removed a from %s\n" % dotenv_file)
assert open(dotenv_file, "r").read() == ""


Expand Down
6 changes: 1 addition & 5 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,9 @@ def test_get_key_no_file(tmp_path):


def test_get_key_not_found(dotenv_file):
logger = logging.getLogger("dotenv.main")

with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.get_key(dotenv_file, "foo")
result = dotenv.get_key(dotenv_file, "foo")

assert result is None
mock_warning.assert_called_once_with("Key %s not found in %s.", "foo", dotenv_file)


def test_get_key_ok(dotenv_file):
Expand Down