Repository: theskumar/python-dotenv
Branch: main
Commit: fa4e6a90b454
Files: 42
Total size: 127.9 KB
Directory structure:
gitextract_1wzmeifn/
├── .bumpversion.cfg
├── .editorconfig
├── .github/
│ ├── SECURITY.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── docs/
│ └── reference.md
├── mkdocs.yml
├── pyproject.toml
├── requirements-docs.txt
├── requirements.txt
├── ruff.toml
├── src/
│ └── dotenv/
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli.py
│ ├── ipython.py
│ ├── main.py
│ ├── parser.py
│ ├── py.typed
│ ├── variables.py
│ └── version.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_cli.py
│ ├── test_fifo_dotenv.py
│ ├── test_ipython.py
│ ├── test_is_interactive.py
│ ├── test_lib.py
│ ├── test_main.py
│ ├── test_parser.py
│ ├── test_utils.py
│ ├── test_variables.py
│ └── test_zip_imports.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .bumpversion.cfg
================================================
[bumpversion]
current_version = 1.2.2
commit = True
tag = True
[bumpversion:file:src/dotenv/version.py]
================================================
FILE: .editorconfig
================================================
# see: http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4
[*.yml]
indent_style = space
indent_size = 2
================================================
FILE: .github/SECURITY.md
================================================
# Security Policy
## Supported Versions
| Version | Supported |
| --------- | ------------------ |
| latest | :white_check_mark: |
| 0.x | :x: |
## Reporting a Vulnerability
If you believe you have identified a security issue with python-dotenv, please email
python-dotenv@saurabh-kumar.com. A maintainer will contact you acknowledging the report
and how to continue.
Be sure to include as much detail as necessary in your report. As with reporting normal
issues, a minimal reproducible example will help the maintainers address the issue faster.
If you are able, you may also include a fix for the issue generated with `git format-patch`.
================================================
FILE: .github/dependabot.yml
================================================
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly
================================================
FILE: .github/workflows/release.yml
================================================
name: Upload Python Package
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
# Specifying a GitHub environment is optional, but strongly encouraged
environment: release
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Required for pushing to gh-pages branch
contents: write
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package distributions
run: make sdist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Build Documentation
run: |
pip install -r requirements-docs.txt
pip install -e .
mkdocs build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
================================================
FILE: .github/workflows/test.yml
================================================
name: Run Tests
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
max-parallel: 8
matrix:
os:
- ubuntu-latest
python-version:
["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", pypy3.11]
include:
# Windows: Test lowest and highest supported Python versions
- os: windows-latest
python-version: "3.10"
- os: windows-latest
python-version: "3.14"
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Install dependencies
run: pip install tox tox-gh-actions
- name: Test with tox
run: tox
================================================
FILE: .gitignore
================================================
.DS_Store
.idea
.vscode/
# Created by https://www.gitignore.io/api/python
# Edit at https://www.gitignore.io/?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
### Python Patch ###
.venv/
# End of https://www.gitignore.io/api/python
================================================
FILE: .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
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- ...
## [1.2.2] - 2026-03-01
### Added
- Support for Python 3.14, including the free-threaded (3.14t) build. (#588)
### Changed
- The `dotenv run` command now forwards flags directly to the specified command by [@bbc2] in [#607]
- Improved documentation clarity regarding override behavior and the reference page.
- Updated PyPy support to version 3.11.
- Documentation for FIFO file support.
- Dropped Support for Python 3.9.
### Fixed
- Improved `set_key` and `unset_key` behavior when interacting with symlinks by [@bbc2] in [790c5c0]
- Corrected the license specifier and added missing Python 3.14 classifiers in package metadata by [@JYOuyang] in [#590]
### Breaking Changes
- `dotenv.set_key` and `dotenv.unset_key` used to follow symlinks in some
situations. This is no longer the case. For that behavior to be restored in
all cases, `follow_symlinks=True` should be used.
- In the CLI, `set` and `unset` used to follow symlinks in some situations. This
is no longer the case.
- `dotenv.set_key`, `dotenv.unset_key` and the CLI commands `set` and `unset`
used to reset the file mode of the modified .env file to `0o600` in some
situations. This is no longer the case: The original mode of the file is now
preserved. Is the file needed to be created or wasn't a regular file, mode
`0o600` is used.
## [1.2.1] - 2025-10-26
- Move more config to `pyproject.toml`, removed `setup.cfg`
- Add support for reading `.env` from FIFOs (Unix) by [@sidharth-sudhir] in [#586]
## [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]
- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569]
## [1.1.1] - 2025-06-24
### Fixed
- CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563]
- CLI: revert the use of execvpe on Windows by [@wrongontheinternet] in [#566]
## [1.1.0] - 2025-03-25
### Added
- Add support for python 3.13
- Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt]
### Fixed
- `find_dotenv` and `load_dotenv` now correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42])
### Misc
- Drop support for Python 3.8
## [1.0.1] - 2024-01-23
### Fixed
- Gracefully handle code which has been imported from a zipfile ([#456] by [@samwyma])
- Allow modules using `load_dotenv` to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton])
- Fix file not closed after deletion, handle error in the rewrite function ([#469] by [@Qwerty-133])
### Misc
- Use pathlib.Path in tests ([#466] by [@eumiro])
- Fix year in release date in changelog.md ([#454] by [@jankislinger])
- Use https in README links ([#474] by [@Nicals])
## [1.0.0] - 2023-02-24
### Fixed
- Drop support for python 3.7, add python 3.12-dev (#449 by [@theskumar])
- Handle situations where the cwd does not exist. (#446 by [@jctanner])
## [0.21.1] - 2023-01-21
### Added
- Use Python 3.11 non-beta in CI (#438 by [@bbc2])
- Modernize variables code (#434 by [@Nougat-Waffle])
- Modernize main.py and parser.py code (#435 by [@Nougat-Waffle])
- Improve conciseness of cli.py and **init**.py (#439 by [@Nougat-Waffle])
- Improve error message for `get` and `list` commands when env file can't be opened (#441 by [@bbc2])
- Updated License to align with BSD OSI template (#433 by [@lsmith77])
### Fixed
- Fix Out-of-scope error when "dest" variable is undefined (#413 by [@theGOTOguy])
- Fix IPython test warning about deprecated `magic` (#440 by [@bbc2])
- Fix type hint for dotenv_path var, add StrPath alias (#432 by [@eaf])
## [0.21.0] - 2022-09-03
### Added
- CLI: add support for invocations via 'python -m'. (#395 by [@theskumar])
- `load_dotenv` function now returns `False`. (#388 by [@larsks])
- CLI: add --format= option to list command. (#407 by [@sammck])
### Fixed
- Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants])
- Use `open` instead of `io.open`. (#389 by [@rabinadk1])
- Improve documentation for variables without a value (#390 by [@bbc2])
- Add `parse_it` to Related Projects (#410 by [@naorlivne])
- Update README.md (#415 by [@harveer07])
- Improve documentation with direct use of MkDocs (#398 by [@bbc2])
## [0.20.0] - 2022-03-24
### Added
- Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`.
(#379 by [@bbc2])
### Fixed
- Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by
[@mgorny]).
- Don't build universal wheels (#387 by [@bbc2]).
## [0.19.2] - 2021-11-11
### Fixed
- In `set_key`, add missing newline character before new entry if necessary. (#361 by
[@bbc2])
## [0.19.1] - 2021-08-09
### Added
- Add support for Python 3.10. (#359 by [@theskumar])
## [0.19.0] - 2021-07-24
### Changed
- Require Python 3.5 or a later version. Python 2 and 3.4 are no longer supported. (#341
by [@bbc2]).
### Added
- The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str,
os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).
- The `stream` argument of `load_dotenv` and `dotenv_values` can now be a text stream
(`IO[str]`), which includes values like `io.StringIO("foo")` and `open("file.env",
"r")` (#348 by [@bbc2]).
## [0.18.0] - 2021-06-20
### Changed
- Raise `ValueError` if `quote_mode` isn't one of `always`, `auto` or `never` in
`set_key` (#330 by [@bbc2]).
- When writing a value to a .env file with `set_key` or `dotenv set <key> <value>` (#330
by [@bbc2]):
- Use single quotes instead of double quotes.
- Don't strip surrounding quotes.
- In `auto` mode, don't add quotes if the value is only made of alphanumeric characters
(as determined by `string.isalnum`).
## [0.17.1] - 2021-04-29
### Fixed
- Fixed tests for build environments relying on `PYTHONPATH` (#318 by [@befeleme]).
## [0.17.0] - 2021-04-02
### Changed
- Make `dotenv get <key>` only show the value, not `key=value` (#313 by [@bbc2]).
### Added
- Add `--override`/`--no-override` option to `dotenv run` (#312 by [@zueve] and [@bbc2]).
## [0.16.0] - 2021-03-27
### Changed
- The default value of the `encoding` parameter for `load_dotenv` and `dotenv_values` is
now `"utf-8"` instead of `None` (#306 by [@bbc2]).
- Fix resolution order in variable expansion with `override=False` (#287 by [@bbc2]).
## [0.15.0] - 2020-10-28
### Added
- Add `--export` option to `set` to make it prepend the binding with `export` (#270 by
[@jadutter]).
### Changed
- Make `set` command create the `.env` file in the current directory if no `.env` file was
found (#270 by [@jadutter]).
### Fixed
- Fix potentially empty expanded value for duplicate key (#260 by [@bbc2]).
- Fix import error on Python 3.5.0 and 3.5.1 (#267 by [@gongqingkui]).
- Fix parsing of unquoted values containing several adjacent space or tab characters
(#277 by [@bbc2], review by [@x-yuri]).
## [0.14.0] - 2020-07-03
### Changed
- Privilege definition in file over the environment in variable expansion (#256 by
[@elbehery95]).
### Fixed
- Improve error message for when file isn't found (#245 by [@snobu]).
- Use HTTPS URL in package meta data (#251 by [@ekohl]).
## [0.13.0] - 2020-04-16
### Added
- Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]).
## [0.12.0] - 2020-02-28
### Changed
- Use current working directory to find `.env` when bundled by PyInstaller (#213 by
[@gergelyk]).
### Fixed
- Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]).
- Fix `dotenv run` crashing on environment variables without values (#237 by [@yannham]).
- Remove warning when last line is empty (#238 by [@bbc2]).
## [0.11.0] - 2020-02-07
### Added
- Add `interpolate` argument to `load_dotenv` and `dotenv_values` to disable interpolation
(#232 by [@ulyssessouza]).
### Changed
- Use logging instead of warnings (#231 by [@bbc2]).
### Fixed
- Fix installation in non-UTF-8 environments (#225 by [@altendky]).
- Fix PyPI classifiers (#228 by [@bbc2]).
## [0.10.5] - 2020-01-19
### Fixed
- Fix handling of malformed lines and lines without a value (#222 by [@bbc2]):
- Don't print warning when key has no value.
- Reject more malformed lines (e.g. "A: B", "a='b',c").
- Fix handling of lines with just a comment (#224 by [@bbc2]).
## [0.10.4] - 2020-01-17
### Added
- Make typing optional (#179 by [@techalchemy]).
- Print a warning on malformed line (#211 by [@bbc2]).
- Support keys without a value (#220 by [@ulyssessouza]).
## 0.10.3
- Improve interactive mode detection ([@andrewsmith])([#183]).
- Refactor parser to fix parsing inconsistencies ([@bbc2])([#170]).
- Interpret escapes as control characters only in double-quoted strings.
- Interpret `#` as start of comment only if preceded by whitespace.
## 0.10.2
- Add type hints and expose them to users ([@qnighy])([#172])
- `load_dotenv` and `dotenv_values` now accept an `encoding` parameter, defaults to `None`
([@theskumar])([@earlbread])([#161])
- Fix `str`/`unicode` inconsistency in Python 2: values are always `str` now. ([@bbc2])([#121])
- Fix Unicode error in Python 2, introduced in 0.10.0. ([@bbc2])([#176])
## 0.10.1
- Fix parsing of variable without a value ([@asyncee])([@bbc2])([#158])
## 0.10.0
- Add support for UTF-8 in unquoted values ([@bbc2])([#148])
- Add support for trailing comments ([@bbc2])([#148])
- Add backslashes support in values ([@bbc2])([#148])
- Add support for newlines in values ([@bbc2])([#148])
- Force environment variables to str with Python2 on Windows ([@greyli])
- Drop Python 3.3 support ([@greyli])
- Fix stderr/-out/-in redirection ([@venthur])
## 0.9.0
- Add `--version` parameter to cli ([@venthur])
- Enable loading from current directory ([@cjauvin])
- Add 'dotenv run' command for calling arbitrary shell script with .env ([@venthur])
## 0.8.1
- Add tests for docs ([@Flimm])
- Make 'cli' support optional. Use `pip install python-dotenv[cli]`. ([@theskumar])
## 0.8.0
- `set_key` and `unset_key` only modified the affected file instead of
parsing and re-writing file, this causes comments and other file
entact as it is.
- Add support for `export` prefix in the line.
- Internal refractoring ([@theskumar])
- Allow `load_dotenv` and `dotenv_values` to work with `StringIO())` ([@alanjds])([@theskumar])([#78])
## 0.7.1
- Remove hard dependency on iPython ([@theskumar])
## 0.7.0
- Add support to override system environment variable via .env.
([@milonimrod](https://github.com/milonimrod))
([\#63](https://github.com/theskumar/python-dotenv/issues/63))
- Disable ".env not found" warning by default
([@maxkoryukov](https://github.com/maxkoryukov))
([\#57](https://github.com/theskumar/python-dotenv/issues/57))
## 0.6.5
- Add support for special characters `\`.
([@pjona](https://github.com/pjona))
([\#60](https://github.com/theskumar/python-dotenv/issues/60))
## 0.6.4
- Fix issue with single quotes ([@Flimm])
([\#52](https://github.com/theskumar/python-dotenv/issues/52))
## 0.6.3
- Handle unicode exception in setup.py
([\#46](https://github.com/theskumar/python-dotenv/issues/46))
## 0.6.2
- Fix dotenv list command ([@ticosax](https://github.com/ticosax))
- Add iPython Support
([@tillahoffmann](https://github.com/tillahoffmann))
## 0.6.0
- Drop support for Python 2.6
- Handle escaped characters and newlines in quoted values. (Thanks
[@iameugenejo](https://github.com/iameugenejo))
- Remove any spaces around unquoted key/value. (Thanks
[@paulochf](https://github.com/paulochf))
- Added POSIX variable expansion. (Thanks
[@hugochinchilla](https://github.com/hugochinchilla))
## 0.5.1
- Fix `find_dotenv` - it now start search from the file where this
function is called from.
## 0.5.0
- Add `find_dotenv` method that will try to find a `.env` file.
(Thanks [@isms](https://github.com/isms))
## 0.4.0
- cli: Added `-q/--quote` option to control the behaviour of quotes
around values in `.env`. (Thanks
[@hugochinchilla](https://github.com/hugochinchilla)).
- Improved test coverage.
<!-- PR LINKS -->
[#78]: https://github.com/theskumar/python-dotenv/issues/78
[#121]: https://github.com/theskumar/python-dotenv/issues/121
[#148]: https://github.com/theskumar/python-dotenv/issues/148
[#158]: https://github.com/theskumar/python-dotenv/issues/158
[#170]: https://github.com/theskumar/python-dotenv/issues/170
[#172]: https://github.com/theskumar/python-dotenv/issues/172
[#176]: https://github.com/theskumar/python-dotenv/issues/176
[#183]: https://github.com/theskumar/python-dotenv/issues/183
[#359]: https://github.com/theskumar/python-dotenv/issues/359
[#469]: https://github.com/theskumar/python-dotenv/issues/469
[#456]: https://github.com/theskumar/python-dotenv/issues/456
[#466]: https://github.com/theskumar/python-dotenv/issues/466
[#454]: https://github.com/theskumar/python-dotenv/issues/454
[#474]: https://github.com/theskumar/python-dotenv/issues/474
[#523]: https://github.com/theskumar/python-dotenv/issues/523
[#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
[#586]: https://github.com/theskumar/python-dotenv/issues/586
[#590]: https://github.com/theskumar/python-dotenv/issues/590
[#607]: https://github.com/theskumar/python-dotenv/issues/607
[#588]: https://github.com/theskumar/python-dotenv/issues/588
[#579]: https://github.com/theskumar/python-dotenv/pull/579
[#566]: https://github.com/theskumar/python-dotenv/pull/566
[#563]: https://github.com/theskumar/python-dotenv/pull/563
[#497]: https://github.com/theskumar/python-dotenv/pull/497
[#161]: https://github.com/theskumar/python-dotenv/issues/161
[790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311
<!-- contributors -->
[@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
[@altendky]: https://github.com/altendky
[@andrewsmith]: https://github.com/andrewsmith
[@asyncee]: https://github.com/asyncee
[@bbc2]: https://github.com/bbc2
[@befeleme]: https://github.com/befeleme
[@cjauvin]: https://github.com/cjauvin
[@eaf]: https://github.com/eaf
[@earlbread]: https://github.com/earlbread
[@eekstunt]: https://github.com/eekstunt
[@eggplants]: https://github.com/eggplants
[@ekohl]: https://github.com/ekohl
[@elbehery95]: https://github.com/elbehery95
[@eumiro]: https://github.com/eumiro
[@freddyaboulton]: https://github.com/freddyaboulton
[@gergelyk]: https://github.com/gergelyk
[@gongqingkui]: https://github.com/gongqingkui
[@greyli]: https://github.com/greyli
[@harveer07]: https://github.com/harveer07
[@jadutter]: https://github.com/jadutter
[@jankislinger]: https://github.com/jankislinger
[@jctanner]: https://github.com/jctanner
[@larsks]: https://github.com/larsks
[@lsmith77]: https://github.com/lsmith77
[@matthewfranglen]: https://github.com/matthewfranglen
[@mgorny]: https://github.com/mgorny
[@naorlivne]: https://github.com/naorlivne
[@qnighy]: https://github.com/qnighy
[@rabinadk1]: https://github.com/rabinadk1
[@randomseed42]: https://github.com/randomseed42
[@sammck]: https://github.com/sammck
[@samwyma]: https://github.com/samwyma
[@sidharth-sudhir]: https://github.com/sidharth-sudhir
[@snobu]: https://github.com/snobu
[@techalchemy]: https://github.com/techalchemy
[@theGOTOguy]: https://github.com/theGOTOguy
[@theskumar]: https://github.com/theskumar
[@ulyssessouza]: https://github.com/ulyssessouza
[@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
[@JYOuyang]: https://github.com/JYOuyang
[@burnout-projects]: https://github.com/burnout-projects
[@cpackham-atlnz]: https://github.com/cpackham-atlnz
[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.2...HEAD
[1.2.2]: https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...v1.2.1
[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
[1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0
[0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1
[0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0
[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2
[0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1
[0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/theskumar/python-dotenv/compare/v0.17.1...v0.18.0
[0.17.1]: https://github.com/theskumar/python-dotenv/compare/v0.17.0...v0.17.1
[0.17.0]: https://github.com/theskumar/python-dotenv/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/theskumar/python-dotenv/compare/v0.15.0...v0.16.0
[0.15.0]: https://github.com/theskumar/python-dotenv/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/theskumar/python-dotenv/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/theskumar/python-dotenv/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/theskumar/python-dotenv/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/theskumar/python-dotenv/compare/v0.10.5...v0.11.0
[0.10.5]: https://github.com/theskumar/python-dotenv/compare/v0.10.4...v0.10.5
[0.10.4]: https://github.com/theskumar/python-dotenv/compare/v0.10.3...v0.10.4
================================================
FILE: CONTRIBUTING.md
================================================
Contributing
============
All the contributions are welcome! Please open [an
issue](https://github.com/theskumar/python-dotenv/issues/new) or send us
a pull request.
Executing the tests:
$ 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
$ 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.
================================================
FILE: LICENSE
================================================
Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv)
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of django-dotenv nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: MANIFEST.in
================================================
include LICENSE *.md *.yml *.yaml *.toml
include tox.ini
recursive-include docs *.md
recursive-include tests *.py
include .bumpversion.cfg
include .coveragerc
include .editorconfig
include Makefile
include requirements.txt
include requirements-docs.txt
include src/dotenv/py.typed
================================================
FILE: Makefile
================================================
.PHONY: clean-pyc clean-build test fmt
clean: clean-build clean-pyc
clean-build:
rm -fr build/
rm -rf .mypy_cache/
rm -rf .tox/
rm -rf site/
rm -fr dist/
rm -fr src/*.egg-info
clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
sdist: clean
python -m build -o dist .
ls -l dist
test:
uv pip install -e .
ruff check .
pytest tests/
fmt:
ruff format src tests
coverage:
coverage run --source=dotenv --omit='*tests*' -m py.test tests/ -v --tb=native
coverage report
coverage-html: coverage
coverage html
================================================
FILE: README.md
================================================
# python-dotenv
[![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 variables. It helps in the development of applications following the
[12-factor](https://12factor.net/) principles.
- [Getting Started](#getting-started)
- [Other Use Cases](#other-use-cases)
- [Load configuration without altering the environment](#load-configuration-without-altering-the-environment)
- [Parse configuration as a stream](#parse-configuration-as-a-stream)
- [Load .env files in IPython](#load-env-files-in-ipython)
- [Command-line Interface](#command-line-interface)
- [File format](#file-format)
- [Multiline values](#multiline-values)
- [Variable expansion](#variable-expansion)
- [Related Projects](#related-projects)
- [Acknowledgements](#acknowledgements)
## Getting Started
```shell
pip install python-dotenv
```
If your application takes its configuration from environment variables, like a
12-factor application, launching it in development is not very practical because
you have to set those environment variables yourself.
To help you with that, you can add python-dotenv to your application to make it
load the configuration from a `.env` file when it is present (e.g. in
development) while remaining configurable via the environment:
```python
from dotenv import load_dotenv
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()` 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** existing environment variables (`override=False`). Pass `override=True` to override existing variables.
To configure the development environment, add a `.env` in the root directory of
your project:
```
.
├── .env
└── foo.py
```
The syntax of `.env` files supported by python-dotenv is similar to that of
Bash:
```bash
# Development settings
DOMAIN=example.org
ADMIN_EMAIL=admin@${DOMAIN}
ROOT_URL=${DOMAIN}/app
```
If you use variables in values, ensure they are surrounded with `{` and `}`,
like `${DOMAIN}`, as bare variables such as `$DOMAIN` are not expanded.
You will probably want to add `.env` to your `.gitignore`, especially if it
contains secrets like a password.
See the section "[File format](#file-format)" below for more information about what you can write in a `.env` file.
## Other Use Cases
### Load configuration without altering the environment
The function `dotenv_values` works more or less the same way as `load_dotenv`,
except it doesn't touch the environment, it just returns a `dict` with the
values parsed from the `.env` file.
```python
from dotenv import dotenv_values
config = dotenv_values(".env") # config = {"USER": "foo", "EMAIL": "foo@example.org"}
```
This notably enables advanced configuration management:
```python
import os
from dotenv import dotenv_values
config = {
**dotenv_values(".env.shared"), # load shared development variables
**dotenv_values(".env.secret"), # load sensitive variables
**os.environ, # override loaded values with environment variables
}
```
### Parse configuration as a stream
`load_dotenv` and `dotenv_values` accept [streams][python_streams] via their
`stream` argument. It is thus possible to load the variables from sources other
than the filesystem (e.g. the network).
```python
from io import StringIO
from dotenv import load_dotenv
config = StringIO("USER=foo\nEMAIL=foo@example.org")
load_dotenv(stream=config)
```
### Load .env files in IPython
You can use dotenv in IPython. By default, it will use `find_dotenv` to search for a
`.env` file:
```python
%load_ext dotenv
%dotenv
```
You can also specify a path:
```python
%dotenv relative/or/absolute/path/to/.env
```
Optional flags:
- `-o` to override existing variables.
- `-v` for increased verbosity.
### 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
A CLI interface `dotenv` is also included, which helps you manipulate the `.env`
file without manually opening it.
```shell
$ pip install "python-dotenv[cli]"
$ dotenv set USER foo
$ dotenv set EMAIL foo@example.org
$ dotenv list
USER=foo
EMAIL=foo@example.org
$ dotenv list --format=json
{
"USER": "foo",
"EMAIL": "foo@example.org"
}
$ dotenv run -- python foo.py
```
Run `dotenv --help` for more information about the options and subcommands.
## File format
The format is not formally specified and still improves over time. That being
said, `.env` files should mostly look like Bash files. Reading from FIFOs (named
pipes) on Unix systems is also supported.
Keys can be unquoted or single-quoted. Values can be unquoted, single- or
double-quoted. Spaces before and after keys, equal signs, and values are
ignored. Values can be followed by a comment. Lines can start with the `export`
directive, which does not affect their interpretation.
Allowed escape sequences:
- in single-quoted values: `\\`, `\'`
- in double-quoted values: `\\`, `\'`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v`
### Multiline values
It is possible for single- or double-quoted values to span multiple lines. The
following examples are equivalent:
```bash
FOO="first line
second line"
```
```bash
FOO="first line\nsecond line"
```
### Variable without a value
A variable can have no value:
```bash
FOO
```
It results in `dotenv_values` associating that variable name with the value
`None` (e.g. `{"FOO": None}`. `load_dotenv`, on the other hand, simply ignores
such variables.
This shouldn't be confused with `FOO=`, in which case the variable is associated
with the empty string.
### Variable expansion
python-dotenv can interpolate variables using POSIX variable expansion.
With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable
is the first of the values defined in the following list:
- Value of that variable in the `.env` file.
- Value of that variable in the environment.
- Default value, if provided.
- Empty string.
With `load_dotenv(override=False)`, the value of a variable is the first of the
values defined in the following list:
- Value of that variable in the environment.
- Value of that variable in the `.env` file.
- Default value, if provided.
- Empty string.
## Related Projects
- [environs](https://github.com/sloria/environs)
- [Honcho](https://github.com/nickstenning/honcho)
- [dump-env](https://github.com/sobolevn/dump-env)
- [dynaconf](https://github.com/dynaconf/dynaconf)
- [parse_it](https://github.com/naorlivne/parse_it)
- [django-dotenv](https://github.com/jpadilla/django-dotenv)
- [django-environ](https://github.com/joke2k/django-environ)
- [python-decouple](https://github.com/HBNetwork/python-decouple)
- [django-configuration](https://github.com/jezdez/django-configurations)
## Acknowledgements
This project is currently maintained by [Saurabh Kumar][saurabh-homepage] and
[Bertrand Bonnefoy-Claudet][gh-bbc2] and would not have been possible without
the support of these [awesome people][contributors].
[gh-bbc2]: https://github.com/bbc2
[saurabh-homepage]: https://saurabh-kumar.com
[pypi_link]: https://badge.fury.io/py/python-dotenv
[pypi_badge]: https://badge.fury.io/py/python-dotenv.svg
[python_streams]: https://docs.python.org/3/library/io.html
[contributors]: https://github.com/theskumar/python-dotenv/graphs/contributors
[build_status_link]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml
[build_status_badge]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml/badge.svg
================================================
FILE: docs/reference.md
================================================
# ::: dotenv
================================================
FILE: mkdocs.yml
================================================
site_name: python-dotenv
repo_url: https://github.com/theskumar/python-dotenv
edit_uri: ""
theme:
name: material
palette:
primary: green
features:
- toc.follow
- navigation.sections
markdown_extensions:
- mdx_truly_sane_lists
plugins:
- mkdocstrings:
handlers:
python:
options:
separate_signature: true
show_root_heading: true
show_symbol_type_heading: true
show_symbol_type_toc: true
- search
nav:
- Home: index.md
- Changelog: changelog.md
- Contributing: contributing.md
- Reference: reference.md
- License: license.md
================================================
FILE: pyproject.toml
================================================
[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 = { text = "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.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"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.10"
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"}
[tool.pytest.ini_options]
testpaths = [
"tests",
]
[tool.coverage.run]
relative_files = true
source = ["dotenv"]
[tool.coverage.paths]
source = [
"src/dotenv",
".tox/*/lib/python*/site-packages/dotenv",
".tox/pypy*/site-packages/dotenv",
]
[tool.coverage.report]
show_missing = true
omit = ["*/tests/*"]
exclude_lines = [
"if IS_TYPE_CHECKING:",
"pragma: no cover",
]
[tool.mypy]
check_untyped_defs = true
ignore_missing_imports = true
================================================
FILE: requirements-docs.txt
================================================
mdx_truly_sane_lists>=1.3
mkdocs-include-markdown-plugin>=6.0.0
mkdocs-material>=9.5.0
mkdocstrings[python]>=0.24.0
mkdocs>=1.5.0
================================================
FILE: requirements.txt
================================================
bumpversion
click
ipython
pytest-cov
pytest>=3.9
tox
wheel
ruff
build
pre-commit
================================================
FILE: ruff.toml
================================================
[lint]
select = [
# pycodestyle
"E4",
"E7",
"E9",
# Pyflakes
"F",
# flake8-bugbear
"B",
# iSort
"I",
# flake8-builtins
"A",
]
================================================
FILE: src/dotenv/__init__.py
================================================
from typing import Any, Optional
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)
def get_cli_string(
path: Optional[str] = None,
action: Optional[str] = None,
key: Optional[str] = None,
value: Optional[str] = None,
quote: Optional[str] = None,
):
"""Returns a string suitable for running as a shell script.
Useful for converting a arguments passed to a fabric task
to be passed to a `local` or `run` command.
"""
command = ["dotenv"]
if quote:
command.append(f"-q {quote}")
if path:
command.append(f"-f {path}")
if action:
command.append(action)
if key:
command.append(key)
if value:
if " " in value:
command.append(f'"{value}"')
else:
command.append(value)
return " ".join(command).strip()
__all__ = [
"get_cli_string",
"load_dotenv",
"dotenv_values",
"get_key",
"set_key",
"unset_key",
"find_dotenv",
"load_ipython_extension",
]
================================================
FILE: src/dotenv/__main__.py
================================================
"""Entry point for cli, enables execution with `python -m dotenv`"""
from .cli import cli
if __name__ == "__main__":
cli()
================================================
FILE: src/dotenv/cli.py
================================================
import json
import os
import shlex
import sys
from contextlib import contextmanager
from typing import IO, Any, Dict, Iterator, List, Optional
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.exit(1)
from .main import dotenv_values, set_key, unset_key
from .version import __version__
def enumerate_env() -> Optional[str]:
"""
Return a path for the ${pwd}/.env file.
If pwd does not exist, return None.
"""
try:
cwd = os.getcwd()
except FileNotFoundError:
return None
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.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}
@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)
sys.exit(2)
@cli.command(name="list")
@click.pass_context
@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"]
with stream_file(file) as stream:
values = dotenv_values(stream=stream)
if output_format == "json":
click.echo(json.dumps(values, indent=2, sort_keys=True))
else:
prefix = "export " if output_format == "export" else ""
for k in sorted(values):
v = values[k]
if v is not None:
if output_format in ("export", "shell"):
v = shlex.quote(v)
click.echo(f"{prefix}{k}={v}")
@cli.command(name="set")
@click.pass_context
@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.
This doesn't follow symlinks, to avoid accidentally modifying a file at a
potentially untrusted path.
"""
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}")
else:
sys.exit(1)
@cli.command()
@click.pass_context
@click.argument("key", required=True)
def get(ctx: click.Context, key: Any) -> None:
"""Retrieve the value for the given key."""
file = ctx.obj["FILE"]
with stream_file(file) as stream:
values = dotenv_values(stream=stream)
stored_value = values.get(key)
if stored_value:
click.echo(stored_value)
else:
sys.exit(1)
@cli.command()
@click.pass_context
@click.argument("key", required=True)
def unset(ctx: click.Context, key: Any) -> None:
"""
Removes the given key.
This doesn't follow symlinks, to avoid accidentally modifying a file at a
potentially untrusted path.
"""
file = ctx.obj["FILE"]
quote = ctx.obj["QUOTE"]
success, key = unset_key(file, key, quote)
if success:
click.echo(f"Successfully removed {key}")
else:
sys.exit(1)
@cli.command(
context_settings={
"allow_extra_args": True,
"allow_interspersed_args": False,
"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)
def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None:
"""Run command with environment variables present."""
file = ctx.obj["FILE"]
if not os.path.isfile(file):
raise click.BadParameter(
f"Invalid value for '-f' \"{file}\" does not exist.", 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.")
sys.exit(1)
run_command([*commandline, *ctx.args], dotenv_as_dict)
def run_command(command: List[str], env: Dict[str, str]) -> None:
"""Replace the current process with the specified command.
Replaces the current process with the specified command and the variables from `env`
added in the current environment variables.
Parameters
----------
command: List[str]
The command and it's parameters
env: Dict
The additional environment variables
Returns
-------
None
This function does not return any value. It replaces the current process with the new one.
"""
# copy the current environment variables and add the vales from
# `env`
cmd_env = os.environ.copy()
cmd_env.update(env)
if sys.platform == "win32":
# execvpe on Windows returns control immediately
# rather than once the command has finished.
try:
p = Popen(
command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env
)
except FileNotFoundError:
print(f"Command not found: {command[0]}", file=sys.stderr)
sys.exit(1)
_, _ = p.communicate()
sys.exit(p.returncode)
else:
try:
os.execvpe(command[0], args=command, env=cmd_env)
except FileNotFoundError:
print(f"Command not found: {command[0]}", file=sys.stderr)
sys.exit(1)
================================================
FILE: src/dotenv/ipython.py
================================================
from IPython.core.magic import Magics, line_magic, magics_class # type: ignore
from IPython.core.magic_arguments import (
argument,
magic_arguments,
parse_argstring,
) # type: ignore
from .main import find_dotenv, load_dotenv
@magics_class
class IPythonDotEnv(Magics):
@magic_arguments()
@argument(
"-o",
"--override",
action="store_true",
help="Indicate to override existing variables",
)
@argument(
"-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`",
)
@line_magic
def dotenv(self, line):
args = parse_argstring(self.dotenv, line)
# Locate the .env file
dotenv_path = args.dotenv_path
try:
dotenv_path = find_dotenv(dotenv_path, True, True)
except IOError:
print("cannot find .env file")
return
# Load the .env file
load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)
def load_ipython_extension(ipython):
"""Register the %dotenv magic."""
ipython.register_magics(IPythonDotEnv)
================================================
FILE: src/dotenv/main.py
================================================
import io
import logging
import os
import pathlib
import stat
import sys
import tempfile
from collections import OrderedDict
from contextlib import contextmanager
from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union
from .parser import Binding, parse_stream
from .variables import parse_variables
# A type alias for a string path to be used for the paths in this file.
# These paths may flow to `open()` and `os.replace()`.
StrPath = Union[str, "os.PathLike[str]"]
logger = logging.getLogger(__name__)
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]:
for mapping in mappings:
if mapping.error:
logger.warning(
"python-dotenv could not parse statement starting at line %s",
mapping.original.line,
)
yield mapping
class DotEnv:
def __init__(
self,
dotenv_path: Optional[StrPath],
stream: Optional[IO[str]] = None,
verbose: bool = False,
encoding: Optional[str] = None,
interpolate: bool = True,
override: bool = True,
) -> None:
self.dotenv_path: Optional[StrPath] = dotenv_path
self.stream: Optional[IO[str]] = stream
self._dict: Optional[Dict[str, Optional[str]]] = None
self.verbose: bool = verbose
self.encoding: Optional[str] = encoding
self.interpolate: bool = interpolate
self.override: bool = override
@contextmanager
def _get_stream(self) -> Iterator[IO[str]]:
if self.dotenv_path and _is_file_or_fifo(self.dotenv_path):
with open(self.dotenv_path, encoding=self.encoding) as stream:
yield stream
elif self.stream is not None:
yield self.stream
else:
if self.verbose:
logger.info(
"python-dotenv could not find configuration file %s.",
self.dotenv_path or ".env",
)
yield io.StringIO("")
def dict(self) -> Dict[str, Optional[str]]:
"""Return dotenv as dict"""
if self._dict:
return self._dict
raw_values = self.parse()
if self.interpolate:
self._dict = OrderedDict(
resolve_variables(raw_values, override=self.override)
)
else:
self._dict = OrderedDict(raw_values)
return self._dict
def parse(self) -> Iterator[Tuple[str, Optional[str]]]:
with self._get_stream() as stream:
for mapping in with_warn_for_invalid_lines(parse_stream(stream)):
if mapping.key is not None:
yield mapping.key, mapping.value
def set_as_environment_variables(self) -> bool:
"""
Load the current dotenv as system environment variable.
"""
if not self.dict():
return False
for k, v in self.dict().items():
if k in os.environ and not self.override:
continue
if v is not None:
os.environ[k] = v
return True
def get(self, key: str) -> Optional[str]:
""" """
data = self.dict()
if key in data:
return data[key]
if self.verbose:
logger.warning("Key %s not found in %s.", key, self.dotenv_path)
return None
def get_key(
dotenv_path: StrPath,
key_to_get: str,
encoding: Optional[str] = "utf-8",
) -> 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)
@contextmanager
def rewrite(
path: StrPath,
encoding: Optional[str],
follow_symlinks: bool = False,
) -> Iterator[Tuple[IO[str], IO[str]]]:
if follow_symlinks:
path = os.path.realpath(path)
try:
source: IO[str] = open(path, encoding=encoding)
try:
path_stat = os.lstat(path)
original_mode: Optional[int] = (
stat.S_IMODE(path_stat.st_mode)
if stat.S_ISREG(path_stat.st_mode)
else None
)
except BaseException:
source.close()
raise
except FileNotFoundError:
source = io.StringIO("")
original_mode = None
with tempfile.NamedTemporaryFile(
mode="w",
encoding=encoding,
delete=False,
prefix=".tmp_",
dir=os.path.dirname(os.path.abspath(path)),
) as dest:
dest_path = pathlib.Path(dest.name)
error = None
try:
with source:
yield (source, dest)
except BaseException as err:
error = err
if error is None:
try:
if original_mode is not None:
os.chmod(dest_path, original_mode)
os.replace(dest_path, path)
except BaseException:
dest_path.unlink(missing_ok=True)
raise
else:
dest_path.unlink(missing_ok=True)
raise error from None
def set_key(
dotenv_path: StrPath,
key_to_set: str,
value_to_set: str,
quote_mode: str = "always",
export: bool = False,
encoding: Optional[str] = "utf-8",
follow_symlinks: bool = False,
) -> Tuple[Optional[bool], str, str]:
"""
Adds or Updates a key/value to the given .env
The target .env file is created if it doesn't exist.
This function doesn't follow symlinks by default, to avoid accidentally
modifying a file at a potentially untrusted path. If you don't need this
protection and need symlinks to be followed, use `follow_symlinks`.
"""
if quote_mode not in ("always", "auto", "never"):
raise ValueError(f"Unknown quote_mode: {quote_mode}")
quote = quote_mode == "always" or (
quote_mode == "auto" and not value_to_set.isalnum()
)
if quote:
value_out = "'{}'".format(value_to_set.replace("'", "\\'"))
else:
value_out = value_to_set
if export:
line_out = f"export {key_to_set}={value_out}\n"
else:
line_out = f"{key_to_set}={value_out}\n"
with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as (
source,
dest,
):
replaced = False
missing_newline = False
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
if mapping.key == key_to_set:
dest.write(line_out)
replaced = True
else:
dest.write(mapping.original.string)
missing_newline = not mapping.original.string.endswith("\n")
if not replaced:
if missing_newline:
dest.write("\n")
dest.write(line_out)
return True, key_to_set, value_to_set
def unset_key(
dotenv_path: StrPath,
key_to_unset: str,
quote_mode: str = "always",
encoding: Optional[str] = "utf-8",
follow_symlinks: bool = False,
) -> Tuple[Optional[bool], str]:
"""
Removes a given key from the given `.env` file.
If the .env path given doesn't exist, fails.
If the given key doesn't exist in the .env, fails.
This function doesn't follow symlinks by default, to avoid accidentally
modifying a file at a potentially untrusted path. If you don't need this
protection and need symlinks to be followed, use `follow_symlinks`.
"""
if not os.path.exists(dotenv_path):
logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path)
return None, key_to_unset
removed = False
with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as (
source,
dest,
):
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
if mapping.key == key_to_unset:
removed = True
else:
dest.write(mapping.original.string)
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
return removed, key_to_unset
def resolve_variables(
values: Iterable[Tuple[str, Optional[str]]],
override: bool,
) -> Mapping[str, Optional[str]]:
new_values: Dict[str, Optional[str]] = {}
for name, value in values:
if value is None:
result = None
else:
atoms = parse_variables(value)
env: Dict[str, Optional[str]] = {}
if override:
env.update(os.environ) # type: ignore
env.update(new_values)
else:
env.update(new_values)
env.update(os.environ) # type: ignore
result = "".join(atom.resolve(env) for atom in atoms)
new_values[name] = result
return new_values
def _walk_to_root(path: str) -> Iterator[str]:
"""
Yield directories starting from the given directory up to the root
"""
if not os.path.exists(path):
raise IOError("Starting path not found")
if os.path.isfile(path):
path = os.path.dirname(path)
last_dir = None
current_dir = os.path.abspath(path)
while last_dir != current_dir:
yield current_dir
parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))
last_dir, current_dir = current_dir, parent_dir
def find_dotenv(
filename: str = ".env",
raise_error_if_not_found: bool = False,
usecwd: bool = False,
) -> str:
"""
Search in increasingly higher folders for the given file
Returns path to the file if found, or an empty string otherwise
"""
def _is_interactive():
"""Decide whether this is running in a REPL or IPython notebook"""
if hasattr(sys, "ps1") or hasattr(sys, "ps2"):
return True
try:
main = __import__("__main__", None, None, fromlist=["__file__"])
except ModuleNotFoundError:
return False
return not hasattr(main, "__file__")
def _is_debugger():
return sys.gettrace() is not None
if usecwd or _is_interactive() or _is_debugger() or getattr(sys, "frozen", False):
# Should work without __file__, e.g. in REPL or IPython notebook.
path = os.getcwd()
else:
# will work for .py files
frame = sys._getframe()
current_file = __file__
while frame.f_code.co_filename == current_file or not os.path.exists(
frame.f_code.co_filename
):
assert frame.f_back is not None
frame = frame.f_back
frame_filename = frame.f_code.co_filename
path = os.path.dirname(os.path.abspath(frame_filename))
for dirname in _walk_to_root(path):
check_path = os.path.join(dirname, filename)
if _is_file_or_fifo(check_path):
return check_path
if raise_error_if_not_found:
raise IOError("File not found")
return ""
def load_dotenv(
dotenv_path: Optional[StrPath] = None,
stream: Optional[IO[str]] = None,
verbose: bool = False,
override: bool = False,
interpolate: bool = True,
encoding: Optional[str] = "utf-8",
) -> bool:
"""Parse a .env file and then load all the variables found as environment variables.
Parameters:
dotenv_path: Absolute or relative path to .env file.
stream: Text stream (such as `io.StringIO`) with .env content, used if
`dotenv_path` is `None`.
verbose: Whether to output a warning the .env file is missing.
override: Whether to override the system environment variables with the variables
from the `.env` file.
interpolate: Whether to interpolate variables using POSIX variable expansion.
encoding: Encoding to be used to read the file.
Returns:
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 with it's default parameters. If you need to change the default parameters
of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result
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:
dotenv_path = find_dotenv()
dotenv = DotEnv(
dotenv_path=dotenv_path,
stream=stream,
verbose=verbose,
interpolate=interpolate,
override=override,
encoding=encoding,
)
return dotenv.set_as_environment_variables()
def dotenv_values(
dotenv_path: Optional[StrPath] = None,
stream: Optional[IO[str]] = None,
verbose: bool = False,
interpolate: bool = True,
encoding: Optional[str] = "utf-8",
) -> Dict[str, Optional[str]]:
"""
Parse a .env file and return its content as a dict.
The returned dict will have `None` values for keys without values in the .env file.
For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in
`{"foo": None}`
Parameters:
dotenv_path: Absolute or relative path to the .env file.
stream: `StringIO` object with .env content, used if `dotenv_path` is `None`.
verbose: Whether to output a warning if the .env file is missing.
interpolate: Whether to interpolate variables using POSIX variable expansion.
encoding: Encoding to be used to read the file.
If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the
.env file.
"""
if dotenv_path is None and stream is None:
dotenv_path = find_dotenv()
return DotEnv(
dotenv_path=dotenv_path,
stream=stream,
verbose=verbose,
interpolate=interpolate,
override=True,
encoding=encoding,
).dict()
def _is_file_or_fifo(path: StrPath) -> bool:
"""
Return True if `path` exists and is either a regular file or a FIFO.
"""
if os.path.isfile(path):
return True
try:
st = os.stat(path)
except (FileNotFoundError, OSError):
return False
return stat.S_ISFIFO(st.st_mode)
================================================
FILE: src/dotenv/parser.py
================================================
import codecs
import re
from typing import (
IO,
Iterator,
Match,
NamedTuple,
Optional,
Pattern,
Sequence,
)
def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:
return re.compile(string, re.UNICODE | extra_flags)
_newline = make_regex(r"(\r\n|\n|\r)")
_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE)
_whitespace = make_regex(r"[^\S\r\n]*")
_export = make_regex(r"(?:export[^\S\r\n]+)?")
_single_quoted_key = make_regex(r"'([^']+)'")
_unquoted_key = make_regex(r"([^=\#\s]+)")
_equal_sign = make_regex(r"(=[^\S\r\n]*)")
_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'")
_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"')
_unquoted_value = make_regex(r"([^\r\n]*)")
_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?")
_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)")
_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?")
_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]")
_single_quote_escapes = make_regex(r"\\[\\']")
class Original(NamedTuple):
string: str
line: int
class Binding(NamedTuple):
key: Optional[str]
value: Optional[str]
original: Original
error: bool
class Position:
def __init__(self, chars: int, line: int) -> None:
self.chars = chars
self.line = line
@classmethod
def start(cls) -> "Position":
return cls(chars=0, line=1)
def set(self, other: "Position") -> None:
self.chars = other.chars
self.line = other.line
def advance(self, string: str) -> None:
self.chars += len(string)
self.line += len(re.findall(_newline, string))
class Error(Exception):
pass
class Reader:
def __init__(self, stream: IO[str]) -> None:
self.string = stream.read()
self.position = Position.start()
self.mark = Position.start()
def has_next(self) -> bool:
return self.position.chars < len(self.string)
def set_mark(self) -> None:
self.mark.set(self.position)
def get_marked(self) -> Original:
return Original(
string=self.string[self.mark.chars : self.position.chars],
line=self.mark.line,
)
def peek(self, count: int) -> str:
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]
if len(result) < count:
raise Error("read: End of string")
self.position.advance(result)
return result
def read_regex(self, regex: Pattern[str]) -> Sequence[str]:
match = regex.match(self.string, self.position.chars)
if match is None:
raise Error("read_regex: Pattern not found")
self.position.advance(self.string[match.start() : match.end()])
return match.groups()
def decode_escapes(regex: Pattern[str], string: str) -> str:
def decode_match(match: Match[str]) -> str:
return codecs.decode(match.group(0), "unicode-escape") # type: ignore
return regex.sub(decode_match, string)
def parse_key(reader: Reader) -> Optional[str]:
char = reader.peek(1)
if char == "#":
return None
elif char == "'":
(key,) = reader.read_regex(_single_quoted_key)
else:
(key,) = reader.read_regex(_unquoted_key)
return key
def parse_unquoted_value(reader: Reader) -> str:
(part,) = reader.read_regex(_unquoted_value)
return re.sub(r"\s+#.*", "", part).rstrip()
def parse_value(reader: Reader) -> str:
char = reader.peek(1)
if char == "'":
(value,) = reader.read_regex(_single_quoted_value)
return decode_escapes(_single_quote_escapes, value)
elif char == '"':
(value,) = reader.read_regex(_double_quoted_value)
return decode_escapes(_double_quote_escapes, value)
elif char in ("", "\n", "\r"):
return ""
else:
return parse_unquoted_value(reader)
def parse_binding(reader: Reader) -> Binding:
reader.set_mark()
try:
reader.read_regex(_multiline_whitespace)
if not reader.has_next():
return Binding(
key=None,
value=None,
original=reader.get_marked(),
error=False,
)
reader.read_regex(_export)
key = parse_key(reader)
reader.read_regex(_whitespace)
if reader.peek(1) == "=":
reader.read_regex(_equal_sign)
value: Optional[str] = parse_value(reader)
else:
value = None
reader.read_regex(_comment)
reader.read_regex(_end_of_line)
return Binding(
key=key,
value=value,
original=reader.get_marked(),
error=False,
)
except Error:
reader.read_regex(_rest_of_line)
return Binding(
key=None,
value=None,
original=reader.get_marked(),
error=True,
)
def parse_stream(stream: IO[str]) -> Iterator[Binding]:
reader = Reader(stream)
while reader.has_next():
yield parse_binding(reader)
================================================
FILE: src/dotenv/py.typed
================================================
# Marker file for PEP 561
================================================
FILE: src/dotenv/variables.py
================================================
import re
from abc import ABCMeta, abstractmethod
from typing import Iterator, Mapping, Optional, Pattern
_posix_variable: Pattern[str] = re.compile(
r"""
\$\{
(?P<name>[^\}:]*)
(?::-
(?P<default>[^\}]*)
)?
\}
""",
re.VERBOSE,
)
class Atom(metaclass=ABCMeta):
def __ne__(self, other: object) -> bool:
result = self.__eq__(other)
if result is NotImplemented:
return NotImplemented
return not result
@abstractmethod
def resolve(self, env: Mapping[str, Optional[str]]) -> str: ...
class Literal(Atom):
def __init__(self, value: str) -> None:
self.value = value
def __repr__(self) -> str:
return f"Literal(value={self.value})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return self.value == other.value
def __hash__(self) -> int:
return hash((self.__class__, self.value))
def resolve(self, env: Mapping[str, Optional[str]]) -> str:
return self.value
class Variable(Atom):
def __init__(self, name: str, default: Optional[str]) -> None:
self.name = name
self.default = default
def __repr__(self) -> str:
return f"Variable(name={self.name}, default={self.default})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, self.__class__):
return NotImplemented
return (self.name, self.default) == (other.name, other.default)
def __hash__(self) -> int:
return hash((self.__class__, self.name, self.default))
def resolve(self, env: Mapping[str, Optional[str]]) -> str:
default = self.default if self.default is not None else ""
result = env.get(self.name, default)
return result if result is not None else ""
def parse_variables(value: str) -> Iterator[Atom]:
cursor = 0
for match in _posix_variable.finditer(value):
(start, end) = match.span()
name = match["name"]
default = match["default"]
if start > cursor:
yield Literal(value=value[cursor:start])
yield Variable(name=name, default=default)
cursor = end
length = len(value)
if cursor < length:
yield Literal(value=value[cursor:length])
================================================
FILE: src/dotenv/version.py
================================================
__version__ = "1.2.2"
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/conftest.py
================================================
import pytest
from click.testing import CliRunner
@pytest.fixture
def cli():
runner = CliRunner()
with runner.isolated_filesystem():
yield runner
@pytest.fixture
def dotenv_path(tmp_path):
path = tmp_path / ".env"
path.write_bytes(b"")
yield path
================================================
FILE: tests/test_cli.py
================================================
import os
from pathlib import Path
from typing import Optional
import pytest
import dotenv
from dotenv.cli import cli as dotenv_cli
from dotenv.version import __version__
from tests.test_lib import check_process, run_dotenv
@pytest.mark.parametrize(
"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"""),
("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"""),
),
)
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"]
if format is not None:
args.extend(["--format", output_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"])
assert result.exit_code == 2, 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):
result = cli.invoke(dotenv.cli.list_values, [])
assert (result.exit_code, result.output) == (1, "")
def test_get_existing_value(cli, dotenv_path):
dotenv_path.write_text("a=b")
result = cli.invoke(dotenv_cli, ["--file", dotenv_path, "get", "a"])
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"])
assert (result.exit_code, result.output) == (1, "")
def test_get_non_existent_file(cli):
result = cli.invoke(dotenv_cli, ["--file", "nx_file", "get", "a"])
assert result.exit_code == 2
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_path):
dotenv_path.write_text("a=b")
result = cli.invoke(dotenv_cli, ["--file", dotenv_path, "unset", "a"])
assert (result.exit_code, result.output) == (0, "Successfully removed a\n")
assert dotenv_path.read_text() == ""
def test_unset_non_existent_value(cli, dotenv_path):
result = cli.invoke(dotenv_cli, ["--file", dotenv_path, "unset", "a"])
assert (result.exit_code, result.output) == (1, "")
assert dotenv_path.read_text() == ""
@pytest.mark.parametrize(
"quote_mode,variable,value,expected",
(
("always", "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"),
),
)
def test_set_quote_options(cli, dotenv_path, quote_mode, variable, value, expected):
result = cli.invoke(
dotenv_cli,
[
"--file",
dotenv_path,
"--export",
"false",
"--quote",
quote_mode,
"set",
variable,
value,
],
)
assert (result.exit_code, result.output) == (0, "{}={}\n".format(variable, value))
assert dotenv_path.read_text() == expected
@pytest.mark.parametrize(
"dotenv_path,export_mode,variable,value,expected",
(
(Path(".nx_file"), "true", "a", "x", "export a='x'\n"),
(Path(".nx_file"), "false", "a", "x", "a='x'\n"),
),
)
def test_set_export(cli, dotenv_path, export_mode, variable, value, expected):
result = cli.invoke(
dotenv_cli,
[
"--file",
dotenv_path,
"--quote",
"always",
"--export",
export_mode,
"set",
variable,
value,
],
)
assert (result.exit_code, result.output) == (0, "{}={}\n".format(variable, value))
assert dotenv_path.read_text() == expected
def test_set_non_existent_file(cli):
result = cli.invoke(dotenv.cli.set_value, ["a", "b"])
assert (result.exit_code, result.output) == (1, "")
def test_set_no_file(cli):
result = cli.invoke(dotenv_cli, ["--file", "nx_file", "set"])
assert result.exit_code == 2
assert "Missing argument" in result.output
def test_get_default_path(tmp_path):
(tmp_path / ".env").write_text("A=x")
result = run_dotenv(["get", "A"], cwd=tmp_path)
check_process(result, exit_code=0, stdout="x\n")
def test_run(tmp_path):
(tmp_path / ".env").write_text("A=x")
result = run_dotenv(["run", "printenv", "A"], cwd=tmp_path)
check_process(result, exit_code=0, stdout="x\n")
def test_run_with_existing_variable(tmp_path):
(tmp_path / ".env").write_text("A=x")
env = dict(os.environ)
env.update({"LANG": "en_US.UTF-8", "A": "y"})
result = run_dotenv(["run", "printenv", "A"], cwd=tmp_path, env=env)
check_process(result, exit_code=0, stdout="x\n")
def test_run_with_existing_variable_not_overridden(tmp_path):
(tmp_path / ".env").write_text("A=x")
env = dict(os.environ)
env.update({"LANG": "en_US.UTF-8", "A": "C"})
result = run_dotenv(
["run", "--no-override", "printenv", "A"], cwd=tmp_path, env=env
)
check_process(result, exit_code=0, stdout="C\n")
def test_run_with_none_value(tmp_path):
(tmp_path / ".env").write_text("A=x\nc")
result = run_dotenv(["run", "printenv", "A"], cwd=tmp_path)
check_process(result, exit_code=0, stdout="x\n")
def test_run_with_other_env(dotenv_path, tmp_path):
dotenv_path.write_text("A=x")
result = run_dotenv(
["--file", str(dotenv_path), "run", "printenv", "A"],
cwd=tmp_path,
)
check_process(result, exit_code=0, stdout="x\n")
def test_run_without_cmd(tmp_path):
result = run_dotenv(["run"], cwd=tmp_path)
check_process(result, exit_code=2)
assert "Invalid value for '-f'" in result.stderr
def test_run_with_invalid_cmd(dotenv_path, tmp_path):
result = run_dotenv(
["--file", str(dotenv_path), "run", "i_do_not_exist"],
cwd=tmp_path,
)
check_process(result, exit_code=1)
assert "Command not found: i_do_not_exist" in result.stderr
def test_run_with_env_missing_and_invalid_cmd(tmp_path):
"""
Check that an .env file missing takes precedence over a command not found error.
"""
result = run_dotenv(["run", "i_do_not_exist"], cwd=tmp_path)
check_process(result, exit_code=2)
assert "Invalid value for '-f'" in result.stderr
def test_run_with_version(tmp_path):
result = run_dotenv(["--version"], cwd=tmp_path)
check_process(result, exit_code=0)
assert result.stdout.strip().endswith(__version__)
def test_run_with_command_flags(dotenv_path, tmp_path):
"""
Check that command flags passed after `dotenv run` are not interpreted.
Here, we want to run `printenv --version`, not `dotenv --version`.
"""
result = run_dotenv(
["--file", str(dotenv_path), "run", "printenv", "--version"],
cwd=tmp_path,
)
check_process(result, exit_code=0)
assert result.stdout.strip().startswith("printenv ")
def test_run_with_dotenv_and_command_flags(dotenv_path, tmp_path):
"""
Check that dotenv flags supersede command flags.
"""
result = run_dotenv(
["--version", "--file", str(dotenv_path), "run", "printenv", "--version"],
cwd=tmp_path,
)
check_process(result, exit_code=0)
assert result.stdout.strip().startswith("dotenv, version")
================================================
FILE: tests/test_fifo_dotenv.py
================================================
import os
import pathlib
import sys
import threading
import pytest
from dotenv import load_dotenv
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="FIFOs are Unix-only")
def test_load_dotenv_from_fifo(tmp_path: pathlib.Path, monkeypatch):
fifo = tmp_path / ".env"
os.mkfifo(fifo) # create named pipe
def writer():
with open(fifo, "w", encoding="utf-8") as w:
w.write("MY_PASSWORD=pipe-secret\n")
t = threading.Thread(target=writer)
t.start()
# Ensure env is clean
monkeypatch.delenv("MY_PASSWORD", raising=False)
ok = load_dotenv(dotenv_path=str(fifo), override=True)
t.join(timeout=2)
assert ok is True
assert os.getenv("MY_PASSWORD") == "pipe-secret"
================================================
FILE: tests/test_ipython.py
================================================
import os
import sys
from unittest import mock
import pytest
pytest.importorskip("IPython")
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_ipython_existing_variable_no_override(tmp_path):
from IPython.terminal.embed import InteractiveShellEmbed
dotenv_file = tmp_path / ".env"
dotenv_file.write_text("a=b\n")
os.chdir(tmp_path)
os.environ["a"] = "c"
ipshell = InteractiveShellEmbed()
ipshell.run_line_magic("load_ext", "dotenv")
ipshell.run_line_magic("dotenv", "")
assert os.environ == {"a": "c"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_ipython_existing_variable_override(tmp_path):
from IPython.terminal.embed import InteractiveShellEmbed
dotenv_file = tmp_path / ".env"
dotenv_file.write_text("a=b\n")
os.chdir(tmp_path)
os.environ["a"] = "c"
ipshell = InteractiveShellEmbed()
ipshell.run_line_magic("load_ext", "dotenv")
ipshell.run_line_magic("dotenv", "-o")
assert os.environ == {"a": "b"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_ipython_new_variable(tmp_path):
from IPython.terminal.embed import InteractiveShellEmbed
dotenv_file = tmp_path / ".env"
dotenv_file.write_text("a=b\n")
os.chdir(tmp_path)
ipshell = InteractiveShellEmbed()
ipshell.run_line_magic("load_ext", "dotenv")
ipshell.run_line_magic("dotenv", "")
assert os.environ == {"a": "b"}
================================================
FILE: tests/test_is_interactive.py
================================================
import builtins
import sys
from unittest import mock
from dotenv.main import find_dotenv
class TestIsInteractive:
"""Tests for the _is_interactive helper function within find_dotenv.
The _is_interactive function is used by find_dotenv to determine if the code
is running in an interactive environment (like a REPL, IPython notebook, etc.)
versus a normal script execution.
Interactive environments include:
- Python REPL (has sys.ps1 or sys.ps2)
- IPython notebooks (no __file__ in __main__)
- Interactive shells
Non-interactive environments include:
- Normal script execution (has __file__ in __main__)
- Module imports
Examples of the behavior:
>>> import sys
>>> # In a REPL:
>>> hasattr(sys, 'ps1') # True
>>> # In a script:
>>> hasattr(sys, 'ps1') # False
"""
def _create_dotenv_file(self, tmp_path):
"""Helper to create a test .env file."""
dotenv_path = tmp_path / ".env"
dotenv_path.write_text("TEST=value")
return dotenv_path
def _setup_subdir_and_chdir(self, tmp_path, monkeypatch):
"""Helper to create subdirectory and change to it."""
test_dir = tmp_path / "subdir"
test_dir.mkdir()
monkeypatch.chdir(test_dir)
return test_dir
def _remove_ps_attributes(self, monkeypatch):
"""Helper to remove ps1/ps2 attributes if they exist."""
if hasattr(sys, "ps1"):
monkeypatch.delattr(sys, "ps1")
if hasattr(sys, "ps2"):
monkeypatch.delattr(sys, "ps2")
def _mock_main_import(self, monkeypatch, mock_main_module):
"""Helper to mock __main__ module import."""
original_import = builtins.__import__
def mock_import(name, *args, **kwargs):
if name == "__main__":
return mock_main_module
return original_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", mock_import)
def _mock_main_import_error(self, monkeypatch):
"""Helper to mock __main__ module import that raises ModuleNotFoundError."""
original_import = builtins.__import__
def mock_import(name, *args, **kwargs):
if name == "__main__":
raise ModuleNotFoundError("No module named '__main__'")
return original_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", mock_import)
def test_is_interactive_with_ps1(self, tmp_path, monkeypatch):
"""Test that _is_interactive returns True when sys.ps1 exists."""
dotenv_path = self._create_dotenv_file(tmp_path)
# Mock sys.ps1 to simulate interactive shell
monkeypatch.setattr(sys, "ps1", ">>> ", raising=False)
self._setup_subdir_and_chdir(tmp_path, monkeypatch)
# When _is_interactive() returns True, find_dotenv should search from cwd
result = find_dotenv()
assert result == str(dotenv_path)
def test_is_interactive_with_ps2(self, tmp_path, monkeypatch):
"""Test that _is_interactive returns True when sys.ps2 exists."""
dotenv_path = self._create_dotenv_file(tmp_path)
# Mock sys.ps2 to simulate multi-line interactive input
monkeypatch.setattr(sys, "ps2", "... ", raising=False)
self._setup_subdir_and_chdir(tmp_path, monkeypatch)
# When _is_interactive() returns True, find_dotenv should search from cwd
result = find_dotenv()
assert result == str(dotenv_path)
def test_is_interactive_main_module_not_found(self, tmp_path, monkeypatch):
"""Test that _is_interactive returns False when __main__ module import fails."""
self._remove_ps_attributes(monkeypatch)
self._mock_main_import_error(monkeypatch)
# Change to directory and test
monkeypatch.chdir(tmp_path)
# Since _is_interactive() returns False, find_dotenv should not find anything
# without usecwd=True
result = find_dotenv()
assert result == ""
def test_is_interactive_main_without_file(self, tmp_path, monkeypatch):
"""Test that _is_interactive returns True when __main__ has no __file__ attribute."""
self._remove_ps_attributes(monkeypatch)
dotenv_path = self._create_dotenv_file(tmp_path)
# Mock __main__ module without __file__ attribute
mock_main = mock.MagicMock()
del mock_main.__file__ # Remove __file__ attribute
self._mock_main_import(monkeypatch, mock_main)
self._setup_subdir_and_chdir(tmp_path, monkeypatch)
# When _is_interactive() returns True, find_dotenv should search from cwd
result = find_dotenv()
assert result == str(dotenv_path)
def test_is_interactive_main_with_file(self, tmp_path, monkeypatch):
"""Test that _is_interactive returns False when __main__ has __file__ attribute."""
self._remove_ps_attributes(monkeypatch)
# Mock __main__ module with __file__ attribute
mock_main = mock.MagicMock()
mock_main.__file__ = "/path/to/script.py"
self._mock_main_import(monkeypatch, mock_main)
# Change to directory and test
monkeypatch.chdir(tmp_path)
# Since _is_interactive() returns False, find_dotenv should not find anything
# without usecwd=True
result = find_dotenv()
assert result == ""
def test_is_interactive_precedence_ps1_over_main(self, tmp_path, monkeypatch):
"""Test that ps1/ps2 attributes take precedence over __main__ module check."""
dotenv_path = self._create_dotenv_file(tmp_path)
# Set ps1 attribute
monkeypatch.setattr(sys, "ps1", ">>> ", raising=False)
# Mock __main__ module with __file__ attribute (which would normally return False)
mock_main = mock.MagicMock()
mock_main.__file__ = "/path/to/script.py"
self._mock_main_import(monkeypatch, mock_main)
self._setup_subdir_and_chdir(tmp_path, monkeypatch)
# ps1 should take precedence, so _is_interactive() returns True
result = find_dotenv()
assert result == str(dotenv_path)
def test_is_interactive_ps1_and_ps2_both_exist(self, tmp_path, monkeypatch):
"""Test that _is_interactive returns True when both ps1 and ps2 exist."""
dotenv_path = self._create_dotenv_file(tmp_path)
# Set both ps1 and ps2 attributes
monkeypatch.setattr(sys, "ps1", ">>> ", raising=False)
monkeypatch.setattr(sys, "ps2", "... ", raising=False)
self._setup_subdir_and_chdir(tmp_path, monkeypatch)
# Should return True with either attribute present
result = find_dotenv()
assert result == str(dotenv_path)
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."""
self._remove_ps_attributes(monkeypatch)
# Mock __main__ module with __file__ = None
mock_main = mock.MagicMock()
mock_main.__file__ = None
self._mock_main_import(monkeypatch, mock_main)
# Mock sys.gettrace to ensure debugger detection returns False
monkeypatch.setattr("sys.gettrace", lambda: None)
monkeypatch.chdir(tmp_path)
# __file__ = None should still be considered non-interactive
# and with no debugger, find_dotenv should not search from cwd
result = find_dotenv()
assert result == ""
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."""
self._remove_ps_attributes(monkeypatch)
# Don't mock anything - let it use the real __main__ module
# which should have a __file__ attribute in normal execution
# Change to directory and test
monkeypatch.chdir(tmp_path)
# In normal execution, _is_interactive() should return False
# so find_dotenv should not find anything without usecwd=True
result = find_dotenv()
assert result == ""
def test_is_interactive_with_usecwd_override(self, tmp_path, monkeypatch):
"""Test that usecwd=True overrides _is_interactive behavior."""
self._remove_ps_attributes(monkeypatch)
dotenv_path = self._create_dotenv_file(tmp_path)
# Mock __main__ module with __file__ attribute (non-interactive)
mock_main = mock.MagicMock()
mock_main.__file__ = "/path/to/script.py"
self._mock_main_import(monkeypatch, mock_main)
self._setup_subdir_and_chdir(tmp_path, monkeypatch)
# Even though _is_interactive() returns False, usecwd=True should find the file
result = find_dotenv(usecwd=True)
assert result == str(dotenv_path)
================================================
FILE: tests/test_lib.py
================================================
import subprocess
from pathlib import Path
from typing import Sequence
def run_dotenv(
args: Sequence[str],
cwd: str | Path | None = None,
env: dict | None = None,
) -> subprocess.CompletedProcess:
"""
Run the `dotenv` CLI in a subprocess with the given arguments.
"""
process = subprocess.run(
["dotenv", *args],
capture_output=True,
text=True,
cwd=cwd,
env=env,
)
return process
def check_process(
process: subprocess.CompletedProcess,
exit_code: int,
stdout: str | None = None,
):
"""
Check that the process completed with the expected exit code and output.
This provides better error messages than directly checking the attributes.
"""
assert process.returncode == exit_code, (
f"Unexpected exit code {process.returncode} (expected {exit_code})\n"
f"stdout:\n{process.stdout}\n"
f"stderr:\n{process.stderr}"
)
if stdout is not None:
assert process.stdout == stdout, (
f"Unexpected output: {process.stdout.strip()!r} (expected {stdout!r})"
)
================================================
FILE: tests/test_main.py
================================================
import io
import logging
import os
import stat
import subprocess
import sys
import textwrap
from unittest import mock
import pytest
import dotenv
def test_set_key_no_file(tmp_path):
nx_path = tmp_path / "nx"
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "warning"):
result = dotenv.set_key(nx_path, "foo", "bar")
assert result == (True, "foo", "bar")
assert nx_path.exists()
@pytest.mark.parametrize(
"before,key,value,expected,after",
[
("", "a", "", (True, "a", ""), "a=''\n"),
("", "a", "b", (True, "a", "b"), "a='b'\n"),
("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"),
("", "a", '"b"', (True, "a", '"b"'), "a='\"b\"'\n"),
("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"),
("", "a", 'b"c', (True, "a", 'b"c'), "a='b\"c'\n"),
("a=b", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"),
("a=b\nc=d", "a", "e", (True, "a", "e"), "a='e'\nc=d"),
("a=b\nc=d\ne=f", "c", "g", (True, "c", "g"), "a=b\nc='g'\ne=f"),
("a=b\n", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"),
("a=b", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"),
],
)
def test_set_key(dotenv_path, before, key, value, expected, after):
logger = logging.getLogger("dotenv.main")
dotenv_path.write_text(before)
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.set_key(dotenv_path, key, value)
assert result == expected
assert dotenv_path.read_text() == after
mock_warning.assert_not_called()
def test_set_key_encoding(dotenv_path):
encoding = "latin-1"
result = dotenv.set_key(dotenv_path, "a", "é", encoding=encoding)
assert result == (True, "a", "é")
assert dotenv_path.read_text(encoding=encoding) == "a='é'\n"
@pytest.mark.skipif(
sys.platform == "win32", reason="file mode bits behave differently on Windows"
)
def test_set_key_preserves_file_mode(dotenv_path):
dotenv_path.write_text("a=x\n")
dotenv_path.chmod(0o640)
mode_before = stat.S_IMODE(dotenv_path.stat().st_mode)
dotenv.set_key(dotenv_path, "a", "y")
mode_after = stat.S_IMODE(dotenv_path.stat().st_mode)
assert mode_before == mode_after
def test_rewrite_closes_file_handle_on_lstat_failure(tmp_path):
dotenv_path = tmp_path / ".env"
dotenv_path.write_text("a=x\n")
real_open = open
opened_handles = []
def tracking_open(*args, **kwargs):
handle = real_open(*args, **kwargs)
opened_handles.append(handle)
return handle
with mock.patch("dotenv.main.os.lstat", side_effect=FileNotFoundError):
with mock.patch("dotenv.main.open", side_effect=tracking_open):
dotenv.set_key(dotenv_path, "a", "x")
assert opened_handles, "expected at least one file to be opened"
assert all(handle.closed for handle in opened_handles)
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks require elevated privileges on Windows"
)
def test_set_key_symlink_to_existing_file(tmp_path):
target = tmp_path / "target.env"
target.write_text("a=x\n")
symlink = tmp_path / ".env"
symlink.symlink_to(target)
dotenv.set_key(symlink, "a", "y")
assert target.read_text() == "a=x\n"
assert not symlink.is_symlink()
assert "a='y'" in symlink.read_text()
assert stat.S_IMODE(symlink.stat().st_mode) == 0o600
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks require elevated privileges on Windows"
)
def test_set_key_symlink_to_missing_file(tmp_path):
target = tmp_path / "nx"
symlink = tmp_path / ".env"
symlink.symlink_to(target)
dotenv.set_key(symlink, "a", "x")
assert not target.exists()
assert not symlink.is_symlink()
assert symlink.read_text() == "a='x'\n"
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks require elevated privileges on Windows"
)
def test_set_key_follow_symlinks(tmp_path):
target = tmp_path / "target.env"
target.write_text("a=x\n")
symlink = tmp_path / ".env"
symlink.symlink_to(target)
dotenv.set_key(symlink, "a", "y", follow_symlinks=True)
assert target.read_text() == "a='y'\n"
assert symlink.is_symlink()
@pytest.mark.skipif(
sys.platform != "win32" and os.geteuid() == 0,
reason="Root user can access files even with 000 permissions.",
)
def test_set_key_permission_error(dotenv_path):
if sys.platform == "win32":
# On Windows, make file read-only
dotenv_path.chmod(stat.S_IREAD)
else:
# On Unix, remove all permissions
dotenv_path.chmod(0o000)
with pytest.raises(PermissionError):
dotenv.set_key(dotenv_path, "a", "b")
# Restore permissions
if sys.platform == "win32":
dotenv_path.chmod(stat.S_IWRITE | stat.S_IREAD)
else:
dotenv_path.chmod(0o600)
assert dotenv_path.read_text() == ""
def test_get_key_no_file(tmp_path):
nx_path = 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,
):
result = dotenv.get_key(nx_path, "foo")
assert result is None
mock_info.assert_has_calls(
calls=[
mock.call("python-dotenv could not find configuration file %s.", nx_path)
],
)
mock_warning.assert_has_calls(
calls=[mock.call("Key %s not found in %s.", "foo", nx_path)],
)
def test_get_key_not_found(dotenv_path):
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.get_key(dotenv_path, "foo")
assert result is None
mock_warning.assert_called_once_with("Key %s not found in %s.", "foo", dotenv_path)
def test_get_key_ok(dotenv_path):
logger = logging.getLogger("dotenv.main")
dotenv_path.write_text("foo=bar")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.get_key(dotenv_path, "foo")
assert result == "bar"
mock_warning.assert_not_called()
def test_get_key_encoding(dotenv_path):
encoding = "latin-1"
dotenv_path.write_text("é=è", encoding=encoding)
result = dotenv.get_key(dotenv_path, "é", encoding=encoding)
assert result == "è"
def test_get_key_none(dotenv_path):
logger = logging.getLogger("dotenv.main")
dotenv_path.write_text("foo")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.get_key(dotenv_path, "foo")
assert result is None
mock_warning.assert_not_called()
def test_unset_with_value(dotenv_path):
logger = logging.getLogger("dotenv.main")
dotenv_path.write_text("a=b\nc=d")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.unset_key(dotenv_path, "a")
assert result == (True, "a")
assert dotenv_path.read_text() == "c=d"
mock_warning.assert_not_called()
def test_unset_no_value(dotenv_path):
logger = logging.getLogger("dotenv.main")
dotenv_path.write_text("foo")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.unset_key(dotenv_path, "foo")
assert result == (True, "foo")
assert dotenv_path.read_text() == ""
mock_warning.assert_not_called()
def test_unset_encoding(dotenv_path):
encoding = "latin-1"
dotenv_path.write_text("é=x", encoding=encoding)
result = dotenv.unset_key(dotenv_path, "é", encoding=encoding)
assert result == (True, "é")
assert dotenv_path.read_text(encoding=encoding) == ""
def test_unset_non_existent_file(tmp_path):
nx_path = tmp_path / "nx"
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.unset_key(nx_path, "foo")
assert result == (None, "foo")
mock_warning.assert_called_once_with(
"Can't delete from %s - it doesn't exist.",
nx_path,
)
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks require elevated privileges on Windows"
)
def test_unset_key_symlink_to_existing_file(tmp_path):
target = tmp_path / "target.env"
target.write_text("a=x\n")
symlink = tmp_path / ".env"
symlink.symlink_to(target)
dotenv.unset_key(symlink, "a")
assert target.read_text() == "a=x\n"
assert not symlink.is_symlink()
assert symlink.read_text() == ""
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks require elevated privileges on Windows"
)
def test_unset_key_symlink_to_missing_file(tmp_path):
target = tmp_path / "nx"
symlink = tmp_path / ".env"
symlink.symlink_to(target)
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "warning") as mock_warning:
result = dotenv.unset_key(symlink, "a")
assert result == (None, "a")
assert symlink.is_symlink()
mock_warning.assert_called_once()
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks require elevated privileges on Windows"
)
def test_unset_key_follow_symlinks(tmp_path):
target = tmp_path / "target.env"
target.write_text("a=b\n")
symlink = tmp_path / ".env"
symlink.symlink_to(target)
dotenv.unset_key(symlink, "a", follow_symlinks=True)
assert target.read_text() == ""
assert symlink.is_symlink()
def prepare_file_hierarchy(path):
"""
Create a temporary folder structure like the following:
test_find_dotenv0/
└── child1
├── child2
│ └── child3
│ └── child4
└── .env
Then try to automatically `find_dotenv` starting in `child4`
"""
leaf = path / "child1" / "child2" / "child3" / "child4"
leaf.mkdir(parents=True, exist_ok=True)
return leaf
def test_find_dotenv_no_file_raise(tmp_path):
leaf = prepare_file_hierarchy(tmp_path)
os.chdir(leaf)
with pytest.raises(IOError):
dotenv.find_dotenv(raise_error_if_not_found=True, usecwd=True)
def test_find_dotenv_no_file_no_raise(tmp_path):
leaf = prepare_file_hierarchy(tmp_path)
os.chdir(leaf)
result = dotenv.find_dotenv(usecwd=True)
assert result == ""
def test_find_dotenv_found(tmp_path):
leaf = prepare_file_hierarchy(tmp_path)
os.chdir(leaf)
dotenv_path = tmp_path / ".env"
dotenv_path.write_bytes(b"TEST=test\n")
result = dotenv.find_dotenv(usecwd=True)
assert result == str(dotenv_path)
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_existing_file(dotenv_path):
dotenv_path.write_text("a=b")
result = dotenv.load_dotenv(dotenv_path)
assert result is True
assert os.environ == {"a": "b"}
@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.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@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():
logger = logging.getLogger("dotenv.main")
with mock.patch.object(logger, "info") as mock_info:
result = dotenv.load_dotenv(".does_not_exist", verbose=True)
assert result is False
mock_info.assert_called_once_with(
"python-dotenv could not find configuration file %s.", ".does_not_exist"
)
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_existing_variable_no_override(dotenv_path):
dotenv_path.write_text("a=b")
result = dotenv.load_dotenv(dotenv_path, override=False)
assert result is True
assert os.environ == {"a": "c"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_existing_variable_override(dotenv_path):
dotenv_path.write_text("a=b")
result = dotenv.load_dotenv(dotenv_path, override=True)
assert result is True
assert os.environ == {"a": "b"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path):
dotenv_path.write_text('a=b\nd="${a}"')
result = dotenv.load_dotenv(dotenv_path)
assert result is True
assert os.environ == {"a": "c", "d": "c"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {"a": "c"}, clear=True)
def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path):
dotenv_path.write_text('a=b\nd="${a}"')
result = dotenv.load_dotenv(dotenv_path, override=True)
assert result is True
assert os.environ == {"a": "b", "d": "b"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_string_io_utf_8():
stream = io.StringIO("a=à")
result = dotenv.load_dotenv(stream=stream)
assert result is True
assert os.environ == {"a": "à"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@mock.patch.dict(os.environ, {}, clear=True)
def test_load_dotenv_file_stream(dotenv_path):
dotenv_path.write_text("a=b")
with dotenv_path.open() as f:
result = dotenv.load_dotenv(stream=f)
assert result is True
assert os.environ == {"a": "b"}
def test_load_dotenv_in_current_dir(tmp_path):
dotenv_path = tmp_path / ".env"
dotenv_path.write_bytes(b"a=b")
code_path = tmp_path / "code.py"
code_path.write_text(
textwrap.dedent("""
import dotenv
import os
dotenv.load_dotenv(verbose=True)
print(os.environ['a'])
""")
)
os.chdir(tmp_path)
result = subprocess.run(
[sys.executable, str(code_path)],
capture_output=True,
text=True,
check=True,
)
assert result.stdout == "b\n"
def test_dotenv_values_file(dotenv_path):
dotenv_path.write_text("a=b")
result = dotenv.dotenv_values(dotenv_path)
assert result == {"a": "b"}
@pytest.mark.skipif(
sys.platform == "win32", reason="This test assumes case-sensitive variable names"
)
@pytest.mark.parametrize(
"env,string,interpolate,expected",
[
# Defined in environment, with and without interpolation
({"b": "c"}, "a=$b", False, {"a": "$b"}),
({"b": "c"}, "a=$b", True, {"a": "$b"}),
({"b": "c"}, "a=${b}", False, {"a": "${b}"}),
({"b": "c"}, "a=${b}", True, {"a": "c"}),
({"b": "c"}, "a=${b:-d}", False, {"a": "${b:-d}"}),
({"b": "c"}, "a=${b:-d}", True, {"a": "c"}),
# Defined in file
({}, "b=c\na=${b}", True, {"a": "c", "b": "c"}),
# Undefined
({}, "a=${b}", True, {"a": ""}),
({}, "a=${b:-d}", True, {"a": "d"}),
# With quotes
({"b": "c"}, 'a="${b}"', True, {"a": "c"}),
({"b": "c"}, "a='${b}'", True, {"a": "c"}),
# With surrounding text
({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}),
# Self-referential
({"a": "b"}, "a=${a}", True, {"a": "b"}),
({}, "a=${a}", True, {"a": ""}),
({"a": "b"}, "a=${a:-c}", True, {"a": "b"}),
({}, "a=${a:-c}", True, {"a": "c"}),
# Reused
({"b": "c"}, "a=${b}${b}", True, {"a": "cc"}),
# Re-defined and used in file
({"b": "c"}, "b=d\na=${b}", True, {"a": "d", "b": "d"}),
({}, "a=b\na=c\nd=${a}", True, {"a": "c", "d": "c"}),
({}, "a=b\nc=${a}\nd=e\nc=${d}", True, {"a": "b", "c": "e", "d": "e"}),
],
)
def test_dotenv_values_string_io(env, string, interpolate, expected):
with mock.patch.dict(os.environ, env, clear=True):
stream = io.StringIO(string)
stream.seek(0)
result = dotenv.dotenv_values(stream=stream, interpolate=interpolate)
assert result == expected
def test_dotenv_values_file_stream(dotenv_path):
dotenv_path.write_text("a=b")
with dotenv_path.open() as f:
result = dotenv.dotenv_values(stream=f)
assert result == {"a": "b"}
================================================
FILE: tests/test_parser.py
================================================
import io
import pytest
from dotenv.parser import Binding, Original, parse_stream
@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):
result = parse_stream(io.StringIO(test_input))
assert list(result) == expected
================================================
FILE: tests/test_utils.py
================================================
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"'
)
================================================
FILE: tests/test_variables.py
================================================
import pytest
from dotenv.variables import Literal, Variable, parse_variables
@pytest.mark.parametrize(
"value,expected",
[
("", []),
("a", [Literal(value="a")]),
("${a}", [Variable(name="a", default=None)]),
("${a:-b}", [Variable(name="a", default="b")]),
(
"${a}${b}",
[
Variable(name="a", default=None),
Variable(name="b", default=None),
],
),
(
"a${b}c${d}e",
[
Literal(value="a"),
Variable(name="b", default=None),
Literal(value="c"),
Variable(name="d", default=None),
Literal(value="e"),
],
),
],
)
def test_parse_variables(value, expected):
result = parse_variables(value)
assert list(result) == expected
================================================
FILE: tests/test_zip_imports.py
================================================
import os
import posixpath
import subprocess
import sys
import textwrap
from typing import List
from unittest import mock
from zipfile import ZipFile
def walk_to_root(path: str):
last_dir = None
current_dir = path
while last_dir != current_dir:
yield current_dir
parent_dir = posixpath.dirname(current_dir)
last_dir, current_dir = current_dir, parent_dir
class FileToAdd:
def __init__(self, content: str, path: str):
self.content = content
self.path = path
def setup_zipfile(path, files: List[FileToAdd]):
zip_file_path = path / "test.zip"
dirs_init_py_added_to = set()
with ZipFile(zip_file_path, "w") as zipfile:
for f in files:
zipfile.writestr(data=f.content, zinfo_or_arcname=f.path)
for dirname in walk_to_root(posixpath.dirname(f.path)):
if dirname not in dirs_init_py_added_to:
init_path = posixpath.join(dirname, "__init__.py")
print(f"setup_zipfile: {init_path}")
zipfile.writestr(data="", zinfo_or_arcname=init_path)
dirs_init_py_added_to.add(dirname)
return zip_file_path
@mock.patch.object(sys, "path", list(sys.path))
def test_load_dotenv_gracefully_handles_zip_imports_when_no_env_file(tmp_path):
zip_file_path = setup_zipfile(
tmp_path,
[
FileToAdd(
content=textwrap.dedent(
"""
from dotenv import load_dotenv
load_dotenv()
"""
),
path="child1/child2/test.py",
),
],
)
# Should run without an error
sys.path.append(str(zip_file_path))
import child1.child2.test # noqa
def test_load_dotenv_outside_zip_file_when_called_in_zipfile(tmp_path):
zip_file_path = setup_zipfile(
tmp_path,
[
FileToAdd(
content=textwrap.dedent(
"""
from dotenv import load_dotenv
load_dotenv()
"""
),
path="child1/child2/test.py",
),
],
)
dotenv_path = tmp_path / ".env"
dotenv_path.write_bytes(b"A=x")
code_path = tmp_path / "code.py"
code_path.write_text(
textwrap.dedent(
f"""
import os
import sys
sys.path.append({str(zip_file_path)!r})
import child1.child2.test
print(os.environ['A'])
"""
)
)
result = subprocess.run(
[sys.executable, str(code_path)],
capture_output=True,
check=True,
cwd=tmp_path,
text=True,
env={
k: v for k, v in os.environ.items() if k.upper() != "A"
}, # env without 'A'
)
assert result.stdout == "x\n"
================================================
FILE: tox.ini
================================================
[tox]
envlist = lint,py{310,311,312,313,314,314t},pypy3,manifest,coverage-report
[gh-actions]
python =
3.10: py310
3.11: py311
3.12: py312
3.13: py313, lint, manifest
3.14: py314
3.14t: py314t
pypy-3.11: pypy3
[testenv]
deps =
pytest
pytest-cov
click
py{310,311,312,313,314,314t,pypy3}: ipython
commands = pytest --cov --cov-report=term-missing {posargs}
depends =
py{310,311,312,313,314,314t},pypy3: coverage-clean
coverage-report: py{310,311,312,313,314,314t},pypy3
[testenv:lint]
skip_install = true
deps =
ruff
mypy
commands =
ruff check src tests
ruff format --check src tests
mypy --python-version=3.14 src tests
mypy --python-version=3.13 src tests
mypy --python-version=3.12 src tests
mypy --python-version=3.11 src tests
mypy --python-version=3.10 src tests
[testenv:format]
skip_install = true
deps = ruff
commands = ruff format src tests
[testenv:manifest]
deps = check-manifest
skip_install = true
commands = check-manifest
[testenv:coverage-clean]
deps = coverage
skip_install = true
commands = coverage erase
[testenv:coverage-report]
deps = coverage
skip_install = true
commands =
coverage report
gitextract_1wzmeifn/ ├── .bumpversion.cfg ├── .editorconfig ├── .github/ │ ├── SECURITY.md │ ├── dependabot.yml │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs/ │ └── reference.md ├── mkdocs.yml ├── pyproject.toml ├── requirements-docs.txt ├── requirements.txt ├── ruff.toml ├── src/ │ └── dotenv/ │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── ipython.py │ ├── main.py │ ├── parser.py │ ├── py.typed │ ├── variables.py │ └── version.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── test_cli.py │ ├── test_fifo_dotenv.py │ ├── test_ipython.py │ ├── test_is_interactive.py │ ├── test_lib.py │ ├── test_main.py │ ├── test_parser.py │ ├── test_utils.py │ ├── test_variables.py │ └── test_zip_imports.py └── tox.ini
SYMBOL INDEX (173 symbols across 17 files)
FILE: src/dotenv/__init__.py
function load_ipython_extension (line 6) | def load_ipython_extension(ipython: Any) -> None:
function get_cli_string (line 12) | def get_cli_string(
FILE: src/dotenv/cli.py
function enumerate_env (line 24) | def enumerate_env() -> Optional[str]:
function cli (line 62) | def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
function stream_file (line 68) | def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
function list_values (line 93) | def list_values(ctx: click.Context, output_format: str) -> None:
function set_value (line 116) | def set_value(ctx: click.Context, key: Any, value: Any) -> None:
function get (line 137) | def get(ctx: click.Context, key: Any) -> None:
function unset (line 154) | def unset(ctx: click.Context, key: Any) -> None:
function run (line 184) | def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]...
function run_command (line 204) | def run_command(command: List[str], env: Dict[str, str]) -> None:
FILE: src/dotenv/ipython.py
class IPythonDotEnv (line 12) | class IPythonDotEnv(Magics):
method dotenv (line 34) | def dotenv(self, line):
function load_ipython_extension (line 48) | def load_ipython_extension(ipython):
FILE: src/dotenv/main.py
function _load_dotenv_disabled (line 22) | def _load_dotenv_disabled() -> bool:
function with_warn_for_invalid_lines (line 32) | def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator...
class DotEnv (line 42) | class DotEnv:
method __init__ (line 43) | def __init__(
method _get_stream (line 61) | def _get_stream(self) -> Iterator[IO[str]]:
method dict (line 75) | def dict(self) -> Dict[str, Optional[str]]:
method parse (line 91) | def parse(self) -> Iterator[Tuple[str, Optional[str]]]:
method set_as_environment_variables (line 97) | def set_as_environment_variables(self) -> bool:
method get (line 112) | def get(self, key: str) -> Optional[str]:
function get_key (line 125) | def get_key(
function rewrite (line 139) | def rewrite(
function set_key (line 193) | def set_key(
function unset_key (line 248) | def unset_key(
function resolve_variables (line 289) | def resolve_variables(
function _walk_to_root (line 314) | def _walk_to_root(path: str) -> Iterator[str]:
function find_dotenv (line 332) | def find_dotenv(
function load_dotenv (line 383) | def load_dotenv(
function dotenv_values (line 433) | def dotenv_values(
function _is_file_or_fifo (line 470) | def _is_file_or_fifo(path: StrPath) -> bool:
FILE: src/dotenv/parser.py
function make_regex (line 14) | def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:
class Original (line 35) | class Original(NamedTuple):
class Binding (line 40) | class Binding(NamedTuple):
class Position (line 47) | class Position:
method __init__ (line 48) | def __init__(self, chars: int, line: int) -> None:
method start (line 53) | def start(cls) -> "Position":
method set (line 56) | def set(self, other: "Position") -> None:
method advance (line 60) | def advance(self, string: str) -> None:
class Error (line 65) | class Error(Exception):
class Reader (line 69) | class Reader:
method __init__ (line 70) | def __init__(self, stream: IO[str]) -> None:
method has_next (line 75) | def has_next(self) -> bool:
method set_mark (line 78) | def set_mark(self) -> None:
method get_marked (line 81) | def get_marked(self) -> Original:
method peek (line 87) | def peek(self, count: int) -> str:
method read (line 90) | def read(self, count: int) -> str:
method read_regex (line 97) | def read_regex(self, regex: Pattern[str]) -> Sequence[str]:
function decode_escapes (line 105) | def decode_escapes(regex: Pattern[str], string: str) -> str:
function parse_key (line 112) | def parse_key(reader: Reader) -> Optional[str]:
function parse_unquoted_value (line 123) | def parse_unquoted_value(reader: Reader) -> str:
function parse_value (line 128) | def parse_value(reader: Reader) -> str:
function parse_binding (line 142) | def parse_binding(reader: Reader) -> Binding:
function parse_stream (line 179) | def parse_stream(stream: IO[str]) -> Iterator[Binding]:
FILE: src/dotenv/variables.py
class Atom (line 18) | class Atom(metaclass=ABCMeta):
method __ne__ (line 19) | def __ne__(self, other: object) -> bool:
method resolve (line 26) | def resolve(self, env: Mapping[str, Optional[str]]) -> str: ...
class Literal (line 29) | class Literal(Atom):
method __init__ (line 30) | def __init__(self, value: str) -> None:
method __repr__ (line 33) | def __repr__(self) -> str:
method __eq__ (line 36) | def __eq__(self, other: object) -> bool:
method __hash__ (line 41) | def __hash__(self) -> int:
method resolve (line 44) | def resolve(self, env: Mapping[str, Optional[str]]) -> str:
class Variable (line 48) | class Variable(Atom):
method __init__ (line 49) | def __init__(self, name: str, default: Optional[str]) -> None:
method __repr__ (line 53) | def __repr__(self) -> str:
method __eq__ (line 56) | def __eq__(self, other: object) -> bool:
method __hash__ (line 61) | def __hash__(self) -> int:
method resolve (line 64) | def resolve(self, env: Mapping[str, Optional[str]]) -> str:
function parse_variables (line 70) | def parse_variables(value: str) -> Iterator[Atom]:
FILE: tests/conftest.py
function cli (line 6) | def cli():
function dotenv_path (line 13) | def dotenv_path(tmp_path):
FILE: tests/test_cli.py
function test_list (line 28) | def test_list(
function test_list_non_existent_file (line 42) | def test_list_non_existent_file(cli):
function test_list_not_a_file (line 49) | def test_list_not_a_file(cli):
function test_list_no_file (line 56) | def test_list_no_file(cli):
function test_get_existing_value (line 62) | def test_get_existing_value(cli, dotenv_path):
function test_get_non_existent_value (line 70) | def test_get_non_existent_value(cli, dotenv_path):
function test_get_non_existent_file (line 76) | def test_get_non_existent_file(cli):
function test_get_not_a_file (line 83) | def test_get_not_a_file(cli):
function test_unset_existing_value (line 90) | def test_unset_existing_value(cli, dotenv_path):
function test_unset_non_existent_value (line 99) | def test_unset_non_existent_value(cli, dotenv_path):
function test_set_quote_options (line 116) | def test_set_quote_options(cli, dotenv_path, quote_mode, variable, value...
function test_set_export (line 143) | def test_set_export(cli, dotenv_path, export_mode, variable, value, expe...
function test_set_non_existent_file (line 163) | def test_set_non_existent_file(cli):
function test_set_no_file (line 169) | def test_set_no_file(cli):
function test_get_default_path (line 176) | def test_get_default_path(tmp_path):
function test_run (line 184) | def test_run(tmp_path):
function test_run_with_existing_variable (line 192) | def test_run_with_existing_variable(tmp_path):
function test_run_with_existing_variable_not_overridden (line 202) | def test_run_with_existing_variable_not_overridden(tmp_path):
function test_run_with_none_value (line 214) | def test_run_with_none_value(tmp_path):
function test_run_with_other_env (line 222) | def test_run_with_other_env(dotenv_path, tmp_path):
function test_run_without_cmd (line 233) | def test_run_without_cmd(tmp_path):
function test_run_with_invalid_cmd (line 240) | def test_run_with_invalid_cmd(dotenv_path, tmp_path):
function test_run_with_env_missing_and_invalid_cmd (line 250) | def test_run_with_env_missing_and_invalid_cmd(tmp_path):
function test_run_with_version (line 261) | def test_run_with_version(tmp_path):
function test_run_with_command_flags (line 268) | def test_run_with_command_flags(dotenv_path, tmp_path):
function test_run_with_dotenv_and_command_flags (line 284) | def test_run_with_dotenv_and_command_flags(dotenv_path, tmp_path):
FILE: tests/test_fifo_dotenv.py
function test_load_dotenv_from_fifo (line 13) | def test_load_dotenv_from_fifo(tmp_path: pathlib.Path, monkeypatch):
FILE: tests/test_ipython.py
function test_ipython_existing_variable_no_override (line 14) | def test_ipython_existing_variable_no_override(tmp_path):
function test_ipython_existing_variable_override (line 33) | def test_ipython_existing_variable_override(tmp_path):
function test_ipython_new_variable (line 52) | def test_ipython_new_variable(tmp_path):
FILE: tests/test_is_interactive.py
class TestIsInteractive (line 8) | class TestIsInteractive:
method _create_dotenv_file (line 32) | def _create_dotenv_file(self, tmp_path):
method _setup_subdir_and_chdir (line 38) | def _setup_subdir_and_chdir(self, tmp_path, monkeypatch):
method _remove_ps_attributes (line 45) | def _remove_ps_attributes(self, monkeypatch):
method _mock_main_import (line 52) | def _mock_main_import(self, monkeypatch, mock_main_module):
method _mock_main_import_error (line 63) | def _mock_main_import_error(self, monkeypatch):
method test_is_interactive_with_ps1 (line 74) | def test_is_interactive_with_ps1(self, tmp_path, monkeypatch):
method test_is_interactive_with_ps2 (line 87) | def test_is_interactive_with_ps2(self, tmp_path, monkeypatch):
method test_is_interactive_main_module_not_found (line 100) | def test_is_interactive_main_module_not_found(self, tmp_path, monkeypa...
method test_is_interactive_main_without_file (line 113) | def test_is_interactive_main_without_file(self, tmp_path, monkeypatch):
method test_is_interactive_main_with_file (line 129) | def test_is_interactive_main_with_file(self, tmp_path, monkeypatch):
method test_is_interactive_precedence_ps1_over_main (line 147) | def test_is_interactive_precedence_ps1_over_main(self, tmp_path, monke...
method test_is_interactive_ps1_and_ps2_both_exist (line 165) | def test_is_interactive_ps1_and_ps2_both_exist(self, tmp_path, monkeyp...
method test_is_interactive_main_module_with_file_attribute_none (line 179) | def test_is_interactive_main_module_with_file_attribute_none(
method test_is_interactive_no_ps_attributes_and_normal_execution (line 201) | def test_is_interactive_no_ps_attributes_and_normal_execution(
method test_is_interactive_with_usecwd_override (line 218) | def test_is_interactive_with_usecwd_override(self, tmp_path, monkeypat...
FILE: tests/test_lib.py
function run_dotenv (line 6) | def run_dotenv(
function check_process (line 26) | def check_process(
FILE: tests/test_main.py
function test_set_key_no_file (line 15) | def test_set_key_no_file(tmp_path):
function test_set_key (line 44) | def test_set_key(dotenv_path, before, key, value, expected, after):
function test_set_key_encoding (line 56) | def test_set_key_encoding(dotenv_path):
function test_set_key_preserves_file_mode (line 68) | def test_set_key_preserves_file_mode(dotenv_path):
function test_rewrite_closes_file_handle_on_lstat_failure (line 79) | def test_rewrite_closes_file_handle_on_lstat_failure(tmp_path):
function test_set_key_symlink_to_existing_file (line 101) | def test_set_key_symlink_to_existing_file(tmp_path):
function test_set_key_symlink_to_missing_file (line 118) | def test_set_key_symlink_to_missing_file(tmp_path):
function test_set_key_follow_symlinks (line 133) | def test_set_key_follow_symlinks(tmp_path):
function test_set_key_permission_error (line 149) | def test_set_key_permission_error(dotenv_path):
function test_get_key_no_file (line 168) | def test_get_key_no_file(tmp_path):
function test_get_key_not_found (line 189) | def test_get_key_not_found(dotenv_path):
function test_get_key_ok (line 199) | def test_get_key_ok(dotenv_path):
function test_get_key_encoding (line 210) | def test_get_key_encoding(dotenv_path):
function test_get_key_none (line 219) | def test_get_key_none(dotenv_path):
function test_unset_with_value (line 230) | def test_unset_with_value(dotenv_path):
function test_unset_no_value (line 242) | def test_unset_no_value(dotenv_path):
function test_unset_encoding (line 254) | def test_unset_encoding(dotenv_path):
function test_unset_non_existent_file (line 264) | def test_unset_non_existent_file(tmp_path):
function test_unset_key_symlink_to_existing_file (line 281) | def test_unset_key_symlink_to_existing_file(tmp_path):
function test_unset_key_symlink_to_missing_file (line 297) | def test_unset_key_symlink_to_missing_file(tmp_path):
function test_unset_key_follow_symlinks (line 314) | def test_unset_key_follow_symlinks(tmp_path):
function prepare_file_hierarchy (line 326) | def prepare_file_hierarchy(path):
function test_find_dotenv_no_file_raise (line 345) | def test_find_dotenv_no_file_raise(tmp_path):
function test_find_dotenv_no_file_no_raise (line 353) | def test_find_dotenv_no_file_no_raise(tmp_path):
function test_find_dotenv_found (line 362) | def test_find_dotenv_found(tmp_path):
function test_load_dotenv_existing_file (line 377) | def test_load_dotenv_existing_file(dotenv_path):
function test_load_dotenv_disabled (line 402) | def test_load_dotenv_disabled(dotenv_path, flag_value):
function test_load_dotenv_disabled_notification (line 431) | def test_load_dotenv_disabled_notification(dotenv_path, flag_value):
function test_load_dotenv_enabled (line 467) | def test_load_dotenv_enabled(dotenv_path, flag_value):
function test_load_dotenv_enabled_no_notification (line 497) | def test_load_dotenv_enabled_no_notification(dotenv_path, flag_value):
function test_load_dotenv_doesnt_disable_itself (line 512) | def test_load_dotenv_doesnt_disable_itself(dotenv_path):
function test_load_dotenv_no_file_verbose (line 521) | def test_load_dotenv_no_file_verbose():
function test_load_dotenv_existing_variable_no_override (line 537) | def test_load_dotenv_existing_variable_no_override(dotenv_path):
function test_load_dotenv_existing_variable_override (line 550) | def test_load_dotenv_existing_variable_override(dotenv_path):
function test_load_dotenv_redefine_var_used_in_file_no_override (line 563) | def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path):
function test_load_dotenv_redefine_var_used_in_file_with_override (line 576) | def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path):
function test_load_dotenv_string_io_utf_8 (line 589) | def test_load_dotenv_string_io_utf_8():
function test_load_dotenv_file_stream (line 602) | def test_load_dotenv_file_stream(dotenv_path):
function test_load_dotenv_in_current_dir (line 612) | def test_load_dotenv_in_current_dir(tmp_path):
function test_dotenv_values_file (line 637) | def test_dotenv_values_file(dotenv_path):
function test_dotenv_values_string_io (line 681) | def test_dotenv_values_string_io(env, string, interpolate, expected):
function test_dotenv_values_file_stream (line 691) | def test_dotenv_values_file_stream(dotenv_path):
FILE: tests/test_parser.py
function test_parse_stream (line 550) | def test_parse_stream(test_input, expected):
FILE: tests/test_utils.py
function test_to_cli_string (line 4) | def test_to_cli_string():
FILE: tests/test_variables.py
function test_parse_variables (line 32) | def test_parse_variables(value, expected):
FILE: tests/test_zip_imports.py
function walk_to_root (line 11) | def walk_to_root(path: str):
class FileToAdd (line 20) | class FileToAdd:
method __init__ (line 21) | def __init__(self, content: str, path: str):
function setup_zipfile (line 26) | def setup_zipfile(path, files: List[FileToAdd]):
function test_load_dotenv_gracefully_handles_zip_imports_when_no_env_file (line 42) | def test_load_dotenv_gracefully_handles_zip_imports_when_no_env_file(tmp...
function test_load_dotenv_outside_zip_file_when_called_in_zipfile (line 64) | def test_load_dotenv_outside_zip_file_when_called_in_zipfile(tmp_path):
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (141K chars).
[
{
"path": ".bumpversion.cfg",
"chars": 105,
"preview": "[bumpversion]\ncurrent_version = 1.2.2\ncommit = True\ntag = True\n\n[bumpversion:file:src/dotenv/version.py]\n"
},
{
"path": ".editorconfig",
"chars": 243,
"preview": "# see: http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trail"
},
{
"path": ".github/SECURITY.md",
"chars": 680,
"preview": "# Security Policy\n\n## Supported Versions\n\n| Version | Supported |\n| --------- | ------------------ |\n| latest"
},
{
"path": ".github/dependabot.yml",
"chars": 579,
"preview": "# Keep GitHub Actions up to date with GitHub's Dependabot...\n# https://docs.github.com/en/code-security/dependabot/worki"
},
{
"path": ".github/workflows/release.yml",
"chars": 1172,
"preview": "name: Upload Python Package\n\non:\n release:\n types: [created]\n\njobs:\n publish:\n runs-on: ubuntu-latest\n # Spec"
},
{
"path": ".github/workflows/test.yml",
"chars": 1105,
"preview": "name: Run Tests\n\non:\n push:\n branches:\n - main\n pull_request:\n\nconcurrency:\n group: ${{ github.workflow }}-${"
},
{
"path": ".gitignore",
"chars": 1566,
"preview": ".DS_Store\n.idea\n.vscode/\n\n# Created by https://www.gitignore.io/api/python\n# Edit at https://www.gitignore.io/?templates"
},
{
"path": ".pre-commit-config.yaml",
"chars": 182,
"preview": "repos:\n - repo: https://github.com/astral-sh/ruff-pre-commit\n rev: v0.12.0\n hooks:\n # Run the linter.\n "
},
{
"path": "CHANGELOG.md",
"chars": 18502,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "CONTRIBUTING.md",
"chars": 677,
"preview": "Contributing\n============\n\nAll the contributions are welcome! Please open [an\nissue](https://github.com/theskumar/python"
},
{
"path": "LICENSE",
"chars": 1556,
"preview": "Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-"
},
{
"path": "MANIFEST.in",
"chars": 283,
"preview": "include LICENSE *.md *.yml *.yaml *.toml\n\ninclude tox.ini\nrecursive-include docs *.md\nrecursive-include tests *.py\n\nincl"
},
{
"path": "Makefile",
"chars": 597,
"preview": ".PHONY: clean-pyc clean-build test fmt\n\nclean: clean-build clean-pyc\n\nclean-build:\n\trm -fr build/\n\trm -rf .mypy_cache/\n\t"
},
{
"path": "README.md",
"chars": 7962,
"preview": "# python-dotenv\n\n[![Build Status][build_status_badge]][build_status_link]\n[![PyPI version][pypi_badge]][pypi_link]\n\npyth"
},
{
"path": "docs/reference.md",
"chars": 14,
"preview": "# ::: dotenv\n\n"
},
{
"path": "mkdocs.yml",
"chars": 630,
"preview": "site_name: python-dotenv\nrepo_url: https://github.com/theskumar/python-dotenv\nedit_uri: \"\"\ntheme:\n name: material\n pal"
},
{
"path": "pyproject.toml",
"chars": 2131,
"preview": "[build-system]\nrequires = [\"setuptools >= 77.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"python-doten"
},
{
"path": "requirements-docs.txt",
"chars": 130,
"preview": "mdx_truly_sane_lists>=1.3\nmkdocs-include-markdown-plugin>=6.0.0\nmkdocs-material>=9.5.0\nmkdocstrings[python]>=0.24.0\nmkdo"
},
{
"path": "requirements.txt",
"chars": 81,
"preview": "bumpversion\nclick\nipython\npytest-cov\npytest>=3.9\ntox\nwheel\nruff\nbuild\npre-commit\n"
},
{
"path": "ruff.toml",
"chars": 178,
"preview": "[lint]\nselect = [\n # pycodestyle\n \"E4\",\n \"E7\",\n \"E9\",\n\n # Pyflakes\n \"F\",\n\n # flake8-bugbear\n \"B\""
},
{
"path": "src/dotenv/__init__.py",
"chars": 1230,
"preview": "from typing import Any, Optional\n\nfrom .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key"
},
{
"path": "src/dotenv/__main__.py",
"chars": 129,
"preview": "\"\"\"Entry point for cli, enables execution with `python -m dotenv`\"\"\"\n\nfrom .cli import cli\n\nif __name__ == \"__main__\":\n "
},
{
"path": "src/dotenv/cli.py",
"chars": 6865,
"preview": "import json\nimport os\nimport shlex\nimport sys\nfrom contextlib import contextmanager\nfrom typing import IO, Any, Dict, It"
},
{
"path": "src/dotenv/ipython.py",
"chars": 1326,
"preview": "from IPython.core.magic import Magics, line_magic, magics_class # type: ignore\nfrom IPython.core.magic_arguments import"
},
{
"path": "src/dotenv/main.py",
"chars": 14855,
"preview": "import io\nimport logging\nimport os\nimport pathlib\nimport stat\nimport sys\nimport tempfile\nfrom collections import Ordered"
},
{
"path": "src/dotenv/parser.py",
"chars": 5179,
"preview": "import codecs\nimport re\nfrom typing import (\n IO,\n Iterator,\n Match,\n NamedTuple,\n Optional,\n Pattern,"
},
{
"path": "src/dotenv/py.typed",
"chars": 26,
"preview": "# Marker file for PEP 561\n"
},
{
"path": "src/dotenv/variables.py",
"chars": 2348,
"preview": "import re\nfrom abc import ABCMeta, abstractmethod\nfrom typing import Iterator, Mapping, Optional, Pattern\n\n_posix_variab"
},
{
"path": "src/dotenv/version.py",
"chars": 22,
"preview": "__version__ = \"1.2.2\"\n"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/conftest.py",
"chars": 279,
"preview": "import pytest\nfrom click.testing import CliRunner\n\n\n@pytest.fixture\ndef cli():\n runner = CliRunner()\n with runner."
},
{
"path": "tests/test_cli.py",
"chars": 8135,
"preview": "import os\nfrom pathlib import Path\nfrom typing import Optional\n\nimport pytest\n\nimport dotenv\nfrom dotenv.cli import cli "
},
{
"path": "tests/test_fifo_dotenv.py",
"chars": 743,
"preview": "import os\nimport pathlib\nimport sys\nimport threading\n\nimport pytest\n\nfrom dotenv import load_dotenv\n\npytestmark = pytest"
},
{
"path": "tests/test_ipython.py",
"chars": 1739,
"preview": "import os\nimport sys\nfrom unittest import mock\n\nimport pytest\n\npytest.importorskip(\"IPython\")\n\n\n@pytest.mark.skipif(\n "
},
{
"path": "tests/test_is_interactive.py",
"chars": 8921,
"preview": "import builtins\nimport sys\nfrom unittest import mock\n\nfrom dotenv.main import find_dotenv\n\n\nclass TestIsInteractive:\n "
},
{
"path": "tests/test_lib.py",
"chars": 1122,
"preview": "import subprocess\nfrom pathlib import Path\nfrom typing import Sequence\n\n\ndef run_dotenv(\n args: Sequence[str],\n cw"
},
{
"path": "tests/test_main.py",
"chars": 19454,
"preview": "import io\nimport logging\nimport os\nimport stat\nimport subprocess\nimport sys\nimport textwrap\nfrom unittest import mock\n\ni"
},
{
"path": "tests/test_parser.py",
"chars": 14523,
"preview": "import io\n\nimport pytest\n\nfrom dotenv.parser import Binding, Original, parse_stream\n\n\n@pytest.mark.parametrize(\n \"tes"
},
{
"path": "tests/test_utils.py",
"chars": 747,
"preview": "from dotenv import get_cli_string as c\n\n\ndef test_to_cli_string():\n assert c() == \"dotenv\"\n assert c(path=\"/etc/.e"
},
{
"path": "tests/test_variables.py",
"chars": 884,
"preview": "import pytest\n\nfrom dotenv.variables import Literal, Variable, parse_variables\n\n\n@pytest.mark.parametrize(\n \"value,ex"
},
{
"path": "tests/test_zip_imports.py",
"chars": 2864,
"preview": "import os\nimport posixpath\nimport subprocess\nimport sys\nimport textwrap\nfrom typing import List\nfrom unittest import moc"
},
{
"path": "tox.ini",
"chars": 1212,
"preview": "[tox]\nenvlist = lint,py{310,311,312,313,314,314t},pypy3,manifest,coverage-report\n\n[gh-actions]\npython =\n 3.10: py310\n"
}
]
About this extraction
This page contains the full source code of the theskumar/python-dotenv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (127.9 KB), approximately 33.9k tokens, and a symbol index with 173 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.