Turn Item into a protocol
Now we do not need to inherit from Item explicitly.
This commit is contained in:
parent
4b237b8ecd
commit
3dd5d02721
@ -23,9 +23,9 @@ The observer simply dispatches the function called (as ``<function ..>``, not as
|
||||
Let's start with creating a Canvas instance and some items:
|
||||
|
||||
>>> from gaphas.canvas import Canvas
|
||||
>>> from gaphas.item import Item
|
||||
>>> from examples.exampleitems import Circle
|
||||
>>> canvas = Canvas()
|
||||
>>> item1, item2 = Item(), Item()
|
||||
>>> item1, item2 = Circle(), Circle()
|
||||
|
||||
For this demonstration let's use the Canvas class (which contains an add/remove
|
||||
method pair).
|
||||
@ -36,11 +36,11 @@ It works (see how the add method automatically schedules the item for update):
|
||||
... print('event handled', event)
|
||||
>>> state.observers.add(handler)
|
||||
>>> canvas.add(item1) # doctest: +ELLIPSIS
|
||||
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <gaphas.item.Item object at ...>), {})
|
||||
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <examples.exampleitems.Circle object at ...>), {})
|
||||
>>> canvas.add(item2, parent=item1) # doctest: +ELLIPSIS
|
||||
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <gaphas.item.Item object at ...>), {'parent': <gaphas.item.Item object at ...>})
|
||||
event handled (<function Canvas.add at ...>, (<gaphas.canvas.Canvas object at ...>, <examples.exampleitems.Circle object at ...>), {'parent': <examples.exampleitems.Circle object at ...>})
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
Note that the handler is invoked before the actual call is made. This is
|
||||
important if you want to store the (old) state for an undo mechanism.
|
||||
@ -52,8 +52,8 @@ Therefore some careful crafting of methods may be necessary in order to get the
|
||||
right effect (items should be removed in the right order, child first):
|
||||
|
||||
>>> canvas.remove(item1) # doctest: +ELLIPSIS
|
||||
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>), {})
|
||||
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <gaphas.item.Item object at 0x...>), {})
|
||||
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <examples.exampleitems.Circle object at 0x...>), {})
|
||||
event handled (<function Canvas._remove at ...>, (<gaphas.canvas.Canvas object at 0x...>, <examples.exampleitems.Circle object at 0x...>), {})
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
|
||||
@ -126,10 +126,11 @@ Handlers for the reverse events should be registered on the subscribers list:
|
||||
|
||||
After that, signals can be received of undoable (reverse-)events:
|
||||
|
||||
>>> canvas.add(Item()) # doctest: +ELLIPSIS
|
||||
event handler (<function Canvas._remove at ...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <gaphas.item.Item object at 0x...>})
|
||||
>>> canvas.add(Circle()) # doctest: +ELLIPSIS
|
||||
event handler (<function Handle._set_movable at ...>, {'self': <Handle object on (Variable(0, 20), Variable(0, 20))>, 'movable': True})
|
||||
event handler (<function Canvas._remove at ...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <examples.exampleitems.Circle object at 0x...>})
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
As you can see this event is constructed of only two parameters: the function
|
||||
that does the inverse operation of ``add()`` and the arguments that should be
|
||||
@ -139,7 +140,7 @@ The inverse operation is easiest performed by the function ``saveapply()``. Of
|
||||
course an inverse operation is emitting a change event too:
|
||||
|
||||
>>> state.saveapply(*events.pop()) # doctest: +ELLIPSIS
|
||||
event handler (<function Canvas.add at 0x...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <gaphas.item.Item object at 0x...>, 'parent': None, 'index': 0})
|
||||
event handler (<function Canvas.add at 0x...>, {'self': <gaphas.canvas.Canvas object at 0x...>, 'item': <examples.exampleitems.Circle object at 0x...>, 'parent': None, 'index': 0})
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
|
||||
|
@ -111,25 +111,26 @@ Again, rotate does not result in an exact match, but it's close enough.
|
||||
canvas.py: Canvas
|
||||
-----------------
|
||||
|
||||
>>> from gaphas import Canvas, Item
|
||||
>>> from gaphas import Canvas
|
||||
>>> from examples.exampleitems import Circle
|
||||
>>> canvas = Canvas()
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
>>> item = Item()
|
||||
>>> item = Circle()
|
||||
>>> canvas.add(item)
|
||||
|
||||
The ``request_update()`` method is observed:
|
||||
|
||||
>>> len(undo_list)
|
||||
1
|
||||
2
|
||||
>>> canvas.request_update(item)
|
||||
>>> len(undo_list)
|
||||
2
|
||||
3
|
||||
|
||||
On the canvas only ``add()`` and ``remove()`` are monitored:
|
||||
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
@ -140,7 +141,7 @@ On the canvas only ``add()`` and ``remove()`` are monitored:
|
||||
[]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
>>> undo_list
|
||||
[]
|
||||
|
||||
@ -149,15 +150,15 @@ Parent-child relationships are restored as well:
|
||||
TODO!
|
||||
|
||||
|
||||
>>> child = Item()
|
||||
>>> child = Circle()
|
||||
>>> canvas.add(child, parent=item)
|
||||
>>> canvas.get_parent(child) is item
|
||||
True
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
>>> child in canvas.get_all_items()
|
||||
False
|
||||
|
||||
@ -168,32 +169,35 @@ Now redo the previous undo action:
|
||||
>>> canvas.get_parent(child) is item
|
||||
True
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
Remove also works when items are removed recursively (an item and it's
|
||||
children):
|
||||
|
||||
>>> child = Item()
|
||||
>>> child = Circle()
|
||||
>>> canvas.add(child, parent=item)
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
>>> del undo_list[:]
|
||||
>>> canvas.remove(item)
|
||||
>>> list(canvas.get_all_items())
|
||||
[]
|
||||
>>> undo()
|
||||
>>> list(canvas.get_all_items()) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>, <gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>, <examples.exampleitems.Circle object at 0x...>]
|
||||
>>> canvas.get_children(item) # doctest: +ELLIPSIS
|
||||
[<gaphas.item.Item object at 0x...>]
|
||||
[<examples.exampleitems.Circle object at 0x...>]
|
||||
|
||||
As well as the reparent() method:
|
||||
|
||||
>>> canvas = Canvas()
|
||||
>>> class NameItem(Item):
|
||||
>>> class NameItem:
|
||||
... def __init__(self, name):
|
||||
... super(NameItem, self).__init__()
|
||||
... self.name = name
|
||||
... def handles(self): return []
|
||||
... def ports(self): return []
|
||||
... def point(self, x, y): return 0
|
||||
... def __repr__(self):
|
||||
... return '<%s>' % self.name
|
||||
>>> ni1 = NameItem('a')
|
||||
@ -347,9 +351,9 @@ Also creation and removal of connected lines is recorded and can be undone:
|
||||
... def real_disconnect():
|
||||
... pass
|
||||
... canvas.connections.connect_item(hitem, handle, item, port=None, constraint=None, callback=real_disconnect)
|
||||
>>> b0 = Item()
|
||||
>>> b0 = Circle()
|
||||
>>> canvas.add(b0)
|
||||
>>> b1 = Item()
|
||||
>>> b1 = Circle()
|
||||
>>> canvas.add(b1)
|
||||
>>> l = Line(Connections())
|
||||
>>> canvas.add(l)
|
||||
|
@ -3,7 +3,7 @@
|
||||
These items are used in various tests.
|
||||
"""
|
||||
from gaphas.connector import Handle
|
||||
from gaphas.item import NW, Element, Item
|
||||
from gaphas.item import NW, Element, Matrices, Updateable
|
||||
from gaphas.util import path_ellipse, text_align, text_multiline
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ class Box(Element):
|
||||
c.stroke()
|
||||
|
||||
|
||||
class Text(Item):
|
||||
class Text(Matrices, Updateable):
|
||||
"""Simple item showing some text on the canvas."""
|
||||
|
||||
def __init__(self, text=None, plain=False, multiline=False, align_x=1, align_y=-1):
|
||||
@ -40,6 +40,15 @@ class Text(Item):
|
||||
self.align_x = align_x
|
||||
self.align_y = align_y
|
||||
|
||||
def handles(self):
|
||||
return []
|
||||
|
||||
def ports(self):
|
||||
return []
|
||||
|
||||
def point(self, x, y):
|
||||
return 0
|
||||
|
||||
def draw(self, context):
|
||||
cr = context.cairo
|
||||
if self.multiline:
|
||||
@ -49,14 +58,11 @@ class Text(Item):
|
||||
else:
|
||||
text_align(cr, 0, 0, self.text, self.align_x, self.align_y)
|
||||
|
||||
def point(self, x, y):
|
||||
return 0
|
||||
|
||||
|
||||
class Circle(Item):
|
||||
class Circle(Matrices, Updateable):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._handles.extend((Handle(), Handle()))
|
||||
self._handles = [Handle(), Handle()]
|
||||
h1, h2 = self._handles
|
||||
h1.movable = False
|
||||
|
||||
@ -72,6 +78,12 @@ class Circle(Item):
|
||||
|
||||
radius = property(_get_radius, _set_radius)
|
||||
|
||||
def handles(self):
|
||||
return self._handles
|
||||
|
||||
def ports(self):
|
||||
return []
|
||||
|
||||
def point(self, x, y):
|
||||
h1, _ = self._handles
|
||||
p1 = h1.pos
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Basic items."""
|
||||
from math import atan2
|
||||
from typing import Sequence
|
||||
from typing import Protocol, Sequence
|
||||
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.connector import Handle, LinePort, Port
|
||||
@ -16,31 +16,48 @@ from gaphas.state import (
|
||||
)
|
||||
|
||||
|
||||
class Item(Protocol):
|
||||
@property
|
||||
def matrix(self) -> Matrix:
|
||||
...
|
||||
|
||||
@property
|
||||
def matrix_i2c(self) -> Matrix:
|
||||
...
|
||||
|
||||
def handles(self) -> Sequence[Handle]:
|
||||
"""Return a list of handles owned by the item."""
|
||||
|
||||
def ports(self) -> Sequence[Port]:
|
||||
"""Return list of ports."""
|
||||
|
||||
def point(self, x: float, y: float) -> float:
|
||||
"""Get the distance from a point (``x``, ``y``) to the item.
|
||||
|
||||
``x`` and ``y`` are in item coordinates.
|
||||
"""
|
||||
|
||||
def draw(self, context: Context):
|
||||
"""Render the item to a canvas view. Context contains the following
|
||||
attributes:
|
||||
|
||||
- cairo: the Cairo Context use this one to draw
|
||||
- selected, focused, hovered, dropzone: view state of items
|
||||
(True/False)
|
||||
"""
|
||||
|
||||
|
||||
def matrix_i2i(from_item, to_item):
|
||||
i2c = from_item.matrix_i2c
|
||||
c2i = to_item.matrix_i2c.inverse()
|
||||
return i2c.multiply(c2i)
|
||||
|
||||
|
||||
class Item:
|
||||
"""Base class (or interface) for items on a canvas.Canvas.
|
||||
|
||||
Attributes:
|
||||
|
||||
- matrix: item's transformation matrix
|
||||
|
||||
Private:
|
||||
|
||||
- _handles: list of handles owned by an item
|
||||
- _ports: list of ports, connectable areas of an item
|
||||
"""
|
||||
|
||||
class Matrices:
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs) # type: ignore[call-arg]
|
||||
self._matrix = Matrix()
|
||||
self._matrix_i2c = Matrix()
|
||||
self._handles = []
|
||||
self._ports = []
|
||||
|
||||
@property
|
||||
def matrix(self) -> Matrix:
|
||||
@ -50,6 +67,8 @@ class Item:
|
||||
def matrix_i2c(self) -> Matrix:
|
||||
return self._matrix_i2c
|
||||
|
||||
|
||||
class Updateable:
|
||||
def pre_update(self, context: Context):
|
||||
"""Perform any changes before item update here, for example:
|
||||
|
||||
@ -76,36 +95,11 @@ class Item:
|
||||
"""
|
||||
pass
|
||||
|
||||
def draw(self, context: Context):
|
||||
"""Render the item to a canvas view. Context contains the following
|
||||
attributes:
|
||||
|
||||
- cairo: the Cairo Context use this one to draw
|
||||
- selected, focused, hovered, dropzone: view state of items
|
||||
(True/False)
|
||||
"""
|
||||
pass
|
||||
|
||||
def handles(self) -> Sequence[Handle]:
|
||||
"""Return a list of handles owned by the item."""
|
||||
return self._handles
|
||||
|
||||
def ports(self) -> Sequence[Port]:
|
||||
"""Return list of ports."""
|
||||
return self._ports
|
||||
|
||||
def point(self, x: float, y: float):
|
||||
"""Get the distance from a point (``x``, ``y``) to the item.
|
||||
|
||||
``x`` and ``y`` are in item coordinates.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
[NW, NE, SE, SW] = list(range(4))
|
||||
|
||||
|
||||
class Element(Item):
|
||||
class Element(Matrices, Updateable):
|
||||
"""An Element has 4 handles (for a start)::
|
||||
|
||||
NW +---+ NE | | SW +---+ SE
|
||||
@ -199,6 +193,14 @@ class Element(Item):
|
||||
|
||||
height = property(_get_height, _set_height)
|
||||
|
||||
def handles(self) -> Sequence[Handle]:
|
||||
"""Return a list of handles owned by the item."""
|
||||
return self._handles
|
||||
|
||||
def ports(self) -> Sequence[Port]:
|
||||
"""Return list of ports."""
|
||||
return self._ports
|
||||
|
||||
def point(self, x, y):
|
||||
"""Distance from the point (x, y) to the item.
|
||||
|
||||
@ -212,6 +214,9 @@ class Element(Item):
|
||||
list(map(float, (pnw.x, pnw.y, pse.x, pse.y))), (x, y)
|
||||
)
|
||||
|
||||
def draw(self, context: Context):
|
||||
pass
|
||||
|
||||
|
||||
def create_orthogonal_constraints(handles, horizontal):
|
||||
rest = 1 if horizontal else 0
|
||||
@ -224,7 +229,7 @@ def create_orthogonal_constraints(handles, horizontal):
|
||||
yield EqualsConstraint(a=p0.y, b=p1.y)
|
||||
|
||||
|
||||
class Line(Item):
|
||||
class Line(Matrices, Updateable):
|
||||
"""A Line item.
|
||||
|
||||
Properties:
|
||||
@ -411,6 +416,14 @@ class Line(Item):
|
||||
p1, p0 = h1.pos, h0.pos
|
||||
self._tail_angle = atan2(p1.y - p0.y, p1.x - p0.x) # type: ignore[assignment]
|
||||
|
||||
def handles(self) -> Sequence[Handle]:
|
||||
"""Return a list of handles owned by the item."""
|
||||
return self._handles
|
||||
|
||||
def ports(self) -> Sequence[Port]:
|
||||
"""Return list of ports."""
|
||||
return self._ports
|
||||
|
||||
def point(self, x, y):
|
||||
"""
|
||||
>>> a = Line()
|
||||
|
@ -102,7 +102,7 @@ class BoundingBoxPainter:
|
||||
):
|
||||
self.item_painter = item_painter
|
||||
|
||||
def paint_item(self, item, cairo):
|
||||
def paint_item(self, item: Item, cairo):
|
||||
cairo = CairoBoundingBoxContext(cairo)
|
||||
self.item_painter.paint_item(item, cairo)
|
||||
# Bounding box is in view (cairo root) coordinates
|
||||
|
@ -86,7 +86,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, canvas: Optional[Model[Item]] = None):
|
||||
def __init__(self, canvas: Optional[Model] = None):
|
||||
Gtk.DrawingArea.__init__(self)
|
||||
|
||||
self._dirty_items: Set[Item] = set()
|
||||
@ -117,7 +117,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
|
||||
|
||||
self._qtree: Quadtree[Item, Tuple[float, float, float, float]] = Quadtree()
|
||||
|
||||
self._canvas: Optional[Model[Item]] = None
|
||||
self._canvas: Optional[Model] = None
|
||||
if canvas:
|
||||
self._set_canvas(canvas)
|
||||
|
||||
@ -152,7 +152,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable):
|
||||
m.invert()
|
||||
return m
|
||||
|
||||
def _set_canvas(self, canvas: Optional[Model[Item]]) -> None:
|
||||
def _set_canvas(self, canvas: Optional[Model]) -> None:
|
||||
"""
|
||||
Use view.canvas = my_canvas to set the canvas to be rendered
|
||||
in the view.
|
||||
|
@ -1,45 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Optional, Sequence, TypeVar
|
||||
from typing import Collection, Iterable, Optional
|
||||
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
|
||||
T = TypeVar("T")
|
||||
T_ct = TypeVar("T_ct", contravariant=True)
|
||||
from gaphas.item import Item
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Model(Protocol[T]):
|
||||
def get_all_items(self) -> Iterable[T]:
|
||||
class View(Protocol):
|
||||
def request_update(
|
||||
self,
|
||||
items: Collection[Item],
|
||||
matrix_only_items: Collection[Item],
|
||||
removed_items: Collection[Item],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def get_parent(self, item: T) -> Optional[T]:
|
||||
|
||||
@runtime_checkable
|
||||
class Model(Protocol):
|
||||
def get_all_items(self) -> Iterable[Item]:
|
||||
...
|
||||
|
||||
def get_children(self, item: T) -> Iterable[T]:
|
||||
def get_parent(self, item: Item) -> Optional[Item]:
|
||||
...
|
||||
|
||||
def sort(self, items: Sequence[T]) -> Iterable[T]:
|
||||
def get_children(self, item: Item) -> Iterable[Item]:
|
||||
...
|
||||
|
||||
def sort(self, items: Collection[Item]) -> Iterable[Item]:
|
||||
...
|
||||
|
||||
def update_now(
|
||||
self, dirty_items: Sequence[T], dirty_matrix_items: Sequence[T]
|
||||
self, dirty_items: Collection[Item], dirty_matrix_items: Collection[Item]
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def register_view(self, view: View[T]) -> None:
|
||||
def register_view(self, view: View) -> None:
|
||||
...
|
||||
|
||||
def unregister_view(self, view: View[T]) -> None:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class View(Protocol[T_ct]):
|
||||
def request_update(
|
||||
self,
|
||||
items: Sequence[T_ct],
|
||||
matrix_only_items: Sequence[T_ct],
|
||||
removed_items: Sequence[T_ct],
|
||||
) -> None:
|
||||
def unregister_view(self, view: View) -> None:
|
||||
...
|
||||
|
@ -3,12 +3,12 @@
|
||||
import pytest
|
||||
|
||||
from gaphas.aspect import InMotion, Selection
|
||||
from gaphas.item import Item
|
||||
from gaphas.item import Element
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def item():
|
||||
return Item()
|
||||
def item(connections):
|
||||
return Element(connections)
|
||||
|
||||
|
||||
def test_selection_select(canvas, view, item):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Test segment aspects for items."""
|
||||
import pytest
|
||||
|
||||
from gaphas.item import Item
|
||||
from gaphas.item import Element
|
||||
from gaphas.segment import HandleFinder, Line, Segment, SegmentHandleFinder
|
||||
from gaphas.tool import ConnectHandleTool
|
||||
|
||||
@ -11,15 +11,13 @@ def tool(view):
|
||||
return ConnectHandleTool(view)
|
||||
|
||||
|
||||
def test_segment_fails_for_item(canvas):
|
||||
"""Test if Segment aspect can be applied to Item."""
|
||||
item = Item()
|
||||
def test_segment_fails_for_element(canvas, connections):
|
||||
item = Element(connections)
|
||||
with pytest.raises(TypeError):
|
||||
Segment(canvas, item)
|
||||
|
||||
|
||||
def test_segment(canvas, connections):
|
||||
"""Test add a new segment to a line."""
|
||||
def test_add_segment_to_line(canvas, connections):
|
||||
line = Line(connections)
|
||||
canvas.add(line)
|
||||
segment = Segment(line, canvas)
|
||||
@ -32,7 +30,6 @@ def test_segment(canvas, connections):
|
||||
|
||||
|
||||
def test_split_single(canvas, line):
|
||||
"""Test single line splitting."""
|
||||
# Start with 2 handles & 1 port, after split: expect 3 handles & 2 ports
|
||||
assert len(line.handles()) == 2
|
||||
assert len(line.ports()) == 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user