diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0779431c..342d5ce8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,17 +24,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.2.2 - - name: Set up Python 3.10 - uses: actions/setup-python@v4.7.0 + - name: Set up Python 3.12 + uses: actions/setup-python@v5.3.0 with: - python-version: '3.10' + python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions setuptools - name: Check MANIFEST.in for completeness run: tox -e manifest @@ -44,7 +44,7 @@ jobs: - name: Archive build artifacts if: success() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: # To ensure that jobs don't overwrite existing artifacts, # use a different name per job. @@ -64,12 +64,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.2.2 - - name: Set up Python 3.10 - uses: actions/setup-python@v4.7.0 + - name: Set up Python 3.12 + uses: actions/setup-python@v5.3.0 with: - python-version: '3.10' + python-version: '3.12' - name: Install in dev mode run: python -m pip install -e . diff --git a/.github/workflows/change-pr-target.yml b/.github/workflows/change-pr-target.yml index eb12a30c..af419d11 100644 --- a/.github/workflows/change-pr-target.yml +++ b/.github/workflows/change-pr-target.yml @@ -8,7 +8,7 @@ jobs: check-branch: runs-on: ubuntu-latest steps: - - uses: Vankka/pr-target-branch-action@v2 + - uses: Vankka/pr-target-branch-action@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ecdb2fe..0d80bfe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,38 +47,29 @@ jobs: matrix: python: - - '3.8' - '3.9' - '3.10' - '3.11' - - 'pypy-3.7' + - '3.12' + - '3.13' + - 'pypy-3.10' os: [ ubuntu-latest, macos-latest, windows-latest ] - # These versions are no longer supported by Python team, and may - # eventually be dropped from GitHub Actions. The support of these - # versions by django-environ will continue for as long as possible, - # and may be discontinued at any time. - include: - - python: '3.6' - os: ubuntu-20.04 - - python: '3.7' - os: ubuntu-20.04 - steps: - name: Checkout code - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.2.2 with: fetch-depth: 5 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4.7.0 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox tox-gh-actions + python -m pip install tox tox-gh-actions setuptools - name: Setuptools self-test run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 07dd2008..d8236f32 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,16 +45,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.2.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml index fc596beb..30d41b99 100644 --- a/.github/workflows/cs.yml +++ b/.github/workflows/cs.yml @@ -25,17 +25,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.2.2 - - name: Set up Python 3.10 - uses: actions/setup-python@v4.7.0 + - name: Set up Python 3.12 + uses: actions/setup-python@v5.3.0 with: - python-version: '3.10' + python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions setuptools - name: Lint with tox run: tox -e lint diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e03e0d8b..0ae19c36 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,17 +26,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3.6.0 + uses: actions/checkout@v4.2.2 - - name: Set up Python 3.10 - uses: actions/setup-python@v4.7.0 + - name: Set up Python 3.12 + uses: actions/setup-python@v5.3.0 with: - python-version: '3.10' + python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions setuptools - name: Check external links in the package documentation run: tox -e linkcheck @@ -46,7 +46,7 @@ jobs: - name: Archive docs artifacts if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docs path: docs diff --git a/.gitignore b/.gitignore index 58629014..ec6473a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/.readthedocs.yml b/.readthedocs.yml index 27f93688..191e9990 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -17,7 +17,7 @@ build: tools: # Keep version in sync with tox.ini (testenv:docs) and # docs.yml (GitHub Action Workflow). - python: '3.10' + python: '3.12' python: install: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 57c4c8dd..9ccc1824 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,8 +5,41 @@ All notable changes to this project will be documented in this file. The format is inspired by `Keep a Changelog `_ and this project adheres to `Semantic Versioning `_. +`v0.12.0`_ - 8-November-2024 +----------------------------- +Fixed ++++++ +- Include prefix in the ``ImproperlyConfigured`` error message + `#513 `_. + +Added ++++++ +- Add support for Python 3.12 and 3.13 + `#538 `_. +- Add support for Django 5.1 + `#535 `_. +- Add support for Django CockroachDB driver + `#509 `_. +- Add support for Django Channels + `#266 `_. + +Changed ++++++++ +- Disabled inline comments handling by default due to potential side effects. + While the feature itself is useful, the project's philosophy dictates that + it should not be enabled by default for all users + `#499 `_. + +Removed ++++++++ +- Removed support of Python 3.6, 3.7 and 3.8 + `#538 `_. +- Removed support of Django 1.x. + `#538 `_. + + `v0.11.2`_ - 1-September-2023 -------------------------------- +----------------------------- Fixed +++++ - Revert "Add variable expansion." feature @@ -31,7 +64,7 @@ Added `#463 `_. - Added variable expansion `#468 `_. -- Added capability to handle comments after #, after quoted values, +- Added capability to handle comments after ``#``, after quoted values, like ``KEY= 'part1 # part2' # comment`` `#475 `_. - Added support for ``interpolate`` parameter @@ -388,6 +421,7 @@ Added - Initial release. +.. _v0.12.0: https://github.com/joke2k/django-environ/compare/v0.11.2...v0.12.0 .. _v0.11.2: https://github.com/joke2k/django-environ/compare/v0.11.1...v0.11.2 .. _v0.11.1: https://github.com/joke2k/django-environ/compare/v0.11.0...v0.11.1 .. _v0.11.0: https://github.com/joke2k/django-environ/compare/v0.10.0...v0.11.0 @@ -405,4 +439,4 @@ Added .. _v0.4.1: https://github.com/joke2k/django-environ/compare/v0.4...v0.4.1 .. _v0.4: https://github.com/joke2k/django-environ/compare/v0.3.1...v0.4 .. _v0.3.1: https://github.com/joke2k/django-environ/compare/v0.3...v0.3.1 -.. _v0.3: https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3 \ No newline at end of file +.. _v0.3: https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3 diff --git a/LICENSE.txt b/LICENSE.txt index 0bd208d6..97cfd65e 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2021, Serghei Iakovlev +Copyright (c) 2021-2024, Serghei Iakovlev Copyright (c) 2013-2021, Daniele Faraglia Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/MANIFEST.in b/MANIFEST.in index 9c2f064c..694e938d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/README.rst b/README.rst index 528d1df1..ff8e0125 100644 --- a/README.rst +++ b/README.rst @@ -97,7 +97,8 @@ approach, some connection strings are expressed as url, so this package can pars it and return a ``urllib.parse.ParseResult``. These strings from ``os.environ`` are loaded from a ``.env`` file and filled in ``os.environ`` with ``setdefault`` method, to avoid to overwrite the real environ. -A similar approach is used in `Two Scoops of Django `_ +A similar approach is used in +`Two Scoops of Django `_ book and explained in `12factor-django `_ article. @@ -126,8 +127,8 @@ its documentation lives at `Read the Docs `_, and the latest release on `PyPI `_. -It’s rigorously tested on Python 3.6+, and officially supports -Django 1.11, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1 and 4.2. +It’s rigorously tested on Python 3.9+, and officially supports +Django 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0, and 5.1. If you'd like to contribute to ``django-environ`` you're most welcome! diff --git a/SECURITY.rst b/SECURITY.rst index 16ffdceb..f3ea149f 100644 --- a/SECURITY.rst +++ b/SECURITY.rst @@ -6,5 +6,5 @@ Reporting a Vulnerability ------------------------- If you discover a security vulnerability within ``django-environ``, please -send an e-mail to Serghei Iakovlev via egrep@protonmail.ch. All security +send an e-mail to Serghei Iakovlev via oss@serghei.pl. All security vulnerabilities will be promptly addressed. diff --git a/docs/Makefile b/docs/Makefile index 887c0bba..343425b6 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/docs/conf.py b/docs/conf.py index 8beac1f4..0e91d43c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2023, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/docs/docutils.conf b/docs/docutils.conf index 4ed0bf5a..d624b452 100644 --- a/docs/docutils.conf +++ b/docs/docutils.conf @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/docs/install.rst b/docs/install.rst index 3dacca9e..b1a73c18 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -6,8 +6,8 @@ Installation Requirements ============ -* `Django `_ >= 1.11 -* `Python `_ >= 3.5 +* `Django `_ >= 2.2 +* `Python `_ >= 3.9 Installing django-environ ========================= diff --git a/docs/tips.rst b/docs/tips.rst index 20b8c3e1..66538c40 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -2,6 +2,71 @@ Tips ==== +Handling Inline Comments in .env Files +====================================== + +``django-environ`` provides an optional feature to parse inline comments in ``.env`` +files. This is controlled by the ``parse_comments`` parameter in the ``read_env`` +method. + +Modes +----- + +- **Enabled (``parse_comments=True``)**: Inline comments starting with ``#`` will be ignored. +- **Disabled (``parse_comments=False``)**: The entire line, including comments, will be read as the value. +- **Default**: The behavior is the same as when ``parse_comments=False``. + +Side Effects +------------ + +While this feature can be useful for adding context to your ``.env`` files, +it can introduce unexpected behavior. For example, if your value includes +a ``#`` symbol, it will be truncated when ``parse_comments=True``. + +Why Disabled by Default? +------------------------ + +In line with the project's philosophy of being explicit and avoiding unexpected behavior, +this feature is disabled by default. If you understand the implications and find the feature +useful, you can enable it explicitly. + +Example +------- + +Here is an example demonstrating the different modes of handling inline comments. + +**.env file contents**: + +.. code-block:: shell + + # .env file contents + BOOL_TRUE_WITH_COMMENT=True # This is a comment + STR_WITH_HASH=foo#bar # This is also a comment + +**Python code**: + +.. code-block:: python + + import environ + + # Using parse_comments=True + env = environ.Env() + env.read_env(parse_comments=True) + print(env('BOOL_TRUE_WITH_COMMENT')) # Output: True + print(env('STR_WITH_HASH')) # Output: foo + + # Using parse_comments=False + env = environ.Env() + env.read_env(parse_comments=False) + print(env('BOOL_TRUE_WITH_COMMENT')) # Output: True # This is a comment + print(env('STR_WITH_HASH')) # Output: foo#bar # This is also a comment + + # Using default behavior + env = environ.Env() + env.read_env() + print(env('BOOL_TRUE_WITH_COMMENT')) # Output: True # This is a comment + print(env('STR_WITH_HASH')) # Output: foo#bar # This is also a comment + Docker-style file based variables ================================= diff --git a/environ/__init__.py b/environ/__init__.py index 28d4a5aa..8d29d2f1 100644 --- a/environ/__init__.py +++ b/environ/__init__.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2023, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -21,7 +21,7 @@ __copyright__ = 'Copyright (C) 2013-2023 Daniele Faraglia' """The copyright notice of the package.""" -__version__ = '0.11.2' +__version__ = '0.12.0' """The version of the package.""" __license__ = 'MIT' @@ -36,7 +36,7 @@ __maintainer__ = 'Serghei Iakovlev' """The maintainer of the package.""" -__maintainer_email__ = 'egrep@protonmail.ch' +__maintainer_email__ = 'oss@serghei.pl' """The email of the maintainer of the package.""" __url__ = 'https://django-environ.readthedocs.org' diff --git a/environ/compat.py b/environ/compat.py index 49b5b480..55953fe5 100644 --- a/environ/compat.py +++ b/environ/compat.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/environ/environ.py b/environ/environ.py index f74884be..5536f2c7 100644 --- a/environ/environ.py +++ b/environ/environ.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -115,6 +115,7 @@ class Env: 'psql': DJANGO_POSTGRES, 'pgsql': DJANGO_POSTGRES, 'postgis': 'django.contrib.gis.db.backends.postgis', + 'cockroachdb': 'django_cockroachdb', 'mysql': 'django.db.backends.mysql', 'mysql2': 'django.db.backends.mysql', 'mysql-connector': 'mysql.connector.django', @@ -189,6 +190,13 @@ class Env: for s in ('', 's')] CLOUDSQL = 'cloudsql' + DEFAULT_CHANNELS_ENV = "CHANNELS_URL" + CHANNELS_SCHEMES = { + "inmemory": "channels.layers.InMemoryChannelLayer", + "redis": "channels_redis.core.RedisChannelLayer", + "redis+pubsub": "channels_redis.pubsub.RedisPubSubChannelLayer" + } + def __init__(self, **scheme): self.smart_cast = True self.escape_proxy = False @@ -337,6 +345,19 @@ def search_url(self, var=DEFAULT_SEARCH_ENV, default=NOTSET, engine=None): engine=engine ) + def channels_url(self, var=DEFAULT_CHANNELS_ENV, default=NOTSET, + backend=None): + """Returns a config dictionary, defaulting to CHANNELS_URL. + + :rtype: dict + """ + return self.channels_url_config( + self.url(var, default=default), + backend=backend + ) + + channels = channels_url + def path(self, var, default=NOTSET, **kwargs): """ :rtype: Path @@ -388,7 +409,7 @@ def get_value(self, var, cast=None, default=NOTSET, parse_default=False): value = self.ENVIRON[var_name] except KeyError as exc: if default is self.NOTSET: - error_msg = f'Set the {var} environment variable' + error_msg = f'Set the {var_name} environment variable' raise ImproperlyConfigured(error_msg) from exc value = default @@ -736,6 +757,33 @@ def email_url_config(cls, url, backend=None): return config + @classmethod + def channels_url_config(cls, url, backend=None): + """Parse an arbitrary channels URL. + + :param urllib.parse.ParseResult or str url: + Email URL to parse. + :param str or None backend: + If None, the backend is evaluates from the ``url``. + :return: Parsed channels URL. + :rtype: dict + """ + config = {} + url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url + + if backend: + config["BACKEND"] = backend + elif url.scheme not in cls.CHANNELS_SCHEMES: + raise ImproperlyConfigured(f"Invalid channels schema {url.scheme}") + else: + config["BACKEND"] = cls.CHANNELS_SCHEMES[url.scheme] + if url.scheme in ("redis", "redis+pubsub"): + config["CONFIG"] = { + "hosts": [url._replace(scheme="redis").geturl()] + } + + return config + @classmethod def _parse_common_search_params(cls, url): cfg = {} @@ -809,7 +857,7 @@ def search_url_config(cls, url, engine=None): :param urllib.parse.ParseResult or str url: Search URL to parse. :param str or None engine: - If None, the engine is evaluates from the ``url``. + If None, the engine is evaluating from the ``url``. :return: Parsed search URL. :rtype: dict """ @@ -862,8 +910,8 @@ def search_url_config(cls, url, engine=None): return config @classmethod - def read_env(cls, env_file=None, overwrite=False, encoding='utf8', - **overrides): + def read_env(cls, env_file=None, overwrite=False, parse_comments=False, + encoding='utf8', **overrides): r"""Read a .env file into os.environ. If not given a path to a dotenv path, does filthy magic stack @@ -883,6 +931,8 @@ def read_env(cls, env_file=None, overwrite=False, encoding='utf8', the Django settings module from the Django project root. :param overwrite: ``overwrite=True`` will force an overwrite of existing environment variables. + :param parse_comments: Determines whether to recognize and ignore + inline comments in the .env file. Default is False. :param encoding: The encoding to use when reading the environment file. :param \**overrides: Any additional keyword arguments provided directly to read_env will be added to the environment. If the key matches an @@ -927,22 +977,40 @@ def _keep_escaped_format_characters(match): for line in content.splitlines(): m1 = re.match(r'\A(?:export )?([A-Za-z_0-9]+)=(.*)\Z', line) if m1: + + # Example: + # + # line: KEY_499=abc#def + # key: KEY_499 + # val: abc#def key, val = m1.group(1), m1.group(2) - # Look for value in quotes, ignore post-# comments - # (outside quotes) - m2 = re.match(r"\A\s*'(? +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/setup.py b/setup.py index 30525752..e9272177 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # # This file is part of the django-environ. # -# Copyright (c) 2021-2023, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -135,9 +135,6 @@ def get_version_string(): 'Development Status :: 5 - Production/Stable', 'Framework :: Django', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', @@ -145,6 +142,8 @@ def get_version_string(): 'Framework :: Django :: 4.0', 'Framework :: Django :: 4.1', 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.0', + 'Framework :: Django :: 5.1', 'Operating System :: OS Independent', @@ -153,12 +152,11 @@ def get_version_string(): 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - '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 :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', @@ -183,11 +181,12 @@ def get_version_string(): 'testing': [ 'coverage[toml]>=5.0a4', # Code coverage measurement for Python 'pytest>=4.6.11', # Our tests framework + 'setuptools>=71.0.0', # Needed as a dependency for some tests ], # Dependencies that are required to build documentation 'docs': [ - 'furo>=2021.8.17b43,==2021.8.*', # Sphinx documentation theme - 'sphinx>=3.5.0', # Python documentation generator + 'furo>=2024.8.6', # Sphinx documentation theme + 'sphinx>=5.0', # Python documentation generator 'sphinx-notfound-page', # Create a custom 404 page ], } @@ -230,7 +229,7 @@ def get_version_string(): platforms=['any'], include_package_data=True, zip_safe=False, - python_requires='>=3.6,<4', + python_requires='>=3.9,<4', install_requires=INSTALL_REQUIRES, dependency_links=DEPENDENCY_LINKS, extras_require=EXTRAS_REQUIRE, diff --git a/tests/__init__.py b/tests/__init__.py index 19f3cf24..c5fd8236 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/asserts.py b/tests/asserts.py index 3389e526..00d94b28 100644 --- a/tests/asserts.py +++ b/tests/asserts.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/conftest.py b/tests/conftest.py index 38550f73..ffdbf292 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/fixtures.py b/tests/fixtures.py index 69e5e90f..dc9fbd42 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/test_cache.py b/tests/test_cache.py index a8aff161..58e57e0b 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/test_channels.py b/tests/test_channels.py new file mode 100644 index 00000000..64485a6f --- /dev/null +++ b/tests/test_channels.py @@ -0,0 +1,25 @@ +# This file is part of the django-environ. +# +# Copyright (c) 2021-2024, Serghei Iakovlev +# Copyright (c) 2013-2021, Daniele Faraglia +# +# For the full copyright and license information, please view +# the LICENSE.txt file that was distributed with this source code. + +from environ import Env + + +def test_channels_parsing(): + url = "inmemory://" + result = Env.channels_url_config(url) + assert result["BACKEND"] == "channels.layers.InMemoryChannelLayer" + + url = "redis://user:password@localhost:6379/0" + result = Env.channels_url_config(url) + assert result["BACKEND"] == "channels_redis.core.RedisChannelLayer" + assert result["CONFIG"]["hosts"][0] == "redis://user:password@localhost:6379/0" + + url = "redis+pubsub://user:password@localhost:6379/0" + result = Env.channels_url_config(url) + assert result["BACKEND"] == "channels_redis.pubsub.RedisPubSubChannelLayer" + assert result["CONFIG"]["hosts"][0] == "redis://user:password@localhost:6379/0" diff --git a/tests/test_db.py b/tests/test_db.py index 8101a4a7..e26c5357 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -59,6 +59,14 @@ '', '' ), + # cockroachdb://username:secret@test.example.com:26258/dbname + ('cockroachdb://username:secret@test.example.com:26258/dbname', + 'django_cockroachdb', + 'dbname', + 'test.example.com', + 'username', + 'secret', + 26258), # mysqlgis://user:password@host:port/dbname ('mysqlgis://enigma:secret@example.com:5431/dbname', 'django.contrib.gis.db.backends.mysql', @@ -156,6 +164,7 @@ 'postgis', 'postgres_cluster', 'postgres_no_ports', + 'cockroachdb', 'mysqlgis', 'cleardb', 'mysql_no_password', @@ -188,6 +197,16 @@ def test_db_parsing(url, engine, name, host, user, passwd, port): assert config['OPTIONS'] == {'reconnect': 'true'} +def test_custom_db_engine(): + """Override ENGINE determined from schema.""" + env_url = 'postgres://enigma:secret@example.com:5431/dbname' + + engine = 'mypackage.backends.whatever' + url = Env.db_url_config(env_url, engine=engine) + + assert url['ENGINE'] == engine + + def test_postgres_complex_db_name_parsing(): """Make sure we can use complex postgres host.""" env_url = ( diff --git a/tests/test_email.py b/tests/test_email.py index 30d38139..1c9a8754 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -22,3 +22,13 @@ def test_smtp_parsing(): assert url['EMAIL_PORT'] == 587 assert url['EMAIL_USE_TLS'] is True assert url['EMAIL_FILE_PATH'] == '' + + +def test_custom_email_backend(): + """Override EMAIL_BACKEND determined from schema.""" + url = 'smtps://user@domain.com:password@smtp.example.com:587' + + backend = 'mypackage.backends.whatever' + url = Env.email_url_config(url, backend=backend) + + assert url['EMAIL_BACKEND'] == backend diff --git a/tests/test_env.py b/tests/test_env.py index 0b105f55..5f0f4c9b 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -1,12 +1,13 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view # the LICENSE.txt file that was distributed with this source code. import os +import tempfile from urllib.parse import quote import pytest @@ -21,6 +22,59 @@ from .fixtures import FakeEnv +@pytest.mark.parametrize( + 'variable,value,raw_value,parse_comments', + [ + # parse_comments=True + ('BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT', 'True', "'True' # comment\n", True), + ('BOOL_TRUE_BOOL_WITH_COMMENT', 'True ', "True # comment\n", True), + ('STR_QUOTED_IGNORE_COMMENT', 'foo', " 'foo' # comment\n", True), + ('STR_QUOTED_INCLUDE_HASH', 'foo # with hash', "'foo # with hash' # not comment\n", True), + ('SECRET_KEY_1', '"abc', '"abc#def"\n', True), + ('SECRET_KEY_2', 'abc', 'abc#def\n', True), + ('SECRET_KEY_3', 'abc#def', "'abc#def'\n", True), + + # parse_comments=False + ('BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT', "'True' # comment", "'True' # comment\n", False), + ('BOOL_TRUE_BOOL_WITH_COMMENT', 'True # comment', "True # comment\n", False), + ('STR_QUOTED_IGNORE_COMMENT', " 'foo' # comment", " 'foo' # comment\n", False), + ('STR_QUOTED_INCLUDE_HASH', "'foo # with hash' # not comment", "'foo # with hash' # not comment\n", False), + ('SECRET_KEY_1', 'abc#def', '"abc#def"\n', False), + ('SECRET_KEY_2', 'abc#def', 'abc#def\n', False), + ('SECRET_KEY_3', 'abc#def', "'abc#def'\n", False), + + # parse_comments is not defined (default behavior) + ('BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT', "'True' # comment", "'True' # comment\n", None), + ('BOOL_TRUE_BOOL_WITH_COMMENT', 'True # comment', "True # comment\n", None), + ('STR_QUOTED_IGNORE_COMMENT', " 'foo' # comment", " 'foo' # comment\n", None), + ('STR_QUOTED_INCLUDE_HASH', "'foo # with hash' # not comment", "'foo # with hash' # not comment\n", None), + ('SECRET_KEY_1', 'abc#def', '"abc#def"\n', None), + ('SECRET_KEY_2', 'abc#def', 'abc#def\n', None), + ('SECRET_KEY_3', 'abc#def', "'abc#def'\n", None), + ], + ) +def test_parse_comments(variable, value, raw_value, parse_comments): + old_environ = os.environ + + with tempfile.TemporaryDirectory() as temp_dir: + env_path = os.path.join(temp_dir, '.env') + + with open(env_path, 'w') as f: + f.write(f'{variable}={raw_value}\n') + f.flush() + + env = Env() + Env.ENVIRON = {} + if parse_comments is None: + env.read_env(env_path) + else: + env.read_env(env_path, parse_comments=parse_comments) + + assert env(variable) == value + + os.environ = old_environ + + class TestEnv: def setup_method(self, method): """ @@ -112,10 +166,8 @@ def test_float(self, value, variable): [ (True, 'BOOL_TRUE_STRING_LIKE_INT'), (True, 'BOOL_TRUE_STRING_LIKE_BOOL'), - (True, 'BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT'), (True, 'BOOL_TRUE_INT'), (True, 'BOOL_TRUE_BOOL'), - (True, 'BOOL_TRUE_BOOL_WITH_COMMENT'), (True, 'BOOL_TRUE_STRING_1'), (True, 'BOOL_TRUE_STRING_2'), (True, 'BOOL_TRUE_STRING_3'), @@ -341,8 +393,6 @@ def test_path(self): def test_smart_cast(self): assert self.env.get_value('STR_VAR', default='string') == 'bar' - assert self.env.get_value('STR_QUOTED_IGNORE_COMMENT', default='string') == 'foo' - assert self.env.get_value('STR_QUOTED_INCLUDE_HASH', default='string') == 'foo # with hash' assert self.env.get_value('BOOL_TRUE_STRING_LIKE_INT', default=True) assert not self.env.get_value( 'BOOL_FALSE_STRING_LIKE_INT', @@ -357,6 +407,13 @@ def test_prefix(self): self.env.prefix = 'PREFIX_' assert self.env('TEST') == 'foo' + def test_prefix_and_not_present_without_default(self): + self.env.prefix = 'PREFIX_' + with pytest.raises(ImproperlyConfigured) as excinfo: + self.env('not_present') + assert str(excinfo.value) == 'Set the PREFIX_not_present environment variable' + assert excinfo.value.__cause__ is not None + class TestFileEnv(TestEnv): def setup_method(self, method): diff --git a/tests/test_env.txt b/tests/test_env.txt index d5480bf6..39ab896a 100644 --- a/tests/test_env.txt +++ b/tests/test_env.txt @@ -25,8 +25,6 @@ BOOL_TRUE_STRING_3='yes' BOOL_TRUE_STRING_4='y' BOOL_TRUE_STRING_5='true' BOOL_TRUE_BOOL=True -BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT='True' # comment -BOOL_TRUE_BOOL_WITH_COMMENT=True # comment BOOL_FALSE_STRING_LIKE_INT='0' BOOL_FALSE_INT=0 BOOL_FALSE_STRING_LIKE_BOOL='False' @@ -47,8 +45,6 @@ INT_VAR=42 STR_LIST_WITH_SPACES= foo, spaces STR_LIST_WITH_SPACES_QUOTED=' foo',' quoted' STR_VAR=bar -STR_QUOTED_IGNORE_COMMENT= 'foo' # comment -STR_QUOTED_INCLUDE_HASH='foo # with hash' # not comment MULTILINE_STR_VAR=foo\nbar MULTILINE_QUOTED_STR_VAR="---BEGIN---\r\n---END---" MULTILINE_ESCAPED_STR_VAR=---BEGIN---\\n---END--- diff --git a/tests/test_fileaware.py b/tests/test_fileaware.py index f411b7de..00442ae6 100644 --- a/tests/test_fileaware.py +++ b/tests/test_fileaware.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/test_path.py b/tests/test_path.py index b97f4caf..5d868d4d 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/test_schema.py b/tests/test_schema.py index 7a7f62ec..45f0e983 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tests/test_search.py b/tests/test_search.py index a6d8f061..38f11f73 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -86,6 +86,16 @@ def test_elasticsearch_parsing(url, engine, scheme): assert url["URL"].startswith(scheme + ":") +def test_custom_search_engine(): + """Override ENGINE determined from schema.""" + env_url = 'elasticsearch://127.0.0.1:9200/index' + + engine = 'mypackage.backends.whatever' + url = Env.db_url_config(env_url, engine=engine) + + assert url['ENGINE'] == engine + + @pytest.mark.parametrize('storage', ['file', 'ram']) def test_whoosh_parsing(whoosh_url, storage): post_limit = 128 * 1024 * 1024 diff --git a/tests/test_utils.py b/tests/test_utils.py index 2f3d32ee..02007179 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2022, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view diff --git a/tox.ini b/tox.ini index d2367374..a0b8dc28 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # This file is part of the django-environ. # -# Copyright (c) 2021-2023, Serghei Iakovlev +# Copyright (c) 2021-2024, Serghei Iakovlev # Copyright (c) 2013-2021, Daniele Faraglia # # For the full copyright and license information, please view @@ -18,26 +18,23 @@ envlist = docs lint manifest - py{36,37,38,39,310,311}-django{111,22} - py{36,37,38,39,310,311}-django{30,31,32} - py{38,39,310,311}-django{40,41,42} - pypy-django{111,22,30,31,32} + py{39,310,311,312,313}-django{22,30,31,32,40,41,42} + py{310,311,312,313}-django{50,51} + pypy-django{22,30,31,32} [gh-actions] python = - 3.6: py36 - 3.7: py37 - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 - pypy-3.7: pypy + 3.12: py312 + 3.13: py313 + pypy-3.10: pypy [testenv] description = Unit tests extras = testing deps = - django111: Django>=1.11,<2 django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 @@ -91,7 +88,7 @@ commands = description = Check external links in the package documentation # Keep basepython in sync with .readthedocs.yml and docs.yml # (GitHub Action Workflow). -basepython = python3.10 +basepython = python3.12 extras = docs commands = {envpython} -m sphinx \ @@ -109,7 +106,7 @@ isolated_build = true description = Build package documentation (HTML) # Keep basepython in sync with .readthedocs.yml and docs.yml # (GitHub Action Workflow). -basepython = python3.10 +basepython = python3.12 extras = docs commands = {envpython} -m sphinx \