diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..00d4d5e4 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,18 @@ +# 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`. 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 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49e1399f..fc86910d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,24 +7,28 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false max-parallel: 8 matrix: os: - ubuntu-latest - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", pypy3.9] + python-version: + ["3.9", "3.10", "3.11", "3.12", "3.13", 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 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true - - name: Install dependencies - run: - python -m pip install --upgrade pip - pip install tox tox-gh-actions + - name: Upgrade pip + run: python -m pip install --upgrade pip - - name: Test with tox - run: tox + - 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 220d1888..f1afd06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,36 @@ 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.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 + +**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** @@ -304,7 +333,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 @@ -328,6 +357,13 @@ 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 +[#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 @@ -338,24 +374,31 @@ 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 +[@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 @@ -365,9 +408,11 @@ 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.0...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 [0.21.0]: https://github.com/theskumar/python-dotenv/compare/v0.20.0...v0.21.0 diff --git a/README.md b/README.md index ddc8ba87..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,20 +29,20 @@ 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: ```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: @@ -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: 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 diff --git a/setup.cfg b/setup.cfg index 3fefd1f0..02dc0695 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0 +current_version = 1.1.0 commit = True tag = True diff --git a/setup.py b/setup.py index 8ceddf92..f3d43ca1 100644 --- a/setup.py +++ b/setup.py @@ -4,59 +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 :: 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/src/dotenv/cli.py b/src/dotenv/cli.py index 65ead461..33ae1485 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -3,8 +3,7 @@ import shlex import sys from contextlib import contextmanager -from subprocess import Popen -from typing import Any, Dict, IO, Iterator, List +from typing import Any, Dict, IO, Iterator, List, Optional try: import click @@ -17,7 +16,7 @@ from .version import __version__ -def enumerate_env(): +def enumerate_env() -> Optional[str]: """ Return a path for the ${pwd}/.env file. @@ -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) diff --git a/src/dotenv/main.py b/src/dotenv/main.py index 383b79f4..1848d602 100644 --- a/src/dotenv/main.py +++ b/src/dotenv/main.py @@ -1,13 +1,13 @@ import io import logging import os +import pathlib import shutil import sys 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 @@ -16,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__) @@ -25,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 @@ -59,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""" @@ -72,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) @@ -100,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: @@ -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( @@ -161,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: @@ -171,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" @@ -218,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 @@ -230,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: @@ -254,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) @@ -268,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: @@ -279,11 +285,17 @@ def find_dotenv( """ def _is_interactive(): - """ Decide whether this is running in a REPL or IPython notebook """ - main = __import__('__main__', None, None, fromlist=['__file__']) - return not hasattr(main, '__file__') + """Decide whether this is running in a REPL or IPython notebook""" + 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): + 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: @@ -305,9 +317,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( @@ -332,7 +344,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() diff --git a/src/dotenv/version.py b/src/dotenv/version.py index 5becc17c..6849410a 100644 --- a/src/dotenv/version.py +++ b/src/dotenv/version.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.1.0" 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"}), 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