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
« 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]
10import logging
11import re
12from importlib.metadata import EntryPoint
13from importlib.metadata import entry_points
14from typing import Callable
15from typing import overload
17DIRECTIVE_PATTERN = re.compile(r"^([a-zA-Z]\w+)/{2}(.*)$")
19logger = logging.getLogger("navdict")
22class Directive:
23 @overload
24 def __init__(self, ep: EntryPoint): ... 24 ↛ exitline 24 didn't return from function '__init__' because
26 @overload
27 def __init__(self, *, name: str, func: Callable): ... 27 ↛ exitline 27 didn't return from function '__init__' because
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
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'")
42 @property
43 def name(self) -> str:
44 return self.ep.name if self.ep else self.directive_name
46 @property
47 def func(self) -> Callable:
48 return self.ep.load() if self.ep else self.directive_func
51# Keep a record of all navdict directive plugins
52_directive_plugins: dict[str, Directive] = {}
55def register_directive(name: str, func: Callable):
56 _directive_plugins[name] = Directive(name=name, func=func)
59def load_directive_plugins():
60 """
61 Load any navdict directive plugins that are available in your environment.
62 """
63 global _directive_plugins
65 eps = entry_points()
66 # logger.debug(f"entrypoint groups: {sorted(eps.groups)}")
67 eps = eps.select(group="navdict.directive")
69 for ep in eps:
70 _directive_plugins[ep.name] = Directive(ep=ep)
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
82def unravel_directive(value: str) -> tuple[str, str]:
83 """
84 Returns the directive key and the directive value in a tuple.
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}")
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)
101# Load all directive plugins during import
102load_directive_plugins()