diff --git a/app/modules/indexer/parser/zhixing.py b/app/modules/indexer/parser/zhixing.py index 319be03a4..3ae3f9e2d 100644 --- a/app/modules/indexer/parser/zhixing.py +++ b/app/modules/indexer/parser/zhixing.py @@ -41,7 +41,7 @@ class ZhixingSiteUserInfo(SiteParserBase): soup = BeautifulSoup(html_text, 'html.parser') return bool(soup.find(id='um')) - def _parse_user_base_info(self, html_text: SitePage): + def _parse_user_base_info(self, html_text: SitePage) -> None: """ 解析用户基本信息,这里把_parse_user_traffic_info和_parse_user_detail_info合并到这里 """ @@ -49,7 +49,7 @@ class ZhixingSiteUserInfo(SiteParserBase): return None soup = BeautifulSoup(html_text, 'html.parser') details_tabs = soup.find_all('div', class_='user-details-tabs') - info_dict = {} + info_dict: dict[str, str] = {} for tab in details_tabs: for p in tab.find_all('p'): text = p.text.strip() @@ -70,9 +70,9 @@ class ZhixingSiteUserInfo(SiteParserBase): self.userid = info_dict.get('UID') self.username = info_dict.get('用户名') self.user_level = info_dict.get('用户组') - self.join_at = time_tools.normalize_datetime(info_dict.get('注册时间')) if '注册时间' in info_dict else None + self.join_at = time_tools.normalize_datetime(info_dict['注册时间']) if '注册时间' in info_dict else None - def num_filesize_safe(s: str): + def num_filesize_safe(s: Optional[str]) -> int: """为不带单位的数字补充字节单位,空值保持零容量。""" if s: s = s.strip() @@ -80,15 +80,15 @@ class ZhixingSiteUserInfo(SiteParserBase): s += ' B' return self.num_filesize(s) if s else 0 - self.upload = num_filesize_safe(info_dict.get('上传流量')) if '上传流量' in info_dict else 0 - self.download = num_filesize_safe(info_dict.get('下载流量')) if '下载流量' in info_dict else 0 - self.ratio = float(info_dict.get('共享率')) if '共享率' in info_dict else 0 - self.bonus = float(info_dict.get('保种积分')) if '保种积分' in info_dict else 0.0 + self.upload = num_filesize_safe(info_dict['上传流量']) if '上传流量' in info_dict else 0 + self.download = num_filesize_safe(info_dict['下载流量']) if '下载流量' in info_dict else 0 + self.ratio = float(info_dict['共享率']) if '共享率' in info_dict else 0 + self.bonus = float(info_dict['保种积分']) if '保种积分' in info_dict else 0.0 self.message_unread = 0 # 暂无消息解析 # Temporarily set seeding from basic, will override or fallback later - self.seeding = int(info_dict.get('当前保种数量')) if '当前保种数量' in info_dict else 0 - self.seeding_size = num_filesize_safe(info_dict.get('当前保种容量')) if '当前保种容量' in info_dict else 0 + self.seeding = int(info_dict['当前保种数量']) if '当前保种数量' in info_dict else 0 + self.seeding_size = num_filesize_safe(info_dict['当前保种容量']) if '当前保种容量' in info_dict else 0 def _parse_user_traffic_info(self, html_text: str) -> None: """该站点在其他解析入口处理此数据,本入口不追加操作。""" @@ -130,7 +130,7 @@ class ZhixingSiteUserInfo(SiteParserBase): """ pass - def parse(self): + def parse(self) -> None: """ 解析站点信息 """ @@ -166,7 +166,7 @@ class ZhixingSiteUserInfo(SiteParserBase): page_num += 1 # Fallback to basic if no seeding found from pages if self.seeding == 0 and hasattr(self, '_basic_info'): - def num_filesize_safe(s: str): + def num_filesize_safe(s: Optional[str]) -> int: """为不带单位的数字补充字节单位,空值保持零容量。""" if s: s = s.strip() @@ -176,17 +176,8 @@ class ZhixingSiteUserInfo(SiteParserBase): self.seeding = int(self._basic_info.get('当前保种数量', 0)) self.seeding_size = num_filesize_safe(self._basic_info.get('当前保种容量', '')) - # 🔑 最终对外统一转字符串,避免 join 报错 + # 文本字段归一为空字符串;流量、数量和比例保留基类约定的数值类型。 self.userid = str(self.userid or "") self.username = str(self.username or "") self.user_level = str(self.user_level or "") self.join_at = str(self.join_at or "") - - self.upload = str(self.upload or 0) - self.download = str(self.download or 0) - self.ratio = str(self.ratio or 0) - self.bonus = str(self.bonus or 0.0) - self.message_unread = str(self.message_unread or 0) - - self.seeding = str(self.seeding or 0) - self.seeding_size = str(self.seeding_size or 0) diff --git a/mypy.ini b/mypy.ini index d8068edaa..d05ba98c4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,6 +7,7 @@ follow_imports = skip show_error_codes = True pretty = True files = + app/modules/indexer/parser/zhixing.py, app/chain/transfer/format.py, app/application/formatting.py, app/modules/transmission/__init__.py, diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json index a7a7fd4c5..25cf576f6 100644 --- a/tests/fixtures/architecture/mypy-baseline.json +++ b/tests/fixtures/architecture/mypy-baseline.json @@ -1157,11 +1157,6 @@ "arg-type": 2, "type-arg": 1 }, - "app/modules/indexer/parser/zhixing.py": { - "arg-type": 7, - "assignment": 7, - "no-untyped-def": 4 - }, "app/modules/indexer/spider/__init__.py": { "arg-type": 12, "assignment": 3, diff --git a/tests/test_zhixing_numeric_contract.py b/tests/test_zhixing_numeric_contract.py new file mode 100644 index 000000000..4ba253159 --- /dev/null +++ b/tests/test_zhixing_numeric_contract.py @@ -0,0 +1,52 @@ +"""知行解析器的数值属性与对外 Schema 兼容合同。""" + +from unittest.mock import Mock + +import pytest + +from app.modules.indexer.parser import SiteParserBase +from app.modules.indexer.parser.zhixing import ZhixingSiteUserInfo +from app.schemas.site import SiteUserData + + +@pytest.mark.parametrize("seeding_page,seeding,seeding_size", [ + ("", 3, 2 * 1024 ** 3), + ('
1 GiB
', 1, 1024 ** 3), +]) +def test_numeric_properties_preserve_site_schema_and_seeding_fallback( + monkeypatch, seeding_page, seeding, seeding_size, +): + """离线解析保留页面统计和详情回退,属性采用基类数值类型,API 数值不变。""" + parser = ZhixingSiteUserInfo( + site_name="offline", url="https://zhixing.invalid/", site_cookie="", apikey=None, token=None, + request_utils=Mock(), + ) + monkeypatch.setattr(SiteParserBase, "parse", lambda _self: None) + parser._parse_site_page("") + parser._index_html = 'offline' + basic = """
+

UID:42

用户名:offline

用户组:Power User

+

上传流量:2048

下载流量:1024

共享率:2

保种积分:1.5

+

当前保种数量:3

当前保种容量:2 GiB

+
""" + pages = { + "https://zhixing.invalid/user/42/": basic, + "https://zhixing.invalid/user/42/seeding/p1": seeding_page, + "https://zhixing.invalid/user/42/seeding/p2": "", + } + reader = Mock(side_effect=lambda *, url: pages[url]) + monkeypatch.setattr(parser, "_get_page_content", reader) + parser.parse() + + expected = { + "upload": 2048, "download": 1024, "ratio": 2.0, "bonus": 1.5, + "message_unread": 0, "seeding": seeding, "seeding_size": seeding_size, + } + public_data = SiteUserData.model_validate(parser).model_dump(include=set(expected)) + assert public_data == expected + assert parser.userid == "42" + assert parser.username == "offline" + assert parser.user_level == "Power User" + for field, value in expected.items(): + assert type(getattr(parser, field)) is type(value), field + assert reader.call_count == (3 if seeding_page else 2)