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

funilrys / PyFunceble / 16098639223

06 Jul 2025 11:41AM UTC coverage: 96.648% (-0.01%) from 96.659%
16098639223

push

github

funilrys
Bump version to v4.3.0a24.dev

1 of 1 new or added line in 1 file covered. (100.0%)

14 existing lines in 4 files now uncovered.

11967 of 12382 relevant lines covered (96.65%)

14.23 hits per line

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

95.58
/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
15✔
54
import os
15✔
55
from typing import Any, Optional
15✔
56

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

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

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

79

80
class ConfigLoader:
15✔
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
15✔
93
    _remote_config_location: Optional[str] = None
15✔
94
    path_to_default_config: Optional[str] = None
15✔
95
    _path_to_overwrite_config: Optional[str] = None
15✔
96

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

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

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

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

119
        self.path_to_remote_config = None
3✔
120

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

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

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

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

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

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

145
            return result
15✔
146

147
        return wrapper
15✔
148

149
    @staticmethod
15✔
150
    def conditional_switch(config: dict) -> dict:
15✔
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 (
15✔
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
15✔
173

174
        if (
15✔
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
15✔
182

183
        if (
15✔
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
15✔
192
            config["cli_testing"]["display_mode"]["dots"] = True
15✔
193
            config["cli_testing"]["autocontinue"] = False
15✔
194
            config["cli_testing"]["inactive_db"] = False
15✔
195
            config["cli_testing"]["mining"] = False
15✔
196
            config["cli_testing"]["local_network"] = False
15✔
197
            config["cli_testing"]["preload_file"] = False
15✔
198
            config["cli_testing"]["display_mode"]["percentage"] = False
15✔
199
            config["lookup"]["platform"] = False
15✔
200

201
        return config
15✔
202

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

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

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

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

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

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

229
        return self._path_to_config
15✔
230

231
    @path_to_config.setter
15✔
232
    def path_to_config(self, value: str) -> None:
15✔
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):
15✔
UNCOV
244
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
×
245

246
        self._path_to_config = value
15✔
247

248
    @property
15✔
249
    def path_to_overwrite_config(self) -> Optional[str]:
15✔
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:
15✔
255
            self._path_to_overwrite_config = os.path.join(
15✔
256
                self.config_dir,
257
                ".PyFunceble.overwrite.yaml",
258
            )
259

260
        return self._path_to_overwrite_config
15✔
261

262
    @path_to_overwrite_config.setter
15✔
263
    def path_to_overwrite_config(self, value: str) -> None:
15✔
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):
15✔
UNCOV
275
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
×
276

277
        self._path_to_overwrite_config = value
15✔
278

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

285
        return self._config_dir
15✔
286

287
    @config_dir.setter
15✔
288
    @reload_config
15✔
289
    def config_dir(self, value: str) -> None:
15✔
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):
15✔
301
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
15✔
302

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

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

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

316
        self.config_dir = value
15✔
317

318
        return self
15✔
319

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

326
        return self._custom_config
15✔
327

328
    @custom_config.setter
15✔
329
    @reload_config
15✔
330
    def custom_config(self, value: dict) -> None:
15✔
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):
15✔
343
            raise TypeError(f"<value> should be {dict}, {type(value)} given.")
15✔
344

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

350
    def set_custom_config(self, value: dict) -> "ConfigLoader":
15✔
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
15✔
360

361
        return self
15✔
362

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

369
        return self._merge_upstream
15✔
370

371
    @merge_upstream.setter
15✔
372
    def merge_upstream(self, value: bool) -> None:
15✔
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):
15✔
381
            raise TypeError(f"<value> should be {bool}, {type(value)} given.")
15✔
382

383
        self._merge_upstream = value
15✔
384

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

390
        self.merge_upstream = value
15✔
391

392
        return self
15✔
393

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

400
        return self._remote_config_location
15✔
401

402
    @remote_config_location.setter
15✔
403
    def remote_config_location(self, value: Optional[str]) -> None:
15✔
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):
15✔
412
            raise TypeError(f"<value> should be {str}, {type(value)} given.")
15✔
413

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

422
        self._remote_config_location = value
15✔
423

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

429
        self.remote_config_location = value
15✔
430

431
        return self
15✔
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(
15✔
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():
15✔
468
            IANADownloader().start()
15✔
469
            PublicSuffixDownloader().start()
15✔
470
            UserAgentsDownloader().start()
15✔
471

472
    def get_config_file_content(self) -> dict:
15✔
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:
15✔
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
15✔
486

487
        def download_remote_config(src: str, dest: str = None) -> None:
15✔
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")):
15✔
UNCOV
498
                if dest is None:
×
UNCOV
499
                    destination = os.path.join(
×
500
                        self.config_dir,
501
                        os.path.basename(dest),
502
                    )
503
                else:
UNCOV
504
                    destination = dest
×
505

UNCOV
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:
15✔
518
            config = self.dict_helper.from_yaml_file(self.path_to_config)
15✔
519
        except (MarkedYAMLError, FileNotFoundError):
15✔
520
            self.file_helper.set_path(self.path_to_default_config).copy(
15✔
521
                self.path_to_config, create_parent=True
522
            )
523
            config = self.dict_helper.from_yaml_file(self.path_to_config)
15✔
524

525
        config_comparer = ConfigComparison(
15✔
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():
15✔
544
            # Early load of the overwrite configuration to allow usage of defined
545
            # proxy settings.
546
            overwrite_data = self.dict_helper.from_yaml_file(
15✔
547
                self.path_to_overwrite_config
548
            )
549

550
            if isinstance(overwrite_data, dict):
15✔
551
                config = Merge(overwrite_data).into(config)
15✔
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(
15✔
558
            config,
559
        )
560

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

569
        if (
15✔
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

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

578
        if self.file_helper.set_path(self.path_to_overwrite_config).exists():
15✔
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(
15✔
582
                self.path_to_overwrite_config
583
            )
584

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

588
        return config
15✔
589

590
    def get_configured_value(self, entry: str) -> Any:
15✔
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():
15✔
607
            raise RuntimeError("Configuration not loaded, yet.")
15✔
608

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

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

614
    def reload(self, keep_custom: bool = False) -> "ConfigLoader":
15✔
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)
15✔
624
        self.start()
15✔
625

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

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

634
        config = self.get_config_file_content()
15✔
635

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

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

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

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

657
        if "special_rules" in config and config["special_rules"]:
15✔
UNCOV
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()
15✔
662

663
        self.__config_loaded = True
15✔
664

665
        return self
15✔
666

667
    def destroy(self, keep_custom: bool = False) -> "ConfigLoader":
15✔
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:
15✔
677
            PyFunceble.storage.CONFIGURATION = Box(
15✔
678
                {},
679
            )
680
            PyFunceble.storage.FLATTEN_CONFIGURATION = {}
15✔
681
            PyFunceble.storage.HTTP_CODES = Box({})
15✔
682
            PyFunceble.storage.PLATFORM = Box({})
15✔
683
            PyFunceble.storage.LINKS = Box({})
15✔
684
            PyFunceble.storage.PROXY = Box({})
15✔
685
            PyFunceble.storage.SPECIAL_RULES = Box({})
15✔
686
        except (AttributeError, TypeError):  # pragma: no cover ## Safety.
687
            pass
688

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

693
        self.__config_loaded = False
15✔
694

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