[
  {
    "path": ".bumpversion.cfg",
    "content": "[bumpversion]\ncurrent_version = 1.2.2\ncommit = True\ntag = True\n\n[bumpversion:file:src/dotenv/version.py]\n"
  },
  {
    "path": ".editorconfig",
    "content": "# see: http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.{py,rst,ini}]\nindent_style = space\nindent_size = 4\n\n[*.yml]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\n| Version   | Supported          |\n| --------- | ------------------ |\n| latest    | :white_check_mark: |\n| 0.x       | :x:                |\n\n## Reporting a Vulnerability\n\nIf you believe you have identified a security issue with python-dotenv, please email\npython-dotenv@saurabh-kumar.com. A maintainer will contact you acknowledging the report\nand how to continue.\n\nBe sure to include as much detail as necessary in your report. As with reporting normal\nissues, a minimal reproducible example will help the maintainers address the issue faster.\nIf you are able, you may also include a fix for the issue generated with `git format-patch`.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Keep GitHub Actions up to date with GitHub's Dependabot...\n# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot\n# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem\nversion: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    groups:\n      github-actions:\n        patterns:\n          - \"*\"  # Group all Actions updates into a single larger pull request\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Upload Python Package\n\non:\n  release:\n    types: [created]\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    # Specifying a GitHub environment is optional, but strongly encouraged\n    environment: release\n    permissions:\n      # IMPORTANT: this permission is mandatory for trusted publishing\n      id-token: write\n      # Required for pushing to gh-pages branch\n      contents: write\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: \"3.x\"\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install build\n\n      - name: Build package distributions\n        run: make sdist\n\n      - name: Publish package distributions to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n\n      - name: Build Documentation\n        run: |\n          pip install -r requirements-docs.txt\n          pip install -e .\n          mkdocs build\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./site\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Run Tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 15\n\n    strategy:\n      fail-fast: false\n      max-parallel: 8\n      matrix:\n        os:\n          - ubuntu-latest\n        python-version:\n          [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\", \"3.14t\", pypy3.11]\n        include:\n          # Windows: Test lowest and highest supported Python versions\n          - os: windows-latest\n            python-version: \"3.10\"\n          - os: windows-latest\n            python-version: \"3.14\"\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n          allow-prereleases: true\n\n      - name: Upgrade pip\n        run: python -m pip install --upgrade pip\n\n      - name: Install dependencies\n        run: pip install tox tox-gh-actions\n\n      - name: Test with tox\n        run: tox\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.idea\n.vscode/\n\n# Created by https://www.gitignore.io/api/python\n# Edit at https://www.gitignore.io/?templates=python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n### Python Patch ###\n.venv/\n\n# End of https://www.gitignore.io/api/python\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.12.0\n    hooks:\n      # Run the linter.\n      - id: ruff\n      # Run the formatter.\n      - id: ruff-format\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this\nproject adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n- ...\n\n## [1.2.2] - 2026-03-01\n\n### Added\n\n- Support for Python 3.14, including the free-threaded (3.14t) build. (#588)\n\n### Changed\n\n- The `dotenv run` command now forwards flags directly to the specified command by [@bbc2] in [#607]\n- Improved documentation clarity regarding override behavior and the reference page.\n- Updated PyPy support to version 3.11.\n- Documentation for FIFO file support.\n- Dropped Support for Python 3.9.\n\n### Fixed\n\n- Improved `set_key` and `unset_key` behavior when interacting with symlinks by [@bbc2] in [790c5c0]\n- Corrected the license specifier and added missing Python 3.14 classifiers in package metadata by [@JYOuyang] in [#590]\n\n### Breaking Changes\n\n- `dotenv.set_key` and `dotenv.unset_key` used to follow symlinks in some\n  situations. This is no longer the case. For that behavior to be restored in\n  all cases, `follow_symlinks=True` should be used.\n\n- In the CLI, `set` and `unset` used to follow symlinks in some situations. This\n  is no longer the case.\n\n- `dotenv.set_key`, `dotenv.unset_key` and the CLI commands `set` and `unset`\n  used to reset the file mode of the modified .env file to `0o600` in some\n  situations. This is no longer the case: The original mode of the file is now\n  preserved. Is the file needed to be created or wasn't a regular file, mode\n  `0o600` is used.\n\n## [1.2.1] - 2025-10-26\n\n- Move more config to `pyproject.toml`, removed `setup.cfg`\n- Add support for reading `.env` from FIFOs (Unix) by [@sidharth-sudhir] in [#586]\n\n## [1.2.0] - 2025-10-26\n\n- Upgrade build system to use PEP 517 & PEP 518 to use `build` and `pyproject.toml` by [@EpicWink] in [#583]\n- Add support for Python 3.14 by [@23f3001135] in [#579]\n- Add support for disabling of `load_dotenv()` using `PYTHON_DOTENV_DISABLED` env var. by [@matthewfranglen] in [#569]\n\n## [1.1.1] - 2025-06-24\n\n### Fixed\n\n- CLI: Ensure `find_dotenv` work reliably on python 3.13 by [@theskumar] in [#563]\n- CLI: revert the use of execvpe on Windows by [@wrongontheinternet] in [#566]\n\n## [1.1.0] - 2025-03-25\n\n### Added\n\n- Add support for python 3.13\n- Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt]\n\n### Fixed\n\n- `find_dotenv` and `load_dotenv` now correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42])\n\n### Misc\n\n- Drop support for Python 3.8\n\n## [1.0.1] - 2024-01-23\n\n### Fixed\n\n- Gracefully handle code which has been imported from a zipfile ([#456] by [@samwyma])\n- Allow modules using `load_dotenv` to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton])\n- Fix file not closed after deletion, handle error in the rewrite function ([#469] by [@Qwerty-133])\n\n### Misc\n\n- Use pathlib.Path in tests ([#466] by [@eumiro])\n- Fix year in release date in changelog.md ([#454] by [@jankislinger])\n- Use https in README links ([#474] by [@Nicals])\n\n## [1.0.0] - 2023-02-24\n\n### Fixed\n\n- Drop support for python 3.7, add python 3.12-dev (#449 by [@theskumar])\n- Handle situations where the cwd does not exist. (#446 by [@jctanner])\n\n## [0.21.1] - 2023-01-21\n\n### Added\n\n- Use Python 3.11 non-beta in CI (#438 by [@bbc2])\n- Modernize variables code (#434 by [@Nougat-Waffle])\n- Modernize main.py and parser.py code (#435 by [@Nougat-Waffle])\n- Improve conciseness of cli.py and **init**.py (#439 by [@Nougat-Waffle])\n- Improve error message for `get` and `list` commands when env file can't be opened (#441 by [@bbc2])\n- Updated License to align with BSD OSI template (#433 by [@lsmith77])\n\n### Fixed\n\n- Fix Out-of-scope error when \"dest\" variable is undefined (#413 by [@theGOTOguy])\n- Fix IPython test warning about deprecated `magic` (#440 by [@bbc2])\n- Fix type hint for dotenv_path var, add StrPath alias (#432 by [@eaf])\n\n## [0.21.0] - 2022-09-03\n\n### Added\n\n- CLI: add support for invocations via 'python -m'. (#395 by [@theskumar])\n- `load_dotenv` function now returns `False`. (#388 by [@larsks])\n- CLI: add --format= option to list command. (#407 by [@sammck])\n\n### Fixed\n\n- Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants])\n- Use `open` instead of `io.open`. (#389 by [@rabinadk1])\n- Improve documentation for variables without a value (#390 by [@bbc2])\n- Add `parse_it` to Related Projects (#410 by [@naorlivne])\n- Update README.md (#415 by [@harveer07])\n- Improve documentation with direct use of MkDocs (#398 by [@bbc2])\n\n## [0.20.0] - 2022-03-24\n\n### Added\n\n- Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`.\n  (#379 by [@bbc2])\n\n### Fixed\n\n- Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by\n  [@mgorny]).\n- Don't build universal wheels (#387 by [@bbc2]).\n\n## [0.19.2] - 2021-11-11\n\n### Fixed\n\n- In `set_key`, add missing newline character before new entry if necessary. (#361 by\n  [@bbc2])\n\n## [0.19.1] - 2021-08-09\n\n### Added\n\n- Add support for Python 3.10. (#359 by [@theskumar])\n\n## [0.19.0] - 2021-07-24\n\n### Changed\n\n- Require Python 3.5 or a later version. Python 2 and 3.4 are no longer supported. (#341\n  by [@bbc2]).\n\n### Added\n\n- The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str,\nos.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]).\n- The `stream` argument of `load_dotenv` and `dotenv_values` can now be a text stream\n  (`IO[str]`), which includes values like `io.StringIO(\"foo\")` and `open(\"file.env\",\n\"r\")` (#348 by [@bbc2]).\n\n## [0.18.0] - 2021-06-20\n\n### Changed\n\n- Raise `ValueError` if `quote_mode` isn't one of `always`, `auto` or `never` in\n  `set_key` (#330 by [@bbc2]).\n- When writing a value to a .env file with `set_key` or `dotenv set <key> <value>` (#330\n  by [@bbc2]):\n  - Use single quotes instead of double quotes.\n  - Don't strip surrounding quotes.\n  - In `auto` mode, don't add quotes if the value is only made of alphanumeric characters\n    (as determined by `string.isalnum`).\n\n## [0.17.1] - 2021-04-29\n\n### Fixed\n\n- Fixed tests for build environments relying on `PYTHONPATH` (#318 by [@befeleme]).\n\n## [0.17.0] - 2021-04-02\n\n### Changed\n\n- Make `dotenv get <key>` only show the value, not `key=value` (#313 by [@bbc2]).\n\n### Added\n\n- Add `--override`/`--no-override` option to `dotenv run` (#312 by [@zueve] and [@bbc2]).\n\n## [0.16.0] - 2021-03-27\n\n### Changed\n\n- The default value of the `encoding` parameter for `load_dotenv` and `dotenv_values` is\n  now `\"utf-8\"` instead of `None` (#306 by [@bbc2]).\n- Fix resolution order in variable expansion with `override=False` (#287 by [@bbc2]).\n\n## [0.15.0] - 2020-10-28\n\n### Added\n\n- Add `--export` option to `set` to make it prepend the binding with `export` (#270 by\n  [@jadutter]).\n\n### Changed\n\n- Make `set` command create the `.env` file in the current directory if no `.env` file was\n  found (#270 by [@jadutter]).\n\n### Fixed\n\n- Fix potentially empty expanded value for duplicate key (#260 by [@bbc2]).\n- Fix import error on Python 3.5.0 and 3.5.1 (#267 by [@gongqingkui]).\n- Fix parsing of unquoted values containing several adjacent space or tab characters\n  (#277 by [@bbc2], review by [@x-yuri]).\n\n## [0.14.0] - 2020-07-03\n\n### Changed\n\n- Privilege definition in file over the environment in variable expansion (#256 by\n  [@elbehery95]).\n\n### Fixed\n\n- Improve error message for when file isn't found (#245 by [@snobu]).\n- Use HTTPS URL in package meta data (#251 by [@ekohl]).\n\n## [0.13.0] - 2020-04-16\n\n### Added\n\n- Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]).\n\n## [0.12.0] - 2020-02-28\n\n### Changed\n\n- Use current working directory to find `.env` when bundled by PyInstaller (#213 by\n  [@gergelyk]).\n\n### Fixed\n\n- Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]).\n- Fix `dotenv run` crashing on environment variables without values (#237 by [@yannham]).\n- Remove warning when last line is empty (#238 by [@bbc2]).\n\n## [0.11.0] - 2020-02-07\n\n### Added\n\n- Add `interpolate` argument to `load_dotenv` and `dotenv_values` to disable interpolation\n  (#232 by [@ulyssessouza]).\n\n### Changed\n\n- Use logging instead of warnings (#231 by [@bbc2]).\n\n### Fixed\n\n- Fix installation in non-UTF-8 environments (#225 by [@altendky]).\n- Fix PyPI classifiers (#228 by [@bbc2]).\n\n## [0.10.5] - 2020-01-19\n\n### Fixed\n\n- Fix handling of malformed lines and lines without a value (#222 by [@bbc2]):\n  - Don't print warning when key has no value.\n  - Reject more malformed lines (e.g. \"A: B\", \"a='b',c\").\n- Fix handling of lines with just a comment (#224 by [@bbc2]).\n\n## [0.10.4] - 2020-01-17\n\n### Added\n\n- Make typing optional (#179 by [@techalchemy]).\n- Print a warning on malformed line (#211 by [@bbc2]).\n- Support keys without a value (#220 by [@ulyssessouza]).\n\n## 0.10.3\n\n- Improve interactive mode detection ([@andrewsmith])([#183]).\n- Refactor parser to fix parsing inconsistencies ([@bbc2])([#170]).\n  - Interpret escapes as control characters only in double-quoted strings.\n  - Interpret `#` as start of comment only if preceded by whitespace.\n\n## 0.10.2\n\n- Add type hints and expose them to users ([@qnighy])([#172])\n- `load_dotenv` and `dotenv_values` now accept an `encoding` parameter, defaults to `None`\n  ([@theskumar])([@earlbread])([#161])\n- Fix `str`/`unicode` inconsistency in Python 2: values are always `str` now. ([@bbc2])([#121])\n- Fix Unicode error in Python 2, introduced in 0.10.0. ([@bbc2])([#176])\n\n## 0.10.1\n\n- Fix parsing of variable without a value ([@asyncee])([@bbc2])([#158])\n\n## 0.10.0\n\n- Add support for UTF-8 in unquoted values ([@bbc2])([#148])\n- Add support for trailing comments ([@bbc2])([#148])\n- Add backslashes support in values ([@bbc2])([#148])\n- Add support for newlines in values ([@bbc2])([#148])\n- Force environment variables to str with Python2 on Windows ([@greyli])\n- Drop Python 3.3 support ([@greyli])\n- Fix stderr/-out/-in redirection ([@venthur])\n\n## 0.9.0\n\n- Add `--version` parameter to cli ([@venthur])\n- Enable loading from current directory ([@cjauvin])\n- Add 'dotenv run' command for calling arbitrary shell script with .env ([@venthur])\n\n## 0.8.1\n\n- Add tests for docs ([@Flimm])\n- Make 'cli' support optional. Use `pip install python-dotenv[cli]`. ([@theskumar])\n\n## 0.8.0\n\n- `set_key` and `unset_key` only modified the affected file instead of\n  parsing and re-writing file, this causes comments and other file\n  entact as it is.\n- Add support for `export` prefix in the line.\n- Internal refractoring ([@theskumar])\n- Allow `load_dotenv` and `dotenv_values` to work with `StringIO())` ([@alanjds])([@theskumar])([#78])\n\n## 0.7.1\n\n- Remove hard dependency on iPython ([@theskumar])\n\n## 0.7.0\n\n- Add support to override system environment variable via .env.\n  ([@milonimrod](https://github.com/milonimrod))\n  ([\\#63](https://github.com/theskumar/python-dotenv/issues/63))\n- Disable \".env not found\" warning by default\n  ([@maxkoryukov](https://github.com/maxkoryukov))\n  ([\\#57](https://github.com/theskumar/python-dotenv/issues/57))\n\n## 0.6.5\n\n- Add support for special characters `\\`.\n  ([@pjona](https://github.com/pjona))\n  ([\\#60](https://github.com/theskumar/python-dotenv/issues/60))\n\n## 0.6.4\n\n- Fix issue with single quotes ([@Flimm])\n  ([\\#52](https://github.com/theskumar/python-dotenv/issues/52))\n\n## 0.6.3\n\n- Handle unicode exception in setup.py\n  ([\\#46](https://github.com/theskumar/python-dotenv/issues/46))\n\n## 0.6.2\n\n- Fix dotenv list command ([@ticosax](https://github.com/ticosax))\n- Add iPython Support\n  ([@tillahoffmann](https://github.com/tillahoffmann))\n\n## 0.6.0\n\n- Drop support for Python 2.6\n- Handle escaped characters and newlines in quoted values. (Thanks\n  [@iameugenejo](https://github.com/iameugenejo))\n- Remove any spaces around unquoted key/value. (Thanks\n  [@paulochf](https://github.com/paulochf))\n- Added POSIX variable expansion. (Thanks\n  [@hugochinchilla](https://github.com/hugochinchilla))\n\n## 0.5.1\n\n- Fix `find_dotenv` - it now start search from the file where this\n  function is called from.\n\n## 0.5.0\n\n- Add `find_dotenv` method that will try to find a `.env` file.\n  (Thanks [@isms](https://github.com/isms))\n\n## 0.4.0\n\n- cli: Added `-q/--quote` option to control the behaviour of quotes\n  around values in `.env`. (Thanks\n  [@hugochinchilla](https://github.com/hugochinchilla)).\n- Improved test coverage.\n\n<!-- PR LINKS -->\n\n[#78]: https://github.com/theskumar/python-dotenv/issues/78\n[#121]: https://github.com/theskumar/python-dotenv/issues/121\n[#148]: https://github.com/theskumar/python-dotenv/issues/148\n[#158]: https://github.com/theskumar/python-dotenv/issues/158\n[#170]: https://github.com/theskumar/python-dotenv/issues/170\n[#172]: https://github.com/theskumar/python-dotenv/issues/172\n[#176]: https://github.com/theskumar/python-dotenv/issues/176\n[#183]: https://github.com/theskumar/python-dotenv/issues/183\n[#359]: https://github.com/theskumar/python-dotenv/issues/359\n[#469]: https://github.com/theskumar/python-dotenv/issues/469\n[#456]: https://github.com/theskumar/python-dotenv/issues/456\n[#466]: https://github.com/theskumar/python-dotenv/issues/466\n[#454]: https://github.com/theskumar/python-dotenv/issues/454\n[#474]: https://github.com/theskumar/python-dotenv/issues/474\n[#523]: https://github.com/theskumar/python-dotenv/issues/523\n[#553]: https://github.com/theskumar/python-dotenv/issues/553\n[#569]: https://github.com/theskumar/python-dotenv/issues/569\n[#583]: https://github.com/theskumar/python-dotenv/issues/583\n[#586]: https://github.com/theskumar/python-dotenv/issues/586\n[#590]: https://github.com/theskumar/python-dotenv/issues/590\n[#607]: https://github.com/theskumar/python-dotenv/issues/607\n[#588]: https://github.com/theskumar/python-dotenv/issues/588\n[#579]: https://github.com/theskumar/python-dotenv/pull/579\n[#566]: https://github.com/theskumar/python-dotenv/pull/566\n[#563]: https://github.com/theskumar/python-dotenv/pull/563\n[#497]: https://github.com/theskumar/python-dotenv/pull/497\n[#161]: https://github.com/theskumar/python-dotenv/issues/161\n[790c5c0]: https://github.com/theskumar/python-dotenv/commit/790c5c02991100aa1bf41ee5330aca75edc51311\n\n<!-- contributors -->\n\n[@23f3001135]: https://github.com/23f3001135\n[@EpicWink]: https://github.com/EpicWink\n[@Flimm]: https://github.com/Flimm\n[@Nicals]: https://github.com/Nicals\n[@Nougat-Waffle]: https://github.com/Nougat-Waffle\n[@Qwerty-133]: https://github.com/Qwerty-133\n[@alanjds]: https://github.com/alanjds\n[@altendky]: https://github.com/altendky\n[@andrewsmith]: https://github.com/andrewsmith\n[@asyncee]: https://github.com/asyncee\n[@bbc2]: https://github.com/bbc2\n[@befeleme]: https://github.com/befeleme\n[@cjauvin]: https://github.com/cjauvin\n[@eaf]: https://github.com/eaf\n[@earlbread]: https://github.com/earlbread\n[@eekstunt]: https://github.com/eekstunt\n[@eggplants]: https://github.com/eggplants\n[@ekohl]: https://github.com/ekohl\n[@elbehery95]: https://github.com/elbehery95\n[@eumiro]: https://github.com/eumiro\n[@freddyaboulton]: https://github.com/freddyaboulton\n[@gergelyk]: https://github.com/gergelyk\n[@gongqingkui]: https://github.com/gongqingkui\n[@greyli]: https://github.com/greyli\n[@harveer07]: https://github.com/harveer07\n[@jadutter]: https://github.com/jadutter\n[@jankislinger]: https://github.com/jankislinger\n[@jctanner]: https://github.com/jctanner\n[@larsks]: https://github.com/larsks\n[@lsmith77]: https://github.com/lsmith77\n[@matthewfranglen]: https://github.com/matthewfranglen\n[@mgorny]: https://github.com/mgorny\n[@naorlivne]: https://github.com/naorlivne\n[@qnighy]: https://github.com/qnighy\n[@rabinadk1]: https://github.com/rabinadk1\n[@randomseed42]: https://github.com/randomseed42\n[@sammck]: https://github.com/sammck\n[@samwyma]: https://github.com/samwyma\n[@sidharth-sudhir]: https://github.com/sidharth-sudhir\n[@snobu]: https://github.com/snobu\n[@techalchemy]: https://github.com/techalchemy\n[@theGOTOguy]: https://github.com/theGOTOguy\n[@theskumar]: https://github.com/theskumar\n[@ulyssessouza]: https://github.com/ulyssessouza\n[@venthur]: https://github.com/venthur\n[@wrongontheinternet]: https://github.com/wrongontheinternet\n[@x-yuri]: https://github.com/x-yuri\n[@yannham]: https://github.com/yannham\n[@zueve]: https://github.com/zueve\n[@JYOuyang]: https://github.com/JYOuyang\n[@burnout-projects]: https://github.com/burnout-projects\n[@cpackham-atlnz]: https://github.com/cpackham-atlnz\n[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.2.2...HEAD\n[1.2.2]: https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2\n[1.2.1]: https://github.com/theskumar/python-dotenv/compare/v1.2.0...v1.2.1\n[1.2.0]: https://github.com/theskumar/python-dotenv/compare/v1.1.1...v1.2.0\n[1.1.1]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...v1.1.1\n[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0\n[1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1\n[1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0\n[0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1\n[0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0\n[0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0\n[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2\n[0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1\n[0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0\n[0.18.0]: https://github.com/theskumar/python-dotenv/compare/v0.17.1...v0.18.0\n[0.17.1]: https://github.com/theskumar/python-dotenv/compare/v0.17.0...v0.17.1\n[0.17.0]: https://github.com/theskumar/python-dotenv/compare/v0.16.0...v0.17.0\n[0.16.0]: https://github.com/theskumar/python-dotenv/compare/v0.15.0...v0.16.0\n[0.15.0]: https://github.com/theskumar/python-dotenv/compare/v0.14.0...v0.15.0\n[0.14.0]: https://github.com/theskumar/python-dotenv/compare/v0.13.0...v0.14.0\n[0.13.0]: https://github.com/theskumar/python-dotenv/compare/v0.12.0...v0.13.0\n[0.12.0]: https://github.com/theskumar/python-dotenv/compare/v0.11.0...v0.12.0\n[0.11.0]: https://github.com/theskumar/python-dotenv/compare/v0.10.5...v0.11.0\n[0.10.5]: https://github.com/theskumar/python-dotenv/compare/v0.10.4...v0.10.5\n[0.10.4]: https://github.com/theskumar/python-dotenv/compare/v0.10.3...v0.10.4\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Contributing\n============\n\nAll the contributions are welcome! Please open [an\nissue](https://github.com/theskumar/python-dotenv/issues/new) or send us\na pull request.\n\nExecuting the tests:\n\n    $ uv venv\n    $ uv pip install -r requirements.txt\n    $ uv pip install -e .\n    $ uv ruff check .\n    $ uv format .\n    $ uv run pytest\n\nor with [tox](https://pypi.org/project/tox/) installed:\n\n    $ tox\n\n\nUse of pre-commit is recommended:\n\n    $ uv run precommit install\n\n\nDocumentation is published with [mkdocs]():\n\n```shell\n$ uv pip install -r requirements-docs.txt\n$ uv pip install -e .\n$ uv run mkdocs serve\n```\n\nOpen http://127.0.0.1:8000/ to view the documentation locally.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv)\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n- Redistributions of source code must retain the above copyright notice,\n  this list of conditions and the following disclaimer.\n\n- Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n- Neither the name of django-dotenv nor the names of its contributors\n  may be used to endorse or promote products derived from this software\n  without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE *.md *.yml *.yaml *.toml\n\ninclude tox.ini\nrecursive-include docs *.md\nrecursive-include tests *.py\n\ninclude .bumpversion.cfg\ninclude .coveragerc\ninclude .editorconfig\ninclude Makefile\ninclude requirements.txt\ninclude requirements-docs.txt\ninclude src/dotenv/py.typed\n"
  },
  {
    "path": "Makefile",
    "content": ".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\trm -rf .tox/\n\trm -rf site/\n\trm -fr dist/\n\trm -fr src/*.egg-info\n\nclean-pyc:\n\tfind . -name '*.pyc' -exec rm -f {} +\n\tfind . -name '*.pyo' -exec rm -f {} +\n\tfind . -name '*~' -exec rm -f {} +\n\nsdist: clean\n\tpython -m build -o dist .\n\tls -l dist\n\ntest:\n\tuv pip install -e .\n\truff check .\n\tpytest tests/\n\nfmt:\n\truff format src tests\n\ncoverage:\n\tcoverage run --source=dotenv --omit='*tests*' -m py.test tests/ -v --tb=native\n\tcoverage report\n\ncoverage-html: coverage\n\tcoverage html\n"
  },
  {
    "path": "README.md",
    "content": "# python-dotenv\n\n[![Build Status][build_status_badge]][build_status_link]\n[![PyPI version][pypi_badge]][pypi_link]\n\npython-dotenv reads key-value pairs from a `.env` file and can set them as\nenvironment variables. It helps in the development of applications following the\n[12-factor](https://12factor.net/) principles.\n\n- [Getting Started](#getting-started)\n- [Other Use Cases](#other-use-cases)\n  - [Load configuration without altering the environment](#load-configuration-without-altering-the-environment)\n  - [Parse configuration as a stream](#parse-configuration-as-a-stream)\n  - [Load .env files in IPython](#load-env-files-in-ipython)\n- [Command-line Interface](#command-line-interface)\n- [File format](#file-format)\n  - [Multiline values](#multiline-values)\n  - [Variable expansion](#variable-expansion)\n- [Related Projects](#related-projects)\n- [Acknowledgements](#acknowledgements)\n\n## Getting Started\n\n```shell\npip install python-dotenv\n```\n\nIf your application takes its configuration from environment variables, like a\n12-factor application, launching it in development is not very practical because\nyou have to set those environment variables yourself.\n\nTo help you with that, you can add python-dotenv to your application to make it\nload the configuration from a `.env` file when it is present (e.g. in\ndevelopment) while remaining configurable via the environment:\n\n```python\nfrom dotenv import load_dotenv\n\nload_dotenv()  # reads variables from a .env file and sets them in os.environ\n\n# Code of your application, which uses environment variables (e.g. from `os.environ` or\n# `os.getenv`) as if they came from the actual environment.\n```\n\nBy default, `load_dotenv()` will:\n\n- Look for a `.env` file in the same directory as the Python script (or higher up the directory tree).\n- Read each key-value pair and add it to `os.environ`.\n- **Not override** existing environment variables (`override=False`). Pass `override=True` to override existing variables.\n\nTo configure the development environment, add a `.env` in the root directory of\nyour project:\n\n```\n.\n├── .env\n└── foo.py\n```\n\nThe syntax of `.env` files supported by python-dotenv is similar to that of\nBash:\n\n```bash\n# Development settings\nDOMAIN=example.org\nADMIN_EMAIL=admin@${DOMAIN}\nROOT_URL=${DOMAIN}/app\n```\n\nIf you use variables in values, ensure they are surrounded with `{` and `}`,\nlike `${DOMAIN}`, as bare variables such as `$DOMAIN` are not expanded.\n\nYou will probably want to add `.env` to your `.gitignore`, especially if it\ncontains secrets like a password.\n\nSee the section \"[File format](#file-format)\" below for more information about what you can write in a `.env` file.\n\n## Other Use Cases\n\n### Load configuration without altering the environment\n\nThe function `dotenv_values` works more or less the same way as `load_dotenv`,\nexcept it doesn't touch the environment, it just returns a `dict` with the\nvalues parsed from the `.env` file.\n\n```python\nfrom dotenv import dotenv_values\n\nconfig = dotenv_values(\".env\")  # config = {\"USER\": \"foo\", \"EMAIL\": \"foo@example.org\"}\n```\n\nThis notably enables advanced configuration management:\n\n```python\nimport os\nfrom dotenv import dotenv_values\n\nconfig = {\n    **dotenv_values(\".env.shared\"),  # load shared development variables\n    **dotenv_values(\".env.secret\"),  # load sensitive variables\n    **os.environ,  # override loaded values with environment variables\n}\n```\n\n### Parse configuration as a stream\n\n`load_dotenv` and `dotenv_values` accept [streams][python_streams] via their\n`stream` argument. It is thus possible to load the variables from sources other\nthan the filesystem (e.g. the network).\n\n```python\nfrom io import StringIO\n\nfrom dotenv import load_dotenv\n\nconfig = StringIO(\"USER=foo\\nEMAIL=foo@example.org\")\nload_dotenv(stream=config)\n```\n\n### Load .env files in IPython\n\nYou can use dotenv in IPython. By default, it will use `find_dotenv` to search for a\n`.env` file:\n\n```python\n%load_ext dotenv\n%dotenv\n```\n\nYou can also specify a path:\n\n```python\n%dotenv relative/or/absolute/path/to/.env\n```\n\nOptional flags:\n\n- `-o` to override existing variables.\n- `-v` for increased verbosity.\n\n### Disable load_dotenv\n\nSet `PYTHON_DOTENV_DISABLED=1` to disable `load_dotenv()` from loading .env\nfiles or streams. Useful when you can't modify third-party package calls or in\nproduction.\n\n## Command-line Interface\n\nA CLI interface `dotenv` is also included, which helps you manipulate the `.env`\nfile without manually opening it.\n\n```shell\n$ pip install \"python-dotenv[cli]\"\n$ dotenv set USER foo\n$ dotenv set EMAIL foo@example.org\n$ dotenv list\nUSER=foo\nEMAIL=foo@example.org\n$ dotenv list --format=json\n{\n  \"USER\": \"foo\",\n  \"EMAIL\": \"foo@example.org\"\n}\n$ dotenv run -- python foo.py\n```\n\nRun `dotenv --help` for more information about the options and subcommands.\n\n## File format\n\nThe format is not formally specified and still improves over time. That being\nsaid, `.env` files should mostly look like Bash files. Reading from FIFOs (named\npipes) on Unix systems is also supported.\n\nKeys can be unquoted or single-quoted. Values can be unquoted, single- or\ndouble-quoted. Spaces before and after keys, equal signs, and values are\nignored. Values can be followed by a comment. Lines can start with the `export`\ndirective, which does not affect their interpretation.\n\nAllowed escape sequences:\n\n- in single-quoted values: `\\\\`, `\\'`\n- in double-quoted values: `\\\\`, `\\'`, `\\\"`, `\\a`, `\\b`, `\\f`, `\\n`, `\\r`, `\\t`, `\\v`\n\n### Multiline values\n\nIt is possible for single- or double-quoted values to span multiple lines. The\nfollowing examples are equivalent:\n\n```bash\nFOO=\"first line\nsecond line\"\n```\n\n```bash\nFOO=\"first line\\nsecond line\"\n```\n\n### Variable without a value\n\nA variable can have no value:\n\n```bash\nFOO\n```\n\nIt results in `dotenv_values` associating that variable name with the value\n`None` (e.g. `{\"FOO\": None}`. `load_dotenv`, on the other hand, simply ignores\nsuch variables.\n\nThis shouldn't be confused with `FOO=`, in which case the variable is associated\nwith the empty string.\n\n### Variable expansion\n\npython-dotenv can interpolate variables using POSIX variable expansion.\n\nWith `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable\nis the first of the values defined in the following list:\n\n- Value of that variable in the `.env` file.\n- Value of that variable in the environment.\n- Default value, if provided.\n- Empty string.\n\nWith `load_dotenv(override=False)`, the value of a variable is the first of the\nvalues defined in the following list:\n\n- Value of that variable in the environment.\n- Value of that variable in the `.env` file.\n- Default value, if provided.\n- Empty string.\n\n## Related Projects\n\n- [environs](https://github.com/sloria/environs)\n- [Honcho](https://github.com/nickstenning/honcho)\n- [dump-env](https://github.com/sobolevn/dump-env)\n- [dynaconf](https://github.com/dynaconf/dynaconf)\n- [parse_it](https://github.com/naorlivne/parse_it)\n- [django-dotenv](https://github.com/jpadilla/django-dotenv)\n- [django-environ](https://github.com/joke2k/django-environ)\n- [python-decouple](https://github.com/HBNetwork/python-decouple)\n- [django-configuration](https://github.com/jezdez/django-configurations)\n\n## Acknowledgements\n\nThis project is currently maintained by [Saurabh Kumar][saurabh-homepage] and\n[Bertrand Bonnefoy-Claudet][gh-bbc2] and would not have been possible without\nthe support of these [awesome people][contributors].\n\n[gh-bbc2]: https://github.com/bbc2\n[saurabh-homepage]: https://saurabh-kumar.com\n[pypi_link]: https://badge.fury.io/py/python-dotenv\n[pypi_badge]: https://badge.fury.io/py/python-dotenv.svg\n[python_streams]: https://docs.python.org/3/library/io.html\n[contributors]: https://github.com/theskumar/python-dotenv/graphs/contributors\n[build_status_link]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml\n[build_status_badge]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml/badge.svg\n"
  },
  {
    "path": "docs/reference.md",
    "content": "# ::: dotenv\n\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: python-dotenv\nrepo_url: https://github.com/theskumar/python-dotenv\nedit_uri: \"\"\ntheme:\n  name: material\n  palette:\n    primary: green\n  features:\n    - toc.follow\n    - navigation.sections\n\nmarkdown_extensions:\n  - mdx_truly_sane_lists\n\nplugins:\n  - mkdocstrings:\n      handlers:\n        python:\n          options:\n            separate_signature: true\n            show_root_heading: true\n            show_symbol_type_heading: true\n            show_symbol_type_toc: true\n  - search\nnav:\n  - Home: index.md\n  - Changelog: changelog.md\n  - Contributing: contributing.md\n  - Reference: reference.md\n  - License: license.md\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools >= 77.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"python-dotenv\"\ndescription = \"Read key-value pairs from a .env file and set them as environment variables\"\nauthors = [\n    {name = \"Saurabh Kumar\", email = \"me+github@saurabh-kumar.com\"},\n]\nlicense = { text = \"BSD-3-Clause\" }\nkeywords = [\n    \"environment variables\",\n    \"deployments\",\n    \"settings\",\n    \"env\",\n    \"dotenv\",\n    \"configurations\",\n    \"python\",\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Programming Language :: Python :: Implementation :: PyPy\",\n    \"Intended Audience :: Developers\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: OS Independent\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n    \"Environment :: Web Environment\",\n]\n\nrequires-python = \">=3.10\"\n\ndynamic = [\"version\", \"readme\"]\n\n[project.urls]\nSource = \"https://github.com/theskumar/python-dotenv\"\n\n[project.optional-dependencies]\ncli = [\n    \"click>=5.0\",\n]\n\n[project.scripts]\ndotenv = \"dotenv.__main__:cli\"\n\n[tool.setuptools]\npackages = [\"dotenv\"]\npackage-dir = {\"\" = \"src\"}\npackage-data = {dotenv = [\"py.typed\"]}\n\n[tool.setuptools.dynamic]\nversion = {attr = \"dotenv.version.__version__\"}\nreadme = {file = [\"README.md\", \"CHANGELOG.md\"], content-type = \"text/markdown\"}\n\n[tool.pytest.ini_options]\ntestpaths = [\n    \"tests\",\n]\n\n[tool.coverage.run]\nrelative_files = true\nsource = [\"dotenv\"]\n\n[tool.coverage.paths]\nsource = [\n    \"src/dotenv\",\n    \".tox/*/lib/python*/site-packages/dotenv\",\n    \".tox/pypy*/site-packages/dotenv\",\n]\n\n[tool.coverage.report]\nshow_missing = true\nomit = [\"*/tests/*\"]\nexclude_lines = [\n    \"if IS_TYPE_CHECKING:\",\n    \"pragma: no cover\",\n]\n\n[tool.mypy]\ncheck_untyped_defs = true\nignore_missing_imports = true\n"
  },
  {
    "path": "requirements-docs.txt",
    "content": "mdx_truly_sane_lists>=1.3\nmkdocs-include-markdown-plugin>=6.0.0\nmkdocs-material>=9.5.0\nmkdocstrings[python]>=0.24.0\nmkdocs>=1.5.0\n"
  },
  {
    "path": "requirements.txt",
    "content": "bumpversion\nclick\nipython\npytest-cov\npytest>=3.9\ntox\nwheel\nruff\nbuild\npre-commit\n"
  },
  {
    "path": "ruff.toml",
    "content": "[lint]\nselect = [\n    # pycodestyle\n    \"E4\",\n    \"E7\",\n    \"E9\",\n\n    # Pyflakes\n    \"F\",\n\n    # flake8-bugbear\n    \"B\",\n\n    # iSort\n    \"I\",\n\n    # flake8-builtins\n    \"A\",\n]\n"
  },
  {
    "path": "src/dotenv/__init__.py",
    "content": "from typing import Any, Optional\n\nfrom .main import dotenv_values, find_dotenv, get_key, load_dotenv, set_key, unset_key\n\n\ndef load_ipython_extension(ipython: Any) -> None:\n    from .ipython import load_ipython_extension\n\n    load_ipython_extension(ipython)\n\n\ndef get_cli_string(\n    path: Optional[str] = None,\n    action: Optional[str] = None,\n    key: Optional[str] = None,\n    value: Optional[str] = None,\n    quote: Optional[str] = None,\n):\n    \"\"\"Returns a string suitable for running as a shell script.\n\n    Useful for converting a arguments passed to a fabric task\n    to be passed to a `local` or `run` command.\n    \"\"\"\n    command = [\"dotenv\"]\n    if quote:\n        command.append(f\"-q {quote}\")\n    if path:\n        command.append(f\"-f {path}\")\n    if action:\n        command.append(action)\n        if key:\n            command.append(key)\n            if value:\n                if \" \" in value:\n                    command.append(f'\"{value}\"')\n                else:\n                    command.append(value)\n\n    return \" \".join(command).strip()\n\n\n__all__ = [\n    \"get_cli_string\",\n    \"load_dotenv\",\n    \"dotenv_values\",\n    \"get_key\",\n    \"set_key\",\n    \"unset_key\",\n    \"find_dotenv\",\n    \"load_ipython_extension\",\n]\n"
  },
  {
    "path": "src/dotenv/__main__.py",
    "content": "\"\"\"Entry point for cli, enables execution with `python -m dotenv`\"\"\"\n\nfrom .cli import cli\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "src/dotenv/cli.py",
    "content": "import json\nimport os\nimport shlex\nimport sys\nfrom contextlib import contextmanager\nfrom typing import IO, Any, Dict, Iterator, List, Optional\n\nif sys.platform == \"win32\":\n    from subprocess import Popen\n\ntry:\n    import click\nexcept ImportError:\n    sys.stderr.write(\n        \"It seems python-dotenv is not installed with cli option. \\n\"\n        'Run pip install \"python-dotenv[cli]\" to fix this.'\n    )\n    sys.exit(1)\n\nfrom .main import dotenv_values, set_key, unset_key\nfrom .version import __version__\n\n\ndef enumerate_env() -> Optional[str]:\n    \"\"\"\n    Return a path for the ${pwd}/.env file.\n\n    If pwd does not exist, return None.\n    \"\"\"\n    try:\n        cwd = os.getcwd()\n    except FileNotFoundError:\n        return None\n    path = os.path.join(cwd, \".env\")\n    return path\n\n\n@click.group()\n@click.option(\n    \"-f\",\n    \"--file\",\n    default=enumerate_env(),\n    type=click.Path(file_okay=True),\n    help=\"Location of the .env file, defaults to .env file in current working directory.\",\n)\n@click.option(\n    \"-q\",\n    \"--quote\",\n    default=\"always\",\n    type=click.Choice([\"always\", \"never\", \"auto\"]),\n    help=\"Whether to quote or not the variable values. Default mode is always. This does not affect parsing.\",\n)\n@click.option(\n    \"-e\",\n    \"--export\",\n    default=False,\n    type=click.BOOL,\n    help=\"Whether to write the dot file as an executable bash script.\",\n)\n@click.version_option(version=__version__)\n@click.pass_context\ndef cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:\n    \"\"\"This script is used to set, get or unset values from a .env file.\"\"\"\n    ctx.obj = {\"QUOTE\": quote, \"EXPORT\": export, \"FILE\": file}\n\n\n@contextmanager\ndef stream_file(path: os.PathLike) -> Iterator[IO[str]]:\n    \"\"\"\n    Open a file and yield the corresponding (decoded) stream.\n\n    Exits with error code 2 if the file cannot be opened.\n    \"\"\"\n\n    try:\n        with open(path) as stream:\n            yield stream\n    except OSError as exc:\n        print(f\"Error opening env file: {exc}\", file=sys.stderr)\n        sys.exit(2)\n\n\n@cli.command(name=\"list\")\n@click.pass_context\n@click.option(\n    \"--format\",\n    \"output_format\",\n    default=\"simple\",\n    type=click.Choice([\"simple\", \"json\", \"shell\", \"export\"]),\n    help=\"The format in which to display the list. Default format is simple, \"\n    \"which displays name=value without quotes.\",\n)\ndef list_values(ctx: click.Context, output_format: str) -> None:\n    \"\"\"Display all the stored key/value.\"\"\"\n    file = ctx.obj[\"FILE\"]\n\n    with stream_file(file) as stream:\n        values = dotenv_values(stream=stream)\n\n    if output_format == \"json\":\n        click.echo(json.dumps(values, indent=2, sort_keys=True))\n    else:\n        prefix = \"export \" if output_format == \"export\" else \"\"\n        for k in sorted(values):\n            v = values[k]\n            if v is not None:\n                if output_format in (\"export\", \"shell\"):\n                    v = shlex.quote(v)\n                click.echo(f\"{prefix}{k}={v}\")\n\n\n@cli.command(name=\"set\")\n@click.pass_context\n@click.argument(\"key\", required=True)\n@click.argument(\"value\", required=True)\ndef set_value(ctx: click.Context, key: Any, value: Any) -> None:\n    \"\"\"\n    Store the given key/value.\n\n    This doesn't follow symlinks, to avoid accidentally modifying a file at a\n    potentially untrusted path.\n    \"\"\"\n\n    file = ctx.obj[\"FILE\"]\n    quote = ctx.obj[\"QUOTE\"]\n    export = ctx.obj[\"EXPORT\"]\n    success, key, value = set_key(file, key, value, quote, export)\n    if success:\n        click.echo(f\"{key}={value}\")\n    else:\n        sys.exit(1)\n\n\n@cli.command()\n@click.pass_context\n@click.argument(\"key\", required=True)\ndef get(ctx: click.Context, key: Any) -> None:\n    \"\"\"Retrieve the value for the given key.\"\"\"\n    file = ctx.obj[\"FILE\"]\n\n    with stream_file(file) as stream:\n        values = dotenv_values(stream=stream)\n\n    stored_value = values.get(key)\n    if stored_value:\n        click.echo(stored_value)\n    else:\n        sys.exit(1)\n\n\n@cli.command()\n@click.pass_context\n@click.argument(\"key\", required=True)\ndef unset(ctx: click.Context, key: Any) -> None:\n    \"\"\"\n    Removes the given key.\n\n    This doesn't follow symlinks, to avoid accidentally modifying a file at a\n    potentially untrusted path.\n    \"\"\"\n    file = ctx.obj[\"FILE\"]\n    quote = ctx.obj[\"QUOTE\"]\n    success, key = unset_key(file, key, quote)\n    if success:\n        click.echo(f\"Successfully removed {key}\")\n    else:\n        sys.exit(1)\n\n\n@cli.command(\n    context_settings={\n        \"allow_extra_args\": True,\n        \"allow_interspersed_args\": False,\n        \"ignore_unknown_options\": True,\n    }\n)\n@click.pass_context\n@click.option(\n    \"--override/--no-override\",\n    default=True,\n    help=\"Override variables from the environment file with those from the .env file.\",\n)\n@click.argument(\"commandline\", nargs=-1, type=click.UNPROCESSED)\ndef run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None:\n    \"\"\"Run command with environment variables present.\"\"\"\n    file = ctx.obj[\"FILE\"]\n    if not os.path.isfile(file):\n        raise click.BadParameter(\n            f\"Invalid value for '-f' \\\"{file}\\\" does not exist.\", ctx=ctx\n        )\n    dotenv_as_dict = {\n        k: v\n        for (k, v) in dotenv_values(file).items()\n        if v is not None and (override or k not in os.environ)\n    }\n\n    if not commandline:\n        click.echo(\"No command given.\")\n        sys.exit(1)\n\n    run_command([*commandline, *ctx.args], dotenv_as_dict)\n\n\ndef run_command(command: List[str], env: Dict[str, str]) -> None:\n    \"\"\"Replace the current process with the specified command.\n\n    Replaces the current process with the specified command and the variables from `env`\n    added in the current environment variables.\n\n    Parameters\n    ----------\n    command: List[str]\n        The command and it's parameters\n    env: Dict\n        The additional environment variables\n\n    Returns\n    -------\n    None\n        This function does not return any value. It replaces the current process with the new one.\n\n    \"\"\"\n    # copy the current environment variables and add the vales from\n    # `env`\n    cmd_env = os.environ.copy()\n    cmd_env.update(env)\n\n    if sys.platform == \"win32\":\n        # execvpe on Windows returns control immediately\n        # rather than once the command has finished.\n        try:\n            p = Popen(\n                command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env\n            )\n        except FileNotFoundError:\n            print(f\"Command not found: {command[0]}\", file=sys.stderr)\n            sys.exit(1)\n\n        _, _ = p.communicate()\n\n        sys.exit(p.returncode)\n    else:\n        try:\n            os.execvpe(command[0], args=command, env=cmd_env)\n        except FileNotFoundError:\n            print(f\"Command not found: {command[0]}\", file=sys.stderr)\n            sys.exit(1)\n"
  },
  {
    "path": "src/dotenv/ipython.py",
    "content": "from IPython.core.magic import Magics, line_magic, magics_class  # type: ignore\nfrom IPython.core.magic_arguments import (\n    argument,\n    magic_arguments,\n    parse_argstring,\n)  # type: ignore\n\nfrom .main import find_dotenv, load_dotenv\n\n\n@magics_class\nclass IPythonDotEnv(Magics):\n    @magic_arguments()\n    @argument(\n        \"-o\",\n        \"--override\",\n        action=\"store_true\",\n        help=\"Indicate to override existing variables\",\n    )\n    @argument(\n        \"-v\",\n        \"--verbose\",\n        action=\"store_true\",\n        help=\"Indicate function calls to be verbose\",\n    )\n    @argument(\n        \"dotenv_path\",\n        nargs=\"?\",\n        type=str,\n        default=\".env\",\n        help=\"Search in increasingly higher folders for the `dotenv_path`\",\n    )\n    @line_magic\n    def dotenv(self, line):\n        args = parse_argstring(self.dotenv, line)\n        # Locate the .env file\n        dotenv_path = args.dotenv_path\n        try:\n            dotenv_path = find_dotenv(dotenv_path, True, True)\n        except IOError:\n            print(\"cannot find .env file\")\n            return\n\n        # Load the .env file\n        load_dotenv(dotenv_path, verbose=args.verbose, override=args.override)\n\n\ndef load_ipython_extension(ipython):\n    \"\"\"Register the %dotenv magic.\"\"\"\n    ipython.register_magics(IPythonDotEnv)\n"
  },
  {
    "path": "src/dotenv/main.py",
    "content": "import io\nimport logging\nimport os\nimport pathlib\nimport stat\nimport sys\nimport tempfile\nfrom collections import OrderedDict\nfrom contextlib import contextmanager\nfrom typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union\n\nfrom .parser import Binding, parse_stream\nfrom .variables import parse_variables\n\n# A type alias for a string path to be used for the paths in this file.\n# These paths may flow to `open()` and `os.replace()`.\nStrPath = Union[str, \"os.PathLike[str]\"]\n\nlogger = logging.getLogger(__name__)\n\n\ndef _load_dotenv_disabled() -> bool:\n    \"\"\"\n    Determine if dotenv loading has been disabled.\n    \"\"\"\n    if \"PYTHON_DOTENV_DISABLED\" not in os.environ:\n        return False\n    value = os.environ[\"PYTHON_DOTENV_DISABLED\"].casefold()\n    return value in {\"1\", \"true\", \"t\", \"yes\", \"y\"}\n\n\ndef with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]:\n    for mapping in mappings:\n        if mapping.error:\n            logger.warning(\n                \"python-dotenv could not parse statement starting at line %s\",\n                mapping.original.line,\n            )\n        yield mapping\n\n\nclass DotEnv:\n    def __init__(\n        self,\n        dotenv_path: Optional[StrPath],\n        stream: Optional[IO[str]] = None,\n        verbose: bool = False,\n        encoding: Optional[str] = None,\n        interpolate: bool = True,\n        override: bool = True,\n    ) -> None:\n        self.dotenv_path: Optional[StrPath] = dotenv_path\n        self.stream: Optional[IO[str]] = stream\n        self._dict: Optional[Dict[str, Optional[str]]] = None\n        self.verbose: bool = verbose\n        self.encoding: Optional[str] = encoding\n        self.interpolate: bool = interpolate\n        self.override: bool = override\n\n    @contextmanager\n    def _get_stream(self) -> Iterator[IO[str]]:\n        if self.dotenv_path and _is_file_or_fifo(self.dotenv_path):\n            with open(self.dotenv_path, encoding=self.encoding) as stream:\n                yield stream\n        elif self.stream is not None:\n            yield self.stream\n        else:\n            if self.verbose:\n                logger.info(\n                    \"python-dotenv could not find configuration file %s.\",\n                    self.dotenv_path or \".env\",\n                )\n            yield io.StringIO(\"\")\n\n    def dict(self) -> Dict[str, Optional[str]]:\n        \"\"\"Return dotenv as dict\"\"\"\n        if self._dict:\n            return self._dict\n\n        raw_values = self.parse()\n\n        if self.interpolate:\n            self._dict = OrderedDict(\n                resolve_variables(raw_values, override=self.override)\n            )\n        else:\n            self._dict = OrderedDict(raw_values)\n\n        return self._dict\n\n    def parse(self) -> Iterator[Tuple[str, Optional[str]]]:\n        with self._get_stream() as stream:\n            for mapping in with_warn_for_invalid_lines(parse_stream(stream)):\n                if mapping.key is not None:\n                    yield mapping.key, mapping.value\n\n    def set_as_environment_variables(self) -> bool:\n        \"\"\"\n        Load the current dotenv as system environment variable.\n        \"\"\"\n        if not self.dict():\n            return False\n\n        for k, v in self.dict().items():\n            if k in os.environ and not self.override:\n                continue\n            if v is not None:\n                os.environ[k] = v\n\n        return True\n\n    def get(self, key: str) -> Optional[str]:\n        \"\"\" \"\"\"\n        data = self.dict()\n\n        if key in data:\n            return data[key]\n\n        if self.verbose:\n            logger.warning(\"Key %s not found in %s.\", key, self.dotenv_path)\n\n        return None\n\n\ndef get_key(\n    dotenv_path: StrPath,\n    key_to_get: str,\n    encoding: Optional[str] = \"utf-8\",\n) -> Optional[str]:\n    \"\"\"\n    Get the value of a given key from the given .env.\n\n    Returns `None` if the key isn't found or doesn't have a value.\n    \"\"\"\n    return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get)\n\n\n@contextmanager\ndef rewrite(\n    path: StrPath,\n    encoding: Optional[str],\n    follow_symlinks: bool = False,\n) -> Iterator[Tuple[IO[str], IO[str]]]:\n    if follow_symlinks:\n        path = os.path.realpath(path)\n\n    try:\n        source: IO[str] = open(path, encoding=encoding)\n        try:\n            path_stat = os.lstat(path)\n            original_mode: Optional[int] = (\n                stat.S_IMODE(path_stat.st_mode)\n                if stat.S_ISREG(path_stat.st_mode)\n                else None\n            )\n        except BaseException:\n            source.close()\n            raise\n    except FileNotFoundError:\n        source = io.StringIO(\"\")\n        original_mode = None\n\n    with tempfile.NamedTemporaryFile(\n        mode=\"w\",\n        encoding=encoding,\n        delete=False,\n        prefix=\".tmp_\",\n        dir=os.path.dirname(os.path.abspath(path)),\n    ) as dest:\n        dest_path = pathlib.Path(dest.name)\n        error = None\n\n        try:\n            with source:\n                yield (source, dest)\n        except BaseException as err:\n            error = err\n\n    if error is None:\n        try:\n            if original_mode is not None:\n                os.chmod(dest_path, original_mode)\n\n            os.replace(dest_path, path)\n        except BaseException:\n            dest_path.unlink(missing_ok=True)\n            raise\n    else:\n        dest_path.unlink(missing_ok=True)\n        raise error from None\n\n\ndef set_key(\n    dotenv_path: StrPath,\n    key_to_set: str,\n    value_to_set: str,\n    quote_mode: str = \"always\",\n    export: bool = False,\n    encoding: Optional[str] = \"utf-8\",\n    follow_symlinks: bool = False,\n) -> Tuple[Optional[bool], str, str]:\n    \"\"\"\n    Adds or Updates a key/value to the given .env\n\n    The target .env file is created if it doesn't exist.\n\n    This function doesn't follow symlinks by default, to avoid accidentally\n    modifying a file at a potentially untrusted path. If you don't need this\n    protection and need symlinks to be followed, use `follow_symlinks`.\n    \"\"\"\n    if quote_mode not in (\"always\", \"auto\", \"never\"):\n        raise ValueError(f\"Unknown quote_mode: {quote_mode}\")\n\n    quote = quote_mode == \"always\" or (\n        quote_mode == \"auto\" and not value_to_set.isalnum()\n    )\n\n    if quote:\n        value_out = \"'{}'\".format(value_to_set.replace(\"'\", \"\\\\'\"))\n    else:\n        value_out = value_to_set\n    if export:\n        line_out = f\"export {key_to_set}={value_out}\\n\"\n    else:\n        line_out = f\"{key_to_set}={value_out}\\n\"\n\n    with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as (\n        source,\n        dest,\n    ):\n        replaced = False\n        missing_newline = False\n        for mapping in with_warn_for_invalid_lines(parse_stream(source)):\n            if mapping.key == key_to_set:\n                dest.write(line_out)\n                replaced = True\n            else:\n                dest.write(mapping.original.string)\n                missing_newline = not mapping.original.string.endswith(\"\\n\")\n        if not replaced:\n            if missing_newline:\n                dest.write(\"\\n\")\n            dest.write(line_out)\n\n    return True, key_to_set, value_to_set\n\n\ndef unset_key(\n    dotenv_path: StrPath,\n    key_to_unset: str,\n    quote_mode: str = \"always\",\n    encoding: Optional[str] = \"utf-8\",\n    follow_symlinks: bool = False,\n) -> Tuple[Optional[bool], str]:\n    \"\"\"\n    Removes a given key from the given `.env` file.\n\n    If the .env path given doesn't exist, fails.\n    If the given key doesn't exist in the .env, fails.\n\n    This function doesn't follow symlinks by default, to avoid accidentally\n    modifying a file at a potentially untrusted path. If you don't need this\n    protection and need symlinks to be followed, use `follow_symlinks`.\n    \"\"\"\n    if not os.path.exists(dotenv_path):\n        logger.warning(\"Can't delete from %s - it doesn't exist.\", dotenv_path)\n        return None, key_to_unset\n\n    removed = False\n    with rewrite(dotenv_path, encoding=encoding, follow_symlinks=follow_symlinks) as (\n        source,\n        dest,\n    ):\n        for mapping in with_warn_for_invalid_lines(parse_stream(source)):\n            if mapping.key == key_to_unset:\n                removed = True\n            else:\n                dest.write(mapping.original.string)\n\n    if not removed:\n        logger.warning(\n            \"Key %s not removed from %s - key doesn't exist.\", key_to_unset, dotenv_path\n        )\n        return None, key_to_unset\n\n    return removed, key_to_unset\n\n\ndef resolve_variables(\n    values: Iterable[Tuple[str, Optional[str]]],\n    override: bool,\n) -> Mapping[str, Optional[str]]:\n    new_values: Dict[str, Optional[str]] = {}\n\n    for name, value in values:\n        if value is None:\n            result = None\n        else:\n            atoms = parse_variables(value)\n            env: Dict[str, Optional[str]] = {}\n            if override:\n                env.update(os.environ)  # type: ignore\n                env.update(new_values)\n            else:\n                env.update(new_values)\n                env.update(os.environ)  # type: ignore\n            result = \"\".join(atom.resolve(env) for atom in atoms)\n\n        new_values[name] = result\n\n    return new_values\n\n\ndef _walk_to_root(path: str) -> Iterator[str]:\n    \"\"\"\n    Yield directories starting from the given directory up to the root\n    \"\"\"\n    if not os.path.exists(path):\n        raise IOError(\"Starting path not found\")\n\n    if os.path.isfile(path):\n        path = os.path.dirname(path)\n\n    last_dir = None\n    current_dir = os.path.abspath(path)\n    while last_dir != current_dir:\n        yield current_dir\n        parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir))\n        last_dir, current_dir = current_dir, parent_dir\n\n\ndef find_dotenv(\n    filename: str = \".env\",\n    raise_error_if_not_found: bool = False,\n    usecwd: bool = False,\n) -> str:\n    \"\"\"\n    Search in increasingly higher folders for the given file\n\n    Returns path to the file if found, or an empty string otherwise\n    \"\"\"\n\n    def _is_interactive():\n        \"\"\"Decide whether this is running in a REPL or IPython notebook\"\"\"\n        if hasattr(sys, \"ps1\") or hasattr(sys, \"ps2\"):\n            return True\n        try:\n            main = __import__(\"__main__\", None, None, fromlist=[\"__file__\"])\n        except ModuleNotFoundError:\n            return False\n        return not hasattr(main, \"__file__\")\n\n    def _is_debugger():\n        return sys.gettrace() is not None\n\n    if usecwd or _is_interactive() or _is_debugger() or getattr(sys, \"frozen\", False):\n        # Should work without __file__, e.g. in REPL or IPython notebook.\n        path = os.getcwd()\n    else:\n        # will work for .py files\n        frame = sys._getframe()\n        current_file = __file__\n\n        while frame.f_code.co_filename == current_file or not os.path.exists(\n            frame.f_code.co_filename\n        ):\n            assert frame.f_back is not None\n            frame = frame.f_back\n        frame_filename = frame.f_code.co_filename\n        path = os.path.dirname(os.path.abspath(frame_filename))\n\n    for dirname in _walk_to_root(path):\n        check_path = os.path.join(dirname, filename)\n        if _is_file_or_fifo(check_path):\n            return check_path\n\n    if raise_error_if_not_found:\n        raise IOError(\"File not found\")\n\n    return \"\"\n\n\ndef load_dotenv(\n    dotenv_path: Optional[StrPath] = None,\n    stream: Optional[IO[str]] = None,\n    verbose: bool = False,\n    override: bool = False,\n    interpolate: bool = True,\n    encoding: Optional[str] = \"utf-8\",\n) -> bool:\n    \"\"\"Parse a .env file and then load all the variables found as environment variables.\n\n    Parameters:\n        dotenv_path: Absolute or relative path to .env file.\n        stream: Text stream (such as `io.StringIO`) with .env content, used if\n            `dotenv_path` is `None`.\n        verbose: Whether to output a warning the .env file is missing.\n        override: Whether to override the system environment variables with the variables\n            from the `.env` file.\n        interpolate: Whether to interpolate variables using POSIX variable expansion.\n        encoding: Encoding to be used to read the file.\n    Returns:\n        Bool: True if at least one environment variable is set else False\n\n    If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the\n    .env file with it's default parameters. If you need to change the default parameters\n    of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result\n    to this function as `dotenv_path`.\n\n    If the environment variable `PYTHON_DOTENV_DISABLED` is set to a truthy value,\n    .env loading is disabled.\n    \"\"\"\n    if _load_dotenv_disabled():\n        logger.debug(\n            \"python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable\"\n        )\n        return False\n\n    if dotenv_path is None and stream is None:\n        dotenv_path = find_dotenv()\n\n    dotenv = DotEnv(\n        dotenv_path=dotenv_path,\n        stream=stream,\n        verbose=verbose,\n        interpolate=interpolate,\n        override=override,\n        encoding=encoding,\n    )\n    return dotenv.set_as_environment_variables()\n\n\ndef dotenv_values(\n    dotenv_path: Optional[StrPath] = None,\n    stream: Optional[IO[str]] = None,\n    verbose: bool = False,\n    interpolate: bool = True,\n    encoding: Optional[str] = \"utf-8\",\n) -> Dict[str, Optional[str]]:\n    \"\"\"\n    Parse a .env file and return its content as a dict.\n\n    The returned dict will have `None` values for keys without values in the .env file.\n    For example, `foo=bar` results in `{\"foo\": \"bar\"}` whereas `foo` alone results in\n    `{\"foo\": None}`\n\n    Parameters:\n        dotenv_path: Absolute or relative path to the .env file.\n        stream: `StringIO` object with .env content, used if `dotenv_path` is `None`.\n        verbose: Whether to output a warning if the .env file is missing.\n        interpolate: Whether to interpolate variables using POSIX variable expansion.\n        encoding: Encoding to be used to read the file.\n\n    If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the\n    .env file.\n    \"\"\"\n    if dotenv_path is None and stream is None:\n        dotenv_path = find_dotenv()\n\n    return DotEnv(\n        dotenv_path=dotenv_path,\n        stream=stream,\n        verbose=verbose,\n        interpolate=interpolate,\n        override=True,\n        encoding=encoding,\n    ).dict()\n\n\ndef _is_file_or_fifo(path: StrPath) -> bool:\n    \"\"\"\n    Return True if `path` exists and is either a regular file or a FIFO.\n    \"\"\"\n    if os.path.isfile(path):\n        return True\n\n    try:\n        st = os.stat(path)\n    except (FileNotFoundError, OSError):\n        return False\n\n    return stat.S_ISFIFO(st.st_mode)\n"
  },
  {
    "path": "src/dotenv/parser.py",
    "content": "import codecs\nimport re\nfrom typing import (\n    IO,\n    Iterator,\n    Match,\n    NamedTuple,\n    Optional,\n    Pattern,\n    Sequence,\n)\n\n\ndef make_regex(string: str, extra_flags: int = 0) -> Pattern[str]:\n    return re.compile(string, re.UNICODE | extra_flags)\n\n\n_newline = make_regex(r\"(\\r\\n|\\n|\\r)\")\n_multiline_whitespace = make_regex(r\"\\s*\", extra_flags=re.MULTILINE)\n_whitespace = make_regex(r\"[^\\S\\r\\n]*\")\n_export = make_regex(r\"(?:export[^\\S\\r\\n]+)?\")\n_single_quoted_key = make_regex(r\"'([^']+)'\")\n_unquoted_key = make_regex(r\"([^=\\#\\s]+)\")\n_equal_sign = make_regex(r\"(=[^\\S\\r\\n]*)\")\n_single_quoted_value = make_regex(r\"'((?:\\\\'|[^'])*)'\")\n_double_quoted_value = make_regex(r'\"((?:\\\\\"|[^\"])*)\"')\n_unquoted_value = make_regex(r\"([^\\r\\n]*)\")\n_comment = make_regex(r\"(?:[^\\S\\r\\n]*#[^\\r\\n]*)?\")\n_end_of_line = make_regex(r\"[^\\S\\r\\n]*(?:\\r\\n|\\n|\\r|$)\")\n_rest_of_line = make_regex(r\"[^\\r\\n]*(?:\\r|\\n|\\r\\n)?\")\n_double_quote_escapes = make_regex(r\"\\\\[\\\\'\\\"abfnrtv]\")\n_single_quote_escapes = make_regex(r\"\\\\[\\\\']\")\n\n\nclass Original(NamedTuple):\n    string: str\n    line: int\n\n\nclass Binding(NamedTuple):\n    key: Optional[str]\n    value: Optional[str]\n    original: Original\n    error: bool\n\n\nclass Position:\n    def __init__(self, chars: int, line: int) -> None:\n        self.chars = chars\n        self.line = line\n\n    @classmethod\n    def start(cls) -> \"Position\":\n        return cls(chars=0, line=1)\n\n    def set(self, other: \"Position\") -> None:\n        self.chars = other.chars\n        self.line = other.line\n\n    def advance(self, string: str) -> None:\n        self.chars += len(string)\n        self.line += len(re.findall(_newline, string))\n\n\nclass Error(Exception):\n    pass\n\n\nclass Reader:\n    def __init__(self, stream: IO[str]) -> None:\n        self.string = stream.read()\n        self.position = Position.start()\n        self.mark = Position.start()\n\n    def has_next(self) -> bool:\n        return self.position.chars < len(self.string)\n\n    def set_mark(self) -> None:\n        self.mark.set(self.position)\n\n    def get_marked(self) -> Original:\n        return Original(\n            string=self.string[self.mark.chars : self.position.chars],\n            line=self.mark.line,\n        )\n\n    def peek(self, count: int) -> str:\n        return self.string[self.position.chars : self.position.chars + count]\n\n    def read(self, count: int) -> str:\n        result = self.string[self.position.chars : self.position.chars + count]\n        if len(result) < count:\n            raise Error(\"read: End of string\")\n        self.position.advance(result)\n        return result\n\n    def read_regex(self, regex: Pattern[str]) -> Sequence[str]:\n        match = regex.match(self.string, self.position.chars)\n        if match is None:\n            raise Error(\"read_regex: Pattern not found\")\n        self.position.advance(self.string[match.start() : match.end()])\n        return match.groups()\n\n\ndef decode_escapes(regex: Pattern[str], string: str) -> str:\n    def decode_match(match: Match[str]) -> str:\n        return codecs.decode(match.group(0), \"unicode-escape\")  # type: ignore\n\n    return regex.sub(decode_match, string)\n\n\ndef parse_key(reader: Reader) -> Optional[str]:\n    char = reader.peek(1)\n    if char == \"#\":\n        return None\n    elif char == \"'\":\n        (key,) = reader.read_regex(_single_quoted_key)\n    else:\n        (key,) = reader.read_regex(_unquoted_key)\n    return key\n\n\ndef parse_unquoted_value(reader: Reader) -> str:\n    (part,) = reader.read_regex(_unquoted_value)\n    return re.sub(r\"\\s+#.*\", \"\", part).rstrip()\n\n\ndef parse_value(reader: Reader) -> str:\n    char = reader.peek(1)\n    if char == \"'\":\n        (value,) = reader.read_regex(_single_quoted_value)\n        return decode_escapes(_single_quote_escapes, value)\n    elif char == '\"':\n        (value,) = reader.read_regex(_double_quoted_value)\n        return decode_escapes(_double_quote_escapes, value)\n    elif char in (\"\", \"\\n\", \"\\r\"):\n        return \"\"\n    else:\n        return parse_unquoted_value(reader)\n\n\ndef parse_binding(reader: Reader) -> Binding:\n    reader.set_mark()\n    try:\n        reader.read_regex(_multiline_whitespace)\n        if not reader.has_next():\n            return Binding(\n                key=None,\n                value=None,\n                original=reader.get_marked(),\n                error=False,\n            )\n        reader.read_regex(_export)\n        key = parse_key(reader)\n        reader.read_regex(_whitespace)\n        if reader.peek(1) == \"=\":\n            reader.read_regex(_equal_sign)\n            value: Optional[str] = parse_value(reader)\n        else:\n            value = None\n        reader.read_regex(_comment)\n        reader.read_regex(_end_of_line)\n        return Binding(\n            key=key,\n            value=value,\n            original=reader.get_marked(),\n            error=False,\n        )\n    except Error:\n        reader.read_regex(_rest_of_line)\n        return Binding(\n            key=None,\n            value=None,\n            original=reader.get_marked(),\n            error=True,\n        )\n\n\ndef parse_stream(stream: IO[str]) -> Iterator[Binding]:\n    reader = Reader(stream)\n    while reader.has_next():\n        yield parse_binding(reader)\n"
  },
  {
    "path": "src/dotenv/py.typed",
    "content": "# Marker file for PEP 561\n"
  },
  {
    "path": "src/dotenv/variables.py",
    "content": "import re\nfrom abc import ABCMeta, abstractmethod\nfrom typing import Iterator, Mapping, Optional, Pattern\n\n_posix_variable: Pattern[str] = re.compile(\n    r\"\"\"\n    \\$\\{\n        (?P<name>[^\\}:]*)\n        (?::-\n            (?P<default>[^\\}]*)\n        )?\n    \\}\n    \"\"\",\n    re.VERBOSE,\n)\n\n\nclass Atom(metaclass=ABCMeta):\n    def __ne__(self, other: object) -> bool:\n        result = self.__eq__(other)\n        if result is NotImplemented:\n            return NotImplemented\n        return not result\n\n    @abstractmethod\n    def resolve(self, env: Mapping[str, Optional[str]]) -> str: ...\n\n\nclass Literal(Atom):\n    def __init__(self, value: str) -> None:\n        self.value = value\n\n    def __repr__(self) -> str:\n        return f\"Literal(value={self.value})\"\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return self.value == other.value\n\n    def __hash__(self) -> int:\n        return hash((self.__class__, self.value))\n\n    def resolve(self, env: Mapping[str, Optional[str]]) -> str:\n        return self.value\n\n\nclass Variable(Atom):\n    def __init__(self, name: str, default: Optional[str]) -> None:\n        self.name = name\n        self.default = default\n\n    def __repr__(self) -> str:\n        return f\"Variable(name={self.name}, default={self.default})\"\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, self.__class__):\n            return NotImplemented\n        return (self.name, self.default) == (other.name, other.default)\n\n    def __hash__(self) -> int:\n        return hash((self.__class__, self.name, self.default))\n\n    def resolve(self, env: Mapping[str, Optional[str]]) -> str:\n        default = self.default if self.default is not None else \"\"\n        result = env.get(self.name, default)\n        return result if result is not None else \"\"\n\n\ndef parse_variables(value: str) -> Iterator[Atom]:\n    cursor = 0\n\n    for match in _posix_variable.finditer(value):\n        (start, end) = match.span()\n        name = match[\"name\"]\n        default = match[\"default\"]\n\n        if start > cursor:\n            yield Literal(value=value[cursor:start])\n\n        yield Variable(name=name, default=default)\n        cursor = end\n\n    length = len(value)\n    if cursor < length:\n        yield Literal(value=value[cursor:length])\n"
  },
  {
    "path": "src/dotenv/version.py",
    "content": "__version__ = \"1.2.2\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\nfrom click.testing import CliRunner\n\n\n@pytest.fixture\ndef cli():\n    runner = CliRunner()\n    with runner.isolated_filesystem():\n        yield runner\n\n\n@pytest.fixture\ndef dotenv_path(tmp_path):\n    path = tmp_path / \".env\"\n    path.write_bytes(b\"\")\n    yield path\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "import os\nfrom pathlib import Path\nfrom typing import Optional\n\nimport pytest\n\nimport dotenv\nfrom dotenv.cli import cli as dotenv_cli\nfrom dotenv.version import __version__\nfrom tests.test_lib import check_process, run_dotenv\n\n\n@pytest.mark.parametrize(\n    \"output_format,content,expected\",\n    (\n        (None, \"x='a b c'\", \"\"\"x=a b c\\n\"\"\"),\n        (\"simple\", \"x='a b c'\", \"\"\"x=a b c\\n\"\"\"),\n        (\"simple\", \"\"\"x='\"a b c\"'\"\"\", \"\"\"x=\"a b c\"\\n\"\"\"),\n        (\"simple\", '''x=\"'a b c'\"''', \"\"\"x='a b c'\\n\"\"\"),\n        (\"json\", \"x='a b c'\", \"\"\"{\\n  \"x\": \"a b c\"\\n}\\n\"\"\"),\n        (\"shell\", \"x='a b c'\", \"x='a b c'\\n\"),\n        (\"shell\", \"\"\"x='\"a b c\"'\"\"\", \"\"\"x='\"a b c\"'\\n\"\"\"),\n        (\"shell\", '''x=\"'a b c'\"''', \"\"\"x=''\"'\"'a b c'\"'\"''\\n\"\"\"),\n        (\"shell\", \"x='a\\nb\\nc'\", \"x='a\\nb\\nc'\\n\"),\n        (\"export\", \"x='a b c'\", \"\"\"export x='a b c'\\n\"\"\"),\n    ),\n)\ndef test_list(\n    cli, dotenv_path, output_format: Optional[str], content: str, expected: str\n):\n    dotenv_path.write_text(content + \"\\n\")\n\n    args = [\"--file\", dotenv_path, \"list\"]\n    if format is not None:\n        args.extend([\"--format\", output_format])\n\n    result = cli.invoke(dotenv_cli, args)\n\n    assert (result.exit_code, result.output) == (0, expected)\n\n\ndef test_list_non_existent_file(cli):\n    result = cli.invoke(dotenv_cli, [\"--file\", \"nx_file\", \"list\"])\n\n    assert result.exit_code == 2, result.output\n    assert \"Error opening env file\" in result.output\n\n\ndef test_list_not_a_file(cli):\n    result = cli.invoke(dotenv_cli, [\"--file\", \".\", \"list\"])\n\n    assert result.exit_code == 2, result.output\n    assert \"Error opening env file\" in result.output\n\n\ndef test_list_no_file(cli):\n    result = cli.invoke(dotenv.cli.list_values, [])\n\n    assert (result.exit_code, result.output) == (1, \"\")\n\n\ndef test_get_existing_value(cli, dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    result = cli.invoke(dotenv_cli, [\"--file\", dotenv_path, \"get\", \"a\"])\n\n    assert (result.exit_code, result.output) == (0, \"b\\n\")\n\n\ndef test_get_non_existent_value(cli, dotenv_path):\n    result = cli.invoke(dotenv_cli, [\"--file\", dotenv_path, \"get\", \"a\"])\n\n    assert (result.exit_code, result.output) == (1, \"\")\n\n\ndef test_get_non_existent_file(cli):\n    result = cli.invoke(dotenv_cli, [\"--file\", \"nx_file\", \"get\", \"a\"])\n\n    assert result.exit_code == 2\n    assert \"Error opening env file\" in result.output\n\n\ndef test_get_not_a_file(cli):\n    result = cli.invoke(dotenv_cli, [\"--file\", \".\", \"get\", \"a\"])\n\n    assert result.exit_code == 2\n    assert \"Error opening env file\" in result.output\n\n\ndef test_unset_existing_value(cli, dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    result = cli.invoke(dotenv_cli, [\"--file\", dotenv_path, \"unset\", \"a\"])\n\n    assert (result.exit_code, result.output) == (0, \"Successfully removed a\\n\")\n    assert dotenv_path.read_text() == \"\"\n\n\ndef test_unset_non_existent_value(cli, dotenv_path):\n    result = cli.invoke(dotenv_cli, [\"--file\", dotenv_path, \"unset\", \"a\"])\n\n    assert (result.exit_code, result.output) == (1, \"\")\n    assert dotenv_path.read_text() == \"\"\n\n\n@pytest.mark.parametrize(\n    \"quote_mode,variable,value,expected\",\n    (\n        (\"always\", \"a\", \"x\", \"a='x'\\n\"),\n        (\"never\", \"a\", \"x\", \"a=x\\n\"),\n        (\"auto\", \"a\", \"x\", \"a=x\\n\"),\n        (\"auto\", \"a\", \"x y\", \"a='x y'\\n\"),\n        (\"auto\", \"a\", \"$\", \"a='$'\\n\"),\n    ),\n)\ndef test_set_quote_options(cli, dotenv_path, quote_mode, variable, value, expected):\n    result = cli.invoke(\n        dotenv_cli,\n        [\n            \"--file\",\n            dotenv_path,\n            \"--export\",\n            \"false\",\n            \"--quote\",\n            quote_mode,\n            \"set\",\n            variable,\n            value,\n        ],\n    )\n\n    assert (result.exit_code, result.output) == (0, \"{}={}\\n\".format(variable, value))\n    assert dotenv_path.read_text() == expected\n\n\n@pytest.mark.parametrize(\n    \"dotenv_path,export_mode,variable,value,expected\",\n    (\n        (Path(\".nx_file\"), \"true\", \"a\", \"x\", \"export a='x'\\n\"),\n        (Path(\".nx_file\"), \"false\", \"a\", \"x\", \"a='x'\\n\"),\n    ),\n)\ndef test_set_export(cli, dotenv_path, export_mode, variable, value, expected):\n    result = cli.invoke(\n        dotenv_cli,\n        [\n            \"--file\",\n            dotenv_path,\n            \"--quote\",\n            \"always\",\n            \"--export\",\n            export_mode,\n            \"set\",\n            variable,\n            value,\n        ],\n    )\n\n    assert (result.exit_code, result.output) == (0, \"{}={}\\n\".format(variable, value))\n    assert dotenv_path.read_text() == expected\n\n\ndef test_set_non_existent_file(cli):\n    result = cli.invoke(dotenv.cli.set_value, [\"a\", \"b\"])\n\n    assert (result.exit_code, result.output) == (1, \"\")\n\n\ndef test_set_no_file(cli):\n    result = cli.invoke(dotenv_cli, [\"--file\", \"nx_file\", \"set\"])\n\n    assert result.exit_code == 2\n    assert \"Missing argument\" in result.output\n\n\ndef test_get_default_path(tmp_path):\n    (tmp_path / \".env\").write_text(\"A=x\")\n\n    result = run_dotenv([\"get\", \"A\"], cwd=tmp_path)\n\n    check_process(result, exit_code=0, stdout=\"x\\n\")\n\n\ndef test_run(tmp_path):\n    (tmp_path / \".env\").write_text(\"A=x\")\n\n    result = run_dotenv([\"run\", \"printenv\", \"A\"], cwd=tmp_path)\n\n    check_process(result, exit_code=0, stdout=\"x\\n\")\n\n\ndef test_run_with_existing_variable(tmp_path):\n    (tmp_path / \".env\").write_text(\"A=x\")\n    env = dict(os.environ)\n    env.update({\"LANG\": \"en_US.UTF-8\", \"A\": \"y\"})\n\n    result = run_dotenv([\"run\", \"printenv\", \"A\"], cwd=tmp_path, env=env)\n\n    check_process(result, exit_code=0, stdout=\"x\\n\")\n\n\ndef test_run_with_existing_variable_not_overridden(tmp_path):\n    (tmp_path / \".env\").write_text(\"A=x\")\n    env = dict(os.environ)\n    env.update({\"LANG\": \"en_US.UTF-8\", \"A\": \"C\"})\n\n    result = run_dotenv(\n        [\"run\", \"--no-override\", \"printenv\", \"A\"], cwd=tmp_path, env=env\n    )\n\n    check_process(result, exit_code=0, stdout=\"C\\n\")\n\n\ndef test_run_with_none_value(tmp_path):\n    (tmp_path / \".env\").write_text(\"A=x\\nc\")\n\n    result = run_dotenv([\"run\", \"printenv\", \"A\"], cwd=tmp_path)\n\n    check_process(result, exit_code=0, stdout=\"x\\n\")\n\n\ndef test_run_with_other_env(dotenv_path, tmp_path):\n    dotenv_path.write_text(\"A=x\")\n\n    result = run_dotenv(\n        [\"--file\", str(dotenv_path), \"run\", \"printenv\", \"A\"],\n        cwd=tmp_path,\n    )\n\n    check_process(result, exit_code=0, stdout=\"x\\n\")\n\n\ndef test_run_without_cmd(tmp_path):\n    result = run_dotenv([\"run\"], cwd=tmp_path)\n\n    check_process(result, exit_code=2)\n    assert \"Invalid value for '-f'\" in result.stderr\n\n\ndef test_run_with_invalid_cmd(dotenv_path, tmp_path):\n    result = run_dotenv(\n        [\"--file\", str(dotenv_path), \"run\", \"i_do_not_exist\"],\n        cwd=tmp_path,\n    )\n\n    check_process(result, exit_code=1)\n    assert \"Command not found: i_do_not_exist\" in result.stderr\n\n\ndef test_run_with_env_missing_and_invalid_cmd(tmp_path):\n    \"\"\"\n    Check that an .env file missing takes precedence over a command not found error.\n    \"\"\"\n\n    result = run_dotenv([\"run\", \"i_do_not_exist\"], cwd=tmp_path)\n\n    check_process(result, exit_code=2)\n    assert \"Invalid value for '-f'\" in result.stderr\n\n\ndef test_run_with_version(tmp_path):\n    result = run_dotenv([\"--version\"], cwd=tmp_path)\n\n    check_process(result, exit_code=0)\n    assert result.stdout.strip().endswith(__version__)\n\n\ndef test_run_with_command_flags(dotenv_path, tmp_path):\n    \"\"\"\n    Check that command flags passed after `dotenv run` are not interpreted.\n\n    Here, we want to run `printenv --version`, not `dotenv --version`.\n    \"\"\"\n\n    result = run_dotenv(\n        [\"--file\", str(dotenv_path), \"run\", \"printenv\", \"--version\"],\n        cwd=tmp_path,\n    )\n\n    check_process(result, exit_code=0)\n    assert result.stdout.strip().startswith(\"printenv \")\n\n\ndef test_run_with_dotenv_and_command_flags(dotenv_path, tmp_path):\n    \"\"\"\n    Check that dotenv flags supersede command flags.\n    \"\"\"\n\n    result = run_dotenv(\n        [\"--version\", \"--file\", str(dotenv_path), \"run\", \"printenv\", \"--version\"],\n        cwd=tmp_path,\n    )\n\n    check_process(result, exit_code=0)\n    assert result.stdout.strip().startswith(\"dotenv, version\")\n"
  },
  {
    "path": "tests/test_fifo_dotenv.py",
    "content": "import os\nimport pathlib\nimport sys\nimport threading\n\nimport pytest\n\nfrom dotenv import load_dotenv\n\npytestmark = pytest.mark.skipif(sys.platform == \"win32\", reason=\"FIFOs are Unix-only\")\n\n\ndef test_load_dotenv_from_fifo(tmp_path: pathlib.Path, monkeypatch):\n    fifo = tmp_path / \".env\"\n    os.mkfifo(fifo)  # create named pipe\n\n    def writer():\n        with open(fifo, \"w\", encoding=\"utf-8\") as w:\n            w.write(\"MY_PASSWORD=pipe-secret\\n\")\n\n    t = threading.Thread(target=writer)\n    t.start()\n\n    # Ensure env is clean\n    monkeypatch.delenv(\"MY_PASSWORD\", raising=False)\n\n    ok = load_dotenv(dotenv_path=str(fifo), override=True)\n    t.join(timeout=2)\n\n    assert ok is True\n    assert os.getenv(\"MY_PASSWORD\") == \"pipe-secret\"\n"
  },
  {
    "path": "tests/test_ipython.py",
    "content": "import os\nimport sys\nfrom unittest import mock\n\nimport pytest\n\npytest.importorskip(\"IPython\")\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_ipython_existing_variable_no_override(tmp_path):\n    from IPython.terminal.embed import InteractiveShellEmbed\n\n    dotenv_file = tmp_path / \".env\"\n    dotenv_file.write_text(\"a=b\\n\")\n    os.chdir(tmp_path)\n    os.environ[\"a\"] = \"c\"\n\n    ipshell = InteractiveShellEmbed()\n    ipshell.run_line_magic(\"load_ext\", \"dotenv\")\n    ipshell.run_line_magic(\"dotenv\", \"\")\n\n    assert os.environ == {\"a\": \"c\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_ipython_existing_variable_override(tmp_path):\n    from IPython.terminal.embed import InteractiveShellEmbed\n\n    dotenv_file = tmp_path / \".env\"\n    dotenv_file.write_text(\"a=b\\n\")\n    os.chdir(tmp_path)\n    os.environ[\"a\"] = \"c\"\n\n    ipshell = InteractiveShellEmbed()\n    ipshell.run_line_magic(\"load_ext\", \"dotenv\")\n    ipshell.run_line_magic(\"dotenv\", \"-o\")\n\n    assert os.environ == {\"a\": \"b\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_ipython_new_variable(tmp_path):\n    from IPython.terminal.embed import InteractiveShellEmbed\n\n    dotenv_file = tmp_path / \".env\"\n    dotenv_file.write_text(\"a=b\\n\")\n    os.chdir(tmp_path)\n\n    ipshell = InteractiveShellEmbed()\n    ipshell.run_line_magic(\"load_ext\", \"dotenv\")\n    ipshell.run_line_magic(\"dotenv\", \"\")\n\n    assert os.environ == {\"a\": \"b\"}\n"
  },
  {
    "path": "tests/test_is_interactive.py",
    "content": "import builtins\nimport sys\nfrom unittest import mock\n\nfrom dotenv.main import find_dotenv\n\n\nclass TestIsInteractive:\n    \"\"\"Tests for the _is_interactive helper function within find_dotenv.\n\n    The _is_interactive function is used by find_dotenv to determine if the code\n    is running in an interactive environment (like a REPL, IPython notebook, etc.)\n    versus a normal script execution.\n\n    Interactive environments include:\n    - Python REPL (has sys.ps1 or sys.ps2)\n    - IPython notebooks (no __file__ in __main__)\n    - Interactive shells\n\n    Non-interactive environments include:\n    - Normal script execution (has __file__ in __main__)\n    - Module imports\n\n    Examples of the behavior:\n    >>> import sys\n    >>> # In a REPL:\n    >>> hasattr(sys, 'ps1')  # True\n    >>> # In a script:\n    >>> hasattr(sys, 'ps1')  # False\n    \"\"\"\n\n    def _create_dotenv_file(self, tmp_path):\n        \"\"\"Helper to create a test .env file.\"\"\"\n        dotenv_path = tmp_path / \".env\"\n        dotenv_path.write_text(\"TEST=value\")\n        return dotenv_path\n\n    def _setup_subdir_and_chdir(self, tmp_path, monkeypatch):\n        \"\"\"Helper to create subdirectory and change to it.\"\"\"\n        test_dir = tmp_path / \"subdir\"\n        test_dir.mkdir()\n        monkeypatch.chdir(test_dir)\n        return test_dir\n\n    def _remove_ps_attributes(self, monkeypatch):\n        \"\"\"Helper to remove ps1/ps2 attributes if they exist.\"\"\"\n        if hasattr(sys, \"ps1\"):\n            monkeypatch.delattr(sys, \"ps1\")\n        if hasattr(sys, \"ps2\"):\n            monkeypatch.delattr(sys, \"ps2\")\n\n    def _mock_main_import(self, monkeypatch, mock_main_module):\n        \"\"\"Helper to mock __main__ module import.\"\"\"\n        original_import = builtins.__import__\n\n        def mock_import(name, *args, **kwargs):\n            if name == \"__main__\":\n                return mock_main_module\n            return original_import(name, *args, **kwargs)\n\n        monkeypatch.setattr(builtins, \"__import__\", mock_import)\n\n    def _mock_main_import_error(self, monkeypatch):\n        \"\"\"Helper to mock __main__ module import that raises ModuleNotFoundError.\"\"\"\n        original_import = builtins.__import__\n\n        def mock_import(name, *args, **kwargs):\n            if name == \"__main__\":\n                raise ModuleNotFoundError(\"No module named '__main__'\")\n            return original_import(name, *args, **kwargs)\n\n        monkeypatch.setattr(builtins, \"__import__\", mock_import)\n\n    def test_is_interactive_with_ps1(self, tmp_path, monkeypatch):\n        \"\"\"Test that _is_interactive returns True when sys.ps1 exists.\"\"\"\n        dotenv_path = self._create_dotenv_file(tmp_path)\n\n        # Mock sys.ps1 to simulate interactive shell\n        monkeypatch.setattr(sys, \"ps1\", \">>> \", raising=False)\n\n        self._setup_subdir_and_chdir(tmp_path, monkeypatch)\n\n        # When _is_interactive() returns True, find_dotenv should search from cwd\n        result = find_dotenv()\n        assert result == str(dotenv_path)\n\n    def test_is_interactive_with_ps2(self, tmp_path, monkeypatch):\n        \"\"\"Test that _is_interactive returns True when sys.ps2 exists.\"\"\"\n        dotenv_path = self._create_dotenv_file(tmp_path)\n\n        # Mock sys.ps2 to simulate multi-line interactive input\n        monkeypatch.setattr(sys, \"ps2\", \"... \", raising=False)\n\n        self._setup_subdir_and_chdir(tmp_path, monkeypatch)\n\n        # When _is_interactive() returns True, find_dotenv should search from cwd\n        result = find_dotenv()\n        assert result == str(dotenv_path)\n\n    def test_is_interactive_main_module_not_found(self, tmp_path, monkeypatch):\n        \"\"\"Test that _is_interactive returns False when __main__ module import fails.\"\"\"\n        self._remove_ps_attributes(monkeypatch)\n        self._mock_main_import_error(monkeypatch)\n\n        # Change to directory and test\n        monkeypatch.chdir(tmp_path)\n\n        # Since _is_interactive() returns False, find_dotenv should not find anything\n        # without usecwd=True\n        result = find_dotenv()\n        assert result == \"\"\n\n    def test_is_interactive_main_without_file(self, tmp_path, monkeypatch):\n        \"\"\"Test that _is_interactive returns True when __main__ has no __file__ attribute.\"\"\"\n        self._remove_ps_attributes(monkeypatch)\n        dotenv_path = self._create_dotenv_file(tmp_path)\n\n        # Mock __main__ module without __file__ attribute\n        mock_main = mock.MagicMock()\n        del mock_main.__file__  # Remove __file__ attribute\n\n        self._mock_main_import(monkeypatch, mock_main)\n        self._setup_subdir_and_chdir(tmp_path, monkeypatch)\n\n        # When _is_interactive() returns True, find_dotenv should search from cwd\n        result = find_dotenv()\n        assert result == str(dotenv_path)\n\n    def test_is_interactive_main_with_file(self, tmp_path, monkeypatch):\n        \"\"\"Test that _is_interactive returns False when __main__ has __file__ attribute.\"\"\"\n        self._remove_ps_attributes(monkeypatch)\n\n        # Mock __main__ module with __file__ attribute\n        mock_main = mock.MagicMock()\n        mock_main.__file__ = \"/path/to/script.py\"\n\n        self._mock_main_import(monkeypatch, mock_main)\n\n        # Change to directory and test\n        monkeypatch.chdir(tmp_path)\n\n        # Since _is_interactive() returns False, find_dotenv should not find anything\n        # without usecwd=True\n        result = find_dotenv()\n        assert result == \"\"\n\n    def test_is_interactive_precedence_ps1_over_main(self, tmp_path, monkeypatch):\n        \"\"\"Test that ps1/ps2 attributes take precedence over __main__ module check.\"\"\"\n        dotenv_path = self._create_dotenv_file(tmp_path)\n\n        # Set ps1 attribute\n        monkeypatch.setattr(sys, \"ps1\", \">>> \", raising=False)\n\n        # Mock __main__ module with __file__ attribute (which would normally return False)\n        mock_main = mock.MagicMock()\n        mock_main.__file__ = \"/path/to/script.py\"\n\n        self._mock_main_import(monkeypatch, mock_main)\n        self._setup_subdir_and_chdir(tmp_path, monkeypatch)\n\n        # ps1 should take precedence, so _is_interactive() returns True\n        result = find_dotenv()\n        assert result == str(dotenv_path)\n\n    def test_is_interactive_ps1_and_ps2_both_exist(self, tmp_path, monkeypatch):\n        \"\"\"Test that _is_interactive returns True when both ps1 and ps2 exist.\"\"\"\n        dotenv_path = self._create_dotenv_file(tmp_path)\n\n        # Set both ps1 and ps2 attributes\n        monkeypatch.setattr(sys, \"ps1\", \">>> \", raising=False)\n        monkeypatch.setattr(sys, \"ps2\", \"... \", raising=False)\n\n        self._setup_subdir_and_chdir(tmp_path, monkeypatch)\n\n        # Should return True with either attribute present\n        result = find_dotenv()\n        assert result == str(dotenv_path)\n\n    def test_is_interactive_main_module_with_file_attribute_none(\n        self, tmp_path, monkeypatch\n    ):\n        \"\"\"Test _is_interactive when __main__ has __file__ attribute set to None.\"\"\"\n        self._remove_ps_attributes(monkeypatch)\n\n        # Mock __main__ module with __file__ = None\n        mock_main = mock.MagicMock()\n        mock_main.__file__ = None\n\n        self._mock_main_import(monkeypatch, mock_main)\n\n        # Mock sys.gettrace to ensure debugger detection returns False\n        monkeypatch.setattr(\"sys.gettrace\", lambda: None)\n\n        monkeypatch.chdir(tmp_path)\n\n        # __file__ = None should still be considered non-interactive\n        # and with no debugger, find_dotenv should not search from cwd\n        result = find_dotenv()\n        assert result == \"\"\n\n    def test_is_interactive_no_ps_attributes_and_normal_execution(\n        self, tmp_path, monkeypatch\n    ):\n        \"\"\"Test normal script execution scenario where _is_interactive should return False.\"\"\"\n        self._remove_ps_attributes(monkeypatch)\n\n        # Don't mock anything - let it use the real __main__ module\n        # which should have a __file__ attribute in normal execution\n\n        # Change to directory and test\n        monkeypatch.chdir(tmp_path)\n\n        # In normal execution, _is_interactive() should return False\n        # so find_dotenv should not find anything without usecwd=True\n        result = find_dotenv()\n        assert result == \"\"\n\n    def test_is_interactive_with_usecwd_override(self, tmp_path, monkeypatch):\n        \"\"\"Test that usecwd=True overrides _is_interactive behavior.\"\"\"\n        self._remove_ps_attributes(monkeypatch)\n        dotenv_path = self._create_dotenv_file(tmp_path)\n\n        # Mock __main__ module with __file__ attribute (non-interactive)\n        mock_main = mock.MagicMock()\n        mock_main.__file__ = \"/path/to/script.py\"\n\n        self._mock_main_import(monkeypatch, mock_main)\n        self._setup_subdir_and_chdir(tmp_path, monkeypatch)\n\n        # Even though _is_interactive() returns False, usecwd=True should find the file\n        result = find_dotenv(usecwd=True)\n        assert result == str(dotenv_path)\n"
  },
  {
    "path": "tests/test_lib.py",
    "content": "import subprocess\nfrom pathlib import Path\nfrom typing import Sequence\n\n\ndef run_dotenv(\n    args: Sequence[str],\n    cwd: str | Path | None = None,\n    env: dict | None = None,\n) -> subprocess.CompletedProcess:\n    \"\"\"\n    Run the `dotenv` CLI in a subprocess with the given arguments.\n    \"\"\"\n\n    process = subprocess.run(\n        [\"dotenv\", *args],\n        capture_output=True,\n        text=True,\n        cwd=cwd,\n        env=env,\n    )\n\n    return process\n\n\ndef check_process(\n    process: subprocess.CompletedProcess,\n    exit_code: int,\n    stdout: str | None = None,\n):\n    \"\"\"\n    Check that the process completed with the expected exit code and output.\n\n    This provides better error messages than directly checking the attributes.\n    \"\"\"\n\n    assert process.returncode == exit_code, (\n        f\"Unexpected exit code {process.returncode} (expected {exit_code})\\n\"\n        f\"stdout:\\n{process.stdout}\\n\"\n        f\"stderr:\\n{process.stderr}\"\n    )\n\n    if stdout is not None:\n        assert process.stdout == stdout, (\n            f\"Unexpected output: {process.stdout.strip()!r} (expected {stdout!r})\"\n        )\n"
  },
  {
    "path": "tests/test_main.py",
    "content": "import io\nimport logging\nimport os\nimport stat\nimport subprocess\nimport sys\nimport textwrap\nfrom unittest import mock\n\nimport pytest\n\nimport dotenv\n\n\ndef test_set_key_no_file(tmp_path):\n    nx_path = tmp_path / \"nx\"\n    logger = logging.getLogger(\"dotenv.main\")\n\n    with mock.patch.object(logger, \"warning\"):\n        result = dotenv.set_key(nx_path, \"foo\", \"bar\")\n\n    assert result == (True, \"foo\", \"bar\")\n    assert nx_path.exists()\n\n\n@pytest.mark.parametrize(\n    \"before,key,value,expected,after\",\n    [\n        (\"\", \"a\", \"\", (True, \"a\", \"\"), \"a=''\\n\"),\n        (\"\", \"a\", \"b\", (True, \"a\", \"b\"), \"a='b'\\n\"),\n        (\"\", \"a\", \"'b'\", (True, \"a\", \"'b'\"), \"a='\\\\'b\\\\''\\n\"),\n        (\"\", \"a\", '\"b\"', (True, \"a\", '\"b\"'), \"a='\\\"b\\\"'\\n\"),\n        (\"\", \"a\", \"b'c\", (True, \"a\", \"b'c\"), \"a='b\\\\'c'\\n\"),\n        (\"\", \"a\", 'b\"c', (True, \"a\", 'b\"c'), \"a='b\\\"c'\\n\"),\n        (\"a=b\", \"a\", \"c\", (True, \"a\", \"c\"), \"a='c'\\n\"),\n        (\"a=b\\n\", \"a\", \"c\", (True, \"a\", \"c\"), \"a='c'\\n\"),\n        (\"a=b\\n\\n\", \"a\", \"c\", (True, \"a\", \"c\"), \"a='c'\\n\\n\"),\n        (\"a=b\\nc=d\", \"a\", \"e\", (True, \"a\", \"e\"), \"a='e'\\nc=d\"),\n        (\"a=b\\nc=d\\ne=f\", \"c\", \"g\", (True, \"c\", \"g\"), \"a=b\\nc='g'\\ne=f\"),\n        (\"a=b\\n\", \"c\", \"d\", (True, \"c\", \"d\"), \"a=b\\nc='d'\\n\"),\n        (\"a=b\", \"c\", \"d\", (True, \"c\", \"d\"), \"a=b\\nc='d'\\n\"),\n    ],\n)\ndef test_set_key(dotenv_path, before, key, value, expected, after):\n    logger = logging.getLogger(\"dotenv.main\")\n    dotenv_path.write_text(before)\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.set_key(dotenv_path, key, value)\n\n    assert result == expected\n    assert dotenv_path.read_text() == after\n    mock_warning.assert_not_called()\n\n\ndef test_set_key_encoding(dotenv_path):\n    encoding = \"latin-1\"\n\n    result = dotenv.set_key(dotenv_path, \"a\", \"é\", encoding=encoding)\n\n    assert result == (True, \"a\", \"é\")\n    assert dotenv_path.read_text(encoding=encoding) == \"a='é'\\n\"\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"file mode bits behave differently on Windows\"\n)\ndef test_set_key_preserves_file_mode(dotenv_path):\n    dotenv_path.write_text(\"a=x\\n\")\n    dotenv_path.chmod(0o640)\n    mode_before = stat.S_IMODE(dotenv_path.stat().st_mode)\n\n    dotenv.set_key(dotenv_path, \"a\", \"y\")\n\n    mode_after = stat.S_IMODE(dotenv_path.stat().st_mode)\n    assert mode_before == mode_after\n\n\ndef test_rewrite_closes_file_handle_on_lstat_failure(tmp_path):\n    dotenv_path = tmp_path / \".env\"\n    dotenv_path.write_text(\"a=x\\n\")\n    real_open = open\n    opened_handles = []\n\n    def tracking_open(*args, **kwargs):\n        handle = real_open(*args, **kwargs)\n        opened_handles.append(handle)\n        return handle\n\n    with mock.patch(\"dotenv.main.os.lstat\", side_effect=FileNotFoundError):\n        with mock.patch(\"dotenv.main.open\", side_effect=tracking_open):\n            dotenv.set_key(dotenv_path, \"a\", \"x\")\n\n    assert opened_handles, \"expected at least one file to be opened\"\n    assert all(handle.closed for handle in opened_handles)\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"symlinks require elevated privileges on Windows\"\n)\ndef test_set_key_symlink_to_existing_file(tmp_path):\n    target = tmp_path / \"target.env\"\n    target.write_text(\"a=x\\n\")\n    symlink = tmp_path / \".env\"\n    symlink.symlink_to(target)\n\n    dotenv.set_key(symlink, \"a\", \"y\")\n\n    assert target.read_text() == \"a=x\\n\"\n    assert not symlink.is_symlink()\n    assert \"a='y'\" in symlink.read_text()\n    assert stat.S_IMODE(symlink.stat().st_mode) == 0o600\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"symlinks require elevated privileges on Windows\"\n)\ndef test_set_key_symlink_to_missing_file(tmp_path):\n    target = tmp_path / \"nx\"\n    symlink = tmp_path / \".env\"\n    symlink.symlink_to(target)\n\n    dotenv.set_key(symlink, \"a\", \"x\")\n\n    assert not target.exists()\n    assert not symlink.is_symlink()\n    assert symlink.read_text() == \"a='x'\\n\"\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"symlinks require elevated privileges on Windows\"\n)\ndef test_set_key_follow_symlinks(tmp_path):\n    target = tmp_path / \"target.env\"\n    target.write_text(\"a=x\\n\")\n    symlink = tmp_path / \".env\"\n    symlink.symlink_to(target)\n\n    dotenv.set_key(symlink, \"a\", \"y\", follow_symlinks=True)\n\n    assert target.read_text() == \"a='y'\\n\"\n    assert symlink.is_symlink()\n\n\n@pytest.mark.skipif(\n    sys.platform != \"win32\" and os.geteuid() == 0,\n    reason=\"Root user can access files even with 000 permissions.\",\n)\ndef test_set_key_permission_error(dotenv_path):\n    if sys.platform == \"win32\":\n        # On Windows, make file read-only\n        dotenv_path.chmod(stat.S_IREAD)\n    else:\n        # On Unix, remove all permissions\n        dotenv_path.chmod(0o000)\n\n    with pytest.raises(PermissionError):\n        dotenv.set_key(dotenv_path, \"a\", \"b\")\n\n    # Restore permissions\n    if sys.platform == \"win32\":\n        dotenv_path.chmod(stat.S_IWRITE | stat.S_IREAD)\n    else:\n        dotenv_path.chmod(0o600)\n    assert dotenv_path.read_text() == \"\"\n\n\ndef test_get_key_no_file(tmp_path):\n    nx_path = tmp_path / \"nx\"\n    logger = logging.getLogger(\"dotenv.main\")\n\n    with (\n        mock.patch.object(logger, \"info\") as mock_info,\n        mock.patch.object(logger, \"warning\") as mock_warning,\n    ):\n        result = dotenv.get_key(nx_path, \"foo\")\n\n    assert result is None\n    mock_info.assert_has_calls(\n        calls=[\n            mock.call(\"python-dotenv could not find configuration file %s.\", nx_path)\n        ],\n    )\n    mock_warning.assert_has_calls(\n        calls=[mock.call(\"Key %s not found in %s.\", \"foo\", nx_path)],\n    )\n\n\ndef test_get_key_not_found(dotenv_path):\n    logger = logging.getLogger(\"dotenv.main\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.get_key(dotenv_path, \"foo\")\n\n    assert result is None\n    mock_warning.assert_called_once_with(\"Key %s not found in %s.\", \"foo\", dotenv_path)\n\n\ndef test_get_key_ok(dotenv_path):\n    logger = logging.getLogger(\"dotenv.main\")\n    dotenv_path.write_text(\"foo=bar\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.get_key(dotenv_path, \"foo\")\n\n    assert result == \"bar\"\n    mock_warning.assert_not_called()\n\n\ndef test_get_key_encoding(dotenv_path):\n    encoding = \"latin-1\"\n    dotenv_path.write_text(\"é=è\", encoding=encoding)\n\n    result = dotenv.get_key(dotenv_path, \"é\", encoding=encoding)\n\n    assert result == \"è\"\n\n\ndef test_get_key_none(dotenv_path):\n    logger = logging.getLogger(\"dotenv.main\")\n    dotenv_path.write_text(\"foo\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.get_key(dotenv_path, \"foo\")\n\n    assert result is None\n    mock_warning.assert_not_called()\n\n\ndef test_unset_with_value(dotenv_path):\n    logger = logging.getLogger(\"dotenv.main\")\n    dotenv_path.write_text(\"a=b\\nc=d\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.unset_key(dotenv_path, \"a\")\n\n    assert result == (True, \"a\")\n    assert dotenv_path.read_text() == \"c=d\"\n    mock_warning.assert_not_called()\n\n\ndef test_unset_no_value(dotenv_path):\n    logger = logging.getLogger(\"dotenv.main\")\n    dotenv_path.write_text(\"foo\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.unset_key(dotenv_path, \"foo\")\n\n    assert result == (True, \"foo\")\n    assert dotenv_path.read_text() == \"\"\n    mock_warning.assert_not_called()\n\n\ndef test_unset_encoding(dotenv_path):\n    encoding = \"latin-1\"\n    dotenv_path.write_text(\"é=x\", encoding=encoding)\n\n    result = dotenv.unset_key(dotenv_path, \"é\", encoding=encoding)\n\n    assert result == (True, \"é\")\n    assert dotenv_path.read_text(encoding=encoding) == \"\"\n\n\ndef test_unset_non_existent_file(tmp_path):\n    nx_path = tmp_path / \"nx\"\n    logger = logging.getLogger(\"dotenv.main\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.unset_key(nx_path, \"foo\")\n\n    assert result == (None, \"foo\")\n    mock_warning.assert_called_once_with(\n        \"Can't delete from %s - it doesn't exist.\",\n        nx_path,\n    )\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"symlinks require elevated privileges on Windows\"\n)\ndef test_unset_key_symlink_to_existing_file(tmp_path):\n    target = tmp_path / \"target.env\"\n    target.write_text(\"a=x\\n\")\n    symlink = tmp_path / \".env\"\n    symlink.symlink_to(target)\n\n    dotenv.unset_key(symlink, \"a\")\n\n    assert target.read_text() == \"a=x\\n\"\n    assert not symlink.is_symlink()\n    assert symlink.read_text() == \"\"\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"symlinks require elevated privileges on Windows\"\n)\ndef test_unset_key_symlink_to_missing_file(tmp_path):\n    target = tmp_path / \"nx\"\n    symlink = tmp_path / \".env\"\n    symlink.symlink_to(target)\n    logger = logging.getLogger(\"dotenv.main\")\n\n    with mock.patch.object(logger, \"warning\") as mock_warning:\n        result = dotenv.unset_key(symlink, \"a\")\n\n    assert result == (None, \"a\")\n    assert symlink.is_symlink()\n    mock_warning.assert_called_once()\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"symlinks require elevated privileges on Windows\"\n)\ndef test_unset_key_follow_symlinks(tmp_path):\n    target = tmp_path / \"target.env\"\n    target.write_text(\"a=b\\n\")\n    symlink = tmp_path / \".env\"\n    symlink.symlink_to(target)\n\n    dotenv.unset_key(symlink, \"a\", follow_symlinks=True)\n\n    assert target.read_text() == \"\"\n    assert symlink.is_symlink()\n\n\ndef prepare_file_hierarchy(path):\n    \"\"\"\n    Create a temporary folder structure like the following:\n\n        test_find_dotenv0/\n        └── child1\n            ├── child2\n            │   └── child3\n            │       └── child4\n            └── .env\n\n    Then try to automatically `find_dotenv` starting in `child4`\n    \"\"\"\n\n    leaf = path / \"child1\" / \"child2\" / \"child3\" / \"child4\"\n    leaf.mkdir(parents=True, exist_ok=True)\n    return leaf\n\n\ndef test_find_dotenv_no_file_raise(tmp_path):\n    leaf = prepare_file_hierarchy(tmp_path)\n    os.chdir(leaf)\n\n    with pytest.raises(IOError):\n        dotenv.find_dotenv(raise_error_if_not_found=True, usecwd=True)\n\n\ndef test_find_dotenv_no_file_no_raise(tmp_path):\n    leaf = prepare_file_hierarchy(tmp_path)\n    os.chdir(leaf)\n\n    result = dotenv.find_dotenv(usecwd=True)\n\n    assert result == \"\"\n\n\ndef test_find_dotenv_found(tmp_path):\n    leaf = prepare_file_hierarchy(tmp_path)\n    os.chdir(leaf)\n    dotenv_path = tmp_path / \".env\"\n    dotenv_path.write_bytes(b\"TEST=test\\n\")\n\n    result = dotenv.find_dotenv(usecwd=True)\n\n    assert result == str(dotenv_path)\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_load_dotenv_existing_file(dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    result = dotenv.load_dotenv(dotenv_path)\n\n    assert result is True\n    assert os.environ == {\"a\": \"b\"}\n\n\n@pytest.mark.parametrize(\n    \"flag_value\",\n    [\n        \"true\",\n        \"yes\",\n        \"1\",\n        \"t\",\n        \"y\",\n        \"True\",\n        \"Yes\",\n        \"TRUE\",\n        \"YES\",\n        \"T\",\n        \"Y\",\n    ],\n)\ndef test_load_dotenv_disabled(dotenv_path, flag_value):\n    expected_environ = {\"PYTHON_DOTENV_DISABLED\": flag_value}\n    with mock.patch.dict(\n        os.environ, {\"PYTHON_DOTENV_DISABLED\": flag_value}, clear=True\n    ):\n        dotenv_path.write_text(\"a=b\")\n\n        result = dotenv.load_dotenv(dotenv_path)\n\n        assert result is False\n        assert os.environ == expected_environ\n\n\n@pytest.mark.parametrize(\n    \"flag_value\",\n    [\n        \"true\",\n        \"yes\",\n        \"1\",\n        \"t\",\n        \"y\",\n        \"True\",\n        \"Yes\",\n        \"TRUE\",\n        \"YES\",\n        \"T\",\n        \"Y\",\n    ],\n)\ndef test_load_dotenv_disabled_notification(dotenv_path, flag_value):\n    with mock.patch.dict(\n        os.environ, {\"PYTHON_DOTENV_DISABLED\": flag_value}, clear=True\n    ):\n        dotenv_path.write_text(\"a=b\")\n\n        logger = logging.getLogger(\"dotenv.main\")\n        with mock.patch.object(logger, \"debug\") as mock_debug:\n            result = dotenv.load_dotenv(dotenv_path)\n\n        assert result is False\n        mock_debug.assert_called_once_with(\n            \"python-dotenv: .env loading disabled by PYTHON_DOTENV_DISABLED environment variable\"\n        )\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@pytest.mark.parametrize(\n    \"flag_value\",\n    [\n        \"\",\n        \"false\",\n        \"no\",\n        \"0\",\n        \"f\",\n        \"n\",\n        \"False\",\n        \"No\",\n        \"FALSE\",\n        \"NO\",\n        \"F\",\n        \"N\",\n    ],\n)\ndef test_load_dotenv_enabled(dotenv_path, flag_value):\n    expected_environ = {\"PYTHON_DOTENV_DISABLED\": flag_value, \"a\": \"b\"}\n    with mock.patch.dict(\n        os.environ, {\"PYTHON_DOTENV_DISABLED\": flag_value}, clear=True\n    ):\n        dotenv_path.write_text(\"a=b\")\n\n        result = dotenv.load_dotenv(dotenv_path)\n\n        assert result is True\n        assert os.environ == expected_environ\n\n\n@pytest.mark.parametrize(\n    \"flag_value\",\n    [\n        \"\",\n        \"false\",\n        \"no\",\n        \"0\",\n        \"f\",\n        \"n\",\n        \"False\",\n        \"No\",\n        \"FALSE\",\n        \"NO\",\n        \"F\",\n        \"N\",\n    ],\n)\ndef test_load_dotenv_enabled_no_notification(dotenv_path, flag_value):\n    with mock.patch.dict(\n        os.environ, {\"PYTHON_DOTENV_DISABLED\": flag_value}, clear=True\n    ):\n        dotenv_path.write_text(\"a=b\")\n\n        logger = logging.getLogger(\"dotenv.main\")\n        with mock.patch.object(logger, \"debug\") as mock_debug:\n            result = dotenv.load_dotenv(dotenv_path)\n\n        assert result is True\n        mock_debug.assert_not_called()\n\n\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_load_dotenv_doesnt_disable_itself(dotenv_path):\n    dotenv_path.write_text(\"PYTHON_DOTENV_DISABLED=true\")\n\n    result = dotenv.load_dotenv(dotenv_path)\n\n    assert result is True\n    assert os.environ == {\"PYTHON_DOTENV_DISABLED\": \"true\"}\n\n\ndef test_load_dotenv_no_file_verbose():\n    logger = logging.getLogger(\"dotenv.main\")\n\n    with mock.patch.object(logger, \"info\") as mock_info:\n        result = dotenv.load_dotenv(\".does_not_exist\", verbose=True)\n\n    assert result is False\n    mock_info.assert_called_once_with(\n        \"python-dotenv could not find configuration file %s.\", \".does_not_exist\"\n    )\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {\"a\": \"c\"}, clear=True)\ndef test_load_dotenv_existing_variable_no_override(dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    result = dotenv.load_dotenv(dotenv_path, override=False)\n\n    assert result is True\n    assert os.environ == {\"a\": \"c\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {\"a\": \"c\"}, clear=True)\ndef test_load_dotenv_existing_variable_override(dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    result = dotenv.load_dotenv(dotenv_path, override=True)\n\n    assert result is True\n    assert os.environ == {\"a\": \"b\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {\"a\": \"c\"}, clear=True)\ndef test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path):\n    dotenv_path.write_text('a=b\\nd=\"${a}\"')\n\n    result = dotenv.load_dotenv(dotenv_path)\n\n    assert result is True\n    assert os.environ == {\"a\": \"c\", \"d\": \"c\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {\"a\": \"c\"}, clear=True)\ndef test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path):\n    dotenv_path.write_text('a=b\\nd=\"${a}\"')\n\n    result = dotenv.load_dotenv(dotenv_path, override=True)\n\n    assert result is True\n    assert os.environ == {\"a\": \"b\", \"d\": \"b\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_load_dotenv_string_io_utf_8():\n    stream = io.StringIO(\"a=à\")\n\n    result = dotenv.load_dotenv(stream=stream)\n\n    assert result is True\n    assert os.environ == {\"a\": \"à\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@mock.patch.dict(os.environ, {}, clear=True)\ndef test_load_dotenv_file_stream(dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    with dotenv_path.open() as f:\n        result = dotenv.load_dotenv(stream=f)\n\n    assert result is True\n    assert os.environ == {\"a\": \"b\"}\n\n\ndef test_load_dotenv_in_current_dir(tmp_path):\n    dotenv_path = tmp_path / \".env\"\n    dotenv_path.write_bytes(b\"a=b\")\n    code_path = tmp_path / \"code.py\"\n    code_path.write_text(\n        textwrap.dedent(\"\"\"\n        import dotenv\n        import os\n\n        dotenv.load_dotenv(verbose=True)\n        print(os.environ['a'])\n    \"\"\")\n    )\n    os.chdir(tmp_path)\n\n    result = subprocess.run(\n        [sys.executable, str(code_path)],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n\n    assert result.stdout == \"b\\n\"\n\n\ndef test_dotenv_values_file(dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    result = dotenv.dotenv_values(dotenv_path)\n\n    assert result == {\"a\": \"b\"}\n\n\n@pytest.mark.skipif(\n    sys.platform == \"win32\", reason=\"This test assumes case-sensitive variable names\"\n)\n@pytest.mark.parametrize(\n    \"env,string,interpolate,expected\",\n    [\n        # Defined in environment, with and without interpolation\n        ({\"b\": \"c\"}, \"a=$b\", False, {\"a\": \"$b\"}),\n        ({\"b\": \"c\"}, \"a=$b\", True, {\"a\": \"$b\"}),\n        ({\"b\": \"c\"}, \"a=${b}\", False, {\"a\": \"${b}\"}),\n        ({\"b\": \"c\"}, \"a=${b}\", True, {\"a\": \"c\"}),\n        ({\"b\": \"c\"}, \"a=${b:-d}\", False, {\"a\": \"${b:-d}\"}),\n        ({\"b\": \"c\"}, \"a=${b:-d}\", True, {\"a\": \"c\"}),\n        # Defined in file\n        ({}, \"b=c\\na=${b}\", True, {\"a\": \"c\", \"b\": \"c\"}),\n        # Undefined\n        ({}, \"a=${b}\", True, {\"a\": \"\"}),\n        ({}, \"a=${b:-d}\", True, {\"a\": \"d\"}),\n        # With quotes\n        ({\"b\": \"c\"}, 'a=\"${b}\"', True, {\"a\": \"c\"}),\n        ({\"b\": \"c\"}, \"a='${b}'\", True, {\"a\": \"c\"}),\n        # With surrounding text\n        ({\"b\": \"c\"}, \"a=x${b}y\", True, {\"a\": \"xcy\"}),\n        # Self-referential\n        ({\"a\": \"b\"}, \"a=${a}\", True, {\"a\": \"b\"}),\n        ({}, \"a=${a}\", True, {\"a\": \"\"}),\n        ({\"a\": \"b\"}, \"a=${a:-c}\", True, {\"a\": \"b\"}),\n        ({}, \"a=${a:-c}\", True, {\"a\": \"c\"}),\n        # Reused\n        ({\"b\": \"c\"}, \"a=${b}${b}\", True, {\"a\": \"cc\"}),\n        # Re-defined and used in file\n        ({\"b\": \"c\"}, \"b=d\\na=${b}\", True, {\"a\": \"d\", \"b\": \"d\"}),\n        ({}, \"a=b\\na=c\\nd=${a}\", True, {\"a\": \"c\", \"d\": \"c\"}),\n        ({}, \"a=b\\nc=${a}\\nd=e\\nc=${d}\", True, {\"a\": \"b\", \"c\": \"e\", \"d\": \"e\"}),\n    ],\n)\ndef test_dotenv_values_string_io(env, string, interpolate, expected):\n    with mock.patch.dict(os.environ, env, clear=True):\n        stream = io.StringIO(string)\n        stream.seek(0)\n\n        result = dotenv.dotenv_values(stream=stream, interpolate=interpolate)\n\n        assert result == expected\n\n\ndef test_dotenv_values_file_stream(dotenv_path):\n    dotenv_path.write_text(\"a=b\")\n\n    with dotenv_path.open() as f:\n        result = dotenv.dotenv_values(stream=f)\n\n    assert result == {\"a\": \"b\"}\n"
  },
  {
    "path": "tests/test_parser.py",
    "content": "import io\n\nimport pytest\n\nfrom dotenv.parser import Binding, Original, parse_stream\n\n\n@pytest.mark.parametrize(\n    \"test_input,expected\",\n    [\n        (\"\", []),\n        (\n            \"a=b\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"'a'=b\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"'a'=b\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"[=b\",\n            [\n                Binding(\n                    key=\"[\",\n                    value=\"b\",\n                    original=Original(string=\"[=b\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \" a = b \",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\" a = b \", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"export a=b\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"export a=b\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \" export 'a'=b\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\" export 'a'=b\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"# a=b\",\n            [\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"# a=b\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b#c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b#c\",\n                    original=Original(string=\"a=b#c\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b #c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b #c\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b\\t#c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\t#c\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b c\",\n                    original=Original(string=\"a=b c\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b\\tc\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\tc\",\n                    original=Original(string=\"a=b\\tc\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b  c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b  c\",\n                    original=Original(string=\"a=b  c\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b\\u00a0 c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\u00a0 c\",\n                    original=Original(string=\"a=b\\u00a0 c\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=b c \",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b c\",\n                    original=Original(string=\"a=b c \", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a='b c '\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b c \",\n                    original=Original(string=\"a='b c '\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            'a=\"b c \"',\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b c \",\n                    original=Original(string='a=\"b c \"', line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"export export_a=1\",\n            [\n                Binding(\n                    key=\"export_a\",\n                    value=\"1\",\n                    original=Original(string=\"export export_a=1\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"export port=8000\",\n            [\n                Binding(\n                    key=\"port\",\n                    value=\"8000\",\n                    original=Original(string=\"export port=8000\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            'a=\"b\\nc\"',\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\nc\",\n                    original=Original(string='a=\"b\\nc\"', line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a='b\\nc'\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\nc\",\n                    original=Original(string=\"a='b\\nc'\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            'a=\"b\\nc\"',\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\nc\",\n                    original=Original(string='a=\"b\\nc\"', line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            'a=\"b\\\\nc\"',\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\nc\",\n                    original=Original(string='a=\"b\\\\nc\"', line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a='b\\\\nc'\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\\\\nc\",\n                    original=Original(string=\"a='b\\\\nc'\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            'a=\"b\\\\\"c\"',\n            [\n                Binding(\n                    key=\"a\",\n                    value='b\"c',\n                    original=Original(string='a=\"b\\\\\"c\"', line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a='b\\\\'c'\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b'c\",\n                    original=Original(string=\"a='b\\\\'c'\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a=à\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"à\",\n                    original=Original(string=\"a=à\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            'a=\"à\"',\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"à\",\n                    original=Original(string='a=\"à\"', line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"no_value_var\",\n            [\n                Binding(\n                    key=\"no_value_var\",\n                    value=None,\n                    original=Original(string=\"no_value_var\", line=1),\n                    error=False,\n                )\n            ],\n        ),\n        (\n            \"a: b\",\n            [\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"a: b\", line=1),\n                    error=True,\n                )\n            ],\n        ),\n        (\n            \"a=b\\nc=d\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=\"c\",\n                    value=\"d\",\n                    original=Original(string=\"c=d\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"a=b\\rc=d\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\r\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=\"c\",\n                    value=\"d\",\n                    original=Original(string=\"c=d\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"a=b\\r\\nc=d\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\r\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=\"c\",\n                    value=\"d\",\n                    original=Original(string=\"c=d\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"a=\\nb=c\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"\",\n                    original=Original(string=\"a=\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=\"b\",\n                    value=\"c\",\n                    original=Original(string=\"b=c\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"\\n\\n\",\n            [\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"\\n\\n\", line=1),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"a=b\\n\\n\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"\\n\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"a=b\\n\\nc=d\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=\"c\",\n                    value=\"d\",\n                    original=Original(string=\"\\nc=d\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            'a=\"\\nb=c',\n            [\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string='a=\"\\n', line=1),\n                    error=True,\n                ),\n                Binding(\n                    key=\"b\",\n                    value=\"c\",\n                    original=Original(string=\"b=c\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            '# comment\\na=\"b\\nc\"\\nd=e\\n',\n            [\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"# comment\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=\"a\",\n                    value=\"b\\nc\",\n                    original=Original(string='a=\"b\\nc\"\\n', line=2),\n                    error=False,\n                ),\n                Binding(\n                    key=\"d\",\n                    value=\"e\",\n                    original=Original(string=\"d=e\\n\", line=4),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"a=b\\n# comment 1\",\n            [\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"# comment 1\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            \"# comment 1\\n# comment 2\",\n            [\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"# comment 1\\n\", line=1),\n                    error=False,\n                ),\n                Binding(\n                    key=None,\n                    value=None,\n                    original=Original(string=\"# comment 2\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n        (\n            'uglyKey[%$=\"S3cr3t_P4ssw#rD\" #\\na=b',\n            [\n                Binding(\n                    key=\"uglyKey[%$\",\n                    value=\"S3cr3t_P4ssw#rD\",\n                    original=Original(\n                        string='uglyKey[%$=\"S3cr3t_P4ssw#rD\" #\\n', line=1\n                    ),\n                    error=False,\n                ),\n                Binding(\n                    key=\"a\",\n                    value=\"b\",\n                    original=Original(string=\"a=b\", line=2),\n                    error=False,\n                ),\n            ],\n        ),\n    ],\n)\ndef test_parse_stream(test_input, expected):\n    result = parse_stream(io.StringIO(test_input))\n\n    assert list(result) == expected\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "from dotenv import get_cli_string as c\n\n\ndef test_to_cli_string():\n    assert c() == \"dotenv\"\n    assert c(path=\"/etc/.env\") == \"dotenv -f /etc/.env\"\n    assert c(path=\"/etc/.env\", action=\"list\") == \"dotenv -f /etc/.env list\"\n    assert c(action=\"list\") == \"dotenv list\"\n    assert c(action=\"get\", key=\"DEBUG\") == \"dotenv get DEBUG\"\n    assert c(action=\"set\", key=\"DEBUG\", value=\"True\") == \"dotenv set DEBUG True\"\n    assert (\n        c(action=\"set\", key=\"SECRET\", value=\"=@asdfasf\")\n        == \"dotenv set SECRET =@asdfasf\"\n    )\n    assert c(action=\"set\", key=\"SECRET\", value=\"a b\") == 'dotenv set SECRET \"a b\"'\n    assert (\n        c(action=\"set\", key=\"SECRET\", value=\"a b\", quote=\"always\")\n        == 'dotenv -q always set SECRET \"a b\"'\n    )\n"
  },
  {
    "path": "tests/test_variables.py",
    "content": "import pytest\n\nfrom dotenv.variables import Literal, Variable, parse_variables\n\n\n@pytest.mark.parametrize(\n    \"value,expected\",\n    [\n        (\"\", []),\n        (\"a\", [Literal(value=\"a\")]),\n        (\"${a}\", [Variable(name=\"a\", default=None)]),\n        (\"${a:-b}\", [Variable(name=\"a\", default=\"b\")]),\n        (\n            \"${a}${b}\",\n            [\n                Variable(name=\"a\", default=None),\n                Variable(name=\"b\", default=None),\n            ],\n        ),\n        (\n            \"a${b}c${d}e\",\n            [\n                Literal(value=\"a\"),\n                Variable(name=\"b\", default=None),\n                Literal(value=\"c\"),\n                Variable(name=\"d\", default=None),\n                Literal(value=\"e\"),\n            ],\n        ),\n    ],\n)\ndef test_parse_variables(value, expected):\n    result = parse_variables(value)\n\n    assert list(result) == expected\n"
  },
  {
    "path": "tests/test_zip_imports.py",
    "content": "import os\nimport posixpath\nimport subprocess\nimport sys\nimport textwrap\nfrom typing import List\nfrom unittest import mock\nfrom zipfile import ZipFile\n\n\ndef walk_to_root(path: str):\n    last_dir = None\n    current_dir = path\n    while last_dir != current_dir:\n        yield current_dir\n        parent_dir = posixpath.dirname(current_dir)\n        last_dir, current_dir = current_dir, parent_dir\n\n\nclass FileToAdd:\n    def __init__(self, content: str, path: str):\n        self.content = content\n        self.path = path\n\n\ndef setup_zipfile(path, files: List[FileToAdd]):\n    zip_file_path = path / \"test.zip\"\n    dirs_init_py_added_to = set()\n    with ZipFile(zip_file_path, \"w\") as zipfile:\n        for f in files:\n            zipfile.writestr(data=f.content, zinfo_or_arcname=f.path)\n            for dirname in walk_to_root(posixpath.dirname(f.path)):\n                if dirname not in dirs_init_py_added_to:\n                    init_path = posixpath.join(dirname, \"__init__.py\")\n                    print(f\"setup_zipfile: {init_path}\")\n                    zipfile.writestr(data=\"\", zinfo_or_arcname=init_path)\n                    dirs_init_py_added_to.add(dirname)\n    return zip_file_path\n\n\n@mock.patch.object(sys, \"path\", list(sys.path))\ndef test_load_dotenv_gracefully_handles_zip_imports_when_no_env_file(tmp_path):\n    zip_file_path = setup_zipfile(\n        tmp_path,\n        [\n            FileToAdd(\n                content=textwrap.dedent(\n                    \"\"\"\n            from dotenv import load_dotenv\n\n            load_dotenv()\n        \"\"\"\n                ),\n                path=\"child1/child2/test.py\",\n            ),\n        ],\n    )\n\n    # Should run without an error\n    sys.path.append(str(zip_file_path))\n    import child1.child2.test  # noqa\n\n\ndef test_load_dotenv_outside_zip_file_when_called_in_zipfile(tmp_path):\n    zip_file_path = setup_zipfile(\n        tmp_path,\n        [\n            FileToAdd(\n                content=textwrap.dedent(\n                    \"\"\"\n            from dotenv import load_dotenv\n\n            load_dotenv()\n        \"\"\"\n                ),\n                path=\"child1/child2/test.py\",\n            ),\n        ],\n    )\n    dotenv_path = tmp_path / \".env\"\n    dotenv_path.write_bytes(b\"A=x\")\n    code_path = tmp_path / \"code.py\"\n    code_path.write_text(\n        textwrap.dedent(\n            f\"\"\"\n            import os\n            import sys\n\n            sys.path.append({str(zip_file_path)!r})\n\n            import child1.child2.test\n\n            print(os.environ['A'])\n            \"\"\"\n        )\n    )\n\n    result = subprocess.run(\n        [sys.executable, str(code_path)],\n        capture_output=True,\n        check=True,\n        cwd=tmp_path,\n        text=True,\n        env={\n            k: v for k, v in os.environ.items() if k.upper() != \"A\"\n        },  # env without 'A'\n    )\n\n    assert result.stdout == \"x\\n\"\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = lint,py{310,311,312,313,314,314t},pypy3,manifest,coverage-report\n\n[gh-actions]\npython =\n    3.10: py310\n    3.11: py311\n    3.12: py312\n    3.13: py313, lint, manifest\n    3.14: py314\n    3.14t: py314t\n    pypy-3.11: pypy3\n\n[testenv]\ndeps =\n    pytest\n    pytest-cov\n    click\n    py{310,311,312,313,314,314t,pypy3}: ipython\ncommands = pytest --cov --cov-report=term-missing {posargs}\ndepends =\n    py{310,311,312,313,314,314t},pypy3: coverage-clean\n    coverage-report: py{310,311,312,313,314,314t},pypy3\n\n[testenv:lint]\nskip_install = true\ndeps =\n    ruff\n    mypy\ncommands =\n    ruff check src tests\n    ruff format --check src tests\n    mypy --python-version=3.14 src tests\n    mypy --python-version=3.13 src tests\n    mypy --python-version=3.12 src tests\n    mypy --python-version=3.11 src tests\n    mypy --python-version=3.10 src tests\n\n\n[testenv:format]\nskip_install = true\ndeps = ruff\ncommands = ruff format src tests\n\n[testenv:manifest]\ndeps = check-manifest\nskip_install = true\ncommands = check-manifest\n\n[testenv:coverage-clean]\ndeps = coverage\nskip_install = true\ncommands = coverage erase\n\n[testenv:coverage-report]\ndeps = coverage\nskip_install = true\ncommands =\n    coverage report\n"
  }
]