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

johntruckenbrodt / pyroSAR / 21948031762

12 Feb 2026 01:12PM UTC coverage: 54.886% (+0.3%) from 54.605%
21948031762

push

github

web-flow
Merge pull request #393 from johntruckenbrodt/cicd/tmp_home_autouse

[tests] session scope for tmp_home fixture

99 of 115 new or added lines in 4 files covered. (86.09%)

3 existing lines in 1 file now uncovered.

4134 of 7532 relevant lines covered (54.89%)

0.55 hits per line

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

82.99
/pyroSAR/examine.py
1
###############################################################################
2
# Examination of SAR processing software
3
# Copyright (c) 2019-2026, the pyroSAR Developers.
4

5
# This file is part of the pyroSAR Project. It is subject to the
6
# license terms in the LICENSE.txt file found in the top-level
7
# directory of this distribution and at
8
# https://github.com/johntruckenbrodt/pyroSAR/blob/master/LICENSE.txt.
9
# No part of the pyroSAR project, including this file, may be
10
# copied, modified, propagated, or distributed except according
11
# to the terms contained in the LICENSE.txt file.
12
###############################################################################
13
import json
1✔
14
import os
1✔
15
import shutil
1✔
16
import re
1✔
17
import warnings
1✔
18
import platform
1✔
19
import subprocess as sp
1✔
20
import importlib.resources
1✔
21

22
from pyroSAR.config import ConfigHandler
1✔
23
from spatialist.ancillary import finder, run
1✔
24

25
import logging
1✔
26

27
log = logging.getLogger(__name__)
1✔
28

29
__config__ = ConfigHandler()
1✔
30

31

32
class ExamineSnap(object):
1✔
33
    """
34
    Class to check if ESA SNAP is installed.
35
    Upon initialization, this class searches for relevant binaries and the accompanying
36
    relative directory structure, which uniquely identify an ESA SNAP installation on a system.
37
    First, all relevant file and folder names are read from the pyroSAR config file if it exists
38
    and their existence is verified.
39
    If this fails, a system check is performed to find relevant binaries in the system PATH variable and
40
    additional files and folders relative to them.
41
    In case SNAP is not installed, a default `snap.auxdata.properties` file delivered with pyroSAR will be copied to
42
    `$HOME/.snap/etc` so that SNAP download URLS and local directory structure can be adapted by other software.
43
    
44
    SNAP configuration can be read and modified via the attribute `snap_properties` of type
45
    :class:`~pyroSAR.examine.SnapProperties` or the properties :attr:`~pyroSAR.examine.ExamineSnap.userpath` and
46
    :attr:`~pyroSAR.examine.ExamineSnap.auxdatapath`.
47
    """
48
    _version_dict = None
1✔
49
    
50
    def __init__(self):
1✔
51
        # update legacy config files
52
        if 'OUTPUT' in __config__.sections:
1✔
53
            __config__.remove_section('OUTPUT')
×
54
        if 'SNAP' in __config__.sections:
1✔
55
            snap_keys = __config__.keys('SNAP')
1✔
56
            for key in ['auxdata', 'auxdatapath', 'properties']:
1✔
57
                if key in snap_keys:
1✔
58
                    __config__.remove_option(section='SNAP', key=key)
×
59
        
60
        # define some attributes which identify SNAP
61
        self.identifiers = ['path', 'gpt', 'etc']
1✔
62
        
63
        # a list of relevant sections
64
        self.sections = ['SNAP', 'SNAP_SUFFIX']
1✔
65
        
66
        # set attributes path, gpt, etc, __suffices
67
        self.__read_config()
1✔
68
        
69
        # if SNAP could not be identified from the config attributes, do a system search for it
70
        # sets attributes path, gpt, etc
71
        if not self.__is_identified():
1✔
72
            log.debug('identifying SNAP')
1✔
73
            self.__identify_snap()
1✔
74
        
75
        # if SNAP cannot be identified, copy the snap.auxdata.properties file to $HOME/.snap/etc
76
        if not self.__is_identified():
1✔
77
            self.etc = os.path.join(os.path.expanduser('~'), '.snap', 'etc')
×
78
            os.makedirs(self.etc, exist_ok=True)
×
79
            dst = os.path.join(self.etc, 'snap.auxdata.properties')
×
80
            if not os.path.isfile(dst):
×
81
                dir_data = importlib.resources.files('pyroSAR') / 'snap' / 'data'
×
82
                src = str(dir_data / 'snap.auxdata.properties')
×
83
                log.debug(f'creating {dst}')
×
84
                shutil.copyfile(src, dst)
×
85
        
86
        # if the SNAP suffices attribute was not yet identified,
87
        # point it to the default file delivered with pyroSAR
88
        if not hasattr(self, '__suffices'):
1✔
89
            dir_data = importlib.resources.files('pyroSAR') / 'snap' / 'data'
1✔
90
            fname_suffices = str(dir_data / 'snap.suffices.properties')
1✔
91
            with open(fname_suffices, 'r') as infile:
1✔
92
                content = infile.read().split('\n')
1✔
93
            self.__suffices = {k: v for k, v in [x.split('=') for x in content]}
1✔
94
        
95
        # SNAP property read/modification interface
96
        self.snap_properties = SnapProperties(path=os.path.dirname(self.etc))
1✔
97
        
98
        # update the config file: this scans for config changes and re-writes the config file if any are found
99
        self.__update_config()
1✔
100
        
101
        if ExamineSnap._version_dict is None:
1✔
102
            ExamineSnap._version_dict = self.__read_version_dict()
1✔
103
        self.version_dict = ExamineSnap._version_dict
1✔
104
    
105
    def __getattr__(self, item):
1✔
106
        if item in ['path', 'gpt']:
1✔
107
            msg = ('SNAP could not be identified. If you have installed it '
1✔
108
                   'please add the path to the SNAP executables (bin subdirectory) '
109
                   'to the PATH environment. E.g. in the Linux .bashrc file add '
110
                   'the following line:\nexport PATH=$PATH:path/to/snap/bin"')
111
        else:
112
            msg = "'ExamineSnap' object has no attribute '{}'".format(item)
1✔
113
        raise AttributeError(msg)
1✔
114
    
115
    def __is_identified(self):
1✔
116
        """
117
        Check if SNAP has been properly identified, i.e. all paths in `self.identifiers`
118
        have been detected and confirmed.
119
        
120
        Returns
121
        -------
122
        bool
123
        """
124
        return sum([hasattr(self, x) for x in self.identifiers]) == len(self.identifiers)
1✔
125
    
126
    def __identify_snap(self):
1✔
127
        """
128
        do a comprehensive search for an ESA SNAP installation
129
        
130
        Returns
131
        -------
132
        bool
133
            has the SNAP properties file been changed?
134
        """
135
        # create a list of possible SNAP executables
136
        defaults = ['snap64.exe', 'snap32.exe', 'snap.exe', 'snap']
1✔
137
        paths = os.environ['PATH'].split(os.path.pathsep)
1✔
138
        options = [os.path.join(path, option) for path in paths for option in defaults]
1✔
139
        options = [x for x in options if os.path.isfile(x)]
1✔
140
        
141
        if not hasattr(self, 'path') or not os.path.isfile(self.path):
1✔
142
            executables = options
1✔
143
        else:
144
            executables = [self.path] + options
×
145
        
146
        if len(executables) == 0:
1✔
147
            log.debug("could not detect any potential 'snap' executables")
×
148
        
149
        # for each possible SNAP executable, check whether additional files and directories exist relative to it
150
        # to confirm whether it actually is an ESA SNAP installation or something else like e.g. the Ubuntu App Manager
151
        for path in executables:
1✔
152
            log.debug('checking candidate {}'.format(path))
1✔
153
            if os.path.islink(path):
1✔
154
                path = os.path.realpath(path)
1✔
155
            
156
            # check whether a directory etc exists relative to the SNAP executable
157
            etc = os.path.join(os.path.dirname(os.path.dirname(path)), 'etc')
1✔
158
            if not os.path.isdir(etc):
1✔
159
                log.debug("could not find the 'etc' directory")
×
160
                continue
×
161
            
162
            # check the content of the etc directory
163
            config_files = os.listdir(etc)
1✔
164
            expected = ['snap.auxdata.properties', 'snap.clusters',
1✔
165
                        'snap.conf', 'snap.properties']
166
            for name in expected:
1✔
167
                if name not in config_files:
1✔
168
                    log.debug(f"could not find the '{name}' file")
×
169
                    continue
×
170
            
171
            # identify the gpt executable
172
            gpt_candidates = finder(os.path.dirname(path), ['gpt', 'gpt.exe'])
1✔
173
            if len(gpt_candidates) == 0:
1✔
174
                log.debug("could not find the 'gpt' executable")
×
175
                continue
×
176
            else:
177
                gpt = gpt_candidates[0]
1✔
178
            
179
            self.path = path
1✔
180
            self.etc = etc
1✔
181
            self.gpt = gpt
1✔
182
            return
1✔
183
    
184
    def __read_config(self):
1✔
185
        """
186
        This method reads the config.ini to examine the snap paths.
187
        If the snap paths are not in the config.ini or the paths are
188
        wrong they will be automatically created.
189

190
        Returns
191
        -------
192

193
        """
194
        for attr in self.identifiers:
1✔
195
            self.__read_config_attr(attr, section='SNAP')
1✔
196
        
197
        suffices = {}
1✔
198
        if 'SNAP_SUFFIX' in __config__.sections:
1✔
199
            suffices = __config__['SNAP_SUFFIX']
1✔
200
        if len(suffices.keys()) > 0:
1✔
201
            self.__suffices = suffices
1✔
202
    
203
    def __read_config_attr(self, attr, section):
1✔
204
        """
205
        read an attribute from the config file and set it as an object attribute
206
        
207
        Parameters
208
        ----------
209
        attr: str
210
            the attribute name
211
        section: str
212
            the config section to read the attribute from
213
        
214
        Returns
215
        -------
216
        
217
        """
218
        if section in __config__.sections:
1✔
219
            if attr in __config__[section].keys():
1✔
220
                val = __config__[section][attr]
1✔
221
                if os.path.exists(val):
1✔
222
                    # log.info('setting attribute {}'.format(attr))
223
                    setattr(self, attr, val)
1✔
224
    
225
    def __read_version_dict(self):
1✔
226
        log.debug('reading SNAP version information')
1✔
227
        out = {}
1✔
228
        
229
        cmd = [self.path, '--nosplash', '--nogui', '--modules',
1✔
230
               '--list', '--refresh']
231
        if platform.system() == 'Windows':
1✔
NEW
232
            cmd.extend(['--console', 'suppress'])
×
233
        
234
        proc = sp.Popen(args=cmd, stdout=sp.PIPE, stderr=sp.STDOUT,
1✔
235
                        text=True, encoding='utf-8', bufsize=1)
236
        
237
        counter = 0
1✔
238
        lines = []
1✔
239
        lines_info = []
1✔
240
        for line in proc.stdout:
1✔
241
            line = line.rstrip()
1✔
242
            lines.append(line)
1✔
243
            if line.startswith('---'):
1✔
244
                counter += 1
1✔
245
            else:
246
                if counter == 1:
1✔
247
                    lines_info.append(line)
1✔
248
            if counter == 2:
1✔
249
                proc.terminate()
1✔
250
        proc.wait()
1✔
251
        
252
        pattern = r'([a-z.]*)\s+([0-9.]+)\s+(.*)'
1✔
253
        for line in lines_info:
1✔
254
            code, version, state = re.search(pattern=pattern, string=line).groups()
1✔
255
            out[code] = {'version': version, 'state': state}
1✔
256
        if len(out) == 0:
1✔
NEW
257
            raise RuntimeError(f'{"\n".join(lines)}\ncould not '
×
258
                               f'read SNAP version information')
259
        return out
1✔
260
    
261
    def __update_config(self):
1✔
262
        for section in self.sections:
1✔
263
            if section not in __config__.sections:
1✔
264
                # log.info('creating section {}..'.format(section))
265
                __config__.add_section(section)
1✔
266
        
267
        for key in self.identifiers:
1✔
268
            if hasattr(self, key):
1✔
269
                self.__update_config_attr(key, getattr(self, key), 'SNAP')
1✔
270
        
271
        for key in sorted(self.__suffices.keys()):
1✔
272
            self.__update_config_attr(key, self.__suffices[key], 'SNAP_SUFFIX')
1✔
273
    
274
    @staticmethod
1✔
275
    def __update_config_attr(attr, value, section):
1✔
276
        if isinstance(value, list):
1✔
277
            value = json.dumps(value)
×
278
        
279
        if attr not in __config__[section].keys() or __config__[section][attr] != value:
1✔
280
            # log.info('updating attribute {0}:{1}..'.format(section, attr))
281
            # log.info('  {0} -> {1}'.format(repr(config[section][attr]), repr(value)))
282
            __config__.set(section, key=attr, value=value, overwrite=True)
1✔
283
    
284
    def get_suffix(self, operator):
1✔
285
        """
286
        get the file name suffix for an operator
287
        
288
        Parameters
289
        ----------
290
        operator: str
291
            the name of the operator
292

293
        Returns
294
        -------
295
        str or None
296
            the file suffix or None if unknown
297
        
298
        Examples
299
        --------
300
        >>> from pyroSAR.examine import ExamineSnap
301
        >>> config = ExamineSnap()
302
        >>> print(config.get_suffix('Terrain-Flattening'))
303
        'TF'
304
        """
305
        if operator in self.__suffices.keys():
1✔
306
            return self.__suffices[operator]
1✔
307
        else:
308
            return None
1✔
309
    
310
    def get_version(self, module: str) -> str:
1✔
311
        """
312
        Read the version and date of different SNAP modules.
313
        The following SNAP command is called to get the information:
314
        
315
        .. code-block:: bash
316

317
            snap --nosplash --nogui --modules --list --refresh --console suppress
318
    
319
        Parameters
320
        ----------
321
        module:
322
            one of the following
323
            
324
            - core
325
            - desktop
326
            - rstb
327
            - opttbx
328
            - microwavetbx
329

330
        Returns
331
        -------
332
            the version number
333
        """
334
        log.debug(f"reading version information for module '{module}'")
1✔
335
        patterns = {'core': 'org.esa.snap.snap.core',
1✔
336
                    'desktop': 'org.esa.snap.snap.ui',
337
                    'rstb': 'org.csa.rstb.rstb.kit',
338
                    'opttbx': 'eu.esa.opt.opttbx.kit',
339
                    'microwavetbx': 'eu.esa.microwavetbx.microwavetbx.kit'}
340
        
341
        if module not in patterns.keys():
1✔
NEW
342
            raise ValueError(f"'{module}' is not a valid module name. "
×
343
                             f"Supported options: {patterns.keys()}")
344
        
345
        for k, v in self.version_dict.items():
1✔
346
            if patterns[module] == k:
1✔
347
                if v['state'] == 'Available':
1✔
NEW
348
                    raise RuntimeError(f'{module} is not installed')
×
349
                log.debug(f'version is {v['version']}')
1✔
350
                return v['version']
1✔
NEW
351
        raise RuntimeError(f"Could not find version "
×
352
                           f"information for module '{module}'.")
353
    
354
    @property
1✔
355
    def auxdatapath(self):
1✔
356
        """
357
        Get/set the SNAP configuration for `AuxDataPath` in `snap.auxdata.properties`.
358
        
359
        Example
360
        -------
361
        >>> from pyroSAR.examine import ExamineSnap
362
        >>> config = ExamineSnap()
363
        >>> config.auxdatapath = '/path/to/snap/auxdata'
364
        # This is equivalent to
365
        >>> config.snap_properties['AuxDataPath'] = '/path/to/snap/auxdata'
366
        """
367
        out = self.snap_properties['AuxDataPath']
1✔
368
        if out is None:
1✔
UNCOV
369
            out = os.path.join(self.userpath, 'auxdata')
×
370
        return out
1✔
371
    
372
    @auxdatapath.setter
1✔
373
    def auxdatapath(self, value):
1✔
374
        self.snap_properties['AuxDataPath'] = value
1✔
375
    
376
    @property
1✔
377
    def userpath(self):
1✔
378
        """
379
        Get/set the SNAP configuration for `snap.userdir` in `snap.properties`.
380

381
        Example
382
        -------
383
        >>> from pyroSAR.examine import ExamineSnap
384
        >>> config = ExamineSnap()
385
        >>> config.userpath = '/path/to/snap/data'
386
        # This is equivalent to
387
        >>> config.snap_properties['snap.userdir'] = '/path/to/snap/data'
388
        """
UNCOV
389
        return self.snap_properties.userpath
×
390
    
391
    @userpath.setter
1✔
392
    def userpath(self, value):
1✔
393
        self.snap_properties.userpath = value
1✔
394

395

396
class ExamineGamma(object):
1✔
397
    """
398
    Class to check if GAMMA is installed.
399
    
400
    Examples
401
    --------
402
    >>> from pyroSAR.examine import ExamineGamma
403
    >>> config = ExamineGamma()
404
    >>> print(config.home)
405
    >>> print(config.version)
406
    
407
    """
408
    
409
    def __init__(self):
1✔
410
        home_sys = os.environ.get('GAMMA_HOME')
1✔
411
        if home_sys is not None and not os.path.isdir(home_sys):
1✔
412
            warnings.warn('found GAMMA_HOME environment variable, but directory does not exist')
×
413
            home_sys = None
×
414
        
415
        self.__read_config()
1✔
416
        
417
        if hasattr(self, 'home'):
1✔
418
            if home_sys is not None and self.home != home_sys:
×
419
                log.info('the value of GAMMA_HOME is different to that in the pyroSAR configuration;\n'
×
420
                         '  was: {}\n'
421
                         '  is : {}\n'
422
                         'resetting the configuration and deleting parsed modules'
423
                         .format(self.home, home_sys))
424
                parsed = os.path.join(os.path.dirname(self.fname), 'gammaparse')
×
425
                shutil.rmtree(parsed)
×
426
                self.home = home_sys
×
427
        if not hasattr(self, 'home'):
1✔
428
            if home_sys is not None:
1✔
429
                setattr(self, 'home', home_sys)
×
430
            else:
431
                raise RuntimeError('could not read GAMMA installation directory')
1✔
432
        self.version = re.search('GAMMA_SOFTWARE[-/](?P<version>[0-9]{8})',
×
433
                                 getattr(self, 'home')).group('version')
434
        
435
        try:
×
436
            out, err = run(['which', 'gdal-config'], void=False)
×
437
            gdal_config = out.strip('\n')
×
438
            self.gdal_config = gdal_config
×
439
        except sp.CalledProcessError:
×
440
            raise RuntimeError('could not find command gdal-config.')
×
441
        self.__update_config()
×
442
    
443
    def __read_config(self):
1✔
444
        self.fname = __config__.file
1✔
445
        if 'GAMMA' in __config__.sections:
1✔
446
            attr = __config__['GAMMA']
×
447
            for key, value in attr.items():
×
448
                setattr(self, key, value)
×
449
    
450
    def __update_config(self):
1✔
451
        if 'GAMMA' not in __config__.sections:
×
452
            __config__.add_section('GAMMA')
×
453
        
454
        for attr in ['home', 'version']:
×
455
            self.__update_config_attr(attr, getattr(self, attr), 'GAMMA')
×
456
    
457
    @staticmethod
1✔
458
    def __update_config_attr(attr, value, section):
1✔
459
        if isinstance(value, list):
×
460
            value = json.dumps(value)
×
461
        
462
        if attr not in __config__[section].keys() or __config__[section][attr] != value:
×
463
            __config__.set(section, key=attr, value=value, overwrite=True)
×
464

465

466
class SnapProperties(object):
1✔
467
    """
468
    SNAP configuration interface. This class enables reading and modifying
469
    SNAP configuration in properties files. Modified properties are directly
470
    written to the files.
471
    Currently, the files `snap.properties`, `snap.auxdata.properties` and `snap.conf`
472
    are supported. These files can be found in two locations:
473
    
474
    - `<SNAP installation directory>/etc`
475
    - `<user directory>/.snap/etc`
476
    
477
    Configuration in the latter has higher priority, and modified properties will
478
    always be written there so that the installation directory is not modified.
479

480
    Parameters
481
    ----------
482
    path: str
483
        SNAP installation directory path
484
    
485
    Examples
486
    --------
487
    >>> from pyroSAR.examine import ExamineSnap, SnapProperties
488
    >>> path = ExamineSnap().path
489
    >>> config = SnapProperties(path=path)
490
    >>> config['snap.userdir'] = '/path/to/snap/auxdata'
491
    """
492
    
493
    def __init__(self, path):
1✔
494
        self.pattern = r'^(?P<comment>#?)(?P<key>[\w\.]*)[ ]*=[ ]*"?(?P<value>[^"\n]*)"?\n*'
1✔
495
        self.pattern_key_replace = r'#?{}[ ]*=[ ]*(?P<value>.*)'
1✔
496
        
497
        self.properties_path = os.path.join(path, 'etc', 'snap.properties')
1✔
498
        log.debug(f"reading {self.properties_path}")
1✔
499
        self.properties = self._to_dict(self.properties_path)
1✔
500
        self.properties.update(self._to_dict(self.userpath_properties))
1✔
501
        
502
        self.auxdata_properties_path = os.path.join(path, 'etc', 'snap.auxdata.properties')
1✔
503
        log.debug(f"reading {self.auxdata_properties_path}")
1✔
504
        self.auxdata_properties = self._to_dict(self.auxdata_properties_path)
1✔
505
        self.auxdata_properties.update(self._to_dict(self.userpath_auxdata_properties))
1✔
506
        
507
        self.conf_path = os.path.join(path, 'etc', 'snap.conf')
1✔
508
        log.debug(f"reading {self.conf_path}")
1✔
509
        str_split = {'default_options': ' '}
1✔
510
        self.conf = self._to_dict(path=self.conf_path, str_split=str_split)
1✔
511
        self.conf.update(self._to_dict(self.userpath_conf, str_split=str_split))
1✔
512
        
513
        self._dicts = [self.properties, self.auxdata_properties, self.conf]
1✔
514
        
515
        # removing this because of
516
        # "RuntimeError: OpenJDK 64-Bit Server VM warning: Options
517
        # -Xverify:none and -noverify were deprecated in JDK 13 and will
518
        # likely be removed in a future release."
519
        if '-J-Xverify:none' in self.conf['default_options']:
1✔
520
            opts = self.conf['default_options'].copy()
1✔
521
            opts.remove('-J-Xverify:none')
1✔
522
            self['default_options'] = opts
1✔
523
        
524
        # some properties need to be read from the default user path to
525
        # be visible to SNAP
526
        pairs = [(self.userpath_properties, self.properties_path),
1✔
527
                 (self.userpath_auxdata_properties, self.auxdata_properties_path)]
528
        for default, defined in pairs:
1✔
529
            if default != defined:
1✔
530
                conf = self._to_dict(default)
1✔
531
                if len(conf.keys()) > 0:
1✔
532
                    log.debug(f"updating keys {list(conf.keys())} from {default}")
1✔
533
                    self.properties.update(conf)
1✔
534
    
535
    def __getitem__(
1✔
536
            self,
537
            key: str
538
    ) -> int | float | str | list[str]:
539
        for section in self._dicts:
1✔
540
            if key in section:
1✔
541
                return section[key].copy() \
1✔
542
                    if hasattr(section[key], 'copy') \
543
                    else section[key]
544
        raise KeyError(f'could not find key {key}')
1✔
545
    
546
    def __setitem__(
1✔
547
            self,
548
            key: str,
549
            value: int | float | str | list[str] | None
550
    ) -> None:
551
        if not (isinstance(value, (int, float, str, list)) or value is None):
1✔
552
            raise TypeError(f'invalid type for key {key}: {type(value)}')
1✔
553
        if value == self[key] and isinstance(value, type(self[key])):
1✔
554
            return
×
555
        if key in self.properties:
1✔
556
            self.properties[key] = value
1✔
557
        elif key in self.auxdata_properties:
1✔
558
            self.auxdata_properties[key] = value
1✔
559
        else:
560
            self.conf[key] = value
1✔
561
        if value is not None:
1✔
562
            if isinstance(value, list):
1✔
563
                value = ' '.join(value)
1✔
564
            value = str(value).encode('unicode-escape').decode()
1✔
565
            value = value.replace(':', '\\:')
1✔
566
        if key in self.properties:
1✔
567
            path = self.userpath_properties
1✔
568
        elif key in self.auxdata_properties:
1✔
569
            path = self.userpath_auxdata_properties
1✔
570
        elif key in self.conf:
1✔
571
            path = self.userpath_conf
1✔
572
        else:
573
            raise KeyError(f'unknown key {key}')
×
574
        if os.path.isfile(path):
1✔
575
            with open(path, 'r') as f:
1✔
576
                content = f.read()
1✔
577
        else:
578
            content = ''
1✔
579
        pattern = self.pattern_key_replace.format(key)
1✔
580
        match = re.search(pattern, content)
1✔
581
        if match:
1✔
582
            repl = f'#{key} =' if value is None else f'{key} = {value}'
1✔
583
            content = content.replace(match.group(), repl)
1✔
584
        else:
585
            content += f'\n{key} = {value}'
1✔
586
        
587
        os.makedirs(os.path.dirname(path), exist_ok=True)
1✔
588
        log.debug(f"writing key '{key}' to '{path}'")
1✔
589
        with open(path, 'w') as f:
1✔
590
            f.write(content)
1✔
591
    
592
    def _to_dict(
1✔
593
            self,
594
            path: str,
595
            str_split: dict[str, str] | None=None
596
    ) -> dict[str, int | float | str | None | list[str]]:
597
        """
598
        Read a properties file into a dictionary.
599
        Converts values into basic python types
600
        
601
        Parameters
602
        ----------
603
        path:
604
            the path to the properties file
605
        str_split:
606
            a dictionary with properties as keys and splitting characters as values
607
            to split a string into a list of strings
608

609
        Returns
610
        -------
611
            the dictionary with the properties
612
        """
613
        out = {}
1✔
614
        if os.path.isfile(path):
1✔
615
            with open(path, 'r') as f:
1✔
616
                for line in f:
1✔
617
                    if re.search(self.pattern, line):
1✔
618
                        match = re.match(re.compile(self.pattern), line)
1✔
619
                        comment, key, value = match.groups()
1✔
620
                        if comment == '':
1✔
621
                            if str_split is not None and key in str_split.keys():
1✔
622
                                value = value.split(str_split[key])
1✔
623
                            else:
624
                                value = self._string_convert(value)
1✔
625
                            out[key] = value
1✔
626
                        else:
627
                            out[key] = None
1✔
628
        return out
1✔
629
    
630
    @staticmethod
1✔
631
    def _string_convert(string):
1✔
632
        if string.lower() == 'none':
1✔
633
            return None
×
634
        elif string.lower() == 'true':
1✔
635
            return True
1✔
636
        elif string.lower() == 'false':
1✔
637
            return False
×
638
        else:
639
            try:
1✔
640
                return int(string)
1✔
641
            except ValueError:
1✔
642
                try:
1✔
643
                    return float(string)
1✔
644
                except ValueError:
1✔
645
                    return string.replace('\\:', ':').replace('\\\\', '\\')
1✔
646
    
647
    def keys(self):
1✔
648
        """
649
        
650
        Returns
651
        -------
652
        list[str]
653
            all known SNAP property keys
654
        """
655
        keys = []
1✔
656
        for item in self._dicts:
1✔
657
            keys.extend(list(item.keys()))
1✔
658
        return sorted(keys)
1✔
659
    
660
    @property
1✔
661
    def userpath(self):
1✔
662
        key = 'snap.userdir'
1✔
663
        if key not in self.keys() or self[key] is None:
1✔
UNCOV
664
            return os.path.join(os.path.expanduser('~'), '.snap')
×
665
        else:
666
            return self[key]
1✔
667
    
668
    @userpath.setter
1✔
669
    def userpath(self, value):
1✔
670
        self['snap.userdir'] = value
1✔
671
    
672
    @property
1✔
673
    def userpath_auxdata_properties(self):
1✔
674
        return os.path.join(os.path.expanduser('~'), '.snap',
1✔
675
                            'etc', 'snap.auxdata.properties')
676
    
677
    @property
1✔
678
    def userpath_properties(self):
1✔
679
        return os.path.join(os.path.expanduser('~'), '.snap',
1✔
680
                            'etc', 'snap.properties')
681
    
682
    @property
1✔
683
    def userpath_conf(self):
1✔
684
        return os.path.join(os.path.expanduser('~'), '.snap',
1✔
685
                            'etc', 'snap.conf')
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