Coverage for src / puzzletree / cli / register.py: 84.85%

27 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-12 20:35 +0000

1"""Command registration module for dynamically discovering and registering CLI commands.""" 

2 

3import importlib 

4from pathlib import Path 

5 

6from typer import Typer 

7 

8from puzzletree.utils.logging import get_logger_console 

9 

10 

11def _register_commands(app: Typer, path: Path | None = None) -> None: 

12 """Dynamically discover and register CLI commands from submodules in the commands directory. 

13 

14 Scans all subdirectories in the commands directory and registers any that have an 'app' 

15 attribute defined in their __init__.py file. 

16 """ 

17 logger, _ = get_logger_console() 

18 

19 # Get the cli directory path 

20 cli_dir = Path(__file__).parent / "commands" if path is None else path 

21 

22 # Find all subdirectories in the commands directory that have __init__.py 

23 command_modules = [ 

24 d.name for d in cli_dir.iterdir() if d.is_dir() and (d / "__init__.py").exists() and d.name != "__pycache__" 

25 ] 

26 

27 registered_commands = set() 

28 

29 # Import and register commands from each submodule 

30 for module_name in command_modules: 

31 logger.debug(f"Importing submodule: {module_name}") 

32 

33 try: 

34 # Dynamically import the submodule (its __init__.py will be loaded) 

35 module = importlib.import_module(f"puzzletree.cli.commands.{module_name}") 

36 

37 # Check if the module has an 'app' attribute 

38 if not hasattr(module, "app"): 

39 logger.debug(f"Submodule '{module_name}' does not have an 'app' attribute. Skipping.") 

40 

41 continue 

42 

43 # Get the app from the module 

44 sub_command = module.app 

45 

46 if module_name in registered_commands: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true

47 logger.warning( 

48 f"Duplicate command name '{module_name}' found in submodule '{module_name}'. Skipping registration.", 

49 ) 

50 continue 

51 

52 app.add_typer(sub_command, name=module_name) 

53 registered_commands.add(module_name) 

54 logger.debug(f"Registered command '{module_name}' from submodule '{module_name}'") 

55 

56 except ImportError as e: 

57 logger.warning(f"Failed to import submodule '{module_name}': {e}. Skipping.") 

58 except (AttributeError, TypeError, ValueError) as e: 

59 logger.warning(f"Error processing submodule '{module_name}': {e}. Skipping.")