diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index d08a2bfff8..dae2d2d6fd 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -7,7 +7,6 @@ from re import IGNORECASE, compile from typing import ( Any, - Callable, ClassVar, Iterable, Mapping, @@ -20,7 +19,7 @@ from bson import ObjectId -from jaclang.compiler.constant import EdgeDir +from jaclang.plugin.feature import JacFeature as Jac from jaclang.runtimelib.architype import ( Access as _Access, AccessLevel, @@ -389,43 +388,6 @@ def disconnect_edge(self, anchor: Anchor) -> None: """Push update that there's edge that has been removed.""" self.pull("edges", anchor) - def allow_root( - self, root: "BaseAnchor", level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Allow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - if (ref_id := root.ref_id) and level != access.anchors.get( - ref_id, AccessLevel.NO_ACCESS - ): - access.anchors[ref_id] = level - self._set.update({f"access.roots.anchors.{ref_id}": level.name}) - self._unset.pop(f"access.roots.anchors.{ref_id}", None) - - def disallow_root( - self, root: Anchor, level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Disallow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - - if (ref_id := root.ref_id) and access.anchors.pop(ref_id, None) is not None: - self._unset.update({f"access.roots.anchors.{ref_id}": True}) - self._set.pop(f"access.roots.anchors.{ref_id}", None) - - def unrestrict(self, level: AccessLevel | int | str = AccessLevel.READ) -> None: - """Allow everyone to access current Architype.""" - level = AccessLevel.cast(level) - if level != self.access.all: - self.access.all = level - self._set.update({"access.all": level.name}) - - def restrict(self) -> None: - """Disallow others to access current Architype.""" - if self.access.all > AccessLevel.NO_ACCESS: - self.access.all = AccessLevel.NO_ACCESS - self._set.update({"access.all": AccessLevel.NO_ACCESS.name}) - #################################################### # POPULATE OPERATIONS # #################################################### @@ -461,14 +423,14 @@ def build_query( bulk_write: BulkWrite, ) -> None: """Save Anchor.""" - if self.state.deleted is False and self.has_write_access(self): # type: ignore[attr-defined] + if self.state.deleted is False and Jac.check_write_access(self): # type: ignore[arg-type] self.state.deleted = True self.delete(bulk_write) elif not self.state.connected: self.state.connected = True self.sync_hash() self.insert(bulk_write) - elif self.has_connect_access(self): # type: ignore[attr-defined] + elif Jac.check_connect_access(self): # type: ignore[arg-type] self.update(bulk_write, True) def apply(self, session: ClientSession | None = None) -> BulkWrite: @@ -507,9 +469,8 @@ def update(self, bulk_write: BulkWrite, propagate: bool = False) -> None: ############################################################ # POPULATE CONTEXT # ############################################################ - from .context import JaseciContext - if JaseciContext.get().root.has_write_access(self): + if Jac.check_write_access(self): # type: ignore[arg-type] set_architype = changes.pop("$set", {}) if is_dataclass(architype := self.architype) and not isinstance( architype, type @@ -587,10 +548,6 @@ def delete(self, bulk_write: BulkWrite) -> None: """Append Delete Query.""" raise NotImplementedError("delete must be implemented in subclasses") - def destroy(self) -> None: - """Delete Anchor.""" - raise NotImplementedError("destroy must be implemented in subclasses") - def has_changed(self) -> int: """Check if needs to update.""" if self.state.full_hash != (new_hash := hash(pdumps(self.serialize()))): @@ -608,49 +565,6 @@ def sync_hash(self) -> None: } self.state.full_hash = hash(pdumps(self.serialize())) - def access_level(self, to: Anchor) -> AccessLevel: - """Access validation.""" - if not to.persistent: - return AccessLevel.WRITE - - from .context import JaseciContext - - jctx = JaseciContext.get() - - jroot = jctx.root - - # if current root is system_root - # if current root id is equal to target anchor's root id - # if current root is the target anchor - if jroot == jctx.system_root or jroot.id == to.root or jroot == to: - return AccessLevel.WRITE - - access_level = AccessLevel.NO_ACCESS - - # if target anchor have set access.all - if (to_access := to.access).all > AccessLevel.NO_ACCESS: - access_level = to_access.all - - # if target anchor's root have set allowed roots - # if current root is allowed to the whole graph of target anchor's root - if to.root and isinstance( - to_root := jctx.mem.find_by_id(NodeAnchor.ref(f"n::{to.root}")), Anchor - ): - if to_root.access.all > access_level: - access_level = to_root.access.all - - level = to_root.access.roots.check(jroot.ref_id) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - # if target anchor have set allowed roots - # if current root is allowed to target anchor - level = to_access.roots.check(jroot.ref_id) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - return access_level - # ---------------------------------------------------------------------- # def report(self) -> dict[str, object]: @@ -760,47 +674,6 @@ def delete(self, bulk_write: BulkWrite) -> None: bulk_write.del_node(self.id) - def destroy(self) -> None: - """Delete Anchor.""" - if self.state.deleted is None: - from .context import JaseciContext - - jctx = JaseciContext.get() - - if jctx.root.has_write_access(self): - - self.state.deleted = False - - for edge in self.edges: - edge.destroy() - jctx.mem.remove(self.id) - - def get_edges( - self, - dir: EdgeDir, - filter_func: Callable[[list["EdgeArchitype"]], list["EdgeArchitype"]] | None, - target_obj: list["NodeArchitype"] | None, - ) -> list["EdgeArchitype"]: - """Get edges connected to this node.""" - from .context import JaseciContext - - JaseciContext.get().mem.populate_data(self.edges) - - return super().get_edges(dir, filter_func, target_obj) - - def edges_to_nodes( - self, - dir: EdgeDir, - filter_func: Callable[[list["EdgeArchitype"]], list["EdgeArchitype"]] | None, - target_obj: list["NodeArchitype"] | None, - ) -> list["NodeArchitype"]: - """Get set of nodes connected to this node.""" - from .context import JaseciContext - - JaseciContext.get().mem.populate_data(self.edges) - - return super().edges_to_nodes(dir, filter_func, target_obj) - def serialize(self) -> dict[str, object]: """Serialize Node Anchor.""" return { @@ -848,11 +721,6 @@ def __document__(cls, doc: Mapping[str, Any]) -> "EdgeAnchor": anchor.sync_hash() return anchor - def __post_init__(self) -> None: - """Populate edge to source and target.""" - # remove parent trigger - pass - @classmethod def ref(cls, ref_id: str) -> "EdgeAnchor": """Return EdgeAnchor instance if existing.""" @@ -883,25 +751,6 @@ def delete(self, bulk_write: BulkWrite) -> None: bulk_write.del_edge(self.id) - def destroy(self) -> None: - """Delete Anchor.""" - if self.state.deleted is None: - from .context import JaseciContext - - jctx = JaseciContext.get() - - if jctx.root.has_write_access(self): - self.state.deleted = False - self.detach() - jctx.mem.remove(self.id) - - def detach(self) -> None: - """Detach edge from nodes.""" - self.source.remove_edge(self) - self.source.disconnect_edge(self) - self.target.remove_edge(self) - self.target.disconnect_edge(self) - def serialize(self) -> dict[str, object]: """Serialize Node Anchor.""" return { @@ -972,61 +821,6 @@ def delete(self, bulk_write: BulkWrite) -> None: """Append Delete Query.""" bulk_write.del_walker(self.id) - def destroy(self) -> None: - """Delete Anchor.""" - if self.state.deleted is None: - from .context import JaseciContext - - jctx = JaseciContext.get() - - if jctx.root.has_write_access(self): - self.state.deleted = False - jctx.mem.remove(self.id) - - def spawn_call(self, node: Anchor) -> "WalkerArchitype": - """Invoke data spatial call.""" - if walker := self.architype: - self.path = [] - self.next = [node] - self.returns = [] - while len(self.next): - if current_node := self.next.pop(0).architype: - for i in current_node._jac_entry_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - self.returns.append(i.func(current_node, walker)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_entry_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func: - self.returns.append(i.func(walker, current_node)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_exit_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func: - self.returns.append(i.func(walker, current_node)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in current_node._jac_exit_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - self.returns.append(i.func(current_node, walker)) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - self.ignores = [] - return walker - raise Exception(f"Invalid Reference {self.id}") - @dataclass(eq=False, repr=False, kw_only=True) class ObjectAnchor(BaseAnchor, Anchor): # type: ignore[misc] @@ -1081,7 +875,7 @@ class NodeArchitype(BaseArchitype, _NodeArchitype): __jac__: NodeAnchor - def __post_init__(self) -> None: + def __init__(self) -> None: """Create node architype.""" self.__jac__ = NodeAnchor( architype=self, @@ -1102,27 +896,6 @@ class EdgeArchitype(BaseArchitype, _EdgeArchitype): __jac__: EdgeAnchor - def __attach__( - self, - source: NodeAnchor, - target: NodeAnchor, - is_undirected: bool, - ) -> None: - """Attach EdgeAnchor properly.""" - jac = self.__jac__ = EdgeAnchor( - architype=self, - name=self.__class__.__name__, - source=source, - target=target, - is_undirected=is_undirected, - access=Permission(), - state=AnchorState(), - ) - source.edges.append(jac) - target.edges.append(jac) - source.connect_edge(jac) - target.connect_edge(jac) - @classmethod def __ref_cls__(cls) -> str: """Get class naming.""" @@ -1134,7 +907,7 @@ class WalkerArchitype(BaseArchitype, _WalkerArchitype): __jac__: WalkerAnchor - def __post_init__(self) -> None: + def __init__(self) -> None: """Create walker architype.""" self.__jac__ = WalkerAnchor( architype=self, @@ -1154,7 +927,7 @@ class ObjectArchitype(BaseArchitype, Architype): __jac__: ObjectAnchor - def __post_init__(self) -> None: + def __init__(self) -> None: """Create default architype.""" self.__jac__ = ObjectAnchor( architype=self, @@ -1171,26 +944,6 @@ class GenericEdge(EdgeArchitype): _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] - def __attach__( - self, - source: NodeAnchor, - target: NodeAnchor, - is_undirected: bool, - ) -> None: - """Attach EdgeAnchor properly.""" - jac = self.__jac__ = EdgeAnchor( - architype=self, - source=source, - target=target, - is_undirected=is_undirected, - access=Permission(), - state=AnchorState(), - ) - source.edges.append(jac) - target.edges.append(jac) - source.connect_edge(jac) - target.connect_edge(jac) - @dataclass(eq=False) class Root(NodeArchitype): @@ -1199,7 +952,7 @@ class Root(NodeArchitype): _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] - def __post_init__(self) -> None: + def __init__(self) -> None: """Create node architype.""" self.__jac__ = NodeAnchor( architype=self, diff --git a/jac-cloud/jac_cloud/core/context.py b/jac-cloud/jac_cloud/core/context.py index 092975966e..fef73da8cf 100644 --- a/jac-cloud/jac_cloud/core/context.py +++ b/jac-cloud/jac_cloud/core/context.py @@ -65,10 +65,6 @@ class JaseciContext(ExecutionContext): base: ExecutionContext request: Request - def validate_access(self) -> bool: - """Validate access.""" - return self.root.has_read_access(self.entry_node) - def close(self) -> None: """Clean up context.""" self.mem.close() diff --git a/jac-cloud/jac_cloud/core/memory.py b/jac-cloud/jac_cloud/core/memory.py index e362f77bdd..2439b0486c 100644 --- a/jac-cloud/jac_cloud/core/memory.py +++ b/jac-cloud/jac_cloud/core/memory.py @@ -6,6 +6,7 @@ from bson import ObjectId +from jaclang.plugin.feature import JacFeature as Jac from jaclang.runtimelib.memory import Memory @@ -115,9 +116,6 @@ def close(self) -> None: def get_bulk_write(self) -> BulkWrite: """Sync memory to database.""" - from .context import JaseciContext - - JaseciContext bulk_write = BulkWrite() for anchor in self.__gc__: @@ -139,8 +137,8 @@ def get_bulk_write(self) -> BulkWrite: bulk_write.operations[anchor.__class__].append( InsertOne(anchor.serialize()) ) - elif (new_hash := anchor.has_changed()) and anchor.has_connect_access( - anchor + elif (new_hash := anchor.has_changed()) and Jac.check_connect_access( + anchor # type: ignore[arg-type] ): anchor.state.full_hash = new_hash if ( diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index d199ffb2e5..acc141d9c3 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -22,7 +22,8 @@ ) from fastapi.responses import ORJSONResponse -from jaclang.plugin.default import JacFeatureDefaults, hookimpl +from jaclang.compiler.constant import EdgeDir +from jaclang.plugin.default import JacFeatureImpl, hookimpl from jaclang.plugin.feature import JacFeature as Jac from jaclang.runtimelib.architype import DSFunc @@ -33,14 +34,18 @@ from starlette.datastructures import UploadFile as BaseUploadFile from ..core.architype import ( + AccessLevel, Anchor, + AnchorState, Architype, BaseAnchor, + EdgeAnchor, EdgeArchitype, GenericEdge, NodeAnchor, NodeArchitype, ObjectArchitype, + Permission, Root, WalkerAnchor, WalkerArchitype, @@ -161,8 +166,8 @@ def api_entry( jctx = JaseciContext.create(request, NodeAnchor.ref(node) if node else None) wlk: WalkerAnchor = cls(**body, **pl["query"], **pl["files"]).__jac__ - if jctx.validate_access(): - wlk.spawn_call(jctx.entry_node) + if Jac.check_read_access(jctx.entry_node): + Jac.spawn_call(wlk.architype, jctx.entry_node.architype) jctx.close() return jctx.response(wlk.returns) else: @@ -258,16 +263,201 @@ class DefaultSpecs: private: bool = False -class JacPlugin: +class JacAccessValidationPlugin: + """Jac Access Validation Implementations.""" + + @staticmethod + @hookimpl + def allow_root( + architype: Architype, root_id: BaseAnchor, level: AccessLevel | int | str + ) -> None: + """Allow all access from target root graph to current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.allow_root( + architype=architype, root_id=root_id, level=level # type: ignore[arg-type] + ) + return + + anchor = architype.__jac__ + + level = AccessLevel.cast(level) + access = anchor.access.roots + if ( + isinstance(anchor, BaseAnchor) + and (ref_id := root_id.ref_id) + and level != access.anchors.get(ref_id, AccessLevel.NO_ACCESS) + ): + access.anchors[ref_id] = level + anchor._set.update({f"access.roots.anchors.{ref_id}": level.name}) + anchor._unset.pop(f"access.roots.anchors.{ref_id}", None) + + @staticmethod + @hookimpl + def disallow_root( + architype: Architype, root_id: BaseAnchor, level: AccessLevel | int | str + ) -> None: + """Disallow all access from target root graph to current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.disallow_root( + architype=architype, root_id=root_id, level=level # type: ignore[arg-type] + ) + return + + anchor = architype.__jac__ + + level = AccessLevel.cast(level) + access = anchor.access.roots + if ( + isinstance(anchor, BaseAnchor) + and (ref_id := root_id.ref_id) + and access.anchors.pop(ref_id, None) is not None + ): + anchor._unset.update({f"access.roots.anchors.{ref_id}": True}) + anchor._set.pop(f"access.roots.anchors.{ref_id}", None) + + @staticmethod + @hookimpl + def unrestrict(architype: Architype, level: AccessLevel | int | str) -> None: + """Allow everyone to access current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.unrestrict(architype=architype, level=level) + return + + anchor = architype.__jac__ + + level = AccessLevel.cast(level) + if isinstance(anchor, BaseAnchor) and level != anchor.access.all: + anchor.access.all = level + anchor._set.update({"access.all": level.name}) + + @staticmethod + @hookimpl + def restrict(architype: Architype) -> None: + """Disallow others to access current Architype.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.restrict(architype=architype) + return + + anchor = architype.__jac__ + + if isinstance(anchor, BaseAnchor) and anchor.access.all > AccessLevel.NO_ACCESS: + anchor.access.all = AccessLevel.NO_ACCESS + anchor._set.update({"access.all": AccessLevel.NO_ACCESS.name}) + + @staticmethod + @hookimpl + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.check_access_level(to=to) + + if not to.persistent: + return AccessLevel.WRITE + + from ..core.context import JaseciContext + + jctx = JaseciContext.get() + + jroot = jctx.root + + # if current root is system_root + # if current root id is equal to target anchor's root id + # if current root is the target anchor + if jroot == jctx.system_root or jroot.id == to.root or jroot == to: + return AccessLevel.WRITE + + access_level = AccessLevel.NO_ACCESS + + # if target anchor have set access.all + if (to_access := to.access).all > AccessLevel.NO_ACCESS: + access_level = to_access.all + + # if target anchor's root have set allowed roots + # if current root is allowed to the whole graph of target anchor's root + if to.root and isinstance( + to_root := jctx.mem.find_by_id(NodeAnchor.ref(f"n::{to.root}")), Anchor + ): + if to_root.access.all > access_level: + access_level = to_root.access.all + + level = to_root.access.roots.check(jroot.ref_id) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + # if target anchor have set allowed roots + # if current root is allowed to target anchor + level = to_access.roots.check(jroot.ref_id) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + return access_level + + +class JacNodePlugin: + """Jac Node Operations.""" + + @staticmethod + @hookimpl + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Callable[[list[EdgeArchitype]], list[EdgeArchitype]] | None, + target_obj: list[NodeArchitype] | None, + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + if FastAPI.is_enabled(): + JaseciContext.get().mem.populate_data(node.edges) + + return JacFeatureImpl.get_edges( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj # type: ignore[arg-type, return-value] + ) + + @staticmethod + @hookimpl + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Callable[[list[EdgeArchitype]], list[EdgeArchitype]] | None, + target_obj: list[NodeArchitype] | None, + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + if FastAPI.is_enabled(): + JaseciContext.get().mem.populate_data(node.edges) + + return JacFeatureImpl.edges_to_nodes( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj # type: ignore[arg-type, return-value] + ) + + +class JacEdgePlugin: + """Jac Edge Operations.""" + + @staticmethod + @hookimpl + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + if not FastAPI.is_enabled(): + JacFeatureImpl.detach(edge=edge) + return + + Jac.remove_edge(node=edge.source, edge=edge) + edge.source.disconnect_edge(edge) + + Jac.remove_edge(node=edge.target, edge=edge) + edge.target.disconnect_edge(edge) + + +class JacPlugin(JacAccessValidationPlugin, JacNodePlugin, JacEdgePlugin): """Jaseci Implementations.""" @staticmethod @hookimpl def get_context() -> ExecutionContext: """Get current execution context.""" - if FastAPI.is_enabled(): - return JaseciContext.get() - return JacFeatureDefaults.get_context() + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_context() + + return JaseciContext.get() @staticmethod @hookimpl @@ -278,46 +468,46 @@ def make_architype( on_exit: list[DSFunc], ) -> Type[Architype]: """Create a new architype.""" - if FastAPI.is_enabled(): - for i in on_entry + on_exit: - i.resolve(cls) - if not hasattr(cls, "_jac_entry_funcs_") or not hasattr( - cls, "_jac_exit_funcs_" - ): - # Saving the module path and reassign it after creating cls - # So the jac modules are part of the correct module - cur_module = cls.__module__ - cls = type(cls.__name__, (cls, arch_base), {}) - cls.__module__ = cur_module - cls._jac_entry_funcs_ = on_entry # type: ignore - cls._jac_exit_funcs_ = on_exit # type: ignore - else: - new_entry_funcs = OrderedDict(zip([i.name for i in on_entry], on_entry)) - entry_funcs = OrderedDict( - zip([i.name for i in cls._jac_entry_funcs_], cls._jac_entry_funcs_) - ) - entry_funcs.update(new_entry_funcs) - cls._jac_entry_funcs_ = list(entry_funcs.values()) + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_architype( + cls=cls, arch_base=arch_base, on_entry=on_entry, on_exit=on_exit + ) + for i in on_entry + on_exit: + i.resolve(cls) + if not hasattr(cls, "_jac_entry_funcs_") or not hasattr( + cls, "_jac_exit_funcs_" + ): + # Saving the module path and reassign it after creating cls + # So the jac modules are part of the correct module + cur_module = cls.__module__ + cls = type(cls.__name__, (cls, arch_base), {}) + cls.__module__ = cur_module + cls._jac_entry_funcs_ = on_entry # type: ignore + cls._jac_exit_funcs_ = on_exit # type: ignore + else: + new_entry_funcs = OrderedDict(zip([i.name for i in on_entry], on_entry)) + entry_funcs = OrderedDict( + zip([i.name for i in cls._jac_entry_funcs_], cls._jac_entry_funcs_) + ) + entry_funcs.update(new_entry_funcs) + cls._jac_entry_funcs_ = list(entry_funcs.values()) - new_exit_funcs = OrderedDict(zip([i.name for i in on_exit], on_exit)) - exit_funcs = OrderedDict( - zip([i.name for i in cls._jac_exit_funcs_], cls._jac_exit_funcs_) - ) - exit_funcs.update(new_exit_funcs) - cls._jac_exit_funcs_ = list(exit_funcs.values()) + new_exit_funcs = OrderedDict(zip([i.name for i in on_exit], on_exit)) + exit_funcs = OrderedDict( + zip([i.name for i in cls._jac_exit_funcs_], cls._jac_exit_funcs_) + ) + exit_funcs.update(new_exit_funcs) + cls._jac_exit_funcs_ = list(exit_funcs.values()) - inner_init = cls.__init__ # type: ignore + inner_init = cls.__init__ # type: ignore - @wraps(inner_init) - def new_init(self: Architype, *args: object, **kwargs: object) -> None: - arch_base.__init__(self) - inner_init(self, *args, **kwargs) + @wraps(inner_init) + def new_init(self: Architype, *args: object, **kwargs: object) -> None: + arch_base.__init__(self) + inner_init(self, *args, **kwargs) - cls.__init__ = new_init # type: ignore - return cls - return JacFeatureDefaults.make_architype( - cls=cls, arch_base=arch_base, on_entry=on_entry, on_exit=on_exit - ) + cls.__init__ = new_init # type: ignore + return cls @staticmethod @hookimpl @@ -325,20 +515,20 @@ def make_obj( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a new architype.""" - if FastAPI.is_enabled(): - - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, - arch_base=ObjectArchitype, - on_entry=on_entry, - on_exit=on_exit, - ) - return cls + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_obj(on_entry=on_entry, on_exit=on_exit) + + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, + arch_base=ObjectArchitype, + on_entry=on_entry, + on_exit=on_exit, + ) + return cls - return decorator - return JacFeatureDefaults.make_obj(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl @@ -346,17 +536,17 @@ def make_node( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a obj architype.""" - if FastAPI.is_enabled(): + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_node(on_entry=on_entry, on_exit=on_exit) - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, arch_base=NodeArchitype, on_entry=on_entry, on_exit=on_exit - ) - return cls + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, arch_base=NodeArchitype, on_entry=on_entry, on_exit=on_exit + ) + return cls - return decorator - return JacFeatureDefaults.make_node(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl @@ -364,17 +554,17 @@ def make_edge( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a edge architype.""" - if FastAPI.is_enabled(): + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_edge(on_entry=on_entry, on_exit=on_exit) - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, arch_base=EdgeArchitype, on_entry=on_entry, on_exit=on_exit - ) - return cls + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, arch_base=EdgeArchitype, on_entry=on_entry, on_exit=on_exit + ) + return cls - return decorator - return JacFeatureDefaults.make_edge(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl @@ -382,46 +572,48 @@ def make_walker( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a walker architype.""" - if FastAPI.is_enabled(): - - def decorator(cls: Type[Architype]) -> Type[Architype]: - """Decorate class.""" - cls = Jac.make_architype( - cls=cls, - arch_base=WalkerArchitype, - on_entry=on_entry, - on_exit=on_exit, - ) - populate_apis(cls) - return cls + if not FastAPI.is_enabled(): + return JacFeatureImpl.make_walker(on_entry=on_entry, on_exit=on_exit) + + def decorator(cls: Type[Architype]) -> Type[Architype]: + """Decorate class.""" + cls = Jac.make_architype( + cls=cls, + arch_base=WalkerArchitype, + on_entry=on_entry, + on_exit=on_exit, + ) + populate_apis(cls) + return cls - return decorator - return JacFeatureDefaults.make_walker(on_entry=on_entry, on_exit=on_exit) + return decorator @staticmethod @hookimpl def report(expr: Any) -> None: # noqa:ANN401 """Jac's report stmt feature.""" - if FastAPI.is_enabled(): - JaseciContext.get().reports.append(expr) - return - JacFeatureDefaults.report(expr=expr) + if not FastAPI.is_enabled(): + return JacFeatureImpl.report(expr=expr) + + JaseciContext.get().reports.append(expr) @staticmethod @hookimpl def get_root() -> Root: """Jac's assign comprehension feature.""" - if FastAPI.is_enabled(): - return JaseciContext.get_root() - return JacFeatureDefaults.get_root() # type:ignore[return-value] + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_root() # type:ignore[return-value] + + return JaseciContext.get_root() @staticmethod @hookimpl def get_root_type() -> Type[Root]: """Jac's root getter.""" - if FastAPI.is_enabled(): - return Root - return JacFeatureDefaults.get_root_type() # type:ignore[return-value] + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_root_type() # type:ignore[return-value] + + return Root @staticmethod @hookimpl @@ -431,33 +623,53 @@ def build_edge( conn_assign: tuple[tuple, tuple] | None, ) -> Callable[[NodeAnchor, NodeAnchor], EdgeArchitype]: """Jac's root getter.""" - if FastAPI.is_enabled(): - conn_type = conn_type if conn_type else GenericEdge - - def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: - edge = conn_type() if isinstance(conn_type, type) else conn_type - edge.__attach__(source, target, is_undirected) - if conn_assign: - for fld, val in zip(conn_assign[0], conn_assign[1]): - if hasattr(edge, fld): - setattr(edge, fld, val) - else: - raise ValueError(f"Invalid attribute: {fld}") - if source.persistent or target.persistent: - edge.__jac__.save() - target.save() - source.save() - return edge - - return builder - return JacFeatureDefaults.build_edge( # type:ignore[return-value] - is_undirected=is_undirected, conn_type=conn_type, conn_assign=conn_assign - ) + if not FastAPI.is_enabled(): + return JacFeatureImpl.build_edge( # type:ignore[return-value] + is_undirected=is_undirected, + conn_type=conn_type, + conn_assign=conn_assign, + ) + + conn_type = conn_type if conn_type else GenericEdge + + def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: + edge = conn_type() if isinstance(conn_type, type) else conn_type + + eanch = edge.__jac__ = EdgeAnchor( + architype=edge, + name=("" if isinstance(edge, GenericEdge) else edge.__class__.__name__), + source=source, + target=target, + is_undirected=is_undirected, + access=Permission(), + state=AnchorState(), + ) + source.edges.append(eanch) + target.edges.append(eanch) + source.connect_edge(eanch) + target.connect_edge(eanch) + + if conn_assign: + for fld, val in zip(conn_assign[0], conn_assign[1]): + if hasattr(edge, fld): + setattr(edge, fld, val) + else: + raise ValueError(f"Invalid attribute: {fld}") + if source.persistent or target.persistent: + Jac.save(eanch) + Jac.save(target) + Jac.save(source) + return edge + + return builder @staticmethod @hookimpl def get_object(id: str) -> Architype | None: """Get object via reference id.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.get_object(id=id) + with suppress(ValueError): if isinstance(architype := BaseAnchor.ref(id).architype, Architype): return architype @@ -468,4 +680,103 @@ def get_object(id: str) -> Architype | None: @hookimpl def object_ref(obj: Architype) -> str: """Get object reference id.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.object_ref(obj=obj) + return str(obj.__jac__.ref_id) + + @staticmethod + @hookimpl + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Invoke data spatial call.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.spawn_call( + op1=op1, op2=op2 + ) # type:ignore[return-value] + + if isinstance(op1, WalkerArchitype): + warch = op1 + walker = op1.__jac__ + if isinstance(op2, NodeArchitype): + node = op2.__jac__ + elif isinstance(op2, EdgeArchitype): + node = op2.__jac__.target + else: + raise TypeError("Invalid target object") + elif isinstance(op2, WalkerArchitype): + warch = op2 + walker = op2.__jac__ + if isinstance(op1, NodeArchitype): + node = op1.__jac__ + elif isinstance(op1, EdgeArchitype): + node = op1.__jac__.target + else: + raise TypeError("Invalid target object") + else: + raise TypeError("Invalid walker object") + + walker.path = [] + walker.next = [node] + walker.returns = [] + while len(walker.next): + if current_node := walker.next.pop(0).architype: + for i in current_node._jac_entry_funcs_: + if not i.trigger or isinstance(walker, i.trigger): + if i.func: + walker.returns.append(i.func(current_node, warch)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_entry_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func: + walker.returns.append(i.func(warch, current_node)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_exit_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func: + walker.returns.append(i.func(warch, current_node)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in current_node._jac_exit_funcs_: + if not i.trigger or isinstance(walker, i.trigger): + if i.func: + walker.returns.append(i.func(current_node, warch)) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + walker.ignores = [] + return warch + + @staticmethod + @hookimpl + def destroy(obj: Architype | Anchor | BaseAnchor) -> None: + """Destroy object.""" + if not FastAPI.is_enabled(): + return JacFeatureImpl.destroy(obj=obj) # type:ignore[arg-type] + + anchor = obj.__jac__ if isinstance(obj, Architype) else obj + + if ( + isinstance(anchor, BaseAnchor) + and anchor.state.deleted is None + and Jac.check_write_access(anchor) # type: ignore[arg-type] + ): + anchor.state.deleted = False + match anchor: + case NodeAnchor(): + for edge in anchor.edges: + Jac.destroy(edge) + case EdgeAnchor(): + Jac.detach(anchor) + case _: + pass + + Jac.get_context().mem.remove(anchor.id) diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index 009c2f9b71..dd8884222b 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -281,7 +281,7 @@ walker manual_detach_nested_node { can enter_root with `root entry { nested = [-->(`?Nested)][0]; detached = here del--> [-->(`?Nested)]; - nested.__jac__.destroy(); + Jac.destroy(nested); nested.__jac__.apply(); # simulate no auto save @@ -296,7 +296,7 @@ walker manual_detach_nested_node { walker delete_nested_node { can enter_root with `root entry { nested = [-->(`?Nested)][0]; - nested.__jac__.destroy(); + Jac.destroy(nested); # nested.__jac__.apply(); report [-->(`?Nested)]; @@ -306,7 +306,7 @@ walker delete_nested_node { walker manual_delete_nested_node { can enter_root with `root entry { nested = [-->(`?Nested)][0]; - nested.__jac__.destroy(); + Jac.destroy(nested); nested.__jac__.apply(); # simulate no auto save @@ -321,7 +321,7 @@ walker manual_delete_nested_node { walker delete_nested_edge { can enter_root with `root entry { nested_edge = :e:[-->][0]; - nested_edge.__jac__.destroy(); + Jac.destroy(nested_edge); report [-->(`?Nested)]; } @@ -330,7 +330,7 @@ walker delete_nested_edge { walker manual_delete_nested_edge { can enter_root with `root entry { nested_edge = :e:[-->][0]; - nested_edge.__jac__.destroy(); + Jac.destroy(nested_edge); nested_edge.__jac__.apply(); # simulate no auto save @@ -347,17 +347,17 @@ walker allow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here, self.level); } else { - here.__jac__.allow_root(BaseAnchor.ref(self.root_id), self.level); + Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); } } can enter_nested with Nested entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here, self.level); } else { - here.__jac__.allow_root(BaseAnchor.ref(self.root_id), self.level); + Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); } } } @@ -367,17 +367,17 @@ walker disallow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here); } else { - here.__jac__.disallow_root(BaseAnchor.ref(self.root_id)); + Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); } } can enter_nested with Nested entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here); } else { - here.__jac__.disallow_root(BaseAnchor.ref(self.root_id)); + Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); } } } diff --git a/jac/.pre-commit-config.yaml b/jac/.pre-commit-config.yaml index 3c64d33a1e..750eb07c79 100644 --- a/jac/.pre-commit-config.yaml +++ b/jac/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: check-json - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 24.1.1 + rev: 24.8.0 hooks: - id: black exclude: "vendor" diff --git a/jac/examples/micro/simple_walk_by_edge.jac b/jac/examples/micro/simple_walk_by_edge.jac new file mode 100644 index 0000000000..1613dbfb53 --- /dev/null +++ b/jac/examples/micro/simple_walk_by_edge.jac @@ -0,0 +1,23 @@ +"""Example of simple walker walking nodes.""" + +node A {} + +walker Walk { + has id: int = 0; + + can skip_root with `root entry { + visit [-->]; + } + + can step with A entry { + print(f'Visited {self.id}'); + } +} + +with entry { + root ++> (a:=A()); + ae = :e:[root-->a][0]; + + Walk(id = 1) spawn ae; + ae spawn Walk(id=2); +} diff --git a/jac/jaclang/__init__.py b/jac/jaclang/__init__.py index 9afb8c8aa9..157fe1ab21 100644 --- a/jac/jaclang/__init__.py +++ b/jac/jaclang/__init__.py @@ -1,17 +1,12 @@ """The Jac Programming Language.""" -from jaclang.plugin.default import ( # noqa: E402 - JacBuiltin, - JacCmdDefaults, - JacFeatureDefaults, -) -from jaclang.plugin.feature import JacFeature, pm # noqa: E402 +from jaclang.plugin.default import JacFeatureImpl +from jaclang.plugin.feature import JacFeature, plugin_manager jac_import = JacFeature.jac_import -pm.register(JacFeatureDefaults) -pm.register(JacBuiltin) -pm.register(JacCmdDefaults) -pm.load_setuptools_entrypoints("jac") + +plugin_manager.register(JacFeatureImpl) +plugin_manager.load_setuptools_entrypoints("jac") __all__ = ["jac_import"] diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index e6e3949cb8..7caf1e5f5b 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -28,6 +28,7 @@ Cmd.create_cmd() +Jac.setup() @cmd_registry.register @@ -283,7 +284,9 @@ def enter( jctx.set_entry_node(node) - if isinstance(architype, WalkerArchitype) and jctx.validate_access(): + if isinstance(architype, WalkerArchitype) and Jac.check_read_access( + jctx.entry_node + ): Jac.spawn_call(jctx.entry_node.architype, architype) jctx.close() diff --git a/jac/jaclang/plugin/builtin.py b/jac/jaclang/plugin/builtin.py index 9288d26ae7..aaeb3fdffe 100644 --- a/jac/jaclang/plugin/builtin.py +++ b/jac/jaclang/plugin/builtin.py @@ -19,9 +19,9 @@ def dotgen( dot_file: Optional[str] = None, ) -> str: """Print the dot graph.""" - from jaclang.plugin.feature import pm + from jaclang.plugin.feature import JacFeature as Jac - root = pm.hook.get_root() + root = Jac.get_root() node = node if node is not None else root depth = depth if depth is not None else -1 traverse = traverse if traverse is not None else False @@ -29,7 +29,7 @@ def dotgen( edge_limit = edge_limit if edge_limit is not None else 512 node_limit = node_limit if node_limit is not None else 512 - return pm.hook.dotgen( + return Jac.dotgen( edge_type=edge_type, node=node, depth=depth, diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 22db8b68a8..21bac84e4d 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -11,59 +11,548 @@ from collections import OrderedDict from dataclasses import field from functools import wraps +from logging import getLogger from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union from uuid import UUID -import jaclang.compiler.absyntree as ast -from jaclang.compiler.constant import EdgeDir, colors -from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass +from jaclang.compiler.constant import colors from jaclang.compiler.semtable import SemInfo, SemRegistry, SemScope -from jaclang.runtimelib.constructs import ( +from jaclang.plugin.feature import ( + AccessLevel, + Anchor, Architype, DSFunc, EdgeAnchor, EdgeArchitype, + EdgeDir, ExecutionContext, - GenericEdge, - JacTestCheck, + JacFeature as Jac, NodeAnchor, NodeArchitype, + P, + PyastGenPass, Root, - WalkerAnchor, + T, WalkerArchitype, + ast, +) +from jaclang.runtimelib.constructs import ( + GenericEdge, + JacTestCheck, ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter from jaclang.runtimelib.machine import JacMachine, JacProgram -from jaclang.runtimelib.utils import traverse_graph -from jaclang.plugin.feature import JacFeature as Jac # noqa: I100 -from jaclang.plugin.spec import P, T +from jaclang.runtimelib.utils import collect_node_connections, traverse_graph import pluggy hookimpl = pluggy.HookimplMarker("jac") +logger = getLogger(__name__) + + +class JacAccessValidationImpl: + """Jac Access Validation Implementations.""" + + @staticmethod + @hookimpl + def allow_root( + architype: Architype, root_id: UUID, level: AccessLevel | int | str + ) -> None: + """Allow all access from target root graph to current Architype.""" + level = AccessLevel.cast(level) + access = architype.__jac__.access.roots + + _root_id = str(root_id) + if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): + access.anchors[_root_id] = level -__all__ = [ - "EdgeAnchor", - "GenericEdge", - "hookimpl", - "JacTestCheck", - "NodeAnchor", - "WalkerAnchor", - "NodeArchitype", - "EdgeArchitype", - "Root", - "WalkerArchitype", - "Architype", - "DSFunc", - "T", -] - - -class JacFeatureDefaults: + @staticmethod + @hookimpl + def disallow_root( + architype: Architype, root_id: UUID, level: AccessLevel | int | str + ) -> None: + """Disallow all access from target root graph to current Architype.""" + level = AccessLevel.cast(level) + access = architype.__jac__.access.roots + + access.anchors.pop(str(root_id), None) + + @staticmethod + @hookimpl + def unrestrict(architype: Architype, level: AccessLevel | int | str) -> None: + """Allow everyone to access current Architype.""" + anchor = architype.__jac__ + level = AccessLevel.cast(level) + if level != anchor.access.all: + anchor.access.all = level + + @staticmethod + @hookimpl + def restrict(architype: Architype) -> None: + """Disallow others to access current Architype.""" + anchor = architype.__jac__ + if anchor.access.all > AccessLevel.NO_ACCESS: + anchor.access.all = AccessLevel.NO_ACCESS + + @staticmethod + @hookimpl + def check_read_access(to: Anchor) -> bool: + """Read Access Validation.""" + if not (access_level := Jac.check_access_level(to) > AccessLevel.NO_ACCESS): + logger.info( + f"Current root doesn't have read access to {to.__class__.__name__}[{to.id}]" + ) + return access_level + + @staticmethod + @hookimpl + def check_connect_access(to: Anchor) -> bool: + """Write Access Validation.""" + if not (access_level := Jac.check_access_level(to) > AccessLevel.READ): + logger.info( + f"Current root doesn't have connect access to {to.__class__.__name__}[{to.id}]" + ) + return access_level + + @staticmethod + @hookimpl + def check_write_access(to: Anchor) -> bool: + """Write Access Validation.""" + if not (access_level := Jac.check_access_level(to) > AccessLevel.CONNECT): + logger.info( + f"Current root doesn't have write access to {to.__class__.__name__}[{to.id}]" + ) + return access_level + + @staticmethod + @hookimpl + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + if not to.persistent: + return AccessLevel.WRITE + + jctx = Jac.get_context() + + jroot = jctx.root + + # if current root is system_root + # if current root id is equal to target anchor's root id + # if current root is the target anchor + if jroot == jctx.system_root or jroot.id == to.root or jroot == to: + return AccessLevel.WRITE + + access_level = AccessLevel.NO_ACCESS + + # if target anchor have set access.all + if (to_access := to.access).all > AccessLevel.NO_ACCESS: + access_level = to_access.all + + # if target anchor's root have set allowed roots + # if current root is allowed to the whole graph of target anchor's root + if to.root and isinstance(to_root := jctx.mem.find_one(to.root), Anchor): + if to_root.access.all > access_level: + access_level = to_root.access.all + + level = to_root.access.roots.check(str(jroot.id)) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + # if target anchor have set allowed roots + # if current root is allowed to target anchor + level = to_access.roots.check(str(jroot.id)) + if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: + access_level = level + + return access_level + + +class JacNodeImpl: + """Jac Node Operations.""" + + @staticmethod + @hookimpl + def node_dot(node: NodeArchitype, dot_file: Optional[str]) -> str: + """Generate Dot file for visualizing nodes and edges.""" + visited_nodes: set[NodeAnchor] = set() + connections: set[tuple[NodeArchitype, NodeArchitype, str]] = set() + unique_node_id_dict = {} + + collect_node_connections(node.__jac__, visited_nodes, connections) + dot_content = 'digraph {\nnode [style="filled", shape="ellipse", fillcolor="invis", fontcolor="black"];\n' + for idx, i in enumerate([nodes_.architype for nodes_ in visited_nodes]): + unique_node_id_dict[i] = (i.__class__.__name__, str(idx)) + dot_content += f'{idx} [label="{i}"];\n' + dot_content += 'edge [color="gray", style="solid"];\n' + + for pair in list(set(connections)): + dot_content += ( + f"{unique_node_id_dict[pair[0]][1]} -> {unique_node_id_dict[pair[1]][1]}" + f' [label="{pair[2]}"];\n' + ) + if dot_file: + with open(dot_file, "w") as f: + f.write(dot_content + "}") + return dot_content + "}" + + @staticmethod + @hookimpl + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + ret_edges: list[EdgeArchitype] = [] + for anchor in node.edges: + if ( + (source := anchor.source) + and (target := anchor.target) + and (not filter_func or filter_func([anchor.architype])) + and source.architype + and target.architype + ): + if ( + dir in [EdgeDir.OUT, EdgeDir.ANY] + and node == source + and (not target_obj or target.architype in target_obj) + and Jac.check_read_access(target) + ): + ret_edges.append(anchor.architype) + if ( + dir in [EdgeDir.IN, EdgeDir.ANY] + and node == target + and (not target_obj or source.architype in target_obj) + and Jac.check_read_access(source) + ): + ret_edges.append(anchor.architype) + return ret_edges + + @staticmethod + @hookimpl + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + ret_edges: list[NodeArchitype] = [] + for anchor in node.edges: + if ( + (source := anchor.source) + and (target := anchor.target) + and (not filter_func or filter_func([anchor.architype])) + and source.architype + and target.architype + ): + if ( + dir in [EdgeDir.OUT, EdgeDir.ANY] + and node == source + and (not target_obj or target.architype in target_obj) + and Jac.check_read_access(target) + ): + ret_edges.append(target.architype) + if ( + dir in [EdgeDir.IN, EdgeDir.ANY] + and node == target + and (not target_obj or source.architype in target_obj) + and Jac.check_read_access(source) + ): + ret_edges.append(source.architype) + return ret_edges + + @staticmethod + @hookimpl + def remove_edge(node: NodeAnchor, edge: EdgeAnchor) -> None: + """Remove reference without checking sync status.""" + for idx, ed in enumerate(node.edges): + if ed.id == edge.id: + node.edges.pop(idx) + break + + +class JacEdgeImpl: + """Jac Edge Operations.""" + + @staticmethod + @hookimpl + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + Jac.remove_edge(node=edge.source, edge=edge) + Jac.remove_edge(node=edge.target, edge=edge) + + +class JacWalkerImpl: + """Jac Edge Operations.""" + + @staticmethod + @hookimpl + def visit_node( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: + """Jac's visit stmt feature.""" + if isinstance(walker, WalkerArchitype): + """Walker visits node.""" + wanch = walker.__jac__ + before_len = len(wanch.next) + for anchor in ( + (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] + ): + if anchor not in wanch.ignores: + if isinstance(anchor, NodeAnchor): + wanch.next.append(anchor) + elif isinstance(anchor, EdgeAnchor): + if target := anchor.target: + wanch.next.append(target) + else: + raise ValueError("Edge has no target.") + return len(wanch.next) > before_len + else: + raise TypeError("Invalid walker object") + + @staticmethod + @hookimpl + def ignore( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: + """Jac's ignore stmt feature.""" + if isinstance(walker, WalkerArchitype): + wanch = walker.__jac__ + before_len = len(wanch.ignores) + for anchor in ( + (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] + ): + if anchor not in wanch.ignores: + if isinstance(anchor, NodeAnchor): + wanch.ignores.append(anchor) + elif isinstance(anchor, EdgeAnchor): + if target := anchor.target: + wanch.ignores.append(target) + else: + raise ValueError("Edge has no target.") + return len(wanch.ignores) > before_len + else: + raise TypeError("Invalid walker object") + + @staticmethod + @hookimpl + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Invoke data spatial call.""" + if isinstance(op1, WalkerArchitype): + warch = op1 + walker = op1.__jac__ + if isinstance(op2, NodeArchitype): + node = op2.__jac__ + elif isinstance(op2, EdgeArchitype): + node = op2.__jac__.target + else: + raise TypeError("Invalid target object") + elif isinstance(op2, WalkerArchitype): + warch = op2 + walker = op2.__jac__ + if isinstance(op1, NodeArchitype): + node = op1.__jac__ + elif isinstance(op1, EdgeArchitype): + node = op1.__jac__.target + else: + raise TypeError("Invalid target object") + else: + raise TypeError("Invalid walker object") + + walker.path = [] + walker.next = [node] + if walker.next: + current_node = walker.next[-1].architype + for i in warch._jac_entry_funcs_: + if not i.trigger: + if i.func: + i.func(warch, current_node) + else: + raise ValueError(f"No function {i.name} to call.") + while len(walker.next): + if current_node := walker.next.pop(0).architype: + for i in current_node._jac_entry_funcs_: + if not i.trigger or isinstance(warch, i.trigger): + if i.func: + i.func(current_node, warch) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_entry_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func and i.trigger: + i.func(warch, current_node) + elif not i.trigger: + continue + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_exit_funcs_: + if not i.trigger or isinstance(current_node, i.trigger): + if i.func and i.trigger: + i.func(warch, current_node) + elif not i.trigger: + continue + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in current_node._jac_exit_funcs_: + if not i.trigger or isinstance(warch, i.trigger): + if i.func: + i.func(current_node, warch) + else: + raise ValueError(f"No function {i.name} to call.") + if walker.disengaged: + return warch + for i in warch._jac_exit_funcs_: + if not i.trigger: + if i.func: + i.func(warch, current_node) + else: + raise ValueError(f"No function {i.name} to call.") + walker.ignores = [] + return warch + + @staticmethod + @hookimpl + def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 + """Jac's disengage stmt feature.""" + walker.__jac__.disengaged = True + return True + + +class JacBuiltinImpl: + """Jac Builtins.""" + + @staticmethod + @hookimpl + def dotgen( + node: NodeArchitype, + depth: int, + traverse: bool, + edge_type: Optional[list[str]], + bfs: bool, + edge_limit: int, + node_limit: int, + dot_file: Optional[str], + ) -> str: + """Generate Dot file for visualizing nodes and edges.""" + edge_type = edge_type if edge_type else [] + visited_nodes: list[NodeArchitype] = [] + node_depths: dict[NodeArchitype, int] = {node: 0} + queue: list = [[node, 0]] + connections: list[tuple[NodeArchitype, NodeArchitype, EdgeArchitype]] = [] + + def dfs(node: NodeArchitype, cur_depth: int) -> None: + """Depth first search.""" + if node not in visited_nodes: + visited_nodes.append(node) + traverse_graph( + node, + cur_depth, + depth, + edge_type, + traverse, + connections, + node_depths, + visited_nodes, + queue, + bfs, + dfs, + node_limit, + edge_limit, + ) + + if bfs: + cur_depth = 0 + while queue: + current_node, cur_depth = queue.pop(0) + if current_node not in visited_nodes: + visited_nodes.append(current_node) + traverse_graph( + current_node, + cur_depth, + depth, + edge_type, + traverse, + connections, + node_depths, + visited_nodes, + queue, + bfs, + dfs, + node_limit, + edge_limit, + ) + else: + dfs(node, cur_depth=0) + dot_content = ( + 'digraph {\nnode [style="filled", shape="ellipse", ' + 'fillcolor="invis", fontcolor="black"];\n' + ) + for source, target, edge in connections: + dot_content += ( + f"{visited_nodes.index(source)} -> {visited_nodes.index(target)} " + f' [label="{html.escape(str(edge.__jac__.architype))} "];\n' + ) + for node_ in visited_nodes: + color = ( + colors[node_depths[node_]] if node_depths[node_] < 25 else colors[24] + ) + dot_content += ( + f'{visited_nodes.index(node_)} [label="{html.escape(str(node_.__jac__.architype))}"' + f'fillcolor="{color}"];\n' + ) + if dot_file: + with open(dot_file, "w") as f: + f.write(dot_content + "}") + return dot_content + "}" + + +class JacCmdImpl: + """Jac CLI command.""" + + @staticmethod + @hookimpl + def create_cmd() -> None: + """Create Jac CLI cmds.""" + pass + + +class JacFeatureImpl( + JacAccessValidationImpl, + JacNodeImpl, + JacEdgeImpl, + JacWalkerImpl, + JacBuiltinImpl, + JacCmdImpl, +): """Jac Feature.""" - pm = pluggy.PluginManager("jac") + @staticmethod + @hookimpl + def setup() -> None: + """Set Class References.""" + pass @staticmethod @hookimpl @@ -74,6 +563,7 @@ def get_context() -> ExecutionContext: @staticmethod @hookimpl def get_object(id: str) -> Architype | None: + """Get object by id.""" if id == "root": return Jac.get_context().root.architype elif obj := Jac.get_context().mem.find_by_id(UUID(id)): @@ -84,6 +574,7 @@ def get_object(id: str) -> Architype | None: @staticmethod @hookimpl def object_ref(obj: Architype) -> str: + """Get object's id.""" return obj.__jac__.id.hex @staticmethod @@ -361,69 +852,11 @@ def has_instance_default(gen_func: Callable[[], T]) -> T: """Jac's has container default feature.""" return field(default_factory=lambda: gen_func()) - @staticmethod - @hookimpl - def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: - """Jac's spawn operator feature.""" - if isinstance(op1, WalkerArchitype): - return op1.__jac__.spawn_call(op2.__jac__) - elif isinstance(op2, WalkerArchitype): - return op2.__jac__.spawn_call(op1.__jac__) - else: - raise TypeError("Invalid walker object") - @staticmethod @hookimpl def report(expr: Any) -> Any: # noqa: ANN401 """Jac's report stmt feature.""" - @staticmethod - @hookimpl - def ignore( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: - """Jac's ignore stmt feature.""" - if isinstance(walker, WalkerArchitype): - return walker.__jac__.ignore_node( - (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] - ) - else: - raise TypeError("Invalid walker object") - - @staticmethod - @hookimpl - def visit_node( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: - """Jac's visit stmt feature.""" - if isinstance(walker, WalkerArchitype): - return walker.__jac__.visit_node( - (i.__jac__ for i in expr) if isinstance(expr, list) else [expr.__jac__] - ) - else: - raise TypeError("Invalid walker object") - - @staticmethod - @hookimpl - def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 - """Jac's disengage stmt feature.""" - walker.__jac__.disengage_now() - return True - @staticmethod @hookimpl def edge_ref( @@ -444,16 +877,16 @@ def edge_ref( if edges_only: connected_edges: list[EdgeArchitype] = [] for node in node_obj: - connected_edges += node.__jac__.get_edges( - dir, filter_func, target_obj=targ_obj_set + connected_edges += Jac.get_edges( + node.__jac__, dir, filter_func, target_obj=targ_obj_set ) return list(set(connected_edges)) else: connected_nodes: list[NodeArchitype] = [] for node in node_obj: connected_nodes.extend( - node.__jac__.edges_to_nodes( - dir, filter_func, target_obj=targ_obj_set + Jac.edges_to_nodes( + node.__jac__, dir, filter_func, target_obj=targ_obj_set ) ) return list(set(connected_nodes)) @@ -474,14 +907,12 @@ def connect( right = [right] if isinstance(right, NodeArchitype) else right edges = [] - root = Jac.get_root().__jac__ - for i in left: _left = i.__jac__ - if root.has_connect_access(_left): + if Jac.check_connect_access(_left): for j in right: _right = j.__jac__ - if root.has_connect_access(_right): + if Jac.check_connect_access(_right): edges.append(edge_spec(_left, _right)) return right if not edges_only else edges @@ -498,8 +929,6 @@ def disconnect( left = [left] if isinstance(left, NodeArchitype) else left right = [right] if isinstance(right, NodeArchitype) else right - root = Jac.get_root().__jac__ - for i in left: node = i.__jac__ for anchor in set(node.edges): @@ -514,17 +943,17 @@ def disconnect( dir in [EdgeDir.OUT, EdgeDir.ANY] and node == source and target.architype in right - and root.has_write_access(target) + and Jac.check_write_access(target) ): - anchor.destroy() if anchor.persistent else anchor.detach() + Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True if ( dir in [EdgeDir.IN, EdgeDir.ANY] and node == target and source.architype in right - and root.has_write_access(source) + and Jac.check_write_access(source) ): - anchor.destroy() if anchor.persistent else anchor.detach() + Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True return disconnect_occurred @@ -565,7 +994,16 @@ def build_edge( def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: edge = conn_type() if isinstance(conn_type, type) else conn_type - edge.__attach__(source, target, is_undirected) + + eanch = edge.__jac__ = EdgeAnchor( + architype=edge, + source=source, + target=target, + is_undirected=is_undirected, + ) + source.edges.append(eanch) + target.edges.append(eanch) + if conn_assign: for fld, val in zip(conn_assign[0], conn_assign[1]): if hasattr(edge, fld): @@ -573,13 +1011,44 @@ def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: else: raise ValueError(f"Invalid attribute: {fld}") if source.persistent or target.persistent: - edge.__jac__.save() - target.save() - source.save() + Jac.save(eanch) + Jac.save(target) + Jac.save(source) return edge return builder + @staticmethod + @hookimpl + def save(obj: Architype | Anchor) -> None: + """Destroy object.""" + anchor = obj.__jac__ if isinstance(obj, Architype) else obj + + jctx = Jac.get_context() + + anchor.persistent = True + anchor.root = jctx.root.id + + jctx.mem.set(anchor.id, anchor) + + @staticmethod + @hookimpl + def destroy(obj: Architype | Anchor) -> None: + """Destroy object.""" + anchor = obj.__jac__ if isinstance(obj, Architype) else obj + + if Jac.check_write_access(anchor): + match anchor: + case NodeAnchor(): + for edge in anchor.edges: + Jac.destroy(edge) + case EdgeAnchor(): + Jac.detach(anchor) + case _: + pass + + Jac.get_context().mem.remove(anchor.id) + @staticmethod @hookimpl def get_semstr_type( @@ -645,6 +1114,7 @@ def obj_scope(file_loc: str, attr: str) -> str: @staticmethod @hookimpl def get_sem_type(file_loc: str, attr: str) -> tuple[str | None, str | None]: + """Jac's get_semstr_type implementation.""" with open( os.path.join( os.path.dirname(file_loc), @@ -843,101 +1313,3 @@ def get_by_llm_call_args(_pass: PyastGenPass, node: ast.FuncCall) -> dict: "include_info": [], "exclude_info": [], } - - -class JacBuiltin: - """Jac Builtins.""" - - @staticmethod - @hookimpl - def dotgen( - node: NodeArchitype, - depth: int, - traverse: bool, - edge_type: list[str], - bfs: bool, - edge_limit: int, - node_limit: int, - dot_file: Optional[str], - ) -> str: - """Generate Dot file for visualizing nodes and edges.""" - edge_type = edge_type if edge_type else [] - visited_nodes: list[NodeArchitype] = [] - node_depths: dict[NodeArchitype, int] = {node: 0} - queue: list = [[node, 0]] - connections: list[tuple[NodeArchitype, NodeArchitype, EdgeArchitype]] = [] - - def dfs(node: NodeArchitype, cur_depth: int) -> None: - """Depth first search.""" - if node not in visited_nodes: - visited_nodes.append(node) - traverse_graph( - node, - cur_depth, - depth, - edge_type, - traverse, - connections, - node_depths, - visited_nodes, - queue, - bfs, - dfs, - node_limit, - edge_limit, - ) - - if bfs: - cur_depth = 0 - while queue: - current_node, cur_depth = queue.pop(0) - if current_node not in visited_nodes: - visited_nodes.append(current_node) - traverse_graph( - current_node, - cur_depth, - depth, - edge_type, - traverse, - connections, - node_depths, - visited_nodes, - queue, - bfs, - dfs, - node_limit, - edge_limit, - ) - else: - dfs(node, cur_depth=0) - dot_content = ( - 'digraph {\nnode [style="filled", shape="ellipse", ' - 'fillcolor="invis", fontcolor="black"];\n' - ) - for source, target, edge in connections: - dot_content += ( - f"{visited_nodes.index(source)} -> {visited_nodes.index(target)} " - f' [label="{html.escape(str(edge.__jac__.architype))} "];\n' - ) - for node_ in visited_nodes: - color = ( - colors[node_depths[node_]] if node_depths[node_] < 25 else colors[24] - ) - dot_content += ( - f'{visited_nodes.index(node_)} [label="{html.escape(str(node_.__jac__.architype))}"' - f'fillcolor="{color}"];\n' - ) - if dot_file: - with open(dot_file, "w") as f: - f.write(dot_content + "}") - return dot_content + "}" - - -class JacCmdDefaults: - """Jac CLI command.""" - - @staticmethod - @hookimpl - def create_cmd() -> None: - """Create Jac CLI cmds.""" - pass diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index fd9fe49e19..3c0b0e7feb 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -4,57 +4,265 @@ import ast as ast3 import types -from typing import Any, Callable, Mapping, Optional, Sequence, Type, TypeAlias, Union +from typing import ( + Any, + Callable, + ClassVar, + Mapping, + Optional, + Sequence, + Type, + TypeAlias, + Union, +) +from uuid import UUID -import jaclang.compiler.absyntree as ast -from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass -from jaclang.plugin.spec import JacBuiltin, JacCmdSpec, JacFeatureSpec, P, T -from jaclang.runtimelib.constructs import ( +from jaclang.plugin.spec import ( + AccessLevel, + Anchor, Architype, + DSFunc, + EdgeAnchor, EdgeArchitype, + EdgeDir, + ExecutionContext, NodeAnchor, NodeArchitype, + P, + PyastGenPass, Root, + T, WalkerArchitype, + ast, + plugin_manager, ) -from jaclang.runtimelib.context import ExecutionContext -import pluggy -pm = pluggy.PluginManager("jac") -pm.add_hookspecs(JacFeatureSpec) -pm.add_hookspecs(JacCmdSpec) -pm.add_hookspecs(JacBuiltin) +class JacAccessValidation: + """Jac Access Validation Specs.""" + @staticmethod + def allow_root( + architype: Architype, + root_id: UUID, + level: AccessLevel | int | str = AccessLevel.READ, + ) -> None: + """Allow all access from target root graph to current Architype.""" + plugin_manager.hook.allow_root( + architype=architype, root_id=root_id, level=level + ) -class JacFeature: - """Jac Feature.""" + @staticmethod + def disallow_root( + architype: Architype, + root_id: UUID, + level: AccessLevel | int | str = AccessLevel.READ, + ) -> None: + """Disallow all access from target root graph to current Architype.""" + plugin_manager.hook.disallow_root( + architype=architype, root_id=root_id, level=level + ) + + @staticmethod + def unrestrict( + architype: Architype, level: AccessLevel | int | str = AccessLevel.READ + ) -> None: + """Allow everyone to access current Architype.""" + plugin_manager.hook.unrestrict(architype=architype, level=level) + + @staticmethod + def restrict(architype: Architype) -> None: + """Disallow others to access current Architype.""" + plugin_manager.hook.restrict(architype=architype) + + @staticmethod + def check_read_access(to: Anchor) -> bool: + """Read Access Validation.""" + return plugin_manager.hook.check_read_access(to=to) + + @staticmethod + def check_connect_access(to: Anchor) -> bool: + """Write Access Validation.""" + return plugin_manager.hook.check_connect_access(to=to) + + @staticmethod + def check_write_access(to: Anchor) -> bool: + """Write Access Validation.""" + return plugin_manager.hook.check_write_access(to=to) + + @staticmethod + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + return plugin_manager.hook.check_access_level(to=to) + + +class JacNode: + """Jac Node Operations.""" + + @staticmethod + def node_dot(node: NodeArchitype, dot_file: Optional[str] = None) -> str: + """Generate Dot file for visualizing nodes and edges.""" + return plugin_manager.hook.node_dot(node=node, dot_file=dot_file) + + @staticmethod + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + return plugin_manager.hook.get_edges( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj + ) + + @staticmethod + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + return plugin_manager.hook.edges_to_nodes( + node=node, dir=dir, filter_func=filter_func, target_obj=target_obj + ) + + @staticmethod + def remove_edge(node: NodeAnchor, edge: EdgeAnchor) -> None: + """Remove reference without checking sync status.""" + return plugin_manager.hook.remove_edge(node=node, edge=edge) + + +class JacEdge: + """Jac Edge Operations.""" + + @staticmethod + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + return plugin_manager.hook.detach(edge=edge) + + +class JacWalker: + """Jac Edge Operations.""" + + @staticmethod + def visit_node( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: # noqa: ANN401 + """Jac's visit stmt feature.""" + return plugin_manager.hook.visit_node(walker=walker, expr=expr) + + @staticmethod + def ignore( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: # noqa: ANN401 + """Jac's ignore stmt feature.""" + return plugin_manager.hook.ignore(walker=walker, expr=expr) + + @staticmethod + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Jac's spawn operator feature.""" + return plugin_manager.hook.spawn_call(op1=op1, op2=op2) + + @staticmethod + def disengage(walker: WalkerArchitype) -> bool: + """Jac's disengage stmt feature.""" + return plugin_manager.hook.disengage(walker=walker) + + +class JacClassReferences: + """Default Classes References.""" + + EdgeDir: ClassVar[TypeAlias] = EdgeDir + DSFunc: ClassVar[TypeAlias] = DSFunc + RootType: ClassVar[TypeAlias] = Root + Obj: ClassVar[TypeAlias] = Architype + Node: ClassVar[TypeAlias] = NodeArchitype + Edge: ClassVar[TypeAlias] = EdgeArchitype + Walker: ClassVar[TypeAlias] = WalkerArchitype + + +class JacBuiltin: + """Jac Builtins.""" + + @staticmethod + def dotgen( + node: NodeArchitype, + depth: int, + traverse: bool, + edge_type: Optional[list[str]], + bfs: bool, + edge_limit: int, + node_limit: int, + dot_file: Optional[str], + ) -> str: + """Generate Dot file for visualizing nodes and edges.""" + return plugin_manager.hook.dotgen( + node=node, + depth=depth, + traverse=traverse, + edge_type=edge_type, + bfs=bfs, + edge_limit=edge_limit, + node_limit=node_limit, + dot_file=dot_file, + ) + + +class JacCmd: + """Jac CLI command.""" - from jaclang.compiler.constant import EdgeDir as EdgeDirType - from jaclang.runtimelib.constructs import DSFunc as DSFuncType + @staticmethod + def create_cmd() -> None: + """Create Jac CLI cmds.""" + return plugin_manager.hook.create_cmd() + + +class JacFeature( + JacClassReferences, + JacAccessValidation, + JacNode, + JacEdge, + JacWalker, + JacBuiltin, + JacCmd, +): + """Jac Feature.""" - EdgeDir: TypeAlias = EdgeDirType - DSFunc: TypeAlias = DSFuncType - RootType: TypeAlias = Root - Obj: TypeAlias = Architype - Node: TypeAlias = NodeArchitype - Edge: TypeAlias = EdgeArchitype - Walker: TypeAlias = WalkerArchitype + @staticmethod + def setup() -> None: + """Set Class References.""" + plugin_manager.hook.setup() @staticmethod def get_context() -> ExecutionContext: """Get current execution context.""" - return pm.hook.get_context() + return plugin_manager.hook.get_context() @staticmethod def get_object(id: str) -> Architype | None: """Get object given id.""" - return pm.hook.get_object(id=id) + return plugin_manager.hook.get_object(id=id) @staticmethod def object_ref(obj: Architype) -> str: """Get object reference id.""" - return pm.hook.object_ref(obj=obj) + return plugin_manager.hook.object_ref(obj=obj) @staticmethod def make_architype( @@ -64,7 +272,7 @@ def make_architype( on_exit: list[DSFunc], ) -> Type[Architype]: """Create a obj architype.""" - return pm.hook.make_architype( + return plugin_manager.hook.make_architype( cls=cls, on_entry=on_entry, on_exit=on_exit, arch_base=arch_base ) @@ -73,35 +281,35 @@ def make_obj( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a obj architype.""" - return pm.hook.make_obj(on_entry=on_entry, on_exit=on_exit) + return plugin_manager.hook.make_obj(on_entry=on_entry, on_exit=on_exit) @staticmethod def make_node( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a node architype.""" - return pm.hook.make_node(on_entry=on_entry, on_exit=on_exit) + return plugin_manager.hook.make_node(on_entry=on_entry, on_exit=on_exit) @staticmethod def make_edge( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a edge architype.""" - return pm.hook.make_edge(on_entry=on_entry, on_exit=on_exit) + return plugin_manager.hook.make_edge(on_entry=on_entry, on_exit=on_exit) @staticmethod def make_walker( on_entry: list[DSFunc], on_exit: list[DSFunc] ) -> Callable[[type], type]: """Create a walker architype.""" - return pm.hook.make_walker(on_entry=on_entry, on_exit=on_exit) + return plugin_manager.hook.make_walker(on_entry=on_entry, on_exit=on_exit) @staticmethod def impl_patch_filename( file_loc: str, ) -> Callable[[Callable[P, T]], Callable[P, T]]: """Update impl file location.""" - return pm.hook.impl_patch_filename(file_loc=file_loc) + return plugin_manager.hook.impl_patch_filename(file_loc=file_loc) @staticmethod def jac_import( @@ -116,7 +324,7 @@ def jac_import( reload_module: Optional[bool] = False, ) -> tuple[types.ModuleType, ...]: """Core Import Process.""" - return pm.hook.jac_import( + return plugin_manager.hook.jac_import( target=target, base_path=base_path, absorb=absorb, @@ -131,7 +339,7 @@ def jac_import( @staticmethod def create_test(test_fun: Callable) -> Callable: """Create a test.""" - return pm.hook.create_test(test_fun=test_fun) + return plugin_manager.hook.create_test(test_fun=test_fun) @staticmethod def run_test( @@ -143,7 +351,7 @@ def run_test( verbose: bool = False, ) -> int: """Run the test suite in the specified .jac file.""" - return pm.hook.run_test( + return plugin_manager.hook.run_test( filepath=filepath, filter=filter, xit=xit, @@ -155,55 +363,17 @@ def run_test( @staticmethod def elvis(op1: Optional[T], op2: T) -> T: """Jac's elvis operator feature.""" - return pm.hook.elvis(op1=op1, op2=op2) + return plugin_manager.hook.elvis(op1=op1, op2=op2) @staticmethod def has_instance_default(gen_func: Callable[[], T]) -> T: """Jac's has container default feature.""" - return pm.hook.has_instance_default(gen_func=gen_func) - - @staticmethod - def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: - """Jac's spawn operator feature.""" - return pm.hook.spawn_call(op1=op1, op2=op2) + return plugin_manager.hook.has_instance_default(gen_func=gen_func) @staticmethod def report(expr: Any) -> Any: # noqa: ANN401 """Jac's report stmt feature.""" - return pm.hook.report(expr=expr) - - @staticmethod - def ignore( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: # noqa: ANN401 - """Jac's ignore stmt feature.""" - return pm.hook.ignore(walker=walker, expr=expr) - - @staticmethod - def visit_node( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: # noqa: ANN401 - """Jac's visit stmt feature.""" - return pm.hook.visit_node(walker=walker, expr=expr) - - @staticmethod - def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 - """Jac's disengage stmt feature.""" - return pm.hook.disengage(walker=walker) + return plugin_manager.hook.report(expr=expr) @staticmethod def edge_ref( @@ -214,7 +384,7 @@ def edge_ref( edges_only: bool = False, ) -> list[NodeArchitype] | list[EdgeArchitype]: """Jac's apply_dir stmt feature.""" - return pm.hook.edge_ref( + return plugin_manager.hook.edge_ref( node_obj=node_obj, target_obj=target_obj, dir=dir, @@ -233,7 +403,7 @@ def connect( Note: connect needs to call assign compr with tuple in op """ - return pm.hook.connect( + return plugin_manager.hook.connect( left=left, right=right, edge_spec=edge_spec, edges_only=edges_only ) @@ -245,7 +415,7 @@ def disconnect( filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], ) -> bool: """Jac's disconnect operator feature.""" - return pm.hook.disconnect( + return plugin_manager.hook.disconnect( left=left, right=right, dir=dir, @@ -257,17 +427,17 @@ def assign_compr( target: list[T], attr_val: tuple[tuple[str], tuple[Any]] ) -> list[T]: """Jac's assign comprehension feature.""" - return pm.hook.assign_compr(target=target, attr_val=attr_val) + return plugin_manager.hook.assign_compr(target=target, attr_val=attr_val) @staticmethod def get_root() -> Root: """Jac's root getter.""" - return pm.hook.get_root() + return plugin_manager.hook.get_root() @staticmethod def get_root_type() -> Type[Root]: """Jac's root type getter.""" - return pm.hook.get_root_type() + return plugin_manager.hook.get_root_type() @staticmethod def build_edge( @@ -276,28 +446,42 @@ def build_edge( conn_assign: Optional[tuple[tuple, tuple]], ) -> Callable[[NodeAnchor, NodeAnchor], EdgeArchitype]: """Jac's root getter.""" - return pm.hook.build_edge( + return plugin_manager.hook.build_edge( is_undirected=is_undirected, conn_type=conn_type, conn_assign=conn_assign ) + @staticmethod + def save( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + plugin_manager.hook.save(obj=obj) + + @staticmethod + def destroy( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + plugin_manager.hook.destroy(obj=obj) + @staticmethod def get_semstr_type( file_loc: str, scope: str, attr: str, return_semstr: bool ) -> Optional[str]: """Jac's get_semstr_type feature.""" - return pm.hook.get_semstr_type( + return plugin_manager.hook.get_semstr_type( file_loc=file_loc, scope=scope, attr=attr, return_semstr=return_semstr ) @staticmethod def obj_scope(file_loc: str, attr: str) -> str: """Jac's get_semstr_type feature.""" - return pm.hook.obj_scope(file_loc=file_loc, attr=attr) + return plugin_manager.hook.obj_scope(file_loc=file_loc, attr=attr) @staticmethod def get_sem_type(file_loc: str, attr: str) -> tuple[str | None, str | None]: """Jac's get_semstr_type feature.""" - return pm.hook.get_sem_type(file_loc=file_loc, attr=attr) + return plugin_manager.hook.get_sem_type(file_loc=file_loc, attr=attr) @staticmethod def with_llm( @@ -314,7 +498,7 @@ def with_llm( _locals: Mapping, ) -> Any: # noqa: ANN401 """Jac's with_llm feature.""" - return pm.hook.with_llm( + return plugin_manager.hook.with_llm( file_loc=file_loc, model=model, model_params=model_params, @@ -331,7 +515,7 @@ def with_llm( @staticmethod def gen_llm_body(_pass: PyastGenPass, node: ast.Ability) -> list[ast3.AST]: """Generate the by LLM body.""" - return pm.hook.gen_llm_body(_pass=_pass, node=node) + return plugin_manager.hook.gen_llm_body(_pass=_pass, node=node) @staticmethod def by_llm_call( @@ -346,7 +530,7 @@ def by_llm_call( exclude_info: list[tuple[str, ast3.AST]], ) -> ast3.Call: """Return the LLM Call, e.g. _Jac.with_llm().""" - return pm.hook.by_llm_call( + return plugin_manager.hook.by_llm_call( _pass=_pass, model=model, model_params=model_params, @@ -361,13 +545,4 @@ def by_llm_call( @staticmethod def get_by_llm_call_args(_pass: PyastGenPass, node: ast.FuncCall) -> dict: """Get the by LLM call args.""" - return pm.hook.get_by_llm_call_args(_pass=_pass, node=node) - - -class JacCmd: - """Jac CLI command.""" - - @staticmethod - def create_cmd() -> None: - """Create Jac CLI cmds.""" - return pm.hook.create_cmd() + return plugin_manager.hook.get_by_llm_call_args(_pass=_pass, node=node) diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index e5fccc63dc..4b6f173388 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -11,37 +11,233 @@ Optional, ParamSpec, Sequence, - TYPE_CHECKING, Type, TypeVar, Union, ) +from uuid import UUID -import jaclang.compiler.absyntree as ast +from jaclang.compiler import absyntree as ast +from jaclang.compiler.constant import EdgeDir from jaclang.compiler.passes.main.pyast_gen_pass import PyastGenPass - -if TYPE_CHECKING: - from jaclang.plugin.default import ( - Architype, - EdgeDir, - WalkerArchitype, - Root, - DSFunc, - ) - from jaclang.runtimelib.constructs import EdgeArchitype, NodeAnchor, NodeArchitype - from jaclang.runtimelib.context import ExecutionContext +from jaclang.runtimelib.constructs import ( + AccessLevel, + Anchor, + Architype, + DSFunc, + EdgeAnchor, + EdgeArchitype, + NodeAnchor, + NodeArchitype, + Root, + WalkerArchitype, +) +from jaclang.runtimelib.context import ExecutionContext import pluggy hookspec = pluggy.HookspecMarker("jac") +plugin_manager = pluggy.PluginManager("jac") T = TypeVar("T") P = ParamSpec("P") -class JacFeatureSpec: +class JacAccessValidationSpec: + """Jac Access Validation Specs.""" + + @staticmethod + @hookspec(firstresult=True) + def allow_root( + architype: Architype, root_id: UUID, level: AccessLevel | int | str + ) -> None: + """Allow all access from target root graph to current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def disallow_root( + architype: Architype, root_id: UUID, level: AccessLevel | int | str + ) -> None: + """Disallow all access from target root graph to current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def unrestrict(architype: Architype, level: AccessLevel | int | str) -> None: + """Allow everyone to access current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def restrict(architype: Architype) -> None: + """Disallow others to access current Architype.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_read_access(to: Anchor) -> bool: + """Read Access Validation.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_connect_access(to: Anchor) -> bool: + """Write Access Validation.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_write_access(to: Anchor) -> bool: + """Write Access Validation.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def check_access_level(to: Anchor) -> AccessLevel: + """Access validation.""" + raise NotImplementedError + + +class JacNodeSpec: + """Jac Node Operations.""" + + @staticmethod + @hookspec(firstresult=True) + def node_dot(node: NodeArchitype, dot_file: Optional[str]) -> str: + """Generate Dot file for visualizing nodes and edges.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def get_edges( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[EdgeArchitype]: + """Get edges connected to this node.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def edges_to_nodes( + node: NodeAnchor, + dir: EdgeDir, + filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], + target_obj: Optional[list[NodeArchitype]], + ) -> list[NodeArchitype]: + """Get set of nodes connected to this node.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def remove_edge(node: NodeAnchor, edge: EdgeAnchor) -> None: + """Remove reference without checking sync status.""" + raise NotImplementedError + + +class JacEdgeSpec: + """Jac Edge Operations.""" + + @staticmethod + @hookspec(firstresult=True) + def detach(edge: EdgeAnchor) -> None: + """Detach edge from nodes.""" + raise NotImplementedError + + +class JacWalkerSpec: + """Jac Edge Operations.""" + + @staticmethod + @hookspec(firstresult=True) + def visit_node( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: # noqa: ANN401 + """Jac's visit stmt feature.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def ignore( + walker: WalkerArchitype, + expr: ( + list[NodeArchitype | EdgeArchitype] + | list[NodeArchitype] + | list[EdgeArchitype] + | NodeArchitype + | EdgeArchitype + ), + ) -> bool: + """Jac's ignore stmt feature.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: + """Invoke data spatial call.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def disengage(walker: WalkerArchitype) -> bool: + """Jac's disengage stmt feature.""" + raise NotImplementedError + + +class JacBuiltinSpec: + """Jac Builtins.""" + + @staticmethod + @hookspec(firstresult=True) + def dotgen( + node: NodeArchitype, + depth: int, + traverse: bool, + edge_type: Optional[list[str]], + bfs: bool, + edge_limit: int, + node_limit: int, + dot_file: Optional[str], + ) -> str: + """Print the dot graph.""" + raise NotImplementedError + + +class JacCmdSpec: + """Jac CLI command.""" + + @staticmethod + @hookspec + def create_cmd() -> None: + """Create Jac CLI cmds.""" + raise NotImplementedError + + +class JacFeatureSpec( + JacAccessValidationSpec, + JacNodeSpec, + JacEdgeSpec, + JacWalkerSpec, + JacBuiltinSpec, + JacCmdSpec, +): """Jac Feature.""" + @staticmethod + @hookspec(firstresult=True) + def setup() -> None: + """Set Class References.""" + raise NotImplementedError + @staticmethod @hookspec(firstresult=True) def get_context() -> ExecutionContext: @@ -51,13 +247,13 @@ def get_context() -> ExecutionContext: @staticmethod @hookspec(firstresult=True) def get_object(id: str) -> Architype | None: - """Get object given id..""" + """Get object by id.""" raise NotImplementedError @staticmethod @hookspec(firstresult=True) def object_ref(obj: Architype) -> str: - """Get object given id..""" + """Get object's id.""" raise NotImplementedError @staticmethod @@ -158,54 +354,12 @@ def has_instance_default(gen_func: Callable[[], T]) -> T: """Jac's has container default feature.""" raise NotImplementedError - @staticmethod - @hookspec(firstresult=True) - def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: - """Jac's spawn operator feature.""" - raise NotImplementedError - @staticmethod @hookspec(firstresult=True) def report(expr: Any) -> Any: # noqa: ANN401 """Jac's report stmt feature.""" raise NotImplementedError - @staticmethod - @hookspec(firstresult=True) - def ignore( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: - """Jac's ignore stmt feature.""" - raise NotImplementedError - - @staticmethod - @hookspec(firstresult=True) - def visit_node( - walker: WalkerArchitype, - expr: ( - list[NodeArchitype | EdgeArchitype] - | list[NodeArchitype] - | list[EdgeArchitype] - | NodeArchitype - | EdgeArchitype - ), - ) -> bool: # noqa: ANN401 - """Jac's visit stmt feature.""" - raise NotImplementedError - - @staticmethod - @hookspec(firstresult=True) - def disengage(walker: WalkerArchitype) -> bool: # noqa: ANN401 - """Jac's disengage stmt feature.""" - raise NotImplementedError - @staticmethod @hookspec(firstresult=True) def edge_ref( @@ -273,6 +427,22 @@ def build_edge( """Jac's root getter.""" raise NotImplementedError + @staticmethod + @hookspec(firstresult=True) + def save( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + raise NotImplementedError + + @staticmethod + @hookspec(firstresult=True) + def destroy( + obj: Architype | Anchor, + ) -> None: + """Destroy object.""" + raise NotImplementedError + @staticmethod @hookspec(firstresult=True) def get_semstr_type( @@ -339,30 +509,4 @@ def get_by_llm_call_args(_pass: PyastGenPass, node: ast.FuncCall) -> dict: raise NotImplementedError -class JacBuiltin: - """Jac Builtins.""" - - @staticmethod - @hookspec(firstresult=True) - def dotgen( - node: NodeArchitype, - depth: int, - traverse: bool, - edge_type: list[str], - bfs: bool, - edge_limit: int, - node_limit: int, - dot_file: Optional[str], - ) -> str: - """Print the dot graph.""" - raise NotImplementedError - - -class JacCmdSpec: - """Jac CLI command.""" - - @staticmethod - @hookspec - def create_cmd() -> None: - """Create Jac CLI cmds.""" - raise NotImplementedError +plugin_manager.add_hookspecs(JacFeatureSpec) diff --git a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac index df5192d9ec..384587fe7f 100644 --- a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac +++ b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac @@ -36,7 +36,7 @@ walker create_node { walker create_other_root { can enter with `root entry { other_root = `root().__jac__; - other_root.save(); + Jac.save(other_root); print(other_root.id); } } @@ -46,17 +46,17 @@ walker allow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here, self.level); } else { - here.__jac__.allow_root(UUID(self.root_id), self.level); + Jac.allow_root(here, UUID(self.root_id), self.level); } } can enter_nested with A entry { if self.via_all { - here.__jac__.unrestrict(self.level); + Jac.unrestrict(here, self.level); } else { - here.__jac__.allow_root(UUID(self.root_id), self.level); + Jac.allow_root(here, UUID(self.root_id), self.level); } } } @@ -66,17 +66,17 @@ walker disallow_other_root_access { can enter_root with `root entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here); } else { - here.__jac__.disallow_root(UUID(self.root_id)); + Jac.disallow_root(here, UUID(self.root_id)); } } can enter_nested with A entry { if self.via_all { - here.__jac__.restrict(); + Jac.restrict(here); } else { - here.__jac__.disallow_root(UUID(self.root_id)); + Jac.disallow_root(here, UUID(self.root_id)); } } } \ No newline at end of file diff --git a/jac/jaclang/plugin/tests/test_features.py b/jac/jaclang/plugin/tests/test_features.py index 22a3f99faa..6c3e8e0afa 100644 --- a/jac/jaclang/plugin/tests/test_features.py +++ b/jac/jaclang/plugin/tests/test_features.py @@ -3,7 +3,7 @@ import inspect from typing import List, Type -from jaclang.plugin.default import JacFeatureDefaults +from jaclang.plugin.default import JacFeatureImpl from jaclang.plugin.feature import JacFeature from jaclang.plugin.spec import JacFeatureSpec from jaclang.utils.test import TestCase @@ -46,7 +46,7 @@ def test_feature_funcs_synced(self) -> None: """Test if JacFeature, JacFeatureDefaults, and JacFeatureSpec have synced methods.""" # Get methods of each class jac_feature_methods = self.get_methods(JacFeature) - jac_feature_defaults_methods = self.get_methods(JacFeatureDefaults) + jac_feature_defaults_methods = self.get_methods(JacFeatureImpl) jac_feature_spec_methods = self.get_methods(JacFeatureSpec) # Check if all methods are the same in all classes diff --git a/jac/jaclang/runtimelib/architype.py b/jac/jaclang/runtimelib/architype.py index c7979fa557..6c1ad9a55c 100644 --- a/jac/jaclang/runtimelib/architype.py +++ b/jac/jaclang/runtimelib/architype.py @@ -7,12 +7,9 @@ from logging import getLogger from pickle import dumps from types import UnionType -from typing import Any, Callable, ClassVar, Iterable, Optional, TypeVar +from typing import Any, Callable, ClassVar, Optional, TypeVar from uuid import UUID, uuid4 -from jaclang.compiler.constant import EdgeDir -from jaclang.runtimelib.utils import collect_node_connections - logger = getLogger(__name__) TARCH = TypeVar("TARCH", bound="Architype") @@ -77,128 +74,6 @@ class Anchor: persistent: bool = False hash: int = 0 - ########################################################################## - # ACCESS CONTROL: TODO: Make Base Type # - ########################################################################## - - def allow_root( - self, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Allow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - - _root_id = str(root_id) - if level != access.anchors.get(_root_id, AccessLevel.NO_ACCESS): - access.anchors[_root_id] = level - - def disallow_root( - self, root_id: UUID, level: AccessLevel | int | str = AccessLevel.READ - ) -> None: - """Disallow all access from target root graph to current Architype.""" - level = AccessLevel.cast(level) - access = self.access.roots - - access.anchors.pop(str(root_id), None) - - def unrestrict(self, level: AccessLevel | int | str = AccessLevel.READ) -> None: - """Allow everyone to access current Architype.""" - level = AccessLevel.cast(level) - if level != self.access.all: - self.access.all = level - - def restrict(self) -> None: - """Disallow others to access current Architype.""" - if self.access.all > AccessLevel.NO_ACCESS: - self.access.all = AccessLevel.NO_ACCESS - - def has_read_access(self, to: Anchor) -> bool: - """Read Access Validation.""" - if not (access_level := self.access_level(to) > AccessLevel.NO_ACCESS): - logger.info( - f"Current root doesn't have read access to {to.__class__.__name__}[{to.id}]" - ) - return access_level - - def has_connect_access(self, to: Anchor) -> bool: - """Write Access Validation.""" - if not (access_level := self.access_level(to) > AccessLevel.READ): - logger.info( - f"Current root doesn't have connect access to {to.__class__.__name__}[{to.id}]" - ) - return access_level - - def has_write_access(self, to: Anchor) -> bool: - """Write Access Validation.""" - if not (access_level := self.access_level(to) > AccessLevel.CONNECT): - logger.info( - f"Current root doesn't have write access to {to.__class__.__name__}[{to.id}]" - ) - return access_level - - def access_level(self, to: Anchor) -> AccessLevel: - """Access validation.""" - if not to.persistent: - return AccessLevel.WRITE - - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - jroot = jctx.root - - # if current root is system_root - # if current root id is equal to target anchor's root id - # if current root is the target anchor - if jroot == jctx.system_root or jroot.id == to.root or jroot == to: - return AccessLevel.WRITE - - access_level = AccessLevel.NO_ACCESS - - # if target anchor have set access.all - if (to_access := to.access).all > AccessLevel.NO_ACCESS: - access_level = to_access.all - - # if target anchor's root have set allowed roots - # if current root is allowed to the whole graph of target anchor's root - if to.root and isinstance(to_root := jctx.mem.find_one(to.root), Anchor): - if to_root.access.all > access_level: - access_level = to_root.access.all - - level = to_root.access.roots.check(str(jroot.id)) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - # if target anchor have set allowed roots - # if current root is allowed to target anchor - level = to_access.roots.check(str(jroot.id)) - if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: - access_level = level - - return access_level - - # ---------------------------------------------------------------------- # - - def save(self) -> None: - """Save Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - self.persistent = True - self.root = jctx.root.id - - jctx.mem.set(self.id, self) - - def destroy(self) -> None: - """Destroy Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - if jctx.root.has_write_access(self): - jctx.mem.remove(self.id) - def is_populated(self) -> bool: """Check if state.""" return "architype" in self.__dict__ @@ -304,122 +179,6 @@ class NodeAnchor(Anchor): architype: NodeArchitype edges: list[EdgeAnchor] - def get_edges( - self, - dir: EdgeDir, - filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], - target_obj: Optional[list[NodeArchitype]], - ) -> list[EdgeArchitype]: - """Get edges connected to this node.""" - from jaclang.plugin.feature import JacFeature as Jac - - root = Jac.get_root().__jac__ - ret_edges: list[EdgeArchitype] = [] - for anchor in self.edges: - if ( - (source := anchor.source) - and (target := anchor.target) - and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype - ): - if ( - dir in [EdgeDir.OUT, EdgeDir.ANY] - and self == source - and (not target_obj or target.architype in target_obj) - and root.has_read_access(target) - ): - ret_edges.append(anchor.architype) - if ( - dir in [EdgeDir.IN, EdgeDir.ANY] - and self == target - and (not target_obj or source.architype in target_obj) - and root.has_read_access(source) - ): - ret_edges.append(anchor.architype) - return ret_edges - - def edges_to_nodes( - self, - dir: EdgeDir, - filter_func: Optional[Callable[[list[EdgeArchitype]], list[EdgeArchitype]]], - target_obj: Optional[list[NodeArchitype]], - ) -> list[NodeArchitype]: - """Get set of nodes connected to this node.""" - from jaclang.plugin.feature import JacFeature as Jac - - root = Jac.get_root().__jac__ - ret_edges: list[NodeArchitype] = [] - for anchor in self.edges: - if ( - (source := anchor.source) - and (target := anchor.target) - and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype - ): - if ( - dir in [EdgeDir.OUT, EdgeDir.ANY] - and self == source - and (not target_obj or target.architype in target_obj) - and root.has_read_access(target) - ): - ret_edges.append(target.architype) - if ( - dir in [EdgeDir.IN, EdgeDir.ANY] - and self == target - and (not target_obj or source.architype in target_obj) - and root.has_read_access(source) - ): - ret_edges.append(source.architype) - return ret_edges - - def remove_edge(self, edge: EdgeAnchor) -> None: - """Remove reference without checking sync status.""" - for idx, ed in enumerate(self.edges): - if ed.id == edge.id: - self.edges.pop(idx) - break - - def gen_dot(self, dot_file: Optional[str] = None) -> str: - """Generate Dot file for visualizing nodes and edges.""" - visited_nodes: set[NodeAnchor] = set() - connections: set[tuple[NodeArchitype, NodeArchitype, str]] = set() - unique_node_id_dict = {} - - collect_node_connections(self, visited_nodes, connections) - dot_content = 'digraph {\nnode [style="filled", shape="ellipse", fillcolor="invis", fontcolor="black"];\n' - for idx, i in enumerate([nodes_.architype for nodes_ in visited_nodes]): - unique_node_id_dict[i] = (i.__class__.__name__, str(idx)) - dot_content += f'{idx} [label="{i}"];\n' - dot_content += 'edge [color="gray", style="solid"];\n' - - for pair in list(set(connections)): - dot_content += ( - f"{unique_node_id_dict[pair[0]][1]} -> {unique_node_id_dict[pair[1]][1]}" - f' [label="{pair[2]}"];\n' - ) - if dot_file: - with open(dot_file, "w") as f: - f.write(dot_content + "}") - return dot_content + "}" - - def spawn_call(self, walk: WalkerAnchor) -> WalkerArchitype: - """Invoke data spatial call.""" - return walk.spawn_call(self) - - def destroy(self) -> None: - """Destroy Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - if jctx.root.has_write_access(self): - for edge in self.edges: - edge.destroy() - - jctx.mem.remove(self.id) - def __getstate__(self) -> dict[str, object]: """Serialize Node Anchor.""" state = super().__getstate__() @@ -439,30 +198,6 @@ class EdgeAnchor(Anchor): target: NodeAnchor is_undirected: bool - def __post_init__(self) -> None: - """Populate edge to source and target.""" - self.source.edges.append(self) - self.target.edges.append(self) - - def detach(self) -> None: - """Detach edge from nodes.""" - self.source.remove_edge(self) - self.target.remove_edge(self) - - def spawn_call(self, walk: WalkerAnchor) -> WalkerArchitype: - """Invoke data spatial call.""" - return walk.spawn_call(self.target) - - def destroy(self) -> None: - """Destroy Anchor.""" - from jaclang.plugin.feature import JacFeature as Jac - - jctx = Jac.get_context() - - if jctx.root.has_write_access(self): - self.detach() - jctx.mem.remove(self.id) - def __getstate__(self) -> dict[str, object]: """Serialize Node Anchor.""" state = super().__getstate__() @@ -489,99 +224,6 @@ class WalkerAnchor(Anchor): ignores: list[Anchor] = field(default_factory=list) disengaged: bool = False - def visit_node(self, anchors: Iterable[NodeAnchor | EdgeAnchor]) -> bool: - """Walker visits node.""" - before_len = len(self.next) - for anchor in anchors: - if anchor not in self.ignores: - if isinstance(anchor, NodeAnchor): - self.next.append(anchor) - elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: - self.next.append(target) - else: - raise ValueError("Edge has no target.") - return len(self.next) > before_len - - def ignore_node(self, anchors: Iterable[NodeAnchor | EdgeAnchor]) -> bool: - """Walker ignores node.""" - before_len = len(self.ignores) - for anchor in anchors: - if anchor not in self.ignores: - if isinstance(anchor, NodeAnchor): - self.ignores.append(anchor) - elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: - self.ignores.append(target) - else: - raise ValueError("Edge has no target.") - return len(self.ignores) > before_len - - def disengage_now(self) -> None: - """Disengage walker from traversal.""" - self.disengaged = True - - def spawn_call(self, node: Anchor) -> WalkerArchitype: - """Invoke data spatial call.""" - if walker := self.architype: - self.path = [] - self.next = [node] - if self.next: - current_node = self.next[-1].architype - for i in walker._jac_entry_funcs_: - if not i.trigger: - if i.func: - i.func(walker, current_node) - else: - raise ValueError(f"No function {i.name} to call.") - while len(self.next): - if current_node := self.next.pop(0).architype: - for i in current_node._jac_entry_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - i.func(current_node, walker) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_entry_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func and i.trigger: - i.func(walker, current_node) - elif not i.trigger: - continue - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_exit_funcs_: - if not i.trigger or isinstance(current_node, i.trigger): - if i.func and i.trigger: - i.func(walker, current_node) - elif not i.trigger: - continue - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in current_node._jac_exit_funcs_: - if not i.trigger or isinstance(walker, i.trigger): - if i.func: - i.func(current_node, walker) - else: - raise ValueError(f"No function {i.name} to call.") - if self.disengaged: - return walker - for i in walker._jac_exit_funcs_: - if not i.trigger: - if i.func: - i.func(walker, current_node) - else: - raise ValueError(f"No function {i.name} to call.") - self.ignores = [] - return walker - raise Exception(f"Invalid Reference {self.id}") - class Architype: """Architype Protocol.""" @@ -613,17 +255,6 @@ class EdgeArchitype(Architype): __jac__: EdgeAnchor - def __attach__( - self, - source: NodeAnchor, - target: NodeAnchor, - is_undirected: bool, - ) -> None: - """Attach EdgeAnchor properly.""" - self.__jac__ = EdgeAnchor( - architype=self, source=source, target=target, is_undirected=is_undirected - ) - class WalkerArchitype(Architype): """Walker Architype Protocol.""" diff --git a/jac/jaclang/runtimelib/constructs.py b/jac/jaclang/runtimelib/constructs.py index 655bd4bc00..b39b0abf0d 100644 --- a/jac/jaclang/runtimelib/constructs.py +++ b/jac/jaclang/runtimelib/constructs.py @@ -4,6 +4,7 @@ from .architype import ( + AccessLevel, Anchor, Architype, DSFunc, @@ -21,6 +22,7 @@ from .test import JacTestCheck, JacTestResult, JacTextTestRunner __all__ = [ + "AccessLevel", "Anchor", "NodeAnchor", "EdgeAnchor", diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index 750ab83484..b300cb430a 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -42,10 +42,6 @@ def init_anchor( raise ValueError(f"Invalid anchor id {anchor_id} !") return default - def validate_access(self) -> bool: - """Validate access.""" - return self.root.has_read_access(self.entry_node) - def set_entry_node(self, entry_node: str | None) -> None: """Override entry.""" self.entry_node = self.init_anchor(entry_node, self.root) diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index a65576468c..86773ed162 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -82,8 +82,6 @@ def close(self) -> None: if isinstance(self.__shelf__, Shelf): from jaclang.plugin.feature import JacFeature as Jac - root = Jac.get_root().__jac__ - for anchor in self.__gc__: self.__shelf__.pop(str(anchor.id), None) self.__mem__.pop(anchor.id, None) @@ -96,14 +94,14 @@ def close(self) -> None: isinstance(p_d, NodeAnchor) and isinstance(d, NodeAnchor) and p_d.edges != d.edges - and root.has_connect_access(d) + and Jac.check_connect_access(d) ): if not d.edges: self.__shelf__.pop(_id, None) continue p_d.edges = d.edges - if root.has_write_access(d): + if Jac.check_write_access(d): if hash(dumps(p_d.access)) != hash(dumps(d.access)): p_d.access = d.access if hash(dumps(p_d.architype)) != hash(dumps(d.architype)): diff --git a/jac/jaclang/tests/fixtures/edge_node_walk.jac b/jac/jaclang/tests/fixtures/edge_node_walk.jac index c4ba4b0b61..7901930743 100644 --- a/jac/jaclang/tests/fixtures/edge_node_walk.jac +++ b/jac/jaclang/tests/fixtures/edge_node_walk.jac @@ -37,7 +37,7 @@ edge Edge_c { with entry { print(root spawn creator()); - print(root.__jac__.gen_dot()); + print(Jac.node_dot(root)); print([root-:Edge_a:->]); print([root-:Edge_c:->]); print([root-:Edge_a:->-:Edge_b:->]); diff --git a/jac/jaclang/tests/fixtures/edges_walk.jac b/jac/jaclang/tests/fixtures/edges_walk.jac index b5b242f0ca..aaef4dd15a 100644 --- a/jac/jaclang/tests/fixtures/edges_walk.jac +++ b/jac/jaclang/tests/fixtures/edges_walk.jac @@ -30,7 +30,7 @@ edge Edge_c{ with entry{ print(root spawn creator()); - print(root.__jac__.gen_dot()); + print(Jac.node_dot(root)); print([root -:Edge_a:->]); print([root -:Edge_c:->]); print([root -:Edge_a:-> -:Edge_b:->]); diff --git a/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac b/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac index 30c8003bbb..8348fc1fee 100644 --- a/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac +++ b/jac/jaclang/tests/fixtures/gendot_bubble_sort.jac @@ -73,5 +73,5 @@ with entry { root spawn walker1(); root spawn walker2(); root spawn walker3(); - print(root.__jac__.gen_dot()); + print(Jac.node_dot(root)); } diff --git a/jac/jaclang/tests/test_language.py b/jac/jaclang/tests/test_language.py index 3c71339e5c..3a8c5c2da7 100644 --- a/jac/jaclang/tests/test_language.py +++ b/jac/jaclang/tests/test_language.py @@ -80,6 +80,18 @@ def test_simple_jac_red(self) -> None: "\nValue: 5\nValue: 6\nValue: 7\nFinal Value: 8\nDone walking.\n", ) + def test_simple_walk_by_edge(self) -> None: + """Parse micro jac file.""" + captured_output = io.StringIO() + sys.stdout = captured_output + jac_import("micro.simple_walk_by_edge", base_path=self.examples_abs_path("")) + sys.stdout = sys.__stdout__ + stdout_value = captured_output.getvalue() + self.assertEqual( + stdout_value, + "Visited 1\nVisited 2\n", + ) + def test_guess_game(self) -> None: """Parse micro jac file.""" captured_output = io.StringIO()