diff --git a/pyproject.toml b/pyproject.toml index b5552a2..9714758 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jiangchang-platform-kit" -version = "1.2.1" +version = "1.2.2" description = "匠厂平台共享组件:Skill 实体 SDK + 桌面应用自动化 SDK" readme = "README.md" requires-python = ">=3.10" diff --git a/src/jiangchang_skill_core/config.py b/src/jiangchang_skill_core/config.py index 0f02986..e5d58b8 100644 --- a/src/jiangchang_skill_core/config.py +++ b/src/jiangchang_skill_core/config.py @@ -3,6 +3,7 @@ from __future__ import annotations import os +import re import shutil from typing import Any @@ -15,6 +16,29 @@ _user_env_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]: """标准库手写 .env 解析(不引 python-dotenv)。""" result: dict[str, str] = {} @@ -29,14 +53,9 @@ def _parse_env_file(path: str) -> dict[str, str]: continue key, _, value = line.partition("=") key = key.strip() - value = value.strip() if not key: continue - if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"): - value = value[1:-1] - elif " #" in value: - value = value.split(" #", 1)[0].strip() - result[key] = value + result[key] = _parse_env_value(value) return result diff --git a/tests/test_config.py b/tests/test_config.py index 85ba6ac..82e8704 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -81,6 +81,49 @@ class TestConfig(unittest.TestCase): self.assertEqual(config.get_float("BAD", 9.9), 9.9) 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: with tempfile.TemporaryDirectory() as tmp: example = os.path.join(tmp, ".env.example")