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