"""Resolution context — carries service and ancestor chain during tree walk.""" class Context: """Evaluation frame for the resolver. Top-level context has ``service`` set, ``parent_object`` as None, and ``ancestors`` as an empty list. Each selected/allocated object pushes a child context so children can reference ``parent`` (immediate) and ``ancestors`` (full chain). ``ancestors[0]`` is the immediate parent (``p1`` in Jinja2), ``ancestors[1]`` is the grandparent (``p2``), etc. """ def __init__(self, service, parent_object=None, ancestors=None): self.service = service self.parent_object = parent_object self.ancestors = ancestors if ancestors is not None else [] def child(self, obj): return Context( service=self.service, parent_object=obj, ancestors=[obj] + self.ancestors, ) @property def parent(self): return self.parent_object def resolve_path(self, path): """Resolve a dotted reference like ``parent.site.management_prefix``. Supported roots: - ``service`` → the Service instance - ``parent`` → the parent object (or None at service scope) Returns None if any segment is None or missing. """ parts = path.split(".") root = parts[0] if root == "service": obj = self.service elif root == "parent": obj = self.parent_object else: raise KeyError(f"Unknown context root: {root!r}") for part in parts[1:]: if obj is None: return None if isinstance(obj, dict): obj = obj.get(part) else: obj = getattr(obj, part, None) return obj