From 9c84698a956a7d20748d0f2299b538c1b6dc0dfc Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Mon, 9 Mar 2026 01:01:22 +0530 Subject: [PATCH 1/2] docs: redo docs, use zensical, and follow diatrix --- .github/workflows/release.yml | 2 +- .gitignore | 16 +-- CHANGELOG.md | 3 + CONTRIBUTING.md | 12 +-- Makefile | 5 + README.md | 172 +----------------------------- docs/changelog.md | 2 +- docs/contributing.md | 2 +- docs/getting-started.md | 88 +++++++++++++++ docs/guides/advanced.md | 129 ++++++++++++++++++++++ docs/guides/cli.md | 73 +++++++++++++ docs/guides/configuration.md | 195 ++++++++++++++++++++++++++++++++++ docs/guides/index.md | 8 ++ docs/guides/ipython.md | 39 +++++++ docs/index.md | 54 +++++++++- docs/license.md | 2 +- docs/reference.md | 2 - docs/reference/api.md | 19 ++++ docs/reference/cli.md | 99 +++++++++++++++++ docs/reference/file-format.md | 138 ++++++++++++++++++++++++ docs/reference/index.md | 7 ++ mkdocs.yml | 59 +++++++++- requirements-docs.txt | 4 +- src/dotenv/__init__.py | 7 +- src/dotenv/cli.py | 24 +---- src/dotenv/ipython.py | 3 + src/dotenv/main.py | 155 +++++++++++++++++++-------- src/dotenv/parser.py | 22 ++++ src/dotenv/variables.py | 11 +- 29 files changed, 1078 insertions(+), 274 deletions(-) mode change 120000 => 100644 docs/changelog.md mode change 120000 => 100644 docs/contributing.md create mode 100644 docs/getting-started.md create mode 100644 docs/guides/advanced.md create mode 100644 docs/guides/cli.md create mode 100644 docs/guides/configuration.md create mode 100644 docs/guides/index.md create mode 100644 docs/guides/ipython.md mode change 120000 => 100644 docs/index.md mode change 120000 => 100644 docs/license.md delete mode 100644 docs/reference.md create mode 100644 docs/reference/api.md create mode 100644 docs/reference/cli.md create mode 100644 docs/reference/file-format.md create mode 100644 docs/reference/index.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc91d369..6cda045b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: run: | pip install -r requirements-docs.txt pip install -e . - mkdocs build + zensical build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 diff --git a/.gitignore b/.gitignore index ba1234ce..a162a4c5 100644 --- a/.gitignore +++ b/.gitignore @@ -67,16 +67,6 @@ coverage.xml local_settings.py db.sqlite3 -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - # PyBuilder target/ @@ -90,9 +80,6 @@ ipython_config.py # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - # SageMath parsed files *.sage.py @@ -112,7 +99,7 @@ venv.bak/ # Rope project settings .ropeproject -# mkdocs documentation +# zensical documentation /site # mypy @@ -125,5 +112,6 @@ dmypy.json ### Python Patch ### .venv/ +uv.lock # End of https://www.gitignore.io/api/python diff --git a/CHANGELOG.md b/CHANGELOG.md index ee732346..ca7de0fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -488,6 +488,9 @@ os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). [@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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49840fa7..add53f3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,4 @@ -Contributing -============ +# Contributing All the contributions are welcome! Please open [an issue](https://github.com/theskumar/python-dotenv/issues/new) or send us @@ -18,18 +17,15 @@ 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](): +Documentation is published with [zensical](https://zensical.org/) and follows +the approach established by [Diátaxis](https://diataxis.fr/): ```shell -$ uv pip install -r requirements-docs.txt -$ uv pip install -e . -$ uv run mkdocs serve +$ make serve-docs ``` Open http://127.0.0.1:8000/ to view the documentation locally. diff --git a/Makefile b/Makefile index 78866a60..23573f6b 100644 --- a/Makefile +++ b/Makefile @@ -33,3 +33,8 @@ coverage: coverage-html: coverage coverage html + +serve-docs: + uv pip install -r requirements-docs.txt + uv pip install -e . + uv run zensical serve diff --git a/README.md b/README.md index a08d6141..98659329 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,7 @@ 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) +> **[Read the full documentation](https://theskumar.github.io/python-dotenv/)** ## Getting Started @@ -73,163 +63,9 @@ 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. +See the [file format specification](https://theskumar.github.io/python-dotenv/reference/file-format/) +for full details on `.env` syntax, multiline values, escape sequences, and +variable expansion. ## Related Projects diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 120000 index 04c99a55..00000000 --- a/docs/changelog.md +++ /dev/null @@ -1 +0,0 @@ -../CHANGELOG.md \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..786b75d5 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 120000 index 44fcc634..00000000 --- a/docs/contributing.md +++ /dev/null @@ -1 +0,0 @@ -../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..383d6158 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1 @@ +--8<-- "contributing.md" diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..433600a8 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,88 @@ +--- +icon: lucide/rocket +--- + +# Getting Started + +This tutorial walks you through installing python-dotenv and using it to load +environment variables from a `.env` file. + +## Installation + +=== "`pip`" + + ```shell + pip install python-dotenv + ``` + +=== "`uv`" + + ```shell + uv add python-dotenv + ``` + +## Create a `.env` file + +Add a `.env` file to the root of your project: + +``` +. +├── .env +└── app.py +``` + +Write your configuration as key-value pairs: + +```bash +# .env +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. See +[Variable expansion](reference/file-format.md#variable-expansion) for the full +syntax. + +!!! tip + + Add `.env` to your `.gitignore`, especially if it contains secrets. + +## Load the `.env` file + +In your Python application: + +```python +from dotenv import load_dotenv +import os + +load_dotenv() + +domain = os.getenv("DOMAIN") +print(domain) # example.org +``` + +`load_dotenv()` looks for a `.env` file starting in the same directory as the +calling script, walking up the directory tree until it finds one. Each key-value +pair is added to `os.environ`. + +## How it works + +By default, `load_dotenv()`: + +- Searches for a `.env` file automatically using `find_dotenv()`. +- Reads each key-value pair and adds it to `os.environ`. +- **Does not override** existing environment variables. Pass `override=True` to change this. + +```python +# Override existing environment variables +load_dotenv(override=True) +``` + +## Next steps + +- [Configuration Patterns](guides/configuration.md): load config as a dict, layer multiple `.env` files. +- [Command-line Interface](guides/cli.md): manage `.env` files and run commands from the terminal. +- [File Format](reference/file-format.md): full specification of `.env` file syntax. +- [Python API Reference](reference/api.md): all functions and their parameters. diff --git a/docs/guides/advanced.md b/docs/guides/advanced.md new file mode 100644 index 00000000..65931139 --- /dev/null +++ b/docs/guides/advanced.md @@ -0,0 +1,129 @@ +--- +icon: lucide/atom +--- + +# Advanced Usage + +## Parse configuration from a stream + +`load_dotenv` and `dotenv_values` accept text streams via the `stream` argument, +allowing you to load variables from sources other than the filesystem: + +```python +from io import StringIO +from dotenv import load_dotenv + +config = StringIO("USER=foo\nEMAIL=foo@example.org") +load_dotenv(stream=config) +``` + +This works with any text stream - `StringIO`, file objects, or responses from +network calls. + +## Read from FIFOs (named pipes) + +On Unix systems, python-dotenv can read `.env` from a FIFO (named pipe). This is +useful when the `.env` content is generated dynamically: + +```shell +mkfifo .env +echo "SECRET=dynamic_value" > .env & +python app.py +``` + +`find_dotenv()` matches FIFOs in addition to regular files. + +## File encoding + +All functions that read or write `.env` files accept an `encoding` parameter +(defaults to `utf-8`): + +```python +from dotenv import load_dotenv, dotenv_values + +load_dotenv(".env", encoding="latin-1") + +config = dotenv_values(".env", encoding="latin-1") +``` + +## Disable `load_dotenv` + +Set the `PYTHON_DOTENV_DISABLED` environment variable to disable `load_dotenv()` +from loading `.env` files or streams. Accepted truthy values: `1`, `true`, `t`, +`yes`, `y` (case-insensitive). + +```shell +PYTHON_DOTENV_DISABLED=1 python app.py +``` + +This is useful when you can't modify third-party package calls to `load_dotenv`, +or in production environments where `.env` files should be ignored. + +## Controlling file discovery with `find_dotenv` + +`find_dotenv()` walks up the directory tree from the calling script's location +until it finds a `.env` file: + +```python +from dotenv import find_dotenv + +path = find_dotenv() # returns absolute path or "" +``` + +Use `usecwd=True` to search from the current working directory instead of the +calling script's directory: + +```python +path = find_dotenv(usecwd=True) +``` + +Search for a file with a different name: + +```python +path = find_dotenv(".env.production") +``` + +Raise an error if the file is not found: + +```python +path = find_dotenv(raise_error_if_not_found=True) # raises IOError +``` + +In REPL, IPython, frozen, and debugger environments, `find_dotenv` automatically +uses the current working directory. + +## Symlink handling + +`set_key` and `unset_key` do **not** follow symlinks by default. If your `.env` +is a symlink and you want writes to target the symlink destination, pass +`follow_symlinks=True`: + +```python +from dotenv import set_key + +set_key(".env", "KEY", "value", follow_symlinks=True) +``` + +!!! warning + + This behavior changed in v1.2.2. Previously, symlinks were followed in some situations. See the [changelog](../changelog/#122-2026-03-01) for details. + +## Debugging with verbose mode + +Pass `verbose=True` to log warnings when files are missing or keys are not found: + +```python +from dotenv import load_dotenv, get_key + +load_dotenv(verbose=True) # warns if .env is missing +get_key(".env", "MISSING_KEY") # warns if key not found +``` + +Messages are emitted via Python's `logging` module under the `dotenv.main` +logger. To see them, configure logging: + +```python +import logging + +logging.basicConfig() +``` diff --git a/docs/guides/cli.md b/docs/guides/cli.md new file mode 100644 index 00000000..c823e36a --- /dev/null +++ b/docs/guides/cli.md @@ -0,0 +1,73 @@ +--- +icon: lucide/square-terminal +--- + +# Command-line Interface + +python-dotenv includes a `dotenv` CLI for managing `.env` files from the +terminal. + +## Installation + +The CLI requires the `click` package. Install it with the `cli` extra: + +```shell +pip install "python-dotenv[cli]" +``` + +The CLI can also be invoked as a Python module: + +```shell +python -m dotenv +``` + +## Set up your .env from the terminal + +Use `set`, `get`, and `unset` to manage key-value pairs: + +```shell +dotenv set USER foo +dotenv set EMAIL foo@example.org +dotenv get EMAIL +dotenv unset EMAIL +``` + +See [CLI Reference](../reference/cli.md) for options such as `--quote` and +`--export`. + +## Run a command with .env loaded + +Load environment variables from `.env` and execute a command: + +```shell +dotenv run -- python app.py +``` + +By default, `.env` values override existing environment variables. Use +`--no-override` to keep existing values. See [CLI +Reference](../reference/cli.md#dotenv-run) for details. + +## Export .env for shell use + +List values in a format suitable for sourcing in a shell: + +```shell +dotenv list --format=export +``` + +Other formats include `simple`, `json`, and `shell`. See [CLI +Reference](../reference/cli.md#dotenv-list) for all options. + +## Use a non-default .env file + +Use `-f` to target a specific file (default is `.env` in the current directory): + +```shell +dotenv -f .env.production list +dotenv -f .env.production run -- python app.py +``` + +## Full CLI reference + +See [CLI Reference](../reference/cli.md) for all commands, options, and exit +codes. diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md new file mode 100644 index 00000000..ecec20e2 --- /dev/null +++ b/docs/guides/configuration.md @@ -0,0 +1,195 @@ +--- +icon: lucide/bolt +--- + +# Configuration Patterns + +## Load configuration without altering the environment + +`dotenv_values` parses a `.env` file and returns a `dict` without modifying +`os.environ`: + +```python +from dotenv import dotenv_values + +config = dotenv_values(".env") +# config = {"USER": "foo", "EMAIL": "foo@example.org"} +``` + +This is useful when you need the values but don't want to pollute the global +environment, for example in tests or when passing config explicitly to your +application. + +## Per-environment files + +Use separate `.env` files for each environment. Load a base file first, then +apply environment-specific overrides: + +```python +import os +from dotenv import load_dotenv + +load_dotenv(".env") # shared defaults + +env = os.getenv("APP_ENV", "development") +load_dotenv(f".env.{env}", override=True) # environment-specific overrides +``` + +`load_dotenv` silently does nothing if the file doesn't exist, so this is safe +to use even when only some override files are present. + +A typical file layout: + +``` +.env # shared defaults (committed) +.env.development # local dev overrides (gitignored) +.env.test # test overrides (committed or gitignored) +.env.production # production overrides (gitignored, or use real env vars) +``` + +The same pattern works with `dotenv_values`: + +```python +import os +from dotenv import dotenv_values + +env = os.getenv("APP_ENV", "development") +config = { + **dotenv_values(".env"), # shared defaults + **dotenv_values(f".env.{env}"), # environment overrides + **os.environ, # real environment wins last +} +``` + +## Layer multiple `.env` files + +Combine multiple `.env` files with environment variables for layered +configuration: + +```python +import os +from dotenv import dotenv_values + +config = { + **dotenv_values(".env.shared"), # shared development variables + **dotenv_values(".env.secret"), # sensitive variables + **os.environ, # override with real environment +} +``` + +Later sources override earlier ones, so environment variables take highest +priority. + +## Get, set, and unset individual keys + +Read a single value from a `.env` file: + +```python +from dotenv import get_key + +value = get_key(".env", "EMAIL") +``` + +Write a key-value pair (creates the file if it doesn't exist): + +```python +from dotenv import set_key + +set_key(".env", "EMAIL", "foo@example.org") +``` + +Remove a key: + +```python +from dotenv import unset_key + +unset_key(".env", "EMAIL") +``` + +## Override behavior + +By default, `load_dotenv` does **not** override existing environment variables: + +```python +from dotenv import load_dotenv + +load_dotenv() # existing env wins +load_dotenv(override=True) # .env wins +``` + +`dotenv_values` always resolves as if `override=True`. + +See [Resolution order](../reference/file-format.md#resolution-order) for the +full precedence rules. + +## Disable variable interpolation + +By default, `${VAR}` references in values are resolved. To treat values as +literal strings, disable interpolation: + +```python +from dotenv import load_dotenv, dotenv_values + +load_dotenv(interpolate=False) + +# or +config = dotenv_values(".env", interpolate=False) +``` + +This is useful when values contain `$` characters that should not be expanded, +such as passwords. + +## Type casting + +python-dotenv returns all values as strings. Cast them in your application code: + +```python +import os +from dotenv import load_dotenv + +load_dotenv() + +# Integer +db_port = int(os.getenv("DB_PORT", "5432")) + +# Boolean - careful, bool("false") is True +debug = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes") + +# Comma-separated list +allowed_hosts = os.getenv("ALLOWED_HOSTS", "").split(",") +``` + +For projects with many typed settings, consider +[pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) +or [python-decouple](https://github.com/HBNetwork/python-decouple), which handle +type casting and validation declaratively. + +## Use a `.env.example` template + +Commit a `.env.example` file with all required keys but no secret values. This +documents the expected configuration and helps with team onboarding: + +```bash +# .env.example - copy to .env and fill in values +SECRET_KEY=change-me +DATABASE_URL=postgresql://user:password@localhost:5432/mydb + +# Optional (defaults shown) +DEBUG=false +LOG_LEVEL=INFO +``` + +New developers can then run: + +```shell +cp .env.example .env +``` + +Add `.env` to `.gitignore` but keep `.env.example` committed: + +```gitignore +.env +.env.production +.env.*.local +!.env.example +``` diff --git a/docs/guides/index.md b/docs/guides/index.md new file mode 100644 index 00000000..017ea2dc --- /dev/null +++ b/docs/guides/index.md @@ -0,0 +1,8 @@ +# How-to Guides + +Task-oriented recipes for common python-dotenv workflows. + +- [**Configuration Patterns**](configuration.md): Load config as a dict, layer multiple `.env` files, get/set/unset keys. +- [**Command-line Interface**](cli.md): Manage `.env` files and run commands from the terminal. +- [**IPython & Jupyter**](ipython.md): Use the `%dotenv` magic command in interactive sessions. +- [**Advanced Usage**](advanced.md): Streams, FIFOs, disabling `load_dotenv`, and symlink handling. diff --git a/docs/guides/ipython.md b/docs/guides/ipython.md new file mode 100644 index 00000000..ca87cff2 --- /dev/null +++ b/docs/guides/ipython.md @@ -0,0 +1,39 @@ +--- +icon: lucide/notebook +--- + +# IPython & Jupyter + +python-dotenv provides a `%dotenv` magic command for IPython and Jupyter sessions. + +## Loading the extension + +```python +%load_ext dotenv +``` + +## Usage + +Load `.env` from the default location (found automatically via `find_dotenv`): + +```python +%dotenv +``` + +Load from a specific path: + +```python +%dotenv relative/or/absolute/path/to/.env +``` + +## Flags + +`-o` +: Override existing environment variables. + +`-v` +: Verbose output. Prints each variable as it is loaded. + +```python +%dotenv -o -v +``` diff --git a/docs/index.md b/docs/index.md deleted file mode 120000 index 32d46ee8..00000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -../README.md \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..b4a70492 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,53 @@ +# 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 sets them as +environment variables. It helps in the development of applications following the +[12-factor](https://12factor.net/) principles. + +=== "`pip`" + + ```shell + pip install python-dotenv + ``` + +=== "`uv`" + + ```shell + uv add python-dotenv + ``` + +```python +from dotenv import load_dotenv + +load_dotenv() # takes variables from .env and adds them to os.environ +``` + +## Documentation + +
+ +- [**Getting Started**](getting-started.md) → + + Install python-dotenv and load your first `.env` file. + +- [**How-to Guides**](guides/index.md) → + + Recipes for configuration patterns, the CLI, IPython, and advanced usage. + +- [**Reference**](reference/index.md) → + + Complete Python API, CLI reference, and `.env` file format specification. + +- [**Changelog**](changelog.md) → + + Release history and upgrade notes. + +
+ +[pypi_link]: https://badge.fury.io/py/python-dotenv +[pypi_badge]: https://badge.fury.io/py/python-dotenv.svg +[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 diff --git a/docs/license.md b/docs/license.md deleted file mode 120000 index ea5b6064..00000000 --- a/docs/license.md +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 00000000..f409d452 --- /dev/null +++ b/docs/license.md @@ -0,0 +1 @@ +--8<-- "LICENSE" diff --git a/docs/reference.md b/docs/reference.md deleted file mode 100644 index 8a3762ad..00000000 --- a/docs/reference.md +++ /dev/null @@ -1,2 +0,0 @@ -# ::: dotenv - diff --git a/docs/reference/api.md b/docs/reference/api.md new file mode 100644 index 00000000..bae8a1bc --- /dev/null +++ b/docs/reference/api.md @@ -0,0 +1,19 @@ +--- +icon: lucide/code +--- + +# Python API + +::: dotenv.load_dotenv + +::: dotenv.dotenv_values + +::: dotenv.find_dotenv + +::: dotenv.get_key + +::: dotenv.set_key + +::: dotenv.unset_key + +::: dotenv.get_cli_string diff --git a/docs/reference/cli.md b/docs/reference/cli.md new file mode 100644 index 00000000..6e78f438 --- /dev/null +++ b/docs/reference/cli.md @@ -0,0 +1,99 @@ +--- +icon: lucide/square-terminal +--- + +# CLI Reference + +Requires the `cli` extra. See [CLI Guide](../guides/cli.md#installation) for installation. + +## Global options + +| Option | Short | Default | Description | +| --------------- | ----- | -------- | ------------------------------------------------------------------------------------------ | +| `--file PATH` | `-f` | `.env` | Location of the `.env` file in the current working directory. | +| `--quote MODE` | `-q` | `always` | Quote mode for values when writing: `always`, `never`, or `auto`. Does not affect parsing. | +| `--export BOOL` | `-e` | `false` | Prepend `export` to entries when writing with `set`. | +| `--version` | | | Show the version and exit. | + +## Commands + +### `dotenv list` + +Display all stored key-value pairs. Output is sorted alphabetically by key. Keys with no value (`None`) are excluded. + +```shell +dotenv list +dotenv list --format=json +``` + +| Option | Values | Default | Description | +| ---------- | ----------------------------------- | -------- | -------------- | +| `--format` | `simple`, `json`, `shell`, `export` | `simple` | Output format. | + +**Formats:** + +- `simple`: `KEY=value` (no quotes) +- `json`: JSON object with indentation, sorted by key +- `shell`: Shell-escaped values (uses `shlex.quote`) +- `export`: Prefixed with `export`, shell-escaped values + +### `dotenv set` + +Store a key-value pair. Creates the file if it doesn't exist. + +```shell +dotenv set KEY value +``` + +Respects the global `--quote` and `--export` options. Does not follow symlinks. + +**Exit code 1** on failure. + +### `dotenv get` + +Retrieve the value for a key. + +```shell +dotenv get KEY +``` + +**Exit code 1** if the key is not found or has no value. + +### `dotenv unset` + +Remove a key from the `.env` file. + +```shell +dotenv unset KEY +``` + +Respects the global `--quote` option. Does not follow symlinks. + +**Exit code 1** on failure. + +### `dotenv run` + +Run a command with the `.env` file loaded into the environment. + +```shell +dotenv run -- python app.py +dotenv run --no-override -- python app.py +``` + +| Option | Default | Description | +| ------------------------------ | ------------ | -------------------------------------------------------------- | +| `--override` / `--no-override` | `--override` | Whether `.env` values override existing environment variables. | + +The `.env` file must exist. The command fails with an error if it does not. + +**Exit code 1** if no command is given. + +On Unix, `dotenv run` replaces the current process with `os.execvpe`. On Windows, it spawns a subprocess. + +## Exit codes + +| Code | Meaning | +| ---- | ----------------------------------------------------------------------------------------------------------- | +| `0` | Success. | +| `1` | Command-specific error: key not found (`get`), write failure (`set`, `unset`), or no command given (`run`). | +| `2` | Could not open the `.env` file. | diff --git a/docs/reference/file-format.md b/docs/reference/file-format.md new file mode 100644 index 00000000..42078a64 --- /dev/null +++ b/docs/reference/file-format.md @@ -0,0 +1,138 @@ +--- +icon: lucide/file-text +--- + +# 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 and values + +Keys can be unquoted or single-quoted. Values can be unquoted, single-quoted, +or double-quoted. Spaces before and after keys, equal signs, and values are +ignored. + +```bash +# Unquoted +USER=foo + +# Single-quoted key and value +'USER'='foo' + +# Double-quoted value +USER="foo" +``` + +Lines can start with the `export` directive, which does not affect their +interpretation: + +```bash +export USER=foo +``` + +## Comments + +Lines starting with `#` are comments: + +```bash +# This is a comment +USER=foo +``` + +Unquoted values can have inline comments after whitespace: + +```bash +USER=foo # this is a comment +``` + +Inline comments are not supported in quoted values. The `#` becomes part of +the value: + +```bash +USER="foo # this is NOT a comment" +``` + +## Escape sequences + +**In single-quoted values:** `\\`, `\'` + +**In double-quoted values:** `\\`, `\'`, `\"`, `\a`, `\b`, `\f`, `\n`, `\r`, `\t`, `\v` + +Unquoted values do not support escape sequences. + +## Multiline values + +Single- or double-quoted values can span multiple lines. The following are +equivalent: + +```bash +FOO="first line +second line" +``` + +```bash +FOO="first line\nsecond line" +``` + +Unquoted values cannot span multiple lines. + +## Variable without a value + +A variable can have no value: + +```bash +FOO +``` + +`dotenv_values` returns `None` for such keys (e.g. `{"FOO": None}`). +`load_dotenv` ignores them. + +This is different from `FOO=`, which sets the variable to an empty string. + +## Variable expansion + +python-dotenv can interpolate variables using POSIX variable expansion. + +Variables must use the `${VAR}` syntax with braces: + +```bash +DOMAIN=example.org +ADMIN_EMAIL=admin@${DOMAIN} +``` + +Bare variables like `$DOMAIN` (without braces) are **not** expanded. + +Default values are supported with `${VAR:-default}`: + +```bash +DATABASE_HOST=${DB_HOST:-localhost} +DATABASE_URL=postgres://${DATABASE_HOST}:5432/mydb +``` + +If `DB_HOST` is not defined in the `.env` file or the environment, `DATABASE_HOST` +resolves to `localhost`. + +### Resolution order + +With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable +is the first of the values defined in the following list: + +1. Value of that variable in the `.env` file. +2. Value of that variable in the environment. +3. Default value, if provided. +4. Empty string. + +With `load_dotenv(override=False)` (the default), the value of a variable is +the first of the values defined in the following list: + +1. Value of that variable in the environment. +2. Value of that variable in the `.env` file. +3. Default value, if provided. +4. Empty string. + +!!! note + + `dotenv_values()` does not have an `override` parameter and always resolves + variables using the `override=True` order above. diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 00000000..ffb9b027 --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,7 @@ +# Reference + +Technical reference for python-dotenv. + +- [**Python API**](api.md): All public functions with their signatures and parameters. +- [**CLI Reference**](cli.md): Complete command-line interface with all options. +- [**File Format**](file-format.md): Specification of the `.env` file syntax. diff --git a/mkdocs.yml b/mkdocs.yml index 3d55d899..c23ccaa1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,30 +1,79 @@ site_name: python-dotenv repo_url: https://github.com/theskumar/python-dotenv -edit_uri: "" theme: - name: material - palette: - primary: green features: + - content.action.edit # Edit this page + - content.action.view # View source of this pageheme: + - content.code.copy - toc.follow - navigation.sections + - navigation.top + - navigation.indexes + - navigation.footer + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: lucide/sun-moon + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: lucide/sun + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: lucide/moon + name: Switch to system preference markdown_extensions: + - admonition + - md_in_html + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true + - pymdownx.superfences - mdx_truly_sane_lists + - pymdownx.snippets: + base_path: ["./"] # Folder(s) where snippets are stored + check_paths: true # Fail build if a snippet is missing + encoding: "utf-8" # Default encoding for reading files plugins: - mkdocstrings: handlers: python: options: + docstring_style: google separate_signature: true show_root_heading: true show_symbol_type_heading: true show_symbol_type_toc: true - search + nav: - Home: index.md + - Getting Started: getting-started.md + - How-to Guides: + - guides/index.md + - Configuration Patterns: guides/configuration.md + - Command line: guides/cli.md + - IPython & Jupyter: guides/ipython.md + - Advanced Usage: guides/advanced.md + - Reference: + - reference/index.md + - Python API: reference/api.md + - CLI Reference: reference/cli.md + - File Format: reference/file-format.md - Changelog: changelog.md - Contributing: contributing.md - - Reference: reference.md - License: license.md diff --git a/requirements-docs.txt b/requirements-docs.txt index b09a710d..b91060d8 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,3 @@ 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 +zensical diff --git a/src/dotenv/__init__.py b/src/dotenv/__init__.py index dde24a01..714fddca 100644 --- a/src/dotenv/__init__.py +++ b/src/dotenv/__init__.py @@ -4,6 +4,7 @@ def load_ipython_extension(ipython: Any) -> None: + """Register the ``%dotenv`` magic command in an IPython session.""" from .ipython import load_ipython_extension load_ipython_extension(ipython) @@ -16,10 +17,10 @@ def get_cli_string( value: Optional[str] = None, quote: Optional[str] = None, ): - """Returns a string suitable for running as a shell script. + """Build a shell command string for invoking the ``dotenv`` CLI. - Useful for converting a arguments passed to a fabric task - to be passed to a `local` or `run` command. + Useful for converting arguments passed to a Fabric task into a string + suitable for ``local`` or ``run``. """ command = ["dotenv"] if quote: diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 47eec047..542b97f2 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -22,10 +22,9 @@ def enumerate_env() -> Optional[str]: - """ - Return a path for the ${pwd}/.env file. + """Return the path to a ``.env`` file in the current working directory. - If pwd does not exist, return None. + Return ``None`` if the working directory no longer exists. """ try: cwd = os.getcwd() @@ -202,23 +201,10 @@ def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> Non 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. + """Replace the current process with *command*, merging *env* into the environment. + On Windows, spawns a subprocess and waits for it instead, because + ``execvpe`` returns immediately on that platform. """ # copy the current environment variables and add the vales from # `env` diff --git a/src/dotenv/ipython.py b/src/dotenv/ipython.py index 4e7edbbf..274e08b4 100644 --- a/src/dotenv/ipython.py +++ b/src/dotenv/ipython.py @@ -10,6 +10,8 @@ @magics_class class IPythonDotEnv(Magics): + """IPython magics for loading ``.env`` files via the ``%dotenv`` command.""" + @magic_arguments() @argument( "-o", @@ -32,6 +34,7 @@ class IPythonDotEnv(Magics): ) @line_magic def dotenv(self, line): + """Load a ``.env`` file into the IPython session's environment.""" args = parse_argstring(self.dotenv, line) # Locate the .env file dotenv_path = args.dotenv_path diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 48e5245a..15ab0168 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -20,9 +20,7 @@ def _load_dotenv_disabled() -> bool: - """ - Determine if dotenv loading has been disabled. - """ + """Return ``True`` if the ``PYTHON_DOTENV_DISABLED`` env var is set to a truthy value.""" if "PYTHON_DOTENV_DISABLED" not in os.environ: return False value = os.environ["PYTHON_DOTENV_DISABLED"].casefold() @@ -30,6 +28,7 @@ def _load_dotenv_disabled() -> bool: def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]: + """Yield each binding, logging a warning for any that failed to parse.""" for mapping in mappings: if mapping.error: logger.warning( @@ -40,6 +39,21 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding class DotEnv: + """Parse a ``.env`` file and expose its key-value pairs. + + Provide the content either as a filesystem path via *dotenv_path* or as an + already-opened text stream via *stream*. Variable interpolation + (``${VAR}`` syntax) is performed by default and can be disabled with + *interpolate*. + + Attributes: + dotenv_path: Path to the ``.env`` file, or ``None`` when using a stream. + encoding: Encoding used to open the file. + interpolate: Whether ``${VAR}`` references in values are resolved. + override: Whether dotenv values take precedence over existing + environment variables during interpolation. + """ + def __init__( self, dotenv_path: Optional[StrPath], @@ -73,7 +87,16 @@ def _get_stream(self) -> Iterator[IO[str]]: yield io.StringIO("") def dict(self) -> Dict[str, Optional[str]]: - """Return dotenv as dict""" + """Return the parsed .env content as an ordered dictionary. + + Results are cached after the first call. If interpolation is enabled, + variable references (e.g. ``${VAR}``) within values are resolved + against previously parsed dotenv values and the system environment. + + Returns: + An ordered dict mapping variable names to their values. + Keys declared without a value will have ``None`` as their value. + """ if self._dict: return self._dict @@ -89,14 +112,17 @@ def dict(self) -> Dict[str, Optional[str]]: return self._dict def parse(self) -> Iterator[Tuple[str, Optional[str]]]: + """Yield ``(key, value)`` pairs parsed from the ``.env`` source.""" 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. + """Set all parsed key-value pairs as environment variables. + + Returns: + ``True`` if at least one variable was present, ``False`` otherwise. """ if not self.dict(): return False @@ -110,7 +136,7 @@ def set_as_environment_variables(self) -> bool: return True def get(self, key: str) -> Optional[str]: - """ """ + """Return the value for *key*, or ``None`` if it is not present.""" data = self.dict() if key in data: @@ -127,10 +153,9 @@ def get_key( key_to_get: str, encoding: Optional[str] = "utf-8", ) -> Optional[str]: - """ - Get the value of a given key from the given .env. + """Return the value of *key_to_get* from the ``.env`` file at *dotenv_path*. - Returns `None` if the key isn't found or doesn't have a value. + Return ``None`` if the key is absent or declared without a value. """ return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) @@ -141,6 +166,25 @@ def rewrite( encoding: Optional[str], follow_symlinks: bool = False, ) -> Iterator[Tuple[IO[str], IO[str]]]: + """Context manager for atomically rewriting a file. + + Yields a ``(source, dest)`` pair of text streams. ``source`` is the + existing file opened for reading (or an empty ``StringIO`` if the file + doesn't exist yet). ``dest`` is a temporary file in the same directory + opened for writing. The caller should read from ``source`` and write the + desired new content to ``dest``. + + On a clean exit the temporary file is moved into place via + ``os.replace()``, preserving the original file's permission bits. If an + exception occurs, the temporary file is removed and the original is left + untouched. + + Parameters: + path: Path to the file to rewrite (created if it doesn't exist). + encoding: Encoding used to open both the source and destination files. + follow_symlinks: If ``True``, resolve symlinks so the real file is + rewritten rather than replacing the symlink itself. + """ if follow_symlinks: path = os.path.realpath(path) @@ -199,14 +243,18 @@ def set_key( encoding: Optional[str] = "utf-8", follow_symlinks: bool = False, ) -> Tuple[Optional[bool], str, str]: - """ - Adds or Updates a key/value to the given .env + """Add or update a key-value pair in the ``.env`` file at *dotenv_path*. - The target .env file is created if it doesn't exist. + Create the file if it does not already exist. Symlinks are **not** + followed by default to avoid writing to an untrusted target; set + *follow_symlinks* to ``True`` to override this. + + Returns: + A ``(True, key, value)`` tuple on success. - 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`. + Raises: + ValueError: If *quote_mode* is not one of ``"always"``, + ``"auto"``, or ``"never"``. """ if quote_mode not in ("always", "auto", "never"): raise ValueError(f"Unknown quote_mode: {quote_mode}") @@ -252,15 +300,14 @@ def unset_key( encoding: Optional[str] = "utf-8", follow_symlinks: bool = False, ) -> Tuple[Optional[bool], str]: - """ - Removes a given key from the given `.env` file. + """Remove *key_to_unset* from the ``.env`` file at *dotenv_path*. - If the .env path given doesn't exist, fails. - If the given key doesn't exist in the .env, fails. + Return ``(None, key)`` and log a warning if the file does not exist or the + key is not found. Symlinks are **not** followed by default; set + *follow_symlinks* to ``True`` to override this. - 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`. + Returns: + ``(True, key)`` on success, ``(None, key)`` on failure. """ if not os.path.exists(dotenv_path): logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path) @@ -290,6 +337,26 @@ def resolve_variables( values: Iterable[Tuple[str, Optional[str]]], override: bool, ) -> Mapping[str, Optional[str]]: + """Resolve variable interpolations in a sequence of key-value pairs. + + Replace variable references (e.g. ``${VAR}`` or ``$VAR``) within values by + looking them up in the already-resolved dotenv values and the system + environment. The *override* flag controls precedence: when ``True``, dotenv + values take priority over existing environment variables; when ``False``, + existing environment variables take priority over dotenv values. + + Parameters: + values: An iterable of ``(key, value)`` pairs as produced by + :meth:`DotEnv.parse`. Values may be ``None`` for keys that were + declared without an assignment. + override: If ``True``, previously parsed dotenv values override system + environment variables during interpolation. If ``False``, system + environment variables take precedence. + + Returns: + An ordered mapping of resolved key-value pairs, preserving the + original iteration order. + """ new_values: Dict[str, Optional[str]] = {} for name, value in values: @@ -299,11 +366,11 @@ def resolve_variables( atoms = parse_variables(value) env: Dict[str, Optional[str]] = {} if override: - env.update(os.environ) # type: ignore + env.update(os.environ) env.update(new_values) else: env.update(new_values) - env.update(os.environ) # type: ignore + env.update(os.environ) result = "".join(atom.resolve(env) for atom in atoms) new_values[name] = result @@ -312,9 +379,7 @@ def resolve_variables( def _walk_to_root(path: str) -> Iterator[str]: - """ - Yield directories starting from the given directory up to the root - """ + """Yield directories starting from *path* up to the filesystem root.""" if not os.path.exists(path): raise IOError("Starting path not found") @@ -334,10 +399,9 @@ def find_dotenv( raise_error_if_not_found: bool = False, usecwd: bool = False, ) -> str: - """ - Search in increasingly higher folders for the given file + """Search for a ``.env`` file by walking up from the caller's directory. - Returns path to the file if found, or an empty string otherwise + Return the absolute path if found, or an empty string otherwise. """ def _is_interactive(): @@ -398,6 +462,7 @@ def load_dotenv( override: Whether to override the system environment variables with the variables from the `.env` file. encoding: Encoding to be used to read the file. + Returns: Bool: True if at least one environment variable is set else False @@ -436,21 +501,21 @@ def dotenv_values( interpolate: bool = True, encoding: Optional[str] = "utf-8", ) -> Dict[str, Optional[str]]: - """ - Parse a .env file and return its content as a dict. + """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}` + Keys declared without a value (e.g. bare ``FOO``) will have ``None`` as + their dict value. - 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. - encoding: Encoding to be used to read the file. + Args: + dotenv_path: Absolute or relative path to the ``.env`` file. + stream: Text stream with ``.env`` content, used if *dotenv_path* is + ``None``. + verbose: Whether to log a warning when the ``.env`` file is missing. + interpolate: Whether to resolve ``${VAR}`` references in values. + encoding: Encoding used to read the file. - If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the - .env file. + If both *dotenv_path* and *stream* are ``None``, :func:`find_dotenv` is + used to locate the ``.env`` file. """ if dotenv_path is None and stream is None: dotenv_path = find_dotenv() @@ -466,9 +531,7 @@ def dotenv_values( def _is_file_or_fifo(path: StrPath) -> bool: - """ - Return True if `path` exists and is either a regular file or a FIFO. - """ + """Return ``True`` if *path* exists and is a regular file or a FIFO.""" if os.path.isfile(path): return True diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py index eb100b47..471132fe 100644 --- a/src/dotenv/parser.py +++ b/src/dotenv/parser.py @@ -33,11 +33,24 @@ def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]: class Original(NamedTuple): + """Raw text of a parsed line and its 1-based line number.""" + string: str line: int class Binding(NamedTuple): + """Parsed result for a single line of a ``.env`` file. + + Attributes: + key: Variable name, or ``None`` for comments, blank lines, and + unparseable lines. + value: Assigned value, or ``None`` when the key has no ``=`` sign + or the line could not be parsed. + original: The raw text and line number this binding was parsed from. + error: ``True`` if the line could not be parsed. + """ + key: Optional[str] value: Optional[str] original: Original @@ -67,6 +80,8 @@ class Error(Exception): class Reader: + """Stateful reader that consumes a ``.env`` stream character by character.""" + def __init__(self, stream: IO[str]) -> None: self.string = stream.read() self.position = Position.start() @@ -103,6 +118,8 @@ def read_regex(self, regex: Pattern[str]) -> Sequence[str]: def decode_escapes(regex: Pattern[str], string: str) -> str: + """Replace escape sequences matched by *regex* with their decoded characters.""" + def decode_match(match: Match[str]) -> str: return codecs.decode(match.group(0), "unicode-escape") # type: ignore @@ -110,6 +127,7 @@ def decode_match(match: Match[str]) -> str: def parse_key(reader: Reader) -> Optional[str]: + """Parse and return the key portion of a binding, or ``None`` for comments.""" char = reader.peek(1) if char == "#": return None @@ -121,11 +139,13 @@ def parse_key(reader: Reader) -> Optional[str]: def parse_unquoted_value(reader: Reader) -> str: + """Parse an unquoted value, stripping inline comments and trailing whitespace.""" (part,) = reader.read_regex(_unquoted_value) return re.sub(r"\s+#.*", "", part).rstrip() def parse_value(reader: Reader) -> str: + """Parse a value that may be single-quoted, double-quoted, or unquoted.""" char = reader.peek(1) if char == "'": (value,) = reader.read_regex(_single_quoted_value) @@ -140,6 +160,7 @@ def parse_value(reader: Reader) -> str: def parse_binding(reader: Reader) -> Binding: + """Parse the next complete binding (key-value pair, comment, or blank line).""" reader.set_mark() try: reader.read_regex(_multiline_whitespace) @@ -177,6 +198,7 @@ def parse_binding(reader: Reader) -> Binding: def parse_stream(stream: IO[str]) -> Iterator[Binding]: + """Yield :class:`Binding` instances for every line in a ``.env`` stream.""" reader = Reader(stream) while reader.has_next(): yield parse_binding(reader) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index 667f2f26..cb9f49ac 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -16,6 +16,8 @@ class Atom(metaclass=ABCMeta): + """Base class for the components of a parsed variable-interpolation string.""" + def __ne__(self, other: object) -> bool: result = self.__eq__(other) if result is NotImplemented: @@ -23,10 +25,14 @@ def __ne__(self, other: object) -> bool: return not result @abstractmethod - def resolve(self, env: Mapping[str, Optional[str]]) -> str: ... + def resolve(self, env: Mapping[str, Optional[str]]) -> str: + """Return the resolved string value using *env* for variable lookups.""" + ... class Literal(Atom): + """Plain text segment that contains no variable references.""" + def __init__(self, value: str) -> None: self.value = value @@ -46,6 +52,8 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str: class Variable(Atom): + """Reference to a variable (``${name}`` or ``${name:-default}``).""" + def __init__(self, name: str, default: Optional[str]) -> None: self.name = name self.default = default @@ -68,6 +76,7 @@ def resolve(self, env: Mapping[str, Optional[str]]) -> str: def parse_variables(value: str) -> Iterator[Atom]: + """Parse *value* into a sequence of :class:`Literal` and :class:`Variable` atoms.""" cursor = 0 for match in _posix_variable.finditer(value): From cf72b8521910a99cdf6ba7e3a2ee4e1072a64608 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Mon, 9 Mar 2026 12:45:23 +0530 Subject: [PATCH 2/2] update --- docs/guides/cli.md | 22 +++++++++++++++++----- src/dotenv/main.py | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/guides/cli.md b/docs/guides/cli.md index c823e36a..47d2a388 100644 --- a/docs/guides/cli.md +++ b/docs/guides/cli.md @@ -11,16 +11,28 @@ terminal. The CLI requires the `click` package. Install it with the `cli` extra: -```shell -pip install "python-dotenv[cli]" -``` +=== "`pip`" + + ```shell + pip install "python-dotenv[cli]" + ``` + +=== "`uv`" -The CLI can also be invoked as a Python module: + ```shell + uv add "python-dotenv[cli]" + ``` + +Verify the installation by checking the version: ```shell -python -m dotenv +dotenv --version ``` +!!! info + + The CLI can also be invoked as a Python module: `python -m dotenv` + ## Set up your .env from the terminal Use `set`, `get`, and `unset` to manage key-value pairs: diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 0683682f..e9feb353 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -511,7 +511,7 @@ def dotenv_values( 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. + interpolate: Whether ``${VAR}`` references in values are resolved. encoding: Encoding to be used to read the file. If both *dotenv_path* and *stream* are ``None``, :func:`find_dotenv` is