• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-MSS / MSS / 15876623240

25 Jun 2025 12:41PM UTC coverage: 69.899% (+0.03%) from 69.872%
15876623240

Pull #2830

github

web-flow
Merge 412cf3942 into 001cc76b1
Pull Request #2830: added a special handling for path strings

8 of 12 new or added lines in 1 file covered. (66.67%)

15 existing lines in 2 files now uncovered.

14483 of 20720 relevant lines covered (69.9%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.04
/mslib/utils/config.py
1
# -*- coding: utf-8 -*-
2
"""
3

4
    mslib.utils.config
5
    ~~~~~~~~~~~~~~~~
6

7
    Collection of functions all around config handling.
8

9
    This file is part of MSS.
10

11
    :copyright: Copyright 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
12
    :copyright: Copyright 2011-2014 Marc Rautenhaus (mr)
13
    :copyright: Copyright 2016-2025 by the MSS team, see AUTHORS.
14
    :license: APACHE-2.0, see LICENSE for details.
15

16
    Licensed under the Apache License, Version 2.0 (the "License");
17
    you may not use this file except in compliance with the License.
18
    You may obtain a copy of the License at
19

20
       http://www.apache.org/licenses/LICENSE-2.0
21

22
    Unless required by applicable law or agreed to in writing, software
23
    distributed under the License is distributed on an "AS IS" BASIS,
24
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
    See the License for the specific language governing permissions and
26
    limitations under the License.
27
"""
28
from PyQt5 import QtCore
1✔
29

30
import copy
1✔
31
import json
1✔
32
import logging
1✔
33
import fs
1✔
34
import os
1✔
35

36
from mslib.utils import FatalUserError
1✔
37
from mslib.msui import constants
1✔
38
from mslib.support.qt_json_view.datatypes import match_type, UrlType, StrType
1✔
39

40

41
class MSUIDefaultConfig:
1✔
42
    """Central configuration for the Mission Support System User Interface
43
       Application (msui).
44

45
    DESCRIPTION:
46
    ============
47

48
    This file includes configuration settings central to the entire
49
    Mission Support User Interface (msui). Among others, define
50
     -- available map projections
51
     -- vertical section interpolation options
52
     -- the lists of predefined web service URLs
53
     -- predefined waypoints for the table view
54
    in this file.
55

56
    Do not change any value for good reasons.
57
    Your values can be set in your personal msui_settings.json file
58
    """
59
    # this skips the verification of the user token on each mscolab request
60
    mscolab_skip_verify_user_token = True
1✔
61

62
    # Default for general filepicker. Pick "default", "qt", or "fs"
63
    filepicker_default = "default"
1✔
64

65
    # dir where msui output files are stored
66
    data_dir = "~/mssdata"
1✔
67

68
    # layout of different views, with immutable they can't resized
69
    layout = {"topview": [963, 702],
1✔
70
              "sideview": [913, 557],
71
              "linearview": [913, 557],
72
              "tableview": [1236, 424],
73
              "immutable": False}
74

75
    # Predefined map regions to be listed in the corresponding topview combobox
76
    predefined_map_sections = {
1✔
77
        "00 global (cyl)": {
78
            "CRS": "EPSG:4326",
79
            "map": {
80
                "llcrnrlon": -180.0,
81
                "llcrnrlat": -90.0,
82
                "urcrnrlon": 180.0,
83
                "urcrnrlat": 90.0
84
            }
85
        },
86
        "01 SADPAP (stereo)": {
87
            "CRS": "EPSG:77890290",
88
            "map": {
89
                "llcrnrlon": -150.0,
90
                "llcrnrlat": -45.0,
91
                "urcrnrlon": -25.0,
92
                "urcrnrlat": -20.0
93
            }
94
        },
95
        "02 SADPAP zoom (stereo)": {
96
            "CRS": "EPSG:77890290",
97
            "map": {
98
                "llcrnrlon": -120.0,
99
                "llcrnrlat": -65.0,
100
                "urcrnrlon": -45.0,
101
                "urcrnrlat": -28.0
102
            }
103
        },
104
        "03 SADPAP (cyl)": {
105
            "CRS": "EPSG:4326",
106
            "map": {
107
                "llcrnrlon": -100.0,
108
                "llcrnrlat": -75.0,
109
                "urcrnrlon": -30.0,
110
                "urcrnrlat": -30.0
111
            }
112
        },
113
        "04 Southern Hemisphere (stereo)": {
114
            "CRS": "EPSG:77889270",
115
            "map": {
116
                "llcrnrlon": 135.0,
117
                "llcrnrlat": 0.0,
118
                "urcrnrlon": -45.0,
119
                "urcrnrlat": 0.0
120
            }
121
        },
122
        "05 EDMO-SAL (cyl)": {
123
            "CRS": "EPSG:4326",
124
            "map": {
125
                "llcrnrlon": -40,
126
                "llcrnrlat": 10,
127
                "urcrnrlon": 30,
128
                "urcrnrlat": 60
129
            }
130
        },
131
        "06 SAL-BA (cyl)": {
132
            "CRS": "EPSG:4326",
133
            "map": {
134
                "llcrnrlon": -80,
135
                "llcrnrlat": -40,
136
                "urcrnrlon": -10,
137
                "urcrnrlat": 30
138
            }
139
        },
140
        "07 Europe (cyl)": {
141
            "CRS": "EPSG:4326",
142
            "map": {
143
                "llcrnrlon": -15.0,
144
                "llcrnrlat": 35.0,
145
                "urcrnrlon": 30.0,
146
                "urcrnrlat": 65.0
147
            }
148
        },
149
        "08 Germany (cyl)": {
150
            "CRS": "EPSG:4326",
151
            "map": {
152
                "llcrnrlon": 5.0,
153
                "llcrnrlat": 45.0,
154
                "urcrnrlon": 15.0,
155
                "urcrnrlat": 57.0
156
            }
157
        },
158
        "09 Northern Hemisphere (stereo)": {
159
            "CRS": "MSS:stere,0,90,90",
160
            "map": {
161
                "llcrnrlon": -45.0,
162
                "llcrnrlat": 0.0,
163
                "urcrnrlon": 135.0,
164
                "urcrnrlat": 0.0
165
            }
166
        }
167
    }
168

169
    # Side View.
170
    # The following two parameters are passed to the WMS in the BBOX
171
    # argument when a vertical cross section is requested.
172

173
    # Number of interpolation points used to interpolate the flight track
174
    # to a great circle.
175
    num_interpolation_points = 201
1✔
176

177
    # Number of x-axis labels in the side view.
178
    num_labels = 10
1✔
179

180
    # Web Map Service Client.
181
    # Settings for the WMS client. Set the URLs of WMS servers that appear
182
    # by default in the WMS control (for examples, see
183
    # http://external.opengis.org/twiki_public/bin/view/MetOceanDWG/MetocWMS_Servers).
184
    # Also set the location of the image file cache and its size.
185

186
    # URLs of default WMS servers.
187
    default_WMS = [
1✔
188
        "http://localhost:8081/",
189
        "https://view.eumetsat.int/geoserver/wms",
190
        "http://eccharts.ecmwf.int/wms/?token=public",
191
        "https://neo.gsfc.nasa.gov/wms/wms"
192
    ]
193

194
    default_VSEC_WMS = [
1✔
195
        "http://localhost:8081/"
196
    ]
197

198
    default_LSEC_WMS = [
1✔
199
        "http://localhost:8081/"
200
    ]
201

202
    # URLs of default mscolab servers
203
    default_MSCOLAB = [
1✔
204
        "http://localhost:8083",
205
    ]
206

207
    # Username used for http auth
208
    MSCOLAB_auth_user_name = "mscolab"
1✔
209

210
    # category for MSC operations
211
    MSCOLAB_category = "default"
1✔
212

213
    # timeout for MSColab in seconds. First value is for connection, second for reply
214
    MSCOLAB_timeout = [2, 10]
1✔
215

216
    # don't query for archived operations
217
    MSCOLAB_skip_archived_operations = False
1✔
218

219
    # list of MSC servers {"http://www.your-mscolab-server.de": "authuser",
220
    # "http://www.your-wms-server.de": "authuser"}
221
    MSS_auth = {}
1✔
222

223
    # timeout of Url request
224
    WMS_request_timeout = 30
1✔
225

226
    WMS_preload = []
1✔
227

228
    # WMS image cache settings:
229
    wms_cache = str(constants.MSUI_CACHE_PATH / "wms_cache")
1✔
230

231
    # Maximum size of the cache in bytes.
232
    wms_cache_max_size_bytes = 20 * 1024 * 1024
1✔
233

234
    # Maximum age of a cached file in seconds.
235
    wms_cache_max_age_seconds = 5 * 86400
1✔
236

237
    wms_prefetch = {
1✔
238
        "validtime_fwd": 0,
239
        "validtime_bck": 0,
240
        "level_up": 0,
241
        "level_down": 0
242
    }
243

244
    locations = {
1✔
245
        "EDMO": [48.08, 11.28],
246
        "Hannover": [52.37, 9.74],
247
        "Hamburg": [53.55, 9.99],
248
        "Juelich": [50.92, 6.36],
249
        "Leipzig": [51.34, 12.37],
250
        "Muenchen": [48.14, 11.57],
251
        "Stuttgart": [48.78, 9.18],
252
        "Wien": [48.20833, 16.373064],
253
        "Zugspitze": [47.42, 10.98],
254
        "Kiruna": [67.821, 20.336],
255
        "Ny-Alesund": [78.928, 11.986],
256
        "Zhukovsky": [55.6, 38.116],
257
        "Paphos": [34.775, 32.425],
258
        "Sharjah": [25.35, 55.65],
259
        "Brindisi": [40.658, 17.947],
260
        "Nagpur": [21.15, 79.083],
261
        "Mumbai": [19.089, 72.868],
262
        "Delhi": [28.566, 77.103],
263
    }
264

265
    # Main application: Template for new flight tracks
266
    # Flight track template that is used when a new flight track is
267
    # created. Specify a list of place names that can be found in the
268
    # "locations" dictionary defined above.
269
    new_flighttrack_template = ["Nagpur", "Delhi"]
1✔
270

271
    # This configures the flight level for waypoints inserted by the
272
    # flighttrack template
273
    new_flighttrack_flightlevel = 0
1✔
274

275
    # None is not wanted here
276
    proxies = {}
1✔
277

278
    # ToDo configurable later
279
    # mscolab server
280
    mscolab_server_url = "http://localhost:8083"
1✔
281
    # ToDo refactor to rename this to data_dir/mss_data_dir
282
    # mss dir
283
    mss_dir = "~/mss"
1✔
284

285
    # list of gravatar email ids to automatically fetch
286
    gravatar_ids = []
1✔
287

288
    # dictionary for export plugins, e.g.  {"Text": ["txt", "mslib.plugins.io.text", "save_to_txt"] }
289
    export_plugins = {}
1✔
290

291
    # dictionary for import plugins, e.g. { "FliteStar": ["txt", "mslib.plugins.io.flitestar", "load_from_flitestar"] }
292
    import_plugins = {}
1✔
293

294
    # dictionary to make title, label and ticklabel sizes for topview and sideview configurable.
295
    # You can put your default value here, whatever you want to give,it should be a number.
296
    topview = {"plot_title_size": 10,
1✔
297
               "axes_label_size": 10}
298

299
    sideview = {"plot_title_size": 10,
1✔
300
                "axes_label_size": 10}
301

302
    linearview = {"plot_title_size": 10,
1✔
303
                  "axes_label_size": 10}
304

305
    automated_plotting_flights = [[]]
1✔
306
    automated_plotting_hsecs = [[]]
1✔
307
    automated_plotting_vsecs = [[]]
1✔
308
    automated_plotting_lsecs = [[]]
1✔
309

310
    # Dictionary options with fixed key/value pairs
311
    fixed_dict_options = ["layout", "wms_prefetch", "topview", "sideview", "linearview"]
1✔
312
    # List options with fixed length
313
    fixed_list_options = ["MSCOLAB_timeout", ]
1✔
314

315
    # Fixed key/value pair options
316
    key_value_options = [
1✔
317
        'mscolab_skip_verify_user_token',
318
        'filepicker_default',
319
        'mss_dir',
320
        'data_dir',
321
        'num_labels',
322
        'num_interpolation_points',
323
        'new_flighttrack_flightlevel',
324
        'MSCOLAB_category',
325
        'MSCOLAB_skip_archived_operations',
326
        'mscolab_server_url',
327
        'MSCOLAB_auth_user_name',
328
        'wms_cache',
329
        'wms_cache_max_size_bytes',
330
        'wms_cache_max_age_seconds',
331
        'WMS_request_timeout',
332
    ]
333

334
    # Dictionary options with predefined structure
335
    dict_option_structure = {
1✔
336
        "MSS_auth": {"http://www.your-wms-server.de": "authusername"},
337
        "predefined_map_sections": {
338
            "new_map_section": {
339
                "CRS": "crs_value",
340
                "map": {
341
                    "llcrnrlon": 0.0,
342
                    "llcrnrlat": 0.0,
343
                    "urcrnrlon": 0.0,
344
                    "urcrnrlat": 0.0,
345
                },
346
            }
347
        },
348
        "locations": {
349
            "new-location": [0.0, 0.0],
350
        },
351
        "export_plugins": {
352
            "plugin-name": ["extension", "module", "function", "default"],
353
        },
354
        "import_plugins": {
355
            "plugin-name": ["extension", "module", "function", "default"],
356
        },
357
        "proxies": {
358
            "https": "https://proxy.com",
359
        },
360
    }
361

362
    # List options with predefined structure
363
    list_option_structure = {
1✔
364
        "default_WMS": ["https://wms-server-url.com"],
365
        "default_VSEC_WMS": ["https://vsec-wms-server-url.com"],
366
        "default_LSEC_WMS": ["https://lsec-wms-server-url.com"],
367
        "default_MSCOLAB": ["https://mscolab-server-url.com"],
368
        "new_flighttrack_template": ["new-location"],
369
        "gravatar_ids": ["example@email.com"],
370
        "WMS_preload": ["https://wms-preload-url.com"],
371
        "MSCOLAB_timeout": [0, 0],
372
        "automated_plotting_flights": [["", "", "", "", "", ""]],
373
        "automated_plotting_hsecs": [["http://www.your-wms-server.de", "", "", ""]],
374
        "automated_plotting_vsecs": [["http://www.your-wms-server.de", "", "", ""]],
375
        "automated_plotting_lsecs": [["http://www.your-wms-server.de", "", "", "pressure"]]
376
    }
377

378
    config_descriptions = {
1✔
379
        "filepicker_default": "Defines the type of file-picker to be used. Can be 'default', 'qt', or 'fs'",
380
        "data_dir": "Directory where MSUI output files are stored",
381
        "predefined_map_sections": "Dictionary containing predefined map sections with their settings",
382
        "num_interpolation_points": "Number of interpolation points used for vertical cross section requests",
383
        "num_labels": "Number of x-axis labels in the side view",
384
        "default_WMS": "List of the URLs of default WMS servers",
385
        "default_VSEC_WMS": "List of the URLs of default Vertical Section WMS servers",
386
        "default_LSEC_WMS": "List of the URLs of default Linear Section WMS servers",
387
        "default_MSCOLAB": "List of the URLs of default MSColab servers",
388
        "MSS_auth": "Dictionary containing credentials for http auth",
389
        "MSCOLAB_auth_user_name": "Username used for http auth",
390
        "MSCOLAB_timeout": "Tuple specifying timeout for MSColab in seconds. First value is for connection,"
391
                           " second for reply",
392
        "WMS_request_timeout": "Timeout of WMS Url request",
393
        "WMS_preload": "List of WMS URLs to preload",
394
        "wms_cache": "Path to WMS image cache directory",
395
        "wms_cache_max_size_bytes": "Maximum size of the cache in bytes",
396
        "wms_cache_max_age_seconds": "Maximum age of a cached file in seconds",
397
        "wms_prefetch": "Dictionary configuring the prefetch behaviour of WMS",
398
        "locations": "Dictionary providing geo-coordinates for predefined locations",
399
        "new_flighttrack_template": "List of predefined location names used as a flight track template"
400
                                    " when creating a new flight track",
401
        "new_flighttrack_flightlevel": "Default flight level for waypoints inserted by the flighttrack template",
402
        "proxies": "Proxy settings for network requests",
403
        "mscolab_server_url": "URL of the MSColab server",
404
        "mss_dir": "Directory path used for MSS",  # ToDo is this needed or can be replaced by data_dir?
405
        "gravatar_ids": "List of gravatar email ids to automatically fetch",
406
        "export_plugins": "Dictionary of export plugins",
407
        "import_plugins": "Dictionary of import plugins",
408
        "layout": "Dictionary for layout of different views",
409
        "topview": "Dictionary to make title, label, and ticklabel sizes for topview configurable",
410
        "sideview": "Dictionary to make title, label, and ticklabel sizes for sideview configurable",
411
        "linearview": "Dictionary to make title, label, and ticklabel sizes for linearview configurable",
412
    }
413

414

415
# default options as dictionary
416
default_options = dict(MSUIDefaultConfig.__dict__)
1✔
417
for key in [
1✔
418
    "__module__",
419
    "__doc__",
420
    "__dict__",
421
    "__weakref__",
422
    "fixed_dict_options",
423
    "fixed_list_options",
424
    "dict_option_structure",
425
    "list_option_structure",
426
    "key_value_options",
427
    "config_descriptions",
428
]:
429
    del default_options[key]
1✔
430

431

432
# user options as dictionary
433
user_options = copy.deepcopy(default_options)
1✔
434

435

436
def read_config_file(path=constants.MSUI_SETTINGS):
1✔
437
    """
438
    reads a config file and updates global user_options
439

440
    Args:
441
        path: path of config file
442

443
    Note:
444
        sole purpose of the path argument is to be able to test with example config files
445
    """
446
    path = path.replace("\\", "/")
1✔
447
    dir_name, file_name = fs.path.split(path)
1✔
448
    json_file_data = {}
1✔
449
    with fs.open_fs(dir_name) as _fs:
1✔
450
        if _fs.exists(file_name):
1✔
451
            file_content = _fs.readtext(file_name)
1✔
452
            try:
1✔
453
                json_file_data = json.loads(file_content, object_pairs_hook=dict_raise_on_duplicates_empty)
1✔
454
            except json.JSONDecodeError as e:
1✔
455
                logging.error("Error while loading json file %s", e)
×
456
                error_message = f"Unexpected error while loading config\n{e}"
×
457
                raise FatalUserError(error_message)
×
458
            except ValueError as e:
1✔
459
                logging.error("Error while loading json file %s", e)
1✔
460
                error_message = f"Invalid keys detected in config\n{e}"
1✔
461
                raise FatalUserError(error_message)
1✔
462
        else:
463
            error_message = f"MSS config File '{path}' not found"
1✔
464
            raise FileNotFoundError(error_message)
1✔
465

466
    global user_options
467
    if json_file_data:
1✔
468
        user_options = merge_dict(copy.deepcopy(default_options), json_file_data)
1✔
469
        logging.debug("Merged default and user settings")
1✔
470
    else:
471
        user_options = copy.deepcopy(default_options)
1✔
472
        logging.debug("No user settings found, using default settings")
1✔
473

474

475
def modify_config_file(data, path=constants.MSUI_SETTINGS):
1✔
476
    """
477
    modifies a config file
478

479
    Args:
480
        data: data to be modified/written
481
        path: path of config file
482

483
    Note:
484
        sole purpose of the path argument is to be able to test with example config files
485
    """
486
    path = path.replace("\\", "/")
1✔
487
    dir_name, file_name = fs.path.split(path)
1✔
488
    json_file_data = {}
1✔
489
    with fs.open_fs(dir_name) as _fs:
1✔
490
        if _fs.exists(file_name):
1✔
491
            try:
1✔
492
                file_content = _fs.readtext(file_name)
1✔
493
                json_file_data = json.loads(file_content, object_pairs_hook=dict_raise_on_duplicates_empty)
1✔
494
                json_file_data_copy = copy.deepcopy(json_file_data)
1✔
495
                for key in data:
1✔
496
                    if key not in json_file_data:
1✔
497
                        json_file_data_copy[key] = config_loader(dataset=key, default=True)
1✔
498
                modified_data = merge_dict(json_file_data_copy, data)
1✔
499
                logging.debug("Merged default and user settings")
1✔
500
                _fs.writetext(file_name, json.dumps(modified_data, indent=4))
1✔
501
                read_config_file()
1✔
502
            except json.JSONDecodeError as e:
1✔
503
                logging.error("Error while loading json file %s", e)
×
504
                error_message = f"Unexpected error while loading config\n{e}"
×
505
                raise FatalUserError(error_message)
×
506
            except ValueError as e:
1✔
507
                logging.error("Error while loading json file %s", e)
×
508
                error_message = f"Invalid keys detected in config\n{e}"
×
509
                raise FatalUserError(error_message)
×
510
        else:
511
            error_message = f"MSS config File '{path}' not found"
×
512
            raise FileNotFoundError(error_message)
×
513

514

515
def config_loader(dataset=None, default=False):
1✔
516
    """
517
    Function for returning config value
518

519
    Args:
520
        dataset: section to pull from json file
521
        default: option to return default config for the dataset
522

523
    Returns: a the dataset value or the config as dictionary
524

525
    """
526
    if dataset is not None and dataset not in user_options:
1✔
527
        raise KeyError(f"requested dataset '{dataset}' not in defaults!")
1✔
528

529
    if dataset is not None:
1✔
530
        if default:
1✔
531
            return default_options[dataset]
1✔
532
        return user_options[dataset]
1✔
533
    else:
534
        if default:
1✔
535
            return default_options
1✔
536
        return user_options
1✔
537

538

539
def save_settings_qsettings(tag, settings):
1✔
540
    """
541
    Saves a dictionary settings to disk.
542

543
    :param tag: string specifying the settings
544
    :param settings: dictionary of settings
545
    :return: None
546
    """
547
    assert isinstance(tag, str)
1✔
548
    assert isinstance(settings, dict)
1✔
549
    # ToDo we have to verify if we can all switch to this definition, not having 3 different
550
    q_settings = QtCore.QSettings(os.path.join(constants.MSUI_CONFIG_SYSPATH, "msui-core.conf"),
1✔
551
                                  QtCore.QSettings.IniFormat)
552

553
    file_path = q_settings.fileName()
1✔
554
    logging.debug("storing settings for %s to %s", tag, file_path)
1✔
555
    try:
1✔
556
        q_settings.setValue(tag, QtCore.QVariant(settings))
1✔
557
    except (OSError, IOError) as ex:
×
558
        logging.warning("Problems storing %s settings (%s: %s).", tag, type(ex), ex)
×
559
    return settings
1✔
560

561

562
def load_settings_qsettings(tag, default_settings=None):
1✔
563
    """
564
    Loads a dictionary of settings from disk. May supply a dictionary of default settings
565
    to return in case the settings file is not present or damaged. The default_settings one will
566
    be updated by the restored one so one may rely on all keys of the default_settings dictionary
567
    being present in the returned dictionary.
568

569
    :param tag: string specifying the settings
570
    :param default_settings: dictionary of settings or None
571
    :return: dictionary of settings
572
    """
573
    if default_settings is None:
1✔
574
        default_settings = {}
1✔
575
    assert isinstance(default_settings, dict)
1✔
576

577
    settings = {}
1✔
578

579
    q_settings = QtCore.QSettings(os.path.join(constants.MSUI_CONFIG_SYSPATH, "msui-core.conf"),
1✔
580
                                  QtCore.QSettings.IniFormat)
581
    file_path = q_settings.fileName()
1✔
582
    logging.debug("loading settings for %s from %s", tag, file_path)
1✔
583
    try:
1✔
584
        settings = q_settings.value(tag)
1✔
585
    except Exception as ex:
×
586
        logging.warning("Problems reloading stored %s settings (%s: %s). Switching to default",
×
587
                        tag, type(ex), ex)
588
    if isinstance(settings, dict):
1✔
589
        default_settings.update(settings)
1✔
590
    return default_settings
1✔
591

592

593
def merge_dict(existing_dict, new_dict):
1✔
594
    """
595
    Merge two dictionaries by comparing all the options from
596
    the MSUIDefaultConfig class
597

598
    Arguments:
599
    existing_dict -- Dict to merge new_dict into
600
    new_dict -- Dict with new values
601
    """
602
    # Check if dictionary options with fixed key/value pairs match data types from default
603
    for key in MSUIDefaultConfig.fixed_dict_options + MSUIDefaultConfig.fixed_list_options:
1✔
604
        if key in new_dict:
1✔
605
            existing_dict[key] = compare_data(
1✔
606
                existing_dict[key], new_dict[key]
607
            )[0]
608

609
    # Check if dictionary options with predefined structure match data types from default
610
    dos = copy.deepcopy(MSUIDefaultConfig.dict_option_structure)
1✔
611
    # adding plugin structure : ["extension", "module", "function"] to
612
    # recognize user plugin options that don't have the optional filepicker option set
613
    dos["import_plugins"]["plugin-name-a"] = dos["import_plugins"]["plugin-name"][:3]
1✔
614
    dos["export_plugins"]["plugin-name-a"] = dos["export_plugins"]["plugin-name"][:3]
1✔
615
    for key in dos:
1✔
616
        if key in new_dict:
1✔
617
            temp_data = {}
1✔
618
            for option_key in new_dict[key]:
1✔
619
                for dos_key_key in dos[key]:
1✔
620
                    data, match = compare_data(dos[key][dos_key_key], new_dict[key][option_key])
1✔
621
                    if key in MSUIDefaultConfig.fixed_list_options:
1✔
622
                        match = len(dos[key][dos_key_key]) == len(new_dict[key][option_key])
×
623
                    if match:
1✔
624
                        temp_data[option_key] = new_dict[key][option_key]
1✔
625
                        break
1✔
626
            if temp_data != {}:
1✔
627
                existing_dict[key] = temp_data
1✔
628

629
    # Check if list options with predefined structure match data types from default
630
    los = copy.deepcopy(MSUIDefaultConfig.list_option_structure)
1✔
631
    for key in los:
1✔
632
        if key in new_dict:
1✔
633
            temp_data = []
1✔
634
            if key not in MSUIDefaultConfig.fixed_list_options:
1✔
635
                for i in range(len(new_dict[key])):
1✔
636
                    for los_key_item in los[key]:
1✔
637
                        data, match = compare_data(los_key_item, new_dict[key][i])
1✔
638
                        if match:
1✔
639
                            temp_data.append(data)
1✔
640
                            break
1✔
641
                if temp_data != []:
1✔
642
                    existing_dict[key] = temp_data
1✔
643

644
    # Check if options with fixed key/value pair structure match data types from default
645
    for key in MSUIDefaultConfig.key_value_options:
1✔
646
        if key in new_dict:
1✔
647
            data, match = compare_data(existing_dict[key], new_dict[key])
1✔
648
            if match:
1✔
649
                existing_dict[key] = data
1✔
650

651
    # add filepicker default to import and export plugins if missing
652
    for plugin_type in ["import_plugins", "export_plugins"]:
1✔
653
        if plugin_type in existing_dict:
1✔
654
            for plugin in existing_dict[plugin_type]:
1✔
655
                if len(existing_dict[plugin_type][plugin]) == 3:
1✔
656
                    existing_dict[plugin_type][plugin].append(
1✔
657
                        existing_dict.get("filepicker_default", "default")
658
                    )
659

660
    return existing_dict
1✔
661

662

663
def compare_data(default, user_data):
1✔
664
    """
665
    Recursively compares two dictionaries based on qt_json_view datatypes
666
    and returns default or user_data appropriately.
667

668
    Arguments:
669
    default -- Dict to return if datatype not matching
670
    user_data -- Dict to return if datatype is matching
671
    """
672
    # If data is neither list not dict type, compare individual type
673
    if not isinstance(default, dict) and not isinstance(default, list):
1✔
674
        if isinstance(default, float) and isinstance(user_data, int):
1✔
675
            user_data = float(default)
1✔
676
        if isinstance(match_type(default), UrlType) and isinstance(match_type(user_data), StrType):
1✔
677
            return user_data, True
×
678
        if isinstance(match_type(default), type(match_type(user_data))):
1✔
679
            return user_data, True
1✔
680
        # Special handling for path strings - both absolute and relative paths should be treated as valid strings
681
        elif isinstance(default, str) and isinstance(user_data, str):
1✔
682
            return user_data, True
1✔
683
        else:
684
            return default, False
1✔
685

686
    data = copy.deepcopy(default)
1✔
687
    matches = []
1✔
688
    # If data is list type, compare all values in list
689
    if isinstance(default, list) and isinstance(user_data, list):
1✔
690
        if len(default) == len(user_data):
1✔
691
            for i in range(len(default)):
1✔
692
                data[i], match = compare_data(default[i], user_data[i])
1✔
693
                matches.append(match)
1✔
694
        else:
695
            return default, False
1✔
696

697
    # If data is dict type, goes through the dict and update
698
    elif isinstance(default, dict) and isinstance(user_data, dict):
1✔
699
        if default.keys() == user_data.keys():
1✔
700
            for key in default:
1✔
701
                if key in user_data:
1✔
702
                    data[key], match = compare_data(default[key], user_data[key])
1✔
703
                    matches.append(match)
1✔
704
                else:
UNCOV
705
                    matches.append(False)
×
706
        else:
707
            return default, False
1✔
708

709
    return data, all(matches)
1✔
710

711

712
def dict_raise_on_duplicates_empty(ordered_pairs):
1✔
713
    """Reject duplicate and empty keys."""
714
    accepted = {}
1✔
715
    for key, value in ordered_pairs:
1✔
716
        if key in accepted:
1✔
717
            raise ValueError(f"duplicate key found: {key}")
1✔
718
        elif key == "":
1✔
719
            raise ValueError("empty key found")
1✔
720
        else:
721
            accepted[key] = value
1✔
722
    return accepted
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc