Skip to content

Commit

Permalink
Make inline comments handling optional and disabled by default
Browse files Browse the repository at this point in the history
Disabled inline comments handling (that was introduced in #475)
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.

This also fix the issue described in the #499.
  • Loading branch information
sergeyklay committed Sep 22, 2023
1 parent 7b5d7f9 commit e3e7fc9
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .gitignore
@@ -1,6 +1,6 @@
# This file is part of the django-environ.
#
# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
# Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
# Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
#
# For the full copyright and license information, please view
Expand Down
16 changes: 14 additions & 2 deletions CHANGELOG.rst
Expand Up @@ -5,8 +5,19 @@ All notable changes to this project will be documented in this file.
The format is inspired by `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>`_.

`v0.11.3`_ - 0-Undefined-2023
-----------------------------
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 <https://github.com/joke2k/django-environ/issues/499>`_.



`v0.11.2`_ - 1-September-2023
-------------------------------
-----------------------------
Fixed
+++++
- Revert "Add variable expansion." feature
Expand All @@ -31,7 +42,7 @@ Added
`#463 <https://github.com/joke2k/django-environ/pull/463>`_.
- Added variable expansion
`#468 <https://github.com/joke2k/django-environ/pull/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 <https://github.com/joke2k/django-environ/pull/475>`_.
- Added support for ``interpolate`` parameter
Expand Down Expand Up @@ -388,6 +399,7 @@ Added
- Initial release.


.. _v0.11.3: https://github.com/joke2k/django-environ/compare/v0.11.2...v0.11.3
.. _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
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
@@ -1,4 +1,4 @@
Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
65 changes: 65 additions & 0 deletions docs/tips.rst
Expand Up @@ -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
=================================
Expand Down
2 changes: 1 addition & 1 deletion environ/__init__.py
Expand Up @@ -21,7 +21,7 @@
__copyright__ = 'Copyright (C) 2013-2023 Daniele Faraglia'
"""The copyright notice of the package."""

__version__ = '0.11.2'
__version__ = '0.11.3'
"""The version of the package."""

__license__ = 'MIT'
Expand Down
46 changes: 33 additions & 13 deletions environ/environ.py
@@ -1,6 +1,6 @@
# This file is part of the django-environ.
#
# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
# Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
# Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
#
# For the full copyright and license information, please view
Expand Down Expand Up @@ -862,8 +862,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
Expand All @@ -883,6 +883,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
Expand Down Expand Up @@ -927,22 +929,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*'(?<!\\)(.*)'\s*(#.*\s*)?\Z", val)
if m2:
val = m2.group(1)

if not parse_comments:
# Default behavior
#
# Look for value in single quotes
m2 = re.match(r"\A'(.*)'\Z", val)
if m2:
val = m2.group(1)
else:
# For no quotes, find value, ignore comments
# after the first #
m2a = re.match(r"\A(.*?)(#.*\s*)?\Z", val)
if m2a:
val = m2a.group(1)
# Ignore post-# comments (outside quotes).
# Something like ['val' # comment] becomes ['val'].
m2 = re.match(r"\A\s*'(?<!\\)(.*)'\s*(#.*\s*)?\Z", val)
if m2:
val = m2.group(1)
else:
# For no quotes, find value, ignore comments
# after the first #
m2a = re.match(r"\A(.*?)(#.*\s*)?\Z", val)
if m2a:
val = m2a.group(1)

# Look for value in double quotes
m3 = re.match(r'\A"(.*)"\Z', val)
if m3:
val = re.sub(r'\\(.)', _keep_escaped_format_characters,
m3.group(1))

overrides[key] = str(val)
elif not line or line.startswith('#'):
# ignore warnings for empty line-breaks or comments
Expand Down
58 changes: 54 additions & 4 deletions tests/test_env.py
Expand Up @@ -7,6 +7,7 @@
# the LICENSE.txt file that was distributed with this source code.

import os
import tempfile
from urllib.parse import quote

import pytest
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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',
Expand Down
4 changes: 0 additions & 4 deletions tests/test_env.txt
Expand Up @@ -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'
Expand All @@ -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---
Expand Down

0 comments on commit e3e7fc9

Please sign in to comment.