New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

python-dotenv

Package Overview
Dependencies
Maintainers
2
Versions
50
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

python-dotenv - pypi Package Compare versions

Comparing version
1.1.1
to
1.2.0
+8
.pre-commit-config.yaml
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
[build-system]
requires = ["setuptools >= 77.0"]
build-backend = "setuptools.build_meta"
[project]
name = "python-dotenv"
description = "Read key-value pairs from a .env file and set them as environment variables"
authors = [
{name = "Saurabh Kumar", email = "me+github@saurabh-kumar.com"},
]
license = "BSD-3-Clause"
keywords = [
"environment variables",
"deployments",
"settings",
"env",
"dotenv",
"configurations",
"python",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: PyPy",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Operating System :: OS Independent",
"Topic :: System :: Systems Administration",
"Topic :: Utilities",
"Environment :: Web Environment",
]
requires-python = ">=3.9"
dynamic = ["version", "readme"]
[project.urls]
Source = "https://github.com/theskumar/python-dotenv"
[project.optional-dependencies]
cli = [
"click>=5.0",
]
[project.scripts]
dotenv = "dotenv.__main__:cli"
[tool.setuptools]
packages = ["dotenv"]
package-dir = {"" = "src"}
package-data = {dotenv = ["py.typed"]}
[tool.setuptools.dynamic]
version = {attr = "dotenv.version.__version__"}
readme = {file = ["README.md", "CHANGELOG.md"], content-type = "text/markdown"}
[lint]
select = [
# pycodestyle
"E4",
"E7",
"E9",
# Pyflakes
"F",
# flake8-bugbear
"B",
# iSort
"I",
# flake8-builtins
"A",
]
+21
-9

@@ -8,5 +8,11 @@ # Changelog

## [1.2.0] - 2025-10-26
- Upgrade build system to use PEP 517 & PEP 518 to use `build` and `pyproject.toml` by [@EpicWink] in [#583]
- Add support for Python 3.14 by [@23f3001135] in [#579](https://github.com/theskumar/python-dotenv/pull/563)
- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569]
## [1.1.1] - 2025-06-24
## Fixed
### Fixed

@@ -375,3 +381,11 @@ * CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563](https://github.com/theskumar/python-dotenv/pull/563)

[#553]: https://github.com/theskumar/python-dotenv/issues/553
[#569]: https://github.com/theskumar/python-dotenv/issues/569
[#583]: https://github.com/theskumar/python-dotenv/issues/583
[@23f3001135]: https://github.com/23f3001135
[@EpicWink]: https://github.com/EpicWink
[@Flimm]: https://github.com/Flimm
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@Qwerty-133]: https://github.com/Qwerty-133
[@alanjds]: https://github.com/alanjds

@@ -391,3 +405,2 @@ [@altendky]: https://github.com/altendky

[@eumiro]: https://github.com/eumiro
[@Flimm]: https://github.com/Flimm
[@freddyaboulton]: https://github.com/freddyaboulton

@@ -403,9 +416,8 @@ [@gergelyk]: https://github.com/gergelyk

[@lsmith77]: https://github.com/lsmith77
[@matthewfranglen]: https://github.com/matthewfranglen
[@mgorny]: https://github.com/mgorny
[@naorlivne]: https://github.com/@naorlivne
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@qnighy]: https://github.com/qnighy
[@Qwerty-133]: https://github.com/Qwerty-133
[@rabinadk1]: https://github.com/@rabinadk1
[@randomseed42]: https://github.com/zueve
[@sammck]: https://github.com/@sammck

@@ -419,10 +431,10 @@ [@samwyma]: https://github.com/samwyma

[@venthur]: https://github.com/venthur
[@wrongontheinternet]: https://github.com/wrongontheinternet
[@x-yuri]: https://github.com/x-yuri
[@yannham]: https://github.com/yannham
[@zueve]: https://github.com/zueve
[@randomseed42]: https://github.com/zueve
[@wrongontheinternet]: https://github.com/wrongontheinternet
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...HEAD
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...1.1.1
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0

@@ -429,0 +441,0 @@ [1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1

@@ -10,6 +10,8 @@ Contributing

$ 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

@@ -21,11 +23,15 @@ or with [tox](https://pypi.org/project/tox/) installed:

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.

@@ -8,5 +8,11 @@ # Changelog

## [1.2.0] - 2025-10-26
- Upgrade build system to use PEP 517 & PEP 518 to use `build` and `pyproject.toml` by [@EpicWink] in [#583]
- Add support for Python 3.14 by [@23f3001135] in [#579](https://github.com/theskumar/python-dotenv/pull/563)
- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569]
## [1.1.1] - 2025-06-24
## Fixed
### Fixed

@@ -375,3 +381,11 @@ * CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563](https://github.com/theskumar/python-dotenv/pull/563)

[#553]: https://github.com/theskumar/python-dotenv/issues/553
[#569]: https://github.com/theskumar/python-dotenv/issues/569
[#583]: https://github.com/theskumar/python-dotenv/issues/583
[@23f3001135]: https://github.com/23f3001135
[@EpicWink]: https://github.com/EpicWink
[@Flimm]: https://github.com/Flimm
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@Qwerty-133]: https://github.com/Qwerty-133
[@alanjds]: https://github.com/alanjds

@@ -391,3 +405,2 @@ [@altendky]: https://github.com/altendky

[@eumiro]: https://github.com/eumiro
[@Flimm]: https://github.com/Flimm
[@freddyaboulton]: https://github.com/freddyaboulton

@@ -403,9 +416,8 @@ [@gergelyk]: https://github.com/gergelyk

[@lsmith77]: https://github.com/lsmith77
[@matthewfranglen]: https://github.com/matthewfranglen
[@mgorny]: https://github.com/mgorny
[@naorlivne]: https://github.com/@naorlivne
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@qnighy]: https://github.com/qnighy
[@Qwerty-133]: https://github.com/Qwerty-133
[@rabinadk1]: https://github.com/@rabinadk1
[@randomseed42]: https://github.com/zueve
[@sammck]: https://github.com/@sammck

@@ -419,10 +431,10 @@ [@samwyma]: https://github.com/samwyma

[@venthur]: https://github.com/venthur
[@wrongontheinternet]: https://github.com/wrongontheinternet
[@x-yuri]: https://github.com/x-yuri
[@yannham]: https://github.com/yannham
[@zueve]: https://github.com/zueve
[@randomseed42]: https://github.com/zueve
[@wrongontheinternet]: https://github.com/wrongontheinternet
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...HEAD
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...1.1.1
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0

@@ -429,0 +441,0 @@ [1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1

@@ -10,6 +10,8 @@ Contributing

$ 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

@@ -21,11 +23,15 @@ or with [tox](https://pypi.org/project/tox/) installed:

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.

@@ -39,10 +39,17 @@ # python-dotenv

load_dotenv() # take environment variables
load_dotenv() # reads variables from a .env file and sets them in os.environ
```
# Code of your application, which uses environment variables (e.g. from `os.environ` or
# `os.getenv`) as if they came from the actual environment.
```
By default, `load_dotenv` doesn't override existing environment variables and looks for a `.env` file in same directory as python script or searches for it incrementally higher up.
By default, `load_dotenv()` will:
- Look for a `.env` file in the same directory as the Python script (or higher up the directory tree).
- Read each key-value pair and add it to `os.environ`.
- **Not override** an environment variable that is already set, unless you explicitly pass `override=True`.
To configure the development environment, add a `.env` in the root directory of your

@@ -138,2 +145,6 @@ project:

### Disable load_dotenv
Set `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env files or streams. Useful when you can't modify third-party package calls or in production.
## Command-line Interface

@@ -140,0 +151,0 @@

@@ -23,9 +23,9 @@ .PHONY: clean-pyc clean-build test

sdist: clean
python setup.py sdist bdist_wheel
python -m build -o dist .
ls -l dist
test:
pip install -e .
flake8 .
py.test tests/
uv pip install -e .
ruff check .
pytest tests/

@@ -32,0 +32,0 @@ coverage:

@@ -1,2 +0,2 @@

include LICENSE *.md *.yml *.toml
include LICENSE *.md *.yml *.yaml *.toml

@@ -3,0 +3,0 @@ include tox.ini

+39
-29
Metadata-Version: 2.4
Name: python-dotenv
Version: 1.1.1
Version: 1.2.0
Summary: Read key-value pairs from a .env file and set them as environment variables
Home-page: https://github.com/theskumar/python-dotenv
Author: Saurabh Kumar
Author-email: me+github@saurabh-kumar.com
License: BSD-3-Clause
Author-email: Saurabh Kumar <me+github@saurabh-kumar.com>
License-Expression: BSD-3-Clause
Project-URL: Source, https://github.com/theskumar/python-dotenv
Keywords: environment variables,deployments,settings,env,dotenv,configurations,python

@@ -21,3 +20,2 @@ Classifier: Development Status :: 5 - Production/Stable

Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent

@@ -32,14 +30,3 @@ Classifier: Topic :: System :: Systems Administration

Requires-Dist: click>=5.0; extra == "cli"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary

@@ -84,10 +71,17 @@ # python-dotenv

load_dotenv() # take environment variables
load_dotenv() # reads variables from a .env file and sets them in os.environ
```
# Code of your application, which uses environment variables (e.g. from `os.environ` or
# `os.getenv`) as if they came from the actual environment.
```
By default, `load_dotenv` doesn't override existing environment variables and looks for a `.env` file in same directory as python script or searches for it incrementally higher up.
By default, `load_dotenv()` will:
- Look for a `.env` file in the same directory as the Python script (or higher up the directory tree).
- Read each key-value pair and add it to `os.environ`.
- **Not override** an environment variable that is already set, unless you explicitly pass `override=True`.
To configure the development environment, add a `.env` in the root directory of your

@@ -183,2 +177,6 @@ project:

### Disable load_dotenv
Set `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env files or streams. Useful when you can't modify third-party package calls or in production.
## Command-line Interface

@@ -303,5 +301,11 @@

## [1.2.0] - 2025-10-26
- Upgrade build system to use PEP 517 & PEP 518 to use `build` and `pyproject.toml` by [@EpicWink] in [#583]
- Add support for Python 3.14 by [@23f3001135] in [#579](https://github.com/theskumar/python-dotenv/pull/563)
- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569]
## [1.1.1] - 2025-06-24
## Fixed
### Fixed

@@ -670,3 +674,11 @@ * CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563](https://github.com/theskumar/python-dotenv/pull/563)

[#553]: https://github.com/theskumar/python-dotenv/issues/553
[#569]: https://github.com/theskumar/python-dotenv/issues/569
[#583]: https://github.com/theskumar/python-dotenv/issues/583
[@23f3001135]: https://github.com/23f3001135
[@EpicWink]: https://github.com/EpicWink
[@Flimm]: https://github.com/Flimm
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@Qwerty-133]: https://github.com/Qwerty-133
[@alanjds]: https://github.com/alanjds

@@ -686,3 +698,2 @@ [@altendky]: https://github.com/altendky

[@eumiro]: https://github.com/eumiro
[@Flimm]: https://github.com/Flimm
[@freddyaboulton]: https://github.com/freddyaboulton

@@ -698,9 +709,8 @@ [@gergelyk]: https://github.com/gergelyk

[@lsmith77]: https://github.com/lsmith77
[@matthewfranglen]: https://github.com/matthewfranglen
[@mgorny]: https://github.com/mgorny
[@naorlivne]: https://github.com/@naorlivne
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@qnighy]: https://github.com/qnighy
[@Qwerty-133]: https://github.com/Qwerty-133
[@rabinadk1]: https://github.com/@rabinadk1
[@randomseed42]: https://github.com/zueve
[@sammck]: https://github.com/@sammck

@@ -714,10 +724,10 @@ [@samwyma]: https://github.com/samwyma

[@venthur]: https://github.com/venthur
[@wrongontheinternet]: https://github.com/wrongontheinternet
[@x-yuri]: https://github.com/x-yuri
[@yannham]: https://github.com/yannham
[@zueve]: https://github.com/zueve
[@randomseed42]: https://github.com/zueve
[@wrongontheinternet]: https://github.com/wrongontheinternet
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...HEAD
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...1.1.1
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0

@@ -724,0 +734,0 @@ [1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1

@@ -39,10 +39,17 @@ # python-dotenv

load_dotenv() # take environment variables
load_dotenv() # reads variables from a .env file and sets them in os.environ
```
# Code of your application, which uses environment variables (e.g. from `os.environ` or
# `os.getenv`) as if they came from the actual environment.
```
By default, `load_dotenv` doesn't override existing environment variables and looks for a `.env` file in same directory as python script or searches for it incrementally higher up.
By default, `load_dotenv()` will:
- Look for a `.env` file in the same directory as the Python script (or higher up the directory tree).
- Read each key-value pair and add it to `os.environ`.
- **Not override** an environment variable that is already set, unless you explicitly pass `override=True`.
To configure the development environment, add a `.env` in the root directory of your

@@ -138,2 +145,6 @@ project:

### Disable load_dotenv
Set `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env files or streams. Useful when you can't modify third-party package calls or in production.
## Command-line Interface

@@ -140,0 +151,0 @@

@@ -1,5 +0,3 @@

black~=22.3.0
bumpversion
click
flake8>=2.2.3
ipython

@@ -12,1 +10,3 @@ pytest-cov

wheel
ruff
pre-commit
[bumpversion]
current_version = 1.1.1
current_version = 1.2.0
commit = True

@@ -16,5 +16,2 @@ tag = True

[metadata]
description_file = README.md
[tool:pytest]

@@ -21,0 +18,0 @@ testpaths = tests

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

@@ -9,2 +8,3 @@

from .ipython import load_ipython_extension
load_ipython_extension(ipython)

@@ -25,7 +25,7 @@

"""
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:

@@ -36,3 +36,3 @@ command.append(action)

if value:
if ' ' in value:
if " " in value:
command.append(f'"{value}"')

@@ -42,12 +42,14 @@ else:

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",
]

@@ -6,5 +6,5 @@ import json

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

@@ -15,4 +15,6 @@

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)

@@ -34,3 +36,3 @@

return None
path = os.path.join(cwd, '.env')
path = os.path.join(cwd, ".env")
return path

@@ -40,11 +42,23 @@

@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__)

@@ -54,3 +68,3 @@ @click.pass_context

"""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}

@@ -71,14 +85,18 @@

print(f"Error opening env file: {exc}", file=sys.stderr)
exit(2)
sys.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"]

@@ -88,28 +106,28 @@ with stream_file(file) as 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)
sys.exit(1)

@@ -119,6 +137,6 @@

@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"]

@@ -132,3 +150,3 @@ with stream_file(file) as stream:

else:
exit(1)
sys.exit(1)

@@ -138,7 +156,7 @@

@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)

@@ -148,6 +166,6 @@ if success:

else:
exit(1)
sys.exit(1)
@cli.command(context_settings={'ignore_unknown_options': True})
@cli.command(context_settings={"ignore_unknown_options": True})
@click.pass_context

@@ -159,10 +177,9 @@ @click.option(

)
@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
)

@@ -176,4 +193,4 @@ dotenv_as_dict = {

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

@@ -206,14 +223,10 @@

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)
sys.exit(p.returncode)
else:
os.execvpe(command[0], args=command, env=cmd_env)
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore
parse_argstring) # type: ignore
from IPython.core.magic_arguments import (
argument,
magic_arguments,
parse_argstring,
) # type: ignore

@@ -10,14 +13,22 @@ from .main import find_dotenv, load_dotenv

class IPythonDotEnv(Magics):
@magic_arguments()
@argument(
'-o', '--override', action='store_true',
help="Indicate to override existing variables"
"-o",
"--override",
action="store_true",
help="Indicate to override existing variables",
)
@argument(
'-v', '--verbose', action='store_true',
help="Indicate function calls to be verbose"
"-v",
"--verbose",
action="store_true",
help="Indicate function calls to be verbose",
)
@argument('dotenv_path', nargs='?', type=str, default='.env',
help='Search in increasingly higher folders for the `dotenv_path`')
@argument(
"dotenv_path",
nargs="?",
type=str,
default=".env",
help="Search in increasingly higher folders for the `dotenv_path`",
)
@line_magic

@@ -24,0 +35,0 @@ def dotenv(self, line):

@@ -24,2 +24,12 @@ import io

def _load_dotenv_disabled() -> bool:
"""
Determine if dotenv loading has been disabled.
"""
if "PYTHON_DOTENV_DISABLED" not in os.environ:
return False
value = os.environ["PYTHON_DOTENV_DISABLED"].casefold()
return value in {"1", "true", "t", "yes", "y"}
def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]:

@@ -353,3 +363,12 @@ for mapping in mappings:

to this function as `dotenv_path`.
If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value,
.env loading is disabled.
"""
if _load_dotenv_disabled():
logger.debug(
"python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable"
)
return False
if dotenv_path is None and stream is None:

@@ -356,0 +375,0 @@ dotenv_path = find_dotenv()

import codecs
import re
from typing import (IO, Iterator, Match, NamedTuple, Optional, # noqa:F401
Pattern, Sequence, Tuple)
from typing import (
IO,
Iterator,
Match,
NamedTuple,
Optional,
Pattern,
Sequence,
)

@@ -76,3 +83,3 @@

return Original(
string=self.string[self.mark.chars:self.position.chars],
string=self.string[self.mark.chars : self.position.chars],
line=self.mark.line,

@@ -82,6 +89,6 @@ )

def peek(self, count: int) -> str:
return self.string[self.position.chars:self.position.chars + count]
return self.string[self.position.chars : self.position.chars + count]
def read(self, count: int) -> str:
result = self.string[self.position.chars:self.position.chars + count]
result = self.string[self.position.chars : self.position.chars + count]
if len(result) < count:

@@ -96,3 +103,3 @@ raise Error("read: End of string")

raise Error("read_regex: Pattern not found")
self.position.advance(self.string[match.start():match.end()])
self.position.advance(self.string[match.start() : match.end()])
return match.groups()

@@ -103,3 +110,3 @@

def decode_match(match: Match[str]) -> str:
return codecs.decode(match.group(0), 'unicode-escape') # type: ignore
return codecs.decode(match.group(0), "unicode-escape") # type: ignore

@@ -127,10 +134,10 @@ return regex.sub(decode_match, string)

char = reader.peek(1)
if char == u"'":
if char == "'":
(value,) = reader.read_regex(_single_quoted_value)
return decode_escapes(_single_quote_escapes, value)
elif char == u'"':
elif char == '"':
(value,) = reader.read_regex(_double_quoted_value)
return decode_escapes(_double_quote_escapes, value)
elif char in (u"", u"\n", u"\r"):
return u""
elif char in ("", "\n", "\r"):
return ""
else:

@@ -137,0 +144,0 @@ return parse_unquoted_value(reader)

@@ -1,1 +0,1 @@

__version__ = "1.1.1"
__version__ = "1.2.0"
Metadata-Version: 2.4
Name: python-dotenv
Version: 1.1.1
Version: 1.2.0
Summary: Read key-value pairs from a .env file and set them as environment variables
Home-page: https://github.com/theskumar/python-dotenv
Author: Saurabh Kumar
Author-email: me+github@saurabh-kumar.com
License: BSD-3-Clause
Author-email: Saurabh Kumar <me+github@saurabh-kumar.com>
License-Expression: BSD-3-Clause
Project-URL: Source, https://github.com/theskumar/python-dotenv
Keywords: environment variables,deployments,settings,env,dotenv,configurations,python

@@ -21,3 +20,2 @@ Classifier: Development Status :: 5 - Production/Stable

Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent

@@ -32,14 +30,3 @@ Classifier: Topic :: System :: Systems Administration

Requires-Dist: click>=5.0; extra == "cli"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary

@@ -84,10 +71,17 @@ # python-dotenv

load_dotenv() # take environment variables
load_dotenv() # reads variables from a .env file and sets them in os.environ
```
# Code of your application, which uses environment variables (e.g. from `os.environ` or
# `os.getenv`) as if they came from the actual environment.
```
By default, `load_dotenv` doesn't override existing environment variables and looks for a `.env` file in same directory as python script or searches for it incrementally higher up.
By default, `load_dotenv()` will:
- Look for a `.env` file in the same directory as the Python script (or higher up the directory tree).
- Read each key-value pair and add it to `os.environ`.
- **Not override** an environment variable that is already set, unless you explicitly pass `override=True`.
To configure the development environment, add a `.env` in the root directory of your

@@ -183,2 +177,6 @@ project:

### Disable load_dotenv
Set `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env files or streams. Useful when you can't modify third-party package calls or in production.
## Command-line Interface

@@ -303,5 +301,11 @@

## [1.2.0] - 2025-10-26
- Upgrade build system to use PEP 517 & PEP 518 to use `build` and `pyproject.toml` by [@EpicWink] in [#583]
- Add support for Python 3.14 by [@23f3001135] in [#579](https://github.com/theskumar/python-dotenv/pull/563)
- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569]
## [1.1.1] - 2025-06-24
## Fixed
### Fixed

@@ -670,3 +674,11 @@ * CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563](https://github.com/theskumar/python-dotenv/pull/563)

[#553]: https://github.com/theskumar/python-dotenv/issues/553
[#569]: https://github.com/theskumar/python-dotenv/issues/569
[#583]: https://github.com/theskumar/python-dotenv/issues/583
[@23f3001135]: https://github.com/23f3001135
[@EpicWink]: https://github.com/EpicWink
[@Flimm]: https://github.com/Flimm
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@Qwerty-133]: https://github.com/Qwerty-133
[@alanjds]: https://github.com/alanjds

@@ -686,3 +698,2 @@ [@altendky]: https://github.com/altendky

[@eumiro]: https://github.com/eumiro
[@Flimm]: https://github.com/Flimm
[@freddyaboulton]: https://github.com/freddyaboulton

@@ -698,9 +709,8 @@ [@gergelyk]: https://github.com/gergelyk

[@lsmith77]: https://github.com/lsmith77
[@matthewfranglen]: https://github.com/matthewfranglen
[@mgorny]: https://github.com/mgorny
[@naorlivne]: https://github.com/@naorlivne
[@Nicals]: https://github.com/Nicals
[@Nougat-Waffle]: https://github.com/Nougat-Waffle
[@qnighy]: https://github.com/qnighy
[@Qwerty-133]: https://github.com/Qwerty-133
[@rabinadk1]: https://github.com/@rabinadk1
[@randomseed42]: https://github.com/zueve
[@sammck]: https://github.com/@sammck

@@ -714,10 +724,10 @@ [@samwyma]: https://github.com/samwyma

[@venthur]: https://github.com/venthur
[@wrongontheinternet]: https://github.com/wrongontheinternet
[@x-yuri]: https://github.com/x-yuri
[@yannham]: https://github.com/yannham
[@zueve]: https://github.com/zueve
[@randomseed42]: https://github.com/zueve
[@wrongontheinternet]: https://github.com/wrongontheinternet
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...HEAD
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...1.1.1
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.0
[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0

@@ -724,0 +734,0 @@ [1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1

.editorconfig
.pre-commit-config.yaml
CHANGELOG.md

@@ -9,6 +10,7 @@ CONTRIBUTING.md

mkdocs.yml
pyproject.toml
requirements-docs.txt
requirements.txt
ruff.toml
setup.cfg
setup.py
tox.ini

@@ -15,0 +17,0 @@ docs/changelog.md

@@ -14,4 +14,4 @@ import pytest

def dotenv_path(tmp_path):
path = tmp_path / '.env'
path.write_bytes(b'')
path = tmp_path / ".env"
path.write_bytes(b"")
yield path
import os
import sh
from pathlib import Path

@@ -7,2 +6,3 @@ from typing import Optional

import pytest
import sh

@@ -15,22 +15,24 @@ import dotenv

@pytest.mark.parametrize(
"format,content,expected",
"output_format,content,expected",
(
(None, "x='a b c'", '''x=a b c\n'''),
("simple", "x='a b c'", '''x=a b c\n'''),
("simple", """x='"a b c"'""", '''x="a b c"\n'''),
("simple", '''x="'a b c'"''', '''x='a b c'\n'''),
("json", "x='a b c'", '''{\n "x": "a b c"\n}\n'''),
(None, "x='a b c'", """x=a b c\n"""),
("simple", "x='a b c'", """x=a b c\n"""),
("simple", """x='"a b c"'""", """x="a b c"\n"""),
("simple", '''x="'a b c'"''', """x='a b c'\n"""),
("json", "x='a b c'", """{\n "x": "a b c"\n}\n"""),
("shell", "x='a b c'", "x='a b c'\n"),
("shell", """x='"a b c"'""", '''x='"a b c"'\n'''),
("shell", '''x="'a b c'"''', '''x=''"'"'a b c'"'"''\n'''),
("shell", """x='"a b c"'""", """x='"a b c"'\n"""),
("shell", '''x="'a b c'"''', """x=''"'"'a b c'"'"''\n"""),
("shell", "x='a\nb\nc'", "x='a\nb\nc'\n"),
("export", "x='a b c'", '''export x='a b c'\n'''),
)
("export", "x='a b c'", """export x='a b c'\n"""),
),
)
def test_list(cli, dotenv_path, format: Optional[str], content: str, expected: str):
dotenv_path.write_text(content + '\n')
def test_list(
cli, dotenv_path, output_format: Optional[str], content: str, expected: str
):
dotenv_path.write_text(content + "\n")
args = ['--file', dotenv_path, 'list']
args = ["--file", dotenv_path, "list"]
if format is not None:
args.extend(['--format', format])
args.extend(["--format", output_format])

@@ -43,3 +45,3 @@ result = cli.invoke(dotenv_cli, args)

def test_list_non_existent_file(cli):
result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'list'])
result = cli.invoke(dotenv_cli, ["--file", "nx_file", "list"])

@@ -51,3 +53,3 @@ assert result.exit_code == 2, result.output

def test_list_not_a_file(cli):
result = cli.invoke(dotenv_cli, ['--file', '.', 'list'])
result = cli.invoke(dotenv_cli, ["--file", ".", "list"])

@@ -59,3 +61,3 @@ assert result.exit_code == 2, result.output

def test_list_no_file(cli):
result = cli.invoke(dotenv.cli.list, [])
result = cli.invoke(dotenv.cli.list_values, [])

@@ -68,3 +70,3 @@ assert (result.exit_code, result.output) == (1, "")

result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a'])
result = cli.invoke(dotenv_cli, ["--file", dotenv_path, "get", "a"])

@@ -75,3 +77,3 @@ assert (result.exit_code, result.output) == (0, "b\n")

def test_get_non_existent_value(cli, dotenv_path):
result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a'])
result = cli.invoke(dotenv_cli, ["--file", dotenv_path, "get", "a"])

@@ -82,3 +84,3 @@ assert (result.exit_code, result.output) == (1, "")

def test_get_non_existent_file(cli):
result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'get', 'a'])
result = cli.invoke(dotenv_cli, ["--file", "nx_file", "get", "a"])

@@ -90,3 +92,3 @@ assert result.exit_code == 2

def test_get_not_a_file(cli):
result = cli.invoke(dotenv_cli, ['--file', '.', 'get', 'a'])
result = cli.invoke(dotenv_cli, ["--file", ".", "get", "a"])

@@ -100,3 +102,3 @@ assert result.exit_code == 2

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

@@ -108,3 +110,3 @@ assert (result.exit_code, result.output) == (0, "Successfully removed a\n")

def test_unset_non_existent_value(cli, dotenv_path):
result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'unset', 'a'])
result = cli.invoke(dotenv_cli, ["--file", dotenv_path, "unset", "a"])

@@ -119,7 +121,7 @@ assert (result.exit_code, result.output) == (1, "")

("always", "a", "x", "a='x'\n"),
("never", "a", "x", 'a=x\n'),
("never", "a", "x", "a=x\n"),
("auto", "a", "x", "a=x\n"),
("auto", "a", "x y", "a='x y'\n"),
("auto", "a", "$", "a='$'\n"),
)
),
)

@@ -129,3 +131,13 @@ def test_set_quote_options(cli, dotenv_path, quote_mode, variable, value, expected):

dotenv_cli,
["--file", dotenv_path, "--export", "false", "--quote", quote_mode, "set", variable, value]
[
"--file",
dotenv_path,
"--export",
"false",
"--quote",
quote_mode,
"set",
variable,
value,
],
)

@@ -142,3 +154,3 @@

(Path(".nx_file"), "false", "a", "x", "a='x'\n"),
)
),
)

@@ -148,3 +160,13 @@ def test_set_export(cli, dotenv_path, export_mode, variable, value, expected):

dotenv_cli,
["--file", dotenv_path, "--quote", "always", "--export", export_mode, "set", variable, value]
[
"--file",
dotenv_path,
"--quote",
"always",
"--export",
export_mode,
"set",
variable,
value,
],
)

@@ -157,3 +179,3 @@

def test_set_non_existent_file(cli):
result = cli.invoke(dotenv.cli.set, ["a", "b"])
result = cli.invoke(dotenv.cli.set_value, ["a", "b"])

@@ -228,3 +250,3 @@ assert (result.exit_code, result.output) == (1, "")

def test_run_without_cmd(cli):
result = cli.invoke(dotenv_cli, ['run'])
result = cli.invoke(dotenv_cli, ["run"])

@@ -236,3 +258,3 @@ assert result.exit_code == 2

def test_run_with_invalid_cmd(cli):
result = cli.invoke(dotenv_cli, ['run', 'i_do_not_exist'])
result = cli.invoke(dotenv_cli, ["run", "i_do_not_exist"])

@@ -244,5 +266,5 @@ assert result.exit_code == 2

def test_run_with_version(cli):
result = cli.invoke(dotenv_cli, ['--version'])
result = cli.invoke(dotenv_cli, ["--version"])
assert result.exit_code == 0
assert result.output.strip().endswith(__version__)

@@ -6,3 +6,2 @@ import os

pytest.importorskip("IPython")

@@ -9,0 +8,0 @@

@@ -0,4 +1,5 @@

import builtins
import sys
import builtins
from unittest import mock
from dotenv.main import find_dotenv

@@ -178,3 +179,5 @@

def test_is_interactive_main_module_with_file_attribute_none(self, tmp_path, monkeypatch):
def test_is_interactive_main_module_with_file_attribute_none(
self, tmp_path, monkeypatch
):
"""Test _is_interactive when __main__ has __file__ attribute set to None."""

@@ -199,3 +202,5 @@ self._remove_ps_attributes(monkeypatch)

def test_is_interactive_no_ps_attributes_and_normal_execution(self, tmp_path, monkeypatch):
def test_is_interactive_no_ps_attributes_and_normal_execution(
self, tmp_path, monkeypatch
):
"""Test normal script execution scenario where _is_interactive should return False."""

@@ -202,0 +207,0 @@ self._remove_ps_attributes(monkeypatch)

@@ -67,3 +67,3 @@ import io

with pytest.raises(Exception):
with pytest.raises(PermissionError):
dotenv.set_key(dotenv_path, "a", "b")

@@ -249,2 +249,126 @@

@pytest.mark.parametrize(
"flag_value",
[
"true",
"yes",
"1",
"t",
"y",
"True",
"Yes",
"TRUE",
"YES",
"T",
"Y",
],
)
def test_load_dotenv_disabled(dotenv_path, flag_value):
expected_environ = {"PYTHON_DOTENV_DISABLED": flag_value}
with mock.patch.dict(os.environ, {"PYTHON_DOTENV_DISABLED": flag_value}, clear=True):
dotenv_path.write_text("a=b")
result = dotenv.load_dotenv(dotenv_path)
assert result is False
assert os.environ == expected_environ
@pytest.mark.parametrize(
"flag_value",
[
"true",
"yes",
"1",
"t",
"y",
"True",
"Yes",
"TRUE",
"YES",
"T",
"Y",
],
)
def test_load_dotenv_disabled_notification(dotenv_path, flag_value):
with mock.patch.dict(os.environ, {"PYTHON_DOTENV_DISABLED": flag_value}, clear=True):
dotenv_path.write_text("a=b")
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "debug") as mock_debug:
result = dotenv.load_dotenv(dotenv_path)
assert result is False
mock_debug.assert_called_once_with(
"python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable"
)
@pytest.mark.parametrize(
"flag_value",
[
"",
"false",
"no",
"0",
"f",
"n",
"False",
"No",
"FALSE",
"NO",
"F",
"N",
],
)
def test_load_dotenv_enabled(dotenv_path, flag_value):
expected_environ = {"PYTHON_DOTENV_DISABLED": flag_value, "a": "b"}
with mock.patch.dict(os.environ, {"PYTHON_DOTENV_DISABLED": flag_value}, clear=True):
dotenv_path.write_text("a=b")
result = dotenv.load_dotenv(dotenv_path)
assert result is True
assert os.environ == expected_environ
@pytest.mark.parametrize(
"flag_value",
[
"",
"false",
"no",
"0",
"f",
"n",
"False",
"No",
"FALSE",
"NO",
"F",
"N",
],
)
def test_load_dotenv_enabled_no_notification(dotenv_path, flag_value):
with mock.patch.dict(os.environ, {"PYTHON_DOTENV_DISABLED": flag_value}, clear=True):
dotenv_path.write_text("a=b")
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "debug") as mock_debug:
result = dotenv.load_dotenv(dotenv_path)
assert result is True
mock_debug.assert_not_called()
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_doesnt_disable_itself(dotenv_path):
dotenv_path.write_text("PYTHON_DOTENV_DISABLED=true")
result = dotenv.load_dotenv(dotenv_path)
assert result is True
assert os.environ == {"PYTHON_DOTENV_DISABLED": "true"}
def test_load_dotenv_no_file_verbose():

@@ -251,0 +375,0 @@ logger = logging.getLogger("dotenv.main")

@@ -8,162 +8,544 @@ import io

@pytest.mark.parametrize("test_input,expected", [
(u"", []),
(u"a=b", [Binding(key=u"a", value=u"b", original=Original(string=u"a=b", line=1), error=False)]),
(u"'a'=b", [Binding(key=u"a", value=u"b", original=Original(string=u"'a'=b", line=1), error=False)]),
(u"[=b", [Binding(key=u"[", value=u"b", original=Original(string=u"[=b", line=1), error=False)]),
(u" a = b ", [Binding(key=u"a", value=u"b", original=Original(string=u" a = b ", line=1), error=False)]),
(u"export a=b", [Binding(key=u"a", value=u"b", original=Original(string=u"export a=b", line=1), error=False)]),
(
u" export 'a'=b",
[Binding(key=u"a", value=u"b", original=Original(string=u" export 'a'=b", line=1), error=False)],
),
(u"# a=b", [Binding(key=None, value=None, original=Original(string=u"# a=b", line=1), error=False)]),
(u"a=b#c", [Binding(key=u"a", value=u"b#c", original=Original(string=u"a=b#c", line=1), error=False)]),
(
u'a=b #c',
[Binding(key=u"a", value=u"b", original=Original(string=u"a=b #c", line=1), error=False)],
),
(
u'a=b\t#c',
[Binding(key=u"a", value=u"b", original=Original(string=u"a=b\t#c", line=1), error=False)],
),
(
u"a=b c",
[Binding(key=u"a", value=u"b c", original=Original(string=u"a=b c", line=1), error=False)],
),
(
u"a=b\tc",
[Binding(key=u"a", value=u"b\tc", original=Original(string=u"a=b\tc", line=1), error=False)],
),
(
u"a=b c",
[Binding(key=u"a", value=u"b c", original=Original(string=u"a=b c", line=1), error=False)],
),
(
u"a=b\u00a0 c",
[Binding(key=u"a", value=u"b\u00a0 c", original=Original(string=u"a=b\u00a0 c", line=1), error=False)],
),
(
u"a=b c ",
[Binding(key=u"a", value=u"b c", original=Original(string=u"a=b c ", line=1), error=False)],
),
(
u"a='b c '",
[Binding(key=u"a", value=u"b c ", original=Original(string=u"a='b c '", line=1), error=False)],
),
(
u'a="b c "',
[Binding(key=u"a", value=u"b c ", original=Original(string=u'a="b c "', line=1), error=False)],
),
(
u"export export_a=1",
[
Binding(key=u"export_a", value=u"1", original=Original(string=u"export export_a=1", line=1), error=False)
],
),
(
u"export port=8000",
[Binding(key=u"port", value=u"8000", original=Original(string=u"export port=8000", line=1), error=False)],
),
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"', line=1), error=False)]),
(u"a='b\nc'", [Binding(key=u"a", value=u"b\nc", original=Original(string=u"a='b\nc'", line=1), error=False)]),
(u'a="b\nc"', [Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"', line=1), error=False)]),
(u'a="b\\nc"', [Binding(key=u"a", value=u'b\nc', original=Original(string=u'a="b\\nc"', line=1), error=False)]),
(u"a='b\\nc'", [Binding(key=u"a", value=u'b\\nc', original=Original(string=u"a='b\\nc'", line=1), error=False)]),
(u'a="b\\"c"', [Binding(key=u"a", value=u'b"c', original=Original(string=u'a="b\\"c"', line=1), error=False)]),
(u"a='b\\'c'", [Binding(key=u"a", value=u"b'c", original=Original(string=u"a='b\\'c'", line=1), error=False)]),
(u"a=à", [Binding(key=u"a", value=u"à", original=Original(string=u"a=à", line=1), error=False)]),
(u'a="à"', [Binding(key=u"a", value=u"à", original=Original(string=u'a="à"', line=1), error=False)]),
(
u'no_value_var',
[Binding(key=u'no_value_var', value=None, original=Original(string=u"no_value_var", line=1), error=False)],
),
(u'a: b', [Binding(key=None, value=None, original=Original(string=u"a: b", line=1), error=True)]),
(
u"a=b\nc=d",
[
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1), error=False),
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2), error=False),
],
),
(
u"a=b\rc=d",
[
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\r", line=1), error=False),
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2), error=False),
],
),
(
u"a=b\r\nc=d",
[
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\r\n", line=1), error=False),
Binding(key=u"c", value=u"d", original=Original(string=u"c=d", line=2), error=False),
],
),
(
u'a=\nb=c',
[
Binding(key=u"a", value=u'', original=Original(string=u'a=\n', line=1), error=False),
Binding(key=u"b", value=u'c', original=Original(string=u"b=c", line=2), error=False),
]
),
(
u"\n\n",
[
Binding(key=None, value=None, original=Original(string=u"\n\n", line=1), error=False),
]
),
(
u"a=b\n\n",
[
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1), error=False),
Binding(key=None, value=None, original=Original(string=u"\n", line=2), error=False),
]
),
(
u'a=b\n\nc=d',
[
Binding(key=u"a", value=u"b", original=Original(string=u"a=b\n", line=1), error=False),
Binding(key=u"c", value=u"d", original=Original(string=u"\nc=d", line=2), error=False),
]
),
(
u'a="\nb=c',
[
Binding(key=None, value=None, original=Original(string=u'a="\n', line=1), error=True),
Binding(key=u"b", value=u"c", original=Original(string=u"b=c", line=2), error=False),
]
),
(
u'# comment\na="b\nc"\nd=e\n',
[
Binding(key=None, value=None, original=Original(string=u"# comment\n", line=1), error=False),
Binding(key=u"a", value=u"b\nc", original=Original(string=u'a="b\nc"\n', line=2), error=False),
Binding(key=u"d", value=u"e", original=Original(string=u"d=e\n", line=4), error=False),
],
),
(
u'a=b\n# comment 1',
[
Binding(key="a", value="b", original=Original(string=u"a=b\n", line=1), error=False),
Binding(key=None, value=None, original=Original(string=u"# comment 1", line=2), error=False),
],
),
(
u'# comment 1\n# comment 2',
[
Binding(key=None, value=None, original=Original(string=u"# comment 1\n", line=1), error=False),
Binding(key=None, value=None, original=Original(string=u"# comment 2", line=2), error=False),
],
),
(
u'uglyKey[%$=\"S3cr3t_P4ssw#rD\" #\na=b',
[
Binding(key=u'uglyKey[%$',
value=u'S3cr3t_P4ssw#rD',
original=Original(string=u"uglyKey[%$=\"S3cr3t_P4ssw#rD\" #\n", line=1), error=False),
Binding(key=u"a", value=u"b", original=Original(string=u'a=b', line=2), error=False),
],
),
])
@pytest.mark.parametrize(
"test_input,expected",
[
("", []),
(
"a=b",
[
Binding(
key="a",
value="b",
original=Original(string="a=b", line=1),
error=False,
)
],
),
(
"'a'=b",
[
Binding(
key="a",
value="b",
original=Original(string="'a'=b", line=1),
error=False,
)
],
),
(
"[=b",
[
Binding(
key="[",
value="b",
original=Original(string="[=b", line=1),
error=False,
)
],
),
(
" a = b ",
[
Binding(
key="a",
value="b",
original=Original(string=" a = b ", line=1),
error=False,
)
],
),
(
"export a=b",
[
Binding(
key="a",
value="b",
original=Original(string="export a=b", line=1),
error=False,
)
],
),
(
" export 'a'=b",
[
Binding(
key="a",
value="b",
original=Original(string=" export 'a'=b", line=1),
error=False,
)
],
),
(
"# a=b",
[
Binding(
key=None,
value=None,
original=Original(string="# a=b", line=1),
error=False,
)
],
),
(
"a=b#c",
[
Binding(
key="a",
value="b#c",
original=Original(string="a=b#c", line=1),
error=False,
)
],
),
(
"a=b #c",
[
Binding(
key="a",
value="b",
original=Original(string="a=b #c", line=1),
error=False,
)
],
),
(
"a=b\t#c",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\t#c", line=1),
error=False,
)
],
),
(
"a=b c",
[
Binding(
key="a",
value="b c",
original=Original(string="a=b c", line=1),
error=False,
)
],
),
(
"a=b\tc",
[
Binding(
key="a",
value="b\tc",
original=Original(string="a=b\tc", line=1),
error=False,
)
],
),
(
"a=b c",
[
Binding(
key="a",
value="b c",
original=Original(string="a=b c", line=1),
error=False,
)
],
),
(
"a=b\u00a0 c",
[
Binding(
key="a",
value="b\u00a0 c",
original=Original(string="a=b\u00a0 c", line=1),
error=False,
)
],
),
(
"a=b c ",
[
Binding(
key="a",
value="b c",
original=Original(string="a=b c ", line=1),
error=False,
)
],
),
(
"a='b c '",
[
Binding(
key="a",
value="b c ",
original=Original(string="a='b c '", line=1),
error=False,
)
],
),
(
'a="b c "',
[
Binding(
key="a",
value="b c ",
original=Original(string='a="b c "', line=1),
error=False,
)
],
),
(
"export export_a=1",
[
Binding(
key="export_a",
value="1",
original=Original(string="export export_a=1", line=1),
error=False,
)
],
),
(
"export port=8000",
[
Binding(
key="port",
value="8000",
original=Original(string="export port=8000", line=1),
error=False,
)
],
),
(
'a="b\nc"',
[
Binding(
key="a",
value="b\nc",
original=Original(string='a="b\nc"', line=1),
error=False,
)
],
),
(
"a='b\nc'",
[
Binding(
key="a",
value="b\nc",
original=Original(string="a='b\nc'", line=1),
error=False,
)
],
),
(
'a="b\nc"',
[
Binding(
key="a",
value="b\nc",
original=Original(string='a="b\nc"', line=1),
error=False,
)
],
),
(
'a="b\\nc"',
[
Binding(
key="a",
value="b\nc",
original=Original(string='a="b\\nc"', line=1),
error=False,
)
],
),
(
"a='b\\nc'",
[
Binding(
key="a",
value="b\\nc",
original=Original(string="a='b\\nc'", line=1),
error=False,
)
],
),
(
'a="b\\"c"',
[
Binding(
key="a",
value='b"c',
original=Original(string='a="b\\"c"', line=1),
error=False,
)
],
),
(
"a='b\\'c'",
[
Binding(
key="a",
value="b'c",
original=Original(string="a='b\\'c'", line=1),
error=False,
)
],
),
(
"a=à",
[
Binding(
key="a",
value="à",
original=Original(string="a=à", line=1),
error=False,
)
],
),
(
'a="à"',
[
Binding(
key="a",
value="à",
original=Original(string='a="à"', line=1),
error=False,
)
],
),
(
"no_value_var",
[
Binding(
key="no_value_var",
value=None,
original=Original(string="no_value_var", line=1),
error=False,
)
],
),
(
"a: b",
[
Binding(
key=None,
value=None,
original=Original(string="a: b", line=1),
error=True,
)
],
),
(
"a=b\nc=d",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\n", line=1),
error=False,
),
Binding(
key="c",
value="d",
original=Original(string="c=d", line=2),
error=False,
),
],
),
(
"a=b\rc=d",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\r", line=1),
error=False,
),
Binding(
key="c",
value="d",
original=Original(string="c=d", line=2),
error=False,
),
],
),
(
"a=b\r\nc=d",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\r\n", line=1),
error=False,
),
Binding(
key="c",
value="d",
original=Original(string="c=d", line=2),
error=False,
),
],
),
(
"a=\nb=c",
[
Binding(
key="a",
value="",
original=Original(string="a=\n", line=1),
error=False,
),
Binding(
key="b",
value="c",
original=Original(string="b=c", line=2),
error=False,
),
],
),
(
"\n\n",
[
Binding(
key=None,
value=None,
original=Original(string="\n\n", line=1),
error=False,
),
],
),
(
"a=b\n\n",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\n", line=1),
error=False,
),
Binding(
key=None,
value=None,
original=Original(string="\n", line=2),
error=False,
),
],
),
(
"a=b\n\nc=d",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\n", line=1),
error=False,
),
Binding(
key="c",
value="d",
original=Original(string="\nc=d", line=2),
error=False,
),
],
),
(
'a="\nb=c',
[
Binding(
key=None,
value=None,
original=Original(string='a="\n', line=1),
error=True,
),
Binding(
key="b",
value="c",
original=Original(string="b=c", line=2),
error=False,
),
],
),
(
'# comment\na="b\nc"\nd=e\n',
[
Binding(
key=None,
value=None,
original=Original(string="# comment\n", line=1),
error=False,
),
Binding(
key="a",
value="b\nc",
original=Original(string='a="b\nc"\n', line=2),
error=False,
),
Binding(
key="d",
value="e",
original=Original(string="d=e\n", line=4),
error=False,
),
],
),
(
"a=b\n# comment 1",
[
Binding(
key="a",
value="b",
original=Original(string="a=b\n", line=1),
error=False,
),
Binding(
key=None,
value=None,
original=Original(string="# comment 1", line=2),
error=False,
),
],
),
(
"# comment 1\n# comment 2",
[
Binding(
key=None,
value=None,
original=Original(string="# comment 1\n", line=1),
error=False,
),
Binding(
key=None,
value=None,
original=Original(string="# comment 2", line=2),
error=False,
),
],
),
(
'uglyKey[%$="S3cr3t_P4ssw#rD" #\na=b',
[
Binding(
key="uglyKey[%$",
value="S3cr3t_P4ssw#rD",
original=Original(
string='uglyKey[%$="S3cr3t_P4ssw#rD" #\n', line=1
),
error=False,
),
Binding(
key="a",
value="b",
original=Original(string="a=b", line=2),
error=False,
),
],
),
],
)
def test_parse_stream(test_input, expected):

@@ -170,0 +552,0 @@ result = parse_stream(io.StringIO(test_input))

@@ -5,10 +5,16 @@ from dotenv import get_cli_string as c

def test_to_cli_string():
assert c() == 'dotenv'
assert c(path='/etc/.env') == 'dotenv -f /etc/.env'
assert c(path='/etc/.env', action='list') == 'dotenv -f /etc/.env list'
assert c(action='list') == 'dotenv list'
assert c(action='get', key='DEBUG') == 'dotenv get DEBUG'
assert c(action='set', key='DEBUG', value='True') == 'dotenv set DEBUG True'
assert c(action='set', key='SECRET', value='=@asdfasf') == 'dotenv set SECRET =@asdfasf'
assert c(action='set', key='SECRET', value='a b') == 'dotenv set SECRET "a b"'
assert c(action='set', key='SECRET', value='a b', quote="always") == 'dotenv -q always set SECRET "a b"'
assert c() == "dotenv"
assert c(path="/etc/.env") == "dotenv -f /etc/.env"
assert c(path="/etc/.env", action="list") == "dotenv -f /etc/.env list"
assert c(action="list") == "dotenv list"
assert c(action="get", key="DEBUG") == "dotenv get DEBUG"
assert c(action="set", key="DEBUG", value="True") == "dotenv set DEBUG True"
assert (
c(action="set", key="SECRET", value="=@asdfasf")
== "dotenv set SECRET =@asdfasf"
)
assert c(action="set", key="SECRET", value="a b") == 'dotenv set SECRET "a b"'
assert (
c(action="set", key="SECRET", value="a b", quote="always")
== 'dotenv -q always set SECRET "a b"'
)

@@ -30,3 +30,3 @@ import pytest

),
]
],
)

@@ -33,0 +33,0 @@ def test_parse_variables(value, expected):

import os
import sys
import sh
import textwrap

@@ -9,3 +8,5 @@ from typing import List

import sh
def walk_to_root(path: str):

@@ -29,12 +30,12 @@ last_dir = None

dirs_init_py_added_to = set()
with ZipFile(zip_file_path, "w") as zip:
with ZipFile(zip_file_path, "w") as zipfile:
for f in files:
zip.writestr(data=f.content, zinfo_or_arcname=f.path)
for dir in walk_to_root(os.path.dirname(f.path)):
if dir not in dirs_init_py_added_to:
print(os.path.join(dir, "__init__.py"))
zip.writestr(
data="", zinfo_or_arcname=os.path.join(dir, "__init__.py")
zipfile.writestr(data=f.content, zinfo_or_arcname=f.path)
for dirname in walk_to_root(os.path.dirname(f.path)):
if dirname not in dirs_init_py_added_to:
print(os.path.join(dirname, "__init__.py"))
zipfile.writestr(
data="", zinfo_or_arcname=os.path.join(dirname, "__init__.py")
)
dirs_init_py_added_to.add(dir)
dirs_init_py_added_to.add(dirname)
return zip_file_path

@@ -41,0 +42,0 @@

@@ -11,2 +11,3 @@ [tox]

3.13: py313, lint, manifest
3.14: py314
pypy-3.9: pypy3

@@ -20,7 +21,7 @@

click
py{39,310,311,312,313,pypy3}: ipython
py{39,310,311,312,313,3.14,pypy3}: ipython
commands = pytest --cov --cov-report=term-missing --cov-config setup.cfg {posargs}
depends =
py{39,310,311,312,313},pypy3: coverage-clean
coverage-report: py{39,310,311,312,313},pypy3
py{39,310,311,312,313,314},pypy3: coverage-clean
coverage-report: py{39,310,311,312,313,314},pypy3

@@ -30,6 +31,8 @@ [testenv:lint]

deps =
flake8
ruff
mypy
commands =
flake8 src tests
ruff check src
ruff check tests
mypy --python-version=3.14 src tests
mypy --python-version=3.13 src tests

@@ -36,0 +39,0 @@ mypy --python-version=3.12 src tests

from setuptools import setup
def read_files(files):
data = []
for file in files:
with open(file, encoding="utf-8") as f:
data.append(f.read())
return "\n".join(data)
long_description = read_files(["README.md", "CHANGELOG.md"])
meta = {}
with open("./src/dotenv/version.py", encoding="utf-8") as f:
exec(f.read(), meta)
setup(
name="python-dotenv",
description="Read key-value pairs from a .env file and set them as environment variables",
long_description=long_description,
long_description_content_type="text/markdown",
version=meta["__version__"],
author="Saurabh Kumar",
author_email="me+github@saurabh-kumar.com",
url="https://github.com/theskumar/python-dotenv",
keywords=[
"environment variables",
"deployments",
"settings",
"env",
"dotenv",
"configurations",
"python",
],
packages=["dotenv"],
package_dir={"": "src"},
package_data={
"dotenv": ["py.typed"],
},
python_requires=">=3.9",
extras_require={
"cli": [
"click>=5.0",
],
},
entry_points={
"console_scripts": [
"dotenv=dotenv.__main__:cli",
],
},
license="BSD-3-Clause",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: PyPy",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Topic :: System :: Systems Administration",
"Topic :: Utilities",
"Environment :: Web Environment",
],
)