From 71f17306fef0f46a0bf7225fad829f611233554f Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 15:04:49 +0100 Subject: [PATCH 01/28] accept multiple files in dotenv run --- src/dotenv/cli.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index b845b95e..8db0e7bf 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -17,7 +17,8 @@ @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', @@ -33,7 +34,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() @@ -121,22 +122,30 @@ 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) - } - + + if not commandline: click.echo('No command given.') exit(1) - ret = run_command(commandline, dotenv_as_dict) + + d = {} + 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 = { + k: v + for (k, v) in dotenv_values(file).items() + if v is not None and (override or k not in os.environ) + } + + for k, v in dotenv_as_dict.items(): + d[k] = v + + + ret = run_command(commandline, d) exit(ret) From 5f7d1f8312e7d30644d0befa38b21b553e292df2 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 18:37:53 +0100 Subject: [PATCH 02/28] allow multiple files for other cli targets --- src/dotenv/cli.py | 83 +++++++++++++++++++++++++++++----------------- src/dotenv/main.py | 2 +- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 8db0e7bf..8d1afe2b 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,4 +1,5 @@ import json +import logging import os import shlex import sys @@ -15,6 +16,8 @@ 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')], @@ -45,24 +48,30 @@ 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) + + d = {} + 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 = dotenv_values(file) + + for k, v in dotenv_as_dict.items(): + d[k] = v + if format == 'json': - click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) + click.echo(json.dumps(d, indent=2, sort_keys=True)) else: prefix = 'export ' if format == 'export' else '' - for k in sorted(dotenv_as_dict): - v = dotenv_as_dict[k] + for k in sorted(d): + v = d[k] if v is not None: if format in ('export', 'shell'): v = shlex.quote(v) click.echo('%s%s=%s' % (prefix, k, v)) - + @cli.command() @click.pass_context @@ -70,14 +79,15 @@ 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() @@ -85,16 +95,23 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: @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}.") exit(1) @@ -103,11 +120,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)) else: exit(1) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 05d377a9..eb07100b 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -117,7 +117,7 @@ def get_key( Returns `None` if the key isn't found or doesn't have a value. """ - return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) + return DotEnv(dotenv_path, verbose=False, encoding=encoding).get(key_to_get) @contextmanager From 6718b16f9bdc388034e35e767cb8225a2d701af9 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 18:48:23 +0100 Subject: [PATCH 03/28] fix behavior or adapt tests --- src/dotenv/cli.py | 10 ++++------ src/dotenv/main.py | 2 +- tests/test_cli.py | 2 +- tests/test_main.py | 12 ++---------- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 8d1afe2b..50c1edf9 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -146,11 +146,6 @@ def unset(ctx: click.Context, key: Any) -> None: def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: """Run command with environment variables present.""" - - if not commandline: - click.echo('No command given.') - exit(1) - d = {} for file in ctx.obj['FILES']: if not os.path.isfile(file): @@ -166,7 +161,10 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: for k, v in dotenv_as_dict.items(): d[k] = v - + + if not commandline: + click.echo('No command given.') + exit(1) ret = run_command(commandline, d) exit(ret) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index eb07100b..05d377a9 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -117,7 +117,7 @@ def get_key( Returns `None` if the key isn't found or doesn't have a value. """ - return DotEnv(dotenv_path, verbose=False, encoding=encoding).get(key_to_get) + return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) @contextmanager diff --git a/tests/test_cli.py b/tests/test_cli.py index ca5ba2a1..eaa7cea1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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, f"Successfully removed a from {dotenv_file}\n") assert open(dotenv_file, "r").read() == "" diff --git a/tests/test_main.py b/tests/test_main.py index 82c73ba1..26147dc9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -77,8 +77,7 @@ def test_get_key_no_file(tmp_path): nx_file = str(tmp_path / "nx") logger = logging.getLogger("dotenv.main") - with mock.patch.object(logger, "info") as mock_info, \ - mock.patch.object(logger, "warning") as mock_warning: + with mock.patch.object(logger, "info") as mock_info: result = dotenv.get_key(nx_file, "foo") assert result is None @@ -87,21 +86,14 @@ def test_get_key_no_file(tmp_path): mock.call("Python-dotenv could not find configuration file %s.", nx_file) ], ) - mock_warning.assert_has_calls( - calls=[ - mock.call("Key %s not found in %s.", "foo", nx_file) - ], - ) 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): From b4f335b7a51292e8720da0554bbad7621feca07e Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 19:15:06 +0100 Subject: [PATCH 04/28] improve readability --- src/dotenv/cli.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 50c1edf9..cc37615b 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -48,25 +48,20 @@ 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.''' - - d = {} + 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 = dotenv_values(file) - - for k, v in dotenv_as_dict.items(): - d[k] = v - + dotenv_as_dict.update(dotenv_values(file)) if format == 'json': - click.echo(json.dumps(d, indent=2, sort_keys=True)) + click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) else: prefix = 'export ' if format == 'export' else '' - for k in sorted(d): - v = d[k] + for k in sorted(dotenv_as_dict): + v = dotenv_as_dict[k] if v is not None: if format in ('export', 'shell'): v = shlex.quote(v) @@ -145,28 +140,22 @@ 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.""" - - d = {} + 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 = { + 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) - } - - for k, v in dotenv_as_dict.items(): - d[k] = v - + }) if not commandline: click.echo('No command given.') exit(1) - - ret = run_command(commandline, d) + ret = run_command(commandline, dotenv_as_dict) exit(ret) From 0660ded18110485d31017eafbf41f443a9b2b24b Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 19:16:45 +0100 Subject: [PATCH 05/28] remove f-string --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index eaa7cea1..c7d1506e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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, f"Successfully removed a from {dotenv_file}\n") + assert (result.exit_code, result.output) == (0, "Successfully removed a from %s\n" % dotenv_file) assert open(dotenv_file, "r").read() == "" From d46b1e662b50cb5d25c4bd624ab7d82ce6ff281e Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 19:40:54 +0100 Subject: [PATCH 06/28] improve formatting --- src/dotenv/cli.py | 4 +--- tests/test_main.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index cc37615b..cf1cd8ae 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -66,7 +66,7 @@ def list(ctx: click.Context, format: bool) -> None: if format in ('export', 'shell'): v = shlex.quote(v) click.echo('%s%s=%s' % (prefix, k, v)) - + @cli.command() @click.pass_context @@ -90,7 +90,6 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: @click.argument('key', required=True) def get(ctx: click.Context, key: Any) -> None: '''Retrieve the value for the given key.''' - value, set = None, False files = ctx.obj['FILES'] for file in files: @@ -102,7 +101,6 @@ def get(ctx: click.Context, key: Any) -> None: stored_value = get_key(file, key) if stored_value: value, set = stored_value, True - if set: click.echo(value) else: diff --git a/tests/test_main.py b/tests/test_main.py index 26147dc9..4f5112bc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -89,8 +89,6 @@ def test_get_key_no_file(tmp_path): def test_get_key_not_found(dotenv_file): - logger = logging.getLogger("dotenv.main") - result = dotenv.get_key(dotenv_file, "foo") assert result is None From bc474ca9ef67fde32a9afba135a43d6bf7310aa3 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 10 Sep 2022 19:44:47 +0100 Subject: [PATCH 07/28] restore code to test for warning --- tests/test_main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 4f5112bc..1980bfdf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -77,7 +77,8 @@ def test_get_key_no_file(tmp_path): nx_file = str(tmp_path / "nx") logger = logging.getLogger("dotenv.main") - with mock.patch.object(logger, "info") as mock_info: + with mock.patch.object(logger, "info") as mock_info, \ + mock.patch.object(logger, "warning") as mock_warning: result = dotenv.get_key(nx_file, "foo") assert result is None @@ -86,6 +87,11 @@ def test_get_key_no_file(tmp_path): mock.call("Python-dotenv could not find configuration file %s.", nx_file) ], ) + mock_warning.assert_has_calls( + calls=[ + mock.call("Key %s not found in %s.", "foo", nx_file) + ], + ) def test_get_key_not_found(dotenv_file): From 2b3a28be6ee47c6e4780c5df6196642bf2e5e894 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 1 Oct 2022 12:39:43 +0100 Subject: [PATCH 08/28] format code --- src/dotenv/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index cf1cd8ae..986323a3 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -123,7 +123,8 @@ def unset(ctx: click.Context, key: Any) -> None: 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)) + source = success_files[0] if len(success_files) == 1 else success_files + click.echo("Successfully removed %s from %s" % (key, source)) else: exit(1) From f730c6d92900f8741ae03f24e177b1a3ef32ecfc Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 1 Oct 2022 13:29:14 +0100 Subject: [PATCH 09/28] add flag to allow suppressing warning in get_key in cli.py we may use multiple files and getting multiple errors but want to avoid multiple errors if at least one file has the key --- src/dotenv/main.py | 3 ++- tests/test_main.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 05d377a9..28661fa0 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -111,13 +111,14 @@ def get_key( dotenv_path: Union[str, os.PathLike], key_to_get: str, encoding: Optional[str] = "utf-8", + verbose: bool = True, ) -> Optional[str]: """ Get the value of a given key from the given .env. Returns `None` if the key isn't found or doesn't have a value. """ - return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) + return DotEnv(dotenv_path, verbose=verbose, encoding=encoding).get(key_to_get) @contextmanager diff --git a/tests/test_main.py b/tests/test_main.py index 1980bfdf..21b6801d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -94,10 +94,24 @@ def test_get_key_no_file(tmp_path): ) -def test_get_key_not_found(dotenv_file): - result = dotenv.get_key(dotenv_file, "foo") +def test_get_key_not_found_verbose(dotenv_file): + logger = logging.getLogger("dotenv.main") + + with mock.patch.object(logger, "warning") as mock_warning: + 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_not_found_silent(dotenv_file): + logger = logging.getLogger("dotenv.main") + + with mock.patch.object(logger, "warning") as mock_warning: + result = dotenv.get_key(dotenv_file, "foo", verbose=False) assert result is None + mock_warning.assert_not_called() def test_get_key_ok(dotenv_file): From d6c1385bc37df07b52fc22ab316bbc9d6d253404 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Sat, 1 Oct 2022 20:18:55 +0100 Subject: [PATCH 10/28] fix warning and test in cli --- src/dotenv/cli.py | 7 ++----- tests/test_cli.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 986323a3..79917dcf 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,5 +1,4 @@ import json -import logging import os import shlex import sys @@ -16,8 +15,6 @@ 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')], @@ -98,13 +95,13 @@ def get(ctx: click.Context, key: Any) -> None: 'Path "%s" does not exist.' % (file), ctx=ctx ) - stored_value = get_key(file, key) + stored_value = get_key(file, key, verbose=False) 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}.") + click.echo(f"Key {key} not found in {files[0] if len(files) == 1 else files}.", err=True) exit(1) diff --git a/tests/test_cli.py b/tests/test_cli.py index c7d1506e..93564fde 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -61,7 +61,7 @@ def test_get_existing_value(cli, dotenv_file): def test_get_non_existent_value(cli, dotenv_file): result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'get', 'a']) - assert (result.exit_code, result.output) == (1, "") + assert (result.exit_code, result.output) == (1, f"Key a not found in {dotenv_file}.\n") def test_get_no_file(cli): From c585a7b3bb2cb00c131af4b9532ac4a00abfdcf1 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Mon, 4 Dec 2023 21:08:43 +0000 Subject: [PATCH 11/28] fix lint warnings --- src/dotenv/cli.py | 15 ++++++--------- tests/test_cli.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 629a2137..15f85fab 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -33,7 +33,7 @@ def enumerate_env(): @click.group() @click.option('-f', '--file', default=enumerate_env(), - type=click.Path(file_okay=True), # TODO: unsure about this + type=click.Path(file_okay=True), # TODO: unsure about this multiple=True, help="Location of the .env file, defaults to .env file in current working directory.") @click.option('-q', '--quote', default='always', @@ -103,7 +103,7 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: quote = ctx.obj['QUOTE'] export = ctx.obj['EXPORT'] - for file in ctx.obj['FILES']: + for file in files: success, key, value = set_key(file, key, value, quote, export) if success: click.echo(f'{key}={value}') @@ -111,8 +111,6 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: exit(1) - - @cli.command() @click.pass_context @click.argument('key', required=True) @@ -125,7 +123,7 @@ def get(ctx: click.Context, key: Any) -> None: with stream_file(file) as stream: file_values = dotenv_values(stream=stream) values.update(file_values) - + stored_value = values.get(key) if stored_value: click.echo(stored_value) @@ -144,12 +142,12 @@ def unset(ctx: click.Context, key: Any) -> None: global_success = False success_files = [] - for file in ctx.obj['FILES']: + for file in files: success, key = unset_key(file, key, quote) if success: global_success = True success_files.append(file) - + if global_success: source = success_files[0] if len(success_files) == 1 else success_files click.echo("Successfully removed %s from %s" % (key, source)) @@ -184,8 +182,7 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: if v is not None and (override or k not in os.environ) } dotenv_as_dict.update(file_dotenv_as_dict) - - + if not commandline: click.echo('No command given.') exit(1) diff --git a/tests/test_cli.py b/tests/test_cli.py index 092149fd..88262cf0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -68,7 +68,7 @@ def test_get_existing_value(cli, dotenv_path): def test_get_non_existent_value(cli, dotenv_path): result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) - assert (result.exit_code, result.output) == (1, f"Key a not found in {dotenv_file}.\n") + assert (result.exit_code, result.output) == (1, "Key a not found.\n") def test_get_non_existent_file(cli): From 9b1b4af76a349a07b52d4b8400ef5371d59d8f6b Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Mon, 4 Dec 2023 21:53:27 +0000 Subject: [PATCH 12/28] fix some problems --- src/dotenv/cli.py | 25 ++++++++++++++++--------- src/dotenv/main.py | 3 ++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 15f85fab..ae5dcca6 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,4 +1,5 @@ import json +import logging import os import shlex import sys @@ -16,6 +17,8 @@ from .main import dotenv_values, set_key, unset_key from .version import __version__ +logger = logging.getLogger(__name__) + def enumerate_env(): """ @@ -44,9 +47,9 @@ def enumerate_env(): help="Whether to write the dot file as an executable bash script.") @click.version_option(version=__version__) @click.pass_context -def cli(ctx: click.Context, files: list[Any], quote: Any, export: Any) -> None: +def cli(ctx: click.Context, file: list[Any], quote: Any, export: Any) -> None: """This script is used to set, get or unset values from a .env file.""" - ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILES': files} + ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILES': file} @contextmanager @@ -103,12 +106,15 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: quote = ctx.obj['QUOTE'] export = ctx.obj['EXPORT'] + successes = [] for file in files: success, key, value = set_key(file, key, value, quote, export) - if success: - click.echo(f'{key}={value}') - else: - exit(1) + successes.append(success) + + if all(successes): + click.echo(f'{key}={value}') + else: + exit(1) @cli.command() @@ -143,16 +149,17 @@ def unset(ctx: click.Context, key: Any) -> None: global_success = False success_files = [] for file in files: - success, key = unset_key(file, key, quote) + success, key = unset_key(file, key, quote, warn_key_not_found=False) if success: global_success = True success_files.append(file) if global_success: source = success_files[0] if len(success_files) == 1 else success_files - click.echo("Successfully removed %s from %s" % (key, source)) - + click.echo("Successfully removed %s from %s." % (key, source)) else: + source = files[0] if len(files) == 1 else files + logger.warning("Key %s not removed from %s - key doesn't exist.", key, source) exit(1) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 351db17d..a56a2277 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -199,6 +199,7 @@ def unset_key( key_to_unset: str, quote_mode: str = "always", encoding: Optional[str] = "utf-8", + warn_key_not_found: bool = True, ) -> Tuple[Optional[bool], str]: """ Removes a given key from the given `.env` file. @@ -218,7 +219,7 @@ def unset_key( else: dest.write(mapping.original.string) - if not removed: + if not removed and warn_key_not_found: logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path) return None, key_to_unset From a2d9365c941504f116ecb29bec70981b593e7abf Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Mon, 4 Dec 2023 22:26:07 +0000 Subject: [PATCH 13/28] fix tests --- tests/test_cli.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 88262cf0..4a4dafab 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -68,7 +68,7 @@ def test_get_existing_value(cli, dotenv_path): def test_get_non_existent_value(cli, dotenv_path): result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) - assert (result.exit_code, result.output) == (1, "Key a not found.\n") + assert (result.exit_code, result.output) == (1, "Key a not found in {}.\n".format(dotenv_path)) def test_get_non_existent_file(cli): @@ -89,8 +89,9 @@ def test_unset_existing_value(cli, dotenv_path): dotenv_path.write_text("a=b") result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'unset', 'a']) + print(result.output) - assert (result.exit_code, result.output) == (0, "Successfully removed a\n") + assert (result.exit_code, result.output) == (0, "Successfully removed a from {}.\n".format(dotenv_path)) assert dotenv_path.read_text() == "" From f5ed3e9bd9c8bcf5d6aae9fc662970d1f7b57e2f Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Mon, 4 Dec 2023 22:33:14 +0000 Subject: [PATCH 14/28] fix type hint --- src/dotenv/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index ae5dcca6..eaa24730 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -47,7 +47,7 @@ def enumerate_env(): help="Whether to write the dot file as an executable bash script.") @click.version_option(version=__version__) @click.pass_context -def cli(ctx: click.Context, file: list[Any], quote: Any, export: Any) -> None: +def cli(ctx: click.Context, file: List[Any], quote: Any, export: Any) -> None: """This script is used to set, get or unset values from a .env file.""" ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILES': file} From 49c34a55f3d0929837a2b5b857562af07669478d Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Mon, 4 Dec 2023 22:51:54 +0000 Subject: [PATCH 15/28] add some tests --- tests/conftest.py | 7 +++++++ tests/test_cli.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 69193de0..2437019d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,3 +14,10 @@ def dotenv_path(tmp_path): path = tmp_path / '.env' path.write_bytes(b'') yield path + + +@pytest.fixture +def extra_dotenv_path(tmp_path): + path = tmp_path / '.env_extra' + path.write_bytes(b'') + yield path diff --git a/tests/test_cli.py b/tests/test_cli.py index 4a4dafab..bc22261c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,7 @@ import os import sh from pathlib import Path -from typing import Optional +from typing import List, Optional import pytest @@ -25,7 +25,7 @@ ("export", "x='a b c'", '''export x='a b c'\n'''), ) ) -def test_list(cli, dotenv_path, format: Optional[str], content: str, expected: str): +def test_list_single_file(cli, dotenv_path, format: Optional[str], content: str, expected: str): dotenv_path.write_text(content + '\n') args = ['--file', dotenv_path, 'list'] @@ -37,6 +37,34 @@ def test_list(cli, dotenv_path, format: Optional[str], content: str, expected: s assert (result.exit_code, result.output) == (0, expected) +@pytest.mark.parametrize( + "format,contents,expected", + ( + (None, ["x='a b c'", "y='a b c'"], '''x=a b c\ny=a b c\n'''), + (None, ["x='a b c'", "x='d e f'"], '''x=d e f\n'''), + (None, ["x='a b c'\ny=d e f", "x='x y z'"], '''x=x y z\ny=d e f\n'''), + ) +) +def test_list_multi_file( + cli, + dotenv_path, + extra_dotenv_path, + format: Optional[str], + contents: List[str], + expected: str +): + dotenv_path.write_text(contents[0] + '\n') + extra_dotenv_path.write_text(contents[1] + '\n') + + args = ['--file', dotenv_path, '--file', extra_dotenv_path, 'list'] + if format is not None: + args.extend(['--format', format]) + + result = cli.invoke(dotenv_cli, args) + + assert (result.exit_code, result.output) == (0, expected) + + def test_list_non_existent_file(cli): result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'list']) From 437a9219cc74a4f7a78c3274666d5e754ea66e1b Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 10:45:45 +0000 Subject: [PATCH 16/28] remove support for unsetting multiple files --- src/dotenv/cli.py | 22 +++++++++------------- src/dotenv/main.py | 3 +-- tests/test_cli.py | 8 +++++++- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index eaa24730..d85a77ad 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -146,20 +146,16 @@ def unset(ctx: click.Context, key: Any) -> None: files = ctx.obj['FILES'] quote = ctx.obj['QUOTE'] - global_success = False - success_files = [] - for file in files: - success, key = unset_key(file, key, quote, warn_key_not_found=False) - if success: - global_success = True - success_files.append(file) - - if global_success: - source = success_files[0] if len(success_files) == 1 else success_files - click.echo("Successfully removed %s from %s." % (key, source)) + if len(files) > 1: + click.echo(f"Unset is not supported for multiple files: {[str(f) for f in files]}.") + exit(1) + + file = files[0] + + success, key = unset_key(file, key, quote) + if success: + click.echo(f"Successfully removed {key}") else: - source = files[0] if len(files) == 1 else files - logger.warning("Key %s not removed from %s - key doesn't exist.", key, source) exit(1) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index a56a2277..351db17d 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -199,7 +199,6 @@ def unset_key( key_to_unset: str, quote_mode: str = "always", encoding: Optional[str] = "utf-8", - warn_key_not_found: bool = True, ) -> Tuple[Optional[bool], str]: """ Removes a given key from the given `.env` file. @@ -219,7 +218,7 @@ def unset_key( else: dest.write(mapping.original.string) - if not removed and warn_key_not_found: + if not removed: logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path) return None, key_to_unset diff --git a/tests/test_cli.py b/tests/test_cli.py index bc22261c..eba706de 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -119,7 +119,7 @@ def test_unset_existing_value(cli, dotenv_path): result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'unset', 'a']) print(result.output) - assert (result.exit_code, result.output) == (0, "Successfully removed a from {}.\n".format(dotenv_path)) + assert (result.exit_code, result.output) == (0, "Successfully removed a\n") assert dotenv_path.read_text() == "" @@ -130,6 +130,12 @@ def test_unset_non_existent_value(cli, dotenv_path): assert dotenv_path.read_text() == "" +def test_unset_multi_file_not_allowed(cli, dotenv_path, extra_dotenv_path): + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, '--file', extra_dotenv_path, 'unset', 'a']) + assert result.exit_code == 1 + assert result.output == f"Unset is not supported for multiple files: ['{dotenv_path}', '{extra_dotenv_path}'].\n" + + @pytest.mark.parametrize( "quote_mode,variable,value,expected", ( From a10e462e5460b88533b5036919f2478be1402c3c Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 10:53:04 +0000 Subject: [PATCH 17/28] improve backward compatibility --- src/dotenv/cli.py | 1 - tests/test_cli.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index d85a77ad..ef03a111 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -134,7 +134,6 @@ def get(ctx: click.Context, key: Any) -> None: if stored_value: click.echo(stored_value) else: - click.echo(f"Key {key} not found in {files[0] if len(files) == 1 else files}.", err=True) exit(1) diff --git a/tests/test_cli.py b/tests/test_cli.py index eba706de..72355e6f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -95,8 +95,7 @@ def test_get_existing_value(cli, dotenv_path): def test_get_non_existent_value(cli, dotenv_path): result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) - - assert (result.exit_code, result.output) == (1, "Key a not found in {}.\n".format(dotenv_path)) + assert (result.exit_code, result.output) == (1, "") def test_get_non_existent_file(cli): @@ -117,7 +116,6 @@ def test_unset_existing_value(cli, dotenv_path): dotenv_path.write_text("a=b") result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'unset', 'a']) - print(result.output) assert (result.exit_code, result.output) == (0, "Successfully removed a\n") assert dotenv_path.read_text() == "" From 186dc81fad5351be11c2a232a127fc9888d572dd Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 10:57:53 +0000 Subject: [PATCH 18/28] remove unused logger --- src/dotenv/cli.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index ef03a111..8e386db1 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,5 +1,4 @@ import json -import logging import os import shlex import sys @@ -17,8 +16,6 @@ from .main import dotenv_values, set_key, unset_key from .version import __version__ -logger = logging.getLogger(__name__) - def enumerate_env(): """ @@ -36,7 +33,7 @@ def enumerate_env(): @click.group() @click.option('-f', '--file', default=enumerate_env(), - type=click.Path(file_okay=True), # TODO: unsure about this + type=click.Path(file_okay=True), multiple=True, help="Location of the .env file, defaults to .env file in current working directory.") @click.option('-q', '--quote', default='always', From dde1654037902d1a292a720b56d00cacf8b43a8e Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 11:12:53 +0000 Subject: [PATCH 19/28] remove support for set on multiple files --- src/dotenv/cli.py | 12 +++++++----- tests/test_cli.py | 6 ++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 8e386db1..fe451c14 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -103,12 +103,14 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: quote = ctx.obj['QUOTE'] export = ctx.obj['EXPORT'] - successes = [] - for file in files: - success, key, value = set_key(file, key, value, quote, export) - successes.append(success) + if len(files) > 1: + click.echo(f"Set is not supported for multiple files: {[str(f) for f in files]}.") + exit(1) - if all(successes): + file = files[0] + + success, key, value = set_key(file, key, value, quote, export) + if success: click.echo(f'{key}={value}') else: exit(1) diff --git a/tests/test_cli.py b/tests/test_cli.py index 72355e6f..44b2ef93 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -184,6 +184,12 @@ def test_set_no_file(cli): assert "Missing argument" in result.output +def test_set_multi_file_not_allowed(cli, dotenv_path, extra_dotenv_path): + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, '--file', extra_dotenv_path, 'set', 'a', 'b']) + assert result.exit_code == 1 + assert result.output == f"Set is not supported for multiple files: ['{dotenv_path}', '{extra_dotenv_path}'].\n" + + def test_get_default_path(tmp_path): with sh.pushd(tmp_path): (tmp_path / ".env").write_text("a=b") From ec238190db3e5859c12c29b873c9cc966a8aba29 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 11:15:08 +0000 Subject: [PATCH 20/28] print errors to stderr --- src/dotenv/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index fe451c14..ec4b9eb7 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -104,7 +104,7 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: export = ctx.obj['EXPORT'] if len(files) > 1: - click.echo(f"Set is not supported for multiple files: {[str(f) for f in files]}.") + click.echo(f"Set is not supported for multiple files: {[str(f) for f in files]}.", err=True) exit(1) file = files[0] @@ -145,7 +145,7 @@ def unset(ctx: click.Context, key: Any) -> None: quote = ctx.obj['QUOTE'] if len(files) > 1: - click.echo(f"Unset is not supported for multiple files: {[str(f) for f in files]}.") + click.echo(f"Unset is not supported for multiple files: {[str(f) for f in files]}.", err=True) exit(1) file = files[0] From febc1538cbb302b078f6b01be0ed7e1413bc7541 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 11:41:19 +0000 Subject: [PATCH 21/28] improve params for test_lit_multi_file --- tests/test_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 44b2ef93..49e55aba 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,9 +40,9 @@ def test_list_single_file(cli, dotenv_path, format: Optional[str], content: str, @pytest.mark.parametrize( "format,contents,expected", ( - (None, ["x='a b c'", "y='a b c'"], '''x=a b c\ny=a b c\n'''), - (None, ["x='a b c'", "x='d e f'"], '''x=d e f\n'''), - (None, ["x='a b c'\ny=d e f", "x='x y z'"], '''x=x y z\ny=d e f\n'''), + (None, ["x='1'", "y='2'"], '''x=1\ny=2\n'''), + (None, ["x='1'", "x='2'"], '''x=2\n'''), + (None, ["x='1'\ny='2'", "y='20'\nz='30'"], '''x=1\ny=20\nz=30\n'''), ) ) def test_list_multi_file( From c52dd9e94136101952bf9dc61725f2dba470c705 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 11:49:33 +0000 Subject: [PATCH 22/28] test get for multiple files --- tests/test_cli.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 49e55aba..16394d40 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -85,7 +85,7 @@ def test_list_no_file(cli): assert (result.exit_code, result.output) == (1, "") -def test_get_existing_value(cli, dotenv_path): +def test_get_existing_value_single_file(cli, dotenv_path): dotenv_path.write_text("a=b") result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) @@ -93,6 +93,24 @@ def test_get_existing_value(cli, dotenv_path): assert (result.exit_code, result.output) == (0, "b\n") +@pytest.mark.parametrize( + "contents,expected_values", + ( + (["a=1", "b=2"], {"a": "1", "b": "2"}), + (["b=2", "a=1"], {"a": "1", "b": "2"}), + (["a=1", "a=2"], {"a": "2"}), + ) +) +def test_get_existing_value_multi_file(cli, contents, expected_values, dotenv_path, extra_dotenv_path): + dotenv_path.write_text(contents[0]) + extra_dotenv_path.write_text(contents[1]) + + for key, value in expected_values.items(): + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, '--file', extra_dotenv_path, 'get', key]) + + assert (result.exit_code, result.output) == (0, f"{value}\n") + + def test_get_non_existent_value(cli, dotenv_path): result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) assert (result.exit_code, result.output) == (1, "") From 9e3df3061c01fdd954ca0ed424754582a703afb2 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 11:53:25 +0000 Subject: [PATCH 23/28] test run with multiple envs --- tests/test_cli.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 16394d40..d5c93742 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -265,6 +265,24 @@ def test_run_with_other_env(dotenv_path): assert result == "b\n" +@pytest.mark.parametrize( + "contents,expected_values", + ( + (["a=1", "b=2"], {"a": "1", "b": "2"}), + (["b=2", "a=1"], {"a": "1", "b": "2"}), + (["a=1", "a=2"], {"a": "2"}), + ) +) +def test_run_with_multiple_envs(contents, expected_values, dotenv_path, extra_dotenv_path): + dotenv_path.write_text(contents[0]) + extra_dotenv_path.write_text(contents[1]) + + for key, value in expected_values.items(): + result = sh.dotenv("--file", dotenv_path, '--file', extra_dotenv_path, "run", "printenv", key) + + assert result == f"{value}\n" + + def test_run_without_cmd(cli): result = cli.invoke(dotenv_cli, ['run']) From 6090d64b53f2af8fa0df76c16f6b5a1ca10d90e4 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 12:03:14 +0000 Subject: [PATCH 24/28] update docs --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ddc8ba87..04da7847 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status][build_status_badge]][build_status_link] [![PyPI version][pypi_badge]][pypi_link] -Python-dotenv reads key-value pairs from a `.env` file and can set them as environment +Python-dotenv reads key-value pairs from `.env` files and can set them as environment variables. It helps in the development of applications following the [12-factor](https://12factor.net/) principles. @@ -154,6 +154,17 @@ $ dotenv list --format=json $ dotenv run -- python foo.py ``` +The CLI interface also supports reading from multiple `.env` files. + +```shell +$ echo -e "a=1\nb=2\n" > .env1 +$ echo -e "b=20\nc=30\n" > .env2 +$ python -m dotenv -f .env1 -f .env2 list +a=1 +b=20 +c=30 +``` + Run `dotenv --help` for more information about the options and subcommands. ## File format From d61c1edfbce2f80cd498b0e7152fb71854554d5c Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 12:25:37 +0000 Subject: [PATCH 25/28] remove unecessary param --- src/dotenv/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 351db17d..383b79f4 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -117,14 +117,13 @@ def get_key( dotenv_path: StrPath, key_to_get: str, encoding: Optional[str] = "utf-8", - verbose: bool = True, ) -> Optional[str]: """ Get the value of a given key from the given .env. Returns `None` if the key isn't found or doesn't have a value. """ - return DotEnv(dotenv_path, verbose=verbose, encoding=encoding).get(key_to_get) + return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) @contextmanager From 46f1b75b4a537d86dcc6fe82997daa22d3912d1e Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Fri, 8 Dec 2023 12:38:33 +0000 Subject: [PATCH 26/28] remove format param for test_list_multi_file --- tests/test_cli.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index d5c93742..f42eceb8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,7 @@ import os import sh from pathlib import Path -from typing import List, Optional +from typing import Dict, List, Optional import pytest @@ -38,27 +38,18 @@ def test_list_single_file(cli, dotenv_path, format: Optional[str], content: str, @pytest.mark.parametrize( - "format,contents,expected", + "contents,expected", ( - (None, ["x='1'", "y='2'"], '''x=1\ny=2\n'''), - (None, ["x='1'", "x='2'"], '''x=2\n'''), - (None, ["x='1'\ny='2'", "y='20'\nz='30'"], '''x=1\ny=20\nz=30\n'''), + (["x='1'", "y='2'"], '''x=1\ny=2\n'''), + (["x='1'", "x='2'"], '''x=2\n'''), + (["x='1'\ny='2'", "y='20'\nz='30'"], '''x=1\ny=20\nz=30\n'''), ) ) -def test_list_multi_file( - cli, - dotenv_path, - extra_dotenv_path, - format: Optional[str], - contents: List[str], - expected: str -): +def test_list_multi_file(cli, dotenv_path, extra_dotenv_path, contents: List[str], expected: str): dotenv_path.write_text(contents[0] + '\n') extra_dotenv_path.write_text(contents[1] + '\n') args = ['--file', dotenv_path, '--file', extra_dotenv_path, 'list'] - if format is not None: - args.extend(['--format', format]) result = cli.invoke(dotenv_cli, args) @@ -101,7 +92,13 @@ def test_get_existing_value_single_file(cli, dotenv_path): (["a=1", "a=2"], {"a": "2"}), ) ) -def test_get_existing_value_multi_file(cli, contents, expected_values, dotenv_path, extra_dotenv_path): +def test_get_existing_value_multi_file( + cli, + dotenv_path, + extra_dotenv_path, + contents: List[str], + expected_values: Dict[str, str] +): dotenv_path.write_text(contents[0]) extra_dotenv_path.write_text(contents[1]) @@ -273,7 +270,7 @@ def test_run_with_other_env(dotenv_path): (["a=1", "a=2"], {"a": "2"}), ) ) -def test_run_with_multiple_envs(contents, expected_values, dotenv_path, extra_dotenv_path): +def test_run_with_multi_envs(dotenv_path, extra_dotenv_path, contents: List[str], expected_values: Dict[str, str]): dotenv_path.write_text(contents[0]) extra_dotenv_path.write_text(contents[1]) From 363aab5b3b228e670c09019b134570949faf5de3 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Thu, 28 Dec 2023 22:35:42 +0000 Subject: [PATCH 27/28] document loading order of multiple .envs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 04da7847..bb735e08 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ $ dotenv run -- python foo.py ``` The CLI interface also supports reading from multiple `.env` files. +Each file overrides the previous: if there are duplicate keys, the last file determines the final value. ```shell $ echo -e "a=1\nb=2\n" > .env1 From 1d33b6ad8e09f8bd1ffef73def57b581eafae4b9 Mon Sep 17 00:00:00 2001 From: duarte-pompeu Date: Thu, 28 Dec 2023 22:37:05 +0000 Subject: [PATCH 28/28] revert change in description changes only affect the CLI, not the whole package --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb735e08..ba617b42 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status][build_status_badge]][build_status_link] [![PyPI version][pypi_badge]][pypi_link] -Python-dotenv reads key-value pairs from `.env` files and can set them as environment +Python-dotenv reads key-value pairs from a `.env` file and can set them as environment variables. It helps in the development of applications following the [12-factor](https://12factor.net/) principles.