Skip to content

Commit 0262f14

Browse files
committed
refactor: 全量 Type Hints 现代化 — dict[str, object] / list[dict[str, object]] 风格
- llm.py: 6 个函数签名更新 - app.py: load_resources 返回类型细化 - utils.py: 3 个函数参数+返回类型细化 - views/: 8 个模块共 15 个函数签名更新 - i18n.py, logging_config.py, views/__init__.py: 已有完整类型无需修改 - 配合 from __future__ import annotations 使用现代 PEP 604 语法
1 parent f3e5c63 commit 0262f14

File tree

13 files changed

+158
-29
lines changed

13 files changed

+158
-29
lines changed

.pytest_cache/v/cache/lastfailed

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
2-
"tests/test_app.py::TestConstants": true,
3-
"tests/test_app.py::TestEncodeProfile": true,
4-
"tests/test_app.py::TestExportJson": true,
5-
"tests/test_app.py::TestExportMarkdown": true,
6-
"tests/test_app.py::TestFilterResources": true
2+
"tests/e2e/test_core_flows.py::TestAppLoads::()::test_title_visible": true,
3+
"tests/test_llm.py::TestGenerateTrendInsights::()::test_api_failure_returns_stale_cache": true
74
}

.pytest_cache/v/cache/nodeids

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,33 @@
2424
"tests/test_app.py::TestConstants::()::test_provider_presets_have_required_keys",
2525
"tests/test_app.py::TestConstants::()::test_direction_domains_are_lists",
2626
"tests/test_app.py::TestConstants::()::test_preset_profiles_have_required_fields",
27+
"tests/test_config.py::TestProviderPresets::()::test_has_required_keys",
28+
"tests/test_config.py::TestProviderPresets::()::test_preset_urls_are_strings",
29+
"tests/test_config.py::TestProviderPresets::()::test_non_custom_have_models",
30+
"tests/test_config.py::TestProviderPresets::()::test_custom_preset_empty",
31+
"tests/test_config.py::TestProviderPresets::()::test_known_providers_exist",
32+
"tests/test_config.py::TestEmojiMaps::()::test_level_emoji_covers_standard_levels",
33+
"tests/test_config.py::TestEmojiMaps::()::test_type_emoji_covers_standard_types",
34+
"tests/test_config.py::TestEmojiMaps::()::test_focus_emoji_covers_all",
35+
"tests/test_config.py::TestEmojiMaps::()::test_level_order_values_ascending",
36+
"tests/test_config.py::TestFormOptions::()::test_levels_not_empty",
37+
"tests/test_config.py::TestFormOptions::()::test_preferences_not_empty",
38+
"tests/test_config.py::TestFormOptions::()::test_languages_not_empty",
39+
"tests/test_config.py::TestFormOptions::()::test_focus_options_not_empty",
40+
"tests/test_config.py::TestFormOptions::()::test_focus_map_covers_all_options",
41+
"tests/test_config.py::TestFormOptions::()::test_focus_map_values_valid",
42+
"tests/test_config.py::TestDirections::()::test_directions_not_empty",
43+
"tests/test_config.py::TestDirections::()::test_all_directions_in_domain_map",
44+
"tests/test_config.py::TestDirections::()::test_domains_are_lists_of_strings",
45+
"tests/test_config.py::TestPresetProfiles::()::test_has_required_fields",
46+
"tests/test_config.py::TestPresetProfiles::()::test_levels_are_valid",
47+
"tests/test_config.py::TestPresetProfiles::()::test_directions_are_valid",
48+
"tests/test_config.py::TestPresetProfiles::()::test_descriptions_match_profiles",
49+
"tests/test_config.py::TestPresetProfiles::()::test_hours_positive",
50+
"tests/test_config.py::TestSystemPrompts::()::test_system_prompt_not_empty",
51+
"tests/test_config.py::TestSystemPrompts::()::test_system_prompt_mentions_json",
52+
"tests/test_config.py::TestSystemPrompts::()::test_chat_prompt_not_empty",
53+
"tests/test_config.py::TestSystemPrompts::()::test_chat_prompt_has_rules",
2754
"tests/test_i18n.py::TestTranslationFunction::()::test_basic_zh",
2855
"tests/test_i18n.py::TestTranslationFunction::()::test_basic_en",
2956
"tests/test_i18n.py::TestTranslationFunction::()::test_default_lang_is_zh",
@@ -33,5 +60,101 @@
3360
"tests/test_i18n.py::TestTranslationFunction::()::test_all_keys_have_zh_and_en",
3461
"tests/test_i18n.py::TestTranslationFunction::()::test_no_empty_translations",
3562
"tests/test_i18n.py::TestTranslationFunction::()::test_all_nav_keys_exist",
36-
"tests/test_i18n.py::TestTranslationFunction::()::test_format_string_keys_have_placeholders"
63+
"tests/test_i18n.py::TestTranslationFunction::()::test_format_string_keys_have_placeholders",
64+
"tests/test_llm.py::TestGetLlmConfig::()::test_api_key_from_session_state",
65+
"tests/test_llm.py::TestGetLlmConfig::()::test_api_key_from_secrets",
66+
"tests/test_llm.py::TestGetLlmConfig::()::test_api_key_from_env",
67+
"tests/test_llm.py::TestGetLlmConfig::()::test_default_provider",
68+
"tests/test_llm.py::TestGetLlmConfig::()::test_custom_provider",
69+
"tests/test_llm.py::TestGetLlmConfig::()::test_known_provider_uses_preset_url",
70+
"tests/test_llm.py::TestGeneratePath::()::test_raises_without_api_key",
71+
"tests/test_llm.py::TestGeneratePath::()::test_returns_parsed_json",
72+
"tests/test_llm.py::TestGeneratePath::()::test_passes_correct_model",
73+
"tests/test_llm.py::TestGeneratePath::()::test_user_message_contains_profile",
74+
"tests/test_llm.py::TestCompactResources::()::test_single_resource",
75+
"tests/test_llm.py::TestCompactResources::()::test_empty_list",
76+
"tests/test_llm.py::TestCompactResources::()::test_topics_truncated",
77+
"tests/test_llm.py::TestCompactResources::()::test_missing_optional_fields",
78+
"tests/test_llm.py::TestCompactResources::()::test_multiple_resources",
79+
"tests/test_llm.py::TestInsightsCache::()::test_no_cache_file",
80+
"tests/test_llm.py::TestInsightsCache::()::test_save_and_load",
81+
"tests/test_llm.py::TestInsightsCache::()::test_stale_cache_returns_none",
82+
"tests/test_llm.py::TestInsightsCache::()::test_corrupted_cache_returns_none",
83+
"tests/test_llm.py::TestGenerateTrendInsights::()::test_no_api_key_returns_empty",
84+
"tests/test_llm.py::TestGenerateTrendInsights::()::test_empty_channels",
85+
"tests/test_llm.py::TestGenerateTrendInsights::()::test_returns_insights_json",
86+
"tests/test_llm.py::TestGenerateTrendInsights::()::test_uses_cache_when_fresh",
87+
"tests/test_llm.py::TestGenerateTrendInsights::()::test_api_failure_returns_empty",
88+
"tests/test_logging_config.py::TestGetLogger::()::test_returns_logger",
89+
"tests/test_logging_config.py::TestGetLogger::()::test_default_name",
90+
"tests/test_logging_config.py::TestGetLogger::()::test_idempotent_init",
91+
"tests/test_logging_config.py::TestGetLogger::()::test_logger_has_handlers",
92+
"tests/test_progress.py::TestCollectProgress::test_collects_chat_messages",
93+
"tests/test_progress.py::TestCollectProgress::test_collects_done_keys",
94+
"tests/test_progress.py::TestCollectProgress::test_collects_profile_and_path",
95+
"tests/test_progress.py::TestCollectProgress::test_empty_state",
96+
"tests/test_progress.py::TestCollectProgress::test_version_field",
97+
"tests/test_progress.py::TestRestoreProgress::test_restores_chat",
98+
"tests/test_progress.py::TestRestoreProgress::test_restores_done_keys",
99+
"tests/test_progress.py::TestRestoreProgress::test_restores_language",
100+
"tests/test_progress.py::TestRestoreProgress::test_restores_profile_and_path",
101+
"tests/test_progress.py::TestRestoreProgress::test_returns_false_on_invalid",
102+
"tests/test_progress.py::TestLocalFilePersistence::test_load_returns_none_when_no_file",
103+
"tests/test_progress.py::TestLocalFilePersistence::test_save_and_load_roundtrip",
104+
"tests/test_progress.py::TestLocalFilePersistence::test_save_returns_none_without_path",
105+
"tests/test_progress.py::TestProgressToJson::test_accepts_explicit_data",
106+
"tests/test_progress.py::TestProgressToJson::test_serializes_to_json",
107+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_special_characters",
108+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_nested_data",
109+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_large_profile",
110+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_decode_empty_string",
111+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_decode_non_base64_chars",
112+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_decode_none_input",
113+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_decode_non_dict_json",
114+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_numeric_values",
115+
"tests/test_utils_extended.py::TestEncodeDecodeEdge::()::test_encode_deterministic",
116+
"tests/test_utils_extended.py::TestFilterEdge::()::test_empty_resources",
117+
"tests/test_utils_extended.py::TestFilterEdge::()::test_empty_direction",
118+
"tests/test_utils_extended.py::TestFilterEdge::()::test_unknown_direction",
119+
"tests/test_utils_extended.py::TestFilterEdge::()::test_focus_both_returns_all",
120+
"tests/test_utils_extended.py::TestFilterEdge::()::test_filter_preserves_resource_integrity",
121+
"tests/test_utils_extended.py::TestFilterEdge::()::test_zh_language_zh_first",
122+
"tests/test_utils_extended.py::TestFilterEdge::()::test_direction_matched_before_general",
123+
"tests/test_utils_extended.py::TestFilterEdge::()::test_applied_focus_ordering",
124+
"tests/test_utils_extended.py::TestFilterEdge::()::test_resources_with_missing_domain",
125+
"tests/test_utils_extended.py::TestExportEdge::()::test_markdown_empty_weeks",
126+
"tests/test_utils_extended.py::TestExportEdge::()::test_markdown_unknown_resource_id",
127+
"tests/test_utils_extended.py::TestExportEdge::()::test_markdown_channel_resource",
128+
"tests/test_utils_extended.py::TestExportEdge::()::test_markdown_multiple_weeks",
129+
"tests/test_utils_extended.py::TestExportEdge::()::test_json_empty_path",
130+
"tests/test_utils_extended.py::TestExportEdge::()::test_json_profile_preserved",
131+
"tests/test_utils_extended.py::TestExportEdge::()::test_markdown_has_checkbox",
132+
"tests/test_utils_extended.py::TestExportEdge::()::test_markdown_profile_fields",
133+
"tests/test_views.py::TestBuildChatContext::()::test_includes_resource_summary",
134+
"tests/test_views.py::TestBuildChatContext::()::test_includes_profile_when_present",
135+
"tests/test_views.py::TestBuildChatContext::()::test_includes_path_when_present",
136+
"tests/test_views.py::TestBuildChatContext::()::test_no_profile_no_path",
137+
"tests/test_views.py::TestBuildChatContext::()::test_topic_truncation",
138+
"tests/test_views.py::TestBuildChatContext::()::test_resource_limit_60",
139+
"tests/test_views.py::TestBuildChatContext::()::test_empty_resources",
140+
"tests/test_views.py::TestBuildChatContext::()::test_path_with_no_weeks",
141+
"tests/test_views.py::TestSubmitFeedback::()::test_local_save_returns_local",
142+
"tests/test_views.py::TestSubmitFeedback::()::test_github_save_returns_github",
143+
"tests/test_views.py::TestSubmitFeedback::()::test_github_failure_falls_back_local",
144+
"tests/test_views.py::TestSubmitFeedback::()::test_local_save_failure_silent",
145+
"tests/test_views.py::TestSubmitFeedback::()::test_feedback_file_content",
146+
"tests/test_views.py::TestSubmitFeedback::()::test_github_issue_body_contains_rating",
147+
"tests/test_views.py::TestImportPlanLogic::()::test_module_importable",
148+
"tests/test_views.py::TestImportPlanLogic::()::test_settings_importable",
149+
"tests/test_views.py::TestImportPlanLogic::()::test_all_views_importable",
150+
"tests/e2e/test_core_flows.py::TestAppLoads::()::test_title_visible",
151+
"tests/e2e/test_core_flows.py::TestAppLoads::()::test_sidebar_present",
152+
"tests/e2e/test_core_flows.py::TestAppLoads::()::test_form_visible_on_first_load",
153+
"tests/e2e/test_core_flows.py::TestNavigation::()::test_navigate_to_chat",
154+
"tests/e2e/test_core_flows.py::TestNavigation::()::test_navigate_to_browser",
155+
"tests/e2e/test_core_flows.py::TestNavigation::()::test_navigate_to_radar",
156+
"tests/e2e/test_core_flows.py::TestResourceBrowser::()::test_resource_count_displayed",
157+
"tests/e2e/test_core_flows.py::TestResourceBrowser::()::test_search_filters_resources",
158+
"tests/e2e/test_core_flows.py::TestLanguageToggle::()::test_toggle_to_english",
159+
"tests/e2e/test_core_flows.py::TestPresetTemplates::()::test_preset_buttons_visible"
37160
]

app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
@st.cache_data(show_spinner=False, ttl=3600)
24-
def load_resources() -> list[dict]:
24+
def load_resources() -> list[dict[str, object]]:
2525
"""Cached wrapper — YAML is parsed once, refreshed every hour."""
2626
return _load_resources_uncached()
2727

llm.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def get_llm_config() -> tuple[str, str, str]:
3939
return api_key, base_url, model
4040

4141

42-
def _compact_resources(resources: list[dict]) -> str:
42+
def _compact_resources(resources: list[dict[str, object]]) -> str:
4343
"""Compact resource list to minimal token footprint."""
4444
lines = []
4545
for r in resources:
@@ -52,7 +52,7 @@ def _compact_resources(resources: list[dict]) -> str:
5252
return "\n".join(lines)
5353

5454

55-
def generate_path(profile: dict, resources: list[dict]) -> dict:
55+
def generate_path(profile: dict[str, object], resources: list[dict[str, object]]) -> dict[str, object]:
5656
"""Call LLM to generate a personalized learning path (streaming)."""
5757
api_key, base_url, model = get_llm_config()
5858
if not api_key:
@@ -130,7 +130,7 @@ def generate_path(profile: dict, resources: list[dict]) -> dict:
130130
}"""
131131

132132

133-
def _load_insights_cache() -> dict | None:
133+
def _load_insights_cache() -> dict[str, object] | None:
134134
"""Load cached insights if fresh (same date)."""
135135
if not os.path.exists(INSIGHTS_CACHE_PATH):
136136
return None
@@ -144,7 +144,7 @@ def _load_insights_cache() -> dict | None:
144144
return None
145145

146146

147-
def _save_insights_cache(data: dict) -> None:
147+
def _save_insights_cache(data: dict[str, object]) -> None:
148148
"""Save insights to local cache file."""
149149
try:
150150
with open(INSIGHTS_CACHE_PATH, "w", encoding="utf-8") as f:
@@ -153,7 +153,7 @@ def _save_insights_cache(data: dict) -> None:
153153
_log.warning("insights_cache_save_failed: %s", e)
154154

155155

156-
def generate_trend_insights(channels: list[dict], force_refresh: bool = False) -> dict:
156+
def generate_trend_insights(channels: list[dict[str, object]], force_refresh: bool = False) -> dict[str, object]:
157157
"""Generate daily AI trend insights via LLM, with local caching."""
158158
if not force_refresh:
159159
cached = _load_insights_cache()

utils.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ def decode_profile(s: str) -> dict | None:
3838
return None
3939

4040

41-
def filter_resources_for_direction(resources: list[dict], direction: str, language: str, focus: str = "both") -> list[dict]:
41+
def filter_resources_for_direction(
42+
resources: list[dict[str, object]],
43+
direction: str,
44+
language: str,
45+
focus: str = "both",
46+
) -> list[dict[str, object]]:
4247
"""Pre-filter resources by direction + language + focus, capped at 50."""
4348
domains = DIRECTION_TO_DOMAIN.get(direction, [])
4449
if domains:
@@ -67,7 +72,11 @@ def filter_resources_for_direction(resources: list[dict], direction: str, langua
6772
return filtered[:35]
6873

6974

70-
def export_plan_markdown(path_data: dict, profile: dict, resources: list[dict]) -> str:
75+
def export_plan_markdown(
76+
path_data: dict[str, object],
77+
profile: dict[str, object],
78+
resources: list[dict[str, object]],
79+
) -> str:
7180
"""Export learning path as readable Markdown."""
7281
ridx = {r["id"]: r for r in resources}
7382
lines = [
@@ -110,7 +119,7 @@ def export_plan_markdown(path_data: dict, profile: dict, resources: list[dict])
110119
return "\n".join(lines)
111120

112121

113-
def export_plan_json(path_data: dict, profile: dict) -> str:
122+
def export_plan_json(path_data: dict[str, object], profile: dict[str, object]) -> str:
114123
"""Export learning path as importable JSON."""
115124
return json.dumps({"profile": profile, "path": path_data}, ensure_ascii=False, indent=2)
116125

views/browser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from views import _lang
1111

1212

13-
def render_resource_browser(resources: list[dict]) -> None:
13+
def render_resource_browser(resources: list[dict[str, object]]) -> None:
1414
L = _lang()
1515
st.title(t("browser_title", L))
1616
st.caption(t("browser_hint", L))

views/chat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
_log = get_logger("chat")
1515

1616

17-
def _build_chat_context(resources: list[dict]) -> str:
17+
def _build_chat_context(resources: list[dict[str, object]]) -> str:
1818
"""Build chat context: user profile + current path + resource summary."""
1919
parts = []
2020
profile = st.session_state.get("profile")
@@ -36,7 +36,7 @@ def _build_chat_context(resources: list[dict]) -> str:
3636
return "\n\n".join(parts)
3737

3838

39-
def render_chat(resources: list[dict]) -> None:
39+
def render_chat(resources: list[dict[str, object]]) -> None:
4040
L = _lang()
4141
st.title(t("chat_title", L))
4242
st.markdown(t("chat_subtitle", L))

views/feedback.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
_log = get_logger("feedback")
1717

1818

19-
def submit_feedback(feedback: dict) -> str:
19+
def submit_feedback(feedback: dict[str, object]) -> str:
2020
"""Save feedback: local file (always try) + GitHub Issues (if token). Returns 'github' or 'local'."""
2121
try:
2222
feedback_dir = os.path.join(os.path.dirname(__file__), "..", "feedback")

views/form.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from views import _lang
1212

1313

14-
def render_form() -> tuple[bool, dict]:
14+
def render_form() -> tuple[bool, dict[str, object]]:
1515
L = _lang()
1616
st.title(t("form_title", L))
1717
st.markdown(t("form_subtitle", L))

views/import_plan.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from views import _lang
1111

1212

13-
def render_import_plan(resources: list[dict]) -> None:
13+
def render_import_plan(resources: list[dict[str, object]]) -> None:
1414
L = _lang()
1515
st.title(t("import_title", L))
1616
st.markdown(t("import_subtitle", L))

0 commit comments

Comments
 (0)