rbx.cp 0.13.4__py3-none-any.whl → 0.13.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. rbx/box/checkers.py +2 -9
  2. rbx/box/cli.py +0 -1
  3. rbx/box/code.py +27 -80
  4. rbx/box/environment.py +16 -6
  5. rbx/box/generators.py +26 -3
  6. rbx/box/global_package.py +1 -1
  7. rbx/box/header.py +26 -8
  8. rbx/box/package.py +0 -14
  9. rbx/box/setter_config.py +11 -0
  10. rbx/box/solutions.py +12 -4
  11. rbx/box/tasks.py +9 -4
  12. rbx/box/testing/testing_package.py +69 -2
  13. rbx/box/ui/screens/run_explorer.py +0 -8
  14. rbx/box/ui/utils/run_ui.py +7 -3
  15. rbx/box/ui/widgets/test_output_box.py +1 -1
  16. rbx/box/unit.py +4 -4
  17. rbx/box/validators.py +3 -1
  18. rbx/grading/caching.py +65 -15
  19. rbx/grading/judge/cacher.py +5 -3
  20. rbx/grading/judge/program.py +300 -0
  21. rbx/grading/judge/sandbox.py +30 -200
  22. rbx/grading/judge/sandboxes/stupid_sandbox.py +234 -240
  23. rbx/grading/judge/sandboxes/tee.py +31 -0
  24. rbx/grading/judge/storage.py +7 -1
  25. rbx/grading/steps.py +89 -201
  26. rbx/grading/steps_with_caching.py +15 -6
  27. rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
  28. rbx/resources/templates/rbx.h +43 -2
  29. rbx/testing_utils.py +7 -0
  30. rbx/utils.py +104 -6
  31. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/METADATA +1 -1
  32. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/RECORD +35 -40
  33. rbx/grading/judge/sandboxes/isolate.py +0 -695
  34. rbx/grading/judge/sandboxes/timeit.py +0 -358
  35. rbx/grading/judge/test.py +0 -38
  36. rbx/grading/judge/testiso.py +0 -54
  37. rbx/grading/processing_context.py +0 -71
  38. rbx/resources/envs/isolate.rbx.yml +0 -36
  39. rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
  40. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/LICENSE +0 -0
  41. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/WHEEL +0 -0
  42. {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/entry_points.txt +0 -0
@@ -9,17 +9,42 @@ import signal
9
9
  import subprocess
10
10
  import sys
11
11
  import tempfile
12
- from typing import Any, Dict, List, Optional
12
+ import typing
13
+ from typing import List, Optional, Tuple
13
14
 
14
15
  from rbx import utils
15
16
  from rbx.grading.judge.cacher import FileCacher
17
+ from rbx.grading.judge.program import (
18
+ FileLike,
19
+ Program,
20
+ ProgramCode,
21
+ ProgramIO,
22
+ ProgramParams,
23
+ ProgramResult,
24
+ )
16
25
  from rbx.grading.judge.sandbox import (
17
26
  SandboxBase,
27
+ SandboxLog,
18
28
  SandboxParams,
19
29
  )
20
30
 
21
31
  logger = logging.getLogger(__name__)
22
32
 
33
+ TEE_CODE = R"""
34
+ import sys
35
+ c = sys.argv[1]
36
+ new = True
37
+ while True:
38
+ l = sys.stdin.read(1)
39
+ if l=='': break
40
+ sys.stdout.write(l)
41
+ sys.stdout.flush()
42
+ if new: sys.stderr.write(c)
43
+ sys.stderr.write(l)
44
+ sys.stderr.flush()
45
+ new = l=='\n'
46
+ """
47
+
23
48
 
24
49
  class StupidSandbox(SandboxBase):
25
50
  """A stupid sandbox implementation. It has very few features and
@@ -31,16 +56,12 @@ class StupidSandbox(SandboxBase):
31
56
  """
32
57
 
33
58
  exec_num: int
34
- popen: Optional[subprocess.Popen]
35
- returncode: Optional[int]
36
- log: Optional[Dict[str, str]]
37
59
 
38
60
  def __init__(
39
61
  self,
40
62
  file_cacher: Optional[FileCacher] = None,
41
63
  name: Optional[str] = None,
42
64
  temp_dir: Optional[pathlib.Path] = None,
43
- params: Optional[SandboxParams] = None,
44
65
  ):
45
66
  """Initialization.
46
67
 
@@ -49,7 +70,7 @@ class StupidSandbox(SandboxBase):
49
70
  """
50
71
  if not temp_dir:
51
72
  temp_dir = pathlib.Path(tempfile.gettempdir())
52
- SandboxBase.__init__(self, file_cacher, name, temp_dir, params)
73
+ SandboxBase.__init__(self, file_cacher, name, temp_dir)
53
74
 
54
75
  # Make box directory
55
76
  self.initialize()
@@ -68,56 +89,6 @@ class StupidSandbox(SandboxBase):
68
89
  # Box parameters
69
90
  self.chdir = self._path
70
91
 
71
- def get_timeit_executable(self) -> pathlib.Path:
72
- with importlib.resources.as_file(
73
- importlib.resources.files('rbx')
74
- / 'grading'
75
- / 'judge'
76
- / 'sandboxes'
77
- / 'timeit.py'
78
- ) as file:
79
- return file
80
-
81
- def get_timeit_args(self) -> List[str]:
82
- args = []
83
- if self.params.timeout:
84
- timeout_in_s = self.params.timeout / 1000
85
- if self.params.extra_timeout:
86
- timeout_in_s += self.params.extra_timeout / 1000
87
- args.append(f'-t{timeout_in_s:.3f}')
88
- if self.params.wallclock_timeout:
89
- walltimeout_in_s = self.params.wallclock_timeout / 1000
90
- args.append(f'-w{walltimeout_in_s:.3f}')
91
- if self.params.address_space:
92
- args.append(f'-m{self.params.address_space}')
93
- if self.params.fsize:
94
- args.append(f'-f{self.params.fsize}')
95
- if self.chdir:
96
- args.append(f'-c{self.chdir}')
97
- if self.use_pgid() and self.params.pgid is not None:
98
- args.append(f'-g{self.params.pgid}')
99
-
100
- file_args = []
101
- if self.params.stdin_file:
102
- file_args.append(f'-i{self.params.stdin_file}')
103
- if self.params.stdout_file:
104
- file_args.append(f'-o{self.params.stdout_file}')
105
- if self.params.stderr_file:
106
- file_args.append(f'-e{self.params.stderr_file}')
107
- if self.params.reverse_io:
108
- file_args.reverse()
109
- args.extend(file_args)
110
-
111
- if self.params.timeit_dups:
112
- for i, files in self.params.timeit_dups.items():
113
- assert i.lower() in ['di', 'do', 'de']
114
- for file in files:
115
- args.append(f'-{i}{file}')
116
- if self.params.timeit_prefix:
117
- args.append(f'-P{self.params.timeit_prefix}')
118
-
119
- return args
120
-
121
92
  def get_root_path(self) -> pathlib.Path:
122
93
  """Return the toplevel path of the sandbox.
123
94
 
@@ -126,171 +97,153 @@ class StupidSandbox(SandboxBase):
126
97
  """
127
98
  return self._path
128
99
 
129
- def get_execution_time(self) -> Optional[float]:
130
- """Return the time spent in the sandbox.
131
-
132
- return (float): time spent in the sandbox.
133
-
134
- """
135
- if self.log is None:
136
- return None
137
- return float(self.log['time'])
138
-
139
- def get_execution_wall_clock_time(self) -> Optional[float]:
140
- """Return the total time from the start of the sandbox to the
141
- conclusion of the task.
142
-
143
- return (float): total time the sandbox was alive.
144
-
145
- """
146
- if self.log is None:
147
- return None
148
- return float(self.log['time-wall'])
149
-
150
100
  def use_soft_timeout(self) -> bool:
151
101
  return True
152
102
 
153
- def use_pgid(self) -> bool:
154
- return True
155
-
156
- def get_memory_used(self) -> Optional[int]:
157
- """Return the memory used by the sandbox.
158
-
159
- return (int): memory used by the sandbox (in bytes).
160
-
161
- """
162
- if self.log is None:
163
- return None
164
- return int(self.log['mem']) * 1024
165
-
166
- def get_killing_signal(self) -> int:
167
- """Return the signal that killed the sandboxed process.
168
-
169
- return (int): offending signal, or 0.
103
+ def _get_exit_status(self, result: ProgramResult) -> str:
104
+ if ProgramCode.TE in result.program_codes:
105
+ return SandboxBase.EXIT_TERMINATED
106
+ if ProgramCode.WT in result.program_codes:
107
+ return SandboxBase.EXIT_TIMEOUT_WALL
108
+ if ProgramCode.TO in result.program_codes:
109
+ return SandboxBase.EXIT_TIMEOUT
110
+ if ProgramCode.OL in result.program_codes:
111
+ return SandboxBase.EXIT_OUTPUT_LIMIT_EXCEEDED
112
+ if ProgramCode.ML in result.program_codes:
113
+ return SandboxBase.EXIT_MEMORY_LIMIT_EXCEEDED
114
+ if ProgramCode.SG in result.program_codes:
115
+ return SandboxBase.EXIT_SIGNAL
116
+ if ProgramCode.RE in result.program_codes:
117
+ return SandboxBase.EXIT_NONZERO_RETURN
118
+ return SandboxBase.EXIT_OK
119
+
120
+ def _get_io(self, params: SandboxParams, pipe_io: bool = False) -> ProgramIO:
121
+ io = ProgramIO()
122
+ if params.stdin_file and not pipe_io:
123
+ io.input = self.relative_path(params.stdin_file)
124
+ if params.stdout_file and not pipe_io:
125
+ io.output = self.relative_path(params.stdout_file)
126
+ if params.stderr_file:
127
+ io.stderr = self.relative_path(params.stderr_file)
128
+ return io
129
+
130
+ def _get_program_params(self, params: SandboxParams) -> ProgramParams:
131
+ return ProgramParams(
132
+ chdir=self.chdir,
133
+ time_limit=params.timeout / 1000 if params.timeout else None,
134
+ wall_time_limit=params.wallclock_timeout / 1000
135
+ if params.wallclock_timeout
136
+ else None,
137
+ memory_limit=params.address_space,
138
+ fs_limit=params.fsize,
139
+ env=params.set_env,
140
+ io=self._get_io(params),
141
+ )
170
142
 
171
- """
172
- assert self.log is not None
173
- if 'exit-sig' not in self.log:
174
- return 0
175
- return int(self.log['exit-sig'])
143
+ def _get_tee_program_params(self, io: ProgramIO, pgid: int) -> ProgramParams:
144
+ return ProgramParams(
145
+ chdir=self.chdir,
146
+ time_limit=None,
147
+ wall_time_limit=None,
148
+ memory_limit=None,
149
+ io=io,
150
+ pgid=pgid,
151
+ )
176
152
 
177
- def get_status_list(self) -> List[str]:
178
- """Reads the sandbox log file, and set and return the status
179
- of the sandbox.
153
+ def _get_sandbox_log(
154
+ self, result: ProgramResult, params: SandboxParams
155
+ ) -> SandboxLog:
156
+ return SandboxLog(
157
+ params=params.model_copy(deep=True),
158
+ execution_time=result.wall_time,
159
+ memory_used=result.memory_used,
160
+ exitcode=result.exitcode,
161
+ exitstatus=self._get_exit_status(result),
162
+ killing_signal=result.killing_signal,
163
+ other_logs={
164
+ 'program_codes': [code.value for code in result.program_codes],
165
+ 'alarm_msg': result.alarm_msg,
166
+ },
167
+ )
180
168
 
181
- return (list): list of statuses of the sandbox.
169
+ def _needs_teeing(
170
+ self,
171
+ params: SandboxParams,
172
+ interactor_params: SandboxParams,
173
+ merged_capture: Optional[pathlib.Path] = None,
174
+ ) -> bool:
175
+ return (
176
+ params.stdout_file is not None
177
+ or interactor_params.stdout_file is not None
178
+ or merged_capture is not None
179
+ )
182
180
 
183
- """
184
- assert self.log is not None
185
- if 'status' in self.log:
186
- return self.log['status'].split(',')
187
- return []
181
+ def _get_tee_executable(self) -> pathlib.Path:
182
+ with importlib.resources.as_file(
183
+ importlib.resources.files('rbx')
184
+ / 'grading'
185
+ / 'judge'
186
+ / 'sandboxes'
187
+ / 'tee.py'
188
+ ) as file:
189
+ return file
188
190
 
189
- # This sandbox only discriminates between processes terminating
190
- # properly or being killed with a signal; all other exceptional
191
- # conditions (RAM or CPU limitations, ...) result in some signal
192
- # being delivered to the process
193
- def get_exit_status(self) -> str:
194
- """Get information about how the sandbox terminated.
191
+ def _get_tee_command(self, char: str, extra: Optional[str] = None) -> List[str]:
192
+ return [
193
+ sys.executable,
194
+ str(utils.abspath(self._get_tee_executable())),
195
+ char,
196
+ extra or '/dev/null',
197
+ ]
195
198
 
196
- return (string): the main reason why the sandbox terminated.
199
+ def _get_tee_program(
200
+ self,
201
+ char: str,
202
+ stdin: FileLike,
203
+ stdout: FileLike,
204
+ pgid: int,
205
+ capture: Optional[pathlib.Path] = None,
206
+ merged_capture: Optional[pathlib.Path] = None,
207
+ ) -> Program:
208
+ io = ProgramIO(input=stdin, output=stdout, stderr=subprocess.DEVNULL)
209
+ if merged_capture:
210
+ io.stderr = self.relative_path(merged_capture).open('ab')
211
+ return Program(
212
+ self._get_tee_command(
213
+ char, str(self.relative_path(capture)) if capture else None
214
+ ),
215
+ self._get_tee_program_params(io, pgid),
216
+ )
197
217
 
198
- """
199
- if self.returncode is not None and not self.translate_box_exitcode(
200
- self.returncode
201
- ):
202
- return self.EXIT_SANDBOX_ERROR
203
- status_list = self.get_status_list()
204
- if 'TE' in status_list:
205
- return self.EXIT_TERMINATED
206
- if 'WT' in status_list:
207
- return self.EXIT_TIMEOUT_WALL
208
- if 'TO' in status_list:
209
- return self.EXIT_TIMEOUT
210
- if 'OL' in status_list:
211
- return self.EXIT_OUTPUT_LIMIT_EXCEEDED
212
- if 'ML' in status_list:
213
- return self.EXIT_MEMORY_LIMIT_EXCEEDED
214
- if 'SG' in status_list:
215
- return self.EXIT_SIGNAL
216
- if 'RE' in status_list:
217
- return self.EXIT_NONZERO_RETURN
218
- return self.EXIT_OK
219
-
220
- def get_exit_code(self) -> int:
221
- """Return the exit code of the sandboxed process.
222
-
223
- return (float): exitcode, or 0.
218
+ def _get_pathlike_stdout(self, io: ProgramIO) -> Optional[pathlib.Path]:
219
+ if isinstance(io.output, str) or isinstance(io.output, pathlib.Path):
220
+ return pathlib.Path(io.output)
221
+ return None
224
222
 
225
- """
226
- assert self.log is not None
227
- return int(self.log['exit-code'])
223
+ def run(self, command: List[str], params: SandboxParams) -> SandboxLog:
224
+ self.exec_num += 1
228
225
 
229
- def get_detailed_logs(self) -> str:
230
- return str(self.log)
226
+ logger.debug(
227
+ "Executing program in sandbox with command: `%s'.", ' '.join(command)
228
+ )
229
+ with open(
230
+ self.relative_path(self.cmd_file), 'at', encoding='utf-8'
231
+ ) as commands:
232
+ commands.write('%s\n' % command)
231
233
 
232
- def get_human_exit_description(self) -> str:
233
- """Get the status of the sandbox and return a human-readable
234
- string describing it.
234
+ program = Program(command, self._get_program_params(params))
235
+ result = program.wait()
235
236
 
236
- return (string): human-readable explaination of why the
237
- sandbox terminated.
237
+ return self._get_sandbox_log(result, params)
238
238
 
239
- """
240
- status = self.get_exit_status()
241
- if status == self.EXIT_OK:
242
- return (
243
- 'Execution successfully finished (with exit code %d)'
244
- % self.get_exit_code()
245
- )
246
- elif status == self.EXIT_SANDBOX_ERROR:
247
- return 'Execution failed because of sandbox error'
248
- elif status == self.EXIT_TIMEOUT:
249
- return 'Execution timed out'
250
- elif status == self.EXIT_TIMEOUT_WALL:
251
- return 'Execution timed out (wall clock limit exceeded)'
252
- elif status == self.EXIT_SIGNAL:
253
- return 'Execution killed with signal %s' % self.get_killing_signal()
254
- elif status == self.EXIT_NONZERO_RETURN:
255
- return 'Execution failed because the return code was nonzero'
256
- elif status == self.EXIT_OUTPUT_LIMIT_EXCEEDED:
257
- return 'Execution exceeded output limit'
258
- return ''
259
-
260
- def get_current_log_name(self) -> pathlib.Path:
261
- return pathlib.Path(f'logs.{self.exec_num}')
262
-
263
- def hydrate_logs(self):
264
- self.log = None
265
- if not self.relative_path(self.get_current_log_name()).is_file():
266
- return
267
- self.log = {}
268
- raw_log = self.get_file_to_string(self.get_current_log_name(), maxlen=None)
269
- for line in raw_log.splitlines():
270
- items = line.split(':', 1)
271
- if len(items) != 2:
272
- continue
273
- key, value = items
274
- self.log[key] = value.strip()
275
-
276
- def execute_without_std(
239
+ def run_communication(
277
240
  self,
278
241
  command: List[str],
279
- ) -> bool:
280
- """Execute the given command in the sandbox using
281
- subprocess.Popen and discarding standard input, output and
282
- error. More specifically, the standard input gets closed just
283
- after the execution has started; standard output and error are
284
- read until the end, in a way that prevents the execution from
285
- being blocked because of insufficient buffering.
286
-
287
- command ([string]): executable filename and arguments of the
288
- command.
289
-
290
- return (bool): True if the sandbox didn't report errors
291
- (caused by the sandbox itself), False otherwise
292
-
293
- """
242
+ params: SandboxParams,
243
+ interactor_command: List[str],
244
+ interactor_params: SandboxParams,
245
+ merged_capture: Optional[pathlib.Path] = None,
246
+ ) -> Tuple[SandboxLog, SandboxLog]:
294
247
  self.exec_num += 1
295
248
 
296
249
  logger.debug(
@@ -301,43 +254,84 @@ class StupidSandbox(SandboxBase):
301
254
  ) as commands:
302
255
  commands.write('%s\n' % command)
303
256
 
304
- real_command = (
305
- [
306
- sys.executable,
307
- str(utils.abspath(self.get_timeit_executable())),
308
- str(utils.abspath(self.relative_path(self.get_current_log_name()))),
309
- ]
310
- + self.get_timeit_args()
311
- + command
257
+ interactor_program_params = self._get_program_params(interactor_params)
258
+ interactor_program_params.io = self._get_io(interactor_params, pipe_io=True)
259
+ interactor = Program(
260
+ interactor_command,
261
+ interactor_program_params,
312
262
  )
313
-
314
- self.clear_pid()
315
- with subprocess.Popen(
316
- real_command,
317
- stdin=subprocess.PIPE,
318
- stdout=subprocess.PIPE,
319
- stderr=subprocess.STDOUT,
320
- env={**os.environ, **self.params.set_env},
321
- ) as p:
322
- self.set_pid(p.pid)
323
- try:
324
- self.returncode = p.wait()
325
- except Exception:
326
- p.kill()
327
- raise
328
- self.hydrate_logs()
329
- return self.translate_box_exitcode(self.returncode)
330
-
331
- def translate_box_exitcode(self, exitcode: int) -> bool:
332
- # SIGALRM can be safely ignored, just in case it leaks away. SIGTERM also.
333
- if self.log is None:
334
- return False
335
- if 'TE' in self.get_status_list():
336
- return True
337
- return super().translate_box_exitcode(exitcode) or -exitcode == signal.SIGALRM
338
-
339
- def debug_message(self) -> Any:
340
- return f'returncode = {self.returncode}\nlogs = {self.log}\ntimeit_args = {self.get_timeit_args()}'
263
+ assert interactor.pipes.output is not None
264
+ assert interactor.pipes.input is not None
265
+ solution_input_pipe = interactor.pipes.output
266
+ solution_output_pipe = interactor.pipes.input
267
+
268
+ group_id = os.getpgid(interactor.pid)
269
+ should_tee = self._needs_teeing(params, interactor_params, merged_capture)
270
+
271
+ if should_tee:
272
+ if merged_capture:
273
+ self.create_file_from_string(merged_capture, '<\n>\n', override=True)
274
+
275
+ solution_tee = self._get_tee_program(
276
+ '>',
277
+ stdin=subprocess.PIPE,
278
+ stdout=interactor.pipes.input,
279
+ capture=self._get_pathlike_stdout(self._get_io(params)),
280
+ merged_capture=merged_capture,
281
+ pgid=group_id,
282
+ )
283
+ interactor_tee = self._get_tee_program(
284
+ '<',
285
+ stdin=interactor.pipes.output,
286
+ stdout=subprocess.PIPE,
287
+ capture=self._get_pathlike_stdout(self._get_io(interactor_params)),
288
+ merged_capture=merged_capture,
289
+ pgid=group_id,
290
+ )
291
+ assert solution_tee.pipes.input is not None
292
+ assert interactor_tee.pipes.output is not None
293
+ solution_input_pipe = interactor_tee.pipes.output
294
+ solution_output_pipe = solution_tee.pipes.input
295
+
296
+ program_params = self._get_program_params(params)
297
+ program_params.io = self._get_io(params, pipe_io=True)
298
+ program_params.io.input = solution_input_pipe
299
+ program_params.io.output = solution_output_pipe
300
+ program_params.pgid = group_id
301
+ program = Program(command, program_params)
302
+
303
+ results: List[Optional[SandboxLog]] = [None, None]
304
+
305
+ for idx in range(4 if should_tee else 2):
306
+ pid, status, ru = os.wait4(-group_id, 0)
307
+
308
+ if pid == interactor.pid:
309
+ program_result = interactor.process_exit(status, ru)
310
+ results[1] = self._get_sandbox_log(program_result, interactor_params)
311
+ results[1].exit_index = idx
312
+
313
+ interactor.pipes.output.close()
314
+ if should_tee:
315
+ assert interactor_tee.pipes.output is not None
316
+ interactor_tee.pipes.output.close()
317
+
318
+ if idx == 0 and program_result.exitcode != 0:
319
+ os.killpg(group_id, signal.SIGKILL)
320
+ elif pid == program.pid:
321
+ program_result = program.process_exit(status, ru)
322
+ results[0] = self._get_sandbox_log(program_result, params)
323
+ results[0].exit_index = idx
324
+
325
+ interactor.pipes.input.close()
326
+ if should_tee:
327
+ assert solution_tee.pipes.input is not None
328
+ solution_tee.pipes.input.close()
329
+ elif should_tee and (pid in (solution_tee.pid, interactor_tee.pid)):
330
+ pass
331
+ else:
332
+ raise RuntimeError(f'Unknown pid: {pid}')
333
+
334
+ return typing.cast(Tuple[SandboxLog, SandboxLog], tuple(results))
341
335
 
342
336
  def cleanup(self, delete=False):
343
337
  """See Sandbox.cleanup()."""
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Forked from https://github.com/RagnarGrootKoerkamp/BAPCtools/blob/master/bin/interactive.py
4
+
5
+ Takes a character and a file name as arguments.
6
+
7
+ Reads from stdin, and writes to stdout and stderr, with the given character prepended to
8
+ every line read from stdin.
9
+ """
10
+
11
+ import sys
12
+
13
+ c = sys.argv[1]
14
+ extra = sys.argv[2]
15
+
16
+ new = True
17
+
18
+ with open(extra, 'w') as f:
19
+ while True:
20
+ rd = sys.stdin.read(1)
21
+ if rd == '':
22
+ break
23
+ sys.stdout.write(rd)
24
+ sys.stdout.flush()
25
+ if new:
26
+ sys.stderr.write(c)
27
+ sys.stderr.write(rd)
28
+ sys.stderr.flush()
29
+
30
+ f.write(rd)
31
+ new = rd == '\n'
@@ -277,8 +277,14 @@ class FilesystemStorage(Storage):
277
277
  return None
278
278
 
279
279
  # Create a temporary file in the same directory
280
+ # Use only the basename for the suffix to avoid issues with subdirectories
281
+ filename_basename = pathlib.Path(filename).name
280
282
  temp_file = tempfile.NamedTemporaryFile(
281
- 'wb', delete=False, prefix='.tmp.', suffix=filename, dir=self.path
283
+ 'wb',
284
+ delete=False,
285
+ prefix='.tmp.',
286
+ suffix=f'.{filename_basename}',
287
+ dir=self.path,
282
288
  )
283
289
  metadata: Dict[str, Optional[BaseModel]] = {'compression': None}
284
290
  if self.compress or grading_context.should_compress():