2424 get_processing_dependencies ,
2525 get_processing_code_hash ,
2626 get_training_code_hash ,
27+ get_code_hash ,
2728 validate_step_args_input ,
2829 override_pipeline_parameter_var ,
2930 trim_request_dict ,
@@ -285,6 +286,20 @@ def test_get_training_code_hash_with_source_dir(self):
285286 assert len (result_with_deps ) == 64
286287 assert result_no_deps != result_with_deps
287288
289+ def test_get_training_code_hash_with_source_dir_and_none_dependencies (self ):
290+ """Test get_training_code_hash with source_dir and None dependencies does not raise TypeError"""
291+ with tempfile .TemporaryDirectory () as temp_dir :
292+ entry_file = Path (temp_dir , "train.py" )
293+ entry_file .write_text ("print('training')" )
294+
295+ # This should NOT raise TypeError: can only concatenate list (not "NoneType") to list
296+ result = get_training_code_hash (
297+ entry_point = str (entry_file ), source_dir = temp_dir , dependencies = None
298+ )
299+
300+ assert result is not None
301+ assert len (result ) == 64
302+
288303 def test_get_training_code_hash_entry_point_only (self ):
289304 """Test get_training_code_hash with entry_point only"""
290305 with tempfile .TemporaryDirectory () as temp_dir :
@@ -308,6 +323,20 @@ def test_get_training_code_hash_entry_point_only(self):
308323 assert len (result_with_deps ) == 64
309324 assert result_no_deps != result_with_deps
310325
326+ def test_get_training_code_hash_entry_point_only_and_none_dependencies (self ):
327+ """Test get_training_code_hash with entry_point only and None dependencies does not raise TypeError"""
328+ with tempfile .TemporaryDirectory () as temp_dir :
329+ entry_file = Path (temp_dir , "train.py" )
330+ entry_file .write_text ("print('training')" )
331+
332+ # This should NOT raise TypeError: can only concatenate list (not "NoneType") to list
333+ result = get_training_code_hash (
334+ entry_point = str (entry_file ), source_dir = None , dependencies = None
335+ )
336+
337+ assert result is not None
338+ assert len (result ) == 64
339+
311340 def test_get_training_code_hash_s3_uri (self ):
312341 """Test get_training_code_hash with S3 URI returns None"""
313342 result = get_training_code_hash (
@@ -325,6 +354,77 @@ def test_get_training_code_hash_pipeline_variable(self):
325354
326355 assert result is None
327356
357+ @pytest .mark .skip (reason = "Requires sagemaker-mlops module which is not installed in sagemaker-core tests" )
358+ def test_get_code_hash_with_training_step_and_no_requirements (self ):
359+ """Test get_code_hash with TrainingStep where SourceCode has requirements=None"""
360+ from sagemaker .mlops .workflow .steps import TrainingStep
361+
362+ with tempfile .TemporaryDirectory () as temp_dir :
363+ entry_file = Path (temp_dir , "train.py" )
364+ entry_file .write_text ("print('training')" )
365+
366+ mock_source_code = Mock ()
367+ mock_source_code .source_dir = temp_dir
368+ mock_source_code .requirements = None # This is the key: requirements is None
369+ mock_source_code .entry_script = str (entry_file )
370+
371+ mock_model_trainer = Mock ()
372+ mock_model_trainer .source_code = mock_source_code
373+
374+ mock_step_args = Mock ()
375+ mock_step_args .func_args = [mock_model_trainer ]
376+
377+ mock_step = Mock (spec = TrainingStep )
378+ mock_step .step_args = mock_step_args
379+
380+ # This should NOT raise TypeError
381+ result = get_code_hash (mock_step )
382+
383+ assert result is not None
384+ assert len (result ) == 64
385+
386+ @pytest .mark .skip (reason = "Requires sagemaker-mlops module which is not installed in sagemaker-core tests" )
387+ def test_get_code_hash_with_training_step_and_requirements (self ):
388+ """Test get_code_hash with TrainingStep where SourceCode has valid requirements"""
389+ from sagemaker .mlops .workflow .steps import TrainingStep
390+
391+ with tempfile .TemporaryDirectory () as temp_dir :
392+ entry_file = Path (temp_dir , "train.py" )
393+ entry_file .write_text ("print('training')" )
394+ requirements_file = Path (temp_dir , "requirements.txt" )
395+ requirements_file .write_text ("numpy==1.21.0" )
396+
397+ mock_source_code_no_req = Mock ()
398+ mock_source_code_no_req .source_dir = temp_dir
399+ mock_source_code_no_req .requirements = None
400+ mock_source_code_no_req .entry_script = str (entry_file )
401+
402+ mock_source_code_with_req = Mock ()
403+ mock_source_code_with_req .source_dir = temp_dir
404+ mock_source_code_with_req .requirements = str (requirements_file )
405+ mock_source_code_with_req .entry_script = str (entry_file )
406+
407+ mock_model_trainer_no_req = Mock ()
408+ mock_model_trainer_no_req .source_code = mock_source_code_no_req
409+
410+ mock_model_trainer_with_req = Mock ()
411+ mock_model_trainer_with_req .source_code = mock_source_code_with_req
412+
413+ mock_step_no_req = Mock (spec = TrainingStep )
414+ mock_step_no_req .step_args = Mock ()
415+ mock_step_no_req .step_args .func_args = [mock_model_trainer_no_req ]
416+
417+ mock_step_with_req = Mock (spec = TrainingStep )
418+ mock_step_with_req .step_args = Mock ()
419+ mock_step_with_req .step_args .func_args = [mock_model_trainer_with_req ]
420+
421+ result_no_req = get_code_hash (mock_step_no_req )
422+ result_with_req = get_code_hash (mock_step_with_req )
423+
424+ assert result_no_req is not None
425+ assert result_with_req is not None
426+ assert result_no_req != result_with_req
427+
328428 def test_validate_step_args_input_valid (self ):
329429 """Test validate_step_args_input with valid input"""
330430 step_args = _StepArguments (
0 commit comments