Repository: theskumar/python-dotenv
Pull request #441 (2022-11-12 13:53:47)

Improve error message for `get` and `list` commands when env file can't be opened
The error message would previously be confusing. For example, `dotenv -f . list` would print:

    Error: Invalid value: Path "." does not exist.

Instead, we now print:

    Error opening env file: [Errno 21] Is a directory: '.'

I used this opportunity to slightly refactor the I/O code (e.g. fewer system calls and possible race conditions) for those two subcommands (`get` and `list`).


Patch:
diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py
index 97d6a94..b490bfa 100644
--- a/src/dotenv/cli.py
+++ b/src/dotenv/cli.py
@@ -2,8 +2,9 @@ import json
 import os
 import shlex
 import sys
+from contextlib import contextmanager
 from subprocess import Popen
-from typing import Any, Dict, List
+from typing import Any, Dict, IO, Iterator, List
 
 try:
     import click
@@ -12,7 +13,7 @@ except ImportError:
                      'Run pip install "python-dotenv[cli]" to fix this.')
     sys.exit(1)
 
-from .main import dotenv_values, get_key, set_key, unset_key
+from .main import dotenv_values, set_key, unset_key
 from .version import __version__
 
 
@@ -33,6 +34,22 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
     ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file}
 
 
+@contextmanager
+def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
+    """
+    Open a file and yield the corresponding (decoded) stream.
+
+    Exits with error code 2 if the file cannot be opened.
+    """
+
+    try:
+        with open(path) as stream:
+            yield stream
+    except OSError as exc:
+        print(f"Error opening env file: {exc}", file=sys.stderr)
+        exit(2)
+
+
 @cli.command()
 @click.pass_context
 @click.option('--format', default='simple',
@@ -42,18 +59,16 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
 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(
-            f'Path "{file}" does not exist.',
-            ctx=ctx
-        )
-    dotenv_as_dict = dotenv_values(file)
+
+    with stream_file(file) as stream:
+        values = dotenv_values(stream=stream)
+
     if format == 'json':
-        click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True))
+        click.echo(json.dumps(values, 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(values):
+            v = values[k]
             if v is not None:
                 if format in ('export', 'shell'):
                     v = shlex.quote(v)
@@ -82,12 +97,11 @@ def set(ctx: click.Context, key: Any, value: Any) -> None:
 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(
-            f'Path "{file}" does not exist.',
-            ctx=ctx
-        )
-    stored_value = get_key(file, key)
+
+    with stream_file(file) as stream:
+        values = dotenv_values(stream=stream)
+
+    stored_value = values.get(key)
     if stored_value:
         click.echo(stored_value)
     else:
diff --git a/src/dotenv/main.py b/src/dotenv/main.py
index e7b8392..36ed94f 100644
--- a/src/dotenv/main.py
+++ b/src/dotenv/main.py
@@ -321,7 +321,7 @@ def load_dotenv(
             from the `.env` file.
         encoding: Encoding to be used to read the file.
     Returns:
-        Bool: True if atleast one environment variable is set else False
+        Bool: True if at least one environment variable is set else False
 
     If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
     .env file.


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ca5ba2a..02dc9d9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -40,7 +40,14 @@ def test_list_non_existent_file(cli):
     result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'list'])
 
     assert result.exit_code == 2, result.output
-    assert "does not exist" in result.output
+    assert "Error opening env file" in result.output
+
+
+def test_list_not_a_file(cli):
+    result = cli.invoke(dotenv_cli, ['--file', '.', 'list'])
+
+    assert result.exit_code == 2, result.output
+    assert "Error opening env file" in result.output
 
 
 def test_list_no_file(cli):
@@ -64,11 +71,18 @@ def test_get_non_existent_value(cli, dotenv_file):
     assert (result.exit_code, result.output) == (1, "")
 
 
-def test_get_no_file(cli):
+def test_get_non_existent_file(cli):
     result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'get', 'a'])
 
     assert result.exit_code == 2
-    assert "does not exist" in result.output
+    assert "Error opening env file" in result.output
+
+
+def test_get_not_a_file(cli):
+    result = cli.invoke(dotenv_cli, ['--file', '.', 'get', 'a'])
+
+    assert result.exit_code == 2
+    assert "Error opening env file" in result.output
 
 
 def test_unset_existing_value(cli, dotenv_file):