Move presentation creation to element factory

Alongside all other model elements
This commit is contained in:
Arjan Molenaar 2021-05-29 21:36:21 +02:00
parent bee26ddd32
commit 3466c2f2d2
No known key found for this signature in database
GPG Key ID: BF977B918996CB13
15 changed files with 111 additions and 79 deletions

View File

@ -49,7 +49,7 @@ def test_association_item_connect(connected_association, element_factory):
asc, c1, c2 = connected_association
# Diagram, Class *2, Property *2, Association
assert len(element_factory.lselect()) == 6
assert len(element_factory.lselect()) == 9
assert asc.head_subject is not None
assert asc.tail_subject is not None

View File

@ -152,8 +152,16 @@ def test_copy_remove_paste_items_with_connections(diagram, element_factory):
new_items = copy_clear_and_paste(
{gen_cls_item, assoc_item, spc_cls_item}, diagram, element_factory
)
new_cls1 = next(element_factory.select(lambda e: e.name == "Gen"))
new_cls2 = next(element_factory.select(lambda e: e.name == "Spc"))
new_cls1 = next(
element_factory.select(
lambda e: isinstance(e, UML.NamedElement) and e.name == "Gen"
)
)
new_cls2 = next(
element_factory.select(
lambda e: isinstance(e, UML.NamedElement) and e.name == "Spc"
)
)
new_assoc = next(element_factory.select(UML.Association))
assert new_assoc.memberEnd[0].type is new_cls1

View File

@ -18,7 +18,10 @@ class UMLModelingLanguage(ModelingLanguage):
return uml_toolbox_actions
def lookup_element(self, name):
return getattr(uml, name, None)
element_type = getattr(uml, name, None)
if not element_type:
element_type = self.lookup_diagram_item(name)
return element_type
def lookup_diagram_item(self, name):
return getattr(diagramitems, name, None)

View File

@ -38,8 +38,16 @@ def test_cut_paste_of_stereotype(diagram, element_factory):
)
copy_clear_and_paste({m_cls_item, st_cls_item, ext_item}, diagram, element_factory)
new_m_cls = next(element_factory.select(lambda e: e.name == "Class"))
new_st_cls = next(element_factory.select(lambda e: e.name == "Stereotype"))
new_m_cls = next(
element_factory.select(
lambda e: isinstance(e, UML.NamedElement) and e.name == "Class"
)
)
new_st_cls = next(
element_factory.select(
lambda e: isinstance(e, UML.NamedElement) and e.name == "Stereotype"
)
)
new_ext = next(element_factory.select(UML.Extension))
assert new_m_cls in new_ext.memberEnd[:].type

View File

@ -8,13 +8,14 @@ import logging
import uuid
from dataclasses import dataclass
from functools import lru_cache
from typing import Iterable, Iterator, Optional, Sequence, Set, Union
from typing import Iterable, Iterator, Optional, Sequence, Set, Type, TypeVar, Union
import gaphas
from typing_extensions import Protocol, runtime_checkable
from gaphor.core.modeling.collection import collection
from gaphor.core.modeling.element import Element, Id, RepositoryProtocol
from gaphor.core.modeling.event import AssociationDeleted, ElementCreated
from gaphor.core.modeling.event import AssociationDeleted
from gaphor.core.modeling.presentation import Presentation
from gaphor.core.modeling.properties import (
association,
@ -197,18 +198,6 @@ class StyledItem:
)
class PseudoCanvas:
"""A "pseudo" canvas implementation, for storing items."""
def __init__(self, diagram: Diagram):
self.diagram = diagram
def save(self, save_func):
for item in self.diagram.ownedPresentation:
if not item.parent:
save_func(item)
class Diagram(Element):
"""Diagrams may contain model elements and can be owned by a Package."""
@ -252,7 +241,6 @@ class Diagram(Element):
"""Apply the supplied save function to this diagram and the canvas."""
super().save(save_func)
save_func("canvas", PseudoCanvas(self))
def postload(self):
"""Handle post-load functionality for the diagram."""
@ -270,17 +258,10 @@ class Diagram(Element):
return self.create_as(type, str(uuid.uuid1()), parent, subject)
def create_as(self, type, id, parent=None, subject=None):
if not (type and issubclass(type, Presentation)):
raise TypeError(
f"Type {type} can not be added to a diagram as it is not a diagram item"
)
# Avoid events that reference this element before its created-event is emitted.
with self.model.block_events():
item = type(diagram=self, id=id)
assert isinstance(
item, gaphas.Item
), f"Type {type} does not comply with Item protocol"
self.model.handle(ElementCreated(self.model, item, self))
assert isinstance(self.model, PresentationRepositoryProtocol)
item = self.model.create_as(type, id, diagram=self)
if not isinstance(item, gaphas.Item):
raise TypeError(f"Type {type} does not comply with Item protocol")
if subject:
item.subject = subject
if parent:
@ -388,3 +369,12 @@ class Diagram(Element):
Presentation.diagram = association(
"diagram", Diagram, upper=1, opposite="ownedPresentation"
)
P = TypeVar("P", bound=Presentation)
@runtime_checkable
class PresentationRepositoryProtocol(Protocol):
def create_as(self, type: Type[P], id: str, diagram: Diagram) -> P:
...

View File

@ -35,6 +35,7 @@ if TYPE_CHECKING:
T = TypeVar("T", bound=Element)
P = TypeVar("P", bound=Presentation)
class ElementFactory(Service):
@ -66,18 +67,33 @@ class ElementFactory(Service):
"""Create a new model element of type ``type``."""
return self.create_as(type, str(uuid.uuid1()))
def create_as(self, type: Type[T], id: str) -> T:
def create_as(self, type: Type[T], id: str, diagram: Diagram = None) -> T:
"""Create a new model element of type 'type' with 'id' as its ID.
This method should only be used when loading models, since it
does not emit an ElementCreated event.
"""
if not type or not issubclass(type, Element) or issubclass(type, Presentation):
if not type:
raise TypeError(f"Type {type} not defined")
elif issubclass(type, Presentation):
if not diagram:
raise TypeError("Presentation types require a diagram")
# Avoid events that reference this element before its created-event is emitted.
with self.block_events():
item = type(diagram=diagram, id=id)
self._elements[id] = item
self.handle(ElementCreated(self, item, diagram))
return item
elif issubclass(type, Element):
if diagram:
raise TypeError("Element types require no diagram")
obj = type(id, self)
self._elements[id] = obj
self.handle(ElementCreated(self, obj))
return obj
else:
raise TypeError(f"Type {type} is not a valid model element")
obj = type(id, self)
self._elements[id] = obj
self.handle(ElementCreated(self, obj))
return obj
def size(self) -> int:
"""Return the amount of elements currently in the factory."""
@ -182,6 +198,8 @@ class ElementFactory(Service):
del self._elements[element.id]
except KeyError:
return
if isinstance(event.element, Presentation):
return
event = ElementDeleted(self, event.element)
if self.event_manager and not self._block_events:
self.event_manager.handle(event)

View File

@ -38,7 +38,7 @@ def test_canvas_is_saved():
saved_keys = []
diagram.save(lambda name, val: saved_keys.append(name))
assert "canvas" in saved_keys
assert "canvas" not in saved_keys
def test_canvas_item_is_created(element_factory):

View File

@ -212,7 +212,8 @@ def _paste_all(copy_data: CopyData, diagram, lookup) -> Set[Presentation]:
return new_elements[ref]
looked_up = lookup(ref)
if looked_up:
# TODO: Presentation element should be created anyhow
if looked_up and not isinstance(looked_up, Presentation):
return looked_up
elif ref in copy_data.elements:

View File

@ -14,7 +14,7 @@ from functools import partial
from gaphor import application
from gaphor.core.modeling.collection import collection
from gaphor.core.modeling.diagram import Diagram, PseudoCanvas
from gaphor.core.modeling.diagram import Diagram
from gaphor.core.modeling.element import Element
from gaphor.core.modeling.presentation import Presentation
from gaphor.core.modeling.stylesheet import StyleSheet
@ -137,10 +137,6 @@ def save_element(name, value, writer):
save_reference(name, value)
elif isinstance(value, collection):
save_collection(name, value)
elif isinstance(value, PseudoCanvas):
writer.startElement("canvas", {})
value.save(save_diagram)
writer.endElement("canvas")
else:
save_value(name, value)
@ -184,6 +180,29 @@ def load_elements_generator(elements, factory, modeling_language, gaphor_version
def _load_elements_and_canvasitems(
elements, factory, modeling_language, gaphor_version, update_status_queue
):
def create_element(elem):
if elem.element:
return
if version_lower_than(gaphor_version, (2, 1, 0)):
elem = upgrade_element_owned_comment_to_comment(elem)
if version_lower_than(gaphor_version, (2, 3, 0)):
elem = upgrade_package_owned_classifier_to_owned_type(elem)
elem = upgrade_implementation_to_interface_realization(elem)
elem = upgrade_feature_parameters_to_owned_parameter(elem)
elem = upgrade_parameter_owner_formal_param(elem)
if version_lower_than(gaphor_version, (2, 5, 0)):
elem = upgrade_diagram_element(elem)
cls = modeling_language.lookup_element(elem.type)
assert cls, f"Type {elem.type} can not be loaded: no such element"
if issubclass(cls, Presentation):
diagram_id = elem.references["diagram"]
diagram_elem = elements[diagram_id]
create_element(diagram_elem)
elem.element = factory.create_as(cls, id, diagram_elem.element)
else:
elem.element = factory.create_as(cls, id)
def create_canvasitems(diagram, canvasitems, parent=None):
"""Diagram is a Core Diagram, items is a list of
parser.canvasitem's."""
@ -208,20 +227,10 @@ def _load_elements_and_canvasitems(
for id, elem in list(elements.items()):
yield from update_status_queue()
if isinstance(elem, parser.element):
if version_lower_than(gaphor_version, (2, 1, 0)):
elem = upgrade_element_owned_comment_to_comment(elem)
if version_lower_than(gaphor_version, (2, 3, 0)):
elem = upgrade_package_owned_classifier_to_owned_type(elem)
elem = upgrade_implementation_to_interface_realization(elem)
elem = upgrade_feature_parameters_to_owned_parameter(elem)
elem = upgrade_parameter_owner_formal_param(elem)
if version_lower_than(gaphor_version, (2, 5, 0)):
elem = upgrade_diagram_element(elem)
cls = modeling_language.lookup_element(elem.type)
assert cls, f"Type {elem.type} can not be loaded: no such element"
elem.element = factory.create_as(cls, id)
if isinstance(elem.element, Diagram):
create_element(elem)
if version_lower_than(gaphor_version, (2, 5, 0)) and isinstance(
elem.element, Diagram
):
assert elem.canvas
create_canvasitems(elem.element, elem.canvas.canvasitems)
elif not isinstance(elem, parser.canvasitem):

View File

@ -68,8 +68,8 @@ class TestStorage:
assert "<Diagram " in out.data
assert "<Comment " in out.data
assert "<canvas>" in out.data
assert ' type="CommentItem"' in out.data, out.data
assert "<canvas>" not in out.data
assert "<CommentItem " in out.data, out.data
def test_load_uml(self, case):
"""Test loading of a freshly saved model."""
@ -101,7 +101,7 @@ class TestStorage:
data = case.save()
case.load(data)
assert len(case.element_factory.lselect()) == 6
assert len(case.element_factory.lselect()) == 9
assert len(case.element_factory.lselect(UML.Package)) == 1
assert len(case.element_factory.lselect(UML.Diagram)) == 1
d = case.element_factory.lselect(UML.Diagram)[0]
@ -163,7 +163,7 @@ class TestStorage:
data = case.save()
case.load(data)
assert len(case.element_factory.lselect()) == 5
assert len(case.element_factory.lselect()) == 8
assert len(case.element_factory.lselect(UML.Package)) == 1
assert len(case.element_factory.lselect(UML.Diagram)) == 1
d = case.element_factory.lselect(UML.Diagram)[0]

View File

@ -2,7 +2,6 @@
from gaphor.core.modeling import Element
from gaphor.core.modeling.collection import collection
from gaphor.core.modeling.diagram import PseudoCanvas
def orphan_references(factory):
@ -37,8 +36,6 @@ def orphan_references(factory):
verify_reference(name, value)
elif isinstance(value, collection):
verify_collection(name, value)
elif isinstance(value, PseudoCanvas):
value.save(verify_canvas)
def verify_canvas(value):
elements.add(value.id)

View File

@ -32,7 +32,7 @@ def test_placement(diagram, page, element_factory):
page.view.request_update([box])
diagram.create(CommentItem, subject=element_factory.create(Comment))
assert len(element_factory.lselect()) == 2
assert len(element_factory.lselect()) == 4
@pytest.mark.skipif(

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "gaphor"
version = "2.4.2"
version = "2.5.0"
description = "Gaphor is the simple modeling tool written in Python."
authors = [
"Arjan J. Molenaar <gaphor@gmail.com>",

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<gaphor xmlns="http://gaphor.sourceforge.net/model" version="3.0" gaphor-version="2.4.1">
<gaphor xmlns="http://gaphor.sourceforge.net/model" version="3.0" gaphor-version="2.5.0">
<Package id="DCE:658E43CC-3A1D-11DC-8B61-000D93868322">
<name>
<val>New model</val>
@ -24,8 +24,8 @@
<ref refid="DCE:68ECCFB2-3A1D-11DC-8B61-000D93868322"/>
</reflist>
</ownedPresentation>
<canvas>
<item id="DCE:6719E9C4-3A1D-11DC-8B61-000D93868322" type="Line">
</Diagram>
<Line id="DCE:6719E9C4-3A1D-11DC-8B61-000D93868322">
<diagram>
<ref refid="DCE:658E851A-3A1D-11DC-8B61-000D93868322"/>
</diagram>
@ -41,8 +41,8 @@
<points>
<val>[(0.0, 0.0), (93.0, -2.0), (87.0, 83.0)]</val>
</points>
</item>
<item id="DCE:680E2142-3A1D-11DC-8B61-000D93868322" type="Box">
</Line>
<Box id="DCE:680E2142-3A1D-11DC-8B61-000D93868322">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 215.0, 121.0)</val>
</matrix>
@ -55,8 +55,8 @@
<diagram>
<ref refid="DCE:658E851A-3A1D-11DC-8B61-000D93868322"/>
</diagram>
</item>
<item id="DCE:68ECCFB2-3A1D-11DC-8B61-000D93868322" type="Ellipse">
</Box>
<Ellipse id="DCE:68ECCFB2-3A1D-11DC-8B61-000D93868322">
<matrix>
<val>(1.0, 0.0, 0.0, 1.0, 332.0, 137.0)</val>
</matrix>
@ -69,9 +69,7 @@
<diagram>
<ref refid="DCE:658E851A-3A1D-11DC-8B61-000D93868322"/>
</diagram>
</item>
</canvas>
</Diagram>
</Ellipse>
<StyleSheet id="d8c43f36-c545-11ea-9af2-f5ca580d221e">
<styleSheet>
<val>diagram {

View File

@ -92,7 +92,7 @@ def set_up_class_and_association(event_manager, element_factory):
connect(a, a.tail, ci2)
# Diagram, Association, 2x Class, Property, LiteralSpecification
assert 9 == len(element_factory.lselect())
assert 12 == len(element_factory.lselect())
assert 18 == len(diagram.connections.solver.constraints)
return diagram, ci1, ci2, a