Skip to content

Commit bb79f80

Browse files
committed
Addon Manager: Linter cleanup of new test code
1 parent ab2c8cf commit bb79f80

File tree

3 files changed

+74
-31
lines changed

3 files changed

+74
-31
lines changed

src/Mod/AddonManager/AddonManagerTest/gui/test_change_branch.py

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@
2121
# * *
2222
# ***************************************************************************
2323

24+
""" Test the Change Branch GUI code"""
25+
26+
# pylint: disable=wrong-import-position, deprecated-module, too-many-return-statements
27+
2428
import sys
2529
import unittest
2630
from unittest.mock import patch, Mock, MagicMock
2731

2832
# So that when run standalone, the Addon Manager classes imported below are available
2933
sys.path.append("../..")
3034

31-
from AddonManagerTest.gui.gui_mocks import DialogWatcher, DialogInteractor, AsynchronousMonitor
35+
from AddonManagerTest.gui.gui_mocks import DialogWatcher, AsynchronousMonitor
3236

3337
from change_branch import ChangeBranchDialog
3438

@@ -45,11 +49,14 @@
4549

4650

4751
class MockFilter(QtCore.QSortFilterProxyModel):
52+
"""Replaces a filter with a non-filter that simply always returns whatever it's given"""
53+
4854
def mapToSource(self, something):
4955
return something
5056

5157

5258
class MockChangeBranchDialogModel(QtCore.QAbstractTableModel):
59+
"""Replace a data-connected model with a static one for testing"""
5360

5461
branches = [
5562
{"ref_name": "ref1", "upstream": "us1"},
@@ -64,40 +71,45 @@ def __init__(self, _: str, parent=None) -> None:
6471
super().__init__(parent)
6572

6673
def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
74+
"""Number of rows: should always return 3"""
6775
if parent.isValid():
6876
return 0
6977
return len(self.branches)
7078

7179
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
80+
"""Number of columns (identical to non-mocked version)"""
7281
if parent.isValid():
7382
return 0
7483
return 3 # Local name, remote name, date
7584

7685
def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole):
86+
"""Mock returns static untranslated strings for DisplayRole, no tooltips at all, and
87+
otherwise matches the non-mock version"""
7788
if not index.isValid():
7889
return None
7990
row = index.row()
8091
column = index.column()
8192
if role == QtCore.Qt.DisplayRole:
8293
if column == 2:
8394
return "date"
84-
elif column == 0:
95+
if column == 0:
8596
return "ref_name"
86-
elif column == 1:
97+
if column == 1:
8798
return "upstream"
88-
else:
89-
return None
90-
elif role == MockChangeBranchDialogModel.DataSortRole:
9199
return None
92-
elif role == MockChangeBranchDialogModel.RefAccessRole:
100+
if role == MockChangeBranchDialogModel.DataSortRole:
101+
return None
102+
if role == MockChangeBranchDialogModel.RefAccessRole:
93103
return self.branches[row]
104+
return None
94105

95106
def headerData(
96107
self,
97108
section: int,
98109
orientation: QtCore.Qt.Orientation,
99110
role: int = QtCore.Qt.DisplayRole,
100111
):
112+
"""Mock returns untranslated strings for DisplayRole, and no tooltips at all"""
101113
if orientation == QtCore.Qt.Vertical:
102114
return None
103115
if role != QtCore.Qt.DisplayRole:
@@ -106,16 +118,18 @@ def headerData(
106118
return "Local"
107119
if section == 1:
108120
return "Remote tracking"
109-
elif section == 2:
121+
if section == 2:
110122
return "Last Updated"
111-
else:
112-
return None
123+
return None
113124

114125
def currentBranch(self) -> str:
126+
"""Mock returns a static string stored in the class: that string could be modified to
127+
return something else by tests that require it."""
115128
return self.current_branch
116129

117130

118131
class TestChangeBranchGui(unittest.TestCase):
132+
"""Tests for the ChangeBranch GUI code"""
119133

120134
MODULE = "test_change_branch" # file name without extension
121135

@@ -128,6 +142,7 @@ def tearDown(self):
128142
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
129143
@patch("change_branch.initialize_git", new=Mock(return_value=None))
130144
def test_no_git(self):
145+
"""If git is not present, a dialog saying so is presented"""
131146
# Arrange
132147
gui = ChangeBranchDialog("/some/path")
133148
ref = {"ref_name": "foo/bar", "upstream": "us1"}
@@ -137,14 +152,15 @@ def test_no_git(self):
137152
)
138153

139154
# Act
140-
gui._change_branch("/foo/bar/baz", ref)
155+
gui.change_branch("/foo/bar/baz", ref)
141156

142157
# Assert
143158
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
144159

145160
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
146161
@patch("change_branch.initialize_git")
147162
def test_git_failed(self, init_git: MagicMock):
163+
"""If git fails when attempting to change branches, a dialog saying so is presented"""
148164
# Arrange
149165
git_manager = MagicMock()
150166
git_manager.checkout = MagicMock()
@@ -158,24 +174,24 @@ def test_git_failed(self, init_git: MagicMock):
158174
)
159175

160176
# Act
161-
gui._change_branch("/foo/bar/baz", ref)
177+
gui.change_branch("/foo/bar/baz", ref)
162178

163179
# Assert
164180
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
165181

166182
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
167183
@patch("change_branch.initialize_git", new=MagicMock)
168184
def test_branch_change_succeeded(self):
169-
# If nothing gets thrown, then the process is assumed to have worked, and the appropriate
170-
# signal is emitted.
185+
"""If nothing gets thrown, then the process is assumed to have worked, and the appropriate
186+
signal is emitted."""
171187

172188
# Arrange
173189
gui = ChangeBranchDialog("/some/path")
174190
ref = {"ref_name": "foo/bar", "upstream": "us1"}
175191
monitor = AsynchronousMonitor(gui.branch_changed)
176192

177193
# Act
178-
gui._change_branch("/foo/bar/baz", ref)
194+
gui.change_branch("/foo/bar/baz", ref)
179195

180196
# Assert
181197
monitor.wait_for_at_most(10) # Should be effectively instantaneous
@@ -185,6 +201,8 @@ def test_branch_change_succeeded(self):
185201
@patch("change_branch.ChangeBranchDialogModel", new=MockChangeBranchDialogModel)
186202
@patch("change_branch.initialize_git", new=MagicMock)
187203
def test_warning_is_shown_when_dialog_is_accepted(self):
204+
"""If the dialog is accepted (e.g. a branch change is requested) then a warning dialog is
205+
displayed, and gives the opportunity to cancel. If cancelled, no signal is emitted."""
188206
# Arrange
189207
gui = ChangeBranchDialog("/some/path")
190208
gui.ui.exec = MagicMock()
@@ -197,12 +215,14 @@ def test_warning_is_shown_when_dialog_is_accepted(self):
197215
translate("AddonsInstaller", "DANGER: Developer feature"),
198216
QtWidgets.QDialogButtonBox.Cancel,
199217
)
218+
monitor = AsynchronousMonitor(gui.branch_changed)
200219

201220
# Act
202221
gui.exec()
203222

204223
# Assert
205224
self.assertTrue(dialog_watcher.dialog_found, "Failed to find the expected dialog box")
225+
self.assertFalse(monitor.good()) # The watcher cancelled the op, so no signal is emitted
206226

207227

208228
if __name__ == "__main__":

src/Mod/AddonManager/addonmanager_utilities.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
""" Utilities to work across different platforms, providers and python versions """
2626

27+
# pylint: disable=deprecated-module, ungrouped-imports
28+
2729
from datetime import datetime
2830
from typing import Optional, Any, List
2931
import os
@@ -52,6 +54,7 @@
5254
except ImportError:
5355

5456
def get_python_exe():
57+
"""Use shutil.which to find python executable"""
5558
return shutil.which("python")
5659

5760

@@ -61,9 +64,17 @@ def get_python_exe():
6164
# loop running this is not possible, so fall back to requests (if available), or the native
6265
# Python urllib.request (if requests is not available).
6366
import NetworkManager # Requires an event loop, so is only available with the GUI
67+
68+
requests = None
69+
ssl = None
70+
urllib = None
6471
else:
72+
NetworkManager = None
6573
try:
6674
import requests
75+
76+
ssl = None
77+
urllib = None
6778
except ImportError:
6879
requests = None
6980
import urllib.request
@@ -85,11 +96,14 @@ def get_python_exe():
8596
except ImportError:
8697

8798
def loadUi(ui_file: str):
99+
"""If there are no available versions of QtUiTools, then raise an error if this
100+
method is used."""
88101
raise RuntimeError("Cannot use QUiLoader without PySide or FreeCAD")
89102

90103
if has_loader:
91104

92105
def loadUi(ui_file: str) -> QtWidgets.QWidget:
106+
"""Load a Qt UI from an on-disk file."""
93107
q_ui_file = QtCore.QFile(ui_file)
94108
q_ui_file.open(QtCore.QFile.OpenModeFlag.ReadOnly)
95109
loader = QUiLoader()
@@ -135,10 +149,13 @@ def symlink(source, link_name):
135149

136150

137151
def rmdir(path: str) -> bool:
152+
"""Remove a directory or symlink, even if it is read-only."""
138153
try:
139154
if os.path.islink(path):
140155
os.unlink(path) # Remove symlink
141156
else:
157+
# NOTE: the onerror argument was deprecated in Python 3.12, replaced by onexc -- replace
158+
# when earlier versions are no longer supported.
142159
shutil.rmtree(path, onerror=remove_readonly)
143160
except (WindowsError, PermissionError, OSError):
144161
return False
@@ -213,7 +230,7 @@ def get_zip_url(repo):
213230

214231

215232
def recognized_git_location(repo) -> bool:
216-
"""Returns whether this repo is based at a known git repo location: works with github, gitlab,
233+
"""Returns whether this repo is based at a known git repo location: works with GitHub, gitlab,
217234
framagit, and salsa.debian.org"""
218235

219236
parsed_url = urlparse(repo.url)
@@ -395,7 +412,7 @@ def is_float(element: Any) -> bool:
395412

396413

397414
def get_pip_target_directory():
398-
# Get the default location to install new pip packages
415+
"""Get the default location to install new pip packages"""
399416
major, minor, _ = platform.python_version_tuple()
400417
vendor_path = os.path.join(
401418
fci.DataPaths().mod_dir, "..", "AdditionalPythonPackages", f"py{major}{minor}"
@@ -417,7 +434,12 @@ def blocking_get(url: str, method=None) -> bytes:
417434
succeeded, or an empty string if it failed, or returned no data. The method argument is
418435
provided mainly for testing purposes."""
419436
p = b""
420-
if fci.FreeCADGui and method is None or method == "networkmanager":
437+
if (
438+
fci.FreeCADGui
439+
and method is None
440+
or method == "networkmanager"
441+
and NetworkManager is not None
442+
):
421443
NetworkManager.InitializeNetworkManager()
422444
p = NetworkManager.AM_NETWORK_MANAGER.blocking_get(url, 10000) # 10 second timeout
423445
if p:
@@ -462,13 +484,13 @@ def run_interruptable_subprocess(args, timeout_secs: int = 10) -> subprocess.Com
462484
# one second timeout allows interrupting the run once per second
463485
stdout, stderr = p.communicate(timeout=1)
464486
return_code = p.returncode
465-
except subprocess.TimeoutExpired:
487+
except subprocess.TimeoutExpired as timeout_exception:
466488
if (
467489
hasattr(QtCore, "QThread")
468490
and QtCore.QThread.currentThread().isInterruptionRequested()
469491
):
470492
p.kill()
471-
raise ProcessInterrupted()
493+
raise ProcessInterrupted() from timeout_exception
472494
if time.time() - start_time >= timeout_secs: # The real timeout
473495
p.kill()
474496
stdout, stderr = p.communicate()
@@ -481,9 +503,10 @@ def run_interruptable_subprocess(args, timeout_secs: int = 10) -> subprocess.Com
481503

482504

483505
def process_date_string_to_python_datetime(date_string: str) -> datetime:
484-
"""For modern macros the expected date format is ISO 8601, YYYY-MM-DD. For older macros this standard was not always
485-
used, and various orderings and separators were used. This function tries to match the majority of those older
486-
macros. Commonly-used separators are periods, slashes, and dashes."""
506+
"""For modern macros the expected date format is ISO 8601, YYYY-MM-DD. For older macros this
507+
standard was not always used, and various orderings and separators were used. This function
508+
tries to match the majority of those older macros. Commonly-used separators are periods,
509+
slashes, and dashes."""
487510

488511
def raise_error(bad_string: str, root_cause: Exception = None):
489512
raise ValueError(
@@ -499,19 +522,19 @@ def raise_error(bad_string: str, root_cause: Exception = None):
499522
# The earliest possible year an addon can be created or edited is 2001:
500523
if split_result[0] > 2000:
501524
return datetime(split_result[0], split_result[1], split_result[2])
502-
elif split_result[2] > 2000:
503-
# Generally speaking it's not possible to distinguish between DD-MM and MM-DD, so try the first, and
504-
# only if that fails try the second
525+
if split_result[2] > 2000:
526+
# Generally speaking it's not possible to distinguish between DD-MM and MM-DD, so try
527+
# the first, and only if that fails try the second
505528
if split_result[1] <= 12:
506529
return datetime(split_result[2], split_result[1], split_result[0])
507530
return datetime(split_result[2], split_result[0], split_result[1])
508-
else:
509-
raise ValueError(f"Invalid year in date string '{date_string}'")
531+
raise ValueError(f"Invalid year in date string '{date_string}'")
510532
except ValueError as exception:
511533
raise_error(date_string, exception)
512534

513535

514536
def get_main_am_window():
537+
"""Find the Addon Manager's main window in the Qt widget hierarchy."""
515538
windows = QtWidgets.QApplication.topLevelWidgets()
516539
for widget in windows:
517540
if widget.objectName() == "AddonManager_Main_Window":
@@ -540,7 +563,7 @@ def create_pip_call(args: List[str]) -> List[str]:
540563
else:
541564
python_exe = get_python_exe()
542565
if not python_exe:
543-
raise (RuntimeError("Could not locate Python executable on this system"))
566+
raise RuntimeError("Could not locate Python executable on this system")
544567
call_args = [python_exe, "-m", "pip", "--disable-pip-version-check"]
545568
call_args.extend(args)
546569
return call_args

src/Mod/AddonManager/change_branch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ def exec(self):
120120
if result == QtWidgets.QMessageBox.Cancel:
121121
return
122122

123-
self._change_branch(self.item_model.path, ref)
123+
self.change_branch(self.item_model.path, ref)
124124

125-
def _change_branch(self, path: str, ref: Dict[str, str]) -> None:
125+
def change_branch(self, path: str, ref: Dict[str, str]) -> None:
126126
"""Change the git clone in `path` to git ref `ref`. Emits the branch_changed signal
127127
on success."""
128128
remote_name = ref["ref_name"]

0 commit comments

Comments
 (0)