Skip to content

Commit d9b0b82

Browse files
CopilotNewFuture
andauthored
feat(config): 支持当行注释 Add JSON comment support with # and // styles for configuration files (NewFuture#515)
* Add JSON comment support with # and // styles Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * Fix Python 2.7 compatibility and remove trailing whitespace Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> * Fix lint issues and simplify comment removal logic Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> Co-authored-by: New Future <NewFuture@users.noreply.github.com>
1 parent f759bc3 commit d9b0b82

6 files changed

Lines changed: 393 additions & 8 deletions

File tree

ddns/cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def sync(self):
6969
with open(self.__filename, "w") as data:
7070
# 只保存非私有字段(不以__开头的字段)
7171
filtered_data = {k: v for k, v in super(Cache, self).items() if not k.startswith("__")}
72-
dump(filtered_data, data, separators=(',', ':'))
72+
dump(filtered_data, data, separators=(",", ":"))
7373
self.__logger.debug("save cache data to %s", self.__filename)
7474
self.__time = time()
7575
self.__changed = False

ddns/config/file.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from io import open
88
from json import loads as json_decode, dumps as json_encode
99
from sys import stderr, stdout
10+
from ..util.comment import remove_comment
1011

1112

1213
def load_config(config_path):
@@ -31,9 +32,11 @@ def load_config(config_path):
3132
except Exception as e:
3233
stderr.write("Failed to load config file `%s`: %s\n" % (config_path, e))
3334
raise
34-
# 优先尝试JSON解析
35+
# 移除注释后尝试JSON解析
3536
try:
36-
config = json_decode(content)
37+
# 移除单行注释(# 和 // 风格)
38+
content_without_comments = remove_comment(content)
39+
config = json_decode(content_without_comments)
3740
except (ValueError, SyntaxError) as json_error:
3841
# JSON解析失败,尝试AST解析
3942
try:

ddns/util/comment.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# -*- coding:utf-8 -*-
2+
"""
3+
Comment removal utility for JSON configuration files.
4+
Supports both # and // style single line comments.
5+
@author: GitHub Copilot
6+
"""
7+
8+
9+
def remove_comment(content):
10+
# type: (str) -> str
11+
"""
12+
移除字符串中的单行注释。
13+
支持 # 和 // 两种注释风格。
14+
15+
Args:
16+
content (str): 包含注释的字符串内容
17+
18+
Returns:
19+
str: 移除注释后的字符串
20+
21+
Examples:
22+
>>> remove_comment('{"key": "value"} // comment')
23+
'{"key": "value"} '
24+
>>> remove_comment('# This is a comment\\n{"key": "value"}')
25+
'\\n{"key": "value"}'
26+
"""
27+
if not content:
28+
return content
29+
30+
lines = content.splitlines()
31+
cleaned_lines = []
32+
33+
for line in lines:
34+
# 移除行内注释,但要小心不要破坏字符串内的内容
35+
cleaned_line = _remove_line_comment(line)
36+
cleaned_lines.append(cleaned_line)
37+
38+
return "\n".join(cleaned_lines)
39+
40+
41+
def _remove_line_comment(line):
42+
# type: (str) -> str
43+
"""
44+
移除单行中的注释部分。
45+
46+
Args:
47+
line (str): 要处理的行
48+
49+
Returns:
50+
str: 移除注释后的行
51+
"""
52+
# 检查是否是整行注释
53+
stripped = line.lstrip()
54+
if stripped.startswith("#") or stripped.startswith("//"):
55+
return ""
56+
57+
# 查找行内注释,需要考虑字符串内容
58+
in_string = False
59+
quote_char = None
60+
i = 0
61+
62+
while i < len(line):
63+
char = line[i]
64+
65+
# 处理字符串内的转义序列
66+
if in_string and char == "\\" and i + 1 < len(line):
67+
i += 2 # 跳过转义字符
68+
continue
69+
70+
# 处理引号字符
71+
if char in ('"', "'"):
72+
if not in_string:
73+
in_string = True
74+
quote_char = char
75+
elif char == quote_char:
76+
in_string = False
77+
quote_char = None
78+
79+
# 在字符串外检查注释标记
80+
elif not in_string:
81+
if char == "#":
82+
return line[:i].rstrip()
83+
elif char == "/" and i + 1 < len(line) and line[i + 1] == "/":
84+
return line[:i].rstrip()
85+
86+
i += 1
87+
88+
return line

tests/test_cache.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ def test_cache_new_custom_path(self):
554554
# Clean up
555555
cache.close()
556556

557-
@patch('ddns.cache.time')
557+
@patch("ddns.cache.time")
558558
def test_cache_new_outdated_cache(self, mock_time):
559559
"""Test Cache.new with outdated cache file (>72 hours old)"""
560560
import logging
@@ -575,7 +575,7 @@ def test_cache_new_outdated_cache(self, mock_time):
575575
# Mock the file modification time to be 73 hours ago
576576
old_mtime = current_time - (73 * 3600) # 73 hours ago
577577

578-
with patch('ddns.cache.stat') as mock_stat:
578+
with patch("ddns.cache.stat") as mock_stat:
579579
mock_stat.return_value.st_mtime = old_mtime
580580
cache = Cache.new(self.cache_file, "test_hash", logger)
581581

@@ -608,7 +608,7 @@ def test_cache_new_empty_cache(self):
608608
# Clean up
609609
cache.close()
610610

611-
@patch('ddns.cache.time')
611+
@patch("ddns.cache.time")
612612
def test_cache_new_valid_cache(self, mock_time):
613613
"""Test Cache.new with valid cache file with data"""
614614
import logging
@@ -619,7 +619,7 @@ def test_cache_new_valid_cache(self, mock_time):
619619
# Create a cache file with test data
620620
test_data = {
621621
"domain1.com": {"ip": "1.2.3.4", "timestamp": 1234567890},
622-
"domain2.com": {"ip": "5.6.7.8", "timestamp": 1234567891}
622+
"domain2.com": {"ip": "5.6.7.8", "timestamp": 1234567891},
623623
}
624624
with open(self.cache_file, "w") as f:
625625
json.dump(test_data, f)
@@ -631,7 +631,7 @@ def test_cache_new_valid_cache(self, mock_time):
631631
# Mock file modification time to be recent (within 72 hours)
632632
recent_mtime = current_time - (24 * 3600) # 24 hours ago
633633

634-
with patch('ddns.cache.stat') as mock_stat:
634+
with patch("ddns.cache.stat") as mock_stat:
635635
mock_stat.return_value.st_mtime = recent_mtime
636636
cache = Cache.new(self.cache_file, "test_hash", logger)
637637

tests/test_config_file.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,52 @@ def test_load_config_file_encoding_utf8(self):
452452
self.assertEqual(loaded_config["symbols"], "αβγδε")
453453
self.assertEqual(loaded_config["emoji"], "🌍🔧⚡")
454454

455+
def test_load_config_json_with_hash_comments(self):
456+
"""测试加载带有 # 注释的JSON配置文件"""
457+
json_with_comments = """{
458+
# Configuration for DDNS
459+
"dns": "cloudflare", # DNS provider
460+
"id": "test@example.com",
461+
"token": "secret123", # API token
462+
"ttl": 300
463+
# End of config
464+
}"""
465+
file_path = self.create_test_file("test_hash_comments.json", json_with_comments)
466+
467+
config = load_config(file_path)
468+
469+
expected = {"dns": "cloudflare", "id": "test@example.com", "token": "secret123", "ttl": 300}
470+
self.assertEqual(config, expected)
471+
472+
def test_load_config_json_with_double_slash_comments(self):
473+
"""测试加载带有 // 注释的JSON配置文件"""
474+
json_with_comments = """{
475+
// Configuration for DDNS
476+
"$schema": "https://ddns.newfuture.cc/schema/v4.0.json", // Schema validation
477+
"debug": false, // false=disable, true=enable
478+
"dns": "dnspod_com", // DNS provider
479+
"id": "1008666",
480+
"token": "ae86$cbbcctv666666666666666", // API Token
481+
"ipv4": ["test.lorzl.ml"], // IPv4 domains
482+
"ipv6": ["test.lorzl.ml"], // IPv6 domains
483+
"proxy": null // Proxy settings
484+
}"""
485+
file_path = self.create_test_file("test_double_slash_comments.json", json_with_comments)
486+
487+
config = load_config(file_path)
488+
489+
expected = {
490+
"$schema": "https://ddns.newfuture.cc/schema/v4.0.json",
491+
"debug": False,
492+
"dns": "dnspod_com",
493+
"id": "1008666",
494+
"token": "ae86$cbbcctv666666666666666",
495+
"ipv4": ["test.lorzl.ml"],
496+
"ipv6": ["test.lorzl.ml"],
497+
"proxy": None,
498+
}
499+
self.assertEqual(config, expected)
500+
455501
def test_save_config_pretty_format(self):
456502
"""Test that saved JSON is properly formatted"""
457503
config_data = {"dns": "cloudflare", "log_level": "DEBUG", "log_file": "/var/log/ddns.log"}

0 commit comments

Comments
 (0)