diff --git a/jac-cloud/jac_cloud/core/architype.py b/jac-cloud/jac_cloud/core/architype.py index c893c7cbb5..7679b1f7d3 100644 --- a/jac-cloud/jac_cloud/core/architype.py +++ b/jac-cloud/jac_cloud/core/architype.py @@ -9,6 +9,7 @@ is_dataclass, ) from enum import Enum +from functools import cached_property from os import getenv from pickle import dumps as pdumps from re import IGNORECASE, compile @@ -18,6 +19,7 @@ ClassVar, Iterable, Mapping, + Type, TypeVar, cast, get_args, @@ -35,12 +37,12 @@ DSFunc, EdgeAnchor as _EdgeAnchor, EdgeArchitype as _EdgeArchitype, + JID, NodeAnchor as _NodeAnchor, NodeArchitype as _NodeArchitype, ObjectAnchor as _ObjectAnchor, ObjectArchitype as _ObjectArchitype, Permission as _Permission, - TANCH, WalkerAnchor as _WalkerAnchor, WalkerArchitype as _WalkerArchitype, ) @@ -56,14 +58,20 @@ from ..jaseci.utils import logger MANUAL_SAVE = getenv("MANUAL_SAVE") -GENERIC_ID_REGEX = compile(r"^(n|e|w|o):([^:]*):([a-f\d]{24})$", IGNORECASE) -NODE_ID_REGEX = compile(r"^n:([^:]*):([a-f\d]{24})$", IGNORECASE) -EDGE_ID_REGEX = compile(r"^e:([^:]*):([a-f\d]{24})$", IGNORECASE) -WALKER_ID_REGEX = compile(r"^w:([^:]*):([a-f\d]{24})$", IGNORECASE) -OBJECT_ID_REGEX = compile(r"^o:([^:]*):([a-f\d]{24})$", IGNORECASE) +JID_REGEX = compile(r"^(n|e|w|o):([^:]*):([a-f\d]{24})$", IGNORECASE) T = TypeVar("T") TBA = TypeVar("TBA", bound="BaseArchitype") +_ANCHOR = TypeVar("_ANCHOR", "NodeAnchor", "EdgeAnchor", "WalkerAnchor", "ObjectAnchor") +_C_ANCHOR = TypeVar( + "_C_ANCHOR", + "NodeAnchor", + "EdgeAnchor", + "WalkerAnchor", + "ObjectAnchor", + covariant=True, +) + def asdict_factory(data: Iterable[tuple]) -> dict[str, Any]: """Parse dataclass to dict.""" @@ -296,7 +304,7 @@ class Access(_Access): def serialize(self) -> dict[str, object]: """Serialize Access.""" return { - "anchors": {key: val.name for key, val in self.anchors.items()}, + "anchors": {str(key): val.name for key, val in self.anchors.items()}, } @classmethod @@ -304,7 +312,9 @@ def deserialize(cls, data: dict[str, Any]) -> "Access": """Deserialize Access.""" anchors = cast(dict[str, str], data.get("anchors")) return Access( - anchors={key: AccessLevel[val] for key, val in anchors.items()}, + anchors={ + JacCloudJID(key): AccessLevel[val] for key, val in anchors.items() + }, ) @@ -338,14 +348,59 @@ class AnchorState: connected: bool = False -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) +class JacCloudJID(JID[_C_ANCHOR]): + """Jaclang ID Implementation.""" + + def __init__( + self, + id: str | ObjectId | None = None, + type: Type[_C_ANCHOR] | None = None, + name: str = "", + ) -> None: + """Override JID initializer.""" + match id: + case str(): + if matched := JID_REGEX.search(id): + self.id = ObjectId(matched.group(3)) + self.name = matched.group(2) + # currently no way to base hinting on string regex! + match matched.group(1).lower(): + case "n": + self.type = NodeAnchor # type: ignore [assignment] + case "e": + self.type = EdgeAnchor # type: ignore [assignment] + case "w": + self.type = WalkerAnchor # type: ignore [assignment] + case _: + self.type = ObjectAnchor # type: ignore [assignment] + return + raise ValueError("Not a valid JID format!") + case ObjectId(): + self.id = id + case None: + self.id = ObjectId() + case _: + raise ValueError("Not a valid id for JID!") + + if type is None: + raise ValueError("Type is required from non string JID!") + self.type = type + self.name = name + + def __hash__(self) -> int: + """Return default hasher.""" + return hash(self.__cached_repr__) + + +@dataclass(eq=False, kw_only=True) class BaseAnchor: """Base Anchor.""" architype: "BaseArchitype" name: str = "" id: ObjectId = field(default_factory=ObjectId) - root: ObjectId | None = None + root: JacCloudJID["NodeAnchor"] | None = None access: Permission state: AnchorState @@ -354,33 +409,21 @@ class Collection(BaseCollection["BaseAnchor"]): pass - @property - def ref_id(self) -> str: - """Return id in reference type.""" - return f"{self.__class__.__name__[:1].lower()}:{self.name}:{self.id}" + @cached_property + def jid(self: _ANCHOR) -> JacCloudJID[_ANCHOR]: # type: ignore[misc] + """Get JID representation.""" + jid = JacCloudJID[_ANCHOR]( + self.id, + self.__class__, + ( + "" + if isinstance(self.architype, (GenericEdge, Root)) + else self.architype.__class__.__name__ + ), + ) + jid.anchor = self - @staticmethod - def ref(ref_id: str) -> "BaseAnchor | Anchor": - """Return ObjectAnchor instance if .""" - if match := GENERIC_ID_REGEX.search(ref_id): - cls: type[BaseAnchor] - - match match.group(1): - case "n": - cls = NodeAnchor - case "e": - cls = EdgeAnchor - case "w": - cls = WalkerAnchor - case "o": - cls = ObjectAnchor - case _: - raise ValueError(f"[{ref_id}] is not a valid reference!") - anchor = object.__new__(cls) - anchor.name = str(match.group(2)) - anchor.id = ObjectId(match.group(3)) - return anchor - raise ValueError(f"[{ref_id}] is not a valid reference!") + return jid #################################################### # QUERY OPERATIONS # @@ -452,33 +495,6 @@ def disconnect_edge(self, anchor: Anchor) -> None: # POPULATE OPERATIONS # #################################################### - def is_populated(self) -> bool: - """Check if populated.""" - return "architype" in self.__dict__ - - def make_stub(self: "BaseAnchor | TANCH") -> "BaseAnchor | TANCH": - """Return unsynced copy of anchor.""" - if self.is_populated(): - unloaded = object.__new__(self.__class__) - # this will be refactored on abstraction - unloaded.name = self.name # type: ignore[attr-defined] - unloaded.id = self.id # type: ignore[attr-defined] - return unloaded # type: ignore[return-value] - return self - - def populate(self) -> None: - """Retrieve the Architype from db and return.""" - from .context import JaseciContext - - jsrc = JaseciContext.get().mem - - if anchor := jsrc.find_by_id(self): - self.__dict__.update(anchor.__dict__) - else: - raise ValueError( - f"{self.__class__.__name__} [{self.ref_id}] is not a valid reference!" - ) - def build_query( self, bulk_write: BulkWrite, @@ -557,15 +573,15 @@ def update(self, bulk_write: BulkWrite, propagate: bool = False) -> None: ############################################################ # POPULATE ADDED EDGES # ############################################################ - added_edges: set[BaseAnchor | Anchor] = ( + added_edges: set[BaseAnchor] = ( changes.get("$addToSet", {}).get("edges", {}).get("$each", []) ) if added_edges: _added_edges = [] for anchor in added_edges: if propagate: - anchor.build_query(bulk_write) # type: ignore[operator] - _added_edges.append(anchor.ref_id) + anchor.build_query(bulk_write) + _added_edges.append(str(anchor.jid)) changes["$addToSet"]["edges"]["$each"] = _added_edges else: changes.pop("$addToSet", None) @@ -575,17 +591,16 @@ def update(self, bulk_write: BulkWrite, propagate: bool = False) -> None: ############################################################ # POPULATE REMOVED EDGES # ############################################################ - pulled_edges: set[BaseAnchor | Anchor] = ( + pulled_edges: set[BaseAnchor] = ( changes.get("$pull", {}).get("edges", {}).get("$in", []) ) if pulled_edges: _pulled_edges = [] for anchor in pulled_edges: - # will be refactored on abstraction - if propagate and anchor.state.deleted is not True: # type: ignore[attr-defined] - anchor.state.deleted = True # type: ignore[attr-defined] - bulk_write.del_edge(anchor.id) # type: ignore[attr-defined, arg-type] - _pulled_edges.append(anchor.ref_id) + if propagate and anchor.state.deleted is not True: + anchor.state.deleted = True + bulk_write.del_edge(anchor.id) + _pulled_edges.append(str(anchor.jid)) if added_edges: # Isolate pull to avoid conflict with addToSet @@ -618,26 +633,19 @@ def has_changed(self) -> int: def sync_hash(self) -> None: """Sync current serialization hash.""" - if is_dataclass(architype := self.architype) and not isinstance( - architype, type - ): - self.state.context_hashes = { - key: hash(val if isinstance(val, bytes) else dumps(val)) - for key, val in architype.__serialize__().items() # type:ignore[attr-defined] # mypy issue - } - self.state.full_hash = hash(pdumps(self.serialize())) + self.state.context_hashes = { + key: hash(val if isinstance(val, bytes) else dumps(val)) + for key, val in self.architype.__serialize__().items() + } + self.state.full_hash = hash(pdumps(self.serialize())) # ---------------------------------------------------------------------- # def report(self) -> dict[str, object]: """Report Anchor.""" return { - "id": self.ref_id, - "context": ( - self.architype.__serialize__() # type:ignore[attr-defined] # mypy issue - if is_dataclass(self.architype) and not isinstance(self.architype, type) - else {} - ), + "id": str(self.jid), + "context": self.architype.__serialize__(), } def serialize(self) -> dict[str, object]: @@ -645,35 +653,18 @@ def serialize(self) -> dict[str, object]: return { "_id": self.id, "name": self.name, - "root": self.root, + "root": str(self.root) if self.root else None, "access": self.access.serialize(), - "architype": ( - self.architype.__serialize__() # type:ignore[attr-defined] # mypy issue - if is_dataclass(self.architype) and not isinstance(self.architype, type) - else {} - ), + "architype": self.architype.__serialize__(), } - def __repr__(self) -> str: - """Override representation.""" - if self.is_populated(): - attrs = "" - for f in fields(self): - if f.name in self.__dict__: - attrs += f"{f.name}={self.__dict__[f.name]}, " - attrs = attrs[:-2] - else: - attrs = f"name={self.name}, id={self.id}" - - return f"{self.__class__.__name__}({attrs})" - -@dataclass(eq=False, repr=False, kw_only=True) -class NodeAnchor(BaseAnchor, _NodeAnchor): # type: ignore[misc] +@dataclass(eq=False, kw_only=True) +class NodeAnchor(BaseAnchor, _NodeAnchor): # type:ignore[misc] """Node Anchor.""" architype: "NodeArchitype" - edges: list["EdgeAnchor"] # type: ignore[assignment] + edges: list[JacCloudJID["EdgeAnchor"]] # type:ignore[assignment] class Collection(BaseCollection["NodeAnchor"]): """NodeAnchor collection interface.""" @@ -695,7 +686,14 @@ def __document__(cls, doc: Mapping[str, Any]) -> "NodeAnchor": anchor = NodeAnchor( architype=architype, id=doc.pop("_id"), - edges=[e for edge in doc.pop("edges") if (e := EdgeAnchor.ref(edge))], + root=( + JacCloudJID[NodeAnchor](root) if (root := doc.pop("root")) else None + ), + edges=[ + e + for edge in doc.pop("edges") + if (e := JacCloudJID[EdgeAnchor](edge)) + ], access=Permission.deserialize(doc.pop("access")), state=AnchorState(connected=True), persistent=True, @@ -705,30 +703,22 @@ def __document__(cls, doc: Mapping[str, Any]) -> "NodeAnchor": anchor.sync_hash() return anchor - @classmethod - def ref(cls, ref_id: str) -> "NodeAnchor": - """Return NodeAnchor instance if existing.""" - if match := NODE_ID_REGEX.search(ref_id): - anchor = object.__new__(cls) - anchor.name = str(match.group(1)) - anchor.id = ObjectId(match.group(2)) - return anchor - raise ValueError(f"[{ref_id}] is not a valid reference!") - def insert( self, bulk_write: BulkWrite, ) -> None: """Append Insert Query.""" - for edge in self.edges: - edge.build_query(bulk_write) + for jid in self.edges: + if anchor := jid.anchor: + anchor.build_query(bulk_write) bulk_write.operations[NodeAnchor].append(InsertOne(self.serialize())) def delete(self, bulk_write: BulkWrite) -> None: """Append Delete Query.""" - for edge in self.edges: - edge.delete(bulk_write) + for jid in self.edges: + if anchor := jid.anchor: + anchor.delete(bulk_write) pulled_edges: set[EdgeAnchor] = self._pull.get("edges", {}).get("$in", []) for edge in pulled_edges: @@ -740,18 +730,17 @@ def serialize(self) -> dict[str, object]: """Serialize Node Anchor.""" return { **super().serialize(), - "edges": [edge.ref_id for edge in self.edges], + "edges": [str(edge) for edge in self.edges], } -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class EdgeAnchor(BaseAnchor, _EdgeAnchor): # type: ignore[misc] """Edge Anchor.""" architype: "EdgeArchitype" - source: NodeAnchor - target: NodeAnchor - is_undirected: bool + source: JacCloudJID[NodeAnchor] + target: JacCloudJID[NodeAnchor] class Collection(BaseCollection["EdgeAnchor"]): """EdgeAnchor collection interface.""" @@ -772,8 +761,11 @@ def __document__(cls, doc: Mapping[str, Any]) -> "EdgeAnchor": anchor = EdgeAnchor( architype=architype, id=doc.pop("_id"), - source=NodeAnchor.ref(doc.pop("source")), - target=NodeAnchor.ref(doc.pop("target")), + root=( + JacCloudJID[NodeAnchor](root) if (root := doc.pop("root")) else None + ), + source=JacCloudJID[NodeAnchor](doc.pop("source")), + target=JacCloudJID[NodeAnchor](doc.pop("target")), access=Permission.deserialize(doc.pop("access")), state=AnchorState(connected=True), persistent=True, @@ -783,32 +775,22 @@ def __document__(cls, doc: Mapping[str, Any]) -> "EdgeAnchor": anchor.sync_hash() return anchor - @classmethod - def ref(cls, ref_id: str) -> "EdgeAnchor": - """Return EdgeAnchor instance if existing.""" - if match := EDGE_ID_REGEX.search(ref_id): - anchor = object.__new__(cls) - anchor.name = str(match.group(1)) - anchor.id = ObjectId(match.group(2)) - return anchor - raise ValueError(f"{ref_id}] is not a valid reference!") - def insert(self, bulk_write: BulkWrite) -> None: """Append Insert Query.""" - if source := self.source: + if source := self.source.anchor: source.build_query(bulk_write) - if target := self.target: + if target := self.target.anchor: target.build_query(bulk_write) bulk_write.operations[EdgeAnchor].append(InsertOne(self.serialize())) def delete(self, bulk_write: BulkWrite) -> None: """Append Delete Query.""" - if source := self.source: + if source := self.source.anchor: source.build_query(bulk_write) - if target := self.target: + if target := self.target.anchor: target.build_query(bulk_write) bulk_write.del_edge(self.id) @@ -817,13 +799,13 @@ def serialize(self) -> dict[str, object]: """Serialize Node Anchor.""" return { **super().serialize(), - "source": self.source.ref_id if self.source else None, - "target": self.target.ref_id if self.target else None, + "source": str(self.source) if self.source else None, + "target": str(self.target) if self.target else None, "is_undirected": self.is_undirected, } -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class WalkerAnchor(BaseAnchor, _WalkerAnchor): # type: ignore[misc] """Walker Anchor.""" @@ -832,7 +814,6 @@ class WalkerAnchor(BaseAnchor, _WalkerAnchor): # type: ignore[misc] next: list[Anchor] = field(default_factory=list) returns: list[Any] = field(default_factory=list) ignores: list[Anchor] = field(default_factory=list) - disengaged: bool = False class Collection(BaseCollection["WalkerAnchor"]): """WalkerAnchor collection interface.""" @@ -853,6 +834,9 @@ def __document__(cls, doc: Mapping[str, Any]) -> "WalkerAnchor": anchor = WalkerAnchor( architype=architype, id=doc.pop("_id"), + root=( + JacCloudJID[NodeAnchor](root) if (root := doc.pop("root")) else None + ), access=Permission.deserialize(doc.pop("access")), state=AnchorState(connected=True), persistent=True, @@ -862,16 +846,6 @@ def __document__(cls, doc: Mapping[str, Any]) -> "WalkerAnchor": anchor.sync_hash() return anchor - @classmethod - def ref(cls, ref_id: str) -> "WalkerAnchor": - """Return EdgeAnchor instance if existing.""" - if match := WALKER_ID_REGEX.search(ref_id): - anchor = object.__new__(cls) - anchor.name = str(match.group(1)) - anchor.id = ObjectId(match.group(2)) - return anchor - raise ValueError(f"{ref_id}] is not a valid reference!") - def insert( self, bulk_write: BulkWrite, @@ -884,7 +858,7 @@ def delete(self, bulk_write: BulkWrite) -> None: bulk_write.del_walker(self.id) -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class ObjectAnchor(BaseAnchor, _ObjectAnchor): # type: ignore[misc] """Object Anchor.""" @@ -910,6 +884,9 @@ def __document__(cls, doc: Mapping[str, Any]) -> "ObjectAnchor": anchor = ObjectAnchor( architype=architype, id=doc.pop("_id"), + root=( + JacCloudJID[NodeAnchor](root) if (root := doc.pop("root")) else None + ), access=Permission.deserialize(doc.pop("access")), state=AnchorState(connected=True), persistent=True, @@ -919,16 +896,6 @@ def __document__(cls, doc: Mapping[str, Any]) -> "ObjectAnchor": anchor.sync_hash() return anchor - @classmethod - def ref(cls, ref_id: str) -> "ObjectAnchor": - """Return NodeAnchor instance if existing.""" - if match := NODE_ID_REGEX.search(ref_id): - anchor = object.__new__(cls) - anchor.name = str(match.group(1)) - anchor.id = ObjectId(match.group(2)) - return anchor - raise ValueError(f"[{ref_id}] is not a valid reference!") - def insert( self, bulk_write: BulkWrite, @@ -947,7 +914,7 @@ class BaseArchitype: __jac_classes__: dict[str, type["BaseArchitype"]] __jac_hintings__: dict[str, type] - __jac__: Anchor + __jac__: BaseAnchor def __serialize__(self) -> dict[str, Any]: """Process default serialization.""" diff --git a/jac-cloud/jac_cloud/core/context.py b/jac-cloud/jac_cloud/core/context.py index 803254265a..d0d602595a 100644 --- a/jac-cloud/jac_cloud/core/context.py +++ b/jac-cloud/jac_cloud/core/context.py @@ -16,6 +16,8 @@ Anchor, AnchorState, BaseArchitype, + JID, + JacCloudJID, NodeAnchor, Permission, Root, @@ -29,8 +31,8 @@ SUPER_ROOT_ID = ObjectId("000000000000000000000000") PUBLIC_ROOT_ID = ObjectId("000000000000000000000001") -SUPER_ROOT = NodeAnchor.ref(f"n::{SUPER_ROOT_ID}") -PUBLIC_ROOT = NodeAnchor.ref(f"n::{PUBLIC_ROOT_ID}") +SUPER_ROOT_JID = JacCloudJID[NodeAnchor](f"n::{SUPER_ROOT_ID}") +PUBLIC_ROOT_JID = JacCloudJID[NodeAnchor](f"n::{PUBLIC_ROOT_ID}") RT = TypeVar("RT") @@ -69,7 +71,7 @@ def close(self) -> None: self.mem.close() @staticmethod - def create(request: Request, entry: NodeAnchor | None = None) -> "JaseciContext": # type: ignore[override] + def create(request: Request, entry: str | None = None) -> "JaseciContext": # type: ignore[override] """Create JacContext.""" ctx = JaseciContext() ctx.base = ExecutionContext.get() @@ -78,7 +80,9 @@ def create(request: Request, entry: NodeAnchor | None = None) -> "JaseciContext" ctx.reports = [] ctx.status = 200 - if not isinstance(system_root := ctx.mem.find_by_id(SUPER_ROOT), NodeAnchor): + if not isinstance( + system_root := ctx.mem.find_by_id(SUPER_ROOT_JID), NodeAnchor + ): system_root = NodeAnchor( architype=object.__new__(Root), id=SUPER_ROOT_ID, @@ -90,16 +94,16 @@ def create(request: Request, entry: NodeAnchor | None = None) -> "JaseciContext" system_root.architype.__jac__ = system_root NodeAnchor.Collection.insert_one(system_root.serialize()) system_root.sync_hash() - ctx.mem.set(system_root.id, system_root) + ctx.mem.set(system_root.jid, system_root) ctx.system_root = system_root if _root := getattr(request, "_root", None): ctx.root = _root - ctx.mem.set(_root.id, _root) + ctx.mem.set(_root.jid, _root) else: if not isinstance( - public_root := ctx.mem.find_by_id(PUBLIC_ROOT), NodeAnchor + public_root := ctx.mem.find_by_id(PUBLIC_ROOT_JID), NodeAnchor ): public_root = NodeAnchor( architype=object.__new__(Root), @@ -110,13 +114,13 @@ def create(request: Request, entry: NodeAnchor | None = None) -> "JaseciContext" edges=[], ) public_root.architype.__jac__ = public_root - ctx.mem.set(public_root.id, public_root) + ctx.mem.set(public_root.jid, public_root) ctx.root = public_root if entry: - if not isinstance(entry_node := ctx.mem.find_by_id(entry), NodeAnchor): - raise ValueError(f"Invalid anchor id {entry.ref_id} !") + if not (entry_node := ctx.mem.find_by_id(JacCloudJID[NodeAnchor](entry))): + raise ValueError(f"Invalid anchor id {entry} !") ctx.entry_node = entry_node else: ctx.entry_node = ctx.root @@ -167,6 +171,8 @@ def clean_response( case dict(): for key, dval in val.items(): self.clean_response(key, dval, val) + case JID(): + cast(dict, obj)[key] = str(val) case Anchor(): cast(dict, obj)[key] = val.report() case BaseArchitype(): diff --git a/jac-cloud/jac_cloud/core/memory.py b/jac-cloud/jac_cloud/core/memory.py index 84cac7c2c8..405d04a060 100644 --- a/jac-cloud/jac_cloud/core/memory.py +++ b/jac-cloud/jac_cloud/core/memory.py @@ -1,8 +1,8 @@ """Memory abstraction for jaseci plugin.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from os import getenv -from typing import Callable, Generator, Iterable, TypeVar, cast +from typing import Callable, Generator, Iterable, TypeVar from bson import ObjectId @@ -14,10 +14,9 @@ from pymongo.client_session import ClientSession from .architype import ( - Anchor, - BaseAnchor, BulkWrite, EdgeAnchor, + JacCloudJID, NodeAnchor, ObjectAnchor, Root, @@ -27,78 +26,91 @@ DISABLE_AUTO_CLEANUP = getenv("DISABLE_AUTO_CLEANUP") == "true" SINGLE_QUERY = getenv("SINGLE_QUERY") == "true" -IDS = ObjectId | Iterable[ObjectId] -BA = TypeVar("BA", bound="BaseAnchor") + +_ANCHOR = TypeVar("_ANCHOR", NodeAnchor, EdgeAnchor, WalkerAnchor, ObjectAnchor) @dataclass -class MongoDB(Memory[ObjectId, BaseAnchor | Anchor]): +class MongoDB(Memory): """Shelf Handler.""" + __mem__: dict[ + JacCloudJID, NodeAnchor | EdgeAnchor | WalkerAnchor | ObjectAnchor + ] = field( + default_factory=dict + ) # type: ignore[assignment] + __gc__: set[JacCloudJID] = field(default_factory=set) # type: ignore[assignment] __session__: ClientSession | None = None - def populate_data(self, edges: Iterable[EdgeAnchor]) -> None: + def populate_data(self, edges: Iterable[JacCloudJID[EdgeAnchor]]) -> None: """Populate data to avoid multiple query.""" if not SINGLE_QUERY: - nodes: set[NodeAnchor] = set() + nodes: set[JacCloudJID] = set() for edge in self.find(edges): - if edge.source: - nodes.add(edge.source) - if edge.target: - nodes.add(edge.target) + nodes.add(edge.source) + nodes.add(edge.target) self.find(nodes) def find( # type: ignore[override] self, - anchors: BA | Iterable[BA], - filter: Callable[[Anchor], Anchor] | None = None, + ids: JacCloudJID[_ANCHOR] | Iterable[JacCloudJID[_ANCHOR]], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, session: ClientSession | None = None, - ) -> Generator[BA, None, None]: + ) -> Generator[_ANCHOR, None, None]: """Find anchors from datasource by ids with filter.""" - if not isinstance(anchors, Iterable): - anchors = [anchors] - - collections: dict[type[Collection[BaseAnchor]], list[ObjectId]] = {} - for anchor in anchors: - if anchor.id not in self.__mem__ and anchor not in self.__gc__: - coll = collections.get(anchor.Collection) + if not isinstance(ids, Iterable): + ids = [ids] + + collections: dict[ + type[ + Collection[NodeAnchor] + | Collection[EdgeAnchor] + | Collection[WalkerAnchor] + | Collection[ObjectAnchor] + ], + list[ObjectId], + ] = {} + for jid in ids: + if jid not in self.__mem__ and jid not in self.__gc__: + coll = collections.get(jid.type.Collection) if coll is None: - coll = collections[anchor.Collection] = [] + coll = collections[jid.type.Collection] = [] - coll.append(anchor.id) + coll.append(jid.id) - for cl, ids in collections.items(): + for cl, oids in collections.items(): for anch_db in cl.find( { - "_id": {"$in": ids}, + "_id": {"$in": oids}, }, session=session or self.__session__, ): - self.__mem__[anch_db.id] = anch_db + self.__mem__[anch_db.jid] = anch_db - for anchor in anchors: + for jid in ids: if ( - anchor not in self.__gc__ - and (anch_mem := self.__mem__.get(anchor.id)) - and (not filter or filter(anch_mem)) # type: ignore[arg-type] + jid not in self.__gc__ + and (anch_mem := self.__mem__.get(jid)) + and isinstance(anch_mem, jid.type) + and (not filter or filter(anch_mem)) ): - yield cast(BA, anch_mem) + yield anch_mem def find_one( # type: ignore[override] self, - anchors: BA | Iterable[BA], - filter: Callable[[Anchor], Anchor] | None = None, + ids: JacCloudJID[_ANCHOR] | Iterable[JacCloudJID[_ANCHOR]], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, session: ClientSession | None = None, - ) -> BA | None: + ) -> _ANCHOR | None: """Find one anchor from memory by ids with filter.""" - return next(self.find(anchors, filter, session), None) + return next(self.find(ids, filter, session), None) - def find_by_id(self, anchor: BA) -> BA | None: + def find_by_id(self, id: JacCloudJID[_ANCHOR]) -> _ANCHOR | None: # type: ignore[override] """Find one by id.""" - data = super().find_by_id(anchor.id) + data = super().find_by_id(id) - if not data and (data := anchor.Collection.find_by_id(anchor.id)): - self.__mem__[data.id] = data + if not data and (data := id.type.Collection.find_by_id(id.id)): + self.__mem__[data.jid] = data return data @@ -115,7 +127,9 @@ def close(self) -> None: super().close() - def sync_mem_to_db(self, bulk_write: BulkWrite, keys: Iterable[ObjectId]) -> None: + def sync_mem_to_db( + self, bulk_write: BulkWrite, keys: Iterable[JacCloudJID] + ) -> None: """Manually sync memory to db.""" for key in keys: if ( @@ -146,19 +160,18 @@ def sync_mem_to_db(self, bulk_write: BulkWrite, keys: Iterable[ObjectId]) -> Non def get_bulk_write(self) -> BulkWrite: """Sync memory to database.""" bulk_write = BulkWrite() - - for anchor in self.__gc__: - match anchor: - case NodeAnchor(): - bulk_write.del_node(anchor.id) - case EdgeAnchor(): - bulk_write.del_edge(anchor.id) - case WalkerAnchor(): - bulk_write.del_walker(anchor.id) - case ObjectAnchor(): - bulk_write.del_object(anchor.id) - case _: - pass + for jid in self.__gc__: + self.__mem__.pop(jid, None) + # match case doesn't work yet with + # type checking for type (not instance) + if jid.type is NodeAnchor: + bulk_write.del_node(jid.id) + elif jid.type is EdgeAnchor: + bulk_write.del_edge(jid.id) + elif jid.type is WalkerAnchor: + bulk_write.del_walker(jid.id) + elif jid.type is ObjectAnchor: + bulk_write.del_object(jid.id) keys = set(self.__mem__.keys()) diff --git a/jac-cloud/jac_cloud/plugin/jaseci.py b/jac-cloud/jac_cloud/plugin/jaseci.py index 2b58283dce..58ee9dfc87 100644 --- a/jac-cloud/jac_cloud/plugin/jaseci.py +++ b/jac-cloud/jac_cloud/plugin/jaseci.py @@ -1,7 +1,6 @@ """Jac Language Features.""" from collections import OrderedDict -from contextlib import suppress from dataclasses import Field, MISSING, fields, is_dataclass from functools import wraps from os import getenv @@ -45,8 +44,11 @@ EdgeAnchor, EdgeArchitype, GenericEdge, + JID, + JacCloudJID, NodeAnchor, NodeArchitype, + ObjectAnchor, ObjectArchitype, Permission, Root, @@ -195,7 +197,7 @@ def api_entry( except ValidationError as e: return ORJSONResponse({"detail": e.errors()}) - jctx = JaseciContext.create(request, NodeAnchor.ref(node) if node else None) + jctx = JaseciContext.create(request, node) wlk: WalkerAnchor = cls(**body, **pl["query"], **pl["files"]).__jac__ if Jac.check_read_access(jctx.entry_node): @@ -211,7 +213,7 @@ def api_entry( return ORJSONResponse(resp, jctx.status) else: error = { - "error": f"You don't have access on target entry{cast(Anchor, jctx.entry_node).ref_id}!" + "error": f"You don't have access on target entry {jctx.entry_node.jid}!" } jctx.close() @@ -312,14 +314,16 @@ class JacCallableImplementation: """Callable Implementations.""" @staticmethod - def get_object(id: str) -> Architype | None: + def get_object(id: str | JacCloudJID) -> Architype | None: """Get object by id.""" if not FastAPI.is_enabled(): return _JacCallableImplementation.get_object(id=id) - with suppress(ValueError): - if isinstance(architype := BaseAnchor.ref(id).architype, Architype): - return architype + if isinstance(id, str): + id = JacCloudJID(id) + + if anchor := id.anchor: + return anchor.architype return None @@ -330,32 +334,28 @@ class JacAccessValidationPlugin: @staticmethod @hookimpl def allow_root( - architype: Architype, root_id: BaseAnchor, level: AccessLevel | int | str + architype: Architype, root_id: JacCloudJID, 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] - ) + JacFeatureImpl.allow_root(architype=architype, root_id=root_id, level=level) 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) + if isinstance(anchor, BaseAnchor) and level != access.anchors.get( + root_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) + access.anchors[root_id] = level + anchor._set.update({f"access.roots.anchors.{root_id}": level.name}) + anchor._unset.pop(f"access.roots.anchors.{root_id}", None) @staticmethod @hookimpl def disallow_root( - architype: Architype, root_id: BaseAnchor, level: AccessLevel | int | str + architype: Architype, root_id: JacCloudJID, level: AccessLevel | int | str ) -> None: """Disallow all access from target root graph to current Architype.""" if not FastAPI.is_enabled(): @@ -370,11 +370,10 @@ def disallow_root( 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 + and access.anchors.pop(root_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) + anchor._unset.update({f"access.roots.anchors.{root_id}": True}) + anchor._set.pop(f"access.roots.anchors.{root_id}", None) @staticmethod @hookimpl @@ -424,7 +423,7 @@ def check_access_level(to: Anchor) -> AccessLevel: # 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: + if jroot == jctx.system_root or jroot.jid == to.root or jroot == to: return AccessLevel.WRITE access_level = AccessLevel.NO_ACCESS @@ -435,19 +434,17 @@ def check_access_level(to: Anchor) -> AccessLevel: # 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 and (to_root := 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) + level = to_root.access.roots.check(jroot.jid) 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) + level = to_access.roots.check(jroot.jid) if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: access_level = level @@ -501,11 +498,13 @@ def detach(edge: EdgeAnchor) -> None: JacFeatureImpl.detach(edge=edge) return - Jac.remove_edge(node=edge.source, edge=edge) - edge.source.disconnect_edge(edge) + if source := edge.source.anchor: + Jac.remove_edge(node=source, edge=edge) + source.disconnect_edge(edge) - Jac.remove_edge(node=edge.target, edge=edge) - edge.target.disconnect_edge(edge) + if target := edge.target.anchor: + Jac.remove_edge(node=target, edge=edge) + target.disconnect_edge(edge) class JacPlugin(JacAccessValidationPlugin, JacNodePlugin, JacEdgePlugin): @@ -529,26 +528,31 @@ def reset_graph(root: Root | None = None) -> int: ctx = JaseciContext.get() ranchor = root.__jac__ if root else ctx.root - + ranchor_jid = str(ranchor.jid) deleted_count = 0 # noqa: SIM113 for node in NodeAnchor.Collection.find( - {"_id": {"$ne": ranchor.id}, "root": ranchor.id} + {"_id": {"$ne": ranchor.id}, "root": ranchor_jid} ): - ctx.mem.__mem__[node.id] = node + ctx.mem.__mem__[node.jid] = node Jac.destroy(node) deleted_count += 1 - for edge in EdgeAnchor.Collection.find({"root": ranchor.id}): - ctx.mem.__mem__[edge.id] = edge + for edge in EdgeAnchor.Collection.find({"root": ranchor_jid}): + ctx.mem.__mem__[edge.jid] = edge Jac.destroy(edge) deleted_count += 1 - for walker in WalkerAnchor.Collection.find({"root": ranchor.id}): - ctx.mem.__mem__[walker.id] = walker + for walker in WalkerAnchor.Collection.find({"root": ranchor_jid}): + ctx.mem.__mem__[walker.jid] = walker Jac.destroy(walker) deleted_count += 1 + for obj in ObjectAnchor.Collection.find({"root": ranchor_jid}): + ctx.mem.__mem__[obj.jid] = obj + Jac.destroy(obj) + deleted_count += 1 + return deleted_count @staticmethod @@ -721,14 +725,14 @@ def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: eanch = edge.__jac__ = EdgeAnchor( architype=edge, name=("" if isinstance(edge, GenericEdge) else edge.__class__.__name__), - source=source, - target=target, + source=source.jid, + target=target.jid, is_undirected=is_undirected, access=Permission(), state=AnchorState(), ) - source.edges.append(eanch) - target.edges.append(eanch) + source.edges.append(eanch.jid) + target.edges.append(eanch.jid) source.connect_edge(eanch) target.connect_edge(eanch) @@ -754,12 +758,12 @@ def get_object_func() -> Callable[[str], Architype | None]: @staticmethod @hookimpl - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object reference id.""" if not FastAPI.is_enabled(): return JacFeatureImpl.object_ref(obj=obj) - return str(obj.__jac__.ref_id) + return obj.__jac__.jid @staticmethod @hookimpl @@ -775,8 +779,10 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker = op1.__jac__ if isinstance(op2, NodeArchitype): node = op2.__jac__ - elif isinstance(op2, EdgeArchitype): - node = op2.__jac__.target + elif isinstance(op2, EdgeArchitype) and ( + target := op2.__jac__.target.anchor + ): + node = target else: raise TypeError("Invalid target object") elif isinstance(op2, WalkerArchitype): @@ -784,8 +790,10 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker = op2.__jac__ if isinstance(op1, NodeArchitype): node = op1.__jac__ - elif isinstance(op1, EdgeArchitype): - node = op1.__jac__.target + elif isinstance(op1, EdgeArchitype) and ( + target := op1.__jac__.target.anchor + ): + node = target else: raise TypeError("Invalid target object") else: @@ -874,10 +882,11 @@ def destroy(obj: Architype | Anchor | BaseAnchor) -> None: match anchor: case NodeAnchor(): for edge in anchor.edges: - Jac.destroy(edge) + if eanch := edge.anchor: + Jac.destroy(eanch) case EdgeAnchor(): Jac.detach(anchor) case _: pass - Jac.get_context().mem.remove(anchor.id) + Jac.get_context().mem.remove(anchor.jid) diff --git a/jac-cloud/jac_cloud/tests/simple_graph.jac b/jac-cloud/jac_cloud/tests/simple_graph.jac index 62437e93ed..cd13a0dd1c 100644 --- a/jac-cloud/jac_cloud/tests/simple_graph.jac +++ b/jac-cloud/jac_cloud/tests/simple_graph.jac @@ -206,7 +206,7 @@ walker update_nested_node { walker detach_nested_node { can enter_root with `root entry { - report here del--> [-->(`?Nested)]; + return here del--> [-->(`?Nested)]; } } @@ -214,14 +214,14 @@ walker visit_nested_node { can enter_root with `root entry { nesteds = [-->(`?Nested)]; if nesteds { - report [-->(`?Nested)][0]; + return [-->(`?Nested)][0]; } else { - report nesteds; + return nesteds; } } can enter_nested with Nested entry { - report here; + return here; } } @@ -251,8 +251,8 @@ walker allow_other_root_access { if self.via_all { Jac.unrestrict(here, self.level); } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); + import:py from jac_cloud.core.architype {JacCloudJID} + Jac.allow_root(here, JacCloudJID(self.root_id), self.level); } } @@ -260,8 +260,8 @@ walker allow_other_root_access { if self.via_all { Jac.unrestrict(here, self.level); } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.allow_root(here, BaseAnchor.ref(self.root_id), self.level); + import:py from jac_cloud.core.architype {JacCloudJID} + Jac.allow_root(here, JacCloudJID(self.root_id), self.level); } } } @@ -273,8 +273,8 @@ walker disallow_other_root_access { if self.via_all { Jac.restrict(here); } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); + import:py from jac_cloud.core.architype {JacCloudJID} + Jac.disallow_root(here, JacCloudJID(self.root_id)); } } @@ -282,8 +282,8 @@ walker disallow_other_root_access { if self.via_all { Jac.restrict(here); } else { - import:py from jac_cloud.core.architype {BaseAnchor} - Jac.disallow_root(here, BaseAnchor.ref(self.root_id)); + import:py from jac_cloud.core.architype {JacCloudJID} + Jac.disallow_root(here, JacCloudJID(self.root_id)); } } } @@ -463,23 +463,6 @@ walker different_return { } } -:walker:detach_nested_node:can:enter_root { - return here del--> [-->(`?Nested)]; -} - -:walker:visit_nested_node:can:enter_root { - nesteds = [-->(`?Nested)]; - if nesteds { - return [-->(`?Nested)][0]; - } else { - return nesteds; - } -} - -:walker:visit_nested_node:can:enter_nested { - return here; -} - walker manual_create_nested_node { can enter_root with `root entry { n = Nested( @@ -704,10 +687,11 @@ walker check_populated_graph { can enter with `root entry { import:py from jac_cloud.core.architype {NodeAnchor, EdgeAnchor, WalkerAnchor} - id = here.__jac__.id; - count = NodeAnchor.Collection.count({"$or": [{"_id": id}, {"root": id}]}); - count += EdgeAnchor.Collection.count({"root": id}); - count += WalkerAnchor.Collection.count({"root": id}); + jid = here.__jac__.jid; + _jid = str(jid); + count = NodeAnchor.Collection.count({"$or": [{"_id": jid.id}, {"root": _jid}]}); + count += EdgeAnchor.Collection.count({"root": _jid}); + count += WalkerAnchor.Collection.count({"root": _jid}); report count; } @@ -793,14 +777,6 @@ walker delete_custom_object { has object_id: str; can enter1 with `root entry { - import:py from jac_cloud.core.architype {BaseAnchor} Jac.destroy(&(self.object_id)); - - # This is similar to - # - # Jac.destroy(BaseAnchor.ref(self.object_id)); - # - # The only difference is BaseAnchor.ref doesn't - # load the actual object and just use it as reference } } \ No newline at end of file diff --git a/jac-cloud/jac_cloud/tests/test_simple_graph.py b/jac-cloud/jac_cloud/tests/test_simple_graph.py index 2ed04f080f..c9a79917cb 100644 --- a/jac-cloud/jac_cloud/tests/test_simple_graph.py +++ b/jac-cloud/jac_cloud/tests/test_simple_graph.py @@ -560,19 +560,19 @@ def trigger_upload_file(self) -> None: "single": { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 17658, + "size": 17079, } }, "multiple": [ { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 17658, + "size": 17079, }, { "name": "simple_graph.jac", "content_type": "application/octet-stream", - "size": 17658, + "size": 17079, }, ], "singleOptional": None, diff --git a/jac/jaclang/cli/cli.py b/jac/jaclang/cli/cli.py index a16c288443..af28f7135c 100644 --- a/jac/jaclang/cli/cli.py +++ b/jac/jaclang/cli/cli.py @@ -21,7 +21,7 @@ from jaclang.plugin.builtin import dotgen from jaclang.plugin.feature import JacCmd as Cmd from jaclang.plugin.feature import JacFeature as Jac -from jaclang.runtimelib.constructs import WalkerArchitype +from jaclang.runtimelib.constructs import Anchor, WalkerArchitype from jaclang.runtimelib.context import ExecutionContext from jaclang.runtimelib.machine import JacMachine, JacProgram from jaclang.utils.helpers import debugger as db @@ -129,7 +129,7 @@ def run( @cmd_registry.register def get_object( filename: str, id: str, session: str = "", main: bool = True, cache: bool = True -) -> dict: +) -> Anchor | None: """Get the object with the specified id.""" if session == "": session = ( @@ -169,10 +169,10 @@ def get_object( JacMachine.detach() raise ValueError("Not a valid file!\nOnly supports `.jac` and `.jir`") - data = {} + data = None obj = Jac.get_object(id) if obj: - data = obj.__jac__.__getstate__() + data = obj.__jac__ else: print(f"Object with id {id} not found.", file=sys.stderr) diff --git a/jac/jaclang/plugin/builtin.py b/jac/jaclang/plugin/builtin.py index aaeb3fdffe..af4a516d65 100644 --- a/jac/jaclang/plugin/builtin.py +++ b/jac/jaclang/plugin/builtin.py @@ -5,7 +5,7 @@ from typing import Optional from jaclang.plugin.feature import JacFeature as Jac -from jaclang.runtimelib.constructs import Architype, NodeArchitype +from jaclang.runtimelib.constructs import Architype, JID, NodeArchitype def dotgen( @@ -41,6 +41,6 @@ def dotgen( ) -def jid(obj: Architype) -> str: +def jid(obj: Architype) -> JID: """Get the id of the object.""" return Jac.object_ref(obj) diff --git a/jac/jaclang/plugin/default.py b/jac/jaclang/plugin/default.py index 76fa0df859..80a2fcffb6 100644 --- a/jac/jaclang/plugin/default.py +++ b/jac/jaclang/plugin/default.py @@ -12,7 +12,6 @@ from functools import wraps from logging import getLogger from typing import Any, Callable, Mapping, Optional, Sequence, Type, Union, cast -from uuid import UUID from jaclang.compiler.constant import colors from jaclang.compiler.semtable import SemInfo, SemRegistry, SemScope @@ -25,6 +24,7 @@ EdgeArchitype, EdgeDir, ExecutionContext, + JID, JacFeature as Jac, NodeAnchor, NodeArchitype, @@ -37,6 +37,7 @@ ) from jaclang.runtimelib.constructs import ( GenericEdge, + JacLangJID, JacTestCheck, ) from jaclang.runtimelib.importer import ImportPathSpec, JacImporter, PythonImporter @@ -55,11 +56,16 @@ class JacCallableImplementation: """Callable Implementations.""" @staticmethod - def get_object(id: str) -> Architype | None: + def get_object(id: str | JID) -> Architype | None: """Get object by id.""" + jctx = Jac.get_context() if id == "root": - return Jac.get_context().root.architype - elif obj := Jac.get_context().mem.find_by_id(UUID(id)): + return jctx.root.architype + + if isinstance(id, str): + id = JacLangJID(id) + + if obj := id.anchor: return obj.architype return None @@ -78,26 +84,25 @@ def elevate_root() -> None: @staticmethod @hookimpl def allow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, 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 + if level != access.anchors.get(root_id, AccessLevel.NO_ACCESS): + access.anchors[root_id] = level @staticmethod @hookimpl def disallow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, 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) + access.anchors.pop(root_id, None) @staticmethod @hookimpl @@ -160,7 +165,7 @@ def check_access_level(to: Anchor) -> AccessLevel: # 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: + if jroot == jctx.system_root or jroot.jid == to.root or jroot == to: return AccessLevel.WRITE access_level = AccessLevel.NO_ACCESS @@ -171,17 +176,17 @@ def check_access_level(to: Anchor) -> AccessLevel: # 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 and (to_root := 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)) + level = to_root.access.roots.check(jroot.jid) 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)) + level = to_access.roots.check(jroot.jid) if level > AccessLevel.NO_ACCESS and access_level == AccessLevel.NO_ACCESS: access_level = level @@ -226,13 +231,12 @@ def get_edges( ) -> list[EdgeArchitype]: """Get edges connected to this node.""" ret_edges: list[EdgeArchitype] = [] - for anchor in node.edges: + for jid in node.edges: if ( - (source := anchor.source) - and (target := anchor.target) + (anchor := jid.anchor) + and (source := anchor.source.anchor) + and (target := anchor.target.anchor) and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype ): if ( dir in [EdgeDir.OUT, EdgeDir.ANY] @@ -260,13 +264,12 @@ def edges_to_nodes( ) -> list[NodeArchitype]: """Get set of nodes connected to this node.""" ret_edges: list[NodeArchitype] = [] - for anchor in node.edges: + for jid in node.edges: if ( - (source := anchor.source) - and (target := anchor.target) + (anchor := jid.anchor) + and (source := anchor.source.anchor) + and (target := anchor.target.anchor) and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype ): if ( dir in [EdgeDir.OUT, EdgeDir.ANY] @@ -289,7 +292,7 @@ def edges_to_nodes( 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: + if ed == edge.jid: node.edges.pop(idx) break @@ -301,8 +304,10 @@ class JacEdgeImpl: @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) + if source := edge.source.anchor: + Jac.remove_edge(node=source, edge=edge) + if target := edge.target.anchor: + Jac.remove_edge(node=target, edge=edge) class JacWalkerImpl: @@ -332,7 +337,7 @@ def visit_node( if isinstance(anchor, NodeAnchor): wanch.next.append(anchor) elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: + if target := anchor.target.anchor: wanch.next.append(target) else: raise ValueError("Edge has no target.") @@ -363,7 +368,7 @@ def ignore( if isinstance(anchor, NodeAnchor): wanch.ignores.append(anchor) elif isinstance(anchor, EdgeAnchor): - if target := anchor.target: + if target := anchor.target.anchor: wanch.ignores.append(target) else: raise ValueError("Edge has no target.") @@ -380,8 +385,10 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker = op1.__jac__ if isinstance(op2, NodeArchitype): node = op2.__jac__ - elif isinstance(op2, EdgeArchitype): - node = op2.__jac__.target + elif isinstance(op2, EdgeArchitype) and ( + target := op2.__jac__.target.anchor + ): + node = target else: raise TypeError("Invalid target object") elif isinstance(op2, WalkerArchitype): @@ -389,8 +396,10 @@ def spawn_call(op1: Architype, op2: Architype) -> WalkerArchitype: walker = op2.__jac__ if isinstance(op1, NodeArchitype): node = op1.__jac__ - elif isinstance(op1, EdgeArchitype): - node = op1.__jac__.target + elif isinstance(op1, EdgeArchitype) and ( + target := op1.__jac__.target.anchor + ): + node = target else: raise TypeError("Invalid target object") else: @@ -602,10 +611,10 @@ def reset_graph(root: Optional[Root] = None) -> int: if isinstance(anchors := mem.__shelf__, Shelf) else mem.__mem__.values() ): - if anchor == ranchor or anchor.root != ranchor.id: + if anchor == ranchor or anchor.root != ranchor.jid: continue - if loaded_anchor := mem.find_by_id(anchor.id): + if loaded_anchor := mem.find_by_id(anchor.jid): deleted_count += 1 Jac.destroy(loaded_anchor) @@ -613,15 +622,15 @@ def reset_graph(root: Optional[Root] = None) -> int: @staticmethod @hookimpl - def get_object_func() -> Callable[[str], Architype | None]: + def get_object_func() -> Callable[[str | JID], Architype | None]: """Get object by id func.""" return JacCallableImplementation.get_object @staticmethod @hookimpl - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object's id.""" - return obj.__jac__.id.hex + return obj.__jac__.jid @staticmethod @hookimpl @@ -988,13 +997,12 @@ def disconnect( for i in left: node = i.__jac__ - for anchor in set(node.edges): + for jid in set(node.edges): if ( - (source := anchor.source) - and (target := anchor.target) + (anchor := jid.anchor) + and (source := anchor.source.anchor) + and (target := anchor.target.anchor) and (not filter_func or filter_func([anchor.architype])) - and source.architype - and target.architype ): if ( dir in [EdgeDir.OUT, EdgeDir.ANY] @@ -1012,7 +1020,6 @@ def disconnect( ): Jac.destroy(anchor) if anchor.persistent else Jac.detach(anchor) disconnect_occurred = True - return disconnect_occurred @staticmethod @@ -1054,12 +1061,12 @@ def builder(source: NodeAnchor, target: NodeAnchor) -> EdgeArchitype: eanch = edge.__jac__ = EdgeAnchor( architype=edge, - source=source, - target=target, + source=source.jid, + target=target.jid, is_undirected=is_undirected, ) - source.edges.append(eanch) - target.edges.append(eanch) + source.edges.append(eanch.jid) + target.edges.append(eanch.jid) if conn_assign: for fld, val in zip(conn_assign[0], conn_assign[1]): @@ -1084,9 +1091,9 @@ def save(obj: Architype | Anchor) -> None: jctx = Jac.get_context() anchor.persistent = True - anchor.root = jctx.root.id + anchor.root = jctx.root.jid - jctx.mem.set(anchor.id, anchor) + jctx.mem.set(anchor.jid, anchor) @staticmethod @hookimpl @@ -1097,14 +1104,15 @@ def destroy(obj: Architype | Anchor) -> None: if Jac.check_write_access(anchor): match anchor: case NodeAnchor(): - for edge in anchor.edges: - Jac.destroy(edge) + for edge in set(anchor.edges): + if eanch := edge.anchor: + Jac.destroy(eanch) case EdgeAnchor(): Jac.detach(anchor) case _: pass - Jac.get_context().mem.remove(anchor.id) + Jac.get_context().mem.remove(anchor.jid) @staticmethod @hookimpl diff --git a/jac/jaclang/plugin/feature.py b/jac/jaclang/plugin/feature.py index 808b254ae9..2dc6fe9cd9 100644 --- a/jac/jaclang/plugin/feature.py +++ b/jac/jaclang/plugin/feature.py @@ -15,7 +15,6 @@ TypeAlias, Union, ) -from uuid import UUID from jaclang.plugin.spec import ( AccessLevel, @@ -26,6 +25,7 @@ EdgeArchitype, EdgeDir, ExecutionContext, + JID, NodeAnchor, NodeArchitype, P, @@ -36,6 +36,7 @@ ast, plugin_manager, ) +from jaclang.runtimelib.constructs import ObjectArchitype class JacAccessValidation: @@ -49,7 +50,7 @@ def elevate_root() -> None: @staticmethod def allow_root( architype: Architype, - root_id: UUID, + root_id: JID, level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Allow all access from target root graph to current Architype.""" @@ -60,7 +61,7 @@ def allow_root( @staticmethod def disallow_root( architype: Architype, - root_id: UUID, + root_id: JID, level: AccessLevel | int | str = AccessLevel.READ, ) -> None: """Disallow all access from target root graph to current Architype.""" @@ -196,7 +197,7 @@ class JacClassReferences: EdgeDir: ClassVar[TypeAlias] = EdgeDir DSFunc: ClassVar[TypeAlias] = DSFunc RootType: ClassVar[TypeAlias] = Root - Obj: ClassVar[TypeAlias] = Architype + Obj: ClassVar[TypeAlias] = ObjectArchitype Node: ClassVar[TypeAlias] = NodeArchitype Edge: ClassVar[TypeAlias] = EdgeArchitype Walker: ClassVar[TypeAlias] = WalkerArchitype @@ -265,17 +266,17 @@ def reset_graph(root: Optional[Root] = None) -> int: return plugin_manager.hook.reset_graph(root=root) @staticmethod - def get_object(id: str) -> Architype | None: + def get_object(id: str | JID) -> Architype | None: """Get object given id.""" return plugin_manager.hook.get_object_func()(id=id) @staticmethod - def get_object_func() -> Callable[[str], Architype | None]: + def get_object_func() -> Callable[[str | JID], Architype | None]: """Get object given id.""" return plugin_manager.hook.get_object_func() @staticmethod - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object reference id.""" return plugin_manager.hook.object_ref(obj=obj) diff --git a/jac/jaclang/plugin/spec.py b/jac/jaclang/plugin/spec.py index bc1bf01259..84bf979aca 100644 --- a/jac/jaclang/plugin/spec.py +++ b/jac/jaclang/plugin/spec.py @@ -15,7 +15,6 @@ TypeVar, Union, ) -from uuid import UUID from jaclang.compiler import absyntree as ast from jaclang.compiler.constant import EdgeDir @@ -27,6 +26,7 @@ DSFunc, EdgeAnchor, EdgeArchitype, + JID, NodeAnchor, NodeArchitype, Root, @@ -55,7 +55,7 @@ def elevate_root() -> None: @staticmethod @hookspec(firstresult=True) def allow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, level: AccessLevel | int | str ) -> None: """Allow all access from target root graph to current Architype.""" raise NotImplementedError @@ -63,7 +63,7 @@ def allow_root( @staticmethod @hookspec(firstresult=True) def disallow_root( - architype: Architype, root_id: UUID, level: AccessLevel | int | str + architype: Architype, root_id: JID, level: AccessLevel | int | str ) -> None: """Disallow all access from target root graph to current Architype.""" raise NotImplementedError @@ -258,13 +258,13 @@ def reset_graph(root: Optional[Root]) -> int: @staticmethod @hookspec(firstresult=True) - def get_object_func() -> Callable[[str], Architype | None]: + def get_object_func() -> Callable[[str | JID], Architype | None]: """Get object by id func.""" raise NotImplementedError @staticmethod @hookspec(firstresult=True) - def object_ref(obj: Architype) -> str: + def object_ref(obj: Architype) -> JID: """Get object's id.""" raise NotImplementedError diff --git a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac index 9b971f6622..be46386d97 100644 --- a/jac/jaclang/plugin/tests/fixtures/other_root_access.jac +++ b/jac/jaclang/plugin/tests/fixtures/other_root_access.jac @@ -1,5 +1,4 @@ -import:py from jaclang.runtimelib.architype {Anchor} -import:py from uuid {UUID} +import:py from jaclang.runtimelib.architype {JacLangJID} node A { has val: int; @@ -48,15 +47,15 @@ walker create_node { can enter with `root entry { a = A(val=self.val); here ++> a; - print(a.__jac__.id); + print(jid(a)); } } walker create_other_root { can enter with `root entry { - other_root = `root().__jac__; + other_root = `root(); Jac.save(other_root); - print(other_root.id); + print(jid(other_root)); } } @@ -67,7 +66,7 @@ walker allow_other_root_access { if self.via_all { Jac.unrestrict(here, self.level); } else { - Jac.allow_root(here, UUID(self.root_id), self.level); + Jac.allow_root(here, JacLangJID(self.root_id), self.level); } } @@ -75,7 +74,7 @@ walker allow_other_root_access { if self.via_all { Jac.unrestrict(here, self.level); } else { - Jac.allow_root(here, UUID(self.root_id), self.level); + Jac.allow_root(here, JacLangJID(self.root_id), self.level); } } } @@ -87,7 +86,7 @@ walker disallow_other_root_access { if self.via_all { Jac.restrict(here); } else { - Jac.disallow_root(here, UUID(self.root_id)); + Jac.disallow_root(here, JacLangJID(self.root_id)); } } @@ -95,7 +94,7 @@ walker disallow_other_root_access { if self.via_all { Jac.restrict(here); } else { - Jac.disallow_root(here, UUID(self.root_id)); + Jac.disallow_root(here, JacLangJID(self.root_id)); } } } \ No newline at end of file diff --git a/jac/jaclang/plugin/tests/test_jaseci.py b/jac/jaclang/plugin/tests/test_jaseci.py index a66a3c2472..ba9d3ea430 100644 --- a/jac/jaclang/plugin/tests/test_jaseci.py +++ b/jac/jaclang/plugin/tests/test_jaseci.py @@ -78,7 +78,7 @@ def test_entrypoint_root(self) -> None: session=session, entrypoint="traverse", args=[], - node=str(obj["id"]), + node=str(obj.jid), ) output = self.capturedOutput.getvalue().strip() self.assertEqual(output, "node a\nnode b") @@ -100,12 +100,12 @@ def test_entrypoint_non_root(self) -> None: ) edge_obj = cli.get_object( filename=self.fixture_abs_path("simple_persistent.jac"), - id=obj["edges"][0].id.hex, + id=str(obj.edges[0]), session=session, ) a_obj = cli.get_object( filename=self.fixture_abs_path("simple_persistent.jac"), - id=edge_obj["target"].id.hex, + id=str(edge_obj.target), session=session, ) self._output2buffer() @@ -113,7 +113,7 @@ def test_entrypoint_non_root(self) -> None: filename=self.fixture_abs_path("simple_persistent.jac"), session=session, entrypoint="traverse", - node=str(a_obj["id"]), + node=str(a_obj.jid), args=[], ) output = self.capturedOutput.getvalue().strip() @@ -132,28 +132,26 @@ def test_get_edge(self) -> None: session=session, id="root", ) - self.assertEqual(len(obj["edges"]), 2) + self.assertEqual(len(obj.edges), 2) edge_objs = [ cli.get_object( filename=self.fixture_abs_path("simple_node_connection.jac"), session=session, - id=e.id.hex, + id=str(e), ) - for e in obj["edges"] + for e in obj.edges ] - node_ids = [obj["target"].id.hex for obj in edge_objs] + node_ids = [str(obj.target) for obj in edge_objs] node_objs = [ cli.get_object( filename=self.fixture_abs_path("simple_node_connection.jac"), session=session, - id=str(n_id), + id=n_id, ) for n_id in node_ids ] self.assertEqual(len(node_objs), 2) - self.assertEqual( - {obj["architype"].tag for obj in node_objs}, {"first", "second"} - ) + self.assertEqual({obj.architype.tag for obj in node_objs}, {"first", "second"}) self._del_session(session) def test_filter_on_edge_get_edge(self) -> None: diff --git a/jac/jaclang/runtimelib/architype.py b/jac/jaclang/runtimelib/architype.py index 382bd41945..e04b8748ae 100644 --- a/jac/jaclang/runtimelib/architype.py +++ b/jac/jaclang/runtimelib/architype.py @@ -3,18 +3,110 @@ from __future__ import annotations import inspect -from dataclasses import asdict, dataclass, field, fields, is_dataclass +from dataclasses import asdict, dataclass, field, is_dataclass from enum import IntEnum +from functools import cached_property from logging import getLogger -from pickle import dumps +from re import IGNORECASE, compile from types import UnionType -from typing import Any, Callable, ClassVar, Optional, TypeVar +from typing import Any, Callable, ClassVar, Generic, Optional, Type, TypeVar from uuid import UUID, uuid4 logger = getLogger(__name__) -TARCH = TypeVar("TARCH", bound="Architype") -TANCH = TypeVar("TANCH", bound="Anchor") + +JID_REGEX = compile( + r"^(n|e|w|o):([^:]*):([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$", + IGNORECASE, +) +_ANCHOR = TypeVar("_ANCHOR", bound="Anchor") +_C_ANCHOR = TypeVar("_C_ANCHOR", bound="Anchor", covariant=True) + + +@dataclass(kw_only=True) +class JID(Generic[_C_ANCHOR]): + """Jaclang ID Interface.""" + + id: Any + type: Type[_C_ANCHOR] + name: str + + @cached_property + def __cached_repr__(self) -> str: + """Cached string representation.""" + return f"{self.type.__name__[:1].lower()}:{self.name}:{self.id}" + + @cached_property + def anchor(self) -> _C_ANCHOR | None: + """Get architype.""" + from jaclang.plugin.feature import JacFeature + + return JacFeature.get_context().mem.find_by_id(self) + + def __repr__(self) -> str: + """Override string representation.""" + return self.__cached_repr__ + + def __str__(self) -> str: + """Override string parsing.""" + return self.__cached_repr__ + + def __hash__(self) -> int: + """Return default hasher.""" + return hash(self.__cached_repr__) + + +@dataclass(kw_only=True) +class JacLangJID(JID[_ANCHOR]): + """Jaclang ID Implementation.""" + + def __init__( + self, + id: str | UUID | None = None, + type: Type[_ANCHOR] | None = None, + name: str = "", + ) -> None: + """Override JID initializer.""" + match id: + case str(): + if matched := JID_REGEX.search(id): + self.id = UUID(matched.group(3)) + self.name = matched.group(2) + # currently no way to base hinting on string regex! + match matched.group(1).lower(): + case "n": + self.type = NodeAnchor # type: ignore [assignment] + case "e": + self.type = EdgeAnchor # type: ignore [assignment] + case "w": + self.type = WalkerAnchor # type: ignore [assignment] + case _: + self.type = ObjectAnchor # type: ignore [assignment] + return + raise ValueError("Not a valid JID format!") + case UUID(): + self.id = id + case None: + self.id = uuid4() + case _: + raise ValueError("Not a valid id for JID!") + + if type is None: + raise ValueError("Type is required from non string JID!") + self.type = type + self.name = name + + def __getstate__(self) -> str: + """Override getstate.""" + return self.__cached_repr__ + + def __setstate__(self, state: str) -> None: + """Override setstate.""" + self.__init__(state) # type: ignore[misc] + + def __hash__(self) -> int: + """Return default hasher.""" + return hash(self.__cached_repr__) class AccessLevel(IntEnum): @@ -41,11 +133,11 @@ def cast(val: int | str | AccessLevel) -> AccessLevel: class Access: """Access Structure.""" - anchors: dict[str, AccessLevel] = field(default_factory=dict) + anchors: dict[JID, AccessLevel] = field(default_factory=dict) - def check(self, anchor: str) -> AccessLevel: + def check(self, id: JID) -> AccessLevel: """Validate access.""" - return self.anchors.get(anchor, AccessLevel.NO_ACCESS) + return self.anchors.get(id, AccessLevel.NO_ACCESS) @dataclass @@ -60,100 +152,41 @@ class Permission: class AnchorReport: """Report Handler.""" - id: str + id: JID context: dict[str, Any] -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class Anchor: """Object Anchor.""" architype: Architype - id: UUID = field(default_factory=uuid4) - root: Optional[UUID] = None + id: Any = field(default_factory=uuid4) + root: Optional[JID[NodeAnchor]] = None access: Permission = field(default_factory=Permission) persistent: bool = False hash: int = 0 - def is_populated(self) -> bool: - """Check if state.""" - return "architype" in self.__dict__ - - def make_stub(self: TANCH) -> TANCH: - """Return unsynced copy of anchor.""" - if self.is_populated(): - unloaded = object.__new__(self.__class__) - unloaded.id = self.id - return unloaded - return self - - def populate(self) -> None: - """Retrieve the Architype from db and return.""" - from jaclang.plugin.feature import JacFeature as Jac - - jsrc = Jac.get_context().mem - - if anchor := jsrc.find_by_id(self.id): - self.__dict__.update(anchor.__dict__) - - def __getattr__(self, name: str) -> object: - """Trigger load if detects unloaded state.""" - if not self.is_populated(): - self.populate() - - if not self.is_populated(): - raise ValueError( - f"{self.__class__.__name__} [{self.id}] is not a valid reference!" - ) - - return getattr(self, name) - - raise AttributeError( - f"'{self.__class__.__name__}' object has not attribute '{name}'" + @cached_property + def jid(self: _ANCHOR) -> JID[_ANCHOR]: + """Get JID representation.""" + jid = JacLangJID[_ANCHOR]( + self.id, + self.__class__, + ( + "" + if isinstance(self.architype, (GenericEdge, Root)) + else self.architype.__class__.__name__ + ), ) + jid.anchor = self - def __getstate__(self) -> dict[str, Any]: # NOTE: May be better type hinting - """Serialize Anchor.""" - if self.is_populated(): - unlinked = object.__new__(self.architype.__class__) - unlinked.__dict__.update(self.architype.__dict__) - unlinked.__dict__.pop("__jac__", None) - - return { - "id": self.id, - "architype": unlinked, - "root": self.root, - "access": self.access, - "persistent": self.persistent, - } - else: - return {"id": self.id} - - def __setstate__(self, state: dict[str, Any]) -> None: - """Deserialize Anchor.""" - self.__dict__.update(state) - - if self.is_populated() and self.architype: - self.architype.__jac__ = self - self.hash = hash(dumps(self)) + return jid - def __repr__(self) -> str: - """Override representation.""" - if self.is_populated(): - attrs = "" - for f in fields(self): - if f.name in self.__dict__: - attrs += f"{f.name}={self.__dict__[f.name]}, " - attrs = attrs[:-2] - else: - attrs = f"id={self.id}" - - return f"{self.__class__.__name__}({attrs})" - - def report(self) -> AnchorReport: + def report(self) -> Any: # noqa: ANN401 """Report Anchor.""" return AnchorReport( - id=self.id.hex, + id=self.jid, context=( asdict(self.architype) if is_dataclass(self.architype) and not isinstance(self.architype, type) @@ -163,59 +196,34 @@ def report(self) -> AnchorReport: def __hash__(self) -> int: """Override hash for anchor.""" - return hash(self.id) - - def __eq__(self, other: object) -> bool: - """Override equal implementation.""" - if isinstance(other, Anchor): - return self.__class__ is other.__class__ and self.id == other.id + return hash(self.jid) + def __eq__(self, value: object) -> bool: + """Override __eq__.""" + if isinstance(value, Anchor): + return value.jid == self.jid return False -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class NodeAnchor(Anchor): """Node Anchor.""" architype: NodeArchitype - edges: list[EdgeAnchor] - - def __getstate__(self) -> dict[str, object]: - """Serialize Node Anchor.""" - state = super().__getstate__() - - if self.is_populated(): - state["edges"] = [edge.make_stub() for edge in self.edges] + edges: list[JID["EdgeAnchor"]] - return state - -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class EdgeAnchor(Anchor): """Edge Anchor.""" architype: EdgeArchitype - source: NodeAnchor - target: NodeAnchor + source: JID["NodeAnchor"] + target: JID["NodeAnchor"] is_undirected: bool - def __getstate__(self) -> dict[str, object]: - """Serialize Node Anchor.""" - state = super().__getstate__() - - if self.is_populated(): - state.update( - { - "source": self.source.make_stub(), - "target": self.target.make_stub(), - "is_undirected": self.is_undirected, - } - ) - - return state - -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class WalkerAnchor(Anchor): """Walker Anchor.""" @@ -226,9 +234,9 @@ class WalkerAnchor(Anchor): disengaged: bool = False -@dataclass(eq=False, repr=False, kw_only=True) +@dataclass(eq=False, kw_only=True) class ObjectAnchor(Anchor): - """Edge Anchor.""" + """Object Anchor.""" architype: ObjectArchitype @@ -239,23 +247,23 @@ class Architype: _jac_entry_funcs_: ClassVar[list[DSFunc]] _jac_exit_funcs_: ClassVar[list[DSFunc]] - def __init__(self) -> None: - """Create default architype.""" - self.__jac__ = Anchor(architype=self) - def __repr__(self) -> str: """Override repr for architype.""" return f"{self.__class__.__name__}" + @cached_property + def __jac__(self) -> Anchor: + """Build anchor reference.""" + return Anchor(architype=self) + class NodeArchitype(Architype): """Node Architype Protocol.""" - __jac__: NodeAnchor - - def __init__(self) -> None: - """Create node architype.""" - self.__jac__ = NodeAnchor(architype=self, edges=[]) + @cached_property + def __jac__(self) -> NodeAnchor: + """Build anchor reference.""" + return NodeAnchor(architype=self, edges=[]) class EdgeArchitype(Architype): @@ -267,21 +275,19 @@ class EdgeArchitype(Architype): class WalkerArchitype(Architype): """Walker Architype Protocol.""" - __jac__: WalkerAnchor - - def __init__(self) -> None: - """Create walker architype.""" - self.__jac__ = WalkerAnchor(architype=self) + @cached_property + def __jac__(self) -> WalkerAnchor: + """Build anchor reference.""" + return WalkerAnchor(architype=self) class ObjectArchitype(Architype): """Walker Architype Protocol.""" - __jac__: ObjectAnchor - - def __init__(self) -> None: - """Create walker architype.""" - self.__jac__ = ObjectAnchor(architype=self) + @cached_property + def __jac__(self) -> ObjectAnchor: + """Build anchor reference.""" + return ObjectAnchor(architype=self) @dataclass(eq=False) @@ -299,9 +305,10 @@ class Root(NodeArchitype): _jac_entry_funcs_: ClassVar[list[DSFunc]] = [] _jac_exit_funcs_: ClassVar[list[DSFunc]] = [] - def __init__(self) -> None: - """Create root node.""" - self.__jac__ = NodeAnchor(architype=self, persistent=True, edges=[]) + @cached_property + def __jac__(self) -> NodeAnchor: + """Build anchor reference.""" + return NodeAnchor(architype=self, persistent=True, edges=[]) @dataclass(eq=False) diff --git a/jac/jaclang/runtimelib/constructs.py b/jac/jaclang/runtimelib/constructs.py index b39b0abf0d..555af339f0 100644 --- a/jac/jaclang/runtimelib/constructs.py +++ b/jac/jaclang/runtimelib/constructs.py @@ -11,8 +11,11 @@ EdgeAnchor, EdgeArchitype, GenericEdge, + JID, + JacLangJID, NodeAnchor, NodeArchitype, + ObjectArchitype, Root, WalkerAnchor, WalkerArchitype, @@ -31,7 +34,10 @@ "NodeArchitype", "EdgeArchitype", "WalkerArchitype", + "ObjectArchitype", "GenericEdge", + "JID", + "JacLangJID", "Root", "DSFunc", "Memory", diff --git a/jac/jaclang/runtimelib/context.py b/jac/jaclang/runtimelib/context.py index aa07b6193a..a65e4285f2 100644 --- a/jac/jaclang/runtimelib/context.py +++ b/jac/jaclang/runtimelib/context.py @@ -8,7 +8,7 @@ from typing import Any, Callable, Optional, cast from uuid import UUID -from .architype import NodeAnchor, Root +from .architype import JacLangJID, NodeAnchor, Root from .memory import Memory, ShelfStorage @@ -39,7 +39,9 @@ def init_anchor( ) -> NodeAnchor: """Load initial anchors.""" if anchor_id: - if isinstance(anchor := self.mem.find_by_id(UUID(anchor_id)), NodeAnchor): + if isinstance( + anchor := self.mem.find_by_id(JacLangJID(anchor_id)), NodeAnchor + ): return anchor raise ValueError(f"Invalid anchor id {anchor_id} !") return default @@ -65,11 +67,11 @@ def create( ctx.reports = [] if not isinstance( - system_root := ctx.mem.find_by_id(SUPER_ROOT_UUID), NodeAnchor + system_root := ctx.mem.find_by_id(SUPER_ROOT_ANCHOR.jid), NodeAnchor ): system_root = Root().__jac__ system_root.id = SUPER_ROOT_UUID - ctx.mem.set(system_root.id, system_root) + ctx.mem.set(system_root.jid, system_root) ctx.system_root = system_root diff --git a/jac/jaclang/runtimelib/memory.py b/jac/jaclang/runtimelib/memory.py index 07fb2bea31..72b198fab5 100644 --- a/jac/jaclang/runtimelib/memory.py +++ b/jac/jaclang/runtimelib/memory.py @@ -5,35 +5,32 @@ from dataclasses import dataclass, field from pickle import dumps from shelve import Shelf, open -from typing import Callable, Generator, Generic, Iterable, TypeVar -from uuid import UUID +from typing import Callable, Generator, Iterable -from .architype import Anchor, NodeAnchor, Root, TANCH - -ID = TypeVar("ID") +from .architype import Anchor, JID, NodeAnchor, Root, _ANCHOR @dataclass -class Memory(Generic[ID, TANCH]): +class Memory: """Generic Memory Handler.""" - __mem__: dict[ID, TANCH] = field(default_factory=dict) - __gc__: set[TANCH] = field(default_factory=set) + __mem__: dict[JID, Anchor] = field(default_factory=dict) + __gc__: set[JID] = field(default_factory=set) def close(self) -> None: """Close memory handler.""" self.__mem__.clear() self.__gc__.clear() - def is_cached(self, id: ID) -> bool: + def is_cached(self, id: JID) -> bool: """Check if id if already cached.""" return id in self.__mem__ def find( self, - ids: ID | Iterable[ID], - filter: Callable[[TANCH], TANCH] | None = None, - ) -> Generator[TANCH, None, None]: + ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, + ) -> Generator[_ANCHOR, None, None]: """Find anchors from memory by ids with filter.""" if not isinstance(ids, Iterable): ids = [ids] @@ -41,37 +38,43 @@ def find( return ( anchor for id in ids - if (anchor := self.__mem__.get(id)) and (not filter or filter(anchor)) + if (anchor := self.__mem__.get(id)) + and isinstance(anchor, id.type) + and (not filter or filter(anchor)) ) def find_one( self, - ids: ID | Iterable[ID], - filter: Callable[[TANCH], TANCH] | None = None, - ) -> TANCH | None: + ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, + ) -> _ANCHOR | None: """Find one anchor from memory by ids with filter.""" return next(self.find(ids, filter), None) - def find_by_id(self, id: ID) -> TANCH | None: + def find_by_id(self, id: JID[_ANCHOR]) -> _ANCHOR | None: """Find one by id.""" - return self.__mem__.get(id) + return ( + anchor + if (anchor := self.__mem__.get(id)) and isinstance(anchor, id.type) + else None + ) - def set(self, id: ID, data: TANCH) -> None: + def set(self, id: JID[_ANCHOR], data: _ANCHOR) -> None: """Save anchor to memory.""" self.__mem__[id] = data - def remove(self, ids: ID | Iterable[ID]) -> None: + def remove(self, ids: JID | Iterable[JID]) -> None: """Remove anchor/s from memory.""" if not isinstance(ids, Iterable): ids = [ids] for id in ids: - if anchor := self.__mem__.pop(id, None): - self.__gc__.add(anchor) + self.__mem__.pop(id, None) + self.__gc__.add(id) @dataclass -class ShelfStorage(Memory[UUID, Anchor]): +class ShelfStorage(Memory): """Shelf Handler.""" __shelf__: Shelf[Anchor] | None = None @@ -84,9 +87,9 @@ def __init__(self, session: str | None = None) -> None: def close(self) -> None: """Close memory handler.""" if isinstance(self.__shelf__, Shelf): - for anchor in self.__gc__: - self.__shelf__.pop(str(anchor.id), None) - self.__mem__.pop(anchor.id, None) + for jid in self.__gc__: + self.__shelf__.pop(str(jid), None) + self.__mem__.pop(jid, None) keys = set(self.__mem__.keys()) @@ -99,7 +102,7 @@ def close(self) -> None: self.__shelf__.close() super().close() - def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: + def sync_mem_to_db(self, keys: Iterable[JID]) -> None: """Manually sync memory to db.""" from jaclang.plugin.feature import JacFeature as Jac @@ -110,7 +113,7 @@ def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: and d.persistent and d.hash != hash(dumps(d)) ): - _id = str(d.id) + _id = str(d.jid) if p_d := self.__shelf__.get(_id): if ( isinstance(p_d, NodeAnchor) @@ -123,11 +126,15 @@ def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: continue p_d.edges = d.edges - 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)): - p_d.architype = d.architype + if hash(dumps(p_d.architype)) != hash( + dumps(d.architype) + ) and Jac.check_write_access(d): + p_d.architype = d.architype + + if hash(dumps(p_d.access)) != hash( + dumps(d.access) + ) and Jac.check_write_access(d): + p_d.access = d.access self.__shelf__[_id] = p_d elif not ( @@ -139,9 +146,9 @@ def sync_mem_to_db(self, keys: Iterable[UUID]) -> None: def find( self, - ids: UUID | Iterable[UUID], - filter: Callable[[Anchor], Anchor] | None = None, - ) -> Generator[Anchor, None, None]: + ids: JID[_ANCHOR] | Iterable[JID[_ANCHOR]], + filter: Callable[[_ANCHOR], _ANCHOR] | None = None, + ) -> Generator[_ANCHOR, None, None]: """Find anchors from datasource by ids with filter.""" if not isinstance(ids, Iterable): ids = [ids] @@ -155,21 +162,28 @@ def find( and id not in self.__gc__ and (_anchor := self.__shelf__.get(str(id))) ): - self.__mem__[id] = anchor = _anchor - if anchor and (not filter or filter(anchor)): + anchor = self.__mem__[id] = _anchor + anchor.architype.__jac__ = anchor + if ( + anchor + and isinstance(anchor, id.type) + and (not filter or filter(anchor)) + ): yield anchor else: yield from super().find(ids, filter) - def find_by_id(self, id: UUID) -> Anchor | None: + def find_by_id(self, id: JID[_ANCHOR]) -> _ANCHOR | None: """Find one by id.""" data = super().find_by_id(id) if ( not data and isinstance(self.__shelf__, Shelf) - and (data := self.__shelf__.get(str(id))) + and (_data := self.__shelf__.get(str(id))) + and isinstance(_data, id.type) ): - self.__mem__[id] = data + data = self.__mem__[id] = _data + data.architype.__jac__ = data return data diff --git a/jac/jaclang/runtimelib/utils.py b/jac/jaclang/runtimelib/utils.py index 83e35ab2cf..fb907b4e05 100644 --- a/jac/jaclang/runtimelib/utils.py +++ b/jac/jaclang/runtimelib/utils.py @@ -37,14 +37,13 @@ def collect_node_connections( if current_node not in visited_nodes: visited_nodes.add(current_node) edges = current_node.edges - for edge_ in edges: - target = edge_.target - if target: + for jid in edges: + if (edge := jid.anchor) and (target := edge.target.anchor): connections.add( ( current_node.architype, target.architype, - edge_.__class__.__name__, + edge.__class__.__name__, ) ) collect_node_connections(target, visited_nodes, connections) @@ -66,16 +65,18 @@ def traverse_graph( edge_limit: int, ) -> None: """Traverse the graph using Breadth-First Search (BFS) or Depth-First Search (DFS).""" - for edge in node.__jac__.edges: - is_self_loop = id(edge.source) == id(edge.target) - is_in_edge = edge.target == node.__jac__ + for jid in node.__jac__.edges: + if not (edge := jid.anchor): + continue + is_self_loop = edge.source == edge.target + is_in_edge = edge.target == node.__jac__.jid if (traverse and is_in_edge) or edge.architype.__class__.__name__ in edge_type: continue if is_self_loop: continue # lets skip self loop for a while, need to handle it later - elif (other_nda := edge.target if not is_in_edge else edge.source) and ( - other_nd := other_nda.architype - ): + elif ( + other_nda := edge.target.anchor if not is_in_edge else edge.source.anchor + ) and (other_nd := other_nda.architype): new_con = ( (node, other_nd, edge.architype) if not is_in_edge diff --git a/jac/jaclang/tests/fixtures/edge_ops.jac b/jac/jaclang/tests/fixtures/edge_ops.jac index 9ace322c16..e024717464 100644 --- a/jac/jaclang/tests/fixtures/edge_ops.jac +++ b/jac/jaclang/tests/fixtures/edge_ops.jac @@ -22,7 +22,7 @@ edge MyEdge { for j=0 to j<3 by j+=1 { end +:MyEdge:val=random.randint(1, 15), val2=random.randint(1, 5):+> node_a(value=j + 10); } - print([(arch.val, arch.val2) for i in end.__jac__.edges if isinstance(arch := i.architype, MyEdge)]); + print([(arch.val, arch.val2) for i in end.__jac__.edges if (anch := i.anchor) and isinstance(arch := anch.architype, MyEdge)]); } } for i=0 to i<3 by i+=1 { diff --git a/jac/jaclang/tests/test_language.py b/jac/jaclang/tests/test_language.py index 92f3394cc1..a403c9d3be 100644 --- a/jac/jaclang/tests/test_language.py +++ b/jac/jaclang/tests/test_language.py @@ -1177,7 +1177,7 @@ def test_object_ref_interface(self) -> None: cli.run(self.fixture_abs_path("objref.jac")) sys.stdout = sys.__stdout__ stdout_value = captured_output.getvalue().split("\n") - self.assertEqual(len(stdout_value[0]), 32) + self.assertEqual(len(stdout_value[0]), 45) self.assertEqual("MyNode(value=0)", stdout_value[1]) self.assertEqual("valid: True", stdout_value[2])