Skip to content

Commit 4c8ea47

Browse files
rootroot
authored andcommitted
merge: hand-written lessons 13-25 with topic-bound practice (PR #17)
2 parents 10f3e2e + ce04b15 commit 4c8ea47

8 files changed

Lines changed: 1141 additions & 36 deletions

File tree

‎app/content.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from app.extended_curriculum import EXTRA_EXAMS, EXTRA_LESSONS, EXTRA_MODULES
6+
from app.lessons_13_25 import LESSONS_13_25
67

78

89
def theory(title: str, text: str, example: str, tip: str = "") -> dict:
@@ -728,6 +729,7 @@ def lesson(
728729

729730

730731
MODULES.extend(EXTRA_MODULES)
732+
LESSONS.extend(LESSONS_13_25)
731733
LESSONS.extend(EXTRA_LESSONS)
732734

733735

@@ -767,7 +769,9 @@ def lesson(
767769

768770
def public_question(question: dict) -> dict:
769771
return {
770-
key: value for key, value in question.items() if key not in {"answer", "answers", "tests"}
772+
key: value
773+
for key, value in question.items()
774+
if key not in {"answer", "answers", "tests", "reference"}
771775
}
772776

773777

‎app/evaluator.py‎

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,57 @@ def normalize(value: Any) -> str:
8484
return str(value or "").strip().casefold()
8585

8686

87+
def _check_source_requirements(tree: ast.AST, tests: list[dict]) -> str | None:
88+
for test in tests:
89+
if test.get("kind") != "source":
90+
continue
91+
if test.get("requires") == "unpacking":
92+
name = test.get("name")
93+
function = test.get("function")
94+
scope: ast.AST = tree
95+
if function:
96+
target_function = next(
97+
(
98+
node
99+
for node in ast.walk(tree)
100+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
101+
and node.name == function
102+
),
103+
None,
104+
)
105+
scope = ast.Module(
106+
body=target_function.body if target_function else [], type_ignores=[]
107+
)
108+
has_unpacking = any(
109+
isinstance(node, ast.Assign)
110+
and isinstance(node.value, ast.Name)
111+
and node.value.id == name
112+
and any(isinstance(target, (ast.Tuple, ast.List)) for target in node.targets)
113+
for node in ast.walk(scope)
114+
)
115+
uses_index = any(isinstance(node, ast.Subscript) for node in ast.walk(scope))
116+
if function:
117+
unpacking_values = {
118+
id(node.value)
119+
for node in ast.walk(scope)
120+
if isinstance(node, ast.Assign)
121+
and isinstance(node.value, ast.Name)
122+
and node.value.id == name
123+
and any(isinstance(target, (ast.Tuple, ast.List)) for target in node.targets)
124+
}
125+
uses_parameter_elsewhere = any(
126+
isinstance(node, ast.Name)
127+
and node.id == name
128+
and id(node) not in unpacking_values
129+
for node in ast.walk(scope)
130+
)
131+
else:
132+
uses_parameter_elsewhere = False
133+
if not has_unpacking or uses_index or uses_parameter_elsewhere:
134+
return "В этом задании нужна распаковка последовательности без индексов."
135+
return None
136+
137+
87138
def run_code(source: str, tests: list[dict]) -> dict:
88139
"""Выполняет небольшой фрагмент в отдельном Python-процессе с лимитом времени."""
89140
if len(source) > 5_000:
@@ -96,7 +147,8 @@ def run_code(source: str, tests: list[dict]) -> dict:
96147
"timed_out": False,
97148
}
98149
try:
99-
SafetyVisitor().visit(ast.parse(source))
150+
tree = ast.parse(source)
151+
SafetyVisitor().visit(tree)
100152
except (SyntaxError, ValueError) as error:
101153
return {
102154
"correct": False,
@@ -107,10 +159,23 @@ def run_code(source: str, tests: list[dict]) -> dict:
107159
"timed_out": False,
108160
}
109161

162+
requirement_error = _check_source_requirements(tree, tests)
163+
if requirement_error:
164+
return {
165+
"correct": False,
166+
"message": requirement_error,
167+
"checks": [],
168+
"stdout": "",
169+
"stderr": "",
170+
"error": requirement_error,
171+
"timed_out": False,
172+
}
173+
174+
runtime_tests = [test for test in tests if test.get("kind") != "source"]
110175
encoded_code = base64.b64encode(source.encode("utf-8")).decode("ascii")
111-
encoded_tests = base64.b64encode(json.dumps(tests, ensure_ascii=False).encode("utf-8")).decode(
112-
"ascii"
113-
)
176+
encoded_tests = base64.b64encode(
177+
json.dumps(runtime_tests, ensure_ascii=False).encode("utf-8")
178+
).decode("ascii")
114179
program = RUNNER.replace("CODE", repr(encoded_code)).replace("TESTS", repr(encoded_tests))
115180
try:
116181
result = subprocess.run(

‎app/extended_curriculum.py‎

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Расширенная часть курса: 27 модулей по 4 микроурока = 108 новых уроков.
1+
"""Каталог расширенного курса и генератор уроков 26–120.
22
33
Материал написан для приложения, а не скопирован из внешних источников. Его порядок
44
следует естественной траектории официального Python Tutorial: синтаксис → данные →
@@ -1433,34 +1433,35 @@ def build_extended_course() -> tuple[
14331433
for lesson_index, spec in enumerate(unit["lessons"]):
14341434
slug, title, subtitle, keyword, example, concept, advice = spec
14351435
lesson_id = f"{module_id}-{slug}"
1436-
questions = _make_questions(lesson_id, spec, TASK_CYCLES[unit_index][lesson_index])
1437-
lessons.append(
1438-
{
1439-
"id": lesson_id,
1440-
"module_id": module_id,
1441-
"order": order,
1442-
"title": title,
1443-
"subtitle": subtitle,
1444-
"duration": 11 + lesson_index,
1445-
"xp": 35 + (unit_index // 4) * 5,
1446-
"theory": [
1447-
_theory(title, concept, example, f"Ориентир: {keyword}."),
1448-
_theory(
1449-
"Когда это применять",
1450-
f"{subtitle}. Это маленький инструмент, который становится полезным в большом проекте.",
1451-
example,
1452-
advice,
1453-
),
1454-
_theory(
1455-
"Проверка понимания",
1456-
"Сформулируй правило своими словами и измени пример так, чтобы увидеть другой результат.",
1457-
f"# Тема: {title}\n# Ключевой термин: {keyword}",
1458-
),
1459-
],
1460-
"questions": questions,
1461-
}
1462-
)
1463-
question_ids.append(questions[0]["id"])
1436+
question_ids.append(f"{lesson_id}-choice")
1437+
if order >= 26:
1438+
questions = _make_questions(lesson_id, spec, TASK_CYCLES[unit_index][lesson_index])
1439+
lessons.append(
1440+
{
1441+
"id": lesson_id,
1442+
"module_id": module_id,
1443+
"order": order,
1444+
"title": title,
1445+
"subtitle": subtitle,
1446+
"duration": 11 + lesson_index,
1447+
"xp": 35 + (unit_index // 4) * 5,
1448+
"theory": [
1449+
_theory(title, concept, example, f"Ориентир: {keyword}."),
1450+
_theory(
1451+
"Когда это применять",
1452+
f"{subtitle}. Это маленький инструмент, который становится полезным в большом проекте.",
1453+
example,
1454+
advice,
1455+
),
1456+
_theory(
1457+
"Проверка понимания",
1458+
"Сформулируй правило своими словами и измени пример так, чтобы увидеть другой результат.",
1459+
f"# Тема: {title}\n# Ключевой термин: {keyword}",
1460+
),
1461+
],
1462+
"questions": questions,
1463+
}
1464+
)
14641465
order += 1
14651466
exams[module_id] = {
14661467
"title": f"Контрольная точка: {unit['title']}",

0 commit comments

Comments
 (0)