Merge remote-tracking branch 'origin/2.2' into 3.0

# Conflicts:
#	CHANGELOG
#	dev-requirements.txt
#	gns3/crash_report.py
#	gns3/version.py
#	tests/test_http_client.py
This commit is contained in:
grossmj
2026-02-11 17:50:18 +08:00
12 changed files with 50 additions and 35 deletions

View File

@@ -1,5 +1,15 @@
# Change Log
## 2.2.56.1 28/01/2026
* Fix line style support for links
* Fix cannot add IOS in preferences
* Fix cannot add IOU in preferences
* Upgrade dependencies
* Drop Python 3.8 support
* Display a warning if a SVG image format isn't supported
* Fix error in profile selection window after PyQt6 migration
## 3.0.6 28/01/2026
* Fixing tab name in MobaXterm

View File

@@ -1,2 +1,3 @@
pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9
pytest==8.4.2; python_version == '3.9' # version 8.4.2 is the last one supporting Python 3.9
pytest==9.0.2; python_version >= '3.10'
pytest-timeout==2.4.0

View File

@@ -65,10 +65,13 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
link.setHovered(True)
self._border_color = pen.color()
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(self._border_color.red(),
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha()))
self.uiBorderColorPushButton.setStyleSheet("background-color: rgba({}, {}, {}, {});".format(
self._border_color.red(),
self._border_color.green(),
self._border_color.blue(),
self._border_color.alpha())
)
self.uiBorderWidthSpinBox.setValue(pen.width())
index = self.uiBorderStyleComboBox.findData(pen.style())
if index != -1:
@@ -102,8 +105,8 @@ class StyleEditorDialogLink(QtWidgets.QDialog, Ui_StyleEditorDialog):
new_link_style = {}
new_link_style["color"] = self._border_color.name()
new_link_style["width"] = self.uiBorderWidthSpinBox.value()
new_link_style["type"] = border_style
new_link_style["type"] = border_style.value
# Store values
self._link.setLinkStyle(new_link_style)
self._link.setHovered(False) # allow to see the new style

View File

@@ -56,7 +56,7 @@ class EthernetLinkItem(LinkItem):
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._link._link_style["width"] + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
else:
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], QtCore.Qt.PenStyle(self._link._link_style["type"]), QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
except:
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._pen_width + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))

View File

@@ -54,7 +54,7 @@ class SerialLinkItem(LinkItem):
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._link._link_style["width"] + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
else:
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], self._link._link_style["type"], QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
self.setPen(QtGui.QPen(QtGui.QColor(self._link._link_style["color"]), self._link._link_style["width"], QtCore.Qt.PenStyle(self._link._link_style["type"]), QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))
except:
if self._hovered:
self.setPen(QtGui.QPen(QtCore.Qt.GlobalColor.red, self._pen_width + 1, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin))

View File

@@ -1,5 +1,6 @@
jsonschema>=4.25.1,<4.26 # version 4.25.1 is the last to support Python 3.9
sentry-sdk>=2.50.0,<3 # optional dependency
psutil>=7.2.1
jsonschema==4.25.1; python_version == '3.9' # version 4.25.1 is the last to support Python 3.9
jsonschema>=4.26.0,<4.27; python_version >= '3.10'
sentry-sdk>=2.52.0,<3 # optional dependency
psutil>=7.2.2
distro>=1.9.0
truststore>=0.10.4; python_version >= '3.10'

View File

@@ -23,8 +23,8 @@ from gns3.qt import QtGui, QtCore
def test_toSvg(project, controller):
ellipse = EllipseItem(width=400, height=100, project=project)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
pen.setStyle(QtCore.Qt.DashLine)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
pen.setStyle(QtCore.Qt.PenStyle.DashLine)
ellipse.setPen(pen)
svg = ET.fromstring(ellipse.toSvg())
assert float(svg.get("width")) == 400.0

View File

@@ -32,7 +32,7 @@ def test_dump():
font.setUnderline(True)
font.setStrikeOut(True)
label.setFont(font)
label.setDefaultTextColor(QtCore.Qt.red)
label.setDefaultTextColor(QtCore.Qt.GlobalColor.red)
assert label.dump() == {
"text": "Test",
@@ -54,7 +54,7 @@ def test_setStyle():
font.setUnderline(True)
font.setStrikeOut(False)
label.setFont(font)
label.setDefaultTextColor(QtCore.Qt.red)
label.setDefaultTextColor(QtCore.Qt.GlobalColor.red)
style = label.dump()["style"]
label2 = LabelItem()
@@ -65,4 +65,4 @@ def test_setStyle():
assert label2.font().bold()
assert label2.font().strikeOut() is False
assert label2.font().underline()
assert label2.defaultTextColor() == QtCore.Qt.red
assert label2.defaultTextColor() == QtCore.Qt.GlobalColor.red

View File

@@ -23,8 +23,8 @@ from gns3.qt import QtGui, QtCore
def test_toSvg(project, controller):
line = LineItem(dst=QtCore.QPointF(400, 280), project=project)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
pen.setStyle(QtCore.Qt.DashLine)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
pen.setStyle(QtCore.Qt.PenStyle.DashLine)
line.setPen(pen)
svg = ET.fromstring(line.toSvg())
assert float(svg.get("width")) == 400.0
@@ -42,8 +42,8 @@ def test_toSvg(project, controller):
def test_toSvg_negative_y(project, controller):
line = LineItem(dst=QtCore.QPointF(400, -280), project=project)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
pen.setStyle(QtCore.Qt.DashLine)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
pen.setStyle(QtCore.Qt.PenStyle.DashLine)
line.setPen(pen)
svg = ET.fromstring(line.toSvg())
assert float(svg.get("width")) == 400.0
@@ -69,7 +69,7 @@ def test_fromSvg(project, controller):
assert line.line().x2() == 250
assert line.line().y2() == 150
assert hex(line.pen().color().rgba()) == "0xff0000ff"
assert line.pen().style() == QtCore.Qt.DashDotLine
assert line.pen().style() == QtCore.Qt.PenStyle.DashDotLine
assert line.pos().x() == 50
assert line.pos().y() == 84
@@ -84,7 +84,7 @@ def test_fromSvg_top_direction(project, controller):
assert line.line().x2() == 250
assert line.line().y2() == 0
assert hex(line.pen().color().rgba()) == "0xff0000ff"
assert line.pen().style() == QtCore.Qt.DashDotLine
assert line.pen().style() == QtCore.Qt.PenStyle.DashDotLine
assert line.pos().x() == 50
assert line.pos().y() == 84
@@ -92,7 +92,7 @@ def test_fromSvg_top_direction(project, controller):
def test_fromSvg_solid_stroke(project, controller):
line = LineItem(project=project)
line.fromSvg('<svg height="150" width="250"><line x1="0" y1="0" x2="250" y2="150" stroke-width="5" stroke="#0000ff" /></svg>')
assert line.pen().style() == QtCore.Qt.SolidLine
assert line.pen().style() == QtCore.Qt.PenStyle.SolidLine
def test_fromEmptySvg(project, controller):

View File

@@ -23,8 +23,8 @@ from gns3.qt import QtGui, QtCore
def test_toSvg(project, controller):
rect = RectangleItem(width=400, height=280, project=project)
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
pen.setStyle(QtCore.Qt.DashLine)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.black, 2, QtCore.Qt.PenStyle.SolidLine, QtCore.Qt.PenCapStyle.RoundCap, QtCore.Qt.PenJoinStyle.RoundJoin)
pen.setStyle(QtCore.Qt.PenStyle.DashLine)
rect.setPen(pen)
svg = ET.fromstring(rect.toSvg())
assert float(svg.get("width")) == 400.0
@@ -49,13 +49,13 @@ def test_fromSvg(project, controller):
assert rect.pen().width() == 5
assert hex(rect.pen().color().rgba()) == "0xff0000ff"
assert hex(rect.brush().color().rgba()) == "0x80ff00ff"
assert rect.pen().style() == QtCore.Qt.DashDotLine
assert rect.pen().style() == QtCore.Qt.PenStyle.DashDotLine
def test_fromSvg_solid_stroke(project, controller):
rect = RectangleItem(project=project)
rect.fromSvg('<svg height="150" width="250"><rect height="150" stroke-width="5" stroke="#0000ff" fill="#ff00ff" width="150" /></svg>')
assert rect.pen().style() == QtCore.Qt.SolidLine
assert rect.pen().style() == QtCore.Qt.PenStyle.SolidLine
def test_fromEmptySvg(project, controller):

View File

@@ -46,7 +46,7 @@ def test_fromSvg(project, controller):
font.setItalic(True)
font.setStrikeOut(True)
text.setFont(font)
text.setDefaultTextColor(QtCore.Qt.red)
text.setDefaultTextColor(QtCore.Qt.GlobalColor.red)
text.setPlainText("Hello")
text2 = TextItem(project=project)

View File

@@ -36,7 +36,7 @@ def network_manager(response):
def response():
response = unittest.mock.MagicMock()
type(response).finished = unittest.mock.PropertyMock(return_value=FakeQtSignal())
response.error.return_value = QtNetwork.QNetworkReply.NoError
response.error.return_value = QtNetwork.QNetworkReply.NetworkError.NoError
response.attribute.return_value = 200
response.header.return_value = "application/json"
return response
@@ -115,7 +115,7 @@ def test_dataReadySlot(http_client):
callback = unittest.mock.MagicMock()
response = unittest.mock.MagicMock()
response.header.return_value = "application/json"
response.error.return_value = QtNetwork.QNetworkReply.NoError
response.error.return_value = QtNetwork.QNetworkReply.NetworkError.NoError
response.attribute.return_value = 200
response.readAll.return_value = b'{"action": "ping"}'
http_client._dataReadySlot(response, callback, {"query_id": "bla"})
@@ -130,7 +130,7 @@ def test_dataReadySlotHTTPError(http_client):
callback = unittest.mock.MagicMock()
response = unittest.mock.MagicMock()
response.header.return_value = "application/json"
response.error.return_value = QtNetwork.QNetworkReply.NoError
response.error.return_value = QtNetwork.QNetworkReply.NetworkError.NoError
response.attribute.return_value = 404
http_client._dataReadySlot(response, callback, {"query_id": "bla"})
assert not callback.called
@@ -141,7 +141,7 @@ def test_dataReadySlotConnectionRefusedError(http_client):
callback = unittest.mock.MagicMock()
response = unittest.mock.MagicMock()
response.header.return_value = "application/json"
response.error.return_value = QtNetwork.QNetworkReply.ConnectionRefusedError
response.error.return_value = QtNetwork.QNetworkReply.NetworkError.ConnectionRefusedError
response.attribute.return_value = 200
http_client._dataReadySlot(response, callback, {"query_id": "bla"})
assert not callback.called
@@ -155,7 +155,7 @@ def test_dataReadySlotPartialJSON(http_client):
response = unittest.mock.MagicMock()
response.header.return_value = "application/json"
response.readAll.return_value = b'{"action": "ping"'
response.error.return_value = QtNetwork.QNetworkReply.NoError
response.error.return_value = QtNetwork.QNetworkReply.NetworkError.NoError
response.attribute.return_value = 200
http_client._dataReadySlot(response, callback, {"query_id": "bla"})
assert not callback.called
@@ -172,7 +172,7 @@ def test_dataReadySlotPartialBytes(http_client):
response = unittest.mock.MagicMock()
response.header.return_value = "application/octet-stream"
response.readAll.return_value = b'hello'
response.error.return_value = QtNetwork.QNetworkReply.NoError
response.error.return_value = QtNetwork.QNetworkReply.NetworkError.NoError
response.attribute.return_value = 200
http_client._dataReadySlot(response, callback, {"query_id": "bla"})