Skip to content
This repository has been archived by the owner on Dec 10, 2024. It is now read-only.

Core: Add reference #69

Merged
merged 14 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/faebryk/core/graphinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@

from faebryk.core.core import ID_REPR, FaebrykLibObject
from faebryk.core.graph_backends.default import GraphImpl
from faebryk.core.link import Link, LinkDirect, LinkNamedParent
from faebryk.core.link import (
Link,
LinkDirect,
LinkNamedParent,
LinkPointer,
LinkSibling,
)
from faebryk.libs.util import (
KeyErrorNotFound,
NotNone,
exceptions_to_log,
find,
try_avoid_endless_recursion,
)

Expand Down Expand Up @@ -191,3 +199,22 @@ def disconnect_parent(self):


class GraphInterfaceSelf(GraphInterface): ...


class GraphInterfaceReference[T: "Node"](GraphInterface):
"""Represents a reference to a node object"""

class UnboundError(Exception):
"""Cannot resolve unbound reference"""

def get_referenced_gif(self) -> GraphInterfaceSelf:
try:
return find(
self.get_links_by_type(LinkPointer),
lambda link: not isinstance(link, LinkSibling),
).self_gif
except KeyErrorNotFound as ex:
raise GraphInterfaceReference.UnboundError from ex

def get_reference(self) -> T:
return self.get_referenced_gif().node
43 changes: 37 additions & 6 deletions src/faebryk/core/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
logger = logging.getLogger(__name__)

if TYPE_CHECKING:
from faebryk.core.graphinterface import GraphInterface, GraphInterfaceHierarchical
from faebryk.core.graphinterface import (
GraphInterface,
GraphInterfaceHierarchical,
GraphInterfaceSelf,
)


class Link(FaebrykLibObject):
Expand Down Expand Up @@ -38,13 +42,36 @@ def __repr__(self) -> str:
return f"{type(self).__name__}()"


class LinkSibling(Link):
def __init__(self, interfaces: list["GraphInterface"]) -> None:
class LinkPointer(Link):
"""A Link that points towards a self-gif"""

def __init__(
self,
interfaces: list["GraphInterfaceSelf | GraphInterface"],
) -> None:
from faebryk.core.graphinterface import GraphInterfaceSelf

super().__init__()
self.interfaces = interfaces
assert len(interfaces) == 2
if isinstance(interfaces[0], GraphInterfaceSelf) and not isinstance(
interfaces[1], GraphInterfaceSelf
):
self.self_gif = interfaces[0]
self.other_gif = interfaces[1]
elif isinstance(interfaces[1], GraphInterfaceSelf) and not isinstance(
interfaces[0], GraphInterfaceSelf
):
self.self_gif = interfaces[1]
self.other_gif = interfaces[0]
else:
raise TypeError("Interfaces must be one self-gif and one other-gif")

def get_connections(self) -> list["GraphInterface"]:
return self.interfaces
return [self.self_gif, self.other_gif]


class LinkSibling(LinkPointer):
"""A link represents a connection between a self-gif and a gif in the same node"""


class LinkParent(Link):
Expand Down Expand Up @@ -83,9 +110,13 @@ def curried(interfaces: list["GraphInterface"]):


class LinkDirect(Link):
"""Represents a symmetrical link between two interfaces of the same type"""

def __init__(self, interfaces: list["GraphInterface"]) -> None:
super().__init__()
assert len(set(map(type, interfaces))) == 1
assert (
len(set(map(type, interfaces))) == 1
), "Interfaces must be of the same type"
self.interfaces = interfaces

def get_connections(self) -> list["GraphInterface"]:
Expand Down
78 changes: 56 additions & 22 deletions src/faebryk/core/node.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# This file is part of the faebryk project
# SPDX-License-Identifier: MIT
import logging
from abc import abstractmethod
from dataclasses import InitVar as dataclass_InitVar
from itertools import chain
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -65,13 +67,29 @@ class fab_field:
pass


class rt_field[T, O](property, fab_field):
class constructed_field[T: "Node", O: "Node"](property, fab_field):
"""
Field which is constructed after the node is created.
The constructor gets one argument: the node instance.

The constructor should return the constructed faebryk object or None.
If a faebryk object is returned, it will be added to the node.
"""

@abstractmethod
def __construct__(self, obj: T) -> O | None:
pass


class rt_field[T, O](constructed_field):
"""TODO: what does an rt_field represent? what does "rt" stand for?"""

def __init__(self, fget: Callable[[T], O]) -> None:
super().__init__()
self.func = fget
self.lookup: dict[T, O] = {}

def _construct(self, obj: T):
def __construct__(self, obj: T):
constructed = self.func(obj)
# TODO find a better way for this
# in python 3.13 name support
Expand Down Expand Up @@ -145,6 +163,14 @@ def __init__(self, node: "Node", other: "Node", *args: object) -> None:
class NodeNoParent(NodeException): ...


class InitVar(dataclass_InitVar):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this belong in Node?

"""
This is a type-marker which instructs the Node constructor to ignore the field.

Inspired by dataclasses.InitVar, which it inherits from.
"""


# -----------------------------------------------------------------------------


Expand All @@ -167,7 +193,7 @@ def __hash__(self) -> int:
# TODO proper hash
return hash(id(self))

def add[T: Node](
def add[T: Node | GraphInterface](
self,
obj: T,
name: str | None = None,
Expand All @@ -176,9 +202,10 @@ def add[T: Node](
assert obj is not None

if container is None:
container = self.runtime_anon
if name:
container = self.runtime
else:
container = self.runtime_anon

try:
container_name = find(vars(self).items(), lambda x: x[1] is container)[0]
Expand All @@ -197,7 +224,11 @@ def add[T: Node](
container.append(obj)
name = f"{container_name}[{len(container) - 1}]"

self._handle_add_node(name, obj)
if isinstance(obj, GraphInterface):
self._handle_add_gif(name, obj)
else:
self._handle_add_node(name, obj)

return obj

def add_to_container[T: Node](
Expand Down Expand Up @@ -260,16 +291,20 @@ def is_genalias_node(obj):
if get_origin(obj):
return is_genalias_node(obj)

if isinstance(obj, rt_field):
if isinstance(obj, constructed_field):
return True

return False

clsfields_unf = {
name: obj
for name, obj in chain(
[(name, f) for name, f in annos.items()],
[(name, f) for name, f in vars_.items() if isinstance(f, fab_field)],
(
(name, obj)
for name, obj in annos.items()
if not isinstance(obj, InitVar)
),
((name, f) for name, f in vars_.items() if isinstance(f, fab_field)),
)
if not name.startswith("_")
}
Expand Down Expand Up @@ -305,7 +340,9 @@ def handle_add(name, obj):
elif isinstance(obj, Node):
self._handle_add_node(name, obj)
else:
assert False
raise TypeError(
f"Cannot handle adding field {name=} of type {type(obj)}"
)

def append(name, inst):
if isinstance(inst, LL_Types):
Expand All @@ -321,21 +358,15 @@ def append(name, inst):
return inst

def _setup_field(name, obj):
def setup_gen_alias(name, obj):
origin = get_origin(obj)
assert origin
if isinstance(obj, str):
raise NotImplementedError()

if origin := get_origin(obj):
if isinstance(origin, type):
setattr(self, name, append(name, origin()))
return
raise NotImplementedError(origin)

if isinstance(obj, str):
raise NotImplementedError()

if get_origin(obj):
setup_gen_alias(name, obj)
return

if isinstance(obj, _d_field):
setattr(self, name, append(name, obj.default_factory()))
return
Expand All @@ -344,8 +375,9 @@ def setup_gen_alias(name, obj):
setattr(self, name, append(name, obj()))
return

if isinstance(obj, rt_field):
append(name, obj._construct(self))
if isinstance(obj, constructed_field):
if constructed := obj.__construct__(self):
append(name, constructed)
return

raise NotImplementedError()
Expand All @@ -360,7 +392,9 @@ def setup_field(name, obj):
f'An exception occurred while constructing field "{name}"',
) from e

nonrt, rt = partition(lambda x: isinstance(x[1], rt_field), clsfields.items())
nonrt, rt = partition(
lambda x: isinstance(x[1], constructed_field), clsfields.items()
)
for name, obj in nonrt:
setup_field(name, obj)

Expand Down
71 changes: 71 additions & 0 deletions src/faebryk/core/reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from collections import defaultdict

from faebryk.core.graphinterface import GraphInterfaceReference
from faebryk.core.link import LinkPointer
from faebryk.core.node import Node, constructed_field


class Reference[O: Node](constructed_field):
"""
Create a simple reference to other nodes that are properly encoded in the graph.
"""

class UnboundError(Exception):
"""Cannot resolve unbound reference"""

def __init__(self, out_type: type[O] | None = None):
self.gifs: dict[Node, GraphInterfaceReference] = defaultdict(
GraphInterfaceReference
)
self.points_to: dict[Node, O] = {}

def get(instance: Node) -> O:
if instance not in self.gifs:
raise Reference.UnboundError

my_gif = self.gifs[instance]

try:
return my_gif.get_reference()
except GraphInterfaceReference.UnboundError as ex:
raise Reference.UnboundError from ex

def set(instance: Node, value: O):
if instance in self.points_to:
# TypeError is also raised when attempting to assign
# to an immutable (eg. tuple)
raise TypeError(
f"{self.__class__.__name__} already set and are immutable"
)

if out_type is not None and not isinstance(value, out_type):
raise TypeError(f"Expected {out_type} got {type(value)}")

self.points_to[instance] = value

# if we've already been graph-constructed
# then immediately attach our gif to what we're referring to
# if not, this is done in the construction
if instance._init:
self.gifs[instance].connect(value.self_gif, LinkPointer)

property.__init__(self, get, set)

def __construct__(self, obj: Node) -> None:
gif = obj.add(self.gifs[obj])

# if what we're referring to is set, then immediately also connect the link
if points_to := self.points_to.get(obj):
gif.connect(points_to.self_gif, LinkPointer)

# don't attach anything additional to the Node during field setup
return None


def reference[O: Node](out_type: type[O] | None = None) -> O | Reference:
"""
Create a simple reference to other nodes properly encoded in the graph.

This final wrapper is primarily to fudge the typing.
"""
return Reference(out_type=out_type)
1 change: 1 addition & 0 deletions src/faebryk/library/_F.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from faebryk.library.has_picker import has_picker
from faebryk.library.has_pcb_layout import has_pcb_layout
from faebryk.library.has_pcb_routing_strategy import has_pcb_routing_strategy
from faebryk.library.has_reference import has_reference
from faebryk.library.has_resistance import has_resistance
from faebryk.library.has_single_connection import has_single_connection
from faebryk.library.is_representable_by_single_value import is_representable_by_single_value
Expand Down
15 changes: 15 additions & 0 deletions src/faebryk/library/has_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from faebryk.core.node import Node
from faebryk.core.reference import Reference
from faebryk.core.trait import Trait


class has_reference[T: Node](Trait.decless()):
"""Trait-attached reference"""

reference: T = Reference()

def __init__(self, reference: T):
super().__init__()
self.reference = reference

# TODO: extend this class with support for other trait-types
Loading