@@ -37,6 +37,19 @@ def dummy_update_rule(self, df):
3737 pass
3838
3939
40+ class InlinePool :
41+ """Small Pool stand-in that executes map calls in process for unit tests."""
42+
43+ def __enter__ (self ):
44+ return self
45+
46+ def __exit__ (self , exc_type , exc , tb ):
47+ return False
48+
49+ def map (self , func , iterable ):
50+ return [func (item ) for item in iterable ]
51+
52+
4053class TestBootstrapParameters :
4154 """Test class for BootstrapParameters dataclass."""
4255
@@ -818,6 +831,47 @@ def test_bootstrap_with_list_input(self):
818831 result = Bootstrap ([df1 , df2 ], ['group' ], [params ])
819832
820833 assert isinstance (result , pd .DataFrame )
834+
835+ def test_bootstrap_with_list_of_pickle_paths (self ):
836+ """Test Bootstrap function with a list of serialized DataFrames."""
837+ with tempfile .NamedTemporaryFile (suffix = '.pkl' , delete = False ) as tmp_file1 :
838+ df1 = pd .DataFrame ({
839+ 'energy' : [100 , 80 ],
840+ 'time' : [10 , 15 ],
841+ 'group' : ['A' , 'A' ]
842+ })
843+ df1 .to_pickle (tmp_file1 .name )
844+
845+ with tempfile .NamedTemporaryFile (suffix = '.pkl' , delete = False ) as tmp_file2 :
846+ df2 = pd .DataFrame ({
847+ 'energy' : [120 , 90 ],
848+ 'time' : [8 , 12 ],
849+ 'group' : ['B' , 'B' ]
850+ })
851+ df2 .to_pickle (tmp_file2 .name )
852+
853+ shared_args = {'response_col' : 'energy' , 'resource_col' : 'time' }
854+ params = BootstrapParameters (shared_args = shared_args , update_rule = dummy_update_rule , downsample = 1 )
855+
856+ try :
857+ with patch ('bootstrap.Pool' ) as mock_pool :
858+ mock_result = pd .DataFrame ({'result' : [1 ], 'group' : ['A' ], 'boots' : [1 ]})
859+ mock_pool .return_value .__enter__ .return_value .map .return_value = [mock_result ]
860+
861+ result = Bootstrap ([tmp_file1 .name , tmp_file2 .name ], ['group' ], [params ])
862+ finally :
863+ os .unlink (tmp_file1 .name )
864+ os .unlink (tmp_file2 .name )
865+
866+ assert isinstance (result , pd .DataFrame )
867+
868+ def test_bootstrap_rejects_unsupported_input_type (self ):
869+ """Test Bootstrap fails clearly for unsupported input objects."""
870+ shared_args = {'response_col' : 'energy' , 'resource_col' : 'time' }
871+ params = BootstrapParameters (shared_args = shared_args , update_rule = dummy_update_rule )
872+
873+ with pytest .raises (TypeError , match = "Expected DataFrame" ):
874+ Bootstrap (42 , ['group' ], [params ])
821875
822876 def test_bootstrap_with_progress_dir (self ):
823877 """Test Bootstrap function with progress directory."""
@@ -843,6 +897,69 @@ def test_bootstrap_with_progress_dir(self):
843897
844898 assert isinstance (result , pd .DataFrame )
845899
900+ def test_bootstrap_executes_grouped_work_in_process (self , monkeypatch ):
901+ """Test Bootstrap's grouped apply path without multiprocessing."""
902+ from pandas .core .groupby .generic import DataFrameGroupBy
903+
904+ monkeypatch .setattr (
905+ DataFrameGroupBy ,
906+ 'progress_apply' ,
907+ DataFrameGroupBy .apply ,
908+ )
909+
910+ df = pd .DataFrame ({
911+ 'energy' : [100 , 80 , 120 , 90 ],
912+ 'time' : [10 , 15 , 8 , 12 ],
913+ 'group' : ['A' , 'A' , 'B' , 'B' ]
914+ })
915+
916+ shared_args = {'response_col' : 'energy' , 'resource_col' : 'time' }
917+ params = BootstrapParameters (
918+ shared_args = shared_args ,
919+ update_rule = dummy_update_rule ,
920+ downsample = 3 ,
921+ )
922+
923+ def grouped_result (group_df , bs_params ):
924+ return pd .DataFrame ({'rows_seen' : [len (group_df )]})
925+
926+ with patch ('bootstrap.Pool' , InlinePool ), patch (
927+ 'bootstrap.BootstrapSingle' , side_effect = grouped_result
928+ ):
929+ result = Bootstrap (df , ['group' ], [params ])
930+
931+ assert set (result ['group' ]) == {'A' , 'B' }
932+ assert result ['rows_seen' ].tolist () == [2 , 2 ]
933+ assert result ['boots' ].eq (3 ).all ()
934+
935+ def test_bootstrap_uses_progress_file_without_recomputing (self ):
936+ """Test Bootstrap loads cached progress files before grouped work."""
937+ with tempfile .TemporaryDirectory () as temp_dir :
938+ df = pd .DataFrame ({
939+ 'energy' : [100 , 80 ],
940+ 'time' : [10 , 15 ],
941+ 'group' : ['A' , 'A' ]
942+ })
943+ shared_args = {'response_col' : 'energy' , 'resource_col' : 'time' }
944+ params = BootstrapParameters (
945+ shared_args = shared_args ,
946+ update_rule = dummy_update_rule ,
947+ downsample = 4 ,
948+ )
949+ cached = pd .DataFrame ({'loaded' : [True ], 'boots' : [4 ]})
950+ cached .to_pickle (
951+ os .path .join (temp_dir , 'bootstrapped_results_boots=4.pkl' )
952+ )
953+
954+ with patch ('bootstrap.Pool' , InlinePool ), patch (
955+ 'bootstrap.BootstrapSingle'
956+ ) as mock_bootstrap_single :
957+ result = Bootstrap (df , ['group' ], [params ], progress_dir = temp_dir )
958+
959+ mock_bootstrap_single .assert_not_called ()
960+ assert result ['loaded' ].tolist () == [True ]
961+ assert result ['boots' ].tolist () == [4 ]
962+
846963
847964class TestBootstrapReduceMem :
848965 """Test class for Bootstrap_reduce_mem function."""
@@ -977,4 +1094,4 @@ def test_init_bootstrap_single_row(self):
9771094
9781095
9791096if __name__ == "__main__" :
980- pytest .main ([__file__ ])
1097+ pytest .main ([__file__ ])
0 commit comments