django-environとは?
- django-environはdjango内で環境変数を簡単に使えるようにするためのパッケージです
事象の説明
-
python runserver
実行時に下記のようなエラーになってしまいました
test-py3.13xxx@EBCI-xxx:~/work/test$ poetry run python manage.py runserver
UserWarning: Engine not recognized from url: {'NAME': 'ysql\\x3a//test_user\\x3aP@ssw0rd1!@127.0.0.1\\x3a3306/test_db', 'USER': '', 'PASSWORD': '', 'HOST': '', 'PORT': '', 'ENGINE': ''}
warnings.warn("Engine not recognized from url: {}".format(config))
UserWarning: Engine not recognized from url: {'NAME': 'ysql\\x3a//test_user\\x3aP@ssw0rd1!@127.0.0.1\\x3a3306/test_db', 'USER': '', 'PASSWORD': '', 'HOST': '', 'PORT': '', 'ENGINE': ''}
warnings.warn("Engine not recognized from url: {}".format(config))
Watching for file changes with StatReloader
Performing system checks...
- ぱっと見、DB接続情報が正しくパースできていないように思えます
-
.env
ファイルは下記のように設定していました
DATABASE_URL="mysql://test_user:P@ssw0rd1!@127.0.0.3306/test_db"
試したこと1(GitHubでissueがないか確認)
- DATABASE_URLがカスタムバックエンドにアンダースコアが含まれている場合に正しく動作しないというissueがありました
- このissueのコメントに
This might be a Py compatibility issue. I think url parsing fails at environ.py L#375
というものを発見 - でもどうすればいいのかわからず..
試したこと2(ソースコードの確認)
- django-environを使っているなら、
settings.py
内で環境変数の取得を行っているのは下記当たりかと思います
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
- shellで確認してみます
env = environ.Env(DEBUG=(bool, False))
env("DATABASE_URL")
-> 'mysql\\x3a//test_user\\x3aPssw0rd1!@127.0.0.1\\x3a3306/test_db'
- 接続情報がエンコードされてしまっているのがわかります
-
env("DATABASE_URL")
は内部的にはget_value
を使っていそうなのでget_values
を見てみます - https://github.com/joke2k/django-environ/blob/develop/environ/environ.py#L367C1-L438C21
def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
"""Return value for given environment variable.
:param str var:
Name of variable.
:param collections.abc.Callable or None cast:
Type to cast return value as.
:param default:
If var not present in environ, return this instead.
:param bool parse_default:
Force to parse default.
:returns: Value from environment or default (if set).
:rtype: typing.IO[typing.Any]
"""
logger.debug(
"get '%s' casted as '%s' with default '%s'",
var, cast, default)
var_name = f'{self.prefix}{var}'
if var_name in self.scheme:
var_info = self.scheme[var_name]
try:
has_default = len(var_info) == 2
except TypeError:
has_default = False
if has_default:
if not cast:
cast = var_info[0]
if default is self.NOTSET:
try:
default = var_info[1]
except IndexError:
pass
else:
if not cast:
cast = var_info
try:
value = self.ENVIRON[var_name]
except KeyError as exc:
if default is self.NOTSET:
error_msg = f'Set the {var_name} environment variable'
raise ImproperlyConfigured(error_msg) from exc
value = default
# Resolve any proxied values
prefix = b'$' if isinstance(value, bytes) else '$'
escape = rb'\$' if isinstance(value, bytes) else r'\$'
if hasattr(value, 'startswith') and value.startswith(prefix):
value = value.lstrip(prefix)
value = self.get_value(value, cast=cast, default=default)
if self.escape_proxy and hasattr(value, 'replace'):
value = value.replace(escape, prefix)
# Smart casting
if self.smart_cast:
if cast is None and default is not None and \
not isinstance(default, NoValue):
cast = type(default)
value = None if default is None and value == '' else value
if value != default or (parse_default and value is not None):
value = self.parse_value(value, cast)
return value
- 409行目で指定された名前(今回は
DATABASE_URL
)に対応する値を取得していそうです
value = self.ENVIRON[var_name]
-
self.ENVIRON
はos.environ
みたいなので、この中身を見てみます
>>> import os
>>> os.environ
>>> 'mysql\\x3a//test_user\\x3aPssw0rd1!@127.0.0.1\\x3a3306/test_db'
-
.env
から環境変数化する際にエスケープされてしまい、それによりdjango内で環境変数を参照した際に誤認識してしまっていそうです
原因
- DATABASE_URLを設定する際はシングルクォートで囲うのがよさそうです
- ダブルクォーテーションで囲ってしまうと「!」などの文字列がコマンドとして認識されてしまうようです
DATABASE_URL='mysql://test_user:P@ssw0rd1!@127.0.0.3306/test_db'
- Bashにおいては、シングルクォートは完全にリテラルとして扱われるのに対して、ダブルクォートりは変数やコマンドが展開されることに注意しましょう!