Skip to content

Commit 75c7036

Browse files
authored
Merge pull request #197 from rage/old-submission-fix
detect directories with ipynb files as python project roots
2 parents e35b044 + 4fa8ff0 commit 75c7036

File tree

3 files changed

+88
-4
lines changed

3 files changed

+88
-4
lines changed

plugins/python3/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ serde_json = "1"
2020
sha2 = "0.9"
2121
thiserror = "1"
2222
walkdir = "2"
23+
zip = "0.5"
2324

2425
[dev-dependencies]
2526
simple_logger = "1"
2627
tempfile = "3"
27-
zip = "0.5"

plugins/python3/src/plugin.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use rand::Rng;
99
use sha2::Sha256;
1010
use std::collections::{HashMap, HashSet};
1111
use std::env;
12-
use std::io::BufReader;
12+
use std::ffi::OsStr;
13+
use std::io::{BufReader, Read, Seek};
1314
use std::path::{Path, PathBuf};
1415
use std::time::Duration;
1516
use tmc_langs_framework::{
@@ -23,6 +24,7 @@ use tmc_langs_util::{
2324
parse_util,
2425
};
2526
use walkdir::WalkDir;
27+
use zip::ZipArchive;
2628

2729
pub struct Python3Plugin {}
2830

@@ -343,6 +345,60 @@ impl LanguagePlugin for Python3Plugin {
343345
}
344346
}
345347

348+
/// Searches the zip for a valid project directory.
349+
/// Note that the returned path may not actually have an entry in the zip.
350+
/// Searches for either a src directory, or the most shallow directory containing an .ipynb file.
351+
/// Ignores everything in a __MACOSX directory.
352+
fn find_project_dir_in_zip<R: Read + Seek>(
353+
zip_archive: &mut ZipArchive<R>,
354+
) -> Result<PathBuf, TmcError> {
355+
let mut shallowest_ipynb_path: Option<PathBuf> = None;
356+
357+
for i in 0..zip_archive.len() {
358+
// zips don't necessarily contain entries for intermediate directories,
359+
// so we need to check every path for src
360+
let file = zip_archive.by_index(i)?;
361+
let file_path = Path::new(file.name());
362+
363+
// todo: do in one pass somehow
364+
if file_path.components().any(|c| c.as_os_str() == "src") {
365+
let path: PathBuf = file_path
366+
.components()
367+
.take_while(|c| c.as_os_str() != "src")
368+
.collect();
369+
370+
if path.components().any(|p| p.as_os_str() == "__MACOSX") {
371+
continue;
372+
}
373+
return Ok(path);
374+
}
375+
if file_path.extension() == Some(OsStr::new("ipynb")) {
376+
if let Some(ipynb_path) = shallowest_ipynb_path.as_mut() {
377+
// make sure we maintain the shallowest ipynb path in the archive
378+
if ipynb_path.components().count() > file_path.components().count() {
379+
*ipynb_path = file_path
380+
.parent()
381+
.map(PathBuf::from)
382+
.unwrap_or_else(|| PathBuf::from(""));
383+
}
384+
} else {
385+
shallowest_ipynb_path = Some(
386+
file_path
387+
.parent()
388+
.map(PathBuf::from)
389+
.unwrap_or_else(|| PathBuf::from("")),
390+
);
391+
}
392+
}
393+
}
394+
if let Some(ipynb_path) = shallowest_ipynb_path {
395+
// no src found, use shallowest ipynb path
396+
Ok(ipynb_path)
397+
} else {
398+
Err(TmcError::NoProjectDirInZip)
399+
}
400+
}
401+
346402
/// Checks if the directory has one of setup.py, requirements.txt., test/__init__.py, or tmc/__main__.py
347403
fn is_exercise_type_correct(path: &Path) -> bool {
348404
let setup = path.join("setup.py");
@@ -769,6 +825,29 @@ class TestErroring(unittest.TestCase):
769825
assert!(res.is_err());
770826
}
771827

828+
#[test]
829+
fn finds_project_dir_from_ipynb() {
830+
init();
831+
832+
let temp_dir = tempfile::tempdir().unwrap();
833+
file_to(&temp_dir, "inner/file.ipynb", "");
834+
file_to(&temp_dir, "file.ipynb", "");
835+
836+
let bytes = dir_to_zip(&temp_dir);
837+
let mut zip = ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
838+
let dir = Python3Plugin::find_project_dir_in_zip(&mut zip).unwrap();
839+
assert_eq!(dir, Path::new(""));
840+
841+
let temp_dir = tempfile::tempdir().unwrap();
842+
file_to(&temp_dir, "dir/inner/file.ipynb", "");
843+
file_to(&temp_dir, "dir/file.ipynb", "");
844+
845+
let bytes = dir_to_zip(&temp_dir);
846+
let mut zip = ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
847+
let dir = Python3Plugin::find_project_dir_in_zip(&mut zip).unwrap();
848+
assert_eq!(dir, Path::new("dir"));
849+
}
850+
772851
#[test]
773852
fn doesnt_give_points_unless_all_relevant_exercises_pass() {
774853
init();

tmc-langs/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,15 @@ pub fn download_or_update_course_exercises(
393393

394394
let mut buf = vec![];
395395
client.download_old_submission(*submission_id, &mut buf)?;
396-
plugin.extract_student_files(
396+
if let Err(err) = plugin.extract_student_files(
397397
Cursor::new(buf),
398398
&download_target.target.path,
399-
)?;
399+
) {
400+
log::error!(
401+
"Something went wrong when downloading old submission: {}",
402+
err
403+
);
404+
}
400405
}
401406
}
402407
// download successful, save to course config

0 commit comments

Comments
 (0)