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 (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