Skip to content
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
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.0
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
22 changes: 14 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,29 @@ a pull request.

Executing the tests:

$ pip install -r requirements.txt
$ pip install -e .
$ flake8
$ pytest
$ uv venv
$ uv pip install -r requirements.txt
$ uv pip install -e .
$ uv ruff check .
$ uv format .
$ uv run pytest

or with [tox](https://pypi.org/project/tox/) installed:

$ tox


Use of pre-commit is recommended:

$ uv run precommit install


Documentation is published with [mkdocs]():

```shell
$ pip install -r requirements-docs.txt
$ pip install -e .
$ mkdocs serve
$ uv pip install -r requirements-docs.txt
$ uv pip install -e .
$ uv run mkdocs serve
```

Open http://127.0.0.1:8000/ to view the documentation locally.

2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
include LICENSE *.md *.yml *.toml
include LICENSE *.md *.yml *.yaml *.toml

include tox.ini
recursive-include docs *.md
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ sdist: clean
ls -l dist

test:
pip install -e .
flake8 .
py.test tests/
uv pip install -e .
ruff check .
pytest tests/

coverage:
coverage run --source=dotenv --omit='*tests*' -m py.test tests/ -v --tb=native
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
black~=22.3.0
bumpversion
click
flake8>=2.2.3
ipython
pytest-cov
pytest>=3.9
sh>=2
tox
twine
wheel
ruff
pre-commit
19 changes: 19 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[lint]
select = [
# pycodestyle
"E4",
"E7",
"E9",

# Pyflakes
"F",

# flake8-bugbear
"B",

# iSort
"I",

# flake8-builtins
"A",
]
32 changes: 17 additions & 15 deletions src/dotenv/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Any, Optional

from .main import (dotenv_values, find_dotenv, get_key, load_dotenv, set_key,
unset_key)
from .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key


def load_ipython_extension(ipython: Any) -> None:
from .ipython import load_ipython_extension

load_ipython_extension(ipython)


Expand All @@ -21,29 +21,31 @@ def get_cli_string(
Useful for converting a arguments passed to a fabric task
to be passed to a `local` or `run` command.
"""
command = ['dotenv']
command = ["dotenv"]
if quote:
command.append(f'-q {quote}')
command.append(f"-q {quote}")
if path:
command.append(f'-f {path}')
command.append(f"-f {path}")
if action:
command.append(action)
if key:
command.append(key)
if value:
if ' ' in value:
if " " in value:
command.append(f'"{value}"')
else:
command.append(value)

return ' '.join(command).strip()
return " ".join(command).strip()


__all__ = ['get_cli_string',
'load_dotenv',
'dotenv_values',
'get_key',
'set_key',
'unset_key',
'find_dotenv',
'load_ipython_extension']
__all__ = [
"get_cli_string",
"load_dotenv",
"dotenv_values",
"get_key",
"set_key",
"unset_key",
"find_dotenv",
"load_ipython_extension",
]
115 changes: 64 additions & 51 deletions src/dotenv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
import shlex
import sys
from contextlib import contextmanager
from typing import Any, Dict, IO, Iterator, List, Optional
from typing import IO, Any, Dict, Iterator, List, Optional

if sys.platform == 'win32':
if sys.platform == "win32":
from subprocess import Popen

try:
import click
except ImportError:
sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
'Run pip install "python-dotenv[cli]" to fix this.')
sys.stderr.write(
"It seems python-dotenv is not installed with cli option. \n"
'Run pip install "python-dotenv[cli]" to fix this.'
)
sys.exit(1)

from .main import dotenv_values, set_key, unset_key
Expand All @@ -29,25 +31,37 @@ def enumerate_env() -> Optional[str]:
cwd = os.getcwd()
except FileNotFoundError:
return None
path = os.path.join(cwd, '.env')
path = os.path.join(cwd, ".env")
return path


@click.group()
@click.option('-f', '--file', default=enumerate_env(),
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',
type=click.Choice(['always', 'never', 'auto']),
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
@click.option('-e', '--export', default=False,
type=click.BOOL,
help="Whether to write the dot file as an executable bash script.")
@click.option(
"-f",
"--file",
default=enumerate_env(),
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",
type=click.Choice(["always", "never", "auto"]),
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.",
)
@click.option(
"-e",
"--export",
default=False,
type=click.BOOL,
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: 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, 'FILE': file}
ctx.obj = {"QUOTE": quote, "EXPORT": export, "FILE": file}


@contextmanager
Expand All @@ -66,53 +80,57 @@ def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
exit(2)


@cli.command()
@cli.command(name="list")
@click.pass_context
@click.option('--format', default='simple',
type=click.Choice(['simple', 'json', 'shell', 'export']),
help="The format in which to display the list. Default format is simple, "
"which displays name=value without quotes.")
def list(ctx: click.Context, format: bool) -> None:
@click.option(
"--format",
"output_format",
default="simple",
type=click.Choice(["simple", "json", "shell", "export"]),
help="The format in which to display the list. Default format is simple, "
"which displays name=value without quotes.",
)
def list_values(ctx: click.Context, output_format: str) -> None:
"""Display all the stored key/value."""
file = ctx.obj['FILE']
file = ctx.obj["FILE"]

with stream_file(file) as stream:
values = dotenv_values(stream=stream)

if format == 'json':
if output_format == "json":
click.echo(json.dumps(values, indent=2, sort_keys=True))
else:
prefix = 'export ' if format == 'export' else ''
prefix = "export " if output_format == "export" else ""
for k in sorted(values):
v = values[k]
if v is not None:
if format in ('export', 'shell'):
if output_format in ("export", "shell"):
v = shlex.quote(v)
click.echo(f'{prefix}{k}={v}')
click.echo(f"{prefix}{k}={v}")


@cli.command()
@cli.command(name="set")
@click.pass_context
@click.argument('key', required=True)
@click.argument('value', required=True)
def set(ctx: click.Context, key: Any, value: Any) -> None:
@click.argument("key", required=True)
@click.argument("value", required=True)
def set_value(ctx: click.Context, key: Any, value: Any) -> None:
"""Store the given key/value."""
file = ctx.obj['FILE']
quote = ctx.obj['QUOTE']
export = ctx.obj['EXPORT']
file = ctx.obj["FILE"]
quote = ctx.obj["QUOTE"]
export = ctx.obj["EXPORT"]
success, key, value = set_key(file, key, value, quote, export)
if success:
click.echo(f'{key}={value}')
click.echo(f"{key}={value}")
else:
exit(1)


@cli.command()
@click.pass_context
@click.argument('key', required=True)
@click.argument("key", required=True)
def get(ctx: click.Context, key: Any) -> None:
"""Retrieve the value for the given key."""
file = ctx.obj['FILE']
file = ctx.obj["FILE"]

with stream_file(file) as stream:
values = dotenv_values(stream=stream)
Expand All @@ -126,33 +144,32 @@ def get(ctx: click.Context, key: Any) -> None:

@cli.command()
@click.pass_context
@click.argument('key', required=True)
@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']
file = ctx.obj["FILE"]
quote = ctx.obj["QUOTE"]
success, key = unset_key(file, key, quote)
if success:
click.echo(f"Successfully removed {key}")
else:
exit(1)


@cli.command(context_settings={'ignore_unknown_options': True})
@cli.command(context_settings={"ignore_unknown_options": True})
@click.pass_context
@click.option(
"--override/--no-override",
default=True,
help="Override variables from the environment file with those from the .env file.",
)
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
@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']
file = ctx.obj["FILE"]
if not os.path.isfile(file):
raise click.BadParameter(
f'Invalid value for \'-f\' "{file}" does not exist.',
ctx=ctx
f"Invalid value for '-f' \"{file}\" does not exist.", ctx=ctx
)
dotenv_as_dict = {
k: v
Expand All @@ -161,7 +178,7 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
}

if not commandline:
click.echo('No command given.')
click.echo("No command given.")
exit(1)
run_command(commandline, dotenv_as_dict)

Expand Down Expand Up @@ -190,14 +207,10 @@ def run_command(command: List[str], env: Dict[str, str]) -> None:
cmd_env = os.environ.copy()
cmd_env.update(env)

if sys.platform == 'win32':
if sys.platform == "win32":
# execvpe on Windows returns control immediately
# rather than once the command has finished.
p = Popen(command,
universal_newlines=True,
bufsize=0,
shell=False,
env=cmd_env)
p = Popen(command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env)
_, _ = p.communicate()

exit(p.returncode)
Expand Down
Loading