Repository: miurahr/aqtinstall
Pull request #246 (2021-05-23 04:02:04)

Improve settings.ini configuration handlings
- Move more constants into settings.ini
- Introduce -c/--config CONFIG command line option
- Multi-line URL for blacklist configuration 
- Azure: use -c option to pass custom configuration
- Azure: windows: use -b option to avoid error

Signed-off-by: Hiroshi Miura <miurahr@linux.com>


Patch:
diff --git a/aqt/helper.py b/aqt/helper.py
index 871330f..9fac5bd 100644
--- a/aqt/helper.py
+++ b/aqt/helper.py
@@ -29,6 +29,7 @@ import os
 import sys
 import xml.etree.ElementTree as ElementTree
 from typing import List, Optional
+from urllib.parse import urlparse
 
 import requests
 import requests.adapters
@@ -52,9 +53,10 @@ def getUrl(url: str, timeout, logger) -> str:
         session.mount("https://", adapter)
         try:
             r = requests.get(url, allow_redirects=False, timeout=timeout)
-            if r.status_code == 302:
+            if 300 < r.status_code < 309:
+                logger.info("Asked to redirect({}) to: {}".format(r.status_code, r.headers["Location"]))
                 newurl = altlink(r.url, r.headers["Location"], logger=logger)
-                logger.info("Redirected URL: {}".format(newurl))
+                logger.info("Redirected: {}".format(urlparse(newurl).hostname))
                 r = session.get(newurl, stream=True, timeout=timeout)
         except (
             ConnectionResetError,
@@ -82,9 +84,10 @@ def downloadBinaryFile(url: str, out: str, hash_algo: str, exp: str, timeout, lo
         session.mount("https://", adapter)
         try:
             r = session.get(url, allow_redirects=False, stream=True, timeout=timeout)
-            if r.status_code == 302:
+            if 300 < r.status_code < 309:
+                logger.info("Asked to redirect({}) to: {}".format(r.status_code, r.headers["Location"]))
                 newurl = altlink(r.url, r.headers["Location"], logger=logger)
-                logger.info("Redirected URL: {}".format(newurl))
+                logger.info("Redirected: {}".format(urlparse(newurl).hostname))
                 r = session.get(newurl, stream=True, timeout=timeout)
         except requests.exceptions.ConnectionError as e:
             logger.error("Connection error: %s" % e.args)
@@ -118,7 +121,7 @@ def altlink(url: str, alt: str, logger=None):
     if logger is None:
         logger = logging.getLogger(__name__)
     blacklist = Settings().blacklist  # type: Optional[List[str]]
-    if blacklist is None or not any(alt.startswith(b) for b in blacklist):
+    if not any(alt.startswith(b) for b in blacklist):
         return alt
     try:
         m = _get_meta(url)
@@ -159,6 +162,23 @@ def altlink(url: str, alt: str, logger=None):
             )
 
 
+class MyConfigParser(configparser.ConfigParser):
+    def getlist(self, section: str, option: str, fallback=[]) -> List[str]:
+        value = self.get(section, option)
+        try:
+            result = list(filter(None, (x.strip() for x in value.splitlines())))
+        except Exception:
+            result = fallback
+        return result
+
+    def getlistint(self, section: str, option: str, fallback=[]):
+        try:
+            result = [int(x) for x in self.getlist(section, option)]
+        except Exception:
+            result = fallback
+        return result
+
+
 class Settings(object):
     """Class to hold configuration and settings.
     Actual values are stored in 'settings.ini' file.
@@ -169,31 +189,30 @@ class Settings(object):
     _shared_state = {
         "config": None,
         "_combinations": None,
-        "_concurrency": None,
-        "_blacklist": None,
         "_lock": multiprocessing.Lock(),
     }
 
-    def __init__(self, config_path=None):
+    def __init__(self, file=None):
         self.__dict__ = self._shared_state
         if self.config is None:
             with self._lock:
                 if self.config is None:
-                    self.config = configparser.ConfigParser()
+                    self.config = MyConfigParser()
                     # load default config file
                     with open(
                         os.path.join(os.path.dirname(__file__), "settings.ini"), "r"
                     ) as f:
                         self.config.read_file(f)
                     # load custom file
-                    if config_path is not None:
-                        self.config.read(config_path)
-                    self._concurrency = self.config.getint(
-                        "aqt", "concurrency", fallback=4
-                    )
-                    self._blacklist = ast.literal_eval(
-                        self.config.get("mirrors", "blacklist", fallback="[]")
-                    )
+                    if file is not None:
+                        if isinstance(file, str):
+                            result = self.config.read(file)
+                            if len(result) == 0:
+                                raise IOError("Fails to load specified config file {}".format(file))
+                        else:
+                            # passed through command line argparse.FileType("r")
+                            self.config.read_file(file)
+                            file.close()
                     # load combinations
                     with open(
                         os.path.join(os.path.dirname(__file__), "combinations.json"),
@@ -241,7 +260,7 @@ class Settings(object):
         :return: concurrency
         :rtype: int
         """
-        return self._concurrency
+        return self.config.getint("aqt", "concurrency", fallback=4)
 
     @property
     def blacklist(self):
@@ -250,4 +269,24 @@ class Settings(object):
         :returns: list of site URLs(scheme and host part)
         :rtype: List[str]
         """
-        return self._blacklist
+        return self.config.getlist("mirrors", "blacklist", fallback=[])
+
+    @property
+    def baseurl(self):
+        return self.config.get("aqt", "baseurl", fallback="https://download.qt.io")
+
+    @property
+    def connection_timeout(self):
+        return self.config.getfloat("aqt", "connection_timeout", fallback=3.5)
+
+    @property
+    def response_timeout(self):
+        return self.config.getfloat("aqt", "response_timeout", fallback=3.5)
+
+    @property
+    def fallbacks(self):
+        return self.config.getlist("mirrors", "fallbacks", fallback=[])
+
+    @property
+    def zipcmd(self):
+        return self.config.get("aqt", "7zcmd", fallback="7z")
diff --git a/aqt/installer.py b/aqt/installer.py
index 8bafa5b..ed4eb0f 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -70,26 +70,12 @@ class ExtractionError(Exception):
     pass
 
 
-BASE_URL = "https://download.qt.io"
-FALLBACK_URLS = [
-    "https://mirrors.ocf.berkeley.edu/qt",
-    "https://ftp.jaist.ac.jp/pub/qtproject",
-    "http://ftp1.nluug.nl/languages/qt",
-    "https://mirrors.dotsrc.org/qtproject",
-]
-
-
 class Cli:
     """CLI main class to parse command line argument and launch proper functions."""
 
-    __slot__ = ["parser", "combinations", "logger"]
+    __slot__ = ["parser", "combinations", "logger", "settings"]
 
-    def __init__(self, env_key="AQT_CONFIG"):
-        config = os.getenv(env_key, None)
-        if config is not None and os.path.exists(config):
-            self.settings = Settings(config)
-        else:
-            self.settings = Settings()
+    def __init__(self):
         self._create_parser()
 
     def _check_tools_arg_combination(self, os_name, tool_name, arch):
@@ -228,7 +214,7 @@ class Cli:
         if args.timeout is not None:
             timeout = (args.timeout, args.timeout)
         else:
-            timeout = (5, 5)
+            timeout = (self.settings.connection_timeout, self.settings.response_timeout)
         arch = self._set_arch(args, arch, os_name, target, qt_version)
         modules = args.modules
         sevenzip = self._set_sevenzip(args.external)
@@ -241,7 +227,7 @@ class Cli:
                 exit(1)
             base = args.base
         else:
-            base = BASE_URL
+            base = self.settings.baseurl
         archives = args.archives
         if args.noarchives:
             if modules is None:
@@ -298,7 +284,7 @@ class Cli:
                     target,
                     qt_version,
                     arch,
-                    random.choice(FALLBACK_URLS),
+                    random.choice(self.settings.fallbacks),
                     subarchives=archives,
                     modules=modules,
                     logging=self.logger,
@@ -340,7 +326,7 @@ class Cli:
         if args.base is not None:
             base = args.base
         else:
-            base = BASE_URL
+            base = self.settings.baseurl
         qt_ver_num = qt_version.replace(".", "")
         packages = ["qt.qt5.{}.{}".format(qt_ver_num, arch)]
         if args.archives is not None:
@@ -378,15 +364,15 @@ class Cli:
         if args.base is not None:
             base = args.base
         else:
-            base = BASE_URL
+            base = self.settings.baseurl
         if args.timeout is not None:
             timeout = (args.timeout, args.timeout)
         else:
-            timeout = (5, 5)
+            timeout = (self.settings.connection_timeout, self.settings.response_timeout)
         sevenzip = self._set_sevenzip(args.external)
         if EXT7Z and sevenzip is None:
             # override when py7zr is not exist
-            sevenzip = self._set_sevenzip("7z")
+            sevenzip = self._set_sevenzip(self.settings.zipcmd)
         modules = args.modules
         archives = args.archives
         all_extra = True if modules is not None and "all" in modules else False
@@ -417,7 +403,7 @@ class Cli:
                     os_name,
                     target,
                     qt_version,
-                    random.choice(FALLBACK_URLS),
+                    random.choice(self.settings.fallbacks),
                     subarchives=archives,
                     modules=modules,
                     logging=self.logger,
@@ -463,17 +449,17 @@ class Cli:
         sevenzip = self._set_sevenzip(args.external)
         if EXT7Z and sevenzip is None:
             # override when py7zr is not exist
-            sevenzip = self._set_sevenzip("7z")
+            sevenzip = self._set_sevenzip(self.settings.zipcmd)
         version = args.version
         keep = args.keep
         if args.base is not None:
             base = args.base
         else:
-            base = BASE_URL
+            base = self.settings.baseurl
         if args.timeout is not None:
             timeout = (args.timeout, args.timeout)
         else:
-            timeout = (5, 5)
+            timeout = (self.settings.connection_timeout, self.settings.response_timeout)
         if not self._check_tools_arg_combination(os_name, tool_name, arch):
             self.logger.warning(
                 "Specified target combination is not valid: {} {} {}".format(
@@ -500,7 +486,7 @@ class Cli:
                     tool_name,
                     version,
                     arch,
-                    random.choice(FALLBACK_URLS),
+                    random.choice(self.settings.fallbacks),
                     logging=self.logger,
                     timeout=timeout,
                 )
@@ -524,9 +510,9 @@ class Cli:
         host = args.host
         target = args.target
         try:
-            pl = PackagesList(qt_version, host, target, BASE_URL)
+            pl = PackagesList(qt_version, host, target, self.settings.baseurl)
         except (ArchiveConnectionError, ArchiveDownloadError):
-            pl = PackagesList(qt_version, host, target, random.choice(FALLBACK_URLS))
+            pl = PackagesList(qt_version, host, target, random.choice(self.settings.fallbacks))
         print("List Qt packages in %s for %s" % (args.qt_version, args.host))
         table = Texttable()
         table.set_deco(Texttable.HEADER)
@@ -613,10 +599,15 @@ class Cli:
             formatter_class=argparse.RawTextHelpFormatter,
             add_help=True,
         )
+        parser.add_argument(
+            "-c",
+            "--config",
+            type=argparse.FileType("r"),
+            help="Configuration ini file.",
+        )
         parser.add_argument(
             "--logging-conf",
             type=argparse.FileType("r"),
-            nargs=1,
             help="Logging configuration ini file.",
         )
         parser.add_argument("--logger", nargs=1, help="Specify logger name")
@@ -765,13 +756,24 @@ class Cli:
         else:
             self.logger = logging.getLogger("aqt")
 
+    def _setup_settings(self, args=None, env_key="AQT_CONFIG"):
+        if args is not None and args.config is not None:
+            self.settings = Settings(args.config)
+        else:
+            config = os.getenv(env_key, None)
+            if config is not None and os.path.exists(config):
+                self.settings = Settings(config)
+            else:
+                self.settings = Settings()
+
     def run(self, arg=None):
         args = self.parser.parse_args(arg)
+        self._setup_settings(args)
         self._setup_logging(args)
         return args.func(args)
 
 
-def installer(qt_archive, base_dir, command, keep=False, response_timeout=30):
+def installer(qt_archive, base_dir, command, keep=False, response_timeout=None):
     """
     Installer function to download archive files and extract it.
     It is called through multiprocessing.Pool()
@@ -784,7 +786,11 @@ def installer(qt_archive, base_dir, command, keep=False, response_timeout=30):
     logger = getLogger("aqt")
     logger.info("Downloading {}...".format(name))
     logger.debug("Download URL: {}".format(url))
-    timeout = (3.5, response_timeout)
+    settings = Settings()
+    if response_timeout is None:
+        timeout = (settings.connection_timeout, settings.response_timeout)
+    else:
+        timeout = (settings.connection_timeout, response_timeout)
     hash = binascii.unhexlify(getUrl(hashurl, timeout, logger))
     downloadBinaryFile(url, archive, "sha1", hash, timeout, logger)
     if command is None:
diff --git a/aqt/settings.ini b/aqt/settings.ini
index f31a959..2ebe464 100644
--- a/aqt/settings.ini
+++ b/aqt/settings.ini
@@ -2,6 +2,18 @@
 
 [aqt]
 concurrency: 4
+connection_timeout: 3.5
+response_timeout: 30
+baseurl: https://download.qt.io
+7zcmd: 7z
 
 [mirrors]
-blacklist: ['http://mirrors.ustc.edu.cn', 'http://mirrors.tuna.tsinghua.edu.cn', 'http://mirrors.geekpie.club']
+blacklist:
+    http://mirrors.ustc.edu.cn
+    http://mirrors.tuna.tsinghua.edu.cn
+    http://mirrors.geekpie.club
+fallbacks:
+    https://mirrors.ocf.berkeley.edu/qt
+    https://ftp.jaist.ac.jp/pub/qtproject
+    http://ftp1.nluug.nl/languages/qt
+    https://mirrors.dotsrc.org/qtproject
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index 2d9e6ae..e65cd1d 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -115,8 +115,9 @@ for platform_build_job in all_platform_build_jobs:
                 ('TARGET', build_job.target),
                 ('ARCH', build_job.arch),
                 ('ARCHDIR', build_job.archdir),
-                ('MODULE', build_job.module if build_job.module else '')
-            ]
+                ('MODULE', build_job.module if build_job.module else ''),
+                ("QT_BASE_MIRROR", "http://mirrors.ocf.berkeley.edu/qt/" if platform_build_job.platform == "windows" else "")
+        ]
         )
 
     matrices[platform_build_job.platform] = matrix_dictionary
diff --git a/ci/settings.ini b/ci/settings.ini
index 262e7dc..4013bd0 100644
--- a/ci/settings.ini
+++ b/ci/settings.ini
@@ -2,6 +2,12 @@
 
 [aqt]
 concurrency: 2
+connection_timeout: 10
+response_timeout: 10
 
 [mirrors]
-blacklist: ['http://mirrors.ustc.edu.cn', 'http://mirrors.tuna.tsinghua.edu.cn', 'http://mirrors.geekpie.club', 'http://mirrors.sjtug.sjtu.edu.cn']
+blacklist:
+    http://mirrors.ustc.edu.cn
+    http://mirrors.tuna.tsinghua.edu.cn
+    http://mirrors.geekpie.club
+    http://mirrors.sjtug.sjtu.edu.cn
diff --git a/ci/steps.yml b/ci/steps.yml
index 54687ef..b67d29f 100644
--- a/ci/steps.yml
+++ b/ci/steps.yml
@@ -28,11 +28,11 @@ steps:
     displayName: Run Aqt (No Base URL Set)
   - powershell: |
       mkdir Qt
+      Copy-Item -Path "$(Build.SourcesDirectory)\ci\settings.ini" -Destination "Qt"
       cd Qt
-      Join-Path -Path "$(Build.SourcesDirectory)" -ChildPath "ci" | Join-Path -ChildPath "settings.ini" | Set-Item -Path env:AQT_CONFIG
-      python -m aqt install $(QT_VERSION) $(HOST) $(TARGET) $(ARCH)
+      python -m aqt -c settings.ini install $(QT_VERSION) $(HOST) $(TARGET) $(ARCH)
       if ('$(TARGET)' -eq 'android') {
-        python -m aqt install $(QT_VERSION) $(HOST) desktop mingw81_64 --archives qtbase
+        python -m aqt -c settings.ini install $(QT_VERSION) $(HOST) desktop mingw81_64 --archives qtbase
       }
     workingDirectory: $(Build.BinariesDirectory)
     condition: and(not(variables['QT_BASE_MIRROR']), not(variables['TOOL_NAME']), not(variables['EXTERNAL']), eq(variables['MODULE'], ''), eq(variables['SUBARCHIVES'], ''), eq( variables['Agent.OS'], 'Windows_NT'))
@@ -59,8 +59,20 @@ steps:
     workingDirectory: $(Build.BinariesDirectory)
     env:
       AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
-    condition: and(not(variables['TOOL_NAME']), not(variables['EXTERNAL']), ne(variables['MODULE'], ''), eq(variables['SUBARCHIVES'], ''))
+    condition: and(not(variables['TOOL_NAME']), not(variables['EXTERNAL']), ne(variables['MODULE'], ''), eq(variables['SUBARCHIVES'], ''), not(variables['QT_BASE_MIRROR']))
     displayName: Run Aqt (With modules)
+  - bash: |
+      number=$RANDOM
+      let "number %= 30"
+      sleep $number
+      mkdir Qt
+      cd Qt
+      python -m aqt install $(QT_VERSION) $(HOST) $(TARGET) $(ARCH) -m $(MODULE) -b $(QT_BASE_MIRROR)
+    workingDirectory: $(Build.BinariesDirectory)
+    env:
+      AQT_CONFIG: $(Build.SourcesDirectory)/ci/settings.ini
+    condition: and(not(variables['TOOL_NAME']), not(variables['EXTERNAL']), ne(variables['MODULE'], ''), eq(variables['SUBARCHIVES'], ''), variables['QT_BASE_MIRROR'])
+    displayName: Run Aqt (With modules, mirror)
   - bash: |
       number=$RANDOM
       let "number %= 30"
diff --git a/docs/configuration.rst b/docs/configuration.rst
new file mode 100644
index 0000000..46bd93e
--- /dev/null
+++ b/docs/configuration.rst
@@ -0,0 +1,78 @@
+.. _configuration-ref:
+
+Configuration
+=============
+
+``aqtinstall`` can be configured through a configuration file.
+A default configuration is stored in ``aqt/settings.ini`` file.
+
+You can specify custom configuration file through ``AQT_CONFIG``
+environment variable or "-c" or "--config" command line option.
+
+A file is like as follows:
+
+.. code-block::
+
+    [DEFAULTS]
+
+    [aqt]
+    concurrency: 4
+    connection_timeout: 3.5
+    response_timeout: 30
+    baseurl: https://download.qt.io
+    7zcmd: 7z
+
+    [mirrors]
+    blacklist:
+        http://mirrors.ustc.edu.cn
+        http://mirrors.tuna.tsinghua.edu.cn
+        http://mirrors.geekpie.club
+    fallbacks:
+        https://mirrors.ocf.berkeley.edu/qt
+        https://ftp.jaist.ac.jp/pub/qtproject
+        http://ftp1.nluug.nl/languages/qt
+        https://mirrors.dotsrc.org/qtproject
+
+
+Settings
+--------
+
+An ``[aqt]`` section is a configuration for basic behavior.
+
+concurrency:
+    ``concurrency`` is a setting how many download concurrently starts.
+    It should be a integer value.
+
+connection_timeout:
+    ``connection_timeout`` is a timeout in second for connection.
+    It is passed to ``requests`` library.
+
+response_timeout:
+    ``response_timeout`` is a timeout in second how much time waiting for response.
+    It is passed to ``requests`` library.
+
+baseurl:
+    ``baseurl`` is a URL of Qt download site.
+    When you have your own Qt download site repository, you can set it here.
+    It is as same as ``--base`` option.
+
+7zcmd:
+    It is a command name of 7-zip. When ``aqtinstall`` is installed **without**
+    recommended library ``py7zr``, it is used to extract archive instead of
+    ``py7zr`` library.
+    When ``--external`` option specified, a value is override with option's one.
+
+
+A ``[mirrors]`` section is a configuration for mirror handling.
+
+blacklist:
+    It is a list of URL where is a problematic mirror site.
+    Some mirror sites ignore a connection from IP addresses out of their preffered one.
+    It will cause connection error or connection timeout.
+    There are some known mirror sites in default.
+    When you are happy with the default sites,
+    you can override with your custom settings.
+
+fallbacks:
+    It is a list of URL where is a good for access.
+    When mirror site cause an error, aqt use fallbacks when possible.
diff --git a/docs/index.rst b/docs/index.rst
index c6ceecd..aa7ddf2 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -9,6 +9,7 @@ Contents:
    installation
    cli
    available_versions
+   configuration
    internals
    changes
    devguide


Test patch:
diff --git a/tests/data/settings.ini b/tests/data/settings.ini
index 70f7efd..2308beb 100644
--- a/tests/data/settings.ini
+++ b/tests/data/settings.ini
@@ -4,4 +4,6 @@
 concurrency: 3
 
 [mirrors]
-blacklist: ['http://mirror.example.com', 'http://mirrors.geekpie.club/']
+blacklist:
+    http://mirror.example.com
+    http://mirrors.geekpie.club
diff --git a/tests/test_archives.py b/tests/test_archives.py
index e30ae24..37a9f96 100644
--- a/tests/test_archives.py
+++ b/tests/test_archives.py
@@ -2,8 +2,8 @@ import os
 
 import pytest
 
+from aqt.helper import Settings
 from aqt.archives import QtArchives
-from aqt.installer import BASE_URL
 
 
 @pytest.mark.parametrize(
@@ -21,7 +21,7 @@ def test_parse_update_xml(monkeypatch, os_name, version, target, datafile):
 
     monkeypatch.setattr(QtArchives, "_download_update_xml", _mock)
 
-    qt_archives = QtArchives(os_name, "desktop", version, target, BASE_URL)
+    qt_archives = QtArchives(os_name, "desktop", version, target, Settings().baseurl)
     assert qt_archives.archives is not None
 
     # Get packages with all extra modules
diff --git a/tests/test_cli.py b/tests/test_cli.py
index a23a789..6657520 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -4,13 +4,15 @@ import aqt
 def test_cli_help(capsys):
     expected = "".join(
         [
-            "usage: aqt [-h] [--logging-conf LOGGING_CONF] [--logger LOGGER]\n",
+            "usage: aqt [-h] [-c CONFIG] [--logging-conf LOGGING_CONF] [--logger LOGGER]\n",
             "           {install,doc,examples,src,tool,list,offline_installer,help} ...\n",
             "\n",
             "Installer for Qt SDK.\n",
             "\n",
             "optional arguments:\n",
             "  -h, --help            show this help message and exit\n",
+            "  -c CONFIG, --config CONFIG\n",
+            "                        Configuration ini file.\n",
             "  --logging-conf LOGGING_CONF\n",
             "                        Logging configuration ini file.\n",
             "  --logger LOGGER       Specify logger name\n",
@@ -30,6 +32,7 @@ def test_cli_help(capsys):
 
 def test_cli_check_module():
     cli = aqt.installer.Cli()
+    cli._setup_settings()
     assert cli._check_modules_arg("5.11.3", ["qtcharts", "qtwebengine"])
     assert not cli._check_modules_arg("5.7", ["not_exist"])
     assert cli._check_modules_arg("5.14.0", None)
@@ -38,6 +41,7 @@ def test_cli_check_module():
 
 def test_cli_check_combination():
     cli = aqt.installer.Cli()
+    cli._setup_settings()
     assert cli._check_qt_arg_combination("5.11.3", "linux", "desktop", "gcc_64")
     assert cli._check_qt_arg_combination("5.11.3", "mac", "desktop", "clang_64")
     assert not cli._check_qt_arg_combination("5.14.0", "android", "desktop", "clang_64")
@@ -45,12 +49,14 @@ def test_cli_check_combination():
 
 def test_cli_check_version():
     cli = aqt.installer.Cli()
+    cli._setup_settings()
     assert cli._check_qt_arg_versions("5.12.0")
     assert not cli._check_qt_arg_versions("5.12")
 
 
 def test_cli_check_mirror():
     cli = aqt.installer.Cli()
+    cli._setup_settings()
     assert cli._check_mirror(None)
     arg = ["install", "5.11.3", "linux", "desktop", "-b", "https://download.qt.io/"]
     args = cli.parser.parse_args(arg)
@@ -61,13 +67,15 @@ def test_cli_check_mirror():
 def test_cli_launch_with_no_argument(capsys):
     expected = "".join(
         [
-            "usage: aqt [-h] [--logging-conf LOGGING_CONF] [--logger LOGGER]\n",
+            "usage: aqt [-h] [-c CONFIG] [--logging-conf LOGGING_CONF] [--logger LOGGER]\n",
             "           {install,doc,examples,src,tool,list,offline_installer,help} ...\n",
             "\n",
             "Installer for Qt SDK.\n",
             "\n",
             "optional arguments:\n",
             "  -h, --help            show this help message and exit\n",
+            "  -c CONFIG, --config CONFIG\n",
+            "                        Configuration ini file.\n",
             "  --logging-conf LOGGING_CONF\n",
             "                        Logging configuration ini file.\n",
             "  --logger LOGGER       Specify logger name\n",
diff --git a/tests/test_doc_archives.py b/tests/test_doc_archives.py
index 1ae1837..e925804 100644
--- a/tests/test_doc_archives.py
+++ b/tests/test_doc_archives.py
@@ -3,7 +3,7 @@ import os
 import pytest
 
 from aqt.archives import QtArchives
-from aqt.installer import BASE_URL
+from aqt.helper import Settings
 
 
 @pytest.mark.parametrize(
@@ -17,12 +17,13 @@ def test_parse_update_xml(monkeypatch, os_name, version, target, datafile):
 
     monkeypatch.setattr(QtArchives, "_download_update_xml", _mock)
 
-    qt_archives = QtArchives(os_name, "desktop", version, target, BASE_URL)
+    settings = Settings()
+    qt_archives = QtArchives(os_name, "desktop", version, target, settings.baseurl)
     assert qt_archives.archives is not None
 
     # Get packages with all extra modules
     qt_archives_all_modules = QtArchives(
-        os_name, "desktop", version, target, BASE_URL, None, ["all"], None, True
+        os_name, "desktop", version, target, settings.baseurl, None, ["all"], None, True
     )
     assert qt_archives_all_modules.archives is not None