Merge branch 'dev_processGraph' of https://github.com/alicevision/meshroom into dev_processGraph

Conflicts:
	meshroom/core/graph.py
This commit is contained in:
Fabien Castan 2017-10-16 12:28:44 +02:00
commit fd1b9f2cea
8 changed files with 381 additions and 43 deletions

View file

@ -99,12 +99,13 @@ class Attribute(BaseObject):
# only dependent of the linked node uid, so it is independent # only dependent of the linked node uid, so it is independent
# from the cache folder which may be used in the filepath. # from the cache folder which may be used in the filepath.
return self.node.uid() return self.node.uid()
if self.isLink(): if self.isLink:
return self.getLinkParam().uid() return self.getLinkParam().uid()
if isinstance(self._value, basestring): if isinstance(self._value, basestring):
return hash(str(self._value)) return hash(str(self._value))
return hash(self._value) return hash(self._value)
@property
def isLink(self): def isLink(self):
""" """
If the attribute is a link to another attribute. If the attribute is a link to another attribute.
@ -115,7 +116,7 @@ class Attribute(BaseObject):
return self in self.node.graph.edges.keys() return self in self.node.graph.edges.keys()
def getLinkParam(self): def getLinkParam(self):
if not self.isLink(): if not self.isLink:
return None return None
return self.node.graph.edge(self).src return self.node.graph.edge(self).src
@ -141,7 +142,7 @@ class Attribute(BaseObject):
def getExportValue(self): def getExportValue(self):
value = self._value value = self._value
# print('getExportValue: ', self.name(), value, self.isLink()) # print('getExportValue: ', self.name(), value, self.isLink())
if self.isLink(): if self.isLink:
value = '{' + self.getLinkParam().fullName() + '}' value = '{' + self.getLinkParam().fullName() + '}'
return value return value
@ -150,6 +151,8 @@ class Attribute(BaseObject):
valueChanged = Signal() valueChanged = Signal()
value = Property("QVariant", value.fget, value.fset, notify=valueChanged) value = Property("QVariant", value.fget, value.fset, notify=valueChanged)
isOutput = Property(bool, isOutput.fget, constant=True) isOutput = Property(bool, isOutput.fget, constant=True)
isLinkChanged = Signal()
isLink = Property(bool, isLink.fget, notify=isLinkChanged)
class Edge(BaseObject): class Edge(BaseObject):
@ -223,6 +226,7 @@ class Node(BaseObject):
self.attribute(k)._value = v self.attribute(k)._value = v
self.status = StatusData(self.name, self.nodeType()) self.status = StatusData(self.name, self.nodeType())
self.statistics = stats.Statistics() self.statistics = stats.Statistics()
self._subprocess = None
def __getattr__(self, k): def __getattr__(self, k):
try: try:
@ -354,11 +358,12 @@ class Node(BaseObject):
""" """
statusFile = self.statusFile() statusFile = self.statusFile()
if not os.path.exists(statusFile): if not os.path.exists(statusFile):
self.status.status = Status.NONE self.upgradeStatusTo(Status.NONE)
return return
with open(statusFile, 'r') as jsonFile: with open(statusFile, 'r') as jsonFile:
statusData = json.load(jsonFile) statusData = json.load(jsonFile)
self.status.fromDict(statusData) self.status.fromDict(statusData)
self.statusChanged.emit()
def saveStatusFile(self): def saveStatusFile(self):
""" """
@ -400,6 +405,7 @@ class Node(BaseObject):
print('WARNING: downgrade status on node "{}" from {} to {}'.format(self._name, self.status.status.name, print('WARNING: downgrade status on node "{}" from {} to {}'.format(self._name, self.status.status.name,
newStatus)) newStatus))
self.status.status = newStatus self.status.status = newStatus
self.statusChanged.emit()
self.saveStatusFile() self.saveStatusFile()
def isAlreadySubmitted(self): def isAlreadySubmitted(self):
@ -411,6 +417,10 @@ class Node(BaseObject):
def beginSequence(self): def beginSequence(self):
self.upgradeStatusTo(Status.SUBMITTED_LOCAL) self.upgradeStatusTo(Status.SUBMITTED_LOCAL)
def stopProcess(self):
if self._subprocess:
self._subprocess.terminate()
def process(self): def process(self):
self.upgradeStatusTo(Status.RUNNING) self.upgradeStatusTo(Status.RUNNING)
statThread = stats.StatisticsThread(self) statThread = stats.StatisticsThread(self)
@ -421,20 +431,20 @@ class Node(BaseObject):
cmd = self.commandLine() cmd = self.commandLine()
print(' - commandLine:', cmd) print(' - commandLine:', cmd)
print(' - logFile:', self.logFile()) print(' - logFile:', self.logFile())
self.proc = psutil.Popen(cmd, stdout=logF, stderr=logF, shell=True) self._subprocess = psutil.Popen(cmd, stdout=logF, stderr=logF, shell=True)
# store process static info into the status file # store process static info into the status file
self.status.commandLine = cmd self.status.commandLine = cmd
# self.status.env = self.proc.environ() # self.status.env = self.proc.environ()
# self.status.createTime = self.proc.create_time() # self.status.createTime = self.proc.create_time()
statThread.proc = self.proc statThread.proc = self._subprocess
stdout, stderr = self.proc.communicate() stdout, stderr = self._subprocess.communicate()
self.proc.wait() self._subprocess.wait()
self.status.returnCode = self.proc.returncode self.status.returnCode = self._subprocess.returncode
if self.proc.returncode != 0: if self._subprocess.returncode != 0:
logContent = '' logContent = ''
with open(self.logFile(), 'r') as logF: with open(self.logFile(), 'r') as logF:
logContent = ''.join(logF.readlines()) logContent = ''.join(logF.readlines())
@ -443,11 +453,13 @@ class Node(BaseObject):
except: except:
self.upgradeStatusTo(Status.ERROR) self.upgradeStatusTo(Status.ERROR)
raise raise
elapsedTime = time.time() - startTime finally:
print(' - elapsed time:', elapsedTime) elapsedTime = time.time() - startTime
statThread.running = False print(' - elapsed time:', elapsedTime)
# Don't need to join, the thread will finish a bit later. self._subprocess = None
# statThread.join() # ask and wait for the stats thread to terminate
statThread.stopRequest()
statThread.join()
self.upgradeStatusTo(Status.SUCCESS) self.upgradeStatusTo(Status.SUCCESS)
@ -457,12 +469,19 @@ class Node(BaseObject):
def getStatus(self): def getStatus(self):
return self.status return self.status
@property
def statusName(self):
return self.status.status.name
name = Property(str, getName, constant=True) name = Property(str, getName, constant=True)
attributes = Property(BaseObject, getAttributes, constant=True) attributes = Property(BaseObject, getAttributes, constant=True)
internalFolderChanged = Signal() internalFolderChanged = Signal()
internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged) internalFolder = Property(str, internalFolder.fget, notify=internalFolderChanged)
depthChanged = Signal() depthChanged = Signal()
depth = Property(int, depth.fget, notify=depthChanged) depth = Property(int, depth.fget, notify=depthChanged)
statusChanged = Signal()
statusName = Property(str, statusName.fget, notify=statusChanged)
WHITE = 0 WHITE = 0
GRAY = 1 GRAY = 1
@ -555,6 +574,7 @@ class Graph(BaseObject):
node._name = self._createUniqueNodeName(node.nodeType()) node._name = self._createUniqueNodeName(node.nodeType())
node.graph = self node.graph = self
self._nodes.add(node) self._nodes.add(node)
self.stopExecutionRequested.connect(node.stopProcess)
# Trigger internal update when an attribute is modified # Trigger internal update when an attribute is modified
for attr in node.attributes: # type: Attribute for attr in node.attributes: # type: Attribute
@ -629,20 +649,31 @@ class Graph(BaseObject):
nodesWithOutput = set([edge.src.node for edge in self.edges]) nodesWithOutput = set([edge.src.node for edge in self.edges])
return set(self._nodes) - nodesWithOutput return set(self._nodes) - nodesWithOutput
def addEdge(self, outputAttr, inputAttr): def addEdge(self, srcAttr, dstAttr):
assert isinstance(outputAttr, Attribute) assert isinstance(srcAttr, Attribute)
assert isinstance(inputAttr, Attribute) assert isinstance(dstAttr, Attribute)
if outputAttr.node.graph != self or inputAttr.node.graph != self: if srcAttr.node.graph != self or dstAttr.node.graph != self:
raise RuntimeError('The attributes of the edge should be part of a common graph.') raise RuntimeError('The attributes of the edge should be part of a common graph.')
if inputAttr in self.edges.keys(): if dstAttr in self.edges.keys():
raise RuntimeError('Input attribute "{}" is already connected.'.format(inputAttr.fullName())) raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.fullName()))
self.edges.add(Edge(outputAttr, inputAttr)) edge = Edge(srcAttr, dstAttr)
inputAttr.valueChanged.emit() self.edges.add(edge)
dstAttr.valueChanged.emit()
dstAttr.isLinkChanged.emit()
return edge
def addEdges(self, *edges): def addEdges(self, *edges):
for edge in edges: for edge in edges:
self.addEdge(*edge) self.addEdge(*edge)
def removeEdge(self, dstAttr):
if dstAttr not in self.edges.keys():
raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.fullName()))
edge = self.edges.pop(dstAttr)
dstAttr.valueChanged.emit()
dstAttr.isLinkChanged.emit()
return edge
def getDepth(self, node): def getDepth(self, node):
# TODO: would be better to use bfs instead of recursive function # TODO: would be better to use bfs instead of recursive function
inputEdges = self.getInputEdges(node) inputEdges = self.getInputEdges(node)
@ -753,6 +784,10 @@ class Graph(BaseObject):
self.updateInternals() self.updateInternals()
self.updateStatusFromCache() self.updateStatusFromCache()
def stopExecution(self):
""" Request graph execution to be stopped """
self.stopExecutionRequested.emit()
@property @property
def nodes(self): def nodes(self):
return self._nodes return self._nodes
@ -764,6 +799,7 @@ class Graph(BaseObject):
nodes = Property(BaseObject, nodes.fget, constant=True) nodes = Property(BaseObject, nodes.fget, constant=True)
edges = Property(BaseObject, edges.fget, constant=True) edges = Property(BaseObject, edges.fget, constant=True)
stopExecutionRequested = Signal()
def loadGraph(filepath): def loadGraph(filepath):
""" """

View file

@ -191,8 +191,8 @@ class StatisticsThread(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.node = node self.node = node
self.proc = None self.proc = None
self.running = True
self.statistics = self.node.statistics self.statistics = self.node.statistics
self._stopFlag = threading.Event()
def updateStats(self): def updateStats(self):
self.lastTime = time.time() self.lastTime = time.time()
@ -200,7 +200,14 @@ class StatisticsThread(threading.Thread):
self.node.saveStatistics() self.node.saveStatistics()
def run(self): def run(self):
while self.running: while True:
self.updateStats() self.updateStats()
time.sleep(60) if self._stopFlag.wait(60):
# stopFlag has been set
# update stats one last time and exit main loop
self.updateStats()
return
def stopRequest(self):
""" Request the thread to exit as soon as possible. """
self._stopFlag.set()

View file

@ -7,6 +7,7 @@ from PySide2.QtQml import QQmlApplicationEngine
from meshroom.ui.reconstruction import Reconstruction from meshroom.ui.reconstruction import Reconstruction
from meshroom.ui.utils import QmlInstantEngine from meshroom.ui.utils import QmlInstantEngine
from meshroom.ui import components
if __name__ == "__main__": if __name__ == "__main__":
@ -17,9 +18,14 @@ if __name__ == "__main__":
engine = QmlInstantEngine() engine = QmlInstantEngine()
engine.addFilesFromDirectory(qmlDir) engine.addFilesFromDirectory(qmlDir)
engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False)) engine.setWatching(os.environ.get("MESHROOM_INSTANT_CODING", False))
components.registerTypes()
r = Reconstruction() r = Reconstruction()
engine.rootContext().setContextProperty("_reconstruction", r) engine.rootContext().setContextProperty("_reconstruction", r)
# Request any potential computation to stop on exit
app.aboutToQuit.connect(r.stopExecution)
engine.load(os.path.normpath(url)) engine.load(os.path.normpath(url))
app.exec_() app.exec_()

View file

@ -79,6 +79,7 @@ class AddNodeCommand(GraphCommand):
def redoImpl(self): def redoImpl(self):
self.node = self.graph.addNewNode(self.nodeType) self.node = self.graph.addNewNode(self.nodeType)
self.setText("Add Node {}".format(self.node.getName())) self.setText("Add Node {}".format(self.node.getName()))
self.node._applyExpr()
return True return True
def undoImpl(self): def undoImpl(self):
@ -103,13 +104,14 @@ class RemoveNodeCommand(GraphCommand):
parent=self.graph, **self.nodeDesc["attributes"] parent=self.graph, **self.nodeDesc["attributes"]
), self.nodeName) ), self.nodeName)
assert (node.getName() == self.nodeName) assert (node.getName() == self.nodeName)
# recreate edges deleted on node removal # recreate edges deleted on node removal
# edges having this node as destination could be retrieved from node description
# but we're missing edges starting from this node
for key, value in self.edges.items(): for key, value in self.edges.items():
iNode, iAttr = key.split(".") dstNode, dstAttr = key.split(".")
oNode, oAttr = value.split(".") srcNode, srcAttr = value.split(".")
self.graph.addEdge(self.graph.node(oNode).attribute(oAttr), self.graph.addEdge(self.graph.node(srcNode).attribute(srcAttr),
self.graph.node(iNode).attribute(iAttr)) self.graph.node(dstNode).attribute(dstAttr))
node.updateInternals() node.updateInternals()
@ -131,3 +133,38 @@ class SetAttributeCommand(GraphCommand):
def undoImpl(self): def undoImpl(self):
self.graph.node(self.nodeName).attribute(self.attrName).value = self.oldValue self.graph.node(self.nodeName).attribute(self.attrName).value = self.oldValue
class AddEdgeCommand(GraphCommand):
def __init__(self, graph, src, dst, parent=None):
super(AddEdgeCommand, self).__init__(graph, parent)
self.srcNode, self.srcAttr = src.fullName().split(".")
self.dstNode, self.dstAttr = dst.fullName().split(".")
self.setText("Connect '{}'->'{}'".format(src.fullName(), dst.fullName()))
def redoImpl(self):
try:
self.graph.addEdge(self.graph.node(self.srcNode).attribute(self.srcAttr),
self.graph.node(self.dstNode).attribute(self.dstAttr))
except RuntimeError:
return False
return True
def undoImpl(self):
self.graph.removeEdge(self.graph.node(self.dstNode).attribute(self.dstAttr))
class RemoveEdgeCommand(GraphCommand):
def __init__(self, graph, edge, parent=None):
super(RemoveEdgeCommand, self).__init__(graph, parent)
self.srcNode, self.srcAttr = edge.src.fullName().split(".")
self.dstNode, self.dstAttr = edge.dst.fullName().split(".")
self.setText("Disconnect '{}'->'{}'".format(edge.src.fullName(), edge.dst.fullName()))
def redoImpl(self):
self.graph.removeEdge(self.graph.node(self.dstNode).attribute(self.dstAttr))
return True
def undoImpl(self):
self.graph.addEdge(self.graph.node(self.srcNode).attribute(self.srcAttr),
self.graph.node(self.dstNode).attribute(self.dstAttr))

View file

@ -0,0 +1,6 @@
def registerTypes():
from PySide2.QtQml import qmlRegisterType
from meshroom.ui.components.edge import EdgeMouseArea
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")

210
meshroom/ui/components/edge.py Executable file
View file

@ -0,0 +1,210 @@
from PySide2.QtCore import Signal, Property, QPointF, Qt, QObject
from PySide2.QtGui import QPainterPath, QVector2D
from PySide2.QtQuick import QQuickItem
class MouseEvent(QObject):
"""
Simple MouseEvent object, since QQuickMouseEvent is not accessible in the public API
"""
def __init__(self, evt):
super(MouseEvent, self).__init__()
self._x = evt.x()
self._y = evt.y()
self._button = evt.button()
x = Property(float, lambda self: self._x, constant=True)
y = Property(float, lambda self: self._y, constant=True)
button = Property(Qt.MouseButton, lambda self: self._button, constant=True)
class EdgeMouseArea(QQuickItem):
"""
Provides a MouseArea shaped as a cubic spline for mouse interaction with edges.
Note: for performance reason, shape is updated only when geometry changes since this is the main use-case with edges.
TODOs:
- update when start/end points change too
- review this when using new QML Shape module
"""
def __init__(self, parent=None):
super(EdgeMouseArea, self).__init__(parent)
self._viewScale = 1.0
self._startX = 0.0
self._startY = 0.0
self._endX = 0.0
self._endY = 0.0
self._curveScale = 0.7
self._edgeThickness = 1.0
self._hullThickness = 2.0
self._containsMouse = False
self._path = None # type: QPainterPath
self.setAcceptHoverEvents(True)
self.setAcceptedMouseButtons(Qt.AllButtons)
def contains(self, point):
return self._path.contains(point)
def hoverEnterEvent(self, evt):
self.setContainsMouse(True)
super(EdgeMouseArea, self).hoverEnterEvent(evt)
def hoverLeaveEvent(self, evt):
self.setContainsMouse(False)
super(EdgeMouseArea, self).hoverLeaveEvent(evt)
def geometryChanged(self, newGeometry, oldGeometry):
super(EdgeMouseArea, self).geometryChanged(newGeometry, oldGeometry)
self.updateShape()
def mousePressEvent(self, evt):
if not self.acceptedMouseButtons() & evt.button():
evt.setAccepted(False)
return
e = MouseEvent(evt)
self.pressed.emit(e)
e.deleteLater()
def mouseReleaseEvent(self, evt):
e = MouseEvent(evt)
self.released.emit(e)
e.deleteLater()
def updateShape(self):
p1 = QPointF(self._startX, self._startY)
p2 = QPointF(self._endX, self._endY)
ctrlPt = QPointF(self.ctrlPtDist, 0)
path = QPainterPath(p1)
path.cubicTo(p1 + ctrlPt, p2 - ctrlPt, p2)
# Compute offset on x and y axis
hullOffset = self._edgeThickness * self._viewScale + self._hullThickness
v = QVector2D(p2 - p1).normalized()
offset = QPointF(hullOffset * -v.y(), hullOffset * v.x())
self._path = QPainterPath(path.toReversed())
self._path.translate(-offset)
path.translate(offset)
self._path.connectPath(path)
@property
def thickness(self):
return self._hullThickness
@thickness.setter
def thickness(self, value):
if self._hullThickness == value:
return
self._hullThickness = value
self.thicknessChanged.emit()
@property
def edgeThickness(self):
return self._edgeThickness
@edgeThickness.setter
def edgeThickness(self, value):
if self._edgeThickness == value:
return
self._edgeThickness = value
self.thicknessChanged.emit()
@property
def viewScale(self):
return self._viewScale
@viewScale.setter
def viewScale(self, value):
if self.viewScale == value:
return
self._viewScale = value
self.viewScaleChanged.emit()
@property
def startX(self):
return self._startX
@startX.setter
def startX(self, value):
self._startX = value
self.startXChanged.emit()
@property
def startY(self):
return self._startY
@startY.setter
def startY(self, value):
self._startY = value
self.startYChanged.emit()
@property
def endX(self):
return self._endX
@endX.setter
def endX(self, value):
self._endX = value
self.endXChanged.emit()
@property
def endY(self):
return self._endY
@endY.setter
def endY(self, value):
self._endY = value
self.endYChanged.emit()
@property
def curveScale(self):
return self._curveScale
@curveScale.setter
def curveScale(self, value):
self._curveScale = value
self.curveScaleChanged.emit()
self.updateShape()
@property
def ctrlPtDist(self):
return self.width() * self.curveScale * (-1 if self._startX > self._endX else 1)
@property
def containsMouse(self):
return self._containsMouse
def setContainsMouse(self, value):
if self._containsMouse == value:
return
self._containsMouse = value
self.containsMouseChanged.emit()
thicknessChanged = Signal()
thickness = Property(float, thickness.fget, thickness.fset, notify=thicknessChanged)
edgeThicknessChanged = Signal()
edgeThickness = Property(float, edgeThickness.fget, edgeThickness.fset, notify=edgeThicknessChanged)
viewScaleChanged = Signal()
viewScale = Property(float, viewScale.fget, viewScale.fset, notify=viewScaleChanged)
startXChanged = Signal()
startX = Property(float, startX.fget, startX.fset, notify=startXChanged)
startYChanged = Signal()
startY = Property(float, startY.fget, startY.fset, notify=startYChanged)
endXChanged = Signal()
endX = Property(float, endX.fget, endX.fset, notify=endXChanged)
endYChanged = Signal()
endY = Property(float, endY.fget, endY.fset, notify=endYChanged)
curveScaleChanged = Signal()
curveScale = Property(float, curveScale.fget, curveScale.fset, notify=curveScaleChanged)
ctrlPtDistChanged = Signal()
ctrlPtDist = Property(float, ctrlPtDist.fget, notify=ctrlPtDist)
containsMouseChanged = Signal()
containsMouse = Property(float, containsMouse.fget, notify=containsMouseChanged)
acceptedButtons = Property(int,
lambda self: super(EdgeMouseArea, self).acceptedMouseButtons,
lambda self, value: super(EdgeMouseArea, self).setAcceptedMouseButtons(value))
pressed = Signal(MouseEvent)
released = Signal(MouseEvent)

View file

@ -1,32 +1,68 @@
from PySide2 import QtCore from threading import Thread
from PySide2.QtCore import QObject, Slot, Property, Signal
from meshroom.core import graph from meshroom.core import graph
from meshroom.ui import commands from meshroom.ui import commands
class Reconstruction(QtCore.QObject): class Reconstruction(QObject):
def __init__(self, parent=None): def __init__(self, parent=None):
super(Reconstruction, self).__init__(parent) super(Reconstruction, self).__init__(parent)
self._graph = graph.Graph("") self._graph = graph.Graph("")
self._undoStack = commands.UndoStack(self) self._undoStack = commands.UndoStack(self)
self._computeThread = Thread()
@QtCore.Slot(str) @Slot(str)
def addNode(self, nodeType): def addNode(self, nodeType):
self._undoStack.tryAndPush(commands.AddNodeCommand(self._graph, nodeType)) self._undoStack.tryAndPush(commands.AddNodeCommand(self._graph, nodeType))
@QtCore.Slot(graph.Node) @Slot(graph.Node)
def removeNode(self, node): def removeNode(self, node):
self._undoStack.tryAndPush(commands.RemoveNodeCommand(self._graph, node)) self._undoStack.tryAndPush(commands.RemoveNodeCommand(self._graph, node))
@QtCore.Slot(graph.Attribute, "QVariant") @Slot(graph.Attribute, graph.Attribute)
def addEdge(self, src, dst):
self._undoStack.tryAndPush(commands.AddEdgeCommand(self._graph, src, dst))
@Slot(graph.Edge)
def removeEdge(self, edge):
self._undoStack.tryAndPush(commands.RemoveEdgeCommand(self._graph, edge))
@Slot(graph.Attribute, "QVariant")
def setAttribute(self, attribute, value): def setAttribute(self, attribute, value):
self._undoStack.tryAndPush(commands.SetAttributeCommand(self._graph, attribute, value)) self._undoStack.tryAndPush(commands.SetAttributeCommand(self._graph, attribute, value))
@QtCore.Slot(str) @Slot(str)
def load(self, filepath): def load(self, filepath):
self._graph.load(filepath) self._graph.load(filepath)
self._graph.update()
self._undoStack.clear()
undoStack = QtCore.Property(QtCore.QObject, lambda self: self._undoStack, constant=True) @Slot(graph.Node)
graph = QtCore.Property(QtCore.QObject, lambda self: self._graph, constant=True) def execute(self, node=None):
nodes = QtCore.Property(QtCore.QObject, lambda self: self._graph.nodes, constant=True) if self.computing:
return
nodes = [node] if node else self._graph.getLeaves()
self._computeThread = Thread(target=self._execute, args=(nodes,))
self._computeThread.start()
def _execute(self, nodes):
self.computingChanged.emit()
graph.execute(self._graph, nodes)
self.computingChanged.emit()
@Slot()
def stopExecution(self):
if not self.computing:
return
self._graph.stopExecution()
self._computeThread.join()
self.computingChanged.emit()
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
graph = Property(graph.Graph, lambda self: self._graph, constant=True)
nodes = Property(QObject, lambda self: self._graph.nodes, constant=True)
computingChanged = Signal()
computing = Property(bool, lambda self: self._computeThread.is_alive(), notify=computingChanged)

View file

@ -13,9 +13,9 @@ def test_depth():
(tB.output, tC.input) (tB.output, tC.input)
) )
assert tA.getDepth() == 1 assert tA.depth == 1
assert tB.getDepth() == 2 assert tB.depth == 2
assert tC.getDepth() == 3 assert tC.depth == 3
if __name__ == '__main__': if __name__ == '__main__':