From 9b1ab5d333f160c2469e8d247b638c53bd05aa70 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 9 Oct 2021 13:38:53 +0530 Subject: [PATCH 01/78] Add Python 3.10 support (#359) * Add Python 3.10 support * fixup! Add Python 3.10 support * update changelog --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 8 ++++++++ setup.py | 1 + tox.ini | 8 +++++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2865cf85..e0b721de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: matrix: os: - ubuntu-latest - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, pypy3] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10", pypy3] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d1305894..d373dfb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add support for Python 3.10. (#359 by [@theskumar]) + + ## [0.19.0] - 2021-07-24 ### Changed @@ -259,6 +266,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [#172]: https://github.com/theskumar/python-dotenv/issues/172 [#176]: https://github.com/theskumar/python-dotenv/issues/176 [#183]: https://github.com/theskumar/python-dotenv/issues/183 +[#359]: https://github.com/theskumar/python-dotenv/issues/359 [@Flimm]: https://github.com/Flimm [@alanjds]: https://github.com/alanjds diff --git a/setup.py b/setup.py index 06ad2dd9..53ba5a07 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ def read_files(files): 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: PyPy', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', diff --git a/tox.ini b/tox.ini index 2cd63024..bf9bf707 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = lint,py{35,36,37,38,39},pypy3,manifest,coverage-report +envlist = lint,py{35,36,37,38,39,310},pypy3,manifest,coverage-report [gh-actions] python = @@ -7,7 +7,8 @@ python = 3.6: py36, coverage-report 3.7: py37, coverage-report 3.8: py38, coverage-report - 3.9: py39, lint, manifest, coverage-report + 3.9: py39, coverage-report + 3.10: py310, lint, manifest, coverage-report pypy3: pypy3, coverage-report [testenv] @@ -17,7 +18,7 @@ deps = coverage sh click - py{35,36,37,38,39,py3}: ipython + py{35,36,37,38,39,310,py3}: ipython commands = coverage run --parallel -m pytest {posargs} [testenv:lint] @@ -28,6 +29,7 @@ deps = types-mock commands = flake8 src tests + mypy --python-version=3.10 src tests mypy --python-version=3.9 src tests mypy --python-version=3.8 src tests mypy --python-version=3.7 src tests From fc138ce8a430b758f4f2c89bc8104f259e2cba38 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 9 Oct 2021 13:47:30 +0530 Subject: [PATCH 02/78] Release version 0.19.1 --- CHANGELOG.md | 6 +++--- setup.cfg | 2 +- src/dotenv/version.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d373dfb6..6b2b2bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.19.1] - 2021-08-09 ### Added - Add support for Python 3.10. (#359 by [@theskumar]) - ## [0.19.0] - 2021-07-24 ### Changed @@ -293,7 +292,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@yannham]: https://github.com/yannham [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...HEAD +[0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1 [0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0 [0.18.0]: https://github.com/theskumar/python-dotenv/compare/v0.17.1...v0.18.0 [0.17.1]: https://github.com/theskumar/python-dotenv/compare/v0.17.0...v0.17.1 diff --git a/setup.cfg b/setup.cfg index a20d2498..b63622d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.19.0 +current_version = 0.19.1 commit = True tag = True diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 11ac8e1a..4c1ca3c8 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "0.19.0" +__version__ = "0.19.1" From 45848bb780c26ef0adf7898656f7d3d3f4e2d8ae Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sat, 23 Oct 2021 11:34:36 +0200 Subject: [PATCH 03/78] Add missing trailing newline when adding new value Sometimes, the source file doesn't have a trailing newline. If we add a new binding in such a case, we need to add a newline before the new binding. --- CHANGELOG.md | 7 +++++++ src/dotenv/main.py | 4 ++++ tests/test_main.py | 1 + 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b2b2bbb..811ed1ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- In `set_key`, add missing newline character before new entry if necessary. (#361 by + [@bbc2]) + ## [0.19.1] - 2021-08-09 ### Added diff --git a/src/dotenv/main.py b/src/dotenv/main.py index b8d0a4e0..d867f023 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -167,13 +167,17 @@ def set_key( with rewrite(dotenv_path) as (source, dest): replaced = False + missing_newline = False for mapping in with_warn_for_invalid_lines(parse_stream(source)): if mapping.key == key_to_set: dest.write(line_out) replaced = True else: dest.write(mapping.original.string) + missing_newline = not mapping.original.string.endswith("\n") if not replaced: + if missing_newline: + dest.write("\n") dest.write(line_out) return True, key_to_set, value_to_set diff --git a/tests/test_main.py b/tests/test_main.py index 13e2791c..541ac5ee 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -37,6 +37,7 @@ def test_set_key_no_file(tmp_path): ("a=b\nc=d", "a", "e", (True, "a", "e"), "a='e'\nc=d"), ("a=b\nc=d\ne=f", "c", "g", (True, "c", "g"), "a=b\nc='g'\ne=f"), ("a=b\n", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"), + ("a=b", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"), ], ) def test_set_key(dotenv_file, before, key, value, expected, after): From 2471a5af1027acca27f8d326ddb97b1d43a2ba23 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Thu, 11 Nov 2021 13:25:19 +0100 Subject: [PATCH 04/78] Release version 0.19.2 --- CHANGELOG.md | 5 +++-- setup.cfg | 2 +- src/dotenv/version.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 811ed1ad..9b18856e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [0.19.2] - 2021-11-11 ### Fixed @@ -299,7 +299,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@yannham]: https://github.com/yannham [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...HEAD +[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2 [0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1 [0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0 [0.18.0]: https://github.com/theskumar/python-dotenv/compare/v0.17.1...v0.18.0 diff --git a/setup.cfg b/setup.cfg index b63622d6..d87b0a6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.19.1 +current_version = 0.19.2 commit = True tag = True diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 4c1ca3c8..aa070c2c 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "0.19.1" +__version__ = "0.19.2" From ba9408c5048e8e512318df423541d2b44ac6019f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 14 Jan 2022 11:23:37 +0200 Subject: [PATCH 05/78] chore: add test with Python 3.11 (#368) --- .github/workflows/test.yml | 2 +- setup.py | 1 + tox.ini | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0b721de..5135ae4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: matrix: os: - ubuntu-latest - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10", pypy3] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10", "3.11.0-alpha - 3.11", pypy3] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/setup.py b/setup.py index 53ba5a07..a8122d3a 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ def read_files(files): 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: PyPy', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', diff --git a/tox.ini b/tox.ini index bf9bf707..c1f89fa1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = lint,py{35,36,37,38,39,310},pypy3,manifest,coverage-report +envlist = lint,py{35,36,37,38,39,310,311},pypy3,manifest,coverage-report [gh-actions] python = @@ -9,6 +9,7 @@ python = 3.8: py38, coverage-report 3.9: py39, coverage-report 3.10: py310, lint, manifest, coverage-report + 3.11: py311, coverage-report pypy3: pypy3, coverage-report [testenv] @@ -18,7 +19,7 @@ deps = coverage sh click - py{35,36,37,38,39,310,py3}: ipython + py{35,36,37,38,39,310,311,py3}: ipython commands = coverage run --parallel -m pytest {posargs} [testenv:lint] @@ -29,6 +30,7 @@ deps = types-mock commands = flake8 src tests + mypy --python-version=3.11 src tests mypy --python-version=3.10 src tests mypy --python-version=3.9 src tests mypy --python-version=3.8 src tests From 157282ce24d4124e934fd543eeb83fe5a65a4234 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sat, 19 Feb 2022 14:38:01 +0100 Subject: [PATCH 06/78] Add encoding parameter to {get,set,unset}_key The parameter already exists for `dotenv_values` and `load_dotenv` and has the same meaning. --- CHANGELOG.md | 7 +++++++ src/dotenv/main.py | 29 +++++++++++++++++++---------- tests/test_main.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b18856e..3d4d014e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`. + (#379 by [@bbc2]) + ## [0.19.2] - 2021-11-11 ### Fixed diff --git a/src/dotenv/main.py b/src/dotenv/main.py index d867f023..20ac61ba 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -109,23 +109,30 @@ def get(self, key: str) -> Optional[str]: return None -def get_key(dotenv_path: Union[str, _PathLike], key_to_get: str) -> Optional[str]: +def get_key( + dotenv_path: Union[str, _PathLike], + key_to_get: str, + encoding: Optional[str] = "utf-8", +) -> Optional[str]: """ - Gets the value of a given key from the given .env + Get the value of a given key from the given .env. - If the .env path given doesn't exist, fails + Returns `None` if the key isn't found or doesn't have a value. """ - return DotEnv(dotenv_path, verbose=True).get(key_to_get) + return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) @contextmanager -def rewrite(path: Union[str, _PathLike]) -> Iterator[Tuple[IO[str], IO[str]]]: +def rewrite( + path: Union[str, _PathLike], + encoding: Optional[str], +) -> Iterator[Tuple[IO[str], IO[str]]]: try: if not os.path.isfile(path): - with io.open(path, "w+") as source: + with io.open(path, "w+", encoding=encoding) as source: source.write("") - with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest: - with io.open(path) as source: + with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) as dest: + with io.open(path, encoding=encoding) as source: yield (source, dest) # type: ignore except BaseException: if os.path.isfile(dest.name): @@ -141,6 +148,7 @@ def set_key( value_to_set: str, quote_mode: str = "always", export: bool = False, + encoding: Optional[str] = "utf-8", ) -> Tuple[Optional[bool], str, str]: """ Adds or Updates a key/value to the given .env @@ -165,7 +173,7 @@ def set_key( else: line_out = "{}={}\n".format(key_to_set, value_out) - with rewrite(dotenv_path) as (source, dest): + with rewrite(dotenv_path, encoding=encoding) as (source, dest): replaced = False missing_newline = False for mapping in with_warn_for_invalid_lines(parse_stream(source)): @@ -187,6 +195,7 @@ def unset_key( dotenv_path: Union[str, _PathLike], key_to_unset: str, quote_mode: str = "always", + encoding: Optional[str] = "utf-8", ) -> Tuple[Optional[bool], str]: """ Removes a given key from the given .env @@ -199,7 +208,7 @@ def unset_key( return None, key_to_unset removed = False - with rewrite(dotenv_path) as (source, dest): + with rewrite(dotenv_path, encoding=encoding) as (source, dest): for mapping in with_warn_for_invalid_lines(parse_stream(source)): if mapping.key == key_to_unset: removed = True diff --git a/tests/test_main.py b/tests/test_main.py index 541ac5ee..364fc24d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -53,6 +53,15 @@ def test_set_key(dotenv_file, before, key, value, expected, after): mock_warning.assert_not_called() +def test_set_key_encoding(dotenv_file): + encoding = "latin-1" + + result = dotenv.set_key(dotenv_file, "a", "é", encoding=encoding) + + assert result == (True, "a", "é") + assert open(dotenv_file, "r", encoding=encoding).read() == "a='é'\n" + + def test_set_key_permission_error(dotenv_file): os.chmod(dotenv_file, 0o000) @@ -107,6 +116,16 @@ def test_get_key_ok(dotenv_file): mock_warning.assert_not_called() +def test_get_key_encoding(dotenv_file): + encoding = "latin-1" + with open(dotenv_file, "w", encoding=encoding) as f: + f.write("é=è") + + result = dotenv.get_key(dotenv_file, "é", encoding=encoding) + + assert result == "è" + + def test_get_key_none(dotenv_file): logger = logging.getLogger("dotenv.main") with open(dotenv_file, "w") as f: @@ -147,6 +166,18 @@ def test_unset_no_value(dotenv_file): mock_warning.assert_not_called() +def test_unset_encoding(dotenv_file): + encoding = "latin-1" + with open(dotenv_file, "w", encoding=encoding) as f: + f.write("é=x") + + result = dotenv.unset_key(dotenv_file, "é", encoding=encoding) + + assert result == (True, "é") + with open(dotenv_file, "r", encoding=encoding) as f: + assert f.read() == "" + + def test_unset_non_existent_file(tmp_path): nx_file = str(tmp_path / "nx") logger = logging.getLogger("dotenv.main") From 06c645c6d6e20bae18dde51e2bb02c82b6d46133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 15 Feb 2022 10:29:34 +0100 Subject: [PATCH 07/78] Fix installing entry points Not sure why or when but the string syntax for entry points does not seem to work correctly anymore (no scripts are installed). Use the explicit list-in-dict syntax instead. --- CHANGELOG.md | 6 ++++++ setup.py | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4d014e..78f8fb79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`. (#379 by [@bbc2]) +### Fixed + +- Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by + [@mgorny]). + ## [0.19.2] - 2021-11-11 ### Fixed @@ -296,6 +301,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@gongqingkui]: https://github.com/gongqingkui [@greyli]: https://github.com/greyli [@jadutter]: https://github.com/jadutter +[@mgorny]: https://github.com/mgorny [@qnighy]: https://github.com/qnighy [@snobu]: https://github.com/snobu [@techalchemy]: https://github.com/techalchemy diff --git a/setup.py b/setup.py index a8122d3a..396cdf61 100644 --- a/setup.py +++ b/setup.py @@ -36,10 +36,11 @@ def read_files(files): extras_require={ 'cli': ['click>=5.0', ], }, - entry_points=''' - [console_scripts] - dotenv=dotenv.cli:cli - ''', + entry_points={ + "console_scripts": [ + "dotenv=dotenv.cli:cli", + ], + }, license='BSD-3-Clause', classifiers=[ 'Development Status :: 5 - Production/Stable', From 38320117cab7b0db0d9b417b2802e147542f80ed Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sun, 13 Mar 2022 18:10:08 +0100 Subject: [PATCH 08/78] Don't mark wheels as universal (#387) --- CHANGELOG.md | 1 + setup.cfg | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78f8fb79..ba0276c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by [@mgorny]). +- Don't build universal wheels (#387 by [@bbc2]). ## [0.19.2] - 2021-11-11 diff --git a/setup.cfg b/setup.cfg index d87b0a6b..348a5b51 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,10 +5,6 @@ tag = True [bumpversion:file:src/dotenv/version.py] - -[bdist_wheel] -universal = 1 - [flake8] max-line-length = 120 exclude = .tox,.git,docs,venv,.venv From 53cee8c7fb2fe1252606202ec9e2746651df738c Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Thu, 24 Mar 2022 15:23:31 +0100 Subject: [PATCH 09/78] Release version 0.20.0 --- CHANGELOG.md | 5 +++-- setup.cfg | 2 +- src/dotenv/version.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0276c1..d4251db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [0.20.0] - 2022-03-24 ### Added @@ -313,7 +313,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@yannham]: https://github.com/yannham [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...HEAD +[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 [0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2 [0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1 [0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0 diff --git a/setup.cfg b/setup.cfg index 348a5b51..09d61034 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.19.2 +current_version = 0.20.0 commit = True tag = True diff --git a/src/dotenv/version.py b/src/dotenv/version.py index aa070c2c..5f4bb0b3 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "0.19.2" +__version__ = "0.20.0" From 65dfa7137fc77405231e1d27673d9f1220a0844e Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Thu, 24 Mar 2022 16:33:57 +0100 Subject: [PATCH 10/78] Fix link typo in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4251db7..874f2134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -314,7 +314,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@zueve]: https://github.com/zueve [Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...HEAD -[0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 +[0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 [0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2 [0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1 [0.19.0]: https://github.com/theskumar/python-dotenv/compare/v0.18.0...v0.19.0 From 07a2fa15aa74d7cb628f727a6bb1e01e416c6603 Mon Sep 17 00:00:00 2001 From: Rabin Adhikari Date: Fri, 25 Mar 2022 14:47:22 +0545 Subject: [PATCH 11/78] Use `open` instead of `io.open` --- setup.py | 5 ++--- src/dotenv/main.py | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 396cdf61..a805c188 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ -import io from setuptools import setup def read_files(files): data = [] for file in files: - with io.open(file, encoding='utf-8') as f: + with open(file, encoding='utf-8') as f: data.append(f.read()) return "\n".join(data) @@ -13,7 +12,7 @@ def read_files(files): long_description = read_files(['README.md', 'CHANGELOG.md']) meta = {} -with io.open('./src/dotenv/version.py', encoding='utf-8') as f: +with open('./src/dotenv/version.py', encoding='utf-8') as f: exec(f.read(), meta) setup( diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 20ac61ba..54a5c42e 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -51,7 +51,7 @@ def __init__( @contextmanager def _get_stream(self) -> Iterator[IO[str]]: if self.dotenv_path and os.path.isfile(self.dotenv_path): - with io.open(self.dotenv_path, encoding=self.encoding) as stream: + with open(self.dotenv_path, encoding=self.encoding) as stream: yield stream elif self.stream is not None: yield self.stream @@ -129,10 +129,10 @@ def rewrite( ) -> Iterator[Tuple[IO[str], IO[str]]]: try: if not os.path.isfile(path): - with io.open(path, "w+", encoding=encoding) as source: + with open(path, "w+", encoding=encoding) as source: source.write("") with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) as dest: - with io.open(path, encoding=encoding) as source: + with open(path, encoding=encoding) as source: yield (source, dest) # type: ignore except BaseException: if os.path.isfile(dest.name): From b4e0c78ea82878d5e8f22ab4e24aee7bffdc2626 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sat, 26 Mar 2022 19:13:33 +0100 Subject: [PATCH 12/78] Docs: Improve documentation for variables without value (#390) --- README.md | 14 ++++++++++++++ src/dotenv/main.py | 18 ++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9b56b546..70de7e09 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,20 @@ second line" FOO="first line\nsecond line" ``` +### Variable without a value + +A variable can have no value: + +```bash +FOO +``` + +It results in `dotenv_values` associating that variable name with the value `None` (e.g. +`{"FOO": None}`. `load_dotenv`, on the other hand, simply ignores such variables. + +This shouldn't be confused with `FOO=`, in which case the variable is associated with the +empty string. + ### Variable expansion Python-dotenv can interpolate variables using POSIX variable expansion. diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 54a5c42e..76ee5993 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -351,14 +351,20 @@ def dotenv_values( """ Parse a .env file and return its content as a dict. - - *dotenv_path*: absolute or relative path to .env file. - - *stream*: `StringIO` object with .env content, used if `dotenv_path` is `None`. - - *verbose*: whether to output a warning the .env file is missing. Defaults to + The returned dict will have `None` values for keys without values in the .env file. + For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in + `{"foo": None}` + + Parameters: + + - `dotenv_path`: absolute or relative path to the .env file. + - `stream`: `StringIO` object with .env content, used if `dotenv_path` is `None`. + - `verbose`: whether to output a warning if the .env file is missing. Defaults to `False`. - in `.env` file. Defaults to `False`. - - *encoding*: encoding to be used to read the file. + - `encoding`: encoding to be used to read the file. Defaults to `"utf-8"`. - If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file. + If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the + .env file. """ if dotenv_path is None and stream is None: dotenv_path = find_dotenv() From 7d9cd4b50904569a2fc828145f4b28186190cc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 7 Apr 2022 19:17:37 +0200 Subject: [PATCH 13/78] Skip test_ipython if IPython is not available (#397) --- tests/test_ipython.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_ipython.py b/tests/test_ipython.py index 8983bf13..aa12adfe 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -2,6 +2,11 @@ import mock +import pytest + + +pytest.importorskip("IPython") + @mock.patch.dict(os.environ, {}, clear=True) def test_ipython_existing_variable_no_override(tmp_path): From 8080d1d999ea38fad101e04a7a78714981029e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 7 Apr 2022 15:24:18 +0200 Subject: [PATCH 14/78] Use built-in unittest.mock instead of third-party mock Python 3 has a built-in version of mock available as unittest.mock. Use it instead of installing the third-party package. --- requirements.txt | 2 -- tests/test_ipython.py | 3 +-- tests/test_main.py | 2 +- tox.ini | 2 -- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 39302b21..de374f43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,12 +2,10 @@ bumpversion click flake8>=2.2.3 ipython -mock pytest-cov pytest>=3.9 sh>=1.09 tox -types-mock wheel twine portray diff --git a/tests/test_ipython.py b/tests/test_ipython.py index aa12adfe..921dfd60 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -1,6 +1,5 @@ import os - -import mock +from unittest import mock import pytest diff --git a/tests/test_main.py b/tests/test_main.py index 364fc24d..ca14b1ac 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,8 +3,8 @@ import os import sys import textwrap +from unittest import mock -import mock import pytest import sh diff --git a/tox.ini b/tox.ini index c1f89fa1..3d7e1b60 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,6 @@ python = [testenv] deps = - mock pytest coverage sh @@ -27,7 +26,6 @@ skip_install = true deps = flake8 mypy - types-mock commands = flake8 src tests mypy --python-version=3.11 src tests From 769a040af6d27a50bbb1ca5203fbcb74b78b38fc Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Mon, 18 Apr 2022 00:13:07 +0530 Subject: [PATCH 15/78] feat(cli): add support for execution via 'python -m' (#395) Co-authored-by: Saurabh Kumar --- setup.py | 2 +- src/dotenv/__main__.py | 6 ++++++ src/dotenv/cli.py | 4 ---- 3 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 src/dotenv/__main__.py diff --git a/setup.py b/setup.py index a805c188..5a35d188 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ def read_files(files): }, entry_points={ "console_scripts": [ - "dotenv=dotenv.cli:cli", + "dotenv=dotenv.__main__:cli", ], }, license='BSD-3-Clause', diff --git a/src/dotenv/__main__.py b/src/dotenv/__main__.py new file mode 100644 index 00000000..3977f55a --- /dev/null +++ b/src/dotenv/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for cli, enables execution with `python -m dotenv`""" + +from .cli import cli + +if __name__ == "__main__": + cli() diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index b7ae24af..3411e346 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -158,7 +158,3 @@ def run_command(command: List[str], env: Dict[str, str]) -> int: _, _ = p.communicate() return p.returncode - - -if __name__ == "__main__": - cli() From cb53e1e530d9262e327c2b7b09e2c5424d544e3e Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sun, 17 Apr 2022 20:59:58 +0200 Subject: [PATCH 16/78] Improve documentation with direct use of MkDocs (#398) Improvements: - Only the public API is documented - Thanks to `mkdocstrings` with `show_submodules: no`. - Function parameter documentation is parsed and shown in tables. - `None` paragraphs are removed. - This was reported at https://github.com/timothycrosley/pdocs/pull/25 but hasn't been merged. - Footer layout is fixed. - It's currently broken with Portray, even on their own documentation (https://timothycrosley.github.io/portray/). - Fix list levels in table of contents on home page. - Thanks to `mdx_truly_sane_lists`. - Remove broken "edit" links. Portray is great but I think we can do better by directly using MkDocs. The new way to deploy the documentation is: mkdocs gh-deploy --- MANIFEST.in | 1 + docs/changelog.md | 1 + docs/contributing.md | 1 + docs/index.md | 1 + docs/reference.md | 3 +++ mkdocs.yml | 23 +++++++++++++++++++++++ pyproject.toml | 5 ----- requirements.txt | 9 +++++++-- src/dotenv/main.py | 35 +++++++++++++++++------------------ 9 files changed, 54 insertions(+), 25 deletions(-) create mode 120000 docs/changelog.md create mode 120000 docs/contributing.md create mode 120000 docs/index.md create mode 100644 docs/reference.md create mode 100644 mkdocs.yml delete mode 100644 pyproject.toml diff --git a/MANIFEST.in b/MANIFEST.in index 78e43e9b..98eaa40b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include LICENSE *.md *.yml *.toml include tox.ini +recursive-include docs *.md recursive-include tests *.py include .bumpversion.cfg diff --git a/docs/changelog.md b/docs/changelog.md new file mode 120000 index 00000000..04c99a55 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 120000 index 00000000..44fcc634 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1 @@ +../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 120000 index 00000000..32d46ee8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 00000000..a126448e --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,3 @@ +# Reference + +::: dotenv diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..27063ca2 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,23 @@ +site_name: python-dotenv +repo_url: https://github.com/theskumar/python-dotenv +edit_uri: "" +theme: + name: material + palette: + primary: green +markdown_extensions: + - mdx_truly_sane_lists +plugins: + - mkdocstrings: + handlers: + python: + rendering: + show_root_heading: yes + show_submodules: no + separate_signature: yes + - search +nav: + - Home: index.md + - Changelog: changelog.md + - Contributing: contributing.md + - Reference: reference.md diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 64b4431f..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,5 +0,0 @@ -[tool.portray] -modules = ["dotenv"] - -[tool.portray.mkdocs] -repo_url = "https://github.com/theskumar/python-dotenv" diff --git a/requirements.txt b/requirements.txt index de374f43..54354312 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,16 @@ +black~=22.3.0 bumpversion click flake8>=2.2.3 ipython +mdx_truly_sane_lists~=1.2 +mkdocs-include-markdown-plugin~=3.3.0 +mkdocs-material~=8.2.9 +mkdocstrings[python]~=0.18.1 +mkdocs~=1.3.0 pytest-cov pytest>=3.9 sh>=1.09 tox -wheel twine -portray +wheel diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 76ee5993..e7ad4308 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -198,10 +198,10 @@ def unset_key( encoding: Optional[str] = "utf-8", ) -> Tuple[Optional[bool], str]: """ - Removes a given key from the given .env + Removes a given key from the given `.env` file. - If the .env path given doesn't exist, fails - If the given key doesn't exist in the .env, fails + If the .env path given doesn't exist, fails. + If the given key doesn't exist in the .env, fails. """ if not os.path.exists(dotenv_path): logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path) @@ -316,16 +316,17 @@ def load_dotenv( ) -> bool: """Parse a .env file and then load all the variables found as environment variables. - - *dotenv_path*: absolute or relative path to .env file. - - *stream*: Text stream (such as `io.StringIO`) with .env content, used if - `dotenv_path` is `None`. - - *verbose*: whether to output a warning the .env file is missing. Defaults to - `False`. - - *override*: whether to override the system environment variables with the variables - in `.env` file. Defaults to `False`. - - *encoding*: encoding to be used to read the file. + Parameters: + dotenv_path: Absolute or relative path to .env file. + stream: Text stream (such as `io.StringIO`) with .env content, used if + `dotenv_path` is `None`. + verbose: Whether to output a warning the .env file is missing. + override: Whether to override the system environment variables with the variables + from the `.env` file. + encoding: Encoding to be used to read the file. - If both `dotenv_path` and `stream`, `find_dotenv()` is used to find the .env file. + If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the + .env file. """ if dotenv_path is None and stream is None: dotenv_path = find_dotenv() @@ -356,12 +357,10 @@ def dotenv_values( `{"foo": None}` Parameters: - - - `dotenv_path`: absolute or relative path to the .env file. - - `stream`: `StringIO` object with .env content, used if `dotenv_path` is `None`. - - `verbose`: whether to output a warning if the .env file is missing. Defaults to - `False`. - - `encoding`: encoding to be used to read the file. Defaults to `"utf-8"`. + dotenv_path: Absolute or relative path to the .env file. + stream: `StringIO` object with .env content, used if `dotenv_path` is `None`. + verbose: Whether to output a warning if the .env file is missing. + encoding: Encoding to be used to read the file. If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the .env file. From 29bceb836965de5bc498af401fd9d2e95194a5c1 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Mon, 18 Apr 2022 00:34:04 +0530 Subject: [PATCH 17/78] chore: add how to run docs locally --- CONTRIBUTING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d989d87f..90760961 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,3 +16,14 @@ or with [tox](https://pypi.org/project/tox/) installed: $ tox + +Documentation is published with [mkdocs](): + +```shell +$ pip install -r requirements.txt +$ pip install -e . +$ mkdocs serve +``` + +Open http://127.0.0.1:8000/ to view the documentation locally. + From ee15221e7c58fa7129214135553c2b39540e8856 Mon Sep 17 00:00:00 2001 From: Lars Kellogg-Stedman Date: Sun, 5 Jun 2022 01:50:42 -0400 Subject: [PATCH 18/78] feat: return False when we do not discover any environment variables (#388) * Fix docstring for load_dotenv The docstring for load_dotenv was missing a word, rendering it confusing. This commits modifies it for clarity. * Return False when we do not discover any environment variables This modifies Dotenv.set_as_environment_variables to return False if we have not discovered any environment variables via either `dotenv_path` or `stream`. The return value gets passed through to `load_dotenv`, so this can be used to determine if `dotenv.load_dotenv` was able to set anything. Closes #321 Co-authored-by: Saurabh Kumar --- src/dotenv/main.py | 5 +++++ tests/test_main.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index e7ad4308..78410660 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -87,6 +87,9 @@ def set_as_environment_variables(self) -> bool: """ Load the current dotenv as system environment variable. """ + if not self.dict(): + return False + for k, v in self.dict().items(): if k in os.environ and not self.override: continue @@ -324,6 +327,8 @@ def load_dotenv( override: Whether to override the system environment variables with the variables from the `.env` file. encoding: Encoding to be used to read the file. + Returns: + Bool: True if atleast one environment variable is set elese False If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the .env file. diff --git a/tests/test_main.py b/tests/test_main.py index ca14b1ac..82c73ba1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -259,8 +259,9 @@ def test_load_dotenv_no_file_verbose(): logger = logging.getLogger("dotenv.main") with mock.patch.object(logger, "info") as mock_info: - dotenv.load_dotenv('.does_not_exist', verbose=True) + result = dotenv.load_dotenv('.does_not_exist', verbose=True) + assert result is False mock_info.assert_called_once_with("Python-dotenv could not find configuration file %s.", ".does_not_exist") From 2cf826f236222d517c847fd5c61d48d3d85333d2 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sun, 5 Jun 2022 12:54:25 +0530 Subject: [PATCH 19/78] chore: add GitHub Actions for deploying docs (#399) Automatically update the documentation when a release is made. Co-authored-by: Saurabh Kumar --- .github/workflows/release.yml | 6 ++++++ CONTRIBUTING.md | 2 +- MANIFEST.in | 1 + requirements-docs.txt | 5 +++++ requirements.txt | 5 ----- 5 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 requirements-docs.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3abd994..7666da09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,3 +23,9 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | make release + + - name: Publish Documentation + run: | + pip install -r requirements-docs.txt + pip install -e . + mkdocs gh-deploy --force diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90760961..fac71bff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ or with [tox](https://pypi.org/project/tox/) installed: Documentation is published with [mkdocs](): ```shell -$ pip install -r requirements.txt +$ pip install -r requirements-docs.txt $ pip install -e . $ mkdocs serve ``` diff --git a/MANIFEST.in b/MANIFEST.in index 98eaa40b..9c457e66 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,4 +9,5 @@ include .coveragerc include .editorconfig include Makefile include requirements.txt +include requirements-docs.txt include src/dotenv/py.typed diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..7f8b71f3 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +mdx_truly_sane_lists~=1.2 +mkdocs-include-markdown-plugin~=3.3.0 +mkdocs-material~=8.2.9 +mkdocstrings[python]~=0.18.1 +mkdocs~=1.3.0 diff --git a/requirements.txt b/requirements.txt index 54354312..0206316f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,6 @@ bumpversion click flake8>=2.2.3 ipython -mdx_truly_sane_lists~=1.2 -mkdocs-include-markdown-plugin~=3.3.0 -mkdocs-material~=8.2.9 -mkdocstrings[python]~=0.18.1 -mkdocs~=1.3.0 pytest-cov pytest>=3.9 sh>=1.09 From 6b1e68bb8a0b3e96eccb53fcc5ed4b727661dd09 Mon Sep 17 00:00:00 2001 From: Naor Livne Date: Sun, 12 Jun 2022 19:18:56 +0300 Subject: [PATCH 20/78] Add `parse_it` to Related Projects (#410) `parse_it` is similar to `dynaconf` but focus more on giving the enduser the ability to use his preferred way to set config, `.env` being one of the choices it's using and makes available to endusers --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 70de7e09..eb6bb538 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,7 @@ defined in the following list: - [dump-env](https://github.com/sobolevn/dump-env) - [environs](https://github.com/sloria/environs) - [dynaconf](https://github.com/rochacbruno/dynaconf) +- [parse_it](https://github.com/naorlivne/parse_it) ## Acknowledgements From a50a3bf3461e7c295723078ebf2db037d9eecd8b Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 23 Jul 2022 11:21:06 +0530 Subject: [PATCH 21/78] Add .vscode to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 172047ac..ba1234ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .idea +.vscode/ # Created by https://www.gitignore.io/api/python # Edit at https://www.gitignore.io/?templates=python From 2f36c082c278bad1a84411f1ad61547f95cecdb8 Mon Sep 17 00:00:00 2001 From: eggplants Date: Sat, 23 Jul 2022 15:07:18 +0900 Subject: [PATCH 22/78] Drop Python 3.5 and 3.6 and upgrade GA (#393) * Drop Python 3.5 and 3.6 * Tox and GitHub Actions upgrade * Fix pypy version Co-authored-by: Saurabh Kumar --- .github/workflows/test.yml | 8 ++++---- setup.py | 4 +--- src/dotenv/main.py | 21 ++++++++------------- tox.ini | 8 ++------ 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5135ae4f..b7c6d504 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,15 +10,15 @@ jobs: matrix: os: - ubuntu-latest - python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10", "3.11.0-alpha - 3.11", pypy3] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-beta.4 - 3.11", pypy3.9] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: | + run: python -m pip install --upgrade pip pip install tox tox-gh-actions - name: Test with tox diff --git a/setup.py b/setup.py index 5a35d188..bcf1b0dc 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def read_files(files): package_data={ 'dotenv': ['py.typed'], }, - python_requires=">=3.5", + python_requires=">=3.7", extras_require={ 'cli': ['click>=5.0', ], }, @@ -45,8 +45,6 @@ def read_files(files): 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 78410660..05d377a9 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -14,11 +14,6 @@ logger = logging.getLogger(__name__) -if sys.version_info >= (3, 6): - _PathLike = os.PathLike -else: - _PathLike = str - def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]: for mapping in mappings: @@ -33,14 +28,14 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding class DotEnv(): def __init__( self, - dotenv_path: Optional[Union[str, _PathLike]], + dotenv_path: Optional[Union[str, os.PathLike]], stream: Optional[IO[str]] = None, verbose: bool = False, encoding: Union[None, str] = None, interpolate: bool = True, override: bool = True, ) -> None: - self.dotenv_path = dotenv_path # type: Optional[Union[str, _PathLike]] + self.dotenv_path = dotenv_path # type: Optional[Union[str, os.PathLike]] self.stream = stream # type: Optional[IO[str]] self._dict = None # type: Optional[Dict[str, Optional[str]]] self.verbose = verbose # type: bool @@ -113,7 +108,7 @@ def get(self, key: str) -> Optional[str]: def get_key( - dotenv_path: Union[str, _PathLike], + dotenv_path: Union[str, os.PathLike], key_to_get: str, encoding: Optional[str] = "utf-8", ) -> Optional[str]: @@ -127,7 +122,7 @@ def get_key( @contextmanager def rewrite( - path: Union[str, _PathLike], + path: Union[str, os.PathLike], encoding: Optional[str], ) -> Iterator[Tuple[IO[str], IO[str]]]: try: @@ -146,7 +141,7 @@ def rewrite( def set_key( - dotenv_path: Union[str, _PathLike], + dotenv_path: Union[str, os.PathLike], key_to_set: str, value_to_set: str, quote_mode: str = "always", @@ -195,7 +190,7 @@ def set_key( def unset_key( - dotenv_path: Union[str, _PathLike], + dotenv_path: Union[str, os.PathLike], key_to_unset: str, quote_mode: str = "always", encoding: Optional[str] = "utf-8", @@ -310,7 +305,7 @@ def _is_interactive(): def load_dotenv( - dotenv_path: Union[str, _PathLike, None] = None, + dotenv_path: Union[str, os.PathLike, None] = None, stream: Optional[IO[str]] = None, verbose: bool = False, override: bool = False, @@ -348,7 +343,7 @@ def load_dotenv( def dotenv_values( - dotenv_path: Union[str, _PathLike, None] = None, + dotenv_path: Union[str, os.PathLike, None] = None, stream: Optional[IO[str]] = None, verbose: bool = False, interpolate: bool = True, diff --git a/tox.ini b/tox.ini index 3d7e1b60..cb8a6625 100644 --- a/tox.ini +++ b/tox.ini @@ -3,14 +3,12 @@ envlist = lint,py{35,36,37,38,39,310,311},pypy3,manifest,coverage-report [gh-actions] python = - 3.5: py35, coverage-report - 3.6: py36, coverage-report 3.7: py37, coverage-report 3.8: py38, coverage-report 3.9: py39, coverage-report 3.10: py310, lint, manifest, coverage-report 3.11: py311, coverage-report - pypy3: pypy3, coverage-report + pypy-3.9: pypy3, coverage-report [testenv] deps = @@ -18,7 +16,7 @@ deps = coverage sh click - py{35,36,37,38,39,310,311,py3}: ipython + py{37,38,39,310,311,py3}: ipython commands = coverage run --parallel -m pytest {posargs} [testenv:lint] @@ -33,8 +31,6 @@ commands = mypy --python-version=3.9 src tests mypy --python-version=3.8 src tests mypy --python-version=3.7 src tests - mypy --python-version=3.6 src tests - mypy --python-version=3.5 src tests [testenv:manifest] deps = check-manifest From 914c68ef0e4c2c085d2753f5cbbf304852f37850 Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Fri, 22 Jul 2022 23:21:31 -0700 Subject: [PATCH 23/78] feat(cli) add --format= option to list command (#407) Allows dumping of all variables in various formats. Currently defined formats: simple: Each variable is output as = with no quoting or escaping. The output is not parseable. This is the default format for backwards compatibility. shell: Each variable is output as =, where is quoted/escaped with shell-compatible rules, the result may be imported into a shell script with eval "$(dotenv list --format=shell)" export: Similar to "shell" but prefixes each line with "export " so that when imported into a shell script, the variables are exported. json: The entire set of variables is output as a JSON-serialized object Co-authored-by: Saurabh Kumar --- README.md | 5 +++++ src/dotenv/cli.py | 20 +++++++++++++++++--- tests/test_cli.py | 29 ++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index eb6bb538..a9d19bfa 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,11 @@ $ dotenv set EMAIL foo@example.org $ dotenv list USER=foo EMAIL=foo@example.org +$ dotenv list --format=json +{ + "USER": "foo", + "EMAIL": "foo@example.org" +} $ dotenv run -- python foo.py ``` diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 3411e346..b845b95e 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,4 +1,6 @@ +import json import os +import shlex import sys from subprocess import Popen from typing import Any, Dict, List @@ -36,7 +38,11 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: @cli.command() @click.pass_context -def list(ctx: click.Context) -> None: +@click.option('--format', default='simple', + type=click.Choice(['simple', 'json', 'shell', 'export']), + help="The format in which to display the list. Default format is simple, " + "which displays name=value without quotes.") +def list(ctx: click.Context, format: bool) -> None: '''Display all the stored key/value.''' file = ctx.obj['FILE'] if not os.path.isfile(file): @@ -45,8 +51,16 @@ def list(ctx: click.Context) -> None: ctx=ctx ) dotenv_as_dict = dotenv_values(file) - for k, v in dotenv_as_dict.items(): - click.echo('%s=%s' % (k, v)) + if format == 'json': + click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) + else: + prefix = 'export ' if format == 'export' else '' + for k in sorted(dotenv_as_dict): + v = dotenv_as_dict[k] + if v is not None: + if format in ('export', 'shell'): + v = shlex.quote(v) + click.echo('%s%s=%s' % (prefix, k, v)) @cli.command() diff --git a/tests/test_cli.py b/tests/test_cli.py index 223476fe..ca5ba2a1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,19 +2,38 @@ import pytest import sh - +from typing import Optional import dotenv from dotenv.cli import cli as dotenv_cli from dotenv.version import __version__ -def test_list(cli, dotenv_file): +@pytest.mark.parametrize( + "format,content,expected", + ( + (None, "x='a b c'", '''x=a b c\n'''), + ("simple", "x='a b c'", '''x=a b c\n'''), + ("simple", """x='"a b c"'""", '''x="a b c"\n'''), + ("simple", '''x="'a b c'"''', '''x='a b c'\n'''), + ("json", "x='a b c'", '''{\n "x": "a b c"\n}\n'''), + ("shell", "x='a b c'", "x='a b c'\n"), + ("shell", """x='"a b c"'""", '''x='"a b c"'\n'''), + ("shell", '''x="'a b c'"''', '''x=''"'"'a b c'"'"''\n'''), + ("shell", "x='a\nb\nc'", "x='a\nb\nc'\n"), + ("export", "x='a b c'", '''export x='a b c'\n'''), + ) +) +def test_list(cli, dotenv_file, format: Optional[str], content: str, expected: str): with open(dotenv_file, "w") as f: - f.write("a=b") + f.write(content + '\n') + + args = ['--file', dotenv_file, 'list'] + if format is not None: + args.extend(['--format', format]) - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'list']) + result = cli.invoke(dotenv_cli, args) - assert (result.exit_code, result.output) == (0, result.output) + assert (result.exit_code, result.output) == (0, expected) def test_list_non_existent_file(cli): From a7c811dd9296fc06524838c08c2ef4a8dec6377b Mon Sep 17 00:00:00 2001 From: harveer07 <71729199+harveer07@users.noreply.github.com> Date: Sat, 3 Sep 2022 06:34:49 -0700 Subject: [PATCH 24/78] Update README.md (#415) fixed grammar error. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9d19bfa..983b7d15 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ The format is not formally specified and still improves over time. That being s Keys can be unquoted or single-quoted. Values can be unquoted, single- or double-quoted. Spaces before and after keys, equal signs, and values are ignored. Values can be followed -by a comment. Lines can start with the `export` directive, which has no effect on their +by a comment. Lines can start with the `export` directive, which does not affect their interpretation. Allowed escape sequences: From a53d652f0618c27a0fd62c4a7946e609919179f3 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Sat, 3 Sep 2022 09:58:22 -0400 Subject: [PATCH 25/78] fix: out of scope error when "dest" variable is undefined #413 --- src/dotenv/main.py | 9 +++++---- tests/test_main.py | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 05d377a9..33217885 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -125,15 +125,16 @@ def rewrite( path: Union[str, os.PathLike], encoding: Optional[str], ) -> Iterator[Tuple[IO[str], IO[str]]]: + dest = None try: if not os.path.isfile(path): with open(path, "w+", encoding=encoding) as source: source.write("") - with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) as dest: - with open(path, encoding=encoding) as source: - yield (source, dest) # type: ignore + dest = tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) + with open(path, encoding=encoding) as source: + yield (source, dest) # type: ignore except BaseException: - if os.path.isfile(dest.name): + if dest and os.path.isfile(dest.name): os.unlink(dest.name) raise else: diff --git a/tests/test_main.py b/tests/test_main.py index 82c73ba1..84a982fe 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -22,6 +22,11 @@ def test_set_key_no_file(tmp_path): assert os.path.exists(nx_file) +def test_set_key_invalid_file(): + with pytest.raises(TypeError): + result = dotenv.set_key(None, "foo", "bar") + + @pytest.mark.parametrize( "before,key,value,expected,after", [ From 6399af6cd8b5f27ae4b68e8758722dff6728bffd Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 3 Sep 2022 19:49:33 +0530 Subject: [PATCH 26/78] chore: fix flake8 issue --- setup.cfg | 2 +- tests/test_main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 09d61034..63d37cd3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,7 +7,7 @@ tag = True [flake8] max-line-length = 120 -exclude = .tox,.git,docs,venv,.venv +exclude = .tox,.git,docs,venv,.venv,build [mypy] check_untyped_defs = true diff --git a/tests/test_main.py b/tests/test_main.py index 84a982fe..5f579ccd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -24,7 +24,7 @@ def test_set_key_no_file(tmp_path): def test_set_key_invalid_file(): with pytest.raises(TypeError): - result = dotenv.set_key(None, "foo", "bar") + dotenv.set_key(None, "foo", "bar") @pytest.mark.parametrize( From b1f041dcef79e796c9b9cda8e13d72ca65727b9b Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 3 Sep 2022 20:14:32 +0530 Subject: [PATCH 27/78] Add release notes for 0.21.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 874f2134..d1f7b023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.21.0] - 2022-09-03 + +### Added +* CLI: add support for invocations via 'python -m'. (#395 by [@theskumar]) +* `load_dotenv` function now returns `False`. (#388 by [@larsks]) +* CLI: add --format= option to list command. (#407 by [@sammck]) + +### Fixed +* Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants]) +* Fix out of scope error when `dest` variable is undefined. (#414 by [@theGOTOguy]) +* Use `open` instead of `io.open`. (#389 by [@rabinadk1]) +* Improve documentation for variables without a value (#390 by [@bbc2]) +* Add `parse_it` to Related Projects by (#410 by [@naorlivne]) +* Update README.md by (#415 by [@harveer07]) +* Improve documentation with direct use of MkDocs by (#398 by [@bbc2]) + ## [0.20.0] - 2022-03-24 ### Added @@ -287,7 +303,6 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [#183]: https://github.com/theskumar/python-dotenv/issues/183 [#359]: https://github.com/theskumar/python-dotenv/issues/359 -[@Flimm]: https://github.com/Flimm [@alanjds]: https://github.com/alanjds [@altendky]: https://github.com/altendky [@andrewsmith]: https://github.com/andrewsmith @@ -296,16 +311,24 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@befeleme]: https://github.com/befeleme [@cjauvin]: https://github.com/cjauvin [@earlbread]: https://github.com/earlbread +[@eggplants]: https://github.com/@eggplants [@ekohl]: https://github.com/ekohl [@elbehery95]: https://github.com/elbehery95 +[@Flimm]: https://github.com/Flimm [@gergelyk]: https://github.com/gergelyk [@gongqingkui]: https://github.com/gongqingkui [@greyli]: https://github.com/greyli +[@harveer07]: https://github.com/@harveer07 [@jadutter]: https://github.com/jadutter +[@larsks]: https://github.com/@larsks [@mgorny]: https://github.com/mgorny +[@naorlivne]: https://github.com/@naorlivne [@qnighy]: https://github.com/qnighy +[@rabinadk1]: https://github.com/@rabinadk1 +[@sammck]: https://github.com/@sammck [@snobu]: https://github.com/snobu [@techalchemy]: https://github.com/techalchemy +[@theGOTOguy]: https://github.com/@theGOTOguy [@theskumar]: https://github.com/theskumar [@ulyssessouza]: https://github.com/ulyssessouza [@venthur]: https://github.com/venthur @@ -313,7 +336,9 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@yannham]: https://github.com/yannham [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...HEAD + +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...HEAD +[0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0 [0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 [0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2 [0.19.1]: https://github.com/theskumar/python-dotenv/compare/v0.19.0...v0.19.1 From 490b116e6548e8d1bbc8184e36768f6ae73c06b2 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 3 Sep 2022 20:38:05 +0530 Subject: [PATCH 28/78] Revert "fix: out of scope error when "dest" variable is undefined #413" This reverts commit a53d652f0618c27a0fd62c4a7946e609919179f3. --- src/dotenv/main.py | 9 ++++----- tests/test_main.py | 5 ----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 33217885..05d377a9 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -125,16 +125,15 @@ def rewrite( path: Union[str, os.PathLike], encoding: Optional[str], ) -> Iterator[Tuple[IO[str], IO[str]]]: - dest = None try: if not os.path.isfile(path): with open(path, "w+", encoding=encoding) as source: source.write("") - dest = tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) - with open(path, encoding=encoding) as source: - yield (source, dest) # type: ignore + with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) as dest: + with open(path, encoding=encoding) as source: + yield (source, dest) # type: ignore except BaseException: - if dest and os.path.isfile(dest.name): + if os.path.isfile(dest.name): os.unlink(dest.name) raise else: diff --git a/tests/test_main.py b/tests/test_main.py index 5f579ccd..82c73ba1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -22,11 +22,6 @@ def test_set_key_no_file(tmp_path): assert os.path.exists(nx_file) -def test_set_key_invalid_file(): - with pytest.raises(TypeError): - dotenv.set_key(None, "foo", "bar") - - @pytest.mark.parametrize( "before,key,value,expected,after", [ From 5d079312ada8fb39643b7789a577e93b415c18b6 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 3 Sep 2022 20:38:55 +0530 Subject: [PATCH 29/78] update changelog --- CHANGELOG.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1f7b023..e53060a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,17 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.21.0] - 2022-09-03 ### Added -* CLI: add support for invocations via 'python -m'. (#395 by [@theskumar]) -* `load_dotenv` function now returns `False`. (#388 by [@larsks]) -* CLI: add --format= option to list command. (#407 by [@sammck]) +* CLI: add support for invocations via 'python -m'. (#395 by @theskumar) +* `load_dotenv` function now returns `False`. (#388 by @larsks) +* CLI: add --format= option to list command. (#407 by @sammck) ### Fixed -* Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants]) -* Fix out of scope error when `dest` variable is undefined. (#414 by [@theGOTOguy]) -* Use `open` instead of `io.open`. (#389 by [@rabinadk1]) -* Improve documentation for variables without a value (#390 by [@bbc2]) -* Add `parse_it` to Related Projects by (#410 by [@naorlivne]) -* Update README.md by (#415 by [@harveer07]) -* Improve documentation with direct use of MkDocs by (#398 by [@bbc2]) +* Drop Python 3.5 and 3.6 and upgrade GA (#393 by @eggplants) +* Use `open` instead of `io.open`. (#389 by @rabinadk1) +* Improve documentation for variables without a value (#390 by @bbc2) +* Add `parse_it` to Related Projects by (#410 by @naorlivne) +* Update README.md by (#415 by @harveer07) +* Improve documentation with direct use of MkDocs by (#398 by @bbc2) ## [0.20.0] - 2022-03-24 @@ -328,7 +327,6 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@sammck]: https://github.com/@sammck [@snobu]: https://github.com/snobu [@techalchemy]: https://github.com/techalchemy -[@theGOTOguy]: https://github.com/@theGOTOguy [@theskumar]: https://github.com/theskumar [@ulyssessouza]: https://github.com/ulyssessouza [@venthur]: https://github.com/venthur From b6fe193b4c296a24d1973b741c19ee8b0066bc86 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 3 Sep 2022 20:42:11 +0530 Subject: [PATCH 30/78] =?UTF-8?q?Bump=20version:=200.20.0=20=E2=86=92=200.?= =?UTF-8?q?21.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 16 ++++++++-------- src/dotenv/version.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index 63d37cd3..4d49c291 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.20.0 +current_version = 0.21.0 commit = True tag = True @@ -24,14 +24,14 @@ relative_files = True source = dotenv [coverage:paths] -source = - src/dotenv - .tox/*/lib/python*/site-packages/dotenv - .tox/pypy*/site-packages/dotenv +source = + src/dotenv + .tox/*/lib/python*/site-packages/dotenv + .tox/pypy*/site-packages/dotenv [coverage:report] show_missing = True include = */site-packages/dotenv/* -exclude_lines = - if IS_TYPE_CHECKING: - pragma: no cover +exclude_lines = + if IS_TYPE_CHECKING: + pragma: no cover diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 5f4bb0b3..6a726d85 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "0.20.0" +__version__ = "0.21.0" From cedd36d395b772adb556ba4635ab42d59ad27567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=82=E1=B4=80=C9=AA=E1=B4=8D=20=E4=B9=87=CA=9Cs?= =?UTF-8?q?=E1=B4=80=C9=B4?= Date: Sun, 18 Sep 2022 16:48:21 +0500 Subject: [PATCH 31/78] Fixed minor spelling mistake (#426) --- src/dotenv/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 05d377a9..83ea3239 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -323,7 +323,7 @@ def load_dotenv( from the `.env` file. encoding: Encoding to be used to read the file. Returns: - Bool: True if atleast one environment variable is set elese False + Bool: True if atleast one environment variable is set else False If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the .env file. From 718307b8a2fbaca7f511248c23e591bd5e58760a Mon Sep 17 00:00:00 2001 From: Praveensenpai <71966071+Praveensenpai@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:20:45 +0530 Subject: [PATCH 32/78] Update __init__.py fstring --- src/dotenv/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotenv/__init__.py b/src/dotenv/__init__.py index 3512d101..9dbd2543 100644 --- a/src/dotenv/__init__.py +++ b/src/dotenv/__init__.py @@ -23,9 +23,9 @@ def get_cli_string( """ command = ['dotenv'] if quote: - command.append('-q %s' % quote) + command.append(f'-q {quote}') if path: - command.append('-f %s' % path) + command.append(f'-f {path}') if action: command.append(action) if key: From 1ecb57dd7dd516d3ecedf7e58ba84520281fbd00 Mon Sep 17 00:00:00 2001 From: Ben Li-Sauerwine Date: Wed, 27 Jul 2022 03:26:26 -0400 Subject: [PATCH 33/78] Fix out of scope error when "dest" variable is undefined Fixes #413 whereby the NamedTemporaryFile "dest" was out of scope in the error handling portion of rewrite. The problem was initially fixed in #414 but it got reverted because of a linter error. This new commit works around that linter error. --- src/dotenv/main.py | 20 +++++++++----------- tests/test_main.py | 7 +++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 83ea3239..43248819 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -125,19 +125,17 @@ def rewrite( path: Union[str, os.PathLike], encoding: Optional[str], ) -> Iterator[Tuple[IO[str], IO[str]]]: - try: - if not os.path.isfile(path): - with open(path, "w+", encoding=encoding) as source: - source.write("") - with tempfile.NamedTemporaryFile(mode="w+", delete=False, encoding=encoding) as dest: + if not os.path.isfile(path): + with open(path, mode="w", encoding=encoding) as source: + source.write("") + with tempfile.NamedTemporaryFile(mode="w", encoding=encoding, delete=False) as dest: + try: with open(path, encoding=encoding) as source: - yield (source, dest) # type: ignore - except BaseException: - if os.path.isfile(dest.name): + yield (source, dest) + except BaseException: os.unlink(dest.name) - raise - else: - shutil.move(dest.name, path) + raise + shutil.move(dest.name, path) def set_key( diff --git a/tests/test_main.py b/tests/test_main.py index 82c73ba1..9c895851 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -178,6 +178,13 @@ def test_unset_encoding(dotenv_file): assert f.read() == "" +def test_set_key_unauthorized_file(dotenv_file): + os.chmod(dotenv_file, 0o000) + + with pytest.raises(PermissionError): + dotenv.set_key(dotenv_file, "a", "x") + + def test_unset_non_existent_file(tmp_path): nx_file = str(tmp_path / "nx") logger = logging.getLogger("dotenv.main") From 7e199c32db62eeb03a9e5e807f34f730e90216d3 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Fri, 11 Nov 2022 17:27:11 +0100 Subject: [PATCH 34/78] Use 3.11 non-beta in CI --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7c6d504..15875768 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: matrix: os: - ubuntu-latest - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0-beta.4 - 3.11", pypy3.9] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy3.9] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From 00cc7ae200aa2228167c689bbe8b1d3c5c7a4751 Mon Sep 17 00:00:00 2001 From: momohakarish Date: Thu, 27 Oct 2022 00:41:15 +0300 Subject: [PATCH 35/78] Modernize some code in variables.py --- src/dotenv/variables.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/dotenv/variables.py b/src/dotenv/variables.py index d77b700c..667f2f26 100644 --- a/src/dotenv/variables.py +++ b/src/dotenv/variables.py @@ -1,8 +1,8 @@ import re -from abc import ABCMeta +from abc import ABCMeta, abstractmethod from typing import Iterator, Mapping, Optional, Pattern -_posix_variable = re.compile( +_posix_variable: Pattern[str] = re.compile( r""" \$\{ (?P[^\}:]*) @@ -12,20 +12,18 @@ \} """, re.VERBOSE, -) # type: Pattern[str] +) -class Atom(): - __metaclass__ = ABCMeta - +class Atom(metaclass=ABCMeta): def __ne__(self, other: object) -> bool: result = self.__eq__(other) if result is NotImplemented: return NotImplemented return not result - def resolve(self, env: Mapping[str, Optional[str]]) -> str: - raise NotImplementedError + @abstractmethod + def resolve(self, env: Mapping[str, Optional[str]]) -> str: ... class Literal(Atom): @@ -33,7 +31,7 @@ def __init__(self, value: str) -> None: self.value = value def __repr__(self) -> str: - return "Literal(value={})".format(self.value) + return f"Literal(value={self.value})" def __eq__(self, other: object) -> bool: if not isinstance(other, self.__class__): @@ -53,7 +51,7 @@ def __init__(self, name: str, default: Optional[str]) -> None: self.default = default def __repr__(self) -> str: - return "Variable(name={}, default={})".format(self.name, self.default) + return f"Variable(name={self.name}, default={self.default})" def __eq__(self, other: object) -> bool: if not isinstance(other, self.__class__): @@ -74,8 +72,8 @@ def parse_variables(value: str) -> Iterator[Atom]: for match in _posix_variable.finditer(value): (start, end) = match.span() - name = match.groupdict()["name"] - default = match.groupdict()["default"] + name = match["name"] + default = match["default"] if start > cursor: yield Literal(value=value[cursor:start]) From cadf4fc60a0c2a6de0a743a225cd9b2c48e1426a Mon Sep 17 00:00:00 2001 From: momohakarish Date: Thu, 27 Oct 2022 22:30:58 +0300 Subject: [PATCH 36/78] Modernize main.py and parser.py code --- src/dotenv/main.py | 28 ++++++++++++++-------------- src/dotenv/parser.py | 29 +++++++++++------------------ 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 43248819..e7b8392a 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -25,23 +25,23 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding yield mapping -class DotEnv(): +class DotEnv: def __init__( self, dotenv_path: Optional[Union[str, os.PathLike]], stream: Optional[IO[str]] = None, verbose: bool = False, - encoding: Union[None, str] = None, + encoding: Optional[str] = None, interpolate: bool = True, override: bool = True, ) -> None: - self.dotenv_path = dotenv_path # type: Optional[Union[str, os.PathLike]] - self.stream = stream # type: Optional[IO[str]] - self._dict = None # type: Optional[Dict[str, Optional[str]]] - self.verbose = verbose # type: bool - self.encoding = encoding # type: Union[None, str] - self.interpolate = interpolate # type: bool - self.override = override # type: bool + self.dotenv_path: Optional[Union[str, os.PathLike]] = dotenv_path + self.stream: Optional[IO[str]] = stream + self._dict: Optional[Dict[str, Optional[str]]] = None + self.verbose: bool = verbose + self.encoding: Optional[str] = encoding + self.interpolate: bool = interpolate + self.override: bool = override @contextmanager def _get_stream(self) -> Iterator[IO[str]]: @@ -153,7 +153,7 @@ def set_key( an orphan .env somewhere in the filesystem """ if quote_mode not in ("always", "auto", "never"): - raise ValueError("Unknown quote_mode: {}".format(quote_mode)) + raise ValueError(f"Unknown quote_mode: {quote_mode}") quote = ( quote_mode == "always" @@ -165,9 +165,9 @@ def set_key( else: value_out = value_to_set if export: - line_out = 'export {}={}\n'.format(key_to_set, value_out) + line_out = f'export {key_to_set}={value_out}\n' else: - line_out = "{}={}\n".format(key_to_set, value_out) + line_out = f"{key_to_set}={value_out}\n" with rewrite(dotenv_path, encoding=encoding) as (source, dest): replaced = False @@ -222,14 +222,14 @@ def resolve_variables( values: Iterable[Tuple[str, Optional[str]]], override: bool, ) -> Mapping[str, Optional[str]]: - new_values = {} # type: Dict[str, Optional[str]] + new_values: Dict[str, Optional[str]] = {} for (name, value) in values: if value is None: result = None else: atoms = parse_variables(value) - env = {} # type: Dict[str, Optional[str]] + env: Dict[str, Optional[str]] = {} if override: env.update(os.environ) # type: ignore env.update(new_values) diff --git a/src/dotenv/parser.py b/src/dotenv/parser.py index 398bd49a..735f14a3 100644 --- a/src/dotenv/parser.py +++ b/src/dotenv/parser.py @@ -25,23 +25,16 @@ def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]: _single_quote_escapes = make_regex(r"\\[\\']") -Original = NamedTuple( - "Original", - [ - ("string", str), - ("line", int), - ], -) - -Binding = NamedTuple( - "Binding", - [ - ("key", Optional[str]), - ("value", Optional[str]), - ("original", Original), - ("error", bool), - ], -) +class Original(NamedTuple): + string: str + line: int + + +class Binding(NamedTuple): + key: Optional[str] + value: Optional[str] + original: Original + error: bool class Position: @@ -155,7 +148,7 @@ def parse_binding(reader: Reader) -> Binding: reader.read_regex(_whitespace) if reader.peek(1) == "=": reader.read_regex(_equal_sign) - value = parse_value(reader) # type: Optional[str] + value: Optional[str] = parse_value(reader) else: value = None reader.read_regex(_comment) From c22bc509218c981b098df0e028b0a53fbbbb193b Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sat, 12 Nov 2022 14:06:02 +0100 Subject: [PATCH 37/78] Fix IPython test warning about deprecated `magic` IPython would complain about: DeprecationWarning: `magic(...)` is deprecated since IPython 0.13 (warning added in 8.1), use run_line_magic(magic_name, parameter_s). --- tests/test_ipython.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_ipython.py b/tests/test_ipython.py index 921dfd60..f988bd9c 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -17,8 +17,8 @@ def test_ipython_existing_variable_no_override(tmp_path): os.environ["a"] = "c" ipshell = InteractiveShellEmbed() - ipshell.magic("load_ext dotenv") - ipshell.magic("dotenv") + ipshell.run_line_magic("load_ext", "dotenv") + ipshell.run_line_magic("dotenv", "") assert os.environ == {"a": "c"} @@ -33,8 +33,8 @@ def test_ipython_existing_variable_override(tmp_path): os.environ["a"] = "c" ipshell = InteractiveShellEmbed() - ipshell.magic("load_ext dotenv") - ipshell.magic("dotenv -o") + ipshell.run_line_magic("load_ext", "dotenv") + ipshell.run_line_magic("dotenv", "-o") assert os.environ == {"a": "b"} @@ -48,7 +48,7 @@ def test_ipython_new_variable(tmp_path): os.chdir(str(tmp_path)) ipshell = InteractiveShellEmbed() - ipshell.magic("load_ext dotenv") - ipshell.magic("dotenv") + ipshell.run_line_magic("load_ext", "dotenv") + ipshell.run_line_magic("dotenv", "") assert os.environ == {"a": "b"} From ceca48c8441f381b1edf4772288326b4be6f435c Mon Sep 17 00:00:00 2001 From: momohakarish Date: Sat, 12 Nov 2022 13:43:38 +0200 Subject: [PATCH 38/78] Use f-strings, make some code more concise and change docstrings to double quotes according to PEP257 --- src/dotenv/__init__.py | 2 +- src/dotenv/cli.py | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/dotenv/__init__.py b/src/dotenv/__init__.py index 9dbd2543..7f4c631b 100644 --- a/src/dotenv/__init__.py +++ b/src/dotenv/__init__.py @@ -32,7 +32,7 @@ def get_cli_string( command.append(key) if value: if ' ' in value: - command.append('"%s"' % value) + command.append(f'"{value}"') else: command.append(value) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index b845b95e..97d6a948 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -29,11 +29,8 @@ @click.version_option(version=__version__) @click.pass_context def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: - '''This script is used to set, get or unset values from a .env file.''' - ctx.obj = {} - ctx.obj['QUOTE'] = quote - ctx.obj['EXPORT'] = export - ctx.obj['FILE'] = file + """This script is used to set, get or unset values from a .env file.""" + ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file} @cli.command() @@ -43,11 +40,11 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: help="The format in which to display the list. Default format is simple, " "which displays name=value without quotes.") def list(ctx: click.Context, format: bool) -> None: - '''Display all the stored key/value.''' + """Display all the stored key/value.""" file = ctx.obj['FILE'] if not os.path.isfile(file): raise click.BadParameter( - 'Path "%s" does not exist.' % (file), + f'Path "{file}" does not exist.', ctx=ctx ) dotenv_as_dict = dotenv_values(file) @@ -60,7 +57,7 @@ def list(ctx: click.Context, format: bool) -> None: if v is not None: if format in ('export', 'shell'): v = shlex.quote(v) - click.echo('%s%s=%s' % (prefix, k, v)) + click.echo(f'{prefix}{k}={v}') @cli.command() @@ -68,13 +65,13 @@ def list(ctx: click.Context, format: bool) -> None: @click.argument('key', required=True) @click.argument('value', required=True) def set(ctx: click.Context, key: Any, value: Any) -> None: - '''Store the given key/value.''' + """Store the given key/value.""" file = ctx.obj['FILE'] quote = ctx.obj['QUOTE'] export = ctx.obj['EXPORT'] success, key, value = set_key(file, key, value, quote, export) if success: - click.echo('%s=%s' % (key, value)) + click.echo(f'{key}={value}') else: exit(1) @@ -83,11 +80,11 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: @click.pass_context @click.argument('key', required=True) def get(ctx: click.Context, key: Any) -> None: - '''Retrieve the value for the given key.''' + """Retrieve the value for the given key.""" file = ctx.obj['FILE'] if not os.path.isfile(file): raise click.BadParameter( - 'Path "%s" does not exist.' % (file), + f'Path "{file}" does not exist.', ctx=ctx ) stored_value = get_key(file, key) @@ -101,12 +98,12 @@ def get(ctx: click.Context, key: Any) -> None: @click.pass_context @click.argument('key', required=True) def unset(ctx: click.Context, key: Any) -> None: - '''Removes the given key.''' + """Removes the given key.""" file = ctx.obj['FILE'] quote = ctx.obj['QUOTE'] success, key = unset_key(file, key, quote) if success: - click.echo("Successfully removed %s" % key) + click.echo(f"Successfully removed {key}") else: exit(1) @@ -124,7 +121,7 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: file = ctx.obj['FILE'] if not os.path.isfile(file): raise click.BadParameter( - 'Invalid value for \'-f\' "%s" does not exist.' % (file), + f'Invalid value for \'-f\' "{file}" does not exist.', ctx=ctx ) dotenv_as_dict = { From 025f762241c9a2435cc33cd534d5d998b9d4c64c Mon Sep 17 00:00:00 2001 From: "Michael V. DePalatis" Date: Wed, 23 Nov 2022 08:41:04 -0700 Subject: [PATCH 39/78] Fix typo --- src/dotenv/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index e7b8392a..36ed94f5 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -321,7 +321,7 @@ def load_dotenv( from the `.env` file. encoding: Encoding to be used to read the file. Returns: - Bool: True if atleast one environment variable is set else False + Bool: True if at least one environment variable is set else False If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the .env file. From 7dc2492805f6bfc314b4ba4380d5ee5ad499b6c1 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Sat, 12 Nov 2022 14:02:33 +0100 Subject: [PATCH 40/78] Improve error message for `get` and `list` commands The error message would previously be confusing. For example, `dotenv -f . list` would print: Error: Invalid value: Path "." does not exist. Instead, we now print: Error opening env file: [Errno 21] Is a directory: '.' I used this opportunity to slightly refactor the I/O code (e.g. fewer system calls and possible race conditions) for those two subcommands (`get` and `list`). --- src/dotenv/cli.py | 48 ++++++++++++++++++++++++++++++----------------- tests/test_cli.py | 20 +++++++++++++++++--- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 97d6a948..b490bfaf 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -2,8 +2,9 @@ import os import shlex import sys +from contextlib import contextmanager from subprocess import Popen -from typing import Any, Dict, List +from typing import Any, Dict, IO, Iterator, List try: import click @@ -12,7 +13,7 @@ 'Run pip install "python-dotenv[cli]" to fix this.') sys.exit(1) -from .main import dotenv_values, get_key, set_key, unset_key +from .main import dotenv_values, set_key, unset_key from .version import __version__ @@ -33,6 +34,22 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file} +@contextmanager +def stream_file(path: os.PathLike) -> Iterator[IO[str]]: + """ + Open a file and yield the corresponding (decoded) stream. + + Exits with error code 2 if the file cannot be opened. + """ + + try: + with open(path) as stream: + yield stream + except OSError as exc: + print(f"Error opening env file: {exc}", file=sys.stderr) + exit(2) + + @cli.command() @click.pass_context @click.option('--format', default='simple', @@ -42,18 +59,16 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: def list(ctx: click.Context, format: bool) -> None: """Display all the stored key/value.""" file = ctx.obj['FILE'] - if not os.path.isfile(file): - raise click.BadParameter( - f'Path "{file}" does not exist.', - ctx=ctx - ) - dotenv_as_dict = dotenv_values(file) + + with stream_file(file) as stream: + values = dotenv_values(stream=stream) + if format == 'json': - click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) + click.echo(json.dumps(values, indent=2, sort_keys=True)) else: prefix = 'export ' if format == 'export' else '' - for k in sorted(dotenv_as_dict): - v = dotenv_as_dict[k] + for k in sorted(values): + v = values[k] if v is not None: if format in ('export', 'shell'): v = shlex.quote(v) @@ -82,12 +97,11 @@ def set(ctx: click.Context, key: Any, value: Any) -> None: def get(ctx: click.Context, key: Any) -> None: """Retrieve the value for the given key.""" file = ctx.obj['FILE'] - if not os.path.isfile(file): - raise click.BadParameter( - f'Path "{file}" does not exist.', - ctx=ctx - ) - stored_value = get_key(file, key) + + with stream_file(file) as stream: + values = dotenv_values(stream=stream) + + stored_value = values.get(key) if stored_value: click.echo(stored_value) else: diff --git a/tests/test_cli.py b/tests/test_cli.py index ca5ba2a1..02dc9d96 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -40,7 +40,14 @@ def test_list_non_existent_file(cli): result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'list']) assert result.exit_code == 2, result.output - assert "does not exist" in result.output + assert "Error opening env file" in result.output + + +def test_list_not_a_file(cli): + result = cli.invoke(dotenv_cli, ['--file', '.', 'list']) + + assert result.exit_code == 2, result.output + assert "Error opening env file" in result.output def test_list_no_file(cli): @@ -64,11 +71,18 @@ def test_get_non_existent_value(cli, dotenv_file): assert (result.exit_code, result.output) == (1, "") -def test_get_no_file(cli): +def test_get_non_existent_file(cli): result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'get', 'a']) assert result.exit_code == 2 - assert "does not exist" in result.output + assert "Error opening env file" in result.output + + +def test_get_not_a_file(cli): + result = cli.invoke(dotenv_cli, ['--file', '.', 'get', 'a']) + + assert result.exit_code == 2 + assert "Error opening env file" in result.output def test_unset_existing_value(cli, dotenv_file): From f75103c655f71015fbc0b90a399133149b17f58c Mon Sep 17 00:00:00 2001 From: Lukas Kahwe Smith Date: Thu, 5 Jan 2023 06:41:54 +0100 Subject: [PATCH 41/78] Updated license format to better Align with BSD OSI template (#433) Co-authored-by: Saurabh Kumar Thanks @lsmith77 --- LICENSE | 64 +-------------------------------------------------------- 1 file changed, 1 insertion(+), 63 deletions(-) diff --git a/LICENSE b/LICENSE index 39372fee..acfe8334 100644 --- a/LICENSE +++ b/LICENSE @@ -1,66 +1,4 @@ -python-dotenv -Copyright (c) 2014, Saurabh Kumar - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of python-dotenv nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -django-dotenv-rw -Copyright (c) 2013, Ted Tieken - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of django-dotenv nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Original django-dotenv -Copyright (c) 2013, Jacob Kaplan-Moss - -All rights reserved. +Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dotenv-rw), 2013, Jacob Kaplan-Moss (django-dotenv) Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: From 09cf4aba6279848e99874ac552660c7108b6ae50 Mon Sep 17 00:00:00 2001 From: Eddie Aftandilian Date: Tue, 10 Jan 2023 21:37:56 -0800 Subject: [PATCH 42/78] Fix type hint for dotenv_path var, add StrPath alias (#432) * Fix type hint for load_dotenv Fixes #431 * Quote type hints to avoid runtime errors in earlier Python versions * Revise type of dotenv_path parameter Based on PR feedback and typeshed's type hint for the built-in open() function: https://github.com/python/typeshed/blob/e2d67bf7034f68c07bd35150247e58e0817725d9/stdlib/builtins.pyi#L1421 * Allow only string paths, not byte paths These paths can flow into `shutil.move`, which does not accept byte paths or (int) file descriptors. See https://github.com/python/typeshed/pull/6832 * Create a type alias for the paths this library accepts And use it consistently in main.py. --- src/dotenv/main.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 36ed94f5..f40c20ea 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -12,6 +12,12 @@ from .parser import Binding, parse_stream from .variables import parse_variables +# A type alias for a string path to be used for the paths in this file. +# These paths may flow to `open()` and `shutil.move()`; `shutil.move()` +# only accepts string paths, not byte paths or file descriptors. See +# https://github.com/python/typeshed/pull/6832. +StrPath = Union[str, 'os.PathLike[str]'] + logger = logging.getLogger(__name__) @@ -28,14 +34,14 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding class DotEnv: def __init__( self, - dotenv_path: Optional[Union[str, os.PathLike]], + dotenv_path: Optional[StrPath], stream: Optional[IO[str]] = None, verbose: bool = False, encoding: Optional[str] = None, interpolate: bool = True, override: bool = True, ) -> None: - self.dotenv_path: Optional[Union[str, os.PathLike]] = dotenv_path + self.dotenv_path: Optional[StrPath] = dotenv_path self.stream: Optional[IO[str]] = stream self._dict: Optional[Dict[str, Optional[str]]] = None self.verbose: bool = verbose @@ -108,7 +114,7 @@ def get(self, key: str) -> Optional[str]: def get_key( - dotenv_path: Union[str, os.PathLike], + dotenv_path: StrPath, key_to_get: str, encoding: Optional[str] = "utf-8", ) -> Optional[str]: @@ -122,7 +128,7 @@ def get_key( @contextmanager def rewrite( - path: Union[str, os.PathLike], + path: StrPath, encoding: Optional[str], ) -> Iterator[Tuple[IO[str], IO[str]]]: if not os.path.isfile(path): @@ -139,7 +145,7 @@ def rewrite( def set_key( - dotenv_path: Union[str, os.PathLike], + dotenv_path: StrPath, key_to_set: str, value_to_set: str, quote_mode: str = "always", @@ -188,7 +194,7 @@ def set_key( def unset_key( - dotenv_path: Union[str, os.PathLike], + dotenv_path: StrPath, key_to_unset: str, quote_mode: str = "always", encoding: Optional[str] = "utf-8", @@ -303,7 +309,7 @@ def _is_interactive(): def load_dotenv( - dotenv_path: Union[str, os.PathLike, None] = None, + dotenv_path: Optional[StrPath] = None, stream: Optional[IO[str]] = None, verbose: bool = False, override: bool = False, @@ -341,7 +347,7 @@ def load_dotenv( def dotenv_values( - dotenv_path: Union[str, os.PathLike, None] = None, + dotenv_path: Optional[StrPath] = None, stream: Optional[IO[str]] = None, verbose: bool = False, interpolate: bool = True, From a18ea18dce89d5f1941710e461c2e72055f590d4 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 21 Jan 2023 15:31:57 +0530 Subject: [PATCH 43/78] Fix coverage reporting (#445) --- .github/workflows/test.yml | 5 +++++ tox.ini | 24 +++++++++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15875768..758fbb46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,21 +5,26 @@ on: [push, pull_request] jobs: test: runs-on: ${{ matrix.os }} + strategy: max-parallel: 8 matrix: os: - ubuntu-latest python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy3.9] + steps: - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: python -m pip install --upgrade pip pip install tox tox-gh-actions + - name: Test with tox run: tox diff --git a/tox.ini b/tox.ini index cb8a6625..9cca82a4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,26 @@ [tox] -envlist = lint,py{35,36,37,38,39,310,311},pypy3,manifest,coverage-report +envlist = lint,py{37,38,39,310,311},pypy3,manifest,coverage-report [gh-actions] python = - 3.7: py37, coverage-report - 3.8: py38, coverage-report - 3.9: py39, coverage-report - 3.10: py310, lint, manifest, coverage-report - 3.11: py311, coverage-report - pypy-3.9: pypy3, coverage-report + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311, lint, manifest + pypy-3.9: pypy3 [testenv] deps = pytest - coverage + pytest-cov sh click - py{37,38,39,310,311,py3}: ipython -commands = coverage run --parallel -m pytest {posargs} + py{37,38,39,310,311,pypy3}: ipython +commands = pytest --cov --cov-report=term-missing --cov-config setup.cfg {posargs} +depends = + py{37,38,39,310,311},pypy3: coverage-clean + coverage-report: py{35,36,37,38,39,310,311},pypy3 [testenv:lint] skip_install = true @@ -46,5 +49,4 @@ commands = coverage erase deps = coverage skip_install = true commands = - coverage combine coverage report From b84bccda57fc973e7b8367380194bfae558a3d4b Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 21 Jan 2023 15:45:30 +0530 Subject: [PATCH 44/78] Add changelog for 0.21.1 --- CHANGELOG.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e53060a5..29e38757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.21.1] - 2022-09-03 + +### Added +* Use Python 3.11 non-beta in CI (#438 by @bbc2) +* Modernize variables code (#434 by @Nougat-Waffle) +* Modernize main.py and parser.py code (#435 by @Nougat-Waffle) +* Improve conciseness of cli.py and __init__.py (#439 by @Nougat-Waffle) +* Improve error message for `get` and `list` commands when env file can't be opened (#441 by @bbc2) +* Updated Licence to align with BSD OSI template (#433 by @lsmith77) + + +### Fixed +* Fix Out-of-scope error when "dest" variable is undefined (#413 by @theGOTOguy) +* Fix IPython test warning about deprecated `magic` (#440 by @bbc2) +* Fix type hint for dotenv_path var, add StrPath alias (#432 by @eaf) + ## [0.21.0] - 2022-09-03 ### Added @@ -16,9 +33,9 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Drop Python 3.5 and 3.6 and upgrade GA (#393 by @eggplants) * Use `open` instead of `io.open`. (#389 by @rabinadk1) * Improve documentation for variables without a value (#390 by @bbc2) -* Add `parse_it` to Related Projects by (#410 by @naorlivne) -* Update README.md by (#415 by @harveer07) -* Improve documentation with direct use of MkDocs by (#398 by @bbc2) +* Add `parse_it` to Related Projects (#410 by @naorlivne) +* Update README.md (#415 by @harveer07) +* Improve documentation with direct use of MkDocs (#398 by @bbc2) ## [0.20.0] - 2022-03-24 From 5317a560e0943a7311bbc5bfbd9e4b4c13bc2bb6 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 21 Jan 2023 15:50:46 +0530 Subject: [PATCH 45/78] =?UTF-8?q?Bump=20version:=200.21.0=20=E2=86=92=200.?= =?UTF-8?q?21.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- src/dotenv/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4d49c291..89b5d30e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.21.0 +current_version = 0.21.1 commit = True tag = True diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 6a726d85..76f24586 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "0.21.0" +__version__ = "0.21.1" From 291fa669804e0ceaef94a35e7cd6cc90b6189cd5 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 21 Jan 2023 16:31:07 +0530 Subject: [PATCH 46/78] Update documtation --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++----------------- LICENSE | 18 +++++++++------- docs/license.md | 1 + docs/reference.md | 3 +-- mkdocs.yml | 8 +++++++ 5 files changed, 55 insertions(+), 29 deletions(-) create mode 120000 docs/license.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e38757..5845146c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,37 +5,48 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +* + +### Fixed + +* + + ## [0.21.1] - 2022-09-03 ### Added -* Use Python 3.11 non-beta in CI (#438 by @bbc2) -* Modernize variables code (#434 by @Nougat-Waffle) -* Modernize main.py and parser.py code (#435 by @Nougat-Waffle) -* Improve conciseness of cli.py and __init__.py (#439 by @Nougat-Waffle) -* Improve error message for `get` and `list` commands when env file can't be opened (#441 by @bbc2) -* Updated Licence to align with BSD OSI template (#433 by @lsmith77) +* Use Python 3.11 non-beta in CI (#438 by [@bbc2]) +* Modernize variables code (#434 by [@Nougat-Waffle]) +* Modernize main.py and parser.py code (#435 by [@Nougat-Waffle]) +* Improve conciseness of cli.py and __init__.py (#439 by [@Nougat-Waffle]) +* Improve error message for `get` and `list` commands when env file can't be opened (#441 by [@bbc2]) +* Updated License to align with BSD OSI template (#433 by [@lsmith77]) ### Fixed -* Fix Out-of-scope error when "dest" variable is undefined (#413 by @theGOTOguy) -* Fix IPython test warning about deprecated `magic` (#440 by @bbc2) -* Fix type hint for dotenv_path var, add StrPath alias (#432 by @eaf) +* Fix Out-of-scope error when "dest" variable is undefined (#413 by [@theGOTOguy]) +* Fix IPython test warning about deprecated `magic` (#440 by [@bbc2]) +* Fix type hint for dotenv_path var, add StrPath alias (#432 by [@eaf]) ## [0.21.0] - 2022-09-03 ### Added -* CLI: add support for invocations via 'python -m'. (#395 by @theskumar) -* `load_dotenv` function now returns `False`. (#388 by @larsks) -* CLI: add --format= option to list command. (#407 by @sammck) +* CLI: add support for invocations via 'python -m'. (#395 by [@theskumar]) +* `load_dotenv` function now returns `False`. (#388 by [@larsks]) +* CLI: add --format= option to list command. (#407 by [@sammck]) ### Fixed -* Drop Python 3.5 and 3.6 and upgrade GA (#393 by @eggplants) -* Use `open` instead of `io.open`. (#389 by @rabinadk1) -* Improve documentation for variables without a value (#390 by @bbc2) -* Add `parse_it` to Related Projects (#410 by @naorlivne) -* Update README.md (#415 by @harveer07) -* Improve documentation with direct use of MkDocs (#398 by @bbc2) +* Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants]) +* Use `open` instead of `io.open`. (#389 by [@rabinadk1]) +* Improve documentation for variables without a value (#390 by [@bbc2]) +* Add `parse_it` to Related Projects (#410 by [@naorlivne]) +* Update README.md (#415 by [@harveer07]) +* Improve documentation with direct use of MkDocs (#398 by [@bbc2]) ## [0.20.0] - 2022-03-24 @@ -326,6 +337,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@bbc2]: https://github.com/bbc2 [@befeleme]: https://github.com/befeleme [@cjauvin]: https://github.com/cjauvin +[@eaf]: https://github.com/eaf [@earlbread]: https://github.com/earlbread [@eggplants]: https://github.com/@eggplants [@ekohl]: https://github.com/ekohl @@ -337,13 +349,16 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@harveer07]: https://github.com/@harveer07 [@jadutter]: https://github.com/jadutter [@larsks]: https://github.com/@larsks +[@lsmith77]: https://github.com/lsmith77 [@mgorny]: https://github.com/mgorny [@naorlivne]: https://github.com/@naorlivne +[@Nougat-Waffle]: https://github.com/Nougat-Waffle [@qnighy]: https://github.com/qnighy [@rabinadk1]: https://github.com/@rabinadk1 [@sammck]: https://github.com/@sammck [@snobu]: https://github.com/snobu [@techalchemy]: https://github.com/techalchemy +[@theGOTOguy]: https://github.com/theGOTOguy [@theskumar]: https://github.com/theskumar [@ulyssessouza]: https://github.com/ulyssessouza [@venthur]: https://github.com/venthur @@ -352,7 +367,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.21.1...HEAD +[0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0 [0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 [0.19.2]: https://github.com/theskumar/python-dotenv/compare/v0.19.1...v0.19.2 diff --git a/LICENSE b/LICENSE index acfe8334..3a971190 100644 --- a/LICENSE +++ b/LICENSE @@ -3,14 +3,16 @@ Copyright (c) 2014, Saurabh Kumar (python-dotenv), 2013, Ted Tieken (django-dote Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of django-dotenv nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of django-dotenv nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT diff --git a/docs/license.md b/docs/license.md new file mode 120000 index 00000000..ea5b6064 --- /dev/null +++ b/docs/license.md @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/docs/reference.md b/docs/reference.md index a126448e..8a3762ad 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,3 +1,2 @@ -# Reference +# ::: dotenv -::: dotenv diff --git a/mkdocs.yml b/mkdocs.yml index 27063ca2..65acc0fb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,8 +5,15 @@ theme: name: material palette: primary: green + features: + - toc.follow + - navigation.sections + markdown_extensions: - mdx_truly_sane_lists + - toc: + toc_depth: 2 + plugins: - mkdocstrings: handlers: @@ -21,3 +28,4 @@ nav: - Changelog: changelog.md - Contributing: contributing.md - Reference: reference.md + - License: license.md From b904a722c8aea0a41948ea50d0c549ccece97729 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sat, 21 Jan 2023 19:17:24 +0530 Subject: [PATCH 47/78] update docs - better toc for changelog --- CHANGELOG.md | 66 ++++++++++++++++++++++++++++------------------------ mkdocs.yml | 2 -- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5845146c..d07533b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,20 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -### Added +**Added** * -### Fixed +**Fixed** * -## [0.21.1] - 2022-09-03 +## [0.21.1] - 2022-01-21 + +**Added** -### Added * Use Python 3.11 non-beta in CI (#438 by [@bbc2]) * Modernize variables code (#434 by [@Nougat-Waffle]) * Modernize main.py and parser.py code (#435 by [@Nougat-Waffle]) @@ -28,19 +29,22 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Updated License to align with BSD OSI template (#433 by [@lsmith77]) -### Fixed +**Fixed** + * Fix Out-of-scope error when "dest" variable is undefined (#413 by [@theGOTOguy]) * Fix IPython test warning about deprecated `magic` (#440 by [@bbc2]) * Fix type hint for dotenv_path var, add StrPath alias (#432 by [@eaf]) ## [0.21.0] - 2022-09-03 -### Added +**Added** + * CLI: add support for invocations via 'python -m'. (#395 by [@theskumar]) * `load_dotenv` function now returns `False`. (#388 by [@larsks]) * CLI: add --format= option to list command. (#407 by [@sammck]) -### Fixed +**Fixed** + * Drop Python 3.5 and 3.6 and upgrade GA (#393 by [@eggplants]) * Use `open` instead of `io.open`. (#389 by [@rabinadk1]) * Improve documentation for variables without a value (#390 by [@bbc2]) @@ -50,12 +54,12 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.20.0] - 2022-03-24 -### Added +**Added** - Add `encoding` (`Optional[str]`) parameter to `get_key`, `set_key` and `unset_key`. (#379 by [@bbc2]) -### Fixed +**Fixed** - Use dict to specify the `entry_points` parameter of `setuptools.setup` (#376 by [@mgorny]). @@ -63,25 +67,25 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.19.2] - 2021-11-11 -### Fixed +**Fixed** - In `set_key`, add missing newline character before new entry if necessary. (#361 by [@bbc2]) ## [0.19.1] - 2021-08-09 -### Added +**Added** - Add support for Python 3.10. (#359 by [@theskumar]) ## [0.19.0] - 2021-07-24 -### Changed +**Changed** - Require Python 3.5 or a later version. Python 2 and 3.4 are no longer supported. (#341 by [@bbc2]). -### Added +**Added** - The `dotenv_path` argument of `set_key` and `unset_key` now has a type of `Union[str, os.PathLike]` instead of just `os.PathLike` (#347 by [@bbc2]). @@ -91,7 +95,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.18.0] - 2021-06-20 -### Changed +**Changed** - Raise `ValueError` if `quote_mode` isn't one of `always`, `auto` or `never` in `set_key` (#330 by [@bbc2]). @@ -104,23 +108,23 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.17.1] - 2021-04-29 -### Fixed +**Fixed** - Fixed tests for build environments relying on `PYTHONPATH` (#318 by [@befeleme]). ## [0.17.0] - 2021-04-02 -### Changed +**Changed** - Make `dotenv get ` only show the value, not `key=value` (#313 by [@bbc2]). -### Added +**Added** - Add `--override`/`--no-override` option to `dotenv run` (#312 by [@zueve] and [@bbc2]). ## [0.16.0] - 2021-03-27 -### Changed +**Changed** - The default value of the `encoding` parameter for `load_dotenv` and `dotenv_values` is now `"utf-8"` instead of `None` (#306 by [@bbc2]). @@ -128,17 +132,17 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.15.0] - 2020-10-28 -### Added +**Added** - Add `--export` option to `set` to make it prepend the binding with `export` (#270 by [@jadutter]). -### Changed +**Changed** - Make `set` command create the `.env` file in the current directory if no `.env` file was found (#270 by [@jadutter]). -### Fixed +**Fixed** - Fix potentially empty expanded value for duplicate key (#260 by [@bbc2]). - Fix import error on Python 3.5.0 and 3.5.1 (#267 by [@gongqingkui]). @@ -147,30 +151,30 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.14.0] - 2020-07-03 -### Changed +**Changed** - Privilege definition in file over the environment in variable expansion (#256 by [@elbehery95]). -### Fixed +**Fixed** - Improve error message for when file isn't found (#245 by [@snobu]). - Use HTTPS URL in package meta data (#251 by [@ekohl]). ## [0.13.0] - 2020-04-16 -### Added +**Added** - Add support for a Bash-like default value in variable expansion (#248 by [@bbc2]). ## [0.12.0] - 2020-02-28 -### Changed +**Changed** - Use current working directory to find `.env` when bundled by PyInstaller (#213 by [@gergelyk]). -### Fixed +**Fixed** - Fix escaping of quoted values written by `set_key` (#236 by [@bbc2]). - Fix `dotenv run` crashing on environment variables without values (#237 by [@yannham]). @@ -178,23 +182,23 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.11.0] - 2020-02-07 -### Added +**Added** - Add `interpolate` argument to `load_dotenv` and `dotenv_values` to disable interpolation (#232 by [@ulyssessouza]). -### Changed +**Changed** - Use logging instead of warnings (#231 by [@bbc2]). -### Fixed +**Fixed** - Fix installation in non-UTF-8 environments (#225 by [@altendky]). - Fix PyPI classifiers (#228 by [@bbc2]). ## [0.10.5] - 2020-01-19 -### Fixed +**Fixed** - Fix handling of malformed lines and lines without a value (#222 by [@bbc2]): - Don't print warning when key has no value. @@ -203,7 +207,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.10.4] - 2020-01-17 -### Added +**Added** - Make typing optional (#179 by [@techalchemy]). - Print a warning on malformed line (#211 by [@bbc2]). diff --git a/mkdocs.yml b/mkdocs.yml index 65acc0fb..331965df 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,8 +11,6 @@ theme: markdown_extensions: - mdx_truly_sane_lists - - toc: - toc_depth: 2 plugins: - mkdocstrings: From fc19a55de9c8774880b73f19148704f311f517b2 Mon Sep 17 00:00:00 2001 From: jctanner Date: Thu, 23 Feb 2023 01:21:29 -0500 Subject: [PATCH 48/78] Handle situations where the cwd does not exist. (#446) This is seen in some situations with dynaconf where the system under test starts from a src code directory that got moved around or deleted during operation. Signed-off-by: James Tanner --- src/dotenv/cli.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index b490bfaf..65ead461 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -17,8 +17,22 @@ from .version import __version__ +def enumerate_env(): + """ + Return a path for the ${pwd}/.env file. + + If pwd does not exist, return None. + """ + try: + cwd = os.getcwd() + except FileNotFoundError: + return None + path = os.path.join(cwd, '.env') + return path + + @click.group() -@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'), +@click.option('-f', '--file', default=enumerate_env(), type=click.Path(file_okay=True), help="Location of the .env file, defaults to .env file in current working directory.") @click.option('-q', '--quote', default='always', From 87e5527fcff35b7118818ce8c35c4fd90fe80844 Mon Sep 17 00:00:00 2001 From: "Kenneth C. Arnold" Date: Fri, 24 Feb 2023 01:23:26 -0500 Subject: [PATCH 49/78] Update readme, add python-decouple as a related project (#451) I usually use dotenv, but I learned about this alternative from libhunt. It seems to have similar goals. (I'll probably stick with dotenv.) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 983b7d15..89fc6434 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,7 @@ defined in the following list: - [environs](https://github.com/sloria/environs) - [dynaconf](https://github.com/rochacbruno/dynaconf) - [parse_it](https://github.com/naorlivne/parse_it) +- [python-decouple](https://github.com/HBNetwork/python-decouple) ## Acknowledgements From 06116434822a4d7dd5c4213ff0a54dc6e41a9be6 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Fri, 24 Feb 2023 11:54:32 +0530 Subject: [PATCH 50/78] Drop support for python 3.7, add python 3.12-dev (#449) * fixes for sh 2.* * Drop support for python 3.7 * Add python 3.12 alpha --- .github/workflows/test.yml | 2 +- requirements.txt | 2 +- setup.py | 4 +-- tests/test_cli.py | 66 +++++++++++++++++++------------------- tox.ini | 14 ++++---- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 758fbb46..49e1399f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: matrix: os: - ubuntu-latest - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy3.9] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", pypy3.9] steps: - uses: actions/checkout@v3 diff --git a/requirements.txt b/requirements.txt index 0206316f..af7e1bc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ flake8>=2.2.3 ipython pytest-cov pytest>=3.9 -sh>=1.09 +sh>=2 tox twine wheel diff --git a/setup.py b/setup.py index bcf1b0dc..8ceddf92 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def read_files(files): package_data={ 'dotenv': ['py.typed'], }, - python_requires=">=3.7", + python_requires=">=3.8", extras_require={ 'cli': ['click>=5.0', ], }, @@ -45,11 +45,11 @@ def read_files(files): 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: PyPy', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', diff --git a/tests/test_cli.py b/tests/test_cli.py index 02dc9d96..8afbe59d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -153,61 +153,61 @@ def test_set_no_file(cli): def test_get_default_path(tmp_path): - sh.cd(str(tmp_path)) - with open(str(tmp_path / ".env"), "w") as f: - f.write("a=b") + with sh.pushd(str(tmp_path)): + with open(str(tmp_path / ".env"), "w") as f: + f.write("a=b") - result = sh.dotenv("get", "a") + result = sh.dotenv("get", "a") - assert result == "b\n" + assert result == "b\n" def test_run(tmp_path): - sh.cd(str(tmp_path)) - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b") + with sh.pushd(str(tmp_path)): + dotenv_file = str(tmp_path / ".env") + with open(dotenv_file, "w") as f: + f.write("a=b") - result = sh.dotenv("run", "printenv", "a") + result = sh.dotenv("run", "printenv", "a") - assert result == "b\n" + assert result == "b\n" def test_run_with_existing_variable(tmp_path): - sh.cd(str(tmp_path)) - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b") - env = dict(os.environ) - env.update({"LANG": "en_US.UTF-8", "a": "c"}) + with sh.pushd(str(tmp_path)): + dotenv_file = str(tmp_path / ".env") + with open(dotenv_file, "w") as f: + f.write("a=b") + env = dict(os.environ) + env.update({"LANG": "en_US.UTF-8", "a": "c"}) - result = sh.dotenv("run", "printenv", "a", _env=env) + result = sh.dotenv("run", "printenv", "a", _env=env) - assert result == "b\n" + assert result == "b\n" def test_run_with_existing_variable_not_overridden(tmp_path): - sh.cd(str(tmp_path)) - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b") - env = dict(os.environ) - env.update({"LANG": "en_US.UTF-8", "a": "c"}) + with sh.pushd(str(tmp_path)): + dotenv_file = str(tmp_path / ".env") + with open(dotenv_file, "w") as f: + f.write("a=b") + env = dict(os.environ) + env.update({"LANG": "en_US.UTF-8", "a": "c"}) - result = sh.dotenv("run", "--no-override", "printenv", "a", _env=env) + result = sh.dotenv("run", "--no-override", "printenv", "a", _env=env) - assert result == "c\n" + assert result == "c\n" def test_run_with_none_value(tmp_path): - sh.cd(str(tmp_path)) - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b\nc") + with sh.pushd(str(tmp_path)): + dotenv_file = str(tmp_path / ".env") + with open(dotenv_file, "w") as f: + f.write("a=b\nc") - result = sh.dotenv("run", "printenv", "a") + result = sh.dotenv("run", "printenv", "a") - assert result == "b\n" + assert result == "b\n" def test_run_with_other_env(dotenv_file): diff --git a/tox.ini b/tox.ini index 9cca82a4..fad86f73 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,26 @@ [tox] -envlist = lint,py{37,38,39,310,311},pypy3,manifest,coverage-report +envlist = lint,py{38,39,310,311,312-dev},pypy3,manifest,coverage-report [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311, lint, manifest + 3.12-dev: py312-dev pypy-3.9: pypy3 [testenv] deps = pytest pytest-cov - sh + sh >= 2.0.2, <3 click - py{37,38,39,310,311,pypy3}: ipython + py{38,39,310,311,py312-dev,pypy3}: ipython commands = pytest --cov --cov-report=term-missing --cov-config setup.cfg {posargs} depends = - py{37,38,39,310,311},pypy3: coverage-clean - coverage-report: py{35,36,37,38,39,310,311},pypy3 + py{38,39,310,311,312-dev},pypy3: coverage-clean + coverage-report: py{38,39,310,311,312-dev},pypy3 [testenv:lint] skip_install = true @@ -29,11 +29,11 @@ deps = mypy commands = flake8 src tests + mypy --python-version=3.12 src tests mypy --python-version=3.11 src tests mypy --python-version=3.10 src tests mypy --python-version=3.9 src tests mypy --python-version=3.8 src tests - mypy --python-version=3.7 src tests [testenv:manifest] deps = check-manifest From 6c8ddd23752b616aee384693381633187ee07107 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Fri, 24 Feb 2023 12:13:58 +0530 Subject: [PATCH 51/78] Prepare for release 1.0.0 --- CHANGELOG.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d07533b6..2f78c0d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -**Added** - -* +## [1.0.0] **Fixed** -* - - +* Drop support for python 3.7, add python 3.12-dev (#449 by [@theskumar]) +* Handle situations where the cwd does not exist. (#446 by [@jctanner]) ## [0.21.1] - 2022-01-21 @@ -352,6 +347,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@greyli]: https://github.com/greyli [@harveer07]: https://github.com/@harveer07 [@jadutter]: https://github.com/jadutter +[@jctanner]: https://github.com/jctanner [@larsks]: https://github.com/@larsks [@lsmith77]: https://github.com/lsmith77 [@mgorny]: https://github.com/mgorny @@ -371,7 +367,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v0.21.1...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0 [0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0 [0.20.0]: https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0 From d0684d1c092fb6a9a208a09d43f02e4876ee8196 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Fri, 24 Feb 2023 12:14:03 +0530 Subject: [PATCH 52/78] =?UTF-8?q?Bump=20version:=200.21.1=20=E2=86=92=201.?= =?UTF-8?q?0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- src/dotenv/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 89b5d30e..3fefd1f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.21.1 +current_version = 1.0.0 commit = True tag = True diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 76f24586..5becc17c 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "0.21.1" +__version__ = "1.0.0" From dd1af684f2586d2c2fdd722f9c45d3212e1e4e59 Mon Sep 17 00:00:00 2001 From: Jan Kislinger Date: Tue, 14 Mar 2023 08:32:42 +0100 Subject: [PATCH 53/78] FIx year in release in changelog (#453) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f78c0d0..220d1888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Drop support for python 3.7, add python 3.12-dev (#449 by [@theskumar]) * Handle situations where the cwd does not exist. (#446 by [@jctanner]) -## [0.21.1] - 2022-01-21 +## [0.21.1] - 2023-01-21 **Added** From 137bc3dc0b8cf3d417a1e800c4065c526e3fb96a Mon Sep 17 00:00:00 2001 From: Sam Wyma Date: Mon, 17 Apr 2023 16:12:57 +0100 Subject: [PATCH 54/78] Gracefully handle code which has been imported from a zipfile (#456) --- src/dotenv/main.py | 4 +- tests/test_zip_imports.py | 101 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 tests/test_zip_imports.py diff --git a/src/dotenv/main.py b/src/dotenv/main.py index f40c20ea..383b79f4 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -291,7 +291,9 @@ def _is_interactive(): frame = sys._getframe() current_file = __file__ - while frame.f_code.co_filename == current_file: + while frame.f_code.co_filename == current_file or not os.path.exists( + frame.f_code.co_filename + ): assert frame.f_back is not None frame = frame.f_back frame_filename = frame.f_code.co_filename diff --git a/tests/test_zip_imports.py b/tests/test_zip_imports.py new file mode 100644 index 00000000..46d3c02e --- /dev/null +++ b/tests/test_zip_imports.py @@ -0,0 +1,101 @@ +import os +import sys +import sh +import textwrap +from typing import List +from unittest import mock +from zipfile import ZipFile + + +def walk_to_root(path: str): + last_dir = None + current_dir = path + while last_dir != current_dir: + yield current_dir + (parent_dir, _) = os.path.split(current_dir) + last_dir, current_dir = current_dir, parent_dir + + +class FileToAdd: + def __init__(self, content: str, path: str): + self.content = content + self.path = path + + +def setup_zipfile(path, files: List[FileToAdd]): + zip_file_path = path / "test.zip" + dirs_init_py_added_to = set() + with ZipFile(zip_file_path, "w") as zip: + for f in files: + zip.writestr(data=f.content, zinfo_or_arcname=f.path) + for dir in walk_to_root(os.path.dirname(f.path)): + if dir not in dirs_init_py_added_to: + print(os.path.join(dir, "__init__.py")) + zip.writestr( + data="", zinfo_or_arcname=os.path.join(dir, "__init__.py") + ) + dirs_init_py_added_to.add(dir) + return zip_file_path + + +@mock.patch.object(sys, "path", list(sys.path)) +def test_load_dotenv_gracefully_handles_zip_imports_when_no_env_file(tmp_path): + zip_file_path = setup_zipfile( + tmp_path, + [ + FileToAdd( + content=textwrap.dedent( + """ + from dotenv import load_dotenv + + load_dotenv() + """ + ), + path="child1/child2/test.py", + ), + ], + ) + + # Should run without an error + sys.path.append(str(zip_file_path)) + import child1.child2.test # noqa + + +def test_load_dotenv_outside_zip_file_when_called_in_zipfile(tmp_path): + zip_file_path = setup_zipfile( + tmp_path, + [ + FileToAdd( + content=textwrap.dedent( + """ + from dotenv import load_dotenv + + load_dotenv() + """ + ), + path="child1/child2/test.py", + ), + ], + ) + dotenv_path = tmp_path / ".env" + dotenv_path.write_bytes(b"a=b") + code_path = tmp_path / "code.py" + code_path.write_text( + textwrap.dedent( + f""" + import os + import sys + + sys.path.append("{zip_file_path}") + + import child1.child2.test + + print(os.environ['a']) + """ + ) + ) + os.chdir(str(tmp_path)) + + result = sh.Command(sys.executable)(code_path) + + assert result == "b\n" From be96be259c7eaf687360367de53e5e099aea48df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= <6774676+eumiro@users.noreply.github.com> Date: Sat, 24 Jun 2023 10:02:12 +0200 Subject: [PATCH 55/78] Use pathlib.Path in tests (#466) --- tests/conftest.py | 8 +- tests/test_cli.py | 93 +++++++++----------- tests/test_ipython.py | 6 +- tests/test_main.py | 196 ++++++++++++++++++------------------------ 4 files changed, 134 insertions(+), 169 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 24a82528..69193de0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ def cli(): @pytest.fixture -def dotenv_file(tmp_path): - file_ = tmp_path / '.env' - file_.write_bytes(b'') - yield str(file_) +def dotenv_path(tmp_path): + path = tmp_path / '.env' + path.write_bytes(b'') + yield path diff --git a/tests/test_cli.py b/tests/test_cli.py index 8afbe59d..fc309b48 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,8 +1,10 @@ import os - -import pytest import sh +from pathlib import Path from typing import Optional + +import pytest + import dotenv from dotenv.cli import cli as dotenv_cli from dotenv.version import __version__ @@ -23,11 +25,10 @@ ("export", "x='a b c'", '''export x='a b c'\n'''), ) ) -def test_list(cli, dotenv_file, format: Optional[str], content: str, expected: str): - with open(dotenv_file, "w") as f: - f.write(content + '\n') +def test_list(cli, dotenv_path, format: Optional[str], content: str, expected: str): + dotenv_path.write_text(content + '\n') - args = ['--file', dotenv_file, 'list'] + args = ['--file', dotenv_path, 'list'] if format is not None: args.extend(['--format', format]) @@ -56,17 +57,16 @@ def test_list_no_file(cli): assert (result.exit_code, result.output) == (1, "") -def test_get_existing_value(cli, dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_get_existing_value(cli, dotenv_path): + dotenv_path.write_text("a=b") - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'get', 'a']) + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) assert (result.exit_code, result.output) == (0, "b\n") -def test_get_non_existent_value(cli, dotenv_file): - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'get', 'a']) +def test_get_non_existent_value(cli, dotenv_path): + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'get', 'a']) assert (result.exit_code, result.output) == (1, "") @@ -85,21 +85,20 @@ def test_get_not_a_file(cli): assert "Error opening env file" in result.output -def test_unset_existing_value(cli, dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_unset_existing_value(cli, dotenv_path): + dotenv_path.write_text("a=b") - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'unset', 'a']) + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'unset', 'a']) assert (result.exit_code, result.output) == (0, "Successfully removed a\n") - assert open(dotenv_file, "r").read() == "" + assert dotenv_path.read_text() == "" -def test_unset_non_existent_value(cli, dotenv_file): - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'unset', 'a']) +def test_unset_non_existent_value(cli, dotenv_path): + result = cli.invoke(dotenv_cli, ['--file', dotenv_path, 'unset', 'a']) assert (result.exit_code, result.output) == (1, "") - assert open(dotenv_file, "r").read() == "" + assert dotenv_path.read_text() == "" @pytest.mark.parametrize( @@ -112,31 +111,31 @@ def test_unset_non_existent_value(cli, dotenv_file): ("auto", "a", "$", "a='$'\n"), ) ) -def test_set_quote_options(cli, dotenv_file, quote_mode, variable, value, expected): +def test_set_quote_options(cli, dotenv_path, quote_mode, variable, value, expected): result = cli.invoke( dotenv_cli, - ["--file", dotenv_file, "--export", "false", "--quote", quote_mode, "set", variable, value] + ["--file", dotenv_path, "--export", "false", "--quote", quote_mode, "set", variable, value] ) assert (result.exit_code, result.output) == (0, "{}={}\n".format(variable, value)) - assert open(dotenv_file, "r").read() == expected + assert dotenv_path.read_text() == expected @pytest.mark.parametrize( - "dotenv_file,export_mode,variable,value,expected", + "dotenv_path,export_mode,variable,value,expected", ( - (".nx_file", "true", "a", "x", "export a='x'\n"), - (".nx_file", "false", "a", "x", "a='x'\n"), + (Path(".nx_file"), "true", "a", "x", "export a='x'\n"), + (Path(".nx_file"), "false", "a", "x", "a='x'\n"), ) ) -def test_set_export(cli, dotenv_file, export_mode, variable, value, expected): +def test_set_export(cli, dotenv_path, export_mode, variable, value, expected): result = cli.invoke( dotenv_cli, - ["--file", dotenv_file, "--quote", "always", "--export", export_mode, "set", variable, value] + ["--file", dotenv_path, "--quote", "always", "--export", export_mode, "set", variable, value] ) assert (result.exit_code, result.output) == (0, "{}={}\n".format(variable, value)) - assert open(dotenv_file, "r").read() == expected + assert dotenv_path.read_text() == expected def test_set_non_existent_file(cli): @@ -153,9 +152,8 @@ def test_set_no_file(cli): def test_get_default_path(tmp_path): - with sh.pushd(str(tmp_path)): - with open(str(tmp_path / ".env"), "w") as f: - f.write("a=b") + with sh.pushd(tmp_path): + (tmp_path / ".env").write_text("a=b") result = sh.dotenv("get", "a") @@ -163,10 +161,8 @@ def test_get_default_path(tmp_path): def test_run(tmp_path): - with sh.pushd(str(tmp_path)): - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b") + with sh.pushd(tmp_path): + (tmp_path / ".env").write_text("a=b") result = sh.dotenv("run", "printenv", "a") @@ -174,10 +170,8 @@ def test_run(tmp_path): def test_run_with_existing_variable(tmp_path): - with sh.pushd(str(tmp_path)): - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b") + with sh.pushd(tmp_path): + (tmp_path / ".env").write_text("a=b") env = dict(os.environ) env.update({"LANG": "en_US.UTF-8", "a": "c"}) @@ -187,10 +181,8 @@ def test_run_with_existing_variable(tmp_path): def test_run_with_existing_variable_not_overridden(tmp_path): - with sh.pushd(str(tmp_path)): - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b") + with sh.pushd(tmp_path): + (tmp_path / ".env").write_text("a=b") env = dict(os.environ) env.update({"LANG": "en_US.UTF-8", "a": "c"}) @@ -200,21 +192,18 @@ def test_run_with_existing_variable_not_overridden(tmp_path): def test_run_with_none_value(tmp_path): - with sh.pushd(str(tmp_path)): - dotenv_file = str(tmp_path / ".env") - with open(dotenv_file, "w") as f: - f.write("a=b\nc") + with sh.pushd(tmp_path): + (tmp_path / ".env").write_text("a=b\nc") result = sh.dotenv("run", "printenv", "a") assert result == "b\n" -def test_run_with_other_env(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_run_with_other_env(dotenv_path): + dotenv_path.write_text("a=b") - result = sh.dotenv("--file", dotenv_file, "run", "printenv", "a") + result = sh.dotenv("--file", dotenv_path, "run", "printenv", "a") assert result == "b\n" diff --git a/tests/test_ipython.py b/tests/test_ipython.py index f988bd9c..960479ba 100644 --- a/tests/test_ipython.py +++ b/tests/test_ipython.py @@ -13,7 +13,7 @@ def test_ipython_existing_variable_no_override(tmp_path): dotenv_file = tmp_path / ".env" dotenv_file.write_text("a=b\n") - os.chdir(str(tmp_path)) + os.chdir(tmp_path) os.environ["a"] = "c" ipshell = InteractiveShellEmbed() @@ -29,7 +29,7 @@ def test_ipython_existing_variable_override(tmp_path): dotenv_file = tmp_path / ".env" dotenv_file.write_text("a=b\n") - os.chdir(str(tmp_path)) + os.chdir(tmp_path) os.environ["a"] = "c" ipshell = InteractiveShellEmbed() @@ -45,7 +45,7 @@ def test_ipython_new_variable(tmp_path): dotenv_file = tmp_path / ".env" dotenv_file.write_text("a=b\n") - os.chdir(str(tmp_path)) + os.chdir(tmp_path) ipshell = InteractiveShellEmbed() ipshell.run_line_magic("load_ext", "dotenv") diff --git a/tests/test_main.py b/tests/test_main.py index 9c895851..fd5e3903 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -12,14 +12,14 @@ def test_set_key_no_file(tmp_path): - nx_file = str(tmp_path / "nx") + nx_path = tmp_path / "nx" logger = logging.getLogger("dotenv.main") with mock.patch.object(logger, "warning"): - result = dotenv.set_key(nx_file, "foo", "bar") + result = dotenv.set_key(nx_path, "foo", "bar") assert result == (True, "foo", "bar") - assert os.path.exists(nx_file) + assert nx_path.exists() @pytest.mark.parametrize( @@ -40,162 +40,151 @@ def test_set_key_no_file(tmp_path): ("a=b", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"), ], ) -def test_set_key(dotenv_file, before, key, value, expected, after): +def test_set_key(dotenv_path, before, key, value, expected, after): logger = logging.getLogger("dotenv.main") - with open(dotenv_file, "w") as f: - f.write(before) + dotenv_path.write_text(before) with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.set_key(dotenv_file, key, value) + result = dotenv.set_key(dotenv_path, key, value) assert result == expected - assert open(dotenv_file, "r").read() == after + assert dotenv_path.read_text() == after mock_warning.assert_not_called() -def test_set_key_encoding(dotenv_file): +def test_set_key_encoding(dotenv_path): encoding = "latin-1" - result = dotenv.set_key(dotenv_file, "a", "é", encoding=encoding) + result = dotenv.set_key(dotenv_path, "a", "é", encoding=encoding) assert result == (True, "a", "é") - assert open(dotenv_file, "r", encoding=encoding).read() == "a='é'\n" + assert dotenv_path.read_text(encoding=encoding) == "a='é'\n" -def test_set_key_permission_error(dotenv_file): - os.chmod(dotenv_file, 0o000) +def test_set_key_permission_error(dotenv_path): + dotenv_path.chmod(0o000) with pytest.raises(Exception): - dotenv.set_key(dotenv_file, "a", "b") + dotenv.set_key(dotenv_path, "a", "b") - os.chmod(dotenv_file, 0o600) - with open(dotenv_file, "r") as fp: - assert fp.read() == "" + dotenv_path.chmod(0o600) + assert dotenv_path.read_text() == "" def test_get_key_no_file(tmp_path): - nx_file = str(tmp_path / "nx") + nx_path = tmp_path / "nx" logger = logging.getLogger("dotenv.main") with mock.patch.object(logger, "info") as mock_info, \ mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.get_key(nx_file, "foo") + result = dotenv.get_key(nx_path, "foo") assert result is None mock_info.assert_has_calls( calls=[ - mock.call("Python-dotenv could not find configuration file %s.", nx_file) + mock.call("Python-dotenv could not find configuration file %s.", nx_path) ], ) mock_warning.assert_has_calls( calls=[ - mock.call("Key %s not found in %s.", "foo", nx_file) + mock.call("Key %s not found in %s.", "foo", nx_path) ], ) -def test_get_key_not_found(dotenv_file): +def test_get_key_not_found(dotenv_path): logger = logging.getLogger("dotenv.main") with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.get_key(dotenv_file, "foo") + result = dotenv.get_key(dotenv_path, "foo") assert result is None - mock_warning.assert_called_once_with("Key %s not found in %s.", "foo", dotenv_file) + mock_warning.assert_called_once_with("Key %s not found in %s.", "foo", dotenv_path) -def test_get_key_ok(dotenv_file): +def test_get_key_ok(dotenv_path): logger = logging.getLogger("dotenv.main") - with open(dotenv_file, "w") as f: - f.write("foo=bar") + dotenv_path.write_text("foo=bar") with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.get_key(dotenv_file, "foo") + result = dotenv.get_key(dotenv_path, "foo") assert result == "bar" mock_warning.assert_not_called() -def test_get_key_encoding(dotenv_file): +def test_get_key_encoding(dotenv_path): encoding = "latin-1" - with open(dotenv_file, "w", encoding=encoding) as f: - f.write("é=è") + dotenv_path.write_text("é=è", encoding=encoding) - result = dotenv.get_key(dotenv_file, "é", encoding=encoding) + result = dotenv.get_key(dotenv_path, "é", encoding=encoding) assert result == "è" -def test_get_key_none(dotenv_file): +def test_get_key_none(dotenv_path): logger = logging.getLogger("dotenv.main") - with open(dotenv_file, "w") as f: - f.write("foo") + dotenv_path.write_text("foo") with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.get_key(dotenv_file, "foo") + result = dotenv.get_key(dotenv_path, "foo") assert result is None mock_warning.assert_not_called() -def test_unset_with_value(dotenv_file): +def test_unset_with_value(dotenv_path): logger = logging.getLogger("dotenv.main") - with open(dotenv_file, "w") as f: - f.write("a=b\nc=d") + dotenv_path.write_text("a=b\nc=d") with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.unset_key(dotenv_file, "a") + result = dotenv.unset_key(dotenv_path, "a") assert result == (True, "a") - with open(dotenv_file, "r") as f: - assert f.read() == "c=d" + assert dotenv_path.read_text() == "c=d" mock_warning.assert_not_called() -def test_unset_no_value(dotenv_file): +def test_unset_no_value(dotenv_path): logger = logging.getLogger("dotenv.main") - with open(dotenv_file, "w") as f: - f.write("foo") + dotenv_path.write_text("foo") with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.unset_key(dotenv_file, "foo") + result = dotenv.unset_key(dotenv_path, "foo") assert result == (True, "foo") - with open(dotenv_file, "r") as f: - assert f.read() == "" + assert dotenv_path.read_text() == "" mock_warning.assert_not_called() -def test_unset_encoding(dotenv_file): +def test_unset_encoding(dotenv_path): encoding = "latin-1" - with open(dotenv_file, "w", encoding=encoding) as f: - f.write("é=x") + dotenv_path.write_text("é=x", encoding=encoding) - result = dotenv.unset_key(dotenv_file, "é", encoding=encoding) + result = dotenv.unset_key(dotenv_path, "é", encoding=encoding) assert result == (True, "é") - with open(dotenv_file, "r", encoding=encoding) as f: - assert f.read() == "" + assert dotenv_path.read_text(encoding=encoding) == "" -def test_set_key_unauthorized_file(dotenv_file): - os.chmod(dotenv_file, 0o000) +def test_set_key_unauthorized_file(dotenv_path): + dotenv_path.chmod(0o000) with pytest.raises(PermissionError): - dotenv.set_key(dotenv_file, "a", "x") + dotenv.set_key(dotenv_path, "a", "x") def test_unset_non_existent_file(tmp_path): - nx_file = str(tmp_path / "nx") + nx_path = tmp_path / "nx" logger = logging.getLogger("dotenv.main") with mock.patch.object(logger, "warning") as mock_warning: - result = dotenv.unset_key(nx_file, "foo") + result = dotenv.unset_key(nx_path, "foo") assert result == (None, "foo") mock_warning.assert_called_once_with( "Can't delete from %s - it doesn't exist.", - nx_file, + nx_path, ) @@ -213,27 +202,22 @@ def prepare_file_hierarchy(path): Then try to automatically `find_dotenv` starting in `child4` """ - curr_dir = path - dirs = [] - for f in ['child1', 'child2', 'child3', 'child4']: - curr_dir /= f - dirs.append(curr_dir) - curr_dir.mkdir() - - return (dirs[0], dirs[-1]) + leaf = path / "child1" / "child2" / "child3" / "child4" + leaf.mkdir(parents=True, exist_ok=True) + return leaf def test_find_dotenv_no_file_raise(tmp_path): - (root, leaf) = prepare_file_hierarchy(tmp_path) - os.chdir(str(leaf)) + leaf = prepare_file_hierarchy(tmp_path) + os.chdir(leaf) with pytest.raises(IOError): dotenv.find_dotenv(raise_error_if_not_found=True, usecwd=True) def test_find_dotenv_no_file_no_raise(tmp_path): - (root, leaf) = prepare_file_hierarchy(tmp_path) - os.chdir(str(leaf)) + leaf = prepare_file_hierarchy(tmp_path) + os.chdir(leaf) result = dotenv.find_dotenv(usecwd=True) @@ -241,22 +225,21 @@ def test_find_dotenv_no_file_no_raise(tmp_path): def test_find_dotenv_found(tmp_path): - (root, leaf) = prepare_file_hierarchy(tmp_path) - os.chdir(str(leaf)) - dotenv_file = root / ".env" - dotenv_file.write_bytes(b"TEST=test\n") + leaf = prepare_file_hierarchy(tmp_path) + os.chdir(leaf) + dotenv_path = tmp_path / ".env" + dotenv_path.write_bytes(b"TEST=test\n") result = dotenv.find_dotenv(usecwd=True) - assert result == str(dotenv_file) + assert result == str(dotenv_path) @mock.patch.dict(os.environ, {}, clear=True) -def test_load_dotenv_existing_file(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_load_dotenv_existing_file(dotenv_path): + dotenv_path.write_text("a=b") - result = dotenv.load_dotenv(dotenv_file) + result = dotenv.load_dotenv(dotenv_path) assert result is True assert os.environ == {"a": "b"} @@ -273,44 +256,40 @@ def test_load_dotenv_no_file_verbose(): @mock.patch.dict(os.environ, {"a": "c"}, clear=True) -def test_load_dotenv_existing_variable_no_override(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_load_dotenv_existing_variable_no_override(dotenv_path): + dotenv_path.write_text("a=b") - result = dotenv.load_dotenv(dotenv_file, override=False) + result = dotenv.load_dotenv(dotenv_path, override=False) assert result is True assert os.environ == {"a": "c"} @mock.patch.dict(os.environ, {"a": "c"}, clear=True) -def test_load_dotenv_existing_variable_override(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_load_dotenv_existing_variable_override(dotenv_path): + dotenv_path.write_text("a=b") - result = dotenv.load_dotenv(dotenv_file, override=True) + result = dotenv.load_dotenv(dotenv_path, override=True) assert result is True assert os.environ == {"a": "b"} @mock.patch.dict(os.environ, {"a": "c"}, clear=True) -def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_file): - with open(dotenv_file, "w") as f: - f.write('a=b\nd="${a}"') +def test_load_dotenv_redefine_var_used_in_file_no_override(dotenv_path): + dotenv_path.write_text('a=b\nd="${a}"') - result = dotenv.load_dotenv(dotenv_file) + result = dotenv.load_dotenv(dotenv_path) assert result is True assert os.environ == {"a": "c", "d": "c"} @mock.patch.dict(os.environ, {"a": "c"}, clear=True) -def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_file): - with open(dotenv_file, "w") as f: - f.write('a=b\nd="${a}"') +def test_load_dotenv_redefine_var_used_in_file_with_override(dotenv_path): + dotenv_path.write_text('a=b\nd="${a}"') - result = dotenv.load_dotenv(dotenv_file, override=True) + result = dotenv.load_dotenv(dotenv_path, override=True) assert result is True assert os.environ == {"a": "b", "d": "b"} @@ -327,11 +306,10 @@ def test_load_dotenv_string_io_utf_8(): @mock.patch.dict(os.environ, {}, clear=True) -def test_load_dotenv_file_stream(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_load_dotenv_file_stream(dotenv_path): + dotenv_path.write_text("a=b") - with open(dotenv_file, "r") as f: + with dotenv_path.open() as f: result = dotenv.load_dotenv(stream=f) assert result is True @@ -349,18 +327,17 @@ def test_load_dotenv_in_current_dir(tmp_path): dotenv.load_dotenv(verbose=True) print(os.environ['a']) """)) - os.chdir(str(tmp_path)) + os.chdir(tmp_path) result = sh.Command(sys.executable)(code_path) assert result == 'b\n' -def test_dotenv_values_file(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_dotenv_values_file(dotenv_path): + dotenv_path.write_text("a=b") - result = dotenv.dotenv_values(dotenv_file) + result = dotenv.dotenv_values(dotenv_path) assert result == {"a": "b"} @@ -415,11 +392,10 @@ def test_dotenv_values_string_io(env, string, interpolate, expected): assert result == expected -def test_dotenv_values_file_stream(dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") +def test_dotenv_values_file_stream(dotenv_path): + dotenv_path.write_text("a=b") - with open(dotenv_file, "r") as f: + with dotenv_path.open() as f: result = dotenv.dotenv_values(stream=f) assert result == {"a": "b"} From 3ffcef60d10813b72ecf85d5941d51b0207cd40e Mon Sep 17 00:00:00 2001 From: Nicolas Appriou Date: Fri, 7 Jul 2023 19:27:15 +0200 Subject: [PATCH 56/78] Use https in README links (#474) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 89fc6434..ddc8ba87 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Python-dotenv reads key-value pairs from a `.env` file and can set them as environment variables. It helps in the development of applications following the -[12-factor](http://12factor.net/) principles. +[12-factor](https://12factor.net/) principles. - [Getting Started](#getting-started) - [Other Use Cases](#other-use-cases) @@ -243,5 +243,5 @@ people](https://github.com/theskumar/python-dotenv/graphs/contributors). [build_status_badge]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml/badge.svg [build_status_link]: https://github.com/theskumar/python-dotenv/actions/workflows/test.yml [pypi_badge]: https://badge.fury.io/py/python-dotenv.svg -[pypi_link]: http://badge.fury.io/py/python-dotenv +[pypi_link]: https://badge.fury.io/py/python-dotenv [python_streams]: https://docs.python.org/3/library/io.html From 0b94ac0822241eb526828cf506048fb0525d5c38 Mon Sep 17 00:00:00 2001 From: Freddy Boulton Date: Mon, 22 Jan 2024 21:45:11 -0800 Subject: [PATCH 57/78] Allow modules using load_dotenv to be reloaded when launched in a separate thread (#497) Update `is_interactive` code --- src/dotenv/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 383b79f4..20c7782e 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -280,7 +280,10 @@ def find_dotenv( def _is_interactive(): """ Decide whether this is running in a REPL or IPython notebook """ - main = __import__('__main__', None, None, fromlist=['__file__']) + try: + main = __import__('__main__', None, None, fromlist=['__file__']) + except ModuleNotFoundError: + return False return not hasattr(main, '__file__') if usecwd or _is_interactive() or getattr(sys, 'frozen', False): From 6ff139147559eff4d124c038ec5a4b60ffcf3033 Mon Sep 17 00:00:00 2001 From: Qwerty-133 <74311372+Qwerty-133@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:21:15 +0530 Subject: [PATCH 58/78] Fix temporary file is deleted before closing, in the rewrite function (#468) Currently, if an error is raised while using files from the rewrite function, then the temporary file is deleted before closing it. This is okay on unix, but unlinking open files causes an error on Windows. --- src/dotenv/main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 20c7782e..7bc54285 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -1,6 +1,7 @@ import io import logging import os +import pathlib import shutil import sys import tempfile @@ -131,17 +132,21 @@ def rewrite( path: StrPath, encoding: Optional[str], ) -> Iterator[Tuple[IO[str], IO[str]]]: - if not os.path.isfile(path): - with open(path, mode="w", encoding=encoding) as source: - source.write("") + pathlib.Path(path).touch() + with tempfile.NamedTemporaryFile(mode="w", encoding=encoding, delete=False) as dest: + error = None try: with open(path, encoding=encoding) as source: yield (source, dest) - except BaseException: - os.unlink(dest.name) - raise - shutil.move(dest.name, path) + except BaseException as err: + error = err + + if error is None: + shutil.move(dest.name, path) + else: + os.unlink(dest.name) + raise error from None def set_key( From b1eebbaaab2cf3e1c48fa5c7ad88cfb00e4b5e54 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 23 Jan 2024 11:34:15 +0530 Subject: [PATCH 59/78] Add python 3.12 and pypy3.10 to test runner --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49e1399f..68503d45 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,10 +11,10 @@ jobs: matrix: os: - ubuntu-latest - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", pypy3.9] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", pypy3.9, pypy3.10] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From 42dc08664bc7cef185a139137a39126a030f272c Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 23 Jan 2024 11:49:30 +0530 Subject: [PATCH 60/78] Update changelog for 1.0.1 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 220d1888..f63a1f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0] +## [1.0.1] - 2024-01-23 + +**Fixed** + +* Gracefully handle code which has been imported from a zipfile ([#456] by [@samwyma]) +* Allow modules using load_dotenv to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton]) +* Fix file not closed after deletion, handle error in the rewrite function ([#469] by [@Qwerty-133]) + +**Misc** +* Use pathlib.Path in tests ([#466] by [@eumiro]) +* Fix year in release date in changelog.md ([#454] by [@jankislinger]) +* Use https in README links ([#474] by [@Nicals]) + +## [1.0.0] - 2023-02-24 **Fixed** @@ -328,6 +341,11 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [#176]: https://github.com/theskumar/python-dotenv/issues/176 [#183]: https://github.com/theskumar/python-dotenv/issues/183 [#359]: https://github.com/theskumar/python-dotenv/issues/359 +[#469]: https://github.com/theskumar/python-dotenv/issues/469 +[#456]: https://github.com/theskumar/python-dotenv/issues/456 +[#466]: https://github.com/theskumar/python-dotenv/issues/466 +[#454]: https://github.com/theskumar/python-dotenv/issues/454 +[#474]: https://github.com/theskumar/python-dotenv/issues/474 [@alanjds]: https://github.com/alanjds [@altendky]: https://github.com/altendky @@ -341,21 +359,27 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@eggplants]: https://github.com/@eggplants [@ekohl]: https://github.com/ekohl [@elbehery95]: https://github.com/elbehery95 +[@eumiro]: https://github.com/eumiro [@Flimm]: https://github.com/Flimm +[@freddyaboulton]: https://github.com/freddyaboulton [@gergelyk]: https://github.com/gergelyk [@gongqingkui]: https://github.com/gongqingkui [@greyli]: https://github.com/greyli [@harveer07]: https://github.com/@harveer07 [@jadutter]: https://github.com/jadutter +[@jankislinger]: https://github.com/jankislinger [@jctanner]: https://github.com/jctanner [@larsks]: https://github.com/@larsks [@lsmith77]: https://github.com/lsmith77 [@mgorny]: https://github.com/mgorny [@naorlivne]: https://github.com/@naorlivne +[@Nicals]: https://github.com/Nicals [@Nougat-Waffle]: https://github.com/Nougat-Waffle [@qnighy]: https://github.com/qnighy +[@Qwerty-133]: https://github.com/Qwerty-133 [@rabinadk1]: https://github.com/@rabinadk1 [@sammck]: https://github.com/@sammck +[@samwyma]: https://github.com/samwyma [@snobu]: https://github.com/snobu [@techalchemy]: https://github.com/techalchemy [@theGOTOguy]: https://github.com/theGOTOguy @@ -367,7 +391,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@zueve]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...HEAD +[1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0 [0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0 From d6c0b9638349a7dd605d60ee555ff60421c1a594 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 23 Jan 2024 12:00:33 +0530 Subject: [PATCH 61/78] Bumpversion 1.0.0 -> 1.0.1 --- src/dotenv/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 5becc17c..5c4105cd 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.0.1" From 6d6070cc43cf5a774b757acb5499b16913cddf32 Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Thu, 4 Apr 2024 22:55:17 +0200 Subject: [PATCH 62/78] Add a security policy This is a basic security policy, mostly to provide an email address. I took inspiration from the example provided by GitHub and the policy from the Pallets project. --- .github/SECURITY.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..dbdabeb1 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| --------- | ------------------ | +| latest | :white_check_mark: | +| 0.x | :x: | + +## Reporting a Vulnerability + +If you believe you have identified a security issue with Python-dotenv, please email +python-dotenv@saurabh-kumar.com. A maintainer will contact you acknowledging the report +and how to continue. + +Be sure to include as much detail as necessary in your report. As with reporting normal +issues, a minimal reproducible example will help the maintainers address the issue faster. +If you are able, you may also include a fix for the issue generated with `git +format-patch`. From bf20c809882c56291cde997722dcb7516e395473 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 11 Mar 2024 15:20:51 +0100 Subject: [PATCH 63/78] Keep GitHub Actions up to date with GitHub's Dependabot Fixes warnings like at the bottom right of https://github.com/theskumar/python-dotenv/actions/runs/7980672386 * https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..be006de9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly From 8c9381e7ab617a4cde425e3df4684417fade72f5 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 8 Apr 2024 18:17:29 +0200 Subject: [PATCH 64/78] ci: fix multiline string in test.yml & use fail-fast strategy (#514) * Fix multiline string in test.yml * strategy: fail-fast: false * Update test.yml --- .github/workflows/test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68503d45..7c73b8b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false max-parallel: 8 matrix: os: @@ -17,14 +18,15 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Upgrade pip + run: python -m pip install --upgrade pip + - name: Install dependencies - run: - python -m pip install --upgrade pip - pip install tox tox-gh-actions + run: pip install tox tox-gh-actions - name: Test with tox run: tox From 08937a1911c042ed3fc7cbeeb4d1d5a73d2674ed Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Mon, 29 Apr 2024 10:01:16 +0530 Subject: [PATCH 65/78] docs: clearify default behaviour of load_dotenv closes https://github.com/theskumar/python-dotenv/issues/457 --- README.md | 4 ++-- src/dotenv/main.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ddc8ba87..1eca986d 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ configurable via the environment: ```python from dotenv import load_dotenv -load_dotenv() # take environment variables from .env. +load_dotenv() # take environment variables # Code of your application, which uses environment variables (e.g. from `os.environ` or # `os.getenv`) as if they came from the actual environment. ``` -By default, `load_dotenv` doesn't override existing environment variables. +By default, `load_dotenv` doesn't override existing environment variables and looks for a `.env` file in same directory as python script or searches for it incrementally higher up. To configure the development environment, add a `.env` in the root directory of your project: diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 7bc54285..052de054 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -340,7 +340,9 @@ def load_dotenv( Bool: True if at least one environment variable is set else False If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the - .env file. + .env file with it's default parameters. If you need to change the default parameters + of `find_dotenv()`, you can explicitly call `find_dotenv()` and pass the result + to this function as `dotenv_path`. """ if dotenv_path is None and stream is None: dotenv_path = find_dotenv() From 4543837fc674f82f131c6a1e0e7e897461feaffd Mon Sep 17 00:00:00 2001 From: eekstunt <51318131+eekstunt@users.noreply.github.com> Date: Thu, 18 Jul 2024 04:56:47 +0100 Subject: [PATCH 66/78] Enhance dotenv run: Switch to execvpe for better resource management and signal handling (#523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current implementation of `dotenv run` CLI uses `subprocess.Popen`, which spawns a child process to execute the specified command. ``` p = Popen(command, universal_newlines=True, bufsize=0, shell=False, env=cmd_env) ``` After spawning the child process, it exits with the same exit code returned by the child process. ``` ret = run_command(commandline, dotenv_as_dict) exit(ret) ``` ### We can enhance `dotenv run` usage dramatically while preserving exactly the same behaviour By switching to `os.execvpe` instead of `subprocess.Popen`, we can replace the parent dotenv process with the new process specified by the user. This results in only one active process—the program the user intended to run. **Benefits:** 1. No hanging parent process `dotenv run` acts as a launcher, so after executing `dotenv run redis-server`, only the Redis server process remains. The dotenv process, along with its Python interpreter, is completely replaced. This prevents the dotenv process from consuming RAM and other resources, which would otherwise persist until the Redis server exits. 2. Proper signal handling When using `subprocess.Popen`, the parent process (e.g., `dotenv`) remains responsible for handling and forwarding signals, which can lead to issues if the command doesn’t receive them directly. For instance, in Docker, if Redis was started without `exec`, it may not get important signals like `SIGTERM` when the container stops, potentially resulting in improper shutdowns or zombie processes. Using `os.execvpe` ensures that the command receives signals directly, improving reliability and making `dotenv` more suitable for production environments and improving reliability for DevOps engineers managing containerized applications. All current logic will be preserved because dotenv run does not do anything special except propagate the child process exit code. Thanks / @eekstunt --- src/dotenv/cli.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 65ead461..b5a97f8b 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -3,7 +3,6 @@ import shlex import sys from contextlib import contextmanager -from subprocess import Popen from typing import Any, Dict, IO, Iterator, List try: @@ -161,14 +160,13 @@ def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: if not commandline: click.echo('No command given.') exit(1) - ret = run_command(commandline, dotenv_as_dict) - exit(ret) + run_command(commandline, dotenv_as_dict) -def run_command(command: List[str], env: Dict[str, str]) -> int: - """Run command in sub process. +def run_command(command: List[str], env: Dict[str, str]) -> None: + """Replace the current process with the specified command. - Runs the command in a sub process with the variables from `env` + Replaces the current process with the specified command and the variables from `env` added in the current environment variables. Parameters @@ -180,8 +178,8 @@ def run_command(command: List[str], env: Dict[str, str]) -> int: Returns ------- - int - The return code of the command + None + This function does not return any value. It replaces the current process with the new one. """ # copy the current environment variables and add the vales from @@ -189,11 +187,4 @@ def run_command(command: List[str], env: Dict[str, str]) -> int: cmd_env = os.environ.copy() cmd_env.update(env) - p = Popen(command, - universal_newlines=True, - bufsize=0, - shell=False, - env=cmd_env) - _, _ = p.communicate() - - return p.returncode + os.execvpe(command[0], args=command, env=cmd_env) From 4d505f2c9bc3569791e64bca0f2e4300f43df0e0 Mon Sep 17 00:00:00 2001 From: Waket Zheng Date: Wed, 24 Jul 2024 03:20:57 +0800 Subject: [PATCH 67/78] ci: add py3.13 to test.yml (#527) * ci: add py3.13 to test.yml * Improve type hints * fix typo --- .github/workflows/test.yml | 3 ++- src/dotenv/cli.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7c73b8b8..74a24ddd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: matrix: os: - ubuntu-latest - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", pypy3.9, pypy3.10] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", pypy3.9, pypy3.10] steps: - uses: actions/checkout@v4 @@ -21,6 +21,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Upgrade pip run: python -m pip install --upgrade pip diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index b5a97f8b..33ae1485 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -3,7 +3,7 @@ import shlex import sys from contextlib import contextmanager -from typing import Any, Dict, IO, Iterator, List +from typing import Any, Dict, IO, Iterator, List, Optional try: import click @@ -16,7 +16,7 @@ from .version import __version__ -def enumerate_env(): +def enumerate_env() -> Optional[str]: """ Return a path for the ${pwd}/.env file. From 533f8ac83c7873391053c1854e539afb7e124a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Thu, 31 Oct 2024 23:12:30 -0600 Subject: [PATCH 68/78] Add Python 3.13 trove classifier (#535) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8ceddf92..b03b8568 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ def read_files(files): 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: PyPy', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', From 2b8635b79f1aa15cade0950117d4e7d12c298766 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:42:56 +0530 Subject: [PATCH 69/78] Bump the github-actions group with 2 updates (#529) Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 2 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v4) Updates `actions/setup-python` from 2 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7666da09..67668d53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,9 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies From 41593889b63bba7f6af22279968e88727ebf5d62 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sun, 9 Mar 2025 23:27:31 +0530 Subject: [PATCH 70/78] Add support for python 3.13 and drop 3.8 (#551) fixes #550 --- .github/workflows/test.yml | 35 ++++++++++--------- CHANGELOG.md | 13 +++++-- setup.cfg | 6 ++-- setup.py | 70 +++++++++++++++++++++----------------- tox.ini | 40 +++++++++++----------- 5 files changed, 91 insertions(+), 73 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74a24ddd..fc86910d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,22 +12,23 @@ jobs: matrix: os: - ubuntu-latest - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", pypy3.9, pypy3.10] + python-version: + ["3.9", "3.10", "3.11", "3.12", "3.13", pypy3.9, pypy3.10] steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - - name: Upgrade pip - run: python -m pip install --upgrade pip - - - name: Install dependencies - run: pip install tox tox-gh-actions - - - name: Test with tox - run: tox + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install dependencies + run: pip install tox tox-gh-actions + + - name: Test with tox + run: tox diff --git a/CHANGELOG.md b/CHANGELOG.md index f63a1f93..a198b1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- Drop support for Python 3.8 +- Add support for python 3.13 +- Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt] + ## [1.0.1] - 2024-01-23 **Fixed** * Gracefully handle code which has been imported from a zipfile ([#456] by [@samwyma]) -* Allow modules using load_dotenv to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton]) +* Allow modules using `load_dotenv` to be reloaded when launched in a separate thread ([#497] by [@freddyaboulton]) * Fix file not closed after deletion, handle error in the rewrite function ([#469] by [@Qwerty-133]) **Misc** @@ -317,7 +324,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 0.5.1 -- Fix find\_dotenv - it now start search from the file where this +- Fix `find_dotenv` - it now start search from the file where this function is called from. ## 0.5.0 @@ -346,6 +353,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [#466]: https://github.com/theskumar/python-dotenv/issues/466 [#454]: https://github.com/theskumar/python-dotenv/issues/454 [#474]: https://github.com/theskumar/python-dotenv/issues/474 +[#523]: https://github.com/theskumar/python-dotenv/issues/523 [@alanjds]: https://github.com/alanjds [@altendky]: https://github.com/altendky @@ -356,6 +364,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@cjauvin]: https://github.com/cjauvin [@eaf]: https://github.com/eaf [@earlbread]: https://github.com/earlbread +[@eekstunt]: https://github.com/eekstunt [@eggplants]: https://github.com/@eggplants [@ekohl]: https://github.com/ekohl [@elbehery95]: https://github.com/elbehery95 diff --git a/setup.cfg b/setup.cfg index 3fefd1f0..4a8f11ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0 +current_version = 1.0.1 commit = True tag = True @@ -24,7 +24,7 @@ relative_files = True source = dotenv [coverage:paths] -source = +source = src/dotenv .tox/*/lib/python*/site-packages/dotenv .tox/pypy*/site-packages/dotenv @@ -32,6 +32,6 @@ source = [coverage:report] show_missing = True include = */site-packages/dotenv/* -exclude_lines = +exclude_lines = if IS_TYPE_CHECKING: pragma: no cover diff --git a/setup.py b/setup.py index b03b8568..f3d43ca1 100644 --- a/setup.py +++ b/setup.py @@ -4,60 +4,68 @@ def read_files(files): data = [] for file in files: - with open(file, encoding='utf-8') as f: + with open(file, encoding="utf-8") as f: data.append(f.read()) return "\n".join(data) -long_description = read_files(['README.md', 'CHANGELOG.md']) +long_description = read_files(["README.md", "CHANGELOG.md"]) meta = {} -with open('./src/dotenv/version.py', encoding='utf-8') as f: +with open("./src/dotenv/version.py", encoding="utf-8") as f: exec(f.read(), meta) setup( name="python-dotenv", description="Read key-value pairs from a .env file and set them as environment variables", long_description=long_description, - long_description_content_type='text/markdown', - version=meta['__version__'], + long_description_content_type="text/markdown", + version=meta["__version__"], author="Saurabh Kumar", author_email="me+github@saurabh-kumar.com", url="https://github.com/theskumar/python-dotenv", - keywords=['environment variables', 'deployments', 'settings', 'env', 'dotenv', - 'configurations', 'python'], - packages=['dotenv'], - package_dir={'': 'src'}, + keywords=[ + "environment variables", + "deployments", + "settings", + "env", + "dotenv", + "configurations", + "python", + ], + packages=["dotenv"], + package_dir={"": "src"}, package_data={ - 'dotenv': ['py.typed'], + "dotenv": ["py.typed"], }, - python_requires=">=3.8", + python_requires=">=3.9", extras_require={ - 'cli': ['click>=5.0', ], + "cli": [ + "click>=5.0", + ], }, entry_points={ "console_scripts": [ "dotenv=dotenv.__main__:cli", ], }, - license='BSD-3-Clause', + license="BSD-3-Clause", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities', - 'Environment :: Web Environment', - ] + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: PyPy", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Topic :: System :: Systems Administration", + "Topic :: Utilities", + "Environment :: Web Environment", + ], ) diff --git a/tox.ini b/tox.ini index fad86f73..057a1ae9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,39 +1,39 @@ [tox] -envlist = lint,py{38,39,310,311,312-dev},pypy3,manifest,coverage-report +envlist = lint,py{39,310,311,312,313},pypy3,manifest,coverage-report [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 - 3.11: py311, lint, manifest - 3.12-dev: py312-dev + 3.11: py311 + 3.12: py312 + 3.13: py313, lint, manifest pypy-3.9: pypy3 [testenv] deps = - pytest - pytest-cov - sh >= 2.0.2, <3 - click - py{38,39,310,311,py312-dev,pypy3}: ipython + pytest + pytest-cov + sh >= 2.0.2, <3 + click + py{39,310,311,312,313,pypy3}: ipython commands = pytest --cov --cov-report=term-missing --cov-config setup.cfg {posargs} depends = - py{38,39,310,311,312-dev},pypy3: coverage-clean - coverage-report: py{38,39,310,311,312-dev},pypy3 + py{39,310,311,312,313},pypy3: coverage-clean + coverage-report: py{39,310,311,312,313},pypy3 [testenv:lint] skip_install = true deps = - flake8 - mypy + flake8 + mypy commands = - flake8 src tests - mypy --python-version=3.12 src tests - mypy --python-version=3.11 src tests - mypy --python-version=3.10 src tests - mypy --python-version=3.9 src tests - mypy --python-version=3.8 src tests + flake8 src tests + mypy --python-version=3.13 src tests + mypy --python-version=3.12 src tests + mypy --python-version=3.11 src tests + mypy --python-version=3.10 src tests + mypy --python-version=3.9 src tests [testenv:manifest] deps = check-manifest @@ -49,4 +49,4 @@ commands = coverage erase deps = coverage skip_install = true commands = - coverage report + coverage report From 3c19c03dd41bd930d115aeb570f64e794d436c5f Mon Sep 17 00:00:00 2001 From: Rod Elias Date: Sun, 9 Mar 2025 14:59:52 -0300 Subject: [PATCH 71/78] s/Python-dotenv/python-dotenv/ (#516) This commit uses the name `python-dotenv` instead of `Python-dotenv` in the README.md file --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1eca986d..e92949ef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status][build_status_badge]][build_status_link] [![PyPI version][pypi_badge]][pypi_link] -Python-dotenv reads key-value pairs from a `.env` file and can set them as environment +python-dotenv reads key-value pairs from a `.env` file and can set them as environment variables. It helps in the development of applications following the [12-factor](https://12factor.net/) principles. @@ -29,7 +29,7 @@ If your application takes its configuration from environment variables, like a 1 application, launching it in development is not very practical because you have to set those environment variables yourself. -To help you with that, you can add Python-dotenv to your application to make it load the +To help you with that, you can add python-dotenv to your application to make it load the configuration from a `.env` file when it is present (e.g. in development) while remaining configurable via the environment: @@ -201,7 +201,7 @@ empty string. ### Variable expansion -Python-dotenv can interpolate variables using POSIX variable expansion. +python-dotenv can interpolate variables using POSIX variable expansion. With `load_dotenv(override=True)` or `dotenv_values()`, the value of a variable is the first of the values defined in the following list: From 9acba4af31757e99e2d6e6700de621ee8f9b98ae Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Sun, 9 Mar 2025 23:44:09 +0530 Subject: [PATCH 72/78] Some more s/Python-dotenv/python-dotenv/ (#552) --- .github/SECURITY.md | 5 ++--- src/dotenv/main.py | 49 +++++++++++++++++++++++---------------------- tests/test_main.py | 43 ++++++++++++++++++--------------------- 3 files changed, 47 insertions(+), 50 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index dbdabeb1..00d4d5e4 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -9,11 +9,10 @@ ## Reporting a Vulnerability -If you believe you have identified a security issue with Python-dotenv, please email +If you believe you have identified a security issue with python-dotenv, please email python-dotenv@saurabh-kumar.com. A maintainer will contact you acknowledging the report and how to continue. Be sure to include as much detail as necessary in your report. As with reporting normal issues, a minimal reproducible example will help the maintainers address the issue faster. -If you are able, you may also include a fix for the issue generated with `git -format-patch`. +If you are able, you may also include a fix for the issue generated with `git format-patch`. diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 052de054..0c81bba5 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -7,8 +7,7 @@ import tempfile from collections import OrderedDict from contextlib import contextmanager -from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, - Union) +from typing import IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, Union from .parser import Binding, parse_stream from .variables import parse_variables @@ -17,7 +16,7 @@ # These paths may flow to `open()` and `shutil.move()`; `shutil.move()` # only accepts string paths, not byte paths or file descriptors. See # https://github.com/python/typeshed/pull/6832. -StrPath = Union[str, 'os.PathLike[str]'] +StrPath = Union[str, "os.PathLike[str]"] logger = logging.getLogger(__name__) @@ -26,7 +25,7 @@ def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding for mapping in mappings: if mapping.error: logger.warning( - "Python-dotenv could not parse statement starting at line %s", + "python-dotenv could not parse statement starting at line %s", mapping.original.line, ) yield mapping @@ -60,10 +59,10 @@ def _get_stream(self) -> Iterator[IO[str]]: else: if self.verbose: logger.info( - "Python-dotenv could not find configuration file %s.", - self.dotenv_path or '.env', + "python-dotenv could not find configuration file %s.", + self.dotenv_path or ".env", ) - yield io.StringIO('') + yield io.StringIO("") def dict(self) -> Dict[str, Optional[str]]: """Return dotenv as dict""" @@ -73,7 +72,9 @@ def dict(self) -> Dict[str, Optional[str]]: raw_values = self.parse() if self.interpolate: - self._dict = OrderedDict(resolve_variables(raw_values, override=self.override)) + self._dict = OrderedDict( + resolve_variables(raw_values, override=self.override) + ) else: self._dict = OrderedDict(raw_values) @@ -101,8 +102,7 @@ def set_as_environment_variables(self) -> bool: return True def get(self, key: str) -> Optional[str]: - """ - """ + """ """ data = self.dict() if key in data: @@ -166,9 +166,8 @@ def set_key( if quote_mode not in ("always", "auto", "never"): raise ValueError(f"Unknown quote_mode: {quote_mode}") - quote = ( - quote_mode == "always" - or (quote_mode == "auto" and not value_to_set.isalnum()) + quote = quote_mode == "always" or ( + quote_mode == "auto" and not value_to_set.isalnum() ) if quote: @@ -176,7 +175,7 @@ def set_key( else: value_out = value_to_set if export: - line_out = f'export {key_to_set}={value_out}\n' + line_out = f"export {key_to_set}={value_out}\n" else: line_out = f"{key_to_set}={value_out}\n" @@ -223,7 +222,9 @@ def unset_key( dest.write(mapping.original.string) if not removed: - logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path) + logger.warning( + "Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path + ) return None, key_to_unset return removed, key_to_unset @@ -235,7 +236,7 @@ def resolve_variables( ) -> Mapping[str, Optional[str]]: new_values: Dict[str, Optional[str]] = {} - for (name, value) in values: + for name, value in values: if value is None: result = None else: @@ -259,7 +260,7 @@ def _walk_to_root(path: str) -> Iterator[str]: Yield directories starting from the given directory up to the root """ if not os.path.exists(path): - raise IOError('Starting path not found') + raise IOError("Starting path not found") if os.path.isfile(path): path = os.path.dirname(path) @@ -273,7 +274,7 @@ def _walk_to_root(path: str) -> Iterator[str]: def find_dotenv( - filename: str = '.env', + filename: str = ".env", raise_error_if_not_found: bool = False, usecwd: bool = False, ) -> str: @@ -284,14 +285,14 @@ def find_dotenv( """ def _is_interactive(): - """ Decide whether this is running in a REPL or IPython notebook """ + """Decide whether this is running in a REPL or IPython notebook""" try: - main = __import__('__main__', None, None, fromlist=['__file__']) + main = __import__("__main__", None, None, fromlist=["__file__"]) except ModuleNotFoundError: return False - return not hasattr(main, '__file__') + return not hasattr(main, "__file__") - if usecwd or _is_interactive() or getattr(sys, 'frozen', False): + if usecwd or _is_interactive() or getattr(sys, "frozen", False): # Should work without __file__, e.g. in REPL or IPython notebook. path = os.getcwd() else: @@ -313,9 +314,9 @@ def _is_interactive(): return check_path if raise_error_if_not_found: - raise IOError('File not found') + raise IOError("File not found") - return '' + return "" def load_dotenv( diff --git a/tests/test_main.py b/tests/test_main.py index fd5e3903..2d63eec1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -28,9 +28,9 @@ def test_set_key_no_file(tmp_path): ("", "a", "", (True, "a", ""), "a=''\n"), ("", "a", "b", (True, "a", "b"), "a='b'\n"), ("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"), - ("", "a", "\"b\"", (True, "a", '"b"'), "a='\"b\"'\n"), + ("", "a", '"b"', (True, "a", '"b"'), "a='\"b\"'\n"), ("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"), - ("", "a", "b\"c", (True, "a", "b\"c"), "a='b\"c'\n"), + ("", "a", 'b"c', (True, "a", 'b"c'), "a='b\"c'\n"), ("a=b", "a", "c", (True, "a", "c"), "a='c'\n"), ("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"), ("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"), @@ -75,20 +75,20 @@ def test_get_key_no_file(tmp_path): nx_path = tmp_path / "nx" logger = logging.getLogger("dotenv.main") - with mock.patch.object(logger, "info") as mock_info, \ - mock.patch.object(logger, "warning") as mock_warning: + with ( + mock.patch.object(logger, "info") as mock_info, + mock.patch.object(logger, "warning") as mock_warning, + ): result = dotenv.get_key(nx_path, "foo") assert result is None mock_info.assert_has_calls( calls=[ - mock.call("Python-dotenv could not find configuration file %s.", nx_path) + mock.call("python-dotenv could not find configuration file %s.", nx_path) ], ) mock_warning.assert_has_calls( - calls=[ - mock.call("Key %s not found in %s.", "foo", nx_path) - ], + calls=[mock.call("Key %s not found in %s.", "foo", nx_path)], ) @@ -249,10 +249,12 @@ def test_load_dotenv_no_file_verbose(): logger = logging.getLogger("dotenv.main") with mock.patch.object(logger, "info") as mock_info: - result = dotenv.load_dotenv('.does_not_exist', verbose=True) + result = dotenv.load_dotenv(".does_not_exist", verbose=True) assert result is False - mock_info.assert_called_once_with("Python-dotenv could not find configuration file %s.", ".does_not_exist") + mock_info.assert_called_once_with( + "python-dotenv could not find configuration file %s.", ".does_not_exist" + ) @mock.patch.dict(os.environ, {"a": "c"}, clear=True) @@ -317,21 +319,23 @@ def test_load_dotenv_file_stream(dotenv_path): def test_load_dotenv_in_current_dir(tmp_path): - dotenv_path = tmp_path / '.env' - dotenv_path.write_bytes(b'a=b') - code_path = tmp_path / 'code.py' - code_path.write_text(textwrap.dedent(""" + dotenv_path = tmp_path / ".env" + dotenv_path.write_bytes(b"a=b") + code_path = tmp_path / "code.py" + code_path.write_text( + textwrap.dedent(""" import dotenv import os dotenv.load_dotenv(verbose=True) print(os.environ['a']) - """)) + """) + ) os.chdir(tmp_path) result = sh.Command(sys.executable)(code_path) - assert result == 'b\n' + assert result == "b\n" def test_dotenv_values_file(dotenv_path): @@ -352,30 +356,23 @@ def test_dotenv_values_file(dotenv_path): ({"b": "c"}, "a=${b}", True, {"a": "c"}), ({"b": "c"}, "a=${b:-d}", False, {"a": "${b:-d}"}), ({"b": "c"}, "a=${b:-d}", True, {"a": "c"}), - # Defined in file ({}, "b=c\na=${b}", True, {"a": "c", "b": "c"}), - # Undefined ({}, "a=${b}", True, {"a": ""}), ({}, "a=${b:-d}", True, {"a": "d"}), - # With quotes ({"b": "c"}, 'a="${b}"', True, {"a": "c"}), ({"b": "c"}, "a='${b}'", True, {"a": "c"}), - # With surrounding text ({"b": "c"}, "a=x${b}y", True, {"a": "xcy"}), - # Self-referential ({"a": "b"}, "a=${a}", True, {"a": "b"}), ({}, "a=${a}", True, {"a": ""}), ({"a": "b"}, "a=${a:-c}", True, {"a": "b"}), ({}, "a=${a:-c}", True, {"a": "c"}), - # Reused ({"b": "c"}, "a=${b}${b}", True, {"a": "cc"}), - # Re-defined and used in file ({"b": "c"}, "b=d\na=${b}", True, {"a": "d", "b": "d"}), ({}, "a=b\na=c\nd=${a}", True, {"a": "c", "d": "c"}), From 8dd413e84b1fb1b3368c02106aab07a533fae015 Mon Sep 17 00:00:00 2001 From: randomseed42 <50793718+randomseed42@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:33:52 +0800 Subject: [PATCH 73/78] Add _is_debugger so load_dotenv will work in pdb (#553) --- src/dotenv/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 0c81bba5..1848d602 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -292,7 +292,10 @@ def _is_interactive(): return False return not hasattr(main, "__file__") - if usecwd or _is_interactive() or getattr(sys, "frozen", False): + def _is_debugger(): + return sys.gettrace() is not None + + if usecwd or _is_interactive() or _is_debugger() or getattr(sys, "frozen", False): # Should work without __file__, e.g. in REPL or IPython notebook. path = os.getcwd() else: From c89fb6d41c0a25f670b34ba05f392260eaa6ccd1 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 25 Mar 2025 15:38:56 +0530 Subject: [PATCH 74/78] Update changelog --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a198b1f7..3544da86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unrelased] -- Drop support for Python 3.8 +**Feature** - Add support for python 3.13 - Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt] +**Fixed** +- `find_dotenv` and `load_dotenv` now correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42]) + +**Misc** +- Drop support for Python 3.8 + ## [1.0.1] - 2024-01-23 **Fixed** @@ -354,6 +360,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [#454]: https://github.com/theskumar/python-dotenv/issues/454 [#474]: https://github.com/theskumar/python-dotenv/issues/474 [#523]: https://github.com/theskumar/python-dotenv/issues/523 +[#553]: https://github.com/theskumar/python-dotenv/issues/553 [@alanjds]: https://github.com/alanjds [@altendky]: https://github.com/altendky @@ -398,7 +405,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@x-yuri]: https://github.com/x-yuri [@yannham]: https://github.com/yannham [@zueve]: https://github.com/zueve - +[@randomseed42]: https://github.com/zueve [Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...HEAD [1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1 From 2198b698c021851201261fac27884ee8db6553d5 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 25 Mar 2025 15:41:24 +0530 Subject: [PATCH 75/78] =?UTF-8?q?Bump=20version:=201.0.1=20=E2=86=92=201.1?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 6 +++--- src/dotenv/version.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4a8f11ac..02dc0695 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.1 +current_version = 1.1.0 commit = True tag = True @@ -24,7 +24,7 @@ relative_files = True source = dotenv [coverage:paths] -source = +source = src/dotenv .tox/*/lib/python*/site-packages/dotenv .tox/pypy*/site-packages/dotenv @@ -32,6 +32,6 @@ source = [coverage:report] show_missing = True include = */site-packages/dotenv/* -exclude_lines = +exclude_lines = if IS_TYPE_CHECKING: pragma: no cover diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 5c4105cd..6849410a 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "1.0.1" +__version__ = "1.1.0" From 36c6270db41e1e88be4ec21d0fb876ba0c79d363 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 25 Mar 2025 15:42:28 +0530 Subject: [PATCH 76/78] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3544da86..ec525352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unrelased] +## [1.1.0] - 2025-03-25 **Feature** - Add support for python 3.13 From 6a02ef5a1034d66338811757df07a113a1169af6 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 25 Mar 2025 16:23:46 +0530 Subject: [PATCH 77/78] update mkdocs -> mkdocstrings config --- mkdocs.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 331965df..ba77fa7f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,13 +13,7 @@ markdown_extensions: - mdx_truly_sane_lists plugins: - - mkdocstrings: - handlers: - python: - rendering: - show_root_heading: yes - show_submodules: no - separate_signature: yes + - mkdocstrings - search nav: - Home: index.md From 01f899733de664cda0550207067eb36a1795062f Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Mon, 31 Mar 2025 13:29:19 +0530 Subject: [PATCH 78/78] docs update --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec525352..f1afd06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,16 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.1.0] - 2025-03-25 **Feature** + - Add support for python 3.13 - Enhance `dotenv run`, switch to `execvpe` for better resource management and signal handling ([#523]) by [@eekstunt] **Fixed** + - `find_dotenv` and `load_dotenv` now correctly looks up at the current directory when running in debugger or pdb ([#553] by [@randomseed42]) **Misc** + - Drop support for Python 3.8 ## [1.0.1] - 2024-01-23 @@ -407,7 +410,8 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [@zueve]: https://github.com/zueve [@randomseed42]: https://github.com/zueve -[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...HEAD +[Unreleased]: https://github.com/theskumar/python-dotenv/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/theskumar/python-dotenv/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/theskumar/python-dotenv/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v1.0.0 [0.21.1]: https://github.com/theskumar/python-dotenv/compare/v0.21.0...v0.21.1