From 369e6a0ce8922d30f46ee5eed552d54863c9e710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= Date: Mon, 22 Apr 2024 16:27:32 +0100 Subject: [PATCH] chore: initial support for dot env loading --- src/appier/config.py | 49 +++++++++++++++++++++++++++++++++++++++ src/appier/test/config.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/src/appier/config.py b/src/appier/config.py index fae6168a..7613c518 100644 --- a/src/appier/config.py +++ b/src/appier/config.py @@ -172,6 +172,7 @@ def load(names=(FILE_NAME,), path=None, encoding="utf-8", ctx=None): for path in paths: for name in names: load_file(name=name, path=path, encoding=encoding, ctx=ctx) + load_dot_env(ctx=ctx) load_env(ctx=ctx) @@ -218,6 +219,54 @@ def load_file(name=FILE_NAME, path=None, encoding="utf-8", ctx=None): configs[key] = value +def load_dot_env(name=".env", encoding="utf-8", ctx=None): + configs = ctx["configs"] if ctx else CONFIGS + config_f = ctx["config_f"] if ctx else CONFIG_F + + file_path = os.path.abspath(name) + file_path = os.path.normpath(file_path) + + exists = os.path.exists(file_path) + if not exists: + return + + exists = file_path in config_f + if exists: + config_f.remove(file_path) + config_f.append(file_path) + + file = open(file_path, "rb") + try: + data = file.read() + finally: + file.close() + if not data: + return + + data = data.decode(encoding) + data = data.strip() + lines = data.splitlines() + lines = [line.strip() for line in lines] + + for line in lines: + line = line.strip() + if not line: + continue + if line.startswith("#"): + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + if ( + value.startswith('"') + and value.endswith('"') + or value.startswith("'") + and value.endswith("'") + ): + value = value[1:-1].replace('\\"', '"') + configs[key] = value + + def load_env(ctx=None): configs = ctx["configs"] if ctx else CONFIGS diff --git a/src/appier/test/config.py b/src/appier/test/config.py index 39090cb4..b56123d7 100644 --- a/src/appier/test/config.py +++ b/src/appier/test/config.py @@ -32,6 +32,11 @@ import appier +try: + import unittest.mock as mock +except ImportError: + mock = None + class ConfigTest(unittest.TestCase): def test_basic(self): @@ -79,3 +84,34 @@ def test_none(self): result = appier.conf("HEIGHT", cast=int) self.assertEqual(result, None) + + def test_load_dot_env(self): + if mock == None: + self.skipTest("Skipping test: mock unavailable") + + mock_data = mock.mock_open( + read_data=b"#This is a comment\nAGE=10\nNAME=colony\n" + ) + + with mock.patch("os.path.exists", return_value=True), mock.patch( + "builtins.open", mock_data, create=True + ) as mock_open: + ctx = dict(configs={}, config_f=[]) + + appier.config.load_dot_env(".env", "utf-8", ctx) + + result = appier.conf("AGE", cast=int) + self.assertEqual(type(result), int) + self.assertEqual(result, 10) + + result = appier.conf("AGE", cast=str) + + self.assertEqual(result, "10") + self.assertEqual(type(result), str) + + result = appier.conf("HEIGHT", cast=int) + self.assertEqual(result, None) + + self.assertEqual(len(ctx["configs"]), 2) + + self.assertEqual(mock_open.return_value.close.call_count, 1)