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

funilrys / PyFunceble / 17046631407

18 Aug 2025 04:35PM UTC coverage: 96.546% (-0.1%) from 96.648%
17046631407

push

github

funilrys
Fix issue with version extraction.

11963 of 12391 relevant lines covered (96.55%)

0.97 hits per line

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

94.74
/PyFunceble/config/loader.py
1
"""
2
The tool to check the availability or syntax of domain, IP or URL.
3

4
::
5

6

7
    ██████╗ ██╗   ██╗███████╗██╗   ██╗███╗   ██╗ ██████╗███████╗██████╗ ██╗     ███████╗
8
    ██╔══██╗╚██╗ ██╔╝██╔════╝██║   ██║████╗  ██║██╔════╝██╔════╝██╔══██╗██║     ██╔════╝
9
    ██████╔╝ ╚████╔╝ █████╗  ██║   ██║██╔██╗ ██║██║     █████╗  ██████╔╝██║     █████╗
10
    ██╔═══╝   ╚██╔╝  ██╔══╝  ██║   ██║██║╚██╗██║██║     ██╔══╝  ██╔══██╗██║     ██╔══╝
11
    ██║        ██║   ██║     ╚██████╔╝██║ ╚████║╚██████╗███████╗██████╔╝███████╗███████╗
12
    ╚═╝        ╚═╝   ╚═╝      ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝ ╚══════╝╚══════╝
13

14
Provides the configuration loader.
15

16
Author:
17
    Nissar Chababy, @funilrys, contactTATAfunilrysTODTODcom
18

19
Special thanks:
20
    https://pyfunceble.github.io/#/special-thanks
21

22
Contributors:
23
    https://pyfunceble.github.io/#/contributors
24

25
Project link:
26
    https://github.com/funilrys/PyFunceble
27

28
Project documentation:
29
    https://docs.pyfunceble.com
30

31
Project homepage:
32
    https://pyfunceble.github.io/
33

34
License:
35
::
36

37

38
    Copyright 2017, 2018, 2019, 2020, 2022, 2023, 2024, 2025 Nissar Chababy
39

40
    Licensed under the Apache License, Version 2.0 (the "License");
41
    you may not use this file except in compliance with the License.
42
    You may obtain a copy of the License at
43

44
        https://www.apache.org/licenses/LICENSE-2.0
45

46
    Unless required by applicable law or agreed to in writing, software
47
    distributed under the License is distributed on an "AS IS" BASIS,
48
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
49
    See the License for the specific language governing permissions and
50
    limitations under the License.
51
"""
52

53
import functools
1✔
54
import os
1✔
55
from typing import Any, Optional
1✔
56

57
try:
1✔
58
    import importlib.resources as package_resources
1✔
59
except ImportError:  # pragma: no cover ## Retro compatibility
60
    import importlib_resources as package_resources
61

62
from box import Box
1✔
63
from dotenv import load_dotenv
1✔
64
from yaml.error import MarkedYAMLError
1✔
65

66
import PyFunceble.cli.storage
1✔
67
import PyFunceble.storage
1✔
68
from PyFunceble.config.compare import ConfigComparison
1✔
69
from PyFunceble.dataset.user_agent import UserAgentDataset
1✔
70
from PyFunceble.downloader.iana import IANADownloader
1✔
71
from PyFunceble.downloader.public_suffix import PublicSuffixDownloader
1✔
72
from PyFunceble.downloader.user_agents import UserAgentsDownloader
1✔
73
from PyFunceble.helpers.dict import DictHelper
1✔
74
from PyFunceble.helpers.download import DownloadHelper
1✔
75
from PyFunceble.helpers.environment_variable import EnvironmentVariableHelper
1✔
76
from PyFunceble.helpers.file import FileHelper
1✔
77
from PyFunceble.helpers.merge import Merge
1✔
78

79

80
class ConfigLoader:
1✔
81
    """
82
    Provides the interface which loads and updates the configuration (if needed).
83

84
    :param merge_upstream:
85
        Authorizes the merging of the upstream configuration.
86

87
        .. note::
88
            If value is set to :py:class:`None` (default), we fallback to the
89
            :code:`PYFUNCEBLE_AUTO_CONFIGURATION` environment variable.
90
    """
91

92
    _path_to_config: Optional[str] = None
1✔
93
    _remote_config_location: Optional[str] = None
1✔
94
    path_to_default_config: Optional[str] = None
1✔
95
    _path_to_overwrite_config: Optional[str] = None
1✔
96

97
    _custom_config: dict = {}
1✔
98
    _merge_upstream: bool = False
1✔
99
    _config_dir: Optional[str] = None
1✔
100
    __config_loaded: bool = False
1✔
101

102
    file_helper: Optional[FileHelper] = None
1✔
103
    dict_helper: Optional[DictHelper] = None
1✔
104

105
    def __init__(
106
        self, merge_upstream: Optional[bool] = None, *, config_dir: Optional[str] = None
107
    ) -> None:
108
        with package_resources.path(
1✔
109
            "PyFunceble.data.infrastructure",
110
            PyFunceble.storage.DISTRIBUTED_CONFIGURATION_FILENAME,
111
        ) as file_path:
112
            self.path_to_default_config = str(file_path)
1✔
113

114
        if config_dir is not None:
1✔
115
            self.config_dir = config_dir
×
116
        else:
117
            self.config_dir = PyFunceble.storage.CONFIG_DIRECTORY
1✔
118

119
        self.path_to_remote_config = None
1✔
120

121
        if merge_upstream is not None:
1✔
122
            self.merge_upstream = merge_upstream
×
123
        elif EnvironmentVariableHelper("PYFUNCEBLE_AUTO_CONFIGURATION").exists():
1✔
124
            self.merge_upstream = True
1✔
125

126
        self.file_helper = FileHelper()
1✔
127
        self.dict_helper = DictHelper()
1✔
128

129
    def __del__(self) -> None:
1✔
130
        self.destroy()
1✔
131

132
    def reload_config(func):  # pylint: disable=no-self-argument
1✔
133
        """
134
        Reload the configuration (if it was already loaded) after launching the
135
        decorated method.
136
        """
137

138
        @functools.wraps(func)
1✔
139
        def wrapper(self, *args, **kwargs):
1✔
140
            result = func(self, *args, **kwargs)  # pylint: disable=not-callable
1✔
141

142
            if self.is_already_loaded():
1✔
143
                self.reload(keep_custom=True)
1✔
144

145
            return result
1✔
146

147
        return wrapper
1✔
148

149
    @staticmethod
1✔
150
    def conditional_switch(config: dict) -> dict:
1✔
151
        """
152
        Given the configuration that we are going to load, switches some of
153
        setting.
154

155
        :param config:
156
            The configuration we are going to load.
157
        """
158

159
        # pylint: disable=too-many-boolean-expressions
160
        if (
1✔
161
            "cli_testing" in config
162
            and "ci" in config["cli_testing"]
163
            and "active" in config["cli_testing"]["ci"]
164
            and "autocontinue" in config["cli_testing"]
165
            and bool(config["cli_testing"]["ci"]["active"])
166
            and not bool(config["cli_testing"]["autocontinue"])
167
        ):
168
            # Conditional autocontinue.
169
            # If we are under continuous integration, the autocontinue should be
170
            # activated.
171

172
            config["cli_testing"]["autocontinue"] = True
1✔
173

174
        if (
1✔
175
            "lookup" in config
176
            and "timeout" in config["lookup"]
177
            and config["lookup"]["timeout"]
178
            and config["lookup"]["timeout"] < 0
179
        ):
180
            # If timeout is set to a negative digit, switch to the default one.
181
            config["lookup"]["timeout"] = 5
1✔
182

183
        if (
1✔
184
            "cli_testing" in config
185
            and "testing_mode" in config["cli_testing"]
186
            and "platform_contribution" in config["cli_testing"]["testing_mode"]
187
            and config["cli_testing"]["testing_mode"]["platform_contribution"]
188
        ):
189
            # If we are under a special testing mode. We shouldn't generate
190
            # any files
191
            config["cli_testing"]["file_generation"]["no_file"] = True
1✔
192
            config["cli_testing"]["display_mode"]["dots"] = True
1✔
193
            config["cli_testing"]["autocontinue"] = False
1✔
194
            config["cli_testing"]["inactive_db"] = False
1✔
195
            config["cli_testing"]["mining"] = False
1✔
196
            config["cli_testing"]["local_network"] = False
1✔
197
            config["cli_testing"]["preload_file"] = False
1✔
198
            config["cli_testing"]["display_mode"]["percentage"] = False
1✔
199
            config["lookup"]["platform"] = False
1✔
200

201
        return config
1✔
202

203
    def is_already_loaded(self) -> bool:
1✔
204
        """
205
        Checks if the configuration was already loaded.
206
        """
207

208
        return bool(PyFunceble.storage.CONFIGURATION)
1✔
209

210
    def __is_completely_loaded(self) -> bool:
1✔
211
        """
212
        Checks if the configuration was completely loaded.
213
        """
214

215
        return self.is_already_loaded() and bool(self.__config_loaded)
1✔
216

217
    @property
1✔
218
    def path_to_config(self) -> Optional[str]:
1✔
219
        """
220
        Provides the current state of the :code:`_path_to_config` attribute.
221
        """
222

223
        if self._path_to_config is None:
1✔
224
            self._path_to_config = os.path.join(
1✔
225
                self.config_dir,
226
                PyFunceble.storage.CONFIGURATION_FILENAME,
227
            )
228

229
        return self._path_to_config
1✔
230

231
    @path_to_config.setter
1✔
232
    def path_to_config(self, value: str) -> None:
1✔
233
        """
234
        Sets the path to the configuration file.
235

236
        :param value:
237
            The value to set.
238

239
        :raise TypeError:
240
            When value is not a :py:class:`str`.
241
        """
242

243
        if not isinstance(value, str):
1✔
244
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
×
245

246
        self._path_to_config = value
1✔
247

248
    @property
1✔
249
    def path_to_overwrite_config(self) -> Optional[str]:
1✔
250
        """
251
        Provides the current state of the :code:`_path_to_overwrite_config` attribute.
252
        """
253

254
        if self._path_to_overwrite_config is None:
1✔
255
            self._path_to_overwrite_config = os.path.join(
1✔
256
                self.config_dir,
257
                ".PyFunceble.overwrite.yaml",
258
            )
259

260
        return self._path_to_overwrite_config
1✔
261

262
    @path_to_overwrite_config.setter
1✔
263
    def path_to_overwrite_config(self, value: str) -> None:
1✔
264
        """
265
        Sets the path to the overwrite configuration file.
266

267
        :param value:
268
            The value to set.
269

270
        :raise TypeError:
271
            When value is not a :py:class:`str`.
272
        """
273

274
        if not isinstance(value, str):
1✔
275
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
×
276

277
        self._path_to_overwrite_config = value
1✔
278

279
    @property
1✔
280
    def config_dir(self) -> Optional[str]:
1✔
281
        """
282
        Provides the current state of the :code:`_config_dir` attribute.
283
        """
284

285
        return self._config_dir
1✔
286

287
    @config_dir.setter
1✔
288
    @reload_config
1✔
289
    def config_dir(self, value: str) -> None:
1✔
290
        """
291
        Sets the configuration directory.
292

293
        :param value:
294
            The value to set.
295

296
        :raise TypeError:
297
            When value is not a :py:class:`str`.
298
        """
299

300
        if not isinstance(value, str):
1✔
301
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
1✔
302

303
        self._config_dir = value
1✔
304
        # Reset the path to the configuration file.
305
        self._path_to_config = None
1✔
306
        self._path_to_overwrite_config = None
1✔
307

308
    def set_config_dir(self, value: str) -> "ConfigLoader":
1✔
309
        """
310
        Sets the configuration directory.
311

312
        :param value:
313
            The value to set.
314
        """
315

316
        self.config_dir = value
1✔
317

318
        return self
1✔
319

320
    @property
1✔
321
    def custom_config(self) -> dict:
1✔
322
        """
323
        Provides the current state of the :code:`_custom_config` attribute.
324
        """
325

326
        return self._custom_config
1✔
327

328
    @custom_config.setter
1✔
329
    @reload_config
1✔
330
    def custom_config(self, value: dict) -> None:
1✔
331
        """
332
        Sets the custom configuration to set after loading.
333

334
        Side Effect:
335
            Directly inject into the configuration variables if it was already
336
            loaded.
337

338
        :raise TypeError:
339
            When :code:`value` is not a :py:class:`dict`.
340
        """
341

342
        if not isinstance(value, dict):
1✔
343
            raise TypeError(f"<value> should be {dict}, {type(value)} given.")
1✔
344

345
        if not self._custom_config:
1✔
346
            self._custom_config = value
1✔
347
        else:
348
            self._custom_config.update(value)
1✔
349

350
    def set_custom_config(self, value: dict) -> "ConfigLoader":
1✔
351
        """
352
        Sets the custom configuration to set after loading.
353

354
        Side Effect:
355
            Directly inject into the configuration variables if it was already
356
            loaded.
357
        """
358

359
        self.custom_config = value
1✔
360

361
        return self
1✔
362

363
    @property
1✔
364
    def merge_upstream(self) -> bool:
1✔
365
        """
366
        Provides the current state of the :code:`_merge_upstream` attribute.
367
        """
368

369
        return self._merge_upstream
1✔
370

371
    @merge_upstream.setter
1✔
372
    def merge_upstream(self, value: bool) -> None:
1✔
373
        """
374
        Updates the value of :code:`_merge_upstream` attribute.
375

376
        :raise TypeError:
377
            When :code:`value` is not a :py:class:`bool`.
378
        """
379

380
        if not isinstance(value, bool):
1✔
381
            raise TypeError(f"<value> should be {bool}, {type(value)} given.")
1✔
382

383
        self._merge_upstream = value
1✔
384

385
    def set_merge_upstream(self, value: bool) -> "ConfigLoader":
1✔
386
        """
387
        Updates the value of :code:`_merge_upstream` attribute.
388
        """
389

390
        self.merge_upstream = value
1✔
391

392
        return self
1✔
393

394
    @property
1✔
395
    def remote_config_location(self) -> Optional[str]:
1✔
396
        """
397
        Provides the current state of the :code:`_remote_config_location` attribute.
398
        """
399

400
        return self._remote_config_location
1✔
401

402
    @remote_config_location.setter
1✔
403
    def remote_config_location(self, value: Optional[str]) -> None:
1✔
404
        """
405
        Updates the value of :code:`_remote_config_location` attribute.
406

407
        :raise TypeError:
408
            When :code:`value` is not a :py:class:`str`.
409
        """
410

411
        if value is not None and not isinstance(value, str):
1✔
412
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
1✔
413

414
        if not value.startswith("http") and not value.startswith("https"):
1✔
415
            self.path_to_remote_config = os.path.realpath(value)
1✔
416
        else:
417
            self.path_to_remote_config = os.path.join(
1✔
418
                self.config_dir,
419
                ".PyFunceble.remote.yaml",
420
            )
421

422
        self._remote_config_location = value
1✔
423

424
    def set_remote_config_location(self, value: Optional[str]) -> "ConfigLoader":
1✔
425
        """
426
        Updates the value of :code:`_remote_config_location` attribute.
427
        """
428

429
        self.remote_config_location = value
1✔
430

431
        return self
1✔
432

433
    def install_missing_infrastructure_files(
434
        self,
435
    ) -> "ConfigLoader":  # pragma: no cover ## Copy method already tested
436
        """
437
        Installs the missing files (when needed).
438

439
        .. note::
440
            Installed if missing:
441
                - The configuration file.
442
                - The directory structure file.
443
        """
444

445
        if not self.is_already_loaded():
446
            if not self.file_helper.set_path(self.path_to_config).exists():
447
                self.file_helper.set_path(self.path_to_default_config).copy(
448
                    self.path_to_config, create_parent=True
449
                )
450

451
        return self
452

453
    def download_dynamic_infrastructure_files(
1✔
454
        self,
455
    ) -> "ConfigLoader":
456
        """
457
        Downloads all the dynamically (generated) infrastructure files.
458

459
        .. note::
460
            Downloaded if missing:
461
                - The IANA dump file.
462
                - The Public Suffix dump file.
463
        """
464

465
        ## pragma: no cover ## Underlying download methods already tested.
466

467
        if not self.is_already_loaded():
1✔
468
            IANADownloader().start()
1✔
469
            PublicSuffixDownloader().start()
1✔
470
            UserAgentsDownloader().start()
1✔
471

472
    def get_config_file_content(self) -> dict:
1✔
473
        """
474
        Provides the content of the configuration file or the one already loaded.
475
        """
476

477
        def is_3_x_version(config: dict) -> bool:
1✔
478
            """
479
            Checks if the given configuration is an old one.
480

481
            :param config:
482
                The config to work with.
483
            """
484

485
            return config and "days_between_inactive_db_clean" in config
1✔
486

487
        def download_remote_config(src: str, dest: str = None) -> None:
1✔
488
            """
489
            Downloads the remote configuration.
490

491
            :param src:
492
                The source to download from.
493
            :param dest:
494
                The destination to download
495
            """
496

497
            if src and (src.startswith("http") or src.startswith("https")):
1✔
498
                if dest is None:
×
499
                    destination = os.path.join(
×
500
                        self.config_dir,
501
                        os.path.basename(dest),
502
                    )
503
                else:
504
                    destination = dest
×
505

506
                DownloadHelper(
×
507
                    src,
508
                    certificate_validation=(
509
                        PyFunceble.storage.CONFIGURATION.verify_ssl_certificate
510
                        if PyFunceble.storage.CONFIGURATION
511
                        else True
512
                    ),
513
                    own_proxy_handler=True,
514
                    proxies=config["proxies"],
515
                ).download_text(destination=destination)
516

517
        try:
1✔
518
            config = self.dict_helper.from_yaml_file(self.path_to_config)
1✔
519
        except (MarkedYAMLError, FileNotFoundError):
1✔
520
            self.file_helper.set_path(self.path_to_default_config).copy(
1✔
521
                self.path_to_config, create_parent=True
522
            )
523
            config = self.dict_helper.from_yaml_file(self.path_to_config)
1✔
524

525
        config_comparer = ConfigComparison(
1✔
526
            local_config=config,
527
            upstream_config=self.dict_helper.from_yaml_file(
528
                self.path_to_default_config
529
            ),
530
        )
531

532
        if (
533
            not config
534
            or not isinstance(config, dict)
535
            or self.merge_upstream
536
            or is_3_x_version(config)
537
            or not config_comparer.is_local_identical()
538
        ):  # pragma: no cover ## Testing the underlying comparison method is sufficient
539
            config = config_comparer.get_merged()
540

541
            self.dict_helper.set_subject(config).to_yaml_file(self.path_to_config)
542

543
        if self.file_helper.set_path(self.path_to_overwrite_config).exists():
1✔
544
            # Early load of the overwrite configuration to allow usage of defined
545
            # proxy settings.
546
            overwrite_data = self.dict_helper.from_yaml_file(
1✔
547
                self.path_to_overwrite_config
548
            )
549

550
            if isinstance(overwrite_data, dict):
1✔
551
                config = Merge(overwrite_data).into(config)
1✔
552
        else:  # pragma: no cover  ## Just make it visible to end-user.
553
            self.file_helper.write("")
554

555
        # Now we preset the storage to enforce the usage of the configuration
556
        # in any downloads.
557
        PyFunceble.storage.CONFIGURATION = Box(
1✔
558
            config,
559
        )
560

561
        if not self.__is_completely_loaded():
1✔
562
            self.install_missing_infrastructure_files()
1✔
563
            self.download_dynamic_infrastructure_files()
1✔
564
            download_remote_config(
1✔
565
                self.remote_config_location, self.path_to_remote_config
566
            )
567
            download_remote_config(self.path_to_config)
1✔
568

569
        if (
1✔
570
            self.path_to_remote_config
571
            and self.file_helper.set_path(self.path_to_remote_config).exists()
572
        ):
573
            remote_data = self.dict_helper.from_yaml_file(self.path_to_remote_config)
×
574

575
            if isinstance(remote_data, dict):
×
576
                config = Merge(remote_data).into(config)
×
577

578
        if self.file_helper.set_path(self.path_to_overwrite_config).exists():
1✔
579
            # Load the overwrite configuration again to ensure that user defined
580
            # settings are always applied - last one wins.
581
            overwrite_data = self.dict_helper.from_yaml_file(
1✔
582
                self.path_to_overwrite_config
583
            )
584

585
            if isinstance(overwrite_data, dict):
1✔
586
                config = Merge(overwrite_data).into(config)
1✔
587

588
        return config
1✔
589

590
    def get_configured_value(self, entry: str) -> Any:
1✔
591
        """
592
        Provides the currently configured value.
593

594
        :param entry:
595
            An entry to check.
596

597
            multilevel should be separated with a point.
598

599
        :raise RuntimeError:
600
            When the configuration is not loaded yet.
601

602
        :raise ValueError:
603
            When the given :code:`entry` is not found.
604
        """
605

606
        if not self.is_already_loaded():
1✔
607
            raise RuntimeError("Configuration not loaded, yet.")
1✔
608

609
        if entry not in PyFunceble.storage.FLATTEN_CONFIGURATION:
1✔
610
            raise ValueError(f"<entry> ({entry!r}) not in loaded configuration.")
1✔
611

612
        return PyFunceble.storage.FLATTEN_CONFIGURATION[entry]
1✔
613

614
    def reload(self, keep_custom: bool = False) -> "ConfigLoader":
1✔
615
        """
616
        Reloads the configuration.
617

618
        :param bool keep_custom:
619
            If set to :code:`True`, we keep the custom configuration, otherwise
620
            we delete it.
621
        """
622

623
        self.destroy(keep_custom=keep_custom)
1✔
624
        self.start()
1✔
625

626
    def start(self) -> "ConfigLoader":
1✔
627
        """
628
        Starts the loading processIs.
629
        """
630

631
        load_dotenv(os.path.join(self.config_dir, ".env"))
1✔
632
        load_dotenv(os.path.join(self.config_dir, PyFunceble.storage.ENV_FILENAME))
1✔
633

634
        config = self.get_config_file_content()
1✔
635

636
        if self.custom_config:
1✔
637
            config = Merge(self.custom_config).into(config)
1✔
638

639
        config = self.conditional_switch(config)
1✔
640

641
        PyFunceble.storage.CONFIGURATION = Box(
1✔
642
            config,
643
        )
644
        PyFunceble.storage.FLATTEN_CONFIGURATION = DictHelper(
1✔
645
            PyFunceble.storage.CONFIGURATION
646
        ).flatten()
647
        PyFunceble.storage.HTTP_CODES = Box(
1✔
648
            config["http_codes"],
649
        )
650
        if "platform" in config and config["platform"]:
1✔
651
            PyFunceble.storage.PLATFORM = Box(config["platform"])
1✔
652
        PyFunceble.storage.LINKS = Box(config["links"])
1✔
653

654
        if "proxy" in config and config["proxy"]:
1✔
655
            PyFunceble.storage.PROXY = Box(config["proxy"])
1✔
656

657
        if "special_rules" in config and config["special_rules"]:
1✔
658
            PyFunceble.storage.SPECIAL_RULES = config["special_rules"]
×
659

660
        # Early load user agents to allow usage of defined user agents.
661
        UserAgentDataset().get_latest()
1✔
662

663
        self.__config_loaded = True
1✔
664

665
        return self
1✔
666

667
    def destroy(self, keep_custom: bool = False) -> "ConfigLoader":
1✔
668
        """
669
        Destroys everything loaded.
670

671
        :param bool keep_custom:
672
            If set to :code:`True`, we keep the custom configuration, otherwise
673
            we delete it.
674
        """
675

676
        try:
1✔
677
            PyFunceble.storage.CONFIGURATION = Box(
1✔
678
                {},
679
            )
680
            PyFunceble.storage.FLATTEN_CONFIGURATION = {}
1✔
681
            PyFunceble.storage.HTTP_CODES = Box({})
1✔
682
            PyFunceble.storage.PLATFORM = Box({})
1✔
683
            PyFunceble.storage.LINKS = Box({})
1✔
684
            PyFunceble.storage.PROXY = Box({})
1✔
685
            PyFunceble.storage.SPECIAL_RULES = Box({})
1✔
686
        except (AttributeError, TypeError):  # pragma: no cover ## Safety.
687
            pass
688

689
        if not keep_custom:
1✔
690
            # This is not a mistake.
691
            self._custom_config = {}
1✔
692

693
        self.__config_loaded = False
1✔
694

695
        return self
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