Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from app.extended_curriculum import EXTRA_EXAMS, EXTRA_LESSONS, EXTRA_MODULES
from app.lessons_13_25 import LESSONS_13_25


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


MODULES.extend(EXTRA_MODULES)
LESSONS.extend(LESSONS_13_25)
LESSONS.extend(EXTRA_LESSONS)


Expand Down Expand Up @@ -767,7 +769,9 @@ def lesson(

def public_question(question: dict) -> dict:
return {
key: value for key, value in question.items() if key not in {"answer", "answers", "tests"}
key: value
for key, value in question.items()
if key not in {"answer", "answers", "tests", "reference"}
}


Expand Down
73 changes: 69 additions & 4 deletions app/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,57 @@ def normalize(value: Any) -> str:
return str(value or "").strip().casefold()


def _check_source_requirements(tree: ast.AST, tests: list[dict]) -> str | None:
for test in tests:
if test.get("kind") != "source":
continue
if test.get("requires") == "unpacking":
name = test.get("name")
function = test.get("function")
scope: ast.AST = tree
if function:
target_function = next(
(
node
for node in ast.walk(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == function
),
None,
)
scope = ast.Module(
body=target_function.body if target_function else [], type_ignores=[]
)
has_unpacking = any(
isinstance(node, ast.Assign)
and isinstance(node.value, ast.Name)
and node.value.id == name
and any(isinstance(target, (ast.Tuple, ast.List)) for target in node.targets)
for node in ast.walk(scope)
)
uses_index = any(isinstance(node, ast.Subscript) for node in ast.walk(scope))
if function:
unpacking_values = {
id(node.value)
for node in ast.walk(scope)
if isinstance(node, ast.Assign)
and isinstance(node.value, ast.Name)
and node.value.id == name
and any(isinstance(target, (ast.Tuple, ast.List)) for target in node.targets)
}
uses_parameter_elsewhere = any(
isinstance(node, ast.Name)
and node.id == name
and id(node) not in unpacking_values
for node in ast.walk(scope)
)
else:
uses_parameter_elsewhere = False
if not has_unpacking or uses_index or uses_parameter_elsewhere:
return "В этом задании нужна распаковка последовательности без индексов."
return None


def run_code(source: str, tests: list[dict]) -> dict:
"""Выполняет небольшой фрагмент в отдельном Python-процессе с лимитом времени."""
if len(source) > 5_000:
Expand All @@ -96,7 +147,8 @@ def run_code(source: str, tests: list[dict]) -> dict:
"timed_out": False,
}
try:
SafetyVisitor().visit(ast.parse(source))
tree = ast.parse(source)
SafetyVisitor().visit(tree)
except (SyntaxError, ValueError) as error:
return {
"correct": False,
Expand All @@ -107,10 +159,23 @@ def run_code(source: str, tests: list[dict]) -> dict:
"timed_out": False,
}

requirement_error = _check_source_requirements(tree, tests)
if requirement_error:
return {
"correct": False,
"message": requirement_error,
"checks": [],
"stdout": "",
"stderr": "",
"error": requirement_error,
"timed_out": False,
}

runtime_tests = [test for test in tests if test.get("kind") != "source"]
encoded_code = base64.b64encode(source.encode("utf-8")).decode("ascii")
encoded_tests = base64.b64encode(json.dumps(tests, ensure_ascii=False).encode("utf-8")).decode(
"ascii"
)
encoded_tests = base64.b64encode(
json.dumps(runtime_tests, ensure_ascii=False).encode("utf-8")
).decode("ascii")
program = RUNNER.replace("CODE", repr(encoded_code)).replace("TESTS", repr(encoded_tests))
try:
result = subprocess.run(
Expand Down
59 changes: 30 additions & 29 deletions app/extended_curriculum.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Расширенная часть курса: 27 модулей по 4 микроурока = 108 новых уроков.
"""Каталог расширенного курса и генератор уроков 26–120.

Материал написан для приложения, а не скопирован из внешних источников. Его порядок
следует естественной траектории официального Python Tutorial: синтаксис → данные →
Expand Down Expand Up @@ -1433,34 +1433,35 @@ def build_extended_course() -> tuple[
for lesson_index, spec in enumerate(unit["lessons"]):
slug, title, subtitle, keyword, example, concept, advice = spec
lesson_id = f"{module_id}-{slug}"
questions = _make_questions(lesson_id, spec, TASK_CYCLES[unit_index][lesson_index])
lessons.append(
{
"id": lesson_id,
"module_id": module_id,
"order": order,
"title": title,
"subtitle": subtitle,
"duration": 11 + lesson_index,
"xp": 35 + (unit_index // 4) * 5,
"theory": [
_theory(title, concept, example, f"Ориентир: {keyword}."),
_theory(
"Когда это применять",
f"{subtitle}. Это маленький инструмент, который становится полезным в большом проекте.",
example,
advice,
),
_theory(
"Проверка понимания",
"Сформулируй правило своими словами и измени пример так, чтобы увидеть другой результат.",
f"# Тема: {title}\n# Ключевой термин: {keyword}",
),
],
"questions": questions,
}
)
question_ids.append(questions[0]["id"])
question_ids.append(f"{lesson_id}-choice")
if order >= 26:
questions = _make_questions(lesson_id, spec, TASK_CYCLES[unit_index][lesson_index])
lessons.append(
{
"id": lesson_id,
"module_id": module_id,
"order": order,
"title": title,
"subtitle": subtitle,
"duration": 11 + lesson_index,
"xp": 35 + (unit_index // 4) * 5,
"theory": [
_theory(title, concept, example, f"Ориентир: {keyword}."),
_theory(
"Когда это применять",
f"{subtitle}. Это маленький инструмент, который становится полезным в большом проекте.",
example,
advice,
),
_theory(
"Проверка понимания",
"Сформулируй правило своими словами и измени пример так, чтобы увидеть другой результат.",
f"# Тема: {title}\n# Ключевой термин: {keyword}",
),
],
"questions": questions,
}
)
order += 1
exams[module_id] = {
"title": f"Контрольная точка: {unit['title']}",
Expand Down
Loading
Loading