@@ -9,7 +9,8 @@ use rand::Rng;
9
9
use sha2:: Sha256 ;
10
10
use std:: collections:: { HashMap , HashSet } ;
11
11
use std:: env;
12
- use std:: io:: BufReader ;
12
+ use std:: ffi:: OsStr ;
13
+ use std:: io:: { BufReader , Read , Seek } ;
13
14
use std:: path:: { Path , PathBuf } ;
14
15
use std:: time:: Duration ;
15
16
use tmc_langs_framework:: {
@@ -23,6 +24,7 @@ use tmc_langs_util::{
23
24
parse_util,
24
25
} ;
25
26
use walkdir:: WalkDir ;
27
+ use zip:: ZipArchive ;
26
28
27
29
pub struct Python3Plugin { }
28
30
@@ -343,6 +345,60 @@ impl LanguagePlugin for Python3Plugin {
343
345
}
344
346
}
345
347
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
+
346
402
/// Checks if the directory has one of setup.py, requirements.txt., test/__init__.py, or tmc/__main__.py
347
403
fn is_exercise_type_correct ( path : & Path ) -> bool {
348
404
let setup = path. join ( "setup.py" ) ;
@@ -769,6 +825,29 @@ class TestErroring(unittest.TestCase):
769
825
assert ! ( res. is_err( ) ) ;
770
826
}
771
827
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
+
772
851
#[ test]
773
852
fn doesnt_give_points_unless_all_relevant_exercises_pass ( ) {
774
853
init ( ) ;
0 commit comments