fakeqgis.py (10081B)
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 QAction: 37 def __init__(self, *args, **kwargs): 38 self._checkable = False 39 self._checked = False 40 self._callbacks = [] 41 42 def setCheckable(self, v): 43 self._checkable = v 44 45 def setChecked(self, v): 46 self._checked = v 47 48 def toggled(self): 49 pass 50 51 def connect(self, cb): 52 self._callbacks.append(cb) 53 54 55 # ── qgis.core stubs ─────────────────────────────────────────────────────── 56 57 class Qgis: 58 Warning = 1 59 Info = 2 60 Success = 3 61 62 63 class QgsField: 64 def __init__(self, name, type_=None): 65 self._name = name 66 67 def name(self): 68 return self._name 69 70 71 class _FakeFields: 72 def __init__(self, fields): 73 self._fields = list(fields) 74 75 def __iter__(self): 76 return iter(self._fields) 77 78 79 class QgsFeature: 80 def __init__(self, fields=None): 81 self._fields = fields if fields is not None else _FakeFields([]) 82 self._attrs = {} 83 self._geom = None 84 85 def fields(self): 86 return self._fields 87 88 def setGeometry(self, geom): 89 self._geom = geom 90 91 def __setitem__(self, key, value): 92 self._attrs[key] = value 93 94 def __getitem__(self, key): 95 return self._attrs[key] 96 97 98 class _FakeGeometry: 99 def __init__(self, empty=False, x=100.0, y=200.0): 100 self._empty = empty 101 self._x = x 102 self._y = y 103 104 def isEmpty(self): 105 return self._empty 106 107 def asPoint(self): 108 return _FakeXY(self._x, self._y) 109 110 def clone(self): 111 return self 112 113 114 class _FakeXY: 115 def __init__(self, x=100.0, y=200.0): 116 self._x = x 117 self._y = y 118 119 def x(self): 120 return self._x 121 122 def y(self): 123 return self._y 124 125 126 class QgsPointXY: 127 def __init__(self, x=0.0, y=0.0): 128 self._x = x 129 self._y = y 130 131 def x(self): 132 return self._x 133 134 def y(self): 135 return self._y 136 137 138 class QgsGeometry: 139 # Set to True in tests to make interpolate() return an empty geometry. 140 _next_interpolate_empty = False 141 142 def __init__(self, obj=None): 143 self._obj = obj 144 145 @classmethod 146 def set_interpolate_empty(cls, v): 147 cls._next_interpolate_empty = v 148 149 def interpolate(self, distance): 150 empty = QgsGeometry._next_interpolate_empty 151 return _FakeGeometry(empty=empty) 152 153 def isEmpty(self): 154 return False 155 156 def asPoint(self): 157 return _FakeXY() 158 159 def clone(self): 160 return self 161 162 163 class QgsPoint: 164 def __init__(self, x=0.0, y=0.0, z=0.0): 165 self._x = x 166 self._y = y 167 self._z = z 168 169 170 class QgsCoordinateTransform: 171 _calls = [] # (src_authid, dst_authid, in_x, in_y) 172 173 def __init__(self, src, dst, project): 174 self._src = src 175 self._dst = dst 176 177 def transform(self, pt): 178 QgsCoordinateTransform._calls.append( 179 (self._src.authid(), self._dst.authid(), pt.x(), pt.y()) 180 ) 181 return QgsPointXY(pt.x() + 1000.0, pt.y() + 1000.0) 182 183 @classmethod 184 def reset(cls): 185 cls._calls.clear() 186 187 188 class QgsMessageLog: 189 _log = [] 190 191 @classmethod 192 def logMessage(cls, msg, tag='', level=None): 193 cls._log.append((tag, msg, level)) 194 195 @classmethod 196 def reset(cls): 197 cls._log.clear() 198 199 200 class QgsWkbTypes: 201 PointZ = 1001 202 Point = 1 203 PointGeometry = 0 # PyQt5 style 204 LineGeometry = 1 205 PolygonGeometry = 2 206 207 class GeometryType: # PyQt6 style 208 PointGeometry = 0 209 LineGeometry = 1 210 PolygonGeometry = 2 211 212 @staticmethod 213 def hasZ(wkb_type): 214 return wkb_type == QgsWkbTypes.PointZ 215 216 217 class QgsVectorDataProvider: 218 AddFeatures = 1 # PyQt5 style 219 220 class Capability: # PyQt6 style 221 AddFeatures = 1 222 223 224 class _FakeDataProvider: 225 _fail_next = False # set True in a test to simulate addFeatures failure 226 227 def __init__(self, caps, features_store, fields_ref): 228 self._caps = caps 229 self._features = features_store 230 self._fields_ref = fields_ref 231 232 def capabilities(self): 233 return self._caps 234 235 def addFeatures(self, features): 236 if _FakeDataProvider._fail_next: 237 _FakeDataProvider._fail_next = False 238 return (False, []) 239 self._features.extend(features) 240 return (True, list(features)) 241 242 def addAttributes(self, attrs): 243 self._fields_ref.extend(attrs) 244 245 246 class QgsVectorLayer: 247 def __init__(self, uri='', name='', provider=''): 248 self._name = name 249 self._features = [] 250 self._fields_list = [] 251 self._geom_type = QgsWkbTypes.PointGeometry 252 self._wkb_type = QgsWkbTypes.PointZ 253 self._provider = _FakeDataProvider( 254 QgsVectorDataProvider.AddFeatures, 255 self._features, 256 self._fields_list, 257 ) 258 259 def name(self): 260 return self._name 261 262 def id(self): 263 return id(self) 264 265 def geometryType(self): 266 return self._geom_type 267 268 def wkbType(self): 269 return self._wkb_type 270 271 def dataProvider(self): 272 return self._provider 273 274 def fields(self): 275 return _FakeFields(self._fields_list) 276 277 def crs(self): 278 return getattr(self, '_crs', _FakeCrs()) 279 280 def getFeatures(self): 281 return iter(self._features) 282 283 def updateExtents(self): 284 pass 285 286 def triggerRepaint(self): 287 pass 288 289 def updateFields(self): 290 pass 291 292 293 class QgsProject: 294 _instance = None 295 296 def __init__(self): 297 self._layers = {} 298 299 @classmethod 300 def instance(cls): 301 if cls._instance is None: 302 cls._instance = cls() 303 return cls._instance 304 305 def mapLayer(self, layer_id): 306 return self._layers.get(layer_id) 307 308 def addMapLayer(self, layer): 309 self._layers[layer.id()] = layer 310 return layer 311 312 313 # ── qgis.gui stubs ──────────────────────────────────────────────────────── 314 315 class QgsPlotTool: 316 def __init__(self, canvas, name): 317 self._canvas = canvas 318 self._name = name 319 320 def plotReleaseEvent(self, event): 321 pass 322 323 324 class QgsPlotToolPan: 325 def __init__(self, canvas): 326 self._canvas = canvas 327 328 329 class QgsElevationProfileCanvas: 330 def __init__(self): 331 self._visible = True 332 self.refresh_count = 0 333 self._tool = None 334 335 def isVisible(self): 336 return self._visible 337 338 def setTool(self, tool): 339 self._tool = tool 340 341 def refresh(self): 342 self.refresh_count += 1 343 344 def canvasPointToPlotPoint(self, pt): 345 return _FakeProfilePoint() 346 347 def profileCurve(self): 348 return _FakeCurve() 349 350 def crs(self): 351 return _FakeCrs() 352 353 354 class _FakeProfilePoint: 355 def __init__(self, distance=10.0, elevation=5.0): 356 self._distance = distance 357 self._elevation = elevation 358 359 def distance(self): 360 return self._distance 361 362 def elevation(self): 363 return self._elevation 364 365 366 class _FakeCurve: 367 def clone(self): 368 return self 369 370 371 class _FakeCrs: 372 def __init__(self, authid='EPSG:4326'): 373 self._authid = authid 374 375 def isValid(self): 376 return True 377 378 def authid(self): 379 return self._authid 380 381 def __eq__(self, other): 382 if not isinstance(other, _FakeCrs): 383 return NotImplemented 384 return self._authid == other._authid 385 386 def __hash__(self): 387 return hash(self._authid) 388 389 390 # ── installer ───────────────────────────────────────────────────────────── 391 392 def install(): 393 """Register all fake qgis sub-modules into sys.modules.""" 394 qgis = ModuleType('qgis') 395 qgis_core = ModuleType('qgis.core') 396 qgis_gui = ModuleType('qgis.gui') 397 qgis_pyqt = ModuleType('qgis.PyQt') 398 qgis_pyqt_qtcore = ModuleType('qgis.PyQt.QtCore') 399 qgis_pyqt_qtgui = ModuleType('qgis.PyQt.QtGui') 400 qgis_pyqt_qtwidgets = ModuleType('qgis.PyQt.QtWidgets') 401 402 for name in [ 403 'Qgis', 'QgsCoordinateTransform', 'QgsFeature', 'QgsField', 404 'QgsGeometry', 'QgsMessageLog', 'QgsPoint', 'QgsPointXY', 405 'QgsProject', 'QgsVectorDataProvider', 'QgsVectorLayer', 'QgsWkbTypes', 406 ]: 407 setattr(qgis_core, name, globals()[name]) 408 409 for name in ['QgsElevationProfileCanvas', 'QgsPlotTool', 'QgsPlotToolPan']: 410 setattr(qgis_gui, name, globals()[name]) 411 412 qgis_pyqt_qtcore.Qt = Qt 413 qgis_pyqt_qtcore.QPointF = QPointF 414 qgis_pyqt_qtcore.QMetaType = QMetaType 415 qgis_pyqt_qtgui.QIcon = QIcon 416 qgis_pyqt_qtwidgets.QAction = QAction 417 418 sys.modules['qgis'] = qgis 419 sys.modules['qgis.core'] = qgis_core 420 sys.modules['qgis.gui'] = qgis_gui 421 sys.modules['qgis.PyQt'] = qgis_pyqt 422 sys.modules['qgis.PyQt.QtCore'] = qgis_pyqt_qtcore 423 sys.modules['qgis.PyQt.QtGui'] = qgis_pyqt_qtgui 424 sys.modules['qgis.PyQt.QtWidgets'] = qgis_pyqt_qtwidgets 425 426 qgis.core = qgis_core 427 qgis.gui = qgis_gui 428 qgis.PyQt = qgis_pyqt 429 qgis_pyqt.QtCore = qgis_pyqt_qtcore 430 qgis_pyqt.QtGui = qgis_pyqt_qtgui 431 qgis_pyqt.QtWidgets = qgis_pyqt_qtwidgets 432 433 # Reset project singleton for each test run. 434 QgsProject._instance = None