Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc46b0555 |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "jiangchang-platform-kit"
|
name = "jiangchang-platform-kit"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK"
|
description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -15,6 +16,29 @@ _user_env_cache: dict[str, str] | None = None
|
|||||||
_example_defaults_cache: dict[str, str] | None = None
|
_example_defaults_cache: dict[str, str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_env_value(raw_value: str) -> str:
|
||||||
|
"""Parse one .env value: strip quotes / trailing comments; empty+# → \"\"."""
|
||||||
|
value = raw_value if raw_value is not None else ""
|
||||||
|
stripped = value.strip()
|
||||||
|
if not stripped:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Quoted values keep interior content (including #).
|
||||||
|
if len(stripped) >= 2 and stripped[0] == stripped[-1] and stripped[0] in ('"', "'"):
|
||||||
|
return stripped[1:-1]
|
||||||
|
|
||||||
|
# Unquoted: ``KEY= # comment`` or ``KEY=# comment`` must be empty, not the comment text.
|
||||||
|
# Also strip ``KEY=value # comment``. Keep ``KEY=https://x/#y`` (no whitespace before #).
|
||||||
|
if re.search(r"(^|\s)#", value):
|
||||||
|
# Split at first whitespace-#-comment or whole-value comment.
|
||||||
|
m = re.match(r"^(.*?)\s+#.*$", value)
|
||||||
|
if m is not None:
|
||||||
|
return m.group(1).strip()
|
||||||
|
if stripped.startswith("#"):
|
||||||
|
return ""
|
||||||
|
return stripped
|
||||||
|
|
||||||
|
|
||||||
def _parse_env_file(path: str) -> dict[str, str]:
|
def _parse_env_file(path: str) -> dict[str, str]:
|
||||||
"""标准库手写 .env 解析(不引 python-dotenv)。"""
|
"""标准库手写 .env 解析(不引 python-dotenv)。"""
|
||||||
result: dict[str, str] = {}
|
result: dict[str, str] = {}
|
||||||
@@ -29,14 +53,9 @@ def _parse_env_file(path: str) -> dict[str, str]:
|
|||||||
continue
|
continue
|
||||||
key, _, value = line.partition("=")
|
key, _, value = line.partition("=")
|
||||||
key = key.strip()
|
key = key.strip()
|
||||||
value = value.strip()
|
|
||||||
if not key:
|
if not key:
|
||||||
continue
|
continue
|
||||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
|
result[key] = _parse_env_value(value)
|
||||||
value = value[1:-1]
|
|
||||||
elif " #" in value:
|
|
||||||
value = value.split(" #", 1)[0].strip()
|
|
||||||
result[key] = value
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,49 @@ class TestConfig(unittest.TestCase):
|
|||||||
self.assertEqual(config.get_float("BAD", 9.9), 9.9)
|
self.assertEqual(config.get_float("BAD", 9.9), 9.9)
|
||||||
self.assertEqual(config.get_int("BAD", 7), 7)
|
self.assertEqual(config.get_int("BAD", 7), 7)
|
||||||
|
|
||||||
|
def test_parse_empty_value_with_inline_comment(self) -> None:
|
||||||
|
"""Regression: ``KEY= # comment`` must be empty, not ``# comment``."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
example = os.path.join(tmp, ".env.example")
|
||||||
|
with open(example, "w", encoding="utf-8") as f:
|
||||||
|
f.write(
|
||||||
|
"DEFAULT_ACCOUNT_ID= # 可填账号管理中的编号\n"
|
||||||
|
"DEFAULT_LOGIN_ID=# 只填登录名\n"
|
||||||
|
"HAS_VALUE=real # trailing note\n"
|
||||||
|
"URL=https://example.com/path#frag\n"
|
||||||
|
"QUOTED=\"keep # hash\"\n"
|
||||||
|
"EMPTY=\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
os.environ["JIANGCHANG_DATA_ROOT"] = tmp
|
||||||
|
os.environ["JIANGCHANG_USER_ID"] = "u1"
|
||||||
|
# Clear process overrides for these keys if any
|
||||||
|
for k in (
|
||||||
|
"DEFAULT_ACCOUNT_ID",
|
||||||
|
"DEFAULT_LOGIN_ID",
|
||||||
|
"HAS_VALUE",
|
||||||
|
"URL",
|
||||||
|
"QUOTED",
|
||||||
|
"EMPTY",
|
||||||
|
):
|
||||||
|
os.environ.pop(k, None)
|
||||||
|
|
||||||
|
dest = config.ensure_env_file("sk-comment", example)
|
||||||
|
config.reset_cache()
|
||||||
|
|
||||||
|
parsed = config._parse_env_file(dest)
|
||||||
|
self.assertEqual(parsed.get("DEFAULT_ACCOUNT_ID"), "")
|
||||||
|
self.assertEqual(parsed.get("DEFAULT_LOGIN_ID"), "")
|
||||||
|
self.assertEqual(parsed.get("HAS_VALUE"), "real")
|
||||||
|
self.assertEqual(parsed.get("URL"), "https://example.com/path#frag")
|
||||||
|
self.assertEqual(parsed.get("QUOTED"), "keep # hash")
|
||||||
|
self.assertEqual(parsed.get("EMPTY"), "")
|
||||||
|
|
||||||
|
# Empty example/user values must not surface as fake non-empty defaults.
|
||||||
|
self.assertIsNone(config.get("DEFAULT_ACCOUNT_ID"))
|
||||||
|
self.assertIsNone(config.get("DEFAULT_LOGIN_ID"))
|
||||||
|
self.assertEqual(config.get("HAS_VALUE"), "real")
|
||||||
|
|
||||||
def test_merge_missing_env_keys_appends_only_new(self) -> None:
|
def test_merge_missing_env_keys_appends_only_new(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
example = os.path.join(tmp, ".env.example")
|
example = os.path.join(tmp, ".env.example")
|
||||||
|
|||||||
Reference in New Issue
Block a user