qgis-profile-interpreter

qgis plugin for placing 3D points along elevation profiles
git clone git://src.adamsgaard.dk/qgis-profile-interpreter # fast
git clone https://src.adamsgaard.dk/qgis-profile-interpreter.git # slow
Log | Files | Refs | README | LICENSE Back to index

fakeqgis.py (14619B)


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