Coverage for /Users/rik/github/navdict/src/navdict/directive.py: 71%

51 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-16 22:37 +0200

1__all__ = [ 

2 "Directive", 

3 "get_directive_plugin", 

4 "is_directive", 

5 "load_directive_plugins", 

6 "unravel_directive", 

7 "register_directive", 

8] 

9 

10import logging 

11import re 

12from importlib.metadata import EntryPoint 

13from importlib.metadata import entry_points 

14from typing import Callable 

15from typing import overload 

16 

17DIRECTIVE_PATTERN = re.compile(r"^([a-zA-Z]\w+)/{2}(.*)$") 

18 

19logger = logging.getLogger("navdict") 

20 

21 

22class Directive: 

23 @overload 

24 def __init__(self, ep: EntryPoint): ... 24 ↛ exitline 24 didn't return from function '__init__' because

25 

26 @overload 

27 def __init__(self, *, name: str, func: Callable): ... 27 ↛ exitline 27 didn't return from function '__init__' because

28 

29 def __init__(self, ep: EntryPoint | None = None, *, name: str | None = None, func: Callable | None = None): 

30 self.ep: EntryPoint | None = None 

31 self.directive_name: str | None = None 

32 self.directive_func: Callable | None = None 

33 

34 if ep is not None: 34 ↛ 36line 34 didn't jump to line 36 because the condition on line 34 was always true

35 self.ep = ep 

36 elif name is not None and func is not None: 

37 self.directive_name = name 

38 self.directive_func = func 

39 else: 

40 raise ValueError("Must provide either 'ep' or both 'name' and 'func'") 

41 

42 @property 

43 def name(self) -> str: 

44 return self.ep.name if self.ep else self.directive_name 

45 

46 @property 

47 def func(self) -> Callable: 

48 return self.ep.load() if self.ep else self.directive_func 

49 

50 

51# Keep a record of all navdict directive plugins 

52_directive_plugins: dict[str, Directive] = {} 

53 

54 

55def register_directive(name: str, func: Callable): 

56 _directive_plugins[name] = Directive(name=name, func=func) 

57 

58 

59def load_directive_plugins(): 

60 """ 

61 Load any navdict directive plugins that are available in your environment. 

62 """ 

63 global _directive_plugins 

64 

65 eps = entry_points() 

66 # logger.debug(f"entrypoint groups: {sorted(eps.groups)}") 

67 eps = eps.select(group="navdict.directive") 

68 

69 for ep in eps: 

70 _directive_plugins[ep.name] = Directive(ep=ep) 

71 

72 

73def is_directive(value: str) -> bool: 

74 """Returns True if the value matches a directive pattern, i.e. 'name//value'.""" 

75 if isinstance(value, str): 

76 match = re.match(DIRECTIVE_PATTERN, value) 

77 return match is not None 

78 else: 

79 return False 

80 

81 

82def unravel_directive(value: str) -> tuple[str, str]: 

83 """ 

84 Returns the directive key and the directive value in a tuple. 

85 

86 Raises: 

87 A ValueError if the given value is not a directive. 

88 """ 

89 match = re.match(DIRECTIVE_PATTERN, value) 

90 if match: 

91 return match[1], match[2] 

92 else: 

93 raise ValueError(f"Value is not a directive: {value}") 

94 

95 

96def get_directive_plugin(name: str) -> Directive | None: 

97 """Returns the directive that matches the given name or None if no plugin was loaded with that name.""" 

98 return _directive_plugins.get(name) 

99 

100 

101# Load all directive plugins during import 

102load_directive_plugins()