Coverage for dataexcept/__main__.py: 95%

40 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-09-03 20:46 +0000

1"""Command line interface for the DataExcept package.""" 

2 

3from __future__ import annotations 

4 

5import argparse 

6import pkgutil 

7import sys 

8from importlib import import_module 

9from types import ModuleType 

10from typing import Iterable 

11 

12from . import __path__ as _PKG_PATH 

13from . import __version__ 

14 

15 

16def _iter_exception_modules() -> Iterable[ModuleType]: 

17 """Yield every submodule that explicitly defines ``__all__``.""" 

18 allowed_suffixes = ("exceptions", "_exceptions") 

19 # dataexcept.base holds DataExceptError, the root of the hierarchy, and 

20 # does not match the suffix rule. 

21 always_include = {"dataexcept.base"} 

22 

23 for module_info in pkgutil.walk_packages( 

24 _PKG_PATH, prefix="dataexcept.", onerror=lambda name: None 

25 ): 

26 if ( 

27 not module_info.name.endswith(allowed_suffixes) 

28 and module_info.name not in always_include 

29 ): 

30 continue 

31 try: 

32 module = import_module(module_info.name) 

33 except ImportError as exc: # pragma: no cover - defensive guard 

34 print( 

35 f"dataexcept: failed to import {module_info.name}: {exc}", 

36 file=sys.stderr, 

37 ) 

38 continue 

39 if getattr(module, "__all__", None): 39 ↛ 23line 39 didn't jump to line 23 because the condition on line 39 was always true

40 yield module 

41 

42 

43def _iter_exception_names() -> Iterable[str]: 

44 seen: set[str] = set() 

45 for module in _iter_exception_modules(): 

46 names = getattr(module, "__all__", None) 

47 if not names: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true

48 continue 

49 for name in names: 

50 if name.endswith("Error") and name not in seen: 

51 seen.add(name) 

52 yield name 

53 

54 

55def _list_exceptions() -> None: 

56 for name in sorted(_iter_exception_names()): 

57 print(name) 

58 

59 

60def main(argv: list[str] | None = None) -> None: 

61 """Entry point for the ``dataexcept`` command.""" 

62 parser = argparse.ArgumentParser( 

63 # Without this, `python -m dataexcept --version` reports "__main__.py". 

64 prog="dataexcept", 

65 description="Utilities for DataExcept", 

66 ) 

67 parser.add_argument( 

68 "--version", 

69 action="version", 

70 version=f"%(prog)s {__version__}", 

71 ) 

72 subparsers = parser.add_subparsers(dest="command") 

73 subparsers.add_parser("list", help="List available exception classes") 

74 

75 args = parser.parse_args(argv) 

76 

77 if args.command == "list": 

78 _list_exceptions() 

79 else: # pragma: no cover - help message 

80 parser.print_help() 

81 

82 

83if __name__ == "__main__": # pragma: no cover - manual invocation 

84 main()