22import signal
33import subprocess
44import sys
5+ from pathlib import Path
56from typing import Any , Literal
67from unittest .mock import MagicMock , patch
78
@@ -490,7 +491,9 @@ def mock_unregister(module):
490491 assert len (unregistered ) == 1
491492 assert unregistered [0 ] == mock_module
492493
493- def test_pickle_local_modules_handles_import_errors (self , caplog ):
494+ def test_pickle_local_modules_handles_import_errors (
495+ self , caplog : pytest .LogCaptureFixture
496+ ):
494497 """Test that import errors are handled gracefully."""
495498
496499 @flow
@@ -506,3 +509,98 @@ def test_flow():
506509
507510 # Check that a debug message was logged about the failure
508511 assert "Failed to register module nonexistent_module" in caplog .text
512+
513+ def test_discover_deeply_nested_local_dependencies (self , tmp_path : Path ):
514+ """Test that local dependencies are discovered recursively through multiple levels.
515+
516+ This tests the scenario where:
517+ - flow_module imports module_b
518+ - module_b imports module_c
519+ - module_c imports module_d
520+
521+ All modules should be discovered, including module_d which is 3 levels deep.
522+ """
523+ # Create temporary package structure with deep nesting
524+ package_root = tmp_path / "test_packages"
525+ package_root .mkdir ()
526+
527+ # Create flow_module package
528+ flow_pkg = package_root / "flow_module"
529+ flow_pkg .mkdir ()
530+ (flow_pkg / "__init__.py" ).write_text ("" )
531+
532+ # Create module_b package
533+ module_b_pkg = package_root / "module_b"
534+ module_b_pkg .mkdir ()
535+ (module_b_pkg / "__init__.py" ).write_text ("" )
536+
537+ # Create module_c package
538+ module_c_pkg = package_root / "module_c"
539+ module_c_pkg .mkdir ()
540+ (module_c_pkg / "__init__.py" ).write_text ("" )
541+
542+ # Create module_d package (deepest level)
543+ module_d_pkg = package_root / "module_d"
544+ module_d_pkg .mkdir ()
545+ (module_d_pkg / "__init__.py" ).write_text ("" )
546+
547+ # Create module_d with a simple function
548+ (module_d_pkg / "utils.py" ).write_text ("""
549+ def function_d():
550+ return "d"
551+ """ )
552+
553+ # Create module_c that imports from module_d
554+ (module_c_pkg / "utils.py" ).write_text ("""
555+ from module_d.utils import function_d
556+
557+ def function_c():
558+ return function_d()
559+ """ )
560+
561+ # Create module_b that imports from module_c
562+ (module_b_pkg / "utils.py" ).write_text ("""
563+ from module_c.utils import function_c
564+
565+ def function_b():
566+ return function_c()
567+ """ )
568+
569+ # Create flow_module that imports from module_b
570+ (flow_pkg / "my_flow.py" ).write_text ("""
571+ from module_b.utils import function_b
572+ from prefect import flow
573+
574+ @flow
575+ def test_flow():
576+ return function_b()
577+ """ )
578+
579+ # Add package_root to sys.path so modules can be imported
580+ sys .path .insert (0 , str (package_root ))
581+
582+ try :
583+ # Import the flow module and get the flow
584+ import flow_module .my_flow
585+
586+ flow_obj = flow_module .my_flow .test_flow
587+
588+ # Discover dependencies
589+ deps = _discover_local_dependencies (flow_obj )
590+
591+ # All four modules should be discovered
592+ assert "flow_module.my_flow" in deps , (
593+ "Flow module itself should be discovered"
594+ )
595+ assert "module_b.utils" in deps , "First-level import should be discovered"
596+ assert "module_c.utils" in deps , "Second-level import should be discovered"
597+ assert "module_d.utils" in deps , "Third-level import should be discovered"
598+
599+ finally :
600+ # Clean up sys.path and sys.modules
601+ sys .path .remove (str (package_root ))
602+ for module in list (sys .modules .keys ()):
603+ if module .startswith (
604+ ("flow_module" , "module_b" , "module_c" , "module_d" )
605+ ):
606+ del sys .modules [module ]
0 commit comments