chore: clean repository history
This commit is contained in:
commit
e1fb2929c0
661 files changed
+167488
No files matched your search
@@ -0,0 +1,76 @@
|
||||
from pathlib import Path
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
|
||||
def load_module(path: Path):
|
||||
spec = importlib.util.spec_from_file_location("extract_practical_python_exercises", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_extracts_exercises_without_exposing_solution_code(tmp_path):
|
||||
kb = tmp_path / "kb"
|
||||
notes = kb / "raw" / "notes" / "01_Introduction"
|
||||
solutions = kb / "private" / "solutions" / "Solutions" / "1_1"
|
||||
exercises = kb / "wiki" / "exercises"
|
||||
private = kb / "private"
|
||||
notes.mkdir(parents=True)
|
||||
solutions.mkdir(parents=True)
|
||||
|
||||
(notes / "01_Python.md").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"# 1.1 Python",
|
||||
"",
|
||||
"Some lesson text.",
|
||||
"",
|
||||
"## Exercises",
|
||||
"",
|
||||
"### Exercise 1.1: Using Python as a Calculator",
|
||||
"",
|
||||
"Try simple expressions.",
|
||||
"",
|
||||
"### Exercise 1.2: Intentionally left blank (skip)",
|
||||
"",
|
||||
"Skip this one.",
|
||||
"",
|
||||
"## Next Section",
|
||||
"",
|
||||
"Not part of exercise 1.2.",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(solutions / "answer.py").write_text("print('secret solution')\n", encoding="utf-8")
|
||||
|
||||
script_path = Path(__file__).resolve().parents[1] / "scripts" / "extract_practical_python_exercises.py"
|
||||
module = load_module(script_path)
|
||||
result = module.extract_exercises(
|
||||
notes_dir=kb / "raw" / "notes",
|
||||
out_dir=exercises,
|
||||
private_dir=private,
|
||||
solutions_dir=kb / "private" / "solutions" / "Solutions",
|
||||
)
|
||||
|
||||
assert result.exercises_written == 2
|
||||
assert result.solutions_mapped == 1
|
||||
|
||||
first = exercises / "1-1-using-python-as-a-calculator.md"
|
||||
second = exercises / "1-2-intentionally-left-blank-skip.md"
|
||||
assert first.exists()
|
||||
assert second.exists()
|
||||
|
||||
first_text = first.read_text(encoding="utf-8")
|
||||
second_text = second.read_text(encoding="utf-8")
|
||||
solution_map = (private / "exercise-solutions-map.json").read_text(encoding="utf-8")
|
||||
|
||||
assert "has_private_solution: true" in first_text
|
||||
assert "student_visible_solution: false" in first_text
|
||||
assert "secret solution" not in first_text
|
||||
assert '"practical-python-1.1": "solutions/Solutions/1_1"' in solution_map
|
||||
assert "skip: true" in second_text
|
||||
assert "Not part of exercise 1.2." not in second_text
|
||||
@@ -0,0 +1,400 @@
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
KB_ROOT = Path(__file__).resolve().parents[1]
|
||||
WIKI_ROOT = KB_ROOT / "wiki"
|
||||
PRIVATE_ROOT = KB_ROOT / "private"
|
||||
RAW_ROOT = KB_ROOT / "raw"
|
||||
CONFIG_PATH = KB_ROOT / ".openkb" / "config.yaml"
|
||||
|
||||
WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
|
||||
TODO_RE = re.compile(r"\b(TODO|FIXME|TBD)\b", re.IGNORECASE)
|
||||
XXX_RE = re.compile(r"(?<!\.)\bXXX\b")
|
||||
PROMPT_INJECTION_RE = re.compile(
|
||||
r"(ignore previous instructions|disregard previous instructions|reveal the system prompt)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
VISIBLE_FORBIDDEN_PATTERNS = (
|
||||
"../private",
|
||||
"private/solutions",
|
||||
".openkb",
|
||||
"wiki/AGENTS",
|
||||
"wiki/log",
|
||||
"student_visible_solution: true",
|
||||
"ADD_YOUR_API_KEY_HERE",
|
||||
)
|
||||
|
||||
SUPPORTED_OPENKB_CONFIG_KEYS = {
|
||||
"api_base",
|
||||
"language",
|
||||
"model",
|
||||
"openai_base_url",
|
||||
"pageindex_threshold",
|
||||
"source_commit",
|
||||
"source_repo",
|
||||
}
|
||||
|
||||
REMOVED_CONCEPT_ALIAS_TARGETS = {
|
||||
"concepts/CSV 数据处理",
|
||||
"concepts/main 函数与脚本结构",
|
||||
"concepts/模块与 import",
|
||||
"concepts/测试、日志与调试",
|
||||
}
|
||||
|
||||
REQUIRED_ENHANCEMENT_CONCEPTS = {
|
||||
"concepts/Python-参数传递",
|
||||
"concepts/变量绑定",
|
||||
"concepts/数据清洗与类型转换",
|
||||
"concepts/Python-真值测试",
|
||||
"concepts/现代-Python-打包实践",
|
||||
"concepts/Python-项目组织",
|
||||
"concepts/Python-导入缓存",
|
||||
"concepts/文件类对象",
|
||||
"concepts/Python-网络请求",
|
||||
"concepts/XML-解析",
|
||||
"concepts/标准输入输出与管道",
|
||||
"concepts/pip-与-PyPI",
|
||||
"concepts/site-packages",
|
||||
"concepts/Python-包结构",
|
||||
"concepts/可变性与引用",
|
||||
"concepts/软件测试",
|
||||
"concepts/Python-自省",
|
||||
}
|
||||
|
||||
OVERLAP_SCOPE_PAGES = {
|
||||
"concepts/变量与数据类型.md",
|
||||
"concepts/Python-对象模型.md",
|
||||
"concepts/Python-可变对象.md",
|
||||
"concepts/Python-不可变对象.md",
|
||||
"concepts/Python-拷贝语义.md",
|
||||
"concepts/浅拷贝与深拷贝.md",
|
||||
"concepts/包与虚拟环境.md",
|
||||
"concepts/依赖管理.md",
|
||||
"concepts/代码分发.md",
|
||||
"concepts/可变性与引用.md",
|
||||
}
|
||||
|
||||
|
||||
def wiki_markdown_files() -> list[Path]:
|
||||
files = []
|
||||
for path in WIKI_ROOT.rglob("*.md"):
|
||||
rel = path.relative_to(WIKI_ROOT)
|
||||
if rel.name in {"AGENTS.md", "log.md"}:
|
||||
continue
|
||||
if rel.parts and rel.parts[0] == "reports":
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def rel(path: Path) -> str:
|
||||
return path.relative_to(KB_ROOT).as_posix()
|
||||
|
||||
|
||||
def frontmatter_value(text: str, key: str) -> str | None:
|
||||
lines = text.splitlines()
|
||||
if not lines or lines[0] != "---":
|
||||
return None
|
||||
for line in lines[1:]:
|
||||
if line == "---":
|
||||
return None
|
||||
if line.startswith(f"{key}:"):
|
||||
value = line.split(":", 1)[1].strip()
|
||||
return value.strip("\"'")
|
||||
return None
|
||||
|
||||
|
||||
def extract_wikilink_target(raw: str) -> str:
|
||||
target = raw.split("|", 1)[0].split("#", 1)[0].strip().strip("/")
|
||||
if target.endswith(".md"):
|
||||
target = target[:-3]
|
||||
return target
|
||||
|
||||
|
||||
def index_lines_for_section(section_name: str) -> list[str]:
|
||||
lines = read_text(WIKI_ROOT / "index.md").splitlines()
|
||||
header = f"## {section_name}"
|
||||
try:
|
||||
start = lines.index(header) + 1
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
section = []
|
||||
for line in lines[start:]:
|
||||
if line.startswith("## "):
|
||||
break
|
||||
if line.startswith("- "):
|
||||
section.append(line)
|
||||
return section
|
||||
|
||||
|
||||
def test_visible_wiki_markdown_is_structurally_clean():
|
||||
errors = []
|
||||
for path in wiki_markdown_files():
|
||||
text = read_text(path)
|
||||
lines = text.splitlines()
|
||||
|
||||
if not any(line.startswith("# ") for line in lines):
|
||||
errors.append(f"{rel(path)} has no H1 heading")
|
||||
|
||||
fence_count = sum(1 for line in lines if line.lstrip().startswith("```"))
|
||||
if fence_count % 2:
|
||||
errors.append(f"{rel(path)} has an unclosed fenced code block")
|
||||
|
||||
if TODO_RE.search(text) or XXX_RE.search(text):
|
||||
errors.append(f"{rel(path)} contains TODO/FIXME/TBD/XXX marker")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_visible_wiki_wikilinks_resolve_to_explicit_pages():
|
||||
pages = {
|
||||
path.relative_to(WIKI_ROOT).with_suffix("").as_posix()
|
||||
for path in WIKI_ROOT.rglob("*.md")
|
||||
}
|
||||
errors = []
|
||||
|
||||
for path in wiki_markdown_files():
|
||||
text = read_text(path)
|
||||
for raw_target in WIKILINK_RE.findall(text):
|
||||
target = extract_wikilink_target(raw_target)
|
||||
if not target:
|
||||
errors.append(f"{rel(path)} has an empty wikilink")
|
||||
continue
|
||||
if "/" not in target:
|
||||
errors.append(f"{rel(path)} uses non-explicit wikilink [[{raw_target}]]")
|
||||
continue
|
||||
if target not in pages:
|
||||
errors.append(f"{rel(path)} has broken wikilink [[{raw_target}]]")
|
||||
if target in REMOVED_CONCEPT_ALIAS_TARGETS:
|
||||
errors.append(f"{rel(path)} links to removed concept alias [[{raw_target}]]")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_visible_wiki_does_not_expose_internal_or_prompt_control_content():
|
||||
errors = []
|
||||
for path in wiki_markdown_files():
|
||||
text = read_text(path)
|
||||
for pattern in VISIBLE_FORBIDDEN_PATTERNS:
|
||||
if pattern in text:
|
||||
errors.append(f"{rel(path)} contains forbidden visible pattern: {pattern}")
|
||||
if PROMPT_INJECTION_RE.search(text):
|
||||
errors.append(f"{rel(path)} contains prompt-control wording")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_raw_and_visible_wiki_do_not_contain_api_key_placeholder():
|
||||
errors = []
|
||||
for root in (RAW_ROOT, WIKI_ROOT):
|
||||
for path in root.rglob("*"):
|
||||
if path.is_file() and path.suffix.lower() in {".md", ".yaml", ".yml", ".json", ".txt"}:
|
||||
if "ADD_YOUR_API_KEY_HERE" in read_text(path):
|
||||
errors.append(f"{rel(path)} contains ADD_YOUR_API_KEY_HERE")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_private_solution_map_matches_non_visible_exercises():
|
||||
map_path = PRIVATE_ROOT / "exercise-solutions-map.json"
|
||||
solution_map = json.loads(read_text(map_path))
|
||||
exercise_ids = {}
|
||||
errors = []
|
||||
|
||||
for path in sorted((WIKI_ROOT / "exercises").glob("*.md")):
|
||||
text = read_text(path)
|
||||
exercise_id = frontmatter_value(text, "id")
|
||||
if exercise_id:
|
||||
exercise_ids[exercise_id] = path
|
||||
|
||||
has_private_solution = frontmatter_value(text, "has_private_solution") == "true"
|
||||
visible_solution = frontmatter_value(text, "student_visible_solution") == "true"
|
||||
|
||||
if has_private_solution and not exercise_id:
|
||||
errors.append(f"{rel(path)} has a private solution but no exercise id")
|
||||
if has_private_solution and exercise_id not in solution_map:
|
||||
errors.append(f"{rel(path)} has no private solution map entry")
|
||||
if has_private_solution and visible_solution:
|
||||
errors.append(f"{rel(path)} exposes a private solution")
|
||||
|
||||
for exercise_id, solution_rel in solution_map.items():
|
||||
if exercise_id not in exercise_ids:
|
||||
errors.append(f"private/exercise-solutions-map.json maps missing exercise {exercise_id}")
|
||||
solution_path = Path(solution_rel)
|
||||
if solution_path.is_absolute() or ".." in solution_path.parts:
|
||||
errors.append(f"private/exercise-solutions-map.json has unsafe path for {exercise_id}")
|
||||
continue
|
||||
if not solution_rel.startswith("solutions/"):
|
||||
errors.append(f"private/exercise-solutions-map.json maps {exercise_id} outside solutions/")
|
||||
continue
|
||||
if not (PRIVATE_ROOT / solution_path).exists():
|
||||
errors.append(f"private/exercise-solutions-map.json maps {exercise_id} to missing path")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_openkb_config_uses_supported_keys_without_secrets():
|
||||
keys = set()
|
||||
text = read_text(CONFIG_PATH)
|
||||
errors = []
|
||||
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
key = stripped.split(":", 1)[0].strip()
|
||||
keys.add(key)
|
||||
|
||||
unknown = keys - SUPPORTED_OPENKB_CONFIG_KEYS
|
||||
if unknown:
|
||||
errors.append(f".openkb/config.yaml has unsupported keys: {sorted(unknown)}")
|
||||
|
||||
for forbidden in ("api_key", "secret", "token", "password"):
|
||||
if forbidden in text.lower():
|
||||
errors.append(f".openkb/config.yaml contains secret-like key text: {forbidden}")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_semantic_lint_known_findings_stay_fixed():
|
||||
errors = []
|
||||
overview = read_text(WIKI_ROOT / "summaries" / "00_Overview.md")
|
||||
index = read_text(WIKI_ROOT / "index.md")
|
||||
|
||||
if "Practical Python Programming 课程总览" not in overview:
|
||||
errors.append("summaries/00_Overview.md is not a course overview")
|
||||
if "本文是课程第 9 章 **Packages(包)** 的总览页" in overview:
|
||||
errors.append("summaries/00_Overview.md still duplicates the Packages overview")
|
||||
|
||||
setup_summary = read_text(WIKI_ROOT / "summaries" / "00_Setup.md")
|
||||
if "课程主体不依赖第三方包" not in setup_summary or "pandas" not in setup_summary:
|
||||
errors.append("summaries/00_Setup.md does not clarify third-party dependencies")
|
||||
if "仍受官方维护的 Python 3.x" not in setup_summary:
|
||||
errors.append("summaries/00_Setup.md does not clarify current Python version guidance")
|
||||
|
||||
python_summary = read_text(WIKI_ROOT / "summaries" / "01_Python.md")
|
||||
if "原课程材料的历史基线" not in python_summary or "仍受官方维护的 Python 3.x" not in python_summary:
|
||||
errors.append("summaries/01_Python.md does not clarify Python 3.6 historical context")
|
||||
if "不保证 URL 长期可用" not in python_summary or "本地 XML 示例文件" not in python_summary:
|
||||
errors.append("summaries/01_Python.md does not clarify external API example stability")
|
||||
if "http://docs.python.org" in python_summary:
|
||||
errors.append("summaries/01_Python.md still uses HTTP docs.python.org link")
|
||||
|
||||
if "[[summaries/practical-python-attribution]]" not in overview:
|
||||
errors.append("summaries/00_Overview.md does not link attribution")
|
||||
if "[[summaries/practical-python-attribution]]" not in index:
|
||||
errors.append("index.md does not link attribution")
|
||||
|
||||
distribution_summary = read_text(WIKI_ROOT / "summaries" / "03_Distribution.md")
|
||||
code_distribution = read_text(WIKI_ROOT / "concepts" / "代码分发.md")
|
||||
packaging_env = read_text(WIKI_ROOT / "concepts" / "包与虚拟环境.md")
|
||||
for path, text in [
|
||||
("summaries/03_Distribution.md", distribution_summary),
|
||||
("concepts/代码分发.md", code_distribution),
|
||||
("concepts/包与虚拟环境.md", packaging_env),
|
||||
]:
|
||||
if "pyproject.toml" not in text or "python -m build" not in text:
|
||||
errors.append(f"{path} does not clarify modern packaging practice")
|
||||
if "summaries/00_Overview]] 同样把代码分发列为第 9 章" in code_distribution:
|
||||
errors.append("concepts/代码分发.md still treats summaries/00_Overview as Packages overview")
|
||||
|
||||
iteration = read_text(WIKI_ROOT / "concepts" / "迭代协议与生成器.md")
|
||||
if "不应在现代 Python 3 新代码中使用" not in iteration:
|
||||
errors.append("concepts/迭代协议与生成器.md does not mark Python 2 i* iterator names as historical")
|
||||
iteration_summary = read_text(WIKI_ROOT / "summaries" / "01_Iteration_protocol.md")
|
||||
if "return sum([s.cost for s in self._holdings])" in iteration_summary:
|
||||
errors.append("summaries/01_Iteration_protocol.md still mixes Stock.cost property and method examples")
|
||||
special_methods = read_text(WIKI_ROOT / "concepts" / "特殊方法.md")
|
||||
if "return sum([s.cost for s in self._holdings])" in special_methods:
|
||||
errors.append("concepts/特殊方法.md still mixes Stock.cost property and method examples")
|
||||
for path in [
|
||||
WIKI_ROOT / "summaries" / "01_Python.md",
|
||||
WIKI_ROOT / "concepts" / "Python-开发环境.md",
|
||||
WIKI_ROOT / "concepts" / "Python-文档与帮助系统.md",
|
||||
WIKI_ROOT / "exercises" / "1-2-getting-help.md",
|
||||
]:
|
||||
if "http://docs.python.org" in read_text(path):
|
||||
errors.append(f"{rel(path)} still uses HTTP docs.python.org link")
|
||||
|
||||
for target in REMOVED_CONCEPT_ALIAS_TARGETS:
|
||||
if (WIKI_ROOT / f"{target}.md").exists():
|
||||
errors.append(f"{target}.md should not exist as an empty alias page")
|
||||
if f"[[{target}]]" in index:
|
||||
errors.append(f"index.md still lists removed alias [[{target}]]")
|
||||
|
||||
for path in [
|
||||
WIKI_ROOT / "concepts" / "模块与-import.md",
|
||||
WIKI_ROOT / "concepts" / "包与虚拟环境.md",
|
||||
WIKI_ROOT / "summaries" / "02_Third_party.md",
|
||||
]:
|
||||
if "/usr/local/lib/python3.6" in read_text(path):
|
||||
errors.append(f"{rel(path)} still uses version-specific python3.6 paths")
|
||||
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_followup_enhancements_are_indexed_and_scoped():
|
||||
index_text = read_text(WIKI_ROOT / "index.md")
|
||||
errors = []
|
||||
|
||||
document_lines = index_lines_for_section("Documents")
|
||||
exercise_lines = index_lines_for_section("Exercises")
|
||||
concept_lines = index_lines_for_section("Concepts")
|
||||
|
||||
summary_targets = {
|
||||
path.relative_to(WIKI_ROOT).with_suffix("").as_posix()
|
||||
for path in (WIKI_ROOT / "summaries").glob("*.md")
|
||||
}
|
||||
indexed_documents = {
|
||||
extract_wikilink_target(WIKILINK_RE.search(line).group(1))
|
||||
for line in document_lines
|
||||
if WIKILINK_RE.search(line)
|
||||
}
|
||||
if summary_targets != indexed_documents:
|
||||
errors.append("index.md Documents section is not synced with summaries/")
|
||||
|
||||
for line in document_lines:
|
||||
if " — " not in line or "type:" not in line or "source:" not in line:
|
||||
errors.append(f"Document index line lacks metadata: {line}")
|
||||
|
||||
exercise_targets = {
|
||||
path.relative_to(WIKI_ROOT).with_suffix("").as_posix()
|
||||
for path in (WIKI_ROOT / "exercises").glob("*.md")
|
||||
}
|
||||
indexed_exercises = {
|
||||
extract_wikilink_target(WIKILINK_RE.search(line).group(1))
|
||||
for line in exercise_lines
|
||||
if WIKILINK_RE.search(line)
|
||||
}
|
||||
if exercise_targets != indexed_exercises:
|
||||
errors.append("index.md Exercises section is not synced with exercises/")
|
||||
|
||||
for line in exercise_lines:
|
||||
for field in ("id:", "title:", "section:", "private_solution:", "skip:"):
|
||||
if field not in line:
|
||||
errors.append(f"Exercise index line lacks {field} metadata: {line}")
|
||||
|
||||
for target in REQUIRED_ENHANCEMENT_CONCEPTS:
|
||||
if not (WIKI_ROOT / f"{target}.md").exists():
|
||||
errors.append(f"Missing enhancement concept page: {target}")
|
||||
if f"[[{target}]]" not in index_text:
|
||||
errors.append(f"index.md does not list enhancement concept: {target}")
|
||||
|
||||
for line in concept_lines:
|
||||
if " — " not in line:
|
||||
errors.append(f"Concept index line lacks one-line brief: {line}")
|
||||
|
||||
for rel_path in OVERLAP_SCOPE_PAGES:
|
||||
text = read_text(WIKI_ROOT / rel_path)
|
||||
if "## 本页边界" not in text:
|
||||
errors.append(f"{rel_path} lacks scope boundary section")
|
||||
|
||||
assert errors == []
|
||||
Reference in new issue
Block a user