@@ -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+
87138def 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 (
0 commit comments