Merge pull request #273 from gaphor/interaction-updates

Sequence diagram updates
This commit is contained in:
Arjan Molenaar 2020-03-09 23:13:36 +01:00 committed by GitHub
commit 3603850ef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 5687 additions and 3376 deletions

View File

@ -6,6 +6,9 @@ help: ## Show this help
dist: translate ## Build application distribution (requires Poetry)
poetry build
test: ## Run all but slow tests (requires PyTest)
pytest -m "not slow"
docs: ## Generate documentation (requirss Sphinx)
$(MAKE) -C docs html
@ -15,9 +18,9 @@ icons: ## Generate icons from stensil (requires Inkscape)
translate: ## Translate and update .po and .mo files (requires PyBabel)
$(MAKE) -C po
model: gaphor/UML/uml2.py ## Generate Python model files from Gaphor models (requires Black)
model: gaphor/UML/uml2.py ## Generate Python model files from Gaphor models (requires Black, MyPy)
gaphor/UML/uml2.py: gaphor/UML/uml2.gaphor utils/model/gen_uml.py utils/model/override.py utils/model/writer.py
utils/model/build_uml.py && black $@
gaphor/UML/uml2.py: gaphor/UML/uml2.gaphor gaphor/UML/uml2.override utils/model/gen_uml.py utils/model/override.py utils/model/writer.py
utils/model/build_uml.py && black $@ && mypy gaphor/UML
.PHONY: help dist docs icons translate model
.PHONY: help dist test docs icons translate model

View File

@ -0,0 +1,292 @@
<?xml version="1.0" encoding="utf-8"?>
<gaphor xmlns="http://gaphor.sourceforge.net/model" version="3.0" gaphor-version="1.2.0rc2">
<Package id="88ff97d6-5c0c-11ea-8042-9771210c7122">
<name>
<val>New model</val>
</name>
<ownedDiagram>
<reflist>
<ref refid="88ff97d7-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</ownedDiagram>
</Package>
<Diagram id="88ff97d7-5c0c-11ea-8042-9771210c7122">
<name>
<val>main</val>
</name>
<package>
<ref refid="88ff97d6-5c0c-11ea-8042-9771210c7122"/>
</package>
<canvas>
<item id="8afbf869-5c0c-11ea-8042-9771210c7122" type="LifelineItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 141.0, 205.5)</val>
</matrix>
<width>
<val>100.0</val>
</width>
<height>
<val>50.0</val>
</height>
<subject>
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
</subject>
<lifetime-length>
<val>172.0</val>
</lifetime-length>
<item id="8ed9440e-5c0c-11ea-8042-9771210c7122" type="ExecutionSpecificationItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 50.0, 75.0)</val>
</matrix>
<points>
<val>[(0.0, 0.0), (0.0, 100.5)]</val>
</points>
<head-connection>
<ref refid="8afbf869-5c0c-11ea-8042-9771210c7122"/>
</head-connection>
<subject>
<ref refid="8ed9440f-5c0c-11ea-8042-9771210c7122"/>
</subject>
</item>
</item>
<item id="91554be3-5c0c-11ea-8042-9771210c7122" type="LifelineItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 416.0, 205.5)</val>
</matrix>
<width>
<val>100.0</val>
</width>
<height>
<val>50.0</val>
</height>
<subject>
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
</subject>
<lifetime-length>
<val>171.5</val>
</lifetime-length>
<item id="9f6d722c-5c0c-11ea-8042-9771210c7122" type="ExecutionSpecificationItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 50.0, 92.0)</val>
</matrix>
<points>
<val>[(0.0, 0.0), (0.0, 74.5)]</val>
</points>
<head-connection>
<ref refid="91554be3-5c0c-11ea-8042-9771210c7122"/>
</head-connection>
<subject>
<ref refid="d4ed0b38-5c0c-11ea-8042-9771210c7122"/>
</subject>
</item>
</item>
<item id="a2af211a-5c0c-11ea-8042-9771210c7122" type="MessageItem">
<subject>
<ref refid="a2af211b-5c0c-11ea-8042-9771210c7122"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 197.0, 308.0)</val>
</matrix>
<orthogonal>
<val>0</val>
</orthogonal>
<horizontal>
<val>0</val>
</horizontal>
<points>
<val>[(0.0, 0.0), (263.0, 2.0)]</val>
</points>
<head-connection>
<ref refid="8ed9440e-5c0c-11ea-8042-9771210c7122"/>
</head-connection>
<tail-connection>
<ref refid="9f6d722c-5c0c-11ea-8042-9771210c7122"/>
</tail-connection>
</item>
<item id="a632aa2a-5c0c-11ea-8042-9771210c7122" type="MessageItem">
<subject>
<ref refid="a632aa2b-5c0c-11ea-8042-9771210c7122"/>
</subject>
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 460.0, 360.5)</val>
</matrix>
<orthogonal>
<val>0</val>
</orthogonal>
<horizontal>
<val>0</val>
</horizontal>
<points>
<val>[(0.0, 0.0), (-263.0, -2.5)]</val>
</points>
<head-connection>
<ref refid="9f6d722c-5c0c-11ea-8042-9771210c7122"/>
</head-connection>
<tail-connection>
<ref refid="8ed9440e-5c0c-11ea-8042-9771210c7122"/>
</tail-connection>
</item>
</canvas>
</Diagram>
<Lifeline id="8afbf868-5c0c-11ea-8042-9771210c7122">
<coveredBy>
<reflist>
<ref refid="8ed94410-5c0c-11ea-8042-9771210c7122"/>
<ref refid="8ed94411-5c0c-11ea-8042-9771210c7122"/>
<ref refid="d4ed0b3c-5c0c-11ea-8042-9771210c7122"/>
<ref refid="d4ed0b3d-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</coveredBy>
<name>
<val>Caller</val>
</name>
<presentation>
<reflist>
<ref refid="8afbf869-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</presentation>
</Lifeline>
<BehaviorExecutionSpecification id="8ed9440f-5c0c-11ea-8042-9771210c7122">
<executionOccurrenceSpecification>
<reflist>
<ref refid="8ed94410-5c0c-11ea-8042-9771210c7122"/>
<ref refid="8ed94411-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</executionOccurrenceSpecification>
<presentation>
<reflist>
<ref refid="8ed9440e-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</presentation>
</BehaviorExecutionSpecification>
<ExecutionOccurrenceSpecification id="8ed94410-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
</covered>
<execution>
<ref refid="8ed9440f-5c0c-11ea-8042-9771210c7122"/>
</execution>
</ExecutionOccurrenceSpecification>
<ExecutionOccurrenceSpecification id="8ed94411-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
</covered>
<execution>
<ref refid="8ed9440f-5c0c-11ea-8042-9771210c7122"/>
</execution>
</ExecutionOccurrenceSpecification>
<Lifeline id="91554be2-5c0c-11ea-8042-9771210c7122">
<coveredBy>
<reflist>
<ref refid="d4ed0b39-5c0c-11ea-8042-9771210c7122"/>
<ref refid="d4ed0b3a-5c0c-11ea-8042-9771210c7122"/>
<ref refid="d4ed0b3b-5c0c-11ea-8042-9771210c7122"/>
<ref refid="d4ed0b3e-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</coveredBy>
<name>
<val>Callee</val>
</name>
<presentation>
<reflist>
<ref refid="91554be3-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</presentation>
</Lifeline>
<Message id="a2af211b-5c0c-11ea-8042-9771210c7122">
<name>
<val>call()</val>
</name>
<presentation>
<reflist>
<ref refid="a2af211a-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</presentation>
<receiveEvent>
<ref refid="d4ed0b3e-5c0c-11ea-8042-9771210c7122"/>
</receiveEvent>
<sendEvent>
<ref refid="d4ed0b3d-5c0c-11ea-8042-9771210c7122"/>
</sendEvent>
</Message>
<Message id="a632aa2b-5c0c-11ea-8042-9771210c7122">
<messageSort>
<val>reply</val>
</messageSort>
<name>
<val></val>
</name>
<presentation>
<reflist>
<ref refid="a632aa2a-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</presentation>
<receiveEvent>
<ref refid="d4ed0b3c-5c0c-11ea-8042-9771210c7122"/>
</receiveEvent>
<sendEvent>
<ref refid="d4ed0b3b-5c0c-11ea-8042-9771210c7122"/>
</sendEvent>
</Message>
<BehaviorExecutionSpecification id="d4ed0b38-5c0c-11ea-8042-9771210c7122">
<executionOccurrenceSpecification>
<reflist>
<ref refid="d4ed0b39-5c0c-11ea-8042-9771210c7122"/>
<ref refid="d4ed0b3a-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</executionOccurrenceSpecification>
<presentation>
<reflist>
<ref refid="9f6d722c-5c0c-11ea-8042-9771210c7122"/>
</reflist>
</presentation>
</BehaviorExecutionSpecification>
<ExecutionOccurrenceSpecification id="d4ed0b39-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
</covered>
<execution>
<ref refid="d4ed0b38-5c0c-11ea-8042-9771210c7122"/>
</execution>
</ExecutionOccurrenceSpecification>
<ExecutionOccurrenceSpecification id="d4ed0b3a-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
</covered>
<execution>
<ref refid="d4ed0b38-5c0c-11ea-8042-9771210c7122"/>
</execution>
</ExecutionOccurrenceSpecification>
<MessageOccurrenceSpecification id="d4ed0b3b-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
</covered>
<sendMessage>
<ref refid="a632aa2b-5c0c-11ea-8042-9771210c7122"/>
</sendMessage>
</MessageOccurrenceSpecification>
<MessageOccurrenceSpecification id="d4ed0b3c-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
</covered>
<receiveMessage>
<ref refid="a632aa2b-5c0c-11ea-8042-9771210c7122"/>
</receiveMessage>
</MessageOccurrenceSpecification>
<MessageOccurrenceSpecification id="d4ed0b3d-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="8afbf868-5c0c-11ea-8042-9771210c7122"/>
</covered>
<sendMessage>
<ref refid="a2af211b-5c0c-11ea-8042-9771210c7122"/>
</sendMessage>
</MessageOccurrenceSpecification>
<MessageOccurrenceSpecification id="d4ed0b3e-5c0c-11ea-8042-9771210c7122">
<covered>
<ref refid="91554be2-5c0c-11ea-8042-9771210c7122"/>
</covered>
<receiveMessage>
<ref refid="a2af211b-5c0c-11ea-8042-9771210c7122"/>
</receiveMessage>
</MessageOccurrenceSpecification>
</gaphor>

View File

@ -169,7 +169,8 @@ class collection(Generic[T]):
self.object.handle(AssociationUpdated(self.object, self.property))
return True
except IndexError:
return False
except ValueError:
except (IndexError, ValueError):
return False
def order(self, key):
self.items.sort(key=key)

View File

@ -111,7 +111,7 @@ relation = Union[relation_one, relation_many]
T = TypeVar("T")
Lower = Union[Literal[0], Literal[1], Literal[2]]
Upper = Union[Literal[1], Literal["*"]]
Upper = Union[Literal[1], Literal[2], Literal["*"]]
class umlproperty:
@ -381,7 +381,9 @@ class association(umlproperty):
def _set_one(self, obj, value, from_opposite, do_notify) -> None:
if not (isinstance(value, self.type) or (value is None)):
raise AttributeError(f"Value should be of type {self.type.__name__}")
raise AttributeError(
f"Value should be of type {self.type.__name__}, got a {type(value)} instead"
)
old = self._get(obj)
@ -616,7 +618,7 @@ class derived(umlproperty, Generic[T]):
pass
def __str__(self):
return f"<derived {self.name}: {str(list(map(str, self.subsets)))[1:-1]}>"
return f"<derived {self.name}: {self.type} {str(list(map(str, self.subsets)))[1:-1]}>"
def _update(self, obj):
"""
@ -841,7 +843,7 @@ class redefine(umlproperty):
self.name = name
self._name = "_" + name
self.type = type
self.original = original
self.original: Union[association, derived] = original
self.upper = original.upper
self.lower = original.lower
@ -872,32 +874,20 @@ class redefine(umlproperty):
def __str__(self) -> str:
return f"<redefine {self.name}[{self.lower}..{self.upper}]: {self.type.__name__} = {str(self.original)}>"
def __get__(self, obj, class_=None):
# No longer needed
if not obj:
return self
return self.original.__get__(obj, class_)
def __set__(self, obj, value: T) -> None:
# No longer needed
if not isinstance(value, self.type):
raise AttributeError(f"Value should be of type {self.type.__name__}")
self.original.__set__(obj, value)
def __delete__(self, obj, value=None):
# No longer needed
self.original.__delete__(obj, value)
def _get(self, obj):
return self.original._get(obj)
def _set(self, obj, value, from_opposite=False):
def _set(self, obj, value, from_opposite=False, do_notify=True):
if not (isinstance(value, self.type) or (self.upper == 1 and value is None)):
raise AttributeError(
f"Value should be of type {self.type.__name__}, got a {type(value)} instead"
)
assert isinstance(self.original, association)
return self.original._set(obj, value, from_opposite)
return self.original._set(obj, value, from_opposite, do_notify)
def _del(self, obj, value, from_opposite=False):
def _del(self, obj, value, from_opposite=False, do_notify=True):
assert isinstance(self.original, association)
return self.original._del(obj, value, from_opposite)
return self.original._del(obj, value, from_opposite, do_notify)
def propagate(self, event):
if event.property is self.original and isinstance(

View File

@ -285,7 +285,7 @@ def test_realization(factory):
# Tests for interaction messages.
def test_create(factory):
def test_interaction_messages_cloning(factory):
"""Test message creation."""
m = factory.create(UML.Message)
send = factory.create(UML.MessageOccurrenceSpecification)

File diff suppressed because it is too large Load Diff

View File

@ -165,3 +165,11 @@ override StructuredClassifier.part: property
StructuredClassifier.part = property(lambda self: tuple(a for a in self.ownedAttribute if a.isComposite), doc="""
Properties owned by a classifier by composition.
""")
%%
override ExecutionSpecification.start(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
ExecutionSpecification.start = derived(ExecutionSpecification, 'start', OccurrenceSpecification, 0, 1,
lambda obj: [eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 0])
%%
override ExecutionSpecification.finish(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
ExecutionSpecification.finish = derived(ExecutionSpecification, 'finish', OccurrenceSpecification, 0, 1,
lambda obj: [eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 1])

View File

@ -529,6 +529,7 @@ class ValuePin(InputPin):
class Action(ExecutableNode):
effect: attribute[str]
interaction: relation_one[Interaction]
output: relation_many[OutputPin]
context_: relation_one[Classifier]
input: relation_many[InputPin]
@ -561,6 +562,7 @@ class ActivityGroup(Element):
class Constraint(PackageableElement):
constrainedElement: relation_many[Element]
specification: attribute[str]
stateInvariant: relation_one[StateInvariant]
owningState: relation_one[State]
context: derivedunion[Namespace]
@ -575,22 +577,17 @@ class Interaction(Behavior, InteractionFragment):
fragment: relation_many[InteractionFragment]
lifeline: relation_many[Lifeline]
message: relation_many[Message]
class ExecutionOccurence(InteractionFragment):
finish: relation_one[OccurrenceSpecification]
start: relation_one[OccurrenceSpecification]
behavior: relation_many[Behavior]
action: relation_many[Action]
class StateInvariant(InteractionFragment):
invariant: relation_one[Constraint]
covered: relation_one[Lifeline] # type: ignore[assignment]
class Lifeline(NamedElement):
coveredBy: relation_many[InteractionFragment]
interaction: relation_one[Interaction]
discriminator: attribute[str]
parse: Callable[[Lifeline, str], None]
render: Callable[[Lifeline], str]
@ -599,10 +596,10 @@ class Message(NamedElement):
messageKind: property
messageSort: enumeration
argument: attribute[str]
signature: relation_one[NamedElement]
sendEvent: relation_one[MessageEnd]
receiveEvent: relation_one[MessageEnd]
interaction: relation_one[Interaction]
signature: relation_one[NamedElement]
class MessageEnd(NamedElement):
@ -611,15 +608,11 @@ class MessageEnd(NamedElement):
class OccurrenceSpecification(InteractionFragment):
toAfter: relation_many[GeneralOrdering]
toBefore: relation_many[GeneralOrdering]
finishExec: relation_many[ExecutionOccurence]
startExec: relation_many[ExecutionOccurence]
covered: relation_one[Lifeline] # type: ignore[assignment]
class GeneralOrdering(NamedElement):
before: relation_one[OccurrenceSpecification]
after: relation_one[OccurrenceSpecification]
interactionFragment: relation_one[InteractionFragment]
class Connector(Feature):
@ -766,38 +759,6 @@ class Event(PackageableElement):
pass
class ExecutionEvent(Event):
pass
class CreationEvent(Event):
pass
class MessageEvent(Event):
pass
class DestructionEvent(Event):
pass
class SendOperationEvent(MessageEvent):
operation: relation_one[Operation]
class SendSignalEvent(MessageEvent):
signal: relation_one[Signal]
class ReceiveOperationEvent(MessageEvent):
operation: relation_one[Operation]
class ReceiveSignalEvent(MessageEvent):
signal: relation_one[Signal]
class Signal(Classifier):
ownedAttribute: relation_many[Property]
@ -806,6 +767,24 @@ class Reception(BehavioralFeature):
signal: relation_one[Signal]
class ExecutionSpecification(InteractionFragment):
executionOccurrenceSpecification: relation_many[ExecutionOccurrenceSpecification]
start: relation_one[ExecutionOccurrenceSpecification]
finish: relation_one[ExecutionOccurrenceSpecification]
class ExecutionOccurrenceSpecification(OccurrenceSpecification):
execution: relation_one[ExecutionSpecification]
class ActionExecutionSpecification(ExecutionSpecification):
action: relation_one[Action]
class BehaviorExecutionSpecification(ExecutionSpecification):
behavior: relation_one[Behavior]
# class 'ValueSpecification' has been stereotyped as 'SimpleAttribute'
# class 'InstanceValue' has been stereotyped as 'SimpleAttribute' too
# class 'Expression' has been stereotyped as 'SimpleAttribute' too
@ -1185,8 +1164,11 @@ InteractionFragment.enclosingInteraction = association(
Interaction.fragment = association(
"fragment", InteractionFragment, opposite="enclosingInteraction"
)
Constraint.stateInvariant = association(
"stateInvariant", StateInvariant, upper=1, opposite="invariant"
)
StateInvariant.invariant = association(
"invariant", Constraint, lower=1, upper=1, composite=True
"invariant", Constraint, lower=1, upper=1, composite=True, opposite="stateInvariant"
)
Lifeline.coveredBy = association("coveredBy", InteractionFragment, opposite="covered")
InteractionFragment.covered = association(
@ -1198,11 +1180,8 @@ Lifeline.interaction = association(
Interaction.lifeline = association(
"lifeline", Lifeline, composite=True, opposite="interaction"
)
# 'Lifeline.discriminator' is a simple attribute
Lifeline.discriminator = attribute("discriminator", str)
# 'Message.argument' is a simple attribute
Message.argument = attribute("argument", str)
Message.signature = association("signature", NamedElement, upper=1)
MessageEnd.sendMessage = association(
"sendMessage", Message, upper=1, opposite="sendEvent"
)
@ -1221,34 +1200,6 @@ Message.interaction = association(
Interaction.message = association(
"message", Message, composite=True, opposite="interaction"
)
InteractionFragment.generalOrdering = association(
"generalOrdering", GeneralOrdering, composite=True
)
GeneralOrdering.before = association(
"before", OccurrenceSpecification, lower=1, upper=1, opposite="toAfter"
)
OccurrenceSpecification.toAfter = association(
"toAfter", GeneralOrdering, opposite="before"
)
GeneralOrdering.after = association(
"after", OccurrenceSpecification, lower=1, upper=1, opposite="toBefore"
)
OccurrenceSpecification.toBefore = association(
"toBefore", GeneralOrdering, opposite="after"
)
ExecutionOccurence.finish = association(
"finish", OccurrenceSpecification, lower=1, upper=1, opposite="finishExec"
)
OccurrenceSpecification.finishExec = association(
"finishExec", ExecutionOccurence, opposite="finish"
)
ExecutionOccurence.start = association(
"start", OccurrenceSpecification, lower=1, upper=1, opposite="startExec"
)
OccurrenceSpecification.startExec = association(
"startExec", ExecutionOccurence, opposite="start"
)
ExecutionOccurence.behavior = association("behavior", Behavior)
StructuredClassifier.ownedConnector = association(
"ownedConnector", Connector, composite=True
)
@ -1343,10 +1294,33 @@ Signal.ownedAttribute = association("ownedAttribute", Property, composite=True)
Reception.signal = association("signal", Signal, upper=1)
Class.ownedReception = association("ownedReception", Reception, composite=True)
Interface.ownedReception = association("ownedReception", Reception, composite=True)
SendOperationEvent.operation = association("operation", Operation, lower=1, upper=1)
SendSignalEvent.signal = association("signal", Signal, lower=1, upper=1)
ReceiveOperationEvent.operation = association("operation", Operation, lower=1, upper=1)
ReceiveSignalEvent.signal = association("signal", Signal, lower=1, upper=1)
Action.interaction = association("interaction", Interaction, upper=1, opposite="action")
Interaction.action = association(
"action", Action, composite=True, opposite="interaction"
)
Message.signature = association("signature", NamedElement, upper=1)
InteractionFragment.generalOrdering = association(
"generalOrdering", GeneralOrdering, composite=True, opposite="interactionFragment"
)
GeneralOrdering.interactionFragment = association(
"interactionFragment", InteractionFragment, upper=1, opposite="generalOrdering"
)
ExecutionSpecification.executionOccurrenceSpecification = association(
"executionOccurrenceSpecification",
ExecutionOccurrenceSpecification,
upper=2,
composite=True,
opposite="execution",
)
ExecutionOccurrenceSpecification.execution = association(
"execution",
ExecutionSpecification,
lower=1,
upper=1,
opposite="executionOccurrenceSpecification",
)
ActionExecutionSpecification.action = association("action", Action, lower=1, upper=1)
BehaviorExecutionSpecification.behavior = association("behavior", Behavior, upper=1)
# 96: override NamedElement.qualifiedName(NamedElement.namespace): derived[List[str]]
# defined in uml2overrides.py
@ -1463,6 +1437,7 @@ NamedElement.namespace = derivedunion(
Parameter.ownerFormalParam,
Property.useCase,
Property.actor,
InteractionFragment.enclosingInteraction,
Lifeline.interaction,
Message.interaction,
Region.stateMachine,
@ -1503,7 +1478,7 @@ Namespace.ownedMember = derivedunion(
BehavioredClassifier.ownedBehavior,
UseCase.ownedAttribute,
Actor.ownedAttribute,
StateInvariant.invariant,
Interaction.fragment,
Interaction.lifeline,
Interaction.message,
StateMachine.region,
@ -1664,7 +1639,10 @@ Element.owner = derivedunion(
PackageImport.importingNamespace,
PackageMerge.mergingPackage,
NamedElement.namespace,
Constraint.stateInvariant,
Pseudostate.state,
Action.interaction,
GeneralOrdering.interactionFragment,
)
Element.ownedElement = derivedunion(
Element,
@ -1687,8 +1665,7 @@ Element.ownedElement = derivedunion(
Activity.edge,
Activity.node,
Action.output,
Interaction.fragment,
InteractionFragment.generalOrdering,
StateInvariant.invariant,
Connector.end,
State.entry,
State.exit,
@ -1697,6 +1674,8 @@ Element.ownedElement = derivedunion(
State.statevariant,
Transition.guard,
DeploymentTarget.deployment,
Interaction.action,
InteractionFragment.generalOrdering,
)
ConnectorEnd.definingEnd = derivedunion(ConnectorEnd, "definingEnd", Property, 0, 1)
# 164: override StructuredClassifier.part: property
@ -1707,6 +1686,30 @@ StructuredClassifier.part = property(
""",
)
# 169: override ExecutionSpecification.start(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
ExecutionSpecification.start = derived(
ExecutionSpecification,
"start",
OccurrenceSpecification,
0,
1,
lambda obj: [
eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 0
],
)
# 173: override ExecutionSpecification.finish(ExecutionSpecification.executionOccurrenceSpecification): relation_one[ExecutionOccurrenceSpecification]
ExecutionSpecification.finish = derived(
ExecutionSpecification,
"finish",
OccurrenceSpecification,
0,
1,
lambda obj: [
eos for i, eos in enumerate(obj.executionOccurrenceSpecification) if i == 1
],
)
# 128: override Class.superClass: derived[Classifier]
Class.superClass = Classifier.general
@ -1774,6 +1777,12 @@ Transition.redefinedTransition = redefine(
"*",
RedefinableElement.redefinedElement,
)
StateInvariant.covered = redefine(
StateInvariant, "covered", Lifeline, 1, InteractionFragment.covered
)
OccurrenceSpecification.covered = redefine(
OccurrenceSpecification, "covered", Lifeline, 1, InteractionFragment.covered
)
# 149: override Lifeline.parse: Callable[[Lifeline, str], None]
# defined in uml2overrides.py

View File

@ -209,7 +209,7 @@ def draw_decision_node(_box, context, _bounding_box):
@represents(UML.ForkNode)
class ForkNodeItem(UML.Presentation, Item):
class ForkNodeItem(UML.Presentation[UML.ForkNode], Item):
"""
Representation of fork and join node.
"""
@ -244,6 +244,9 @@ class ForkNodeItem(UML.Presentation, Item):
self.watch("subject.appliedStereotype.classifier.name")
self.watch("subject[JoinNode].joinSpec")
self.constraint(vertical=(h1.pos, h2.pos))
self.constraint(above=(h1.pos, h2.pos), delta=30)
def save(self, save_func):
save_func("matrix", tuple(self.matrix))
save_func("height", float(self._handles[1].pos.y))
@ -269,28 +272,6 @@ class ForkNodeItem(UML.Presentation, Item):
combined = reversible_property(lambda s: s._combined, _set_combined)
def setup_canvas(self):
assert self.canvas
super().setup_canvas()
h1, h2 = self._handles
cadd = self.canvas.solver.add_constraint
c1 = EqualsConstraint(a=h1.pos.x, b=h2.pos.x)
c2 = LessThanConstraint(smaller=h1.pos.y, bigger=h2.pos.y, delta=30)
self.__constraints = (cadd(c1), cadd(c2))
list(map(self.canvas.solver.add_constraint, self.__constraints))
def teardown_canvas(self):
assert self.canvas
super().teardown_canvas()
list(map(self.canvas.solver.remove_constraint, self.__constraints))
def pre_update(self, context):
cr = context.cairo
_, h2 = self.handles()
_, height = self.shape.size(cr)
h2.pos.y = max(h2.pos.y, height)
def draw(self, context):
h1, h2 = self.handles()
height = h2.pos.y - h1.pos.y

View File

@ -20,7 +20,7 @@ from gaphor.diagram.actions.activitynodes import (
)
from gaphor.diagram.actions.flow import FlowItem
from gaphor.diagram.actions.objectnode import ObjectNodeItem
from gaphor.diagram.connectors import IConnect, UnaryRelationshipConnect
from gaphor.diagram.connectors import Connector, UnaryRelationshipConnect
class FlowConnect(UnaryRelationshipConnect):
@ -77,7 +77,7 @@ class FlowConnect(UnaryRelationshipConnect):
opposite = line.opposite(handle)
otc = self.get_connected(opposite)
if opposite and isinstance(otc, (ForkNodeItem, DecisionNodeItem)):
adapter = IConnect(otc, line)
adapter = Connector(otc, line)
adapter.combine_nodes()
def disconnect_subject(self, handle):
@ -86,15 +86,15 @@ class FlowConnect(UnaryRelationshipConnect):
opposite = line.opposite(handle)
otc = self.get_connected(opposite)
if opposite and isinstance(otc, (ForkNodeItem, DecisionNodeItem)):
adapter = IConnect(otc, line)
adapter = Connector(otc, line)
adapter.decombine_nodes()
IConnect.register(ActionItem, FlowItem)(FlowConnect)
IConnect.register(ActivityNodeItem, FlowItem)(FlowConnect)
IConnect.register(ObjectNodeItem, FlowItem)(FlowConnect)
IConnect.register(SendSignalActionItem, FlowItem)(FlowConnect)
IConnect.register(AcceptEventActionItem, FlowItem)(FlowConnect)
Connector.register(ActionItem, FlowItem)(FlowConnect)
Connector.register(ActivityNodeItem, FlowItem)(FlowConnect)
Connector.register(ObjectNodeItem, FlowItem)(FlowConnect)
Connector.register(SendSignalActionItem, FlowItem)(FlowConnect)
Connector.register(AcceptEventActionItem, FlowItem)(FlowConnect)
class FlowForkDecisionNodeConnect(FlowConnect):
@ -210,7 +210,7 @@ class FlowForkDecisionNodeConnect(FlowConnect):
self.decombine_nodes()
@IConnect.register(ForkNodeItem, FlowItem)
@Connector.register(ForkNodeItem, FlowItem)
class FlowForkNodeConnect(FlowForkDecisionNodeConnect):
"""Connect Flow to a ForkNode."""
@ -218,7 +218,7 @@ class FlowForkNodeConnect(FlowForkDecisionNodeConnect):
join_node_cls = UML.JoinNode
@IConnect.register(DecisionNodeItem, FlowItem)
@Connector.register(DecisionNodeItem, FlowItem)
class FlowDecisionNodeConnect(FlowForkDecisionNodeConnect):
"""Connect Flow to a DecisionNode."""

View File

@ -6,14 +6,14 @@ from gaphor.diagram.classes.dependency import DependencyItem
from gaphor.diagram.classes.generalization import GeneralizationItem
from gaphor.diagram.classes.implementation import ImplementationItem
from gaphor.diagram.connectors import (
IConnect,
Connector,
RelationshipConnect,
UnaryRelationshipConnect,
)
from gaphor.diagram.presentation import Classified, ElementPresentation, Named
@IConnect.register(Named, DependencyItem)
@Connector.register(Named, DependencyItem)
class DependencyConnect(RelationshipConnect):
"""Connect two Named elements using a Dependency."""
@ -68,7 +68,7 @@ class DependencyConnect(RelationshipConnect):
line.subject = relation
@IConnect.register(Classified, GeneralizationItem)
@Connector.register(Classified, GeneralizationItem)
class GeneralizationConnect(RelationshipConnect):
"""Connect Classifiers with a Generalization relationship."""
@ -84,7 +84,7 @@ class GeneralizationConnect(RelationshipConnect):
self.line.subject = relation
@IConnect.register(Classified, AssociationItem)
@Connector.register(Classified, AssociationItem)
class AssociationConnect(UnaryRelationshipConnect):
"""Connect association to classifier."""
@ -171,7 +171,7 @@ class AssociationConnect(UnaryRelationshipConnect):
old.unlink()
@IConnect.register(Named, ImplementationItem)
@Connector.register(Named, ImplementationItem)
class ImplementationConnect(RelationshipConnect):
"""Connect Interface and a BehavioredClassifier using an Implementation."""

View File

@ -1,4 +1,5 @@
import logging
from inspect import isclass
from gaphas.decorators import AsyncIO
from gi.repository import Gtk
@ -93,16 +94,36 @@ class ClassOperations(EditableTreeModel):
return self._item.subject.ownedOperation.swap(o1, o2)
def _issubclass(c, b):
try:
return issubclass(c, b)
except TypeError:
return False
@PropertyPages.register(UML.Class)
class ClassPropertyPage(NamedElementPropertyPage):
"""Adapter which shows a property page for a class view."""
"""Adapter which shows a property page for a class view.
Also handles metaclasses.
"""
subject: UML.Class
CLASSES = list(
sorted(
c
for c in dir(UML)
if _issubclass(getattr(UML, c), UML.Element) and c != "Stereotype"
)
)
def __init__(self, subject):
super().__init__(subject)
def construct(self):
if UML.model.is_metaclass(self.subject):
return self.construct_metaclass()
page = super().construct()
if not self.subject:
@ -127,6 +148,45 @@ class ClassPropertyPage(NamedElementPropertyPage):
def _on_abstract_change(self, button):
self.subject.isAbstract = button.get_active()
def construct_metaclass(self):
page = Gtk.VBox()
subject = self.subject
if not subject:
return page
hbox = create_hbox_label(self, page, gettext("Name"))
model = Gtk.ListStore(str)
for c in self.CLASSES:
model.append([c])
cb = Gtk.ComboBox.new_with_model_and_entry(model)
completion = Gtk.EntryCompletion()
completion.set_model(model)
completion.set_minimum_key_length(1)
completion.set_text_column(0)
cb.get_child().set_completion(completion)
entry = cb.get_child()
entry.set_text(subject and subject.name or "")
hbox.pack_start(cb, True, True, 0)
page.default = entry
# monitor subject.name attribute
changed_id = entry.connect("changed", self._on_name_change)
def handler(event):
if event.element is subject and event.new_value is not None:
entry.handler_block(changed_id)
entry.set_text(event.new_value)
entry.handler_unblock(changed_id)
self.watcher.watch("name", handler).subscribe_all()
entry.connect("destroy", self.watcher.unsubscribe_all)
page.show_all()
return page
@PropertyPages.register(InterfaceItem)
class InterfacePropertyPage(NamedItemPropertyPage):

View File

@ -11,10 +11,10 @@ from gaphor.diagram.classes.classconnect import DependencyConnect, Implementatio
from gaphor.diagram.classes.dependency import DependencyItem
from gaphor.diagram.classes.implementation import ImplementationItem
from gaphor.diagram.classes.interface import Folded, InterfaceItem
from gaphor.diagram.connectors import IConnect
from gaphor.diagram.connectors import Connector
@IConnect.register(InterfaceItem, ImplementationItem)
@Connector.register(InterfaceItem, ImplementationItem)
class ImplementationInterfaceConnect(ImplementationConnect):
"""Connect interface item and a behaviored classifier using an
implementation.
@ -39,7 +39,7 @@ class ImplementationInterfaceConnect(ImplementationConnect):
self.line.request_update()
@IConnect.register(InterfaceItem, DependencyItem)
@Connector.register(InterfaceItem, DependencyItem)
class DependencyInterfaceConnect(DependencyConnect):
"""Connect interface item with dependency item."""

View File

@ -1,7 +1,7 @@
from gi.repository import Gtk
from gaphor import UML
from gaphor.diagram.profiles.metaclasspropertypage import MetaclassNamePropertyPage
from gaphor.diagram.classes.classespropertypages import ClassPropertyPage
from gaphor.tests import TestCase
@ -10,7 +10,7 @@ class MetaclassPropertyPageTest(TestCase):
class_ = self.element_factory.create(UML.Class)
class_.name = "Class"
editor = MetaclassNamePropertyPage(class_)
editor = ClassPropertyPage(class_)
page = editor.construct()
assert page
entry = page.get_children()[0].get_children()[1]
@ -28,7 +28,7 @@ class MetaclassPropertyPageTest(TestCase):
stereotype.name = "NewStereotype"
UML.model.create_extension(metaclass, stereotype)
editor = MetaclassNamePropertyPage(metaclass)
editor = ClassPropertyPage(metaclass)
page = editor.construct()
assert page
combo = page.get_children()[0].get_children()[1]

View File

@ -108,7 +108,7 @@ from gaphor.UML.modelfactory import stereotypes_str
@represents(UML.Connector)
class ConnectorItem(LinePresentation, Named):
class ConnectorItem(LinePresentation[UML.Connector], Named):
"""
Connector item line.

View File

@ -6,15 +6,22 @@ Implemented using interface item in assembly connector mode, see
"""
import operator
from typing import Union
from gaphor import UML
from gaphor.diagram.classes.interface import Folded, InterfaceItem
from gaphor.diagram.components.component import ComponentItem
from gaphor.diagram.components.connector import ConnectorItem
from gaphor.diagram.connectors import AbstractConnect, IConnect
from gaphor.diagram.connectors import BaseConnector, Connector
class ConnectorConnectBase(AbstractConnect):
@Connector.register(ComponentItem, ConnectorItem)
@Connector.register(InterfaceItem, ConnectorItem)
class ConnectorConnectBase(BaseConnector):
element: Union[ComponentItem, InterfaceItem]
line: ConnectorItem
def _get_interfaces(self, c1, c2):
"""
Return list of common interfaces provided by first component and
@ -184,17 +191,3 @@ class ConnectorConnectBase(AbstractConnect):
c = self.get_component(line)
self.drop_uml(line, c)
iface.request_update()
@IConnect.register(ComponentItem, ConnectorItem)
class ComponentConnectorConnect(ConnectorConnectBase):
"""Connection of connector item to a component."""
@IConnect.register(InterfaceItem, ConnectorItem)
class InterfaceConnectorConnect(ConnectorConnectBase):
"""Connect connector to an interface to maintain assembly connection.
See also `AbstractConnect` class for exception of interface item
connections.
"""

View File

@ -9,9 +9,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional, Type, TypeVar, Union
from gaphas.canvas import Connection
from gaphas.canvas import Canvas, Connection
from gaphas.connector import Handle, Port
from generic.multidispatch import FunctionDispatcher, multidispatch
from typing_extensions import Protocol
from gaphor import UML
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
@ -20,52 +21,21 @@ from gaphor.UML.properties import association, redefine, relation
T = TypeVar("T", bound=UML.Element)
class ConnectBase:
"""
This interface is used by the HandleTool to allow connecting
lines to element items. For each specific case (Element, Line) an
adapter could be written.
"""
def __init__(self, item: ElementPresentation, line_item: LinePresentation):
self.item = item
self.line_item = line_item
class ConnectorProtocol(Protocol):
def __init__(self, element: object, line: object,) -> None:
...
def allow(self, handle: Handle, port: Port) -> bool:
"""
Determine if a connection is allowed.
Do some extra checks to see if the items actually can be connected.
"""
return False
...
def connect(self, handle: Handle, port: Port) -> bool:
"""
Connect a line's handle to element.
Note that at the moment of the connect, handle.connected_to may point
to some other item. The implementor should do the disconnect of
the other element themselves.
"""
raise NotImplementedError(f"No connector for {self.item} and {self.line_item}")
...
def disconnect(self, handle: Handle) -> None:
"""
The true disconnect. Disconnect a handle.connected_to from an
element. This requires that the relationship is also removed at
model level.
"""
raise NotImplementedError(f"No connector for {self.item} and {self.line_item}")
...
# Work around issue https://github.com/python/mypy/issues/3135 (Class decorators are not type checked)
# This definition, along with the the ignore below, seems to fix the behaviour for mypy at least.
IConnect: FunctionDispatcher[Type[ConnectBase]] = multidispatch(object, object)(
ConnectBase
)
class AbstractConnect(ConnectBase):
class BaseConnector:
"""
Connection adapter for Gaphor diagram items.
@ -91,41 +61,29 @@ class AbstractConnect(ConnectBase):
def __init__(
self,
element: ElementPresentation[UML.Element],
line: LinePresentation[UML.Element],
element: UML.Presentation[UML.Element],
line: UML.Presentation[UML.Element],
) -> None:
assert element.canvas == line.canvas
assert element.canvas is line.canvas
self.element = element
self.line = line
self.canvas = element.canvas
self.canvas: Canvas = element.canvas
def get_connection(self, handle: Handle) -> Optional[Connection]:
"""
Get connection information
"""
assert self.canvas
return self.canvas.get_connection(handle)
def get_connected(self, handle: Handle) -> Optional[UML.Presentation]:
def get_connected(self, handle: Handle) -> Optional[UML.Presentation[UML.Element]]:
"""
Get item connected to a handle.
"""
assert self.canvas
cinfo = self.canvas.get_connection(handle)
if cinfo:
return cinfo.connected # type: ignore[no-any-return] # noqa: F723
return None
def get_connected_port(self, handle: Handle) -> Optional[Port]:
"""
Get port of item connected to connecting item via specified handle.
"""
assert self.canvas
cinfo = self.canvas.get_connection(handle)
if cinfo:
return cinfo.port
return None
def allow(self, handle: Handle, port: Port) -> bool:
"""
Determine if items can be connected.
@ -152,7 +110,28 @@ class AbstractConnect(ConnectBase):
"""Disconnect UML model level connections."""
class UnaryRelationshipConnect(AbstractConnect):
class NoConnector:
def __init__(self, element, line,) -> None:
pass
def allow(self, handle: Handle, port: Port) -> bool:
return False
def connect(self, handle: Handle, port: Port) -> bool:
return False
def disconnect(self, handle: Handle) -> None:
pass
# Work around issue https://github.com/python/mypy/issues/3135 (Class decorators are not type checked)
# This definition, along with the the ignore below, seems to fix the behaviour for mypy at least.
Connector: FunctionDispatcher[Type[ConnectorProtocol]] = multidispatch(object, object)(
NoConnector
)
class UnaryRelationshipConnect(BaseConnector):
"""
Base class for relationship connections, such as associations,
dependencies and implementations.
@ -164,6 +143,9 @@ class UnaryRelationshipConnect(AbstractConnect):
on the canvas.
"""
element: ElementPresentation[UML.Element]
line: LinePresentation[UML.Element]
def relationship(
self, required_type: Type[UML.Element], head: relation, tail: relation
) -> Optional[UML.Element]:
@ -270,8 +252,6 @@ class UnaryRelationshipConnect(AbstractConnect):
Cause items connected to ``line`` to reconnect, allowing them to
establish or destroy relationships at model level.
"""
assert self.canvas
line = self.line
canvas = self.canvas
solver = canvas.solver
@ -281,7 +261,7 @@ class UnaryRelationshipConnect(AbstractConnect):
for cinfo in connections or canvas.get_connections(connected=line):
if line is cinfo.connected:
continue
adapter = IConnect(line, cinfo.connected)
adapter = Connector(line, cinfo.connected)
assert adapter, "No element to connect {} and {}".format(
line, cinfo.connected
)
@ -295,8 +275,6 @@ class UnaryRelationshipConnect(AbstractConnect):
Returns a list of (item, handle) pairs that were connected (this
list can be used to connect items again with connect_connected_items()).
"""
assert self.canvas
line = self.line
canvas = self.canvas
solver = canvas.solver
@ -305,7 +283,7 @@ class UnaryRelationshipConnect(AbstractConnect):
solver.solve()
connections = list(canvas.get_connections(connected=line))
for cinfo in connections:
adapter = IConnect(cinfo.item, cinfo.connected)
adapter = Connector(cinfo.item, cinfo.connected)
adapter.disconnect(cinfo.handle)
return connections

View File

@ -372,6 +372,16 @@ TOOLBOX_ACTIONS: Sequence[Tuple[str, Sequence[ToolDef]]] = (
diagram.interactions.MessageItem
),
),
ToolDef(
"toolbox-execution-specification",
gettext("Execution Specification"),
"gaphor-execution-specification-symbolic",
None,
item_factory=PlacementTool.new_item_factory(
diagram.interactions.ExecutionSpecificationItem
),
handle_index=0,
),
ToolDef(
"toolbox-interaction",
gettext("Interaction"),

View File

@ -9,19 +9,21 @@ Although Gaphas has quite a few useful tools, some tools need to be extended:
import logging
from gaphas.aspect import Connector, InMotion, ItemConnector
from gaphas.aspect import Connector as ConnectorAspect
from gaphas.aspect import InMotion as InMotionAspect
from gaphas.aspect import ItemConnector
from gaphas.guide import GuidedItemInMotion
from gaphas.tool import ConnectHandleTool, HoverTool, ItemTool
from gaphas.tool import PlacementTool as _PlacementTool
from gaphas.tool import RubberbandTool, Tool, ToolChain
from gi.repository import Gdk, Gtk
from gi.repository import Gdk
from gaphor.core import Transaction, transactional
from gaphor.diagram.connectors import IConnect
from gaphor.diagram.connectors import Connector
from gaphor.diagram.event import DiagramItemPlaced
from gaphor.diagram.grouping import Group
from gaphor.diagram.inlineeditors import InlineEditor
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
from gaphor.diagram.presentation import ElementPresentation, Presentation
# cursor to indicate grouping
IN_CURSOR_TYPE = Gdk.CursorType.DIAMOND_CROSS
@ -32,10 +34,10 @@ OUT_CURSOR_TYPE = Gdk.CursorType.CROSSHAIR
log = logging.getLogger(__name__)
@Connector.register(LinePresentation)
@ConnectorAspect.register(Presentation)
class DiagramItemConnector(ItemConnector):
"""
Handle Tool (acts on item handles) that uses the IConnect protocol
Handle Tool (acts on item handles) that uses the Connector protocol
to connect items to one-another.
It also adds handles to lines when a line is grabbed on the middle of
@ -43,7 +45,7 @@ class DiagramItemConnector(ItemConnector):
"""
def allow(self, sink):
adapter = IConnect(sink.item, self.item)
adapter = Connector(sink.item, self.item)
return adapter and adapter.allow(self.handle, sink.port)
@transactional
@ -65,7 +67,7 @@ class DiagramItemConnector(ItemConnector):
elif cinfo:
# first disconnect but disable disconnection handle as
# reconnection is going to happen
adapter = IConnect(sink.item, item)
adapter = Connector(sink.item, item)
try:
connect = adapter.reconnect
except AttributeError:
@ -81,7 +83,7 @@ class DiagramItemConnector(ItemConnector):
connect(handle, sink.port)
else:
# new connection
adapter = IConnect(sink.item, item)
adapter = Connector(sink.item, item)
self.connect_handle(sink, callback=callback)
adapter.connect(handle, sink.port)
except Exception:
@ -124,7 +126,7 @@ class DisconnectHandle:
else:
log.debug(f"Disconnecting {item}.{handle}")
if cinfo:
adapter = IConnect(cinfo.connected, item)
adapter = Connector(cinfo.connected, item)
adapter.disconnect(handle)
@ -223,7 +225,7 @@ class PlacementTool(_PlacementTool):
# mechanisms
# First make sure all matrices are updated:
view.canvas.update_matrix(self.new_item)
view.canvas.update_matrices([self.new_item])
view.update_matrix(self.new_item)
vpos = event.x, event.y
@ -290,7 +292,7 @@ class PlacementTool(_PlacementTool):
return item
@InMotion.register(ElementPresentation)
@InMotionAspect.register(Presentation)
class DropZoneInMotion(GuidedItemInMotion):
def move(self, pos):
"""

View File

@ -3,7 +3,7 @@ CommentLine -- A line that connects a comment to another model element.
"""
from gaphor.diagram.connectors import IConnect
from gaphor.diagram.connectors import Connector
from gaphor.diagram.presentation import LinePresentation
@ -18,6 +18,6 @@ class CommentLineItem(LinePresentation):
c1 = canvas.get_connection(self.head)
c2 = canvas.get_connection(self.tail)
if c1 and c2:
adapter = IConnect(c1.connected, self)
adapter = Connector(c1.connected, self)
adapter.disconnect(self.head)
super().unlink()

View File

@ -3,9 +3,10 @@ Connect comments.
"""
import logging
from typing import Union
from gaphor import UML
from gaphor.diagram.connectors import AbstractConnect, IConnect
from gaphor.diagram.connectors import BaseConnector, Connector
from gaphor.diagram.general.comment import CommentItem
from gaphor.diagram.general.commentline import CommentLineItem
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
@ -13,11 +14,12 @@ from gaphor.diagram.presentation import ElementPresentation, LinePresentation
logger = logging.getLogger(__name__)
@IConnect.register(CommentItem, CommentLineItem)
@IConnect.register(ElementPresentation, CommentLineItem)
class CommentLineElementConnect(AbstractConnect):
@Connector.register(CommentItem, CommentLineItem)
@Connector.register(ElementPresentation, CommentLineItem)
class CommentLineElementConnect(BaseConnector):
"""Connect a comment line to any element item."""
element: Union[CommentItem, ElementPresentation]
line: CommentLineItem
def allow(self, handle, port):
@ -88,6 +90,7 @@ class CommentLineElementConnect(AbstractConnect):
if hct.subject and isinstance(oct.subject, UML.Comment):
del oct.subject.annotatedElement[hct.subject]
elif hct.subject and oct.subject:
assert isinstance(hct.subject, UML.Comment)
del hct.subject.annotatedElement[oct.subject]
except ValueError:
logger.debug(
@ -97,10 +100,13 @@ class CommentLineElementConnect(AbstractConnect):
super().disconnect(handle)
@IConnect.register(LinePresentation, CommentLineItem)
class CommentLineLineConnect(AbstractConnect):
@Connector.register(LinePresentation, CommentLineItem)
class CommentLineLineConnect(BaseConnector):
"""Connect a comment line to any diagram line."""
element: LinePresentation
line: CommentLineItem
def allow(self, handle, port):
"""
In addition to the normal check, both line ends may not be connected
@ -156,12 +162,15 @@ class CommentLineLineConnect(AbstractConnect):
and c2.subject in c1.subject.annotatedElement
):
del c1.subject.annotatedElement[c2.subject]
elif c2.subject and c1.subject in c2.subject.annotatedElement:
elif (
isinstance(c2.subject, UML.Comment)
and c1.subject in c2.subject.annotatedElement
):
del c2.subject.annotatedElement[c1.subject]
super().disconnect(handle)
@IConnect.register(CommentLineItem, LinePresentation)
@Connector.register(CommentLineItem, LinePresentation)
class InverseCommentLineLineConnect(CommentLineLineConnect):
"""
In case a line is disconnected that contains a comment-line,

View File

@ -1,3 +1,6 @@
from gaphor.diagram.interactions.executionspecification import (
ExecutionSpecificationItem,
)
from gaphor.diagram.interactions.interaction import InteractionItem
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.interactions.message import MessageItem
@ -5,7 +8,7 @@ from gaphor.diagram.interactions.message import MessageItem
def _load():
from gaphor.diagram.interactions import (
messageconnect,
interactionsconnect,
interactionsgrouping,
interactionspropertypages,
)

View File

@ -0,0 +1,122 @@
"""
An ExecutionSpecification is defined by a white recrange overlaying the lifeline
,----------.
| lifeline |
`----------'
| --- Lifeline
,+. -- ExecutionOccurrenceSpecification
| | -- ExecutionSpecification
`+' -- ExecutionOccurrentSpecification
|
ExecutionOccurrenceSpecification.covered <--> Lifeline.coveredBy
ExecutionOccurrenceSpecification.execution <--> ExecutionSpecification.execution
TODO:ExecutionSpecification is abstract. Should use either
ActionExecutionSpecification or BehaviorExecutionSpecification.
What's the difference?
Stick with BehaviorExecutionSpecification, since it has a [0..1] relation to
behavior, whereas ActionExecutionSpecification has a [1] relation to action.
"""
import ast
from gaphas import Handle, Item
from gaphas.connector import LinePort, Position
from gaphas.geometry import Rectangle, distance_rectangle_point
from gaphas.solver import WEAK
from gaphor import UML
from gaphor.diagram.presentation import postload_connect
from gaphor.diagram.shapes import Box, draw_border
from gaphor.diagram.support import represents
from gaphor.UML.modelfactory import stereotypes_str
@represents(UML.ExecutionSpecification)
class ExecutionSpecificationItem(UML.Presentation[UML.ExecutionSpecification], Item):
"""
Representation of interaction execution specification.
"""
def __init__(self, id=None, model=None):
super().__init__(id, model)
self.bar_width = 12
ht, hb = Handle(), Handle()
ht.connectable = True
# TODO: need better interface for this!
self._handles.append(ht)
self._handles.append(hb)
self.constraint(vertical=(ht.pos, hb.pos))
r = self.bar_width / 2
nw = Position((-r, 0), strength=WEAK)
ne = Position((r, 0), strength=WEAK)
se = Position((r, 0), strength=WEAK)
sw = Position((-r, 0), strength=WEAK)
self.constraint(horizontal=(sw, hb.pos))
self.constraint(horizontal=(se, hb.pos))
self._ports.append(LinePort(nw, sw))
self._ports.append(LinePort(ne, se))
self.shape = Box(style={"fill": "white"}, draw=draw_border)
@property
def top(self):
return self._handles[0]
@property
def bottom(self):
return self._handles[1]
def dimensions(self):
d = self.bar_width
pt, pb = (h.pos for h in self._handles)
return Rectangle(pt.x - d / 2, pt.y, d, y1=pb.y)
def draw(self, context):
self.shape.draw(context, self.dimensions())
def point(self, pos):
return distance_rectangle_point(self.dimensions(), pos)
def save(self, save_func):
def save_connection(name, handle):
assert self.canvas
c = self.canvas.get_connection(handle)
if c:
save_func(name, c.connected, reference=True)
points = [tuple(map(float, h.pos)) for h in self.handles()]
save_func("matrix", tuple(self.matrix))
save_func("points", points)
save_connection("head-connection", self.handles()[0])
super().save(save_func)
def load(self, name, value):
if name == "matrix":
self.matrix = ast.literal_eval(value)
elif name == "points":
points = ast.literal_eval(value)
for h, p in zip(self.handles(), points):
h.pos = p
elif name == "head-connection":
self._load_head_connection = value
else:
super().load(name, value)
def postload(self):
if hasattr(self, "_load_head_connection"):
postload_connect(self, self.handles()[0], self._load_head_connection)
del self._load_head_connection
super().postload()

View File

@ -0,0 +1,307 @@
"""Message item connection adapters."""
from typing import Optional
from gaphor import UML
from gaphor.diagram.connectors import BaseConnector, Connector
from gaphor.diagram.interactions.executionspecification import (
ExecutionSpecificationItem,
)
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.interactions.message import MessageItem
from gaphor.diagram.presentation import ElementPresentation
def reparent(canvas, item, new_parent):
old_parent = canvas.get_parent(item)
if old_parent:
canvas.reparent(item, None)
m = canvas.get_matrix_i2c(old_parent)
item.matrix *= m
old_parent.request_update()
if new_parent:
canvas.reparent(item, new_parent)
m = canvas.get_matrix_c2i(new_parent)
item.matrix *= m
new_parent.request_update()
def get_connected(item, handle) -> Optional[UML.Presentation[UML.Element]]:
"""
Get item connected to a handle.
"""
cinfo = item.canvas.get_connection(handle)
if cinfo:
return cinfo.connected # type: ignore[no-any-return] # noqa: F723
return None
def get_lifeline(item, handle):
connected_item = get_connected(item, handle)
if connected_item is None or isinstance(connected_item, LifelineItem):
return connected_item
return get_lifeline(connected_item, connected_item.handles()[0]) # type: ignore[attr-defined]
def order_lifeline_covered_by(lifeline):
canvas = lifeline.canvas
def y_and_occurence(connected):
for conn in canvas.get_connections(connected=connected):
m = canvas.get_matrix_i2c(conn.item)
if isinstance(conn.item, ExecutionSpecificationItem):
yield (
m.transform_point(*conn.handle.pos)[1],
conn.item.subject.start,
)
yield (
m.transform_point(*conn.item.bottom.pos)[1],
conn.item.subject.finish,
)
yield from y_and_occurence(conn.item)
elif isinstance(conn.item, MessageItem):
yield (
m.transform_point(*conn.handle.pos)[1],
conn.item.subject.sendEvent
if conn.handle is conn.item.head
else conn.item.subject.receiveEvent,
)
keys = {o: y for y, o in y_and_occurence(lifeline)}
lifeline.subject.coveredBy.order(keys.get)
def connect_lifelines(line, send, received):
"""
Always create a new Message with two EventOccurrence instances.
"""
def get_subject():
if not line.subject:
message = line.model.create(UML.Message)
message.name = "call()"
line.subject = message
return line.subject
if send:
message = get_subject()
if not message.sendEvent:
event = message.model.create(UML.MessageOccurrenceSpecification)
event.sendMessage = message
event.covered = send.subject
order_lifeline_covered_by(send)
if received:
message = get_subject()
if not message.receiveEvent:
event = message.model.create(UML.MessageOccurrenceSpecification)
event.receiveMessage = message
event.covered = received.subject
order_lifeline_covered_by(received)
def disconnect_lifelines(line, send, received):
"""
Disconnect lifeline and set appropriate kind of message item. If
there are no lifelines connected on both ends, then remove the message
from the data model.
"""
if not line.subject:
return
if send:
event = line.subject.receiveEvent
if event:
event.unlink()
if received:
event = line.subject.sendEvent
if event:
event.unlink()
# one is disconnected and one is about to be disconnected,
# so destroy the message
if not send or not received:
# Both ends are disconnected:
message = line.subject
del line.subject
if not message.presentation:
message.unlink()
@Connector.register(LifelineItem, MessageItem)
class MessageLifelineConnect(BaseConnector):
"""Connect lifeline with a message.
A message can connect to both the lifeline's head (the rectangle)
or the lifetime line. In case it's added to the head, the message
is considered to be part of a communication diagram. If the message is
added to a lifetime line, it's considered a sequence diagram.
"""
element: LifelineItem
line: MessageItem
def allow(self, handle, port):
"""
Glue to lifeline's head or lifetime. If lifeline's lifetime is
visible then disallow connection to lifeline's head.
"""
element = self.element
lifetime = element.lifetime
line = self.line
opposite = line.opposite(handle)
ol = self.get_connected(opposite)
if isinstance(ol, LifelineItem):
opposite_is_visible = ol.lifetime.visible
# connect lifetimes if both are visible or both invisible
return not (lifetime.visible ^ opposite_is_visible)
return not (lifetime.visible ^ (port is element.lifetime.port))
def connect(self, handle, port):
line = self.line
send = self.get_connected(line.head)
received = self.get_connected(line.tail)
connect_lifelines(line, send, received)
lifetime = self.element.lifetime
# if connected to head, then make lifetime invisible
if port is lifetime.port:
lifetime.min_length = lifetime.MIN_LENGTH_VISIBLE
else:
lifetime.visible = False
lifetime.connectable = False
return True
def disconnect(self, handle):
line = self.line
send: Optional[UML.Presentation[UML.Element]] = get_connected(line, line.head)
received = self.get_connected(line.tail)
lifeline = self.element
lifetime = lifeline.lifetime
# if a message is delete message, then disconnection causes
# lifeline to be no longer destroyed (note that there can be
# only one delete message connected to lifeline)
if received and line.subject.messageSort == "deleteMessage":
assert isinstance(received, LifelineItem)
received.is_destroyed = False
received.request_update()
disconnect_lifelines(line, send, received)
if len(list(self.canvas.get_connections(connected=lifeline))) == 1:
# after disconnection count of connected items will be
# zero, so allow connections to lifeline's lifetime
lifetime.connectable = True
lifetime.min_length = lifetime.MIN_LENGTH
@Connector.register(ExecutionSpecificationItem, MessageItem)
class ExecutionSpecificationMessageConnect(BaseConnector):
element: ExecutionSpecificationItem
line: MessageItem
def connect(self, handle, _port):
line = self.line
send = get_lifeline(line, line.head)
received = get_lifeline(line, line.tail)
connect_lifelines(line, send, received)
return True
def disconnect(self, handle):
line = self.line
send = get_lifeline(line, line.head)
received = get_lifeline(line, line.tail)
disconnect_lifelines(line, send, received)
@Connector.register(LifelineItem, ExecutionSpecificationItem)
class LifelineExecutionSpecificationConnect(BaseConnector):
element: LifelineItem
line: ExecutionSpecificationItem
def allow(self, handle, port):
lifetime = self.element.lifetime
return lifetime.visible
def connect(self, handle, port):
lifeline = self.element.subject
exec_spec: UML.ExecutionSpecification = self.line.subject
model = self.element.model
if not exec_spec:
exec_spec = model.create(UML.BehaviorExecutionSpecification)
self.line.subject = exec_spec
start_occurence: UML.ExecutionOccurrenceSpecification = model.create(
UML.ExecutionOccurrenceSpecification
)
start_occurence.covered = lifeline
start_occurence.execution = exec_spec
finish_occurence: UML.ExecutionOccurrenceSpecification = model.create(
UML.ExecutionOccurrenceSpecification
)
finish_occurence.covered = lifeline
finish_occurence.execution = exec_spec
canvas = self.canvas
if canvas.get_parent(self.line) is not self.element:
reparent(canvas, self.line, self.element)
for cinfo in canvas.get_connections(connected=self.line):
Connector(self.line, cinfo.item).connect(cinfo.handle, cinfo.port)
return True
def disconnect(self, handle):
exec_spec: Optional[UML.ExecutionSpecification] = self.line.subject
del self.line.subject
if exec_spec:
exec_spec.unlink()
canvas = self.canvas
if canvas.get_parent(self.line) is self.element:
new_parent = canvas.get_parent(self.element)
reparent(canvas, self.line, new_parent)
for cinfo in canvas.get_connections(connected=self.line):
Connector(self.line, cinfo.item).disconnect(cinfo.handle)
@Connector.register(ExecutionSpecificationItem, ExecutionSpecificationItem)
class ExecutionSpecificationExecutionSpecificationConnect(BaseConnector):
element: ExecutionSpecificationItem
line: ExecutionSpecificationItem
def connect(self, handle, _port):
parent_exec_spec = self.element.subject
if not parent_exec_spec:
# Can connect child exec spec if parent is not connected
return True
connected_item: Optional[UML.Presentation[UML.Element]]
connected_item = self.get_connected(self.element.handles()[0])
assert connected_item
Connector(connected_item, self.line).connect(handle, None)
reparent(self.canvas, self.line, self.element)
return True
def disconnect(self, handle):
exec_spec: Optional[UML.ExecutionSpecification] = self.line.subject
del self.line.subject
if exec_spec and not exec_spec.presentation:
exec_spec.unlink()
for cinfo in self.canvas.get_connections(connected=self.line):
Connector(self.line, cinfo.item).disconnect(cinfo.handle)

View File

@ -2,6 +2,7 @@ from gi.repository import Gtk
from gaphor import UML
from gaphor.core import gettext, transactional
from gaphor.diagram.interactions.interactionsconnect import get_lifeline
from gaphor.diagram.interactions.message import MessageItem
from gaphor.diagram.propertypages import (
EditableTreeModel,
@ -45,10 +46,7 @@ class MessagePropertyPage(NamedItemPropertyPage):
hbox = create_hbox_label(self, page, gettext("Message sort"))
sort_data = self.MESSAGE_SORT
lifeline = None
cinfo = item.canvas.get_connection(item.tail)
if cinfo:
lifeline = cinfo.connected
lifeline = get_lifeline(item, item.tail)
# disallow connecting two delete messages to a lifeline
if (
@ -79,10 +77,7 @@ class MessagePropertyPage(NamedItemPropertyPage):
item = self.item
subject = item.subject
lifeline = None
cinfo = item.canvas.get_connection(item.tail)
if cinfo:
lifeline = cinfo.connected
lifeline = get_lifeline(item, item.tail)
#
# allow only one delete message to connect to lifeline's lifetime

View File

@ -49,6 +49,7 @@ See also ``lifeline`` module documentation.
from math import atan2, pi
from gaphor import UML
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.presentation import LinePresentation, Named
from gaphor.diagram.shapes import Box, EditableText, Text
from gaphor.diagram.text import middle_segment
@ -256,8 +257,8 @@ class MessageItem(LinePresentation[UML.Message], Named):
c1 = canvas.get_connection(self.head)
c2 = canvas.get_connection(self.tail)
return (
c1
isinstance(c1, LifelineItem)
and not c1.connected.lifetime.visible
or c2
or isinstance(c2, LifelineItem)
and not c2.connected.lifetime.visible
)

View File

@ -1,136 +0,0 @@
"""Message item connection adapters."""
from typing import Optional
from gaphor import UML
from gaphor.diagram.connectors import AbstractConnect, IConnect
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.interactions.message import MessageItem
@IConnect.register(LifelineItem, MessageItem)
class MessageLifelineConnect(AbstractConnect):
"""Connect lifeline with a message.
A message can connect to both the lifeline's head (the rectangle)
or the lifetime line. In case it's added to the head, the message
is considered to be part of a communication diagram. If the message is
added to a lifetime line, it's considered a sequence diagram.
"""
line: MessageItem
def connect_lifelines(self, line, send, received):
"""
Always create a new Message with two EventOccurrence instances.
"""
def get_subject():
if not line.subject:
message = line.model.create(UML.Message)
message.name = "call()"
line.subject = message
return line.subject
if send:
message = get_subject()
if not message.sendEvent:
event = message.model.create(UML.MessageOccurrenceSpecification)
event.sendMessage = message
event.covered = send.subject
if received:
message = get_subject()
if not message.receiveEvent:
event = message.model.create(UML.MessageOccurrenceSpecification)
event.receiveMessage = message
event.covered = received.subject
def disconnect_lifelines(self, line):
"""
Disconnect lifeline and set appropriate kind of message item. If
there are no lifelines connected on both ends, then remove the message
from the data model.
"""
send = self.get_connected(line.head)
received = self.get_connected(line.tail)
if send:
event = line.subject.receiveEvent
if event:
event.unlink()
if received:
event = line.subject.sendEvent
if event:
event.unlink()
# one is disconnected and one is about to be disconnected,
# so destroy the message
if not send or not received:
# Both ends are disconnected:
message = line.subject
del line.subject
if not message.presentation:
message.unlink()
def allow(self, handle, port):
"""
Glue to lifeline's head or lifetime. If lifeline's lifetime is
visible then disallow connection to lifeline's head.
"""
element = self.element
lifetime = element.lifetime
line = self.line
opposite = line.opposite(handle)
ol = self.get_connected(opposite)
if ol:
assert isinstance(ol, LifelineItem)
opposite_is_visible = ol.lifetime.visible
# connect lifetimes if both are visible or both invisible
return not (lifetime.visible ^ opposite_is_visible)
return not (lifetime.visible ^ (port is element.lifetime.port))
def connect(self, handle, port):
super().connect(handle, port)
line = self.line
send = self.get_connected(line.head)
received = self.get_connected(line.tail)
self.connect_lifelines(line, send, received)
lifetime = self.element.lifetime
# if connected to head, then make lifetime invisible
if port is lifetime.port:
lifetime.min_length = lifetime.MIN_LENGTH_VISIBLE
else:
lifetime.visible = False
lifetime.connectable = False
def disconnect(self, handle):
assert self.canvas
super().disconnect(handle)
line = self.line
received = self.get_connected(line.tail)
lifeline = self.element
lifetime = lifeline.lifetime
# if a message is delete message, then disconnection causes
# lifeline to be no longer destroyed (note that there can be
# only one delete message connected to lifeline)
if received and line.subject.messageSort == "deleteMessage":
assert isinstance(received, LifelineItem)
received.is_destroyed = False
received.request_update()
self.disconnect_lifelines(line)
if len(list(self.canvas.get_connections(connected=lifeline))) == 1:
# after disconnection count of connected items will be
# zero, so allow connections to lifeline's lifetime
lifetime.connectable = True
lifetime.min_length = lifetime.MIN_LENGTH

View File

@ -0,0 +1,2 @@
import gaphor.diagram.diagramtools
from gaphor.diagram.tests.fixtures import diagram, element_factory, loader, saver

View File

@ -0,0 +1,236 @@
import pytest
from gaphas.canvas import Canvas, Context, instant_cairo_context
from gaphor import UML
from gaphor.diagram.interactions.executionspecification import (
ExecutionSpecificationItem,
)
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.tests.fixtures import allow, connect, disconnect
def test_draw_on_canvas():
canvas = Canvas()
exec_spec = ExecutionSpecificationItem()
canvas.add(exec_spec)
cr = instant_cairo_context()
exec_spec.draw(Context(cairo=cr))
def test_allow_execution_specification_to_lifeline(diagram):
lifeline = diagram.create(LifelineItem)
lifeline.lifetime.visible = True
exec_spec = diagram.create(ExecutionSpecificationItem)
glued = allow(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
assert glued
def test_connect_execution_specification_to_lifeline(diagram, element_factory):
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
exec_spec = diagram.create(ExecutionSpecificationItem)
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
assert exec_spec.subject
assert lifeline.subject
assert exec_spec.subject.start.covered is lifeline.subject
assert (
exec_spec.subject.executionOccurrenceSpecification[0].covered
is lifeline.subject
)
def test_disconnect_execution_specification_from_lifeline(diagram, element_factory):
def elements_of_kind(type):
return element_factory.lselect(lambda e: e.isKindOf(type))
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
exec_spec = diagram.create(ExecutionSpecificationItem)
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
disconnect(exec_spec, exec_spec.handles()[0])
assert lifeline.subject
assert exec_spec.subject is None
assert exec_spec.canvas
assert elements_of_kind(UML.ExecutionSpecification) == []
assert elements_of_kind(UML.ExecutionOccurrenceSpecification) == []
def test_allow_execution_specification_to_execution_specification(diagram):
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
child_exec_spec = diagram.create(ExecutionSpecificationItem)
glued = allow(
parent_exec_spec,
parent_exec_spec.handles()[0],
child_exec_spec,
child_exec_spec.ports()[0],
)
assert glued
def test_connect_execution_specification_to_execution_specification(
diagram, element_factory
):
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
child_exec_spec = diagram.create(ExecutionSpecificationItem)
connect(
child_exec_spec,
child_exec_spec.handles()[0],
parent_exec_spec,
parent_exec_spec.ports()[0],
)
assert not parent_exec_spec.subject
assert not child_exec_spec.subject
def test_connect_execution_specification_to_execution_specification_with_lifeline(
diagram, element_factory
):
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
child_exec_spec = diagram.create(ExecutionSpecificationItem)
connect(
parent_exec_spec,
parent_exec_spec.handles()[0],
lifeline,
lifeline.lifetime.port,
)
connect(
child_exec_spec,
child_exec_spec.handles()[0],
parent_exec_spec,
parent_exec_spec.ports()[0],
)
assert child_exec_spec.subject
assert lifeline.subject
assert child_exec_spec.subject.start.covered is lifeline.subject
assert (
child_exec_spec.subject.executionOccurrenceSpecification[0].covered
is lifeline.subject
)
def test_connect_execution_specification_with_execution_specification_to_lifeline(
diagram, element_factory
):
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
child_exec_spec = diagram.create(ExecutionSpecificationItem)
connect(
child_exec_spec,
child_exec_spec.handles()[0],
parent_exec_spec,
parent_exec_spec.ports()[0],
)
connect(
parent_exec_spec,
parent_exec_spec.handles()[0],
lifeline,
lifeline.lifetime.port,
)
assert parent_exec_spec.subject
assert child_exec_spec.subject
assert lifeline.subject
assert parent_exec_spec.subject.start.covered is lifeline.subject
assert child_exec_spec.subject.start.covered is lifeline.subject
assert (
child_exec_spec.subject.executionOccurrenceSpecification[0].covered
is lifeline.subject
)
def test_disconnect_execution_specification_with_execution_specification_from_lifeline(
diagram, element_factory
):
def elements_of_kind(type):
return element_factory.lselect(lambda e: e.isKindOf(type))
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
parent_exec_spec = diagram.create(ExecutionSpecificationItem)
child_exec_spec = diagram.create(ExecutionSpecificationItem)
grand_child_exec_spec = diagram.create(ExecutionSpecificationItem)
connect(
parent_exec_spec,
parent_exec_spec.handles()[0],
lifeline,
lifeline.lifetime.port,
)
connect(
child_exec_spec,
child_exec_spec.handles()[0],
parent_exec_spec,
parent_exec_spec.ports()[0],
)
connect(
grand_child_exec_spec,
grand_child_exec_spec.handles()[0],
child_exec_spec,
child_exec_spec.ports()[0],
)
disconnect(parent_exec_spec, parent_exec_spec.handles()[0])
assert lifeline.subject
assert parent_exec_spec.subject is None
assert child_exec_spec.subject is None
assert grand_child_exec_spec.subject is None
assert elements_of_kind(UML.ExecutionSpecification) == []
assert elements_of_kind(UML.ExecutionOccurrenceSpecification) == []
def test_save_and_load(diagram, element_factory, saver, loader):
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
exec_spec = diagram.create(ExecutionSpecificationItem)
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
diagram.canvas.update_now()
saved_data = saver()
loader(saved_data)
exec_specs = element_factory.lselect(
lambda e: e.isKindOf(UML.ExecutionSpecification)
)
loaded_exec_spec = exec_specs[0].presentation[0]
assert len(exec_specs) == 1
assert (
len(
element_factory.lselect(
lambda e: e.isKindOf(UML.ExecutionOccurrenceSpecification)
)
)
== 2
)
assert loaded_exec_spec.canvas.get_connection(loaded_exec_spec.handles()[0])

View File

@ -4,18 +4,14 @@ Test messages.
from gaphor import UML
from gaphor.diagram.interactions.message import MessageItem
from gaphor.tests.testcase import TestCase
class MessageTestCase(TestCase):
def test_message_persistence(self):
"""Test message saving/loading
"""
self.create(MessageItem, UML.Message)
def test_message_persistence(diagram, element_factory, saver, loader):
diagram.create(MessageItem, subject=element_factory.create(UML.Message))
data = self.save()
self.load(data)
data = saver()
loader(data)
new_diagram = next(element_factory.select(lambda e: isinstance(e, UML.Diagram)))
item = new_diagram.canvas.select(lambda e: isinstance(e, MessageItem))[0]
item = self.diagram.canvas.select(lambda e: isinstance(e, MessageItem))[0]
assert item
assert item

View File

@ -3,239 +3,299 @@ Message connection adapter tests.
"""
from gaphor import UML
from gaphor.diagram.interactions.executionspecification import (
ExecutionSpecificationItem,
)
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.interactions.message import MessageItem
from gaphor.tests import TestCase
from gaphor.diagram.tests.fixtures import allow, connect, disconnect
class BasicMessageConnectionsTestCase(TestCase):
def test_head_glue(self):
"""Test message head glue
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
def test_head_glue(diagram):
"""Test message head glue
"""
ll = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
# get head port
port = ll.ports()[0]
glued = self.allow(msg, msg.head, ll, port)
assert glued
def test_invisible_lifetime_glue(self):
"""Test message to invisible lifetime glue
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
glued = self.allow(msg, msg.head, ll, ll.lifetime.port)
assert not ll.lifetime.visible
assert not glued
def test_visible_lifetime_glue(self):
"""Test message to visible lifetime glue
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
ll.lifetime.visible = True
glued = self.allow(msg, msg.head, ll, ll.lifetime.port)
assert glued
def test_lost_message_connection(self):
"""Test lost message connection
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
self.connect(msg, msg.head, ll)
# If one side is connected a "lost" message is created
self.assertTrue(msg.subject is not None)
assert msg.subject.messageKind == "lost"
messages = self.kindof(UML.Message)
occurrences = self.kindof(UML.MessageOccurrenceSpecification)
assert 1 == len(messages)
assert 1 == len(occurrences)
assert messages[0] is msg.subject
assert occurrences[0] is msg.subject.sendEvent
def test_found_message_connection(self):
"""Test found message connection
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
self.connect(msg, msg.tail, ll)
# If one side is connected a "found" message is created
self.assertTrue(msg.subject is not None)
assert msg.subject.messageKind == "found"
messages = self.kindof(UML.Message)
occurrences = self.kindof(UML.MessageOccurrenceSpecification)
assert 1 == len(messages)
assert 1 == len(occurrences)
assert messages[0] is msg.subject
assert occurrences[0] is msg.subject.receiveEvent
def test_complete_message_connection(self):
"""Test complete message connection
"""
ll1 = self.create(LifelineItem)
ll2 = self.create(LifelineItem)
msg = self.create(MessageItem)
self.connect(msg, msg.head, ll1)
self.connect(msg, msg.tail, ll2)
# two sides are connected - "complete" message is created
self.assertTrue(msg.subject is not None)
assert msg.subject.messageKind == "complete"
messages = self.kindof(UML.Message)
occurences = self.kindof(UML.MessageOccurrenceSpecification)
assert 1 == len(messages)
assert 2 == len(occurences)
assert messages[0] is msg.subject
assert msg.subject.sendEvent in occurences, f"{occurences}"
assert msg.subject.receiveEvent in occurences, f"{occurences}"
def test_lifetime_connection(self):
"""Test messages' lifetimes connection
"""
msg = self.create(MessageItem)
ll1 = self.create(LifelineItem)
ll2 = self.create(LifelineItem)
# make lifelines to be in sequence diagram mode
ll1.lifetime.visible = True
ll2.lifetime.visible = True
assert ll1.lifetime.visible and ll2.lifetime.visible
# connect lifetimes with messages message to lifeline's head
self.connect(msg, msg.head, ll1, ll1.lifetime.port)
self.connect(msg, msg.tail, ll2, ll2.lifetime.port)
assert msg.subject is not None
assert msg.subject.messageKind == "complete"
def test_disconnection(self):
"""Test message disconnection
"""
ll1 = self.create(LifelineItem)
ll2 = self.create(LifelineItem)
msg = self.create(MessageItem)
self.connect(msg, msg.head, ll1)
self.connect(msg, msg.tail, ll2)
# one side disconnection
self.disconnect(msg, msg.head)
assert msg.subject is not None, f"{msg.subject}"
# 2nd side disconnection
self.disconnect(msg, msg.tail)
assert msg.subject is None, f"{msg.subject}"
def test_lifetime_connectivity_on_head(self):
"""Test lifeline's lifetime connectivity change on head connection
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
# connect message to lifeline's head, lifeline's lifetime
# visibility and connectivity should change
self.connect(msg, msg.head, ll)
assert not ll.lifetime.visible
assert not ll.lifetime.connectable
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
# ... and disconnection
self.disconnect(msg, msg.head)
assert ll.lifetime.connectable
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
def test_lifetime_connectivity_on_lifetime(self):
"""Test lifeline's lifetime connectivity change on lifetime connection
"""
ll = self.create(LifelineItem)
msg = self.create(MessageItem)
ll.lifetime.visible = True
# connect message to lifeline's lifetime, lifeline's lifetime
# visibility and connectivity should be unchanged
self.connect(msg, msg.head, ll, ll.lifetime.port)
assert ll.lifetime.connectable
assert ll.lifetime.MIN_LENGTH_VISIBLE == ll.lifetime.min_length
# ... and disconnection
self.disconnect(msg, msg.head)
assert ll.lifetime.connectable
assert ll.lifetime.visible
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
# get head port
port = ll.ports()[0]
glued = allow(msg, msg.head, ll, port)
assert glued
class DiagramModeMessageConnectionTestCase(TestCase):
def test_message_glue_cd(self):
"""Test gluing message on communication diagram."""
def test_invisible_lifetime_glue(diagram):
"""Test message to invisible lifetime glue
"""
ll = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
lifeline1 = self.create(LifelineItem)
lifeline2 = self.create(LifelineItem)
message = self.create(MessageItem)
glued = allow(msg, msg.head, ll, ll.lifetime.port)
# make second lifeline to be in sequence diagram mode
lifeline2.lifetime.visible = True
assert not ll.lifetime.visible
assert not glued
# connect head of message to lifeline's head
self.connect(message, message.head, lifeline1)
glued = self.allow(message, message.tail, lifeline2, lifeline2.lifetime.port)
# no connection possible as 2nd lifeline is in sequence diagram
# mode
self.assertFalse(glued)
def test_visible_lifetime_glue(diagram):
"""Test message to visible lifetime glue
"""
ll = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
def test_message_glue_sd(self):
"""Test gluing message on sequence diagram."""
ll.lifetime.visible = True
msg = self.create(MessageItem)
ll1 = self.create(LifelineItem)
ll2 = self.create(LifelineItem)
glued = allow(msg, msg.head, ll, ll.lifetime.port)
assert glued
# 1st lifeline - communication diagram
# 2nd lifeline - sequence diagram
ll2.lifetime.visible = True
# connect lifetime of message to lifeline's lifetime
self.connect(msg, msg.head, ll1, ll1.lifetime.port)
def test_lost_message_connection(diagram, element_factory):
"""Test lost message connection
"""
ll = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
glued = self.allow(msg, msg.tail, ll2)
# no connection possible as 2nd lifeline is in communication
# diagram mode
self.assertFalse(glued)
connect(msg, msg.head, ll)
def test_messages_disconnect_cd(self):
"""Test disconnecting messages on communication diagram
"""
ll1 = self.create(LifelineItem)
ll2 = self.create(LifelineItem)
msg = self.create(MessageItem)
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
occurrences = element_factory.lselect(
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
)
self.connect(msg, msg.head, ll1)
self.connect(msg, msg.tail, ll2)
# If one side is connected a "lost" message is created
assert msg.subject is not None
assert msg.subject.messageKind == "lost"
subject = msg.subject
assert 1 == len(messages)
assert 1 == len(occurrences)
assert messages[0] is msg.subject
assert occurrences[0] is msg.subject.sendEvent
assert subject.sendEvent and subject.receiveEvent
messages = list(self.kindof(UML.Message))
occurrences = set(self.kindof(UML.MessageOccurrenceSpecification))
def test_found_message_connection(diagram, element_factory):
"""Test found message connection
"""
ll = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
# verify integrity of messages
self.assertEqual(1, len(messages))
assert 2 == len(occurrences)
connect(msg, msg.tail, ll)
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
occurrences = element_factory.lselect(
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
)
# If one side is connected a "found" message is created
assert msg.subject is not None
assert msg.subject.messageKind == "found"
assert 1 == len(messages)
assert 1 == len(occurrences)
assert messages[0] is msg.subject
assert occurrences[0] is msg.subject.receiveEvent
def test_complete_message_connection(diagram, element_factory):
"""Test complete message connection
"""
ll1 = diagram.create(LifelineItem)
ll2 = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
connect(msg, msg.head, ll1)
connect(msg, msg.tail, ll2)
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
occurrences = element_factory.lselect(
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
)
# two sides are connected - "complete" message is created
assert msg.subject is not None
assert msg.subject.messageKind == "complete"
assert 1 == len(messages)
assert 2 == len(occurrences)
assert messages[0] is msg.subject
assert msg.subject.sendEvent in occurrences, f"{occurrences}"
assert msg.subject.receiveEvent in occurrences, f"{occurrences}"
def test_lifetime_connection(diagram):
"""Test messages' lifetimes connection
"""
msg = diagram.create(MessageItem)
ll1 = diagram.create(LifelineItem)
ll2 = diagram.create(LifelineItem)
# make lifelines to be in sequence diagram mode
ll1.lifetime.visible = True
ll2.lifetime.visible = True
assert ll1.lifetime.visible and ll2.lifetime.visible
# connect lifetimes with messages message to lifeline's head
connect(msg, msg.head, ll1, ll1.lifetime.port)
connect(msg, msg.tail, ll2, ll2.lifetime.port)
assert msg.subject is not None
assert msg.subject.messageKind == "complete"
def test_disconnection(diagram):
"""Test message disconnection
"""
ll1 = diagram.create(LifelineItem)
ll2 = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
connect(msg, msg.head, ll1)
connect(msg, msg.tail, ll2)
# one side disconnection
disconnect(msg, msg.head)
assert msg.subject is not None, f"{msg.subject}"
# 2nd side disconnection
disconnect(msg, msg.tail)
assert msg.subject is None, f"{msg.subject}"
def test_lifetime_connectivity_on_head(diagram, element_factory):
"""Test lifeline's lifetime connectivity change on head connection
"""
ll = diagram.create(LifelineItem, subject=element_factory.create(UML.Lifeline))
msg = diagram.create(MessageItem)
# connect message to lifeline's head, lifeline's lifetime
# visibility and connectivity should change
connect(msg, msg.head, ll)
assert not ll.lifetime.visible
assert not ll.lifetime.connectable
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
# ... and disconnection
disconnect(msg, msg.head)
assert ll.lifetime.connectable
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
def test_lifetime_connectivity_on_lifetime(diagram, element_factory):
"""Test lifeline's lifetime connectivity change on lifetime connection
"""
ll = diagram.create(LifelineItem, subject=element_factory.create(UML.Lifeline))
msg = diagram.create(MessageItem)
ll.lifetime.visible = True
# connect message to lifeline's lifetime, lifeline's lifetime
# visibility and connectivity should be unchanged
connect(msg, msg.head, ll, ll.lifetime.port)
assert ll.lifetime.connectable
assert ll.lifetime.MIN_LENGTH_VISIBLE == ll.lifetime.min_length
# ... and disconnection
disconnect(msg, msg.head)
assert ll.lifetime.connectable
assert ll.lifetime.visible
assert ll.lifetime.MIN_LENGTH == ll.lifetime.min_length
def test_message_glue_cd(diagram):
"""Test gluing message on communication diagram."""
lifeline1 = diagram.create(LifelineItem)
lifeline2 = diagram.create(LifelineItem)
message = diagram.create(MessageItem)
# make second lifeline to be in sequence diagram mode
lifeline2.lifetime.visible = True
# connect head of message to lifeline's head
connect(message, message.head, lifeline1)
glued = allow(message, message.tail, lifeline2, lifeline2.lifetime.port)
# no connection possible as 2nd lifeline is in sequence diagram
# mode
assert not glued
def test_message_glue_sd(diagram):
"""Test gluing message on sequence diagram."""
msg = diagram.create(MessageItem)
ll1 = diagram.create(LifelineItem)
ll2 = diagram.create(LifelineItem)
# 1st lifeline - communication diagram
# 2nd lifeline - sequence diagram
ll2.lifetime.visible = True
# connect lifetime of message to lifeline's lifetime
connect(msg, msg.head, ll1, ll1.lifetime.port)
glued = allow(msg, msg.tail, ll2)
# no connection possible as 2nd lifeline is in communication
# diagram mode
assert not glued
def test_messages_disconnect_cd(diagram, element_factory):
"""Test disconnecting messages on communication diagram
"""
ll1 = diagram.create(LifelineItem)
ll2 = diagram.create(LifelineItem)
msg = diagram.create(MessageItem)
connect(msg, msg.head, ll1)
connect(msg, msg.tail, ll2)
subject = msg.subject
assert subject.sendEvent
assert subject.receiveEvent
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
occurrences = element_factory.lselect(
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
)
# verify integrity of messages
assert 1 == len(messages)
assert 2 == len(occurrences)
def test_message_connect_to_execution_specification(diagram, element_factory):
"""Test gluing message on sequence diagram."""
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
exec_spec = diagram.create(ExecutionSpecificationItem)
message = diagram.create(MessageItem)
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
connect(message, message.head, exec_spec, exec_spec.ports()[0])
assert message.subject
assert message.subject.sendEvent.covered is lifeline.subject
def test_message_disconnect_from_execution_specification(diagram, element_factory):
"""Test gluing message on sequence diagram."""
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
exec_spec = diagram.create(ExecutionSpecificationItem)
message = diagram.create(MessageItem)
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
connect(message, message.head, exec_spec, exec_spec.ports()[0])
disconnect(message, message.head)
messages = element_factory.lselect(lambda e: e.isKindOf(UML.Message))
occurrences = element_factory.lselect(
lambda e: e.isKindOf(UML.MessageOccurrenceSpecification)
)
assert not message.subject
assert not len(messages)
assert not len(occurrences)

View File

@ -0,0 +1,46 @@
from gaphor import UML
from gaphor.diagram.interactions.executionspecification import (
ExecutionSpecificationItem,
)
from gaphor.diagram.interactions.interactionsconnect import order_lifeline_covered_by
from gaphor.diagram.interactions.lifeline import LifelineItem
from gaphor.diagram.interactions.message import MessageItem
from gaphor.diagram.tests.fixtures import connect
def test_ordering(diagram, element_factory):
lifeline = diagram.create(
LifelineItem, subject=element_factory.create(UML.Lifeline)
)
lifeline.lifetime.visible = True
lifeline.lifetime.bottom.pos.y = 1000
lifetime_top = lifeline.lifetime.top.pos
exec_spec = diagram.create(ExecutionSpecificationItem)
message1 = diagram.create(MessageItem)
message2 = diagram.create(MessageItem)
message3 = diagram.create(MessageItem)
message1.head.pos.y = lifetime_top.y + 300
exec_spec.top.pos.y = lifetime_top.y + 400
message2.head.pos.y = lifetime_top.y + 500
exec_spec.bottom.pos.y = lifetime_top.y + 600
message3.tail.pos.y = lifetime_top.y + 700
# Add in "random" order
connect(exec_spec, exec_spec.handles()[0], lifeline, lifeline.lifetime.port)
connect(message3, message3.tail, lifeline, lifeline.lifetime.port)
connect(message1, message1.head, lifeline, lifeline.lifetime.port)
connect(message2, message2.head, exec_spec, exec_spec.ports()[1])
occurrences = [
message1.subject.sendEvent,
exec_spec.subject.start,
message2.subject.sendEvent,
exec_spec.subject.finish,
message3.subject.receiveEvent,
]
order_lifeline_covered_by(lifeline)
assert list(lifeline.subject.coveredBy) == occurrences

View File

@ -1,7 +1,11 @@
from __future__ import annotations
import ast
from typing import Optional
import gaphas
from gaphas.aspect import ConnectionSink
from gaphas.aspect import Connector as ConnectorAspect
from gaphas.geometry import Rectangle, distance_rectangle_point
from gaphor.diagram.text import TextAlign, text_point_at_line
@ -43,6 +47,32 @@ def from_package_str(item):
)
def _get_sink(item, handle, target):
assert item.canvas
hpos = item.canvas.get_matrix_i2i(item, target).transform_point(*handle.pos)
port = None
dist = 10e6
for p in target.ports():
pos, d = p.glue(hpos)
if not port or d < dist:
port = p
dist = d
return ConnectionSink(target, port)
def postload_connect(item: gaphas.Item, handle: gaphas.Handle, target: gaphas.Item):
"""
Helper function: when loading a model, handles should be connected as
part of the `postload` step. This function finds a suitable spot on the
`target` item to connect the handle to.
"""
connector = ConnectorAspect(item, handle)
sink = _get_sink(item, handle, target)
connector.connect(sink)
# Note: the official documentation is using the terms "Shape" and "Edge" for element and line.
@ -247,25 +277,6 @@ class LinePresentation(Presentation[S], gaphas.Line):
def postload(self):
assert self.canvas
def get_sink(handle, item):
assert self.canvas
hpos = self.canvas.get_matrix_i2i(self, item).transform_point(*handle.pos)
port = None
dist = 10e6
for p in item.ports():
pos, d = p.glue(hpos)
if not port or d < dist:
port = p
dist = d
return gaphas.aspect.ConnectionSink(item, port)
def postload_connect(handle, item):
connector = gaphas.aspect.Connector(self, handle)
sink = get_sink(handle, item)
connector.connect(sink)
if hasattr(self, "_load_orthogonal"):
# Ensure there are enough handles
if self._load_orthogonal and len(self._handles) < 3:
@ -274,18 +285,12 @@ class LinePresentation(Presentation[S], gaphas.Line):
self.orthogonal = self._load_orthogonal
del self._load_orthogonal
# First update matrix and solve constraints (NE and SW handle are
# lazy and are resolved by the constraint solver rather than set
# directly.
self.canvas.update_matrix(self)
self.canvas.solver.solve()
if hasattr(self, "_load_head_connection"):
postload_connect(self.head, self._load_head_connection)
postload_connect(self, self.head, self._load_head_connection)
del self._load_head_connection
if hasattr(self, "_load_tail_connection"):
postload_connect(self.tail, self._load_tail_connection)
postload_connect(self, self.tail, self._load_tail_connection)
del self._load_tail_connection
super().postload()

View File

@ -4,7 +4,6 @@ from gaphor.diagram.profiles.extension import ExtensionItem
def _load():
from gaphor.diagram.profiles import (
extensionconnect,
metaclasspropertypage,
stereotypepage,
)

View File

@ -1,10 +1,12 @@
from typing import Optional
from gaphor import UML
from gaphor.diagram.connectors import IConnect, RelationshipConnect
from gaphor.diagram.connectors import Connector, RelationshipConnect
from gaphor.diagram.presentation import Classified
from gaphor.diagram.profiles.extension import ExtensionItem
@IConnect.register(Classified, ExtensionItem)
@Connector.register(Classified, ExtensionItem)
class ExtensionConnect(RelationshipConnect):
"""Connect class and stereotype items using an extension item."""
@ -31,8 +33,11 @@ class ExtensionConnect(RelationshipConnect):
c1 = self.get_connected(line.head)
c2 = self.get_connected(line.tail)
if c1 and c2:
head_type = c1.subject
tail_type = c2.subject
assert isinstance(c1.subject, UML.Class)
assert isinstance(c2.subject, UML.Stereotype)
head_type: UML.Class = c1.subject
tail_type: UML.Stereotype = c2.subject
# First check if we do not already contain the right subject:
if line.subject:

View File

@ -1,82 +0,0 @@
"""
Metaclass item editors.
"""
from gi.repository import Gtk
from gaphor import UML
from gaphor.core import gettext
from gaphor.diagram.propertypages import (
NamedElementPropertyPage,
PropertyPages,
create_hbox_label,
)
def _issubclass(c, b):
try:
return issubclass(c, b)
except TypeError:
return False
@PropertyPages.register(UML.Class)
class MetaclassNamePropertyPage(NamedElementPropertyPage):
"""
Metaclass name editor. Provides editable combo box entry with
predefined list of names of UML classes.
"""
order = 10
NAME_LABEL = gettext("Name")
CLASSES = list(
sorted(
n
for n in dir(UML)
if _issubclass(getattr(UML, n), UML.Element) and n != "Stereotype"
)
)
def construct(self):
if not UML.model.is_metaclass(self.subject):
return super().construct()
page = Gtk.VBox()
subject = self.subject
if not subject:
return page
hbox = create_hbox_label(self, page, self.NAME_LABEL)
model = Gtk.ListStore(str)
for c in self.CLASSES:
model.append([c])
cb = Gtk.ComboBox.new_with_model_and_entry(model)
completion = Gtk.EntryCompletion()
completion.set_model(model)
completion.set_minimum_key_length(1)
completion.set_text_column(0)
cb.get_child().set_completion(completion)
entry = cb.get_child()
entry.set_text(subject and subject.name or "")
hbox.pack_start(cb, True, True, 0)
page.default = entry
# monitor subject.name attribute
changed_id = entry.connect("changed", self._on_name_change)
def handler(event):
if event.element is subject and event.new_value is not None:
entry.handler_block(changed_id)
entry.set_text(event.new_value)
entry.handler_unblock(changed_id)
self.watcher.watch("name", handler).subscribe_all()
entry.connect("destroy", self.watcher.unsubscribe_all)
page.show_all()
return page

View File

@ -26,11 +26,13 @@ Style = TypedDict(
"line-width": float,
"vertical-spacing": float,
"border-radius": float,
"fill": str,
"font": str,
"font-style": FontStyle,
"font-weight": Optional[FontWeight],
"text-decoration": Optional[TextDecoration],
"text-align": TextAlign,
"stoke": str,
"vertical-align": VerticalAlign,
# CommentItem:
"ear": int,
@ -66,6 +68,13 @@ def draw_border(box, context, bounding_box):
cr.rectangle(x, y, width, height)
cr.close_path()
fill = box.style("fill")
if fill:
color = cr.get_source()
cr.set_source_rgb(1, 1, 1) # white
cr.fill_preserve()
cr.set_source(color)
cr.stroke()
@ -98,7 +107,8 @@ class Box:
- min-height
- min-width
- padding: a tuple (top, right, bottom, left)
- vertical-align: alignment of child shapes
- border-radius
"""
def __init__(self, *children, style: Style = {}, draw=None):
@ -110,6 +120,7 @@ class Box:
"padding": (0, 0, 0, 0),
"vertical-align": VerticalAlign.MIDDLE,
"border-radius": 0,
"fill": None,
**style, # type: ignore[misc] # noqa: F821
}
self._draw_border = draw

View File

@ -7,7 +7,7 @@ gaphor.adapter package.
"""
from gaphor import UML
from gaphor.diagram.connectors import IConnect, RelationshipConnect
from gaphor.diagram.connectors import Connector, RelationshipConnect
from gaphor.diagram.states.pseudostates import (
HistoryPseudostateItem,
InitialPseudostateItem,
@ -35,7 +35,7 @@ class VertexConnect(RelationshipConnect):
relation.guard = self.line.model.create(UML.Constraint)
@IConnect.register(VertexItem, TransitionItem)
@Connector.register(VertexItem, TransitionItem)
class TransitionConnect(VertexConnect):
"""Connect two state vertices using transition item."""
@ -59,7 +59,7 @@ class TransitionConnect(VertexConnect):
return None
@IConnect.register(InitialPseudostateItem, TransitionItem)
@Connector.register(InitialPseudostateItem, TransitionItem)
class InitialPseudostateTransitionConnect(VertexConnect):
"""Connect initial pseudostate using transition item.
@ -72,8 +72,6 @@ class InitialPseudostateTransitionConnect(VertexConnect):
Glue to initial pseudostate with transition's head and when there are
no transitions connected.
"""
assert self.canvas
line = self.line
element = self.element
@ -90,7 +88,7 @@ class InitialPseudostateTransitionConnect(VertexConnect):
return None
@IConnect.register(HistoryPseudostateItem, TransitionItem)
@Connector.register(HistoryPseudostateItem, TransitionItem)
class HistoryPseudostateTransitionConnect(VertexConnect):
"""Connect history pseudostate using transition item.

View File

@ -0,0 +1 @@
from gaphor.diagram.tests.fixtures import diagram, element_factory, loader

View File

@ -0,0 +1,94 @@
from io import StringIO
import pytest
from gaphas.aspect import ConnectionSink
from gaphas.aspect import Connector as ConnectorAspect
from gaphor import UML
from gaphor.diagram.connectors import Connector
from gaphor.misc.xmlwriter import XMLWriter
from gaphor.services.eventmanager import EventManager
from gaphor.storage import storage
from gaphor.UML.elementfactory import ElementFactory
@pytest.fixture
def element_factory():
return ElementFactory(EventManager())
@pytest.fixture
def diagram(element_factory):
return element_factory.create(UML.Diagram)
@pytest.fixture
def saver(element_factory):
def save():
"""
Save diagram into string.
"""
f = StringIO()
storage.save(XMLWriter(f), element_factory)
data = f.getvalue()
f.close()
return data
return save
@pytest.fixture
def loader(element_factory):
def load(data):
"""
Load data from specified string.
"""
element_factory.flush()
assert not list(element_factory.select())
f = StringIO(data)
storage.load(f, factory=element_factory)
f.close()
return load
def allow(line, handle, item, port=None):
if port is None and len(item.ports()) > 0:
port = item.ports()[0]
adapter = Connector(item, line)
return adapter.allow(handle, port)
def connect(line, handle, item, port=None):
"""
Connect line's handle to an item.
If port is not provided, then first port is used.
"""
canvas = line.canvas
if port is None and len(item.ports()) > 0:
port = item.ports()[0]
sink = ConnectionSink(item, port)
connector = ConnectorAspect(line, handle)
connector.connect(sink)
cinfo = canvas.get_connection(handle)
assert cinfo.connected is item
assert cinfo.port is port
def disconnect(line, handle):
"""
Disconnect line's handle.
"""
canvas = line.canvas
canvas.disconnect_item(line, handle)
assert not canvas.get_connection(handle)

View File

@ -1,9 +1,5 @@
import pytest
from gaphor import UML
from gaphor.diagram.presentation import ElementPresentation, LinePresentation
from gaphor.services.eventmanager import EventManager
from gaphor.UML.elementfactory import ElementFactory
class DummyVisualComponent:
@ -24,16 +20,6 @@ class StubLine(LinePresentation):
super().__init__(id, model, shape_middle=DummyVisualComponent())
@pytest.fixture
def element_factory():
return ElementFactory(EventManager())
@pytest.fixture
def diagram(element_factory):
return element_factory.create(UML.Diagram)
def test_creation(element_factory):
p = element_factory.create(UML.Presentation)

View File

@ -3,13 +3,13 @@ Use cases related connection adapters.
"""
from gaphor import UML
from gaphor.diagram.connectors import IConnect, RelationshipConnect
from gaphor.diagram.connectors import Connector, RelationshipConnect
from gaphor.diagram.usecases.extend import ExtendItem
from gaphor.diagram.usecases.include import IncludeItem
from gaphor.diagram.usecases.usecase import UseCaseItem
@IConnect.register(UseCaseItem, IncludeItem)
@Connector.register(UseCaseItem, IncludeItem)
class IncludeConnect(RelationshipConnect):
"""Connect use cases with an include item relationship."""
@ -33,7 +33,7 @@ class IncludeConnect(RelationshipConnect):
self.line.subject = relation
@IConnect.register(UseCaseItem, ExtendItem)
@Connector.register(UseCaseItem, ExtendItem)
class ExtendConnect(RelationshipConnect):
"""Connect use cases with an extend item relationship."""

View File

@ -113,7 +113,7 @@ class CopyService(Service, ActionProvider):
# update items' matrix immediately
for item in new_items.values():
item.matrix.translate(10, 10)
canvas.update_matrix(item)
canvas.update_matrices([item])
# solve internal constraints of items immediately as item.postload
# reconnects items and all handles have to be in place

View File

@ -185,8 +185,9 @@ def load_elements_generator(elements, factory, gaphor_version):
yield from _load_attributes_and_references(elements, update_status_queue)
for d in factory.select(lambda e: isinstance(e, UML.Diagram)):
canvas = d.canvas
# update_now() is implicitly called when lock is released
d.canvas.block_updates = False
canvas.block_updates = False
# do a postload:
for id, elem in list(elements.items()):

View File

@ -0,0 +1,34 @@
from io import StringIO
import pytest
from gaphor import UML
from gaphor.services.eventmanager import EventManager
from gaphor.storage import storage
from gaphor.UML.elementfactory import ElementFactory
@pytest.fixture
def element_factory():
return ElementFactory(EventManager())
@pytest.fixture
def diagram(element_factory):
return element_factory.create(UML.Diagram)
@pytest.fixture
def loader(element_factory):
def load(data):
"""
Load data from specified string.
"""
element_factory.flush()
assert not list(element_factory.select())
f = StringIO(data)
storage.load(f, factory=element_factory)
f.close()
return load

View File

@ -0,0 +1,106 @@
import pytest
from gaphor.diagram.classes import DependencyItem
from gaphor.diagram.components import NodeItem
PARENT_X = 189
PARENT_Y = 207
CHILD_ONE_X = 32
CHILD_ONE_Y = 54
CHILD_TWO_X = 44
CHILD_TWO_Y = 208
def handle_pos(item, handle_index):
return tuple(map(float, item.handles()[handle_index].pos))
def test_load_grouped_connected_items(element_factory, loader):
loader(NODE_EXAMPLE_XML)
diagram = element_factory.lselect()[0]
canvas = diagram.canvas
node_item, dep_item = canvas.get_root_items()
child_one, child_two = canvas.get_children(node_item)
assert isinstance(node_item, NodeItem)
assert isinstance(dep_item, DependencyItem)
assert isinstance(child_one, NodeItem)
assert isinstance(child_two, NodeItem)
assert canvas.get_parent(child_one) is node_item
assert tuple(canvas.get_matrix_i2c(child_one)) == (
1.0,
0.0,
0.0,
1.0,
PARENT_X + CHILD_ONE_X,
PARENT_Y + CHILD_ONE_Y,
)
assert tuple(canvas.get_matrix_i2c(child_two)) == (
1.0,
0.0,
0.0,
1.0,
PARENT_X + CHILD_TWO_X,
PARENT_Y + CHILD_TWO_Y,
)
NODE_EXAMPLE_XML = f"""\
<?xml version="1.0" encoding="utf-8"?>
<gaphor xmlns="http://gaphor.sourceforge.net/model" version="3.0" gaphor-version="1.2.0rc2">
<Diagram id="3ea414eb-5eb5-11ea-9ccf-45e3771927d8">
<canvas>
<item id="41f681ab-5eb5-11ea-9ccf-45e3771927d8" type="NodeItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, {PARENT_X}, {PARENT_Y})</val>
</matrix>
<width>
<val>388.5</val>
</width>
<height>
<val>286.5</val>
</height>
<item id="4541c555-5eb5-11ea-9ccf-45e3771927d8" type="NodeItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, {CHILD_ONE_X}, {CHILD_ONE_Y})</val>
</matrix>
<width>
<val>100.0</val>
</width>
<height>
<val>50.0</val>
</height>
</item>
<item id="4f927913-5eb5-11ea-9ccf-45e3771927d8" type="NodeItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, {CHILD_TWO_X}, {CHILD_TWO_Y})</val>
</matrix>
<width>
<val>100.0</val>
</width>
<height>
<val>50.0</val>
</height>
</item>
</item>
<item id="5b4d81cb-5eb5-11ea-9ccf-45e3771927d8" type="DependencyItem">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 274.0, 312.0)</val>
</matrix>
<points>
<val>[(0.0, 0.0), (6.0, 104.0)]</val>
</points>
<head-connection>
<ref refid="4541c555-5eb5-11ea-9ccf-45e3771927d8"/>
</head-connection>
<tail-connection>
<ref refid="4f927913-5eb5-11ea-9ccf-45e3771927d8"/>
</tail-connection>
</item>
</canvas>
</Diagram>
</gaphor>
"""

View File

@ -5,6 +5,8 @@ Unittest the storage and parser modules
import re
from io import StringIO
import pytest
from gaphor import UML
from gaphor.application import distribution
from gaphor.diagram.classes import AssociationItem, ClassItem, InterfaceItem
@ -147,6 +149,7 @@ class StorageTestCase(TestCase):
assert len(elements) == 1, elements
assert elements[0].name == difficult_name, elements[0].name
@pytest.mark.slow
def test_load_uml_metamodel(self):
"""
Test if the meta model can be loaded.

View File

@ -10,13 +10,14 @@ import unittest
from io import StringIO
from typing import Type, TypeVar
from gaphas.aspect import ConnectionSink, Connector
from gaphas.aspect import ConnectionSink
from gaphas.aspect import Connector as ConnectorAspect
# For DiagramItemConnector aspect:
import gaphor.diagram.diagramtools # noqa
from gaphor import UML
from gaphor.application import Session
from gaphor.diagram.connectors import IConnect
from gaphor.diagram.connectors import Connector
from gaphor.diagram.grouping import Group
T = TypeVar("T")
@ -66,7 +67,7 @@ class TestCase(unittest.TestCase):
if port is None and len(item.ports()) > 0:
port = item.ports()[0]
adapter = IConnect(item, line)
adapter = Connector(item, line)
return adapter.allow(handle, port)
def connect(self, line, handle, item, port=None):
@ -82,7 +83,7 @@ class TestCase(unittest.TestCase):
port = item.ports()[0]
sink = ConnectionSink(item, port)
connector = Connector(line, handle)
connector = ConnectorAspect(line, handle)
connector.connect(sink)

View File

@ -167,12 +167,9 @@ class ElementEditor(UIComponent, ActionProvider):
Return an ordered list of (order, name, adapter).
"""
adaptermap = {}
try:
if item.subject:
for adapter in PropertyPages(item.subject):
adaptermap[adapter.name] = (adapter.order, adapter.name, adapter)
except AttributeError:
pass
if item.subject:
for adapter in PropertyPages(item.subject):
adaptermap[adapter.name] = (adapter.order, adapter.name, adapter)
for adapter in PropertyPages(item):
adaptermap[adapter.name] = (adapter.order, adapter.name, adapter)

View File

@ -33,6 +33,7 @@ ICONS=diagram \
send-signal-action \
accept-event-action \
lifeline \
execution-specification \
message \
interaction \
state \

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="7.9999957"
height="15.998676"
viewBox="0 0 2.1166655 4.2329832"
version="1.1"
id="svg4268">
<defs
id="defs4262" />
<metadata
id="metadata4265">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<path
id="path1267-3"
d="m 1.0371094,3.0644531 a 0.18522686,0.18522686 0 0 0 -0.16406252,0.1875 v 0.7929688 a 0.18554686,0.18554686 0 1 0 0.37109372,0 V 3.2519531 a 0.18522686,0.18522686 0 0 0 -0.2070312,-0.1875 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.37041667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="path1267-6"
d="M 1.0546875,0 A 0.18522686,0.18522686 0 0 0 0.87304688,0.1875 v 0.79492188 a 0.18554686,0.18554686 0 1 0 0.37109372,0 V 0.1875 A 0.18522686,0.18522686 0 0 0 1.0546875,0 Z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.37041667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
id="rect1263-5"
d="M 0.23828125,0.79492188 A 0.26460978,0.26460978 0 0 0 0,1.0585938 V 3.1757812 A 0.26460978,0.26460978 0 0 0 0.26367188,3.4394531 H 1.8515625 A 0.26460978,0.26460978 0 0 0 2.1171875,3.1757812 V 1.0585938 A 0.26460978,0.26460978 0 0 0 1.8515625,0.79492188 H 0.26367188 a 0.26460978,0.26460978 0 0 0 -0.0253906,0 z M 0.52929688,1.3242188 H 1.5878906 V 2.9101562 H 0.52929688 Z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.52916664;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -33,11 +33,11 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="40.173252"
inkscape:cx="24.316871"
inkscape:cy="1146.0668"
inkscape:zoom="160.69301"
inkscape:cx="229.52617"
inkscape:cy="1157.2818"
inkscape:document-units="px"
inkscape:current-layer="pointer"
inkscape:current-layer="execution-specification"
showgrid="true"
units="px"
inkscape:window-width="3840"
@ -1219,6 +1219,28 @@
rx="1.5874995"
ry="1.5874976" />
</g>
<g
inkscape:groupmode="layer"
id="execution-specification"
inkscape:label="execution-specification">
<path
style="fill:none;stroke:#000000;stroke-width:0.37041667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.647917,-6.6659362 v 0.7937501"
id="path1267-3"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:0.37041667;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.647917,-9.7294768 v 0.7937496"
id="path1267-6"
inkscape:connector-curvature="0" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.52916664;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect1263-5"
width="1.5874989"
height="2.116667"
x="60.854168"
y="-8.8583422" />
</g>
<g
inkscape:groupmode="layer"
id="lifeline"

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -3,12 +3,13 @@ Test handle tool functionality.
"""
import pytest
from gaphas.aspect import ConnectionSink, Connector
from gaphas.aspect import ConnectionSink
from gaphas.aspect import Connector as ConnectorAspect
from gi.repository import Gdk, Gtk
from gaphor import UML
from gaphor.application import Session
from gaphor.diagram.connectors import IConnect
from gaphor.diagram.connectors import Connector
from gaphor.diagram.diagramtools import ConnectHandleTool, DiagramItemConnector
from gaphor.diagram.general.comment import CommentItem
from gaphor.diagram.general.commentline import CommentLineItem
@ -73,16 +74,16 @@ def commentline(diagram):
def test_aspect_type(commentline):
aspect = Connector(commentline, commentline.handles()[0])
aspect = ConnectorAspect(commentline, commentline.handles()[0])
assert isinstance(aspect, DiagramItemConnector)
def test_query(comment):
assert IConnect(comment, commentline)
def test_query(comment, commentline):
assert Connector(comment, commentline)
def test_allow(commentline, comment):
aspect = Connector(commentline, commentline.handles()[0])
aspect = ConnectorAspect(commentline, commentline.handles()[0])
assert aspect.item is commentline
assert aspect.handle is commentline.handles()[0]
@ -92,7 +93,7 @@ def test_allow(commentline, comment):
def test_connect(diagram, comment, commentline):
sink = ConnectionSink(comment, comment.ports()[0])
aspect = Connector(commentline, commentline.handles()[0])
aspect = ConnectorAspect(commentline, commentline.handles()[0])
aspect.connect(sink)
canvas = diagram.canvas
cinfo = canvas.get_connection(commentline.handles()[0])
@ -146,7 +147,7 @@ def test_iconnect(session, event_manager, element_factory):
assert cinfo.constraint is not None
assert cinfo.connected is actor, cinfo.connected
Connector(line, handle).disconnect()
ConnectorAspect(line, handle).disconnect()
cinfo = diagram.canvas.get_connection(handle)

View File

@ -9,4 +9,7 @@ python_files = test_*.py
# Console tests are failing the GitHub Actions CI (seg fault)
norecursedirs = gaphor/plugins/console/tests
junit_family=xunit1
junit_family=xunit1
markers =
slow: marks tests as slow (deselect with '-m "not slow"')