fakeqgis.py (14019B)
1 """ 2 Minimal fake qgis installed into sys.modules so that profile_interpreter 3 can be imported and unit-tested on a QGIS-free Python runtime (e.g. CI). 4 """ 5 import sys 6 from types import ModuleType 7 8 9 # ── PyQt stubs ──────────────────────────────────────────────────────────── 10 11 class Qt: 12 class MouseButton: 13 LeftButton = 1 14 RightButton = 2 15 LeftButton = 1 16 RightButton = 2 17 18 19 class QPointF: 20 def __init__(self, *args): 21 pass 22 23 24 class QMetaType: 25 class Type: 26 Int = 1 27 Double = 2 28 QString = 3 29 30 31 class QIcon: 32 def __init__(self, *args, **kwargs): 33 pass 34 35 36 class _FakeSignal: 37 def __init__(self): 38 self._callbacks = [] 39 40 def connect(self, cb): 41 self._callbacks.append(cb) 42 43 def emit(self, *args): 44 for cb in self._callbacks: 45 cb(*args) 46 47 48 class QAction: 49 def __init__(self, *args, **kwargs): 50 self._checkable = False 51 self._checked = False 52 self._enabled = True 53 self._tooltip = '' 54 self.toggled = _FakeSignal() 55 self.triggered = _FakeSignal() 56 57 def setCheckable(self, v): 58 self._checkable = v 59 60 def setChecked(self, v): 61 if v == self._checked: 62 return 63 self._checked = v 64 self.toggled.emit(v) 65 66 def isChecked(self): 67 return self._checked 68 69 def setEnabled(self, v): 70 self._enabled = v 71 72 def isEnabled(self): 73 return self._enabled 74 75 def setToolTip(self, v): 76 self._tooltip = v 77 78 79 class QToolBar: 80 def __init__(self, parent=None): 81 self.actions_added = [] 82 83 def addAction(self, action): 84 self.actions_added.append(action) 85 86 def removeAction(self, action): 87 if action in self.actions_added: 88 self.actions_added.remove(action) 89 90 91 # ── qgis.core stubs ─────────────────────────────────────────────────────── 92 93 class Qgis: 94 Warning = 1 95 Info = 2 96 Success = 3 97 98 99 class QgsApplication: 100 @staticmethod 101 def getThemeIcon(name): 102 return QIcon() 103 104 105 class QgsField: 106 def __init__(self, name, type_=None): 107 self._name = name 108 109 def name(self): 110 return self._name 111 112 113 class _FakeFields: 114 def __init__(self, fields): 115 self._fields = list(fields) 116 117 def __iter__(self): 118 return iter(self._fields) 119 120 def indexOf(self, name): 121 names = [f.name() for f in self._fields] 122 return names.index(name) if name in names else -1 123 124 125 class QgsFeature: 126 def __init__(self, fields=None): 127 self._fields = fields if fields is not None else _FakeFields([]) 128 self._attrs = {} 129 self._geom = None 130 self._id = -1 131 132 def id(self): 133 return self._id 134 135 def setId(self, fid): 136 self._id = fid 137 138 def fields(self): 139 return self._fields 140 141 def geometry(self): 142 return self._geom if self._geom is not None else QgsGeometry() 143 144 def setGeometry(self, geom): 145 self._geom = geom 146 147 def __setitem__(self, key, value): 148 self._attrs[key] = value 149 150 def __getitem__(self, key): 151 return self._attrs[key] 152 153 154 class _FakeGeometry: 155 def __init__(self, empty=False, x=100.0, y=200.0): 156 self._empty = empty 157 self._x = x 158 self._y = y 159 160 def isEmpty(self): 161 return self._empty 162 163 def asPoint(self): 164 return _FakeXY(self._x, self._y) 165 166 def clone(self): 167 return self 168 169 170 class _FakeXY: 171 def __init__(self, x=100.0, y=200.0): 172 self._x = x 173 self._y = y 174 175 def x(self): 176 return self._x 177 178 def y(self): 179 return self._y 180 181 182 class QgsPointXY: 183 def __init__(self, x=0.0, y=0.0): 184 self._x = x 185 self._y = y 186 187 def x(self): 188 return self._x 189 190 def y(self): 191 return self._y 192 193 194 class QgsGeometry: 195 # Set to True in tests to make interpolate() return an empty geometry. 196 _next_interpolate_empty = False 197 # Lateral offset of every point from the profile curve, as seen by distance(). 198 _lateral_distance = 0.0 199 200 def __init__(self, obj=None): 201 self._obj = obj 202 203 @classmethod 204 def set_interpolate_empty(cls, v): 205 cls._next_interpolate_empty = v 206 207 def interpolate(self, distance): 208 empty = QgsGeometry._next_interpolate_empty 209 return _FakeGeometry(empty=empty) 210 211 def isEmpty(self): 212 return False 213 214 def asPoint(self): 215 return _FakeXY() 216 217 def clone(self): 218 return self 219 220 def constGet(self): 221 return self._obj 222 223 def lineLocatePoint(self, point_geom): 224 return point_geom.constGet().x() 225 226 def distance(self, other): 227 return QgsGeometry._lateral_distance 228 229 230 class QgsPoint: 231 def __init__(self, x=0.0, y=0.0, z=0.0): 232 self._x = x 233 self._y = y 234 self._z = z 235 236 def x(self): 237 return self._x 238 239 def y(self): 240 return self._y 241 242 def z(self): 243 return self._z 244 245 246 class QgsProfilePoint: 247 def __init__(self, distance=0.0, elevation=0.0): 248 self._distance = distance 249 self._elevation = elevation 250 251 def distance(self): 252 return self._distance 253 254 def elevation(self): 255 return self._elevation 256 257 258 class QgsCoordinateTransform: 259 _calls = [] # (src_authid, dst_authid, in_x, in_y) 260 261 def __init__(self, src, dst, project): 262 self._src = src 263 self._dst = dst 264 265 def transform(self, pt): 266 QgsCoordinateTransform._calls.append( 267 (self._src.authid(), self._dst.authid(), pt.x(), pt.y()) 268 ) 269 return QgsPointXY(pt.x() + 1000.0, pt.y() + 1000.0) 270 271 @classmethod 272 def reset(cls): 273 cls._calls.clear() 274 275 276 class QgsMessageLog: 277 _log = [] 278 279 @classmethod 280 def logMessage(cls, msg, tag='', level=None): 281 cls._log.append((tag, msg, level)) 282 283 @classmethod 284 def reset(cls): 285 cls._log.clear() 286 287 288 class QgsWkbTypes: 289 PointZ = 1001 290 Point = 1 291 PointGeometry = 0 # PyQt5 style 292 LineGeometry = 1 293 PolygonGeometry = 2 294 295 class GeometryType: # PyQt6 style 296 PointGeometry = 0 297 LineGeometry = 1 298 PolygonGeometry = 2 299 300 @staticmethod 301 def hasZ(wkb_type): 302 return wkb_type == QgsWkbTypes.PointZ 303 304 305 class QgsVectorDataProvider: 306 AddFeatures = 1 # PyQt5 style 307 DeleteFeatures = 2 308 ChangeGeometries = 4 309 ChangeAttributeValues = 8 310 311 class Capability: # PyQt6 style 312 AddFeatures = 1 313 DeleteFeatures = 2 314 ChangeGeometries = 4 315 ChangeAttributeValues = 8 316 317 318 class _FakeDataProvider: 319 _fail_next = False # set True in a test to simulate addFeatures failure 320 _fail_next_delete = False # set True in a test to simulate deleteFeatures failure 321 _fail_next_change = False # set True in a test to simulate changeGeometryValues failure 322 323 def __init__(self, caps, features_store, fields_ref): 324 self._caps = caps 325 self._features = features_store 326 self._fields_ref = fields_ref 327 self._next_id = 1 328 329 def capabilities(self): 330 return self._caps 331 332 def addFeatures(self, features): 333 if _FakeDataProvider._fail_next: 334 _FakeDataProvider._fail_next = False 335 return (False, []) 336 for feature in features: 337 feature.setId(self._next_id) 338 self._next_id += 1 339 self._features.extend(features) 340 return (True, list(features)) 341 342 def deleteFeatures(self, ids): 343 if _FakeDataProvider._fail_next_delete: 344 _FakeDataProvider._fail_next_delete = False 345 return False 346 remaining = [f for f in self._features if f.id() not in ids] 347 self._features[:] = remaining 348 return True 349 350 def changeGeometryValues(self, geometries): 351 if _FakeDataProvider._fail_next_change: 352 _FakeDataProvider._fail_next_change = False 353 return False 354 for feature in self._features: 355 if feature.id() in geometries: 356 feature.setGeometry(geometries[feature.id()]) 357 return True 358 359 def changeAttributeValues(self, changes): 360 names = [f.name() for f in self._fields_ref] 361 for feature in self._features: 362 for idx, value in changes.get(feature.id(), {}).items(): 363 feature[names[idx]] = value 364 return True 365 366 def addAttributes(self, attrs): 367 self._fields_ref.extend(attrs) 368 369 370 class QgsVectorLayer: 371 def __init__(self, uri='', name='', provider=''): 372 self._name = name 373 self._features = [] 374 self._fields_list = [] 375 self._geom_type = QgsWkbTypes.PointGeometry 376 self._wkb_type = QgsWkbTypes.PointZ 377 self._provider = _FakeDataProvider( 378 QgsVectorDataProvider.AddFeatures | QgsVectorDataProvider.DeleteFeatures | 379 QgsVectorDataProvider.ChangeGeometries | 380 QgsVectorDataProvider.ChangeAttributeValues, 381 self._features, 382 self._fields_list, 383 ) 384 self.selected_ids = [] 385 386 def name(self): 387 return self._name 388 389 def id(self): 390 return id(self) 391 392 def geometryType(self): 393 return self._geom_type 394 395 def wkbType(self): 396 return self._wkb_type 397 398 def dataProvider(self): 399 return self._provider 400 401 def fields(self): 402 return _FakeFields(self._fields_list) 403 404 def crs(self): 405 return getattr(self, '_crs', _FakeCrs()) 406 407 def getFeatures(self): 408 return iter(self._features) 409 410 def selectByIds(self, ids): 411 self.selected_ids = list(ids) 412 413 def removeSelection(self): 414 self.selected_ids = [] 415 416 def updateExtents(self): 417 pass 418 419 def triggerRepaint(self): 420 pass 421 422 def updateFields(self): 423 pass 424 425 426 class QgsProject: 427 _instance = None 428 429 def __init__(self): 430 self._layers = {} 431 432 @classmethod 433 def instance(cls): 434 if cls._instance is None: 435 cls._instance = cls() 436 return cls._instance 437 438 def mapLayer(self, layer_id): 439 return self._layers.get(layer_id) 440 441 def addMapLayer(self, layer): 442 self._layers[layer.id()] = layer 443 return layer 444 445 446 # ── qgis.gui stubs ──────────────────────────────────────────────────────── 447 448 class QgsPlotTool: 449 def __init__(self, canvas, name): 450 self._canvas = canvas 451 self._name = name 452 453 def plotReleaseEvent(self, event): 454 pass 455 456 457 class QgsPlotToolPan: 458 def __init__(self, canvas): 459 self._canvas = canvas 460 461 462 class QgsElevationProfileCanvas: 463 def __init__(self): 464 self._visible = True 465 self.refresh_count = 0 466 self._tool = None 467 468 def isVisible(self): 469 return self._visible 470 471 def parentWidget(self): 472 return getattr(self, '_parent', None) 473 474 def setTool(self, tool): 475 self._tool = tool 476 477 def refresh(self): 478 self.refresh_count += 1 479 480 def canvasPointToPlotPoint(self, pt): 481 return _FakeProfilePoint() 482 483 def plotPointToCanvasPoint(self, point): 484 return QgsPointXY(point.distance(), point.elevation()) 485 486 def profileCurve(self): 487 return _FakeCurve() 488 489 def crs(self): 490 return _FakeCrs() 491 492 493 class _FakeProfilePoint: 494 def __init__(self, distance=10.0, elevation=5.0): 495 self._distance = distance 496 self._elevation = elevation 497 498 def distance(self): 499 return self._distance 500 501 def elevation(self): 502 return self._elevation 503 504 505 class _FakeCurve: 506 def clone(self): 507 return self 508 509 510 class _FakeCrs: 511 def __init__(self, authid='EPSG:4326'): 512 self._authid = authid 513 514 def isValid(self): 515 return True 516 517 def authid(self): 518 return self._authid 519 520 def __eq__(self, other): 521 if not isinstance(other, _FakeCrs): 522 return NotImplemented 523 return self._authid == other._authid 524 525 def __hash__(self): 526 return hash(self._authid) 527 528 529 # ── installer ───────────────────────────────────────────────────────────── 530 531 def install(): 532 """Register all fake qgis sub-modules into sys.modules.""" 533 qgis = ModuleType('qgis') 534 qgis_core = ModuleType('qgis.core') 535 qgis_gui = ModuleType('qgis.gui') 536 qgis_pyqt = ModuleType('qgis.PyQt') 537 qgis_pyqt_qtcore = ModuleType('qgis.PyQt.QtCore') 538 qgis_pyqt_qtgui = ModuleType('qgis.PyQt.QtGui') 539 qgis_pyqt_qtwidgets = ModuleType('qgis.PyQt.QtWidgets') 540 541 for name in [ 542 'Qgis', 'QgsApplication', 'QgsCoordinateTransform', 'QgsFeature', 'QgsField', 543 'QgsGeometry', 'QgsMessageLog', 'QgsPoint', 'QgsPointXY', 'QgsProfilePoint', 544 'QgsProject', 'QgsVectorDataProvider', 'QgsVectorLayer', 'QgsWkbTypes', 545 ]: 546 setattr(qgis_core, name, globals()[name]) 547 548 for name in ['QgsElevationProfileCanvas', 'QgsPlotTool', 'QgsPlotToolPan']: 549 setattr(qgis_gui, name, globals()[name]) 550 551 qgis_pyqt_qtcore.Qt = Qt 552 qgis_pyqt_qtcore.QPointF = QPointF 553 qgis_pyqt_qtcore.QMetaType = QMetaType 554 qgis_pyqt_qtgui.QIcon = QIcon 555 qgis_pyqt_qtwidgets.QAction = QAction 556 qgis_pyqt_qtwidgets.QToolBar = QToolBar 557 558 sys.modules['qgis'] = qgis 559 sys.modules['qgis.core'] = qgis_core 560 sys.modules['qgis.gui'] = qgis_gui 561 sys.modules['qgis.PyQt'] = qgis_pyqt 562 sys.modules['qgis.PyQt.QtCore'] = qgis_pyqt_qtcore 563 sys.modules['qgis.PyQt.QtGui'] = qgis_pyqt_qtgui 564 sys.modules['qgis.PyQt.QtWidgets'] = qgis_pyqt_qtwidgets 565 566 qgis.core = qgis_core 567 qgis.gui = qgis_gui 568 qgis.PyQt = qgis_pyqt 569 qgis_pyqt.QtCore = qgis_pyqt_qtcore 570 qgis_pyqt.QtGui = qgis_pyqt_qtgui 571 qgis_pyqt.QtWidgets = qgis_pyqt_qtwidgets 572 573 # Reset project singleton for each test run. 574 QgsProject._instance = None