Repository: miurahr/aqtinstall

Issue:
leaked semaphore objects?
aqt 1.2.0b3, macOS 11.4
```
% aqt install --outputdir ~/junkaqt 5.12.11 mac desktop -m qtwebengine

2021-06-11 11:16:07,044 - aqt - INFO - aqtinstall(aqt) vunknown on Python 3.9.4 [CPython Clang 12.0.0 (clang-1200.0.32.29)]
2021-06-11 11:16:07,044 - aqt - WARNING - Specified Qt version is unknown: 5.12.11.
2021-06-11 11:16:56,388 - aqt - INFO - Patching /Users/administrator/junkaqt/5.12.11/clang_64/bin/qmake
2021-06-11 11:16:56,417 - aqt - INFO - Patching /Users/administrator/junkaqt/5.12.11/clang_64/lib/QtCore.framework/Versions/5/QtCore
2021-06-11 11:16:56,429 - aqt - INFO - Patching /Users/administrator/junkaqt/5.12.11/clang_64/lib/QtCore.framework/Versions/5/QtCore_debug
2021-06-11 11:16:56,450 - aqt - INFO - Finished installation
2021-06-11 11:16:56,451 - aqt - INFO - Time elasped: 49.40753163 second
/usr/local/Cellar/python@3.9/3.9.4/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/resource_tracker.py:216: UserWarning: resource_tracker: There appear to be 4 leaked semaphore objects to clean up at shutdown
  warnings.warn('resource_tracker: There appear to be %d '
```

Discussion:
tsteven4: f1d576f4bfa8fc45954f470a8eb901d09f8821d1 fixes the semaphore leak, it does not fix the missing log messages.

miurahr: >  it does not fix the missing log messages.

I do nothing for log. It will be adjust in another PR.

Patch:
diff --git a/aqt/archives.py b/aqt/archives.py
index 5348286..6f21340 100644
--- a/aqt/archives.py
+++ b/aqt/archives.py
@@ -143,7 +143,7 @@ class QtArchives:
         self.arch = arch
         self.os_name = os_name
         self.all_extra = all_extra
-        self.arch_list = [item.get("arch") for item in Settings().qt_combinations]
+        self.arch_list = [item.get("arch") for item in Settings.qt_combinations]
         all_archives = subarchives is None
         self.base = base + "/online/qtsdkrepository/"
         if logging:
diff --git a/aqt/helper.py b/aqt/helper.py
index c201501..d7d7a9b 100644
--- a/aqt/helper.py
+++ b/aqt/helper.py
@@ -23,7 +23,6 @@ import configparser
 import hashlib
 import json
 import logging
-import multiprocessing
 import os
 import sys
 import xml.etree.ElementTree as ElementTree
@@ -129,7 +128,7 @@ def altlink(url: str, alt: str, logger=None):
     xml file, parse it and retrieve best alternative url."""
     if logger is None:
         logger = logging.getLogger(__name__)
-    blacklist = Settings().blacklist  # type: Optional[List[str]]
+    blacklist = Settings.blacklist  # type: Optional[List[str]]
     if not any(alt.startswith(b) for b in blacklist):
         return alt
     try:
@@ -188,50 +187,35 @@ class MyConfigParser(configparser.ConfigParser):
         return result
 
 
-class Settings(object):
-    """Class to hold configuration and settings.
-    Actual values are stored in 'settings.ini' file.
-    It also holds a combinations database.
-    """
-
-    # this class is Borg/Singleton
-    _shared_state = {
-        "config": None,
-        "_combinations": None,
-        "_lock": multiprocessing.Lock(),
-    }
-
-    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 = 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 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"),
-                        "r",
-                    ) as j:
-                        self._combinations = json.load(j)[0]
+class Settings:
+    """Class to hold configuration and settings.		￼
+    ￼    Actual values are stored in 'settings.ini' file.
+    ￼    It also holds a combinations database.
+    ￼"""
+
+    def __init__(self):
+        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 combinations
+        with open(
+            os.path.join(os.path.dirname(__file__), "combinations.json"),
+            "r",
+        ) as j:
+            self._combinations = json.load(j)[0]
+
+    def load_settings(self, file):
+        # load custom file
+        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()
 
     @property
     def qt_combinations(self):
@@ -303,3 +287,6 @@ class Settings(object):
     @property
     def zipcmd(self):
         return self.config.get("aqt", "7zcmd", fallback="7z")
+
+
+Settings = Settings()
diff --git a/aqt/installer.py b/aqt/installer.py
index 69a97c0..b844672 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -62,13 +62,13 @@ class ExtractionError(Exception):
 class Cli:
     """CLI main class to parse command line argument and launch proper functions."""
 
-    __slot__ = ["parser", "combinations", "logger", "settings"]
+    __slot__ = ["parser", "combinations", "logger"]
 
     def __init__(self):
         self._create_parser()
 
     def _check_tools_arg_combination(self, os_name, tool_name, arch):
-        for c in self.settings.tools_combinations:
+        for c in Settings.tools_combinations:
             if (
                 c["os_name"] == os_name
                 and c["tool_name"] == tool_name
@@ -104,16 +104,16 @@ class Cli:
                 "win32_mingw81",
             ]:
                 return False
-        for c in self.settings.qt_combinations:
+        for c in Settings.qt_combinations:
             if c["os_name"] == os_name and c["target"] == target and c["arch"] == arch:
                 return True
         return False
 
     def _check_qt_arg_versions(self, version):
-        return version in self.settings.available_versions
+        return version in Settings.available_versions
 
     def _check_qt_arg_version_offline(self, version):
-        return version in self.settings.available_offline_installer_version
+        return version in Settings.available_offline_installer_version
 
     def _set_sevenzip(self, external):
         sevenzip = external
@@ -172,7 +172,7 @@ class Cli:
     def _check_modules_arg(self, qt_version, modules):
         if modules is None:
             return True
-        available = self.settings.available_modules(qt_version)
+        available = Settings.available_modules(qt_version)
         if available is None:
             return False
         return all([m in available for m in modules])
@@ -181,7 +181,7 @@ class Cli:
         tasks = []
         for arc in qt_archives.get_archives():
             tasks.append((arc, base_dir, sevenzip, keep))
-        pool = multiprocessing.Pool(self.settings.concurrency)
+        pool = multiprocessing.Pool(Settings.concurrency)
         pool.starmap(installer, tasks)
         pool.close()
         pool.join()
@@ -203,7 +203,7 @@ class Cli:
         if args.timeout is not None:
             timeout = (args.timeout, args.timeout)
         else:
-            timeout = (self.settings.connection_timeout, self.settings.response_timeout)
+            timeout = (Settings.connection_timeout, Settings.response_timeout)
         arch = self._set_arch(args, arch, os_name, target, qt_version)
         modules = args.modules
         sevenzip = self._set_sevenzip(args.external)
@@ -216,7 +216,7 @@ class Cli:
                 exit(1)
             base = args.base
         else:
-            base = self.settings.baseurl
+            base = Settings.baseurl
         archives = args.archives
         if args.noarchives:
             if modules is None:
@@ -273,7 +273,7 @@ class Cli:
                     target,
                     qt_version,
                     arch,
-                    random.choice(self.settings.fallbacks),
+                    random.choice(Settings.fallbacks),
                     subarchives=archives,
                     modules=modules,
                     logging=self.logger,
@@ -311,15 +311,15 @@ class Cli:
         if args.base is not None:
             base = args.base
         else:
-            base = self.settings.baseurl
+            base = Settings.baseurl
         if args.timeout is not None:
             timeout = (args.timeout, args.timeout)
         else:
-            timeout = (self.settings.connection_timeout, self.settings.response_timeout)
+            timeout = (Settings.connection_timeout, 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(self.settings.zipcmd)
+            sevenzip = self._set_sevenzip(Settings.zipcmd)
         modules = args.modules
         archives = args.archives
         all_extra = True if modules is not None and "all" in modules else False
@@ -350,7 +350,7 @@ class Cli:
                     os_name,
                     target,
                     qt_version,
-                    random.choice(self.settings.fallbacks),
+                    random.choice(Settings.fallbacks),
                     subarchives=archives,
                     modules=modules,
                     logging=self.logger,
@@ -396,17 +396,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(self.settings.zipcmd)
+            sevenzip = self._set_sevenzip(Settings.zipcmd)
         version = args.version
         keep = args.keep
         if args.base is not None:
             base = args.base
         else:
-            base = self.settings.baseurl
+            base = Settings.baseurl
         if args.timeout is not None:
             timeout = (args.timeout, args.timeout)
         else:
-            timeout = (self.settings.connection_timeout, self.settings.response_timeout)
+            timeout = (Settings.connection_timeout, 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(
@@ -433,7 +433,7 @@ class Cli:
                     tool_name,
                     version,
                     arch,
-                    random.choice(self.settings.fallbacks),
+                    random.choice(Settings.fallbacks),
                     logging=self.logger,
                     timeout=timeout,
                 )
@@ -457,10 +457,10 @@ class Cli:
         host = args.host
         target = args.target
         try:
-            pl = PackagesList(qt_version, host, target, self.settings.baseurl)
+            pl = PackagesList(qt_version, host, target, Settings.baseurl)
         except (ArchiveConnectionError, ArchiveDownloadError):
             pl = PackagesList(
-                qt_version, host, target, random.choice(self.settings.fallbacks)
+                qt_version, host, target, random.choice(Settings.fallbacks)
             )
         print("List Qt packages in %s for %s" % (args.qt_version, args.host))
         table = Texttable()
@@ -662,13 +662,11 @@ class Cli:
 
     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)
+            Settings.load_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()
+                Settings.load_settings(config)
 
     def run(self, arg=None):
         args = self.parser.parse_args(arg)
@@ -690,11 +688,10 @@ def installer(qt_archive, base_dir, command, keep=False, response_timeout=None):
     logger = getLogger("aqt")
     logger.info("Downloading {}...".format(name))
     logger.debug("Download URL: {}".format(url))
-    settings = Settings()
     if response_timeout is None:
-        timeout = (settings.connection_timeout, settings.response_timeout)
+        timeout = (Settings.connection_timeout, Settings.response_timeout)
     else:
-        timeout = (settings.connection_timeout, response_timeout)
+        timeout = (Settings.connection_timeout, response_timeout)
     hash = binascii.unhexlify(getUrl(hashurl, timeout, logger))
     downloadBinaryFile(url, archive, "sha1", hash, timeout, logger)
     if command is None:


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index b035843..e69de29 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +0,0 @@
-import os
-
-from aqt import helper
-
-# Early load custom configuration for test because it is Borg/Singleton
-helper.Settings(os.path.join(os.path.dirname(__file__), "data", "settings.ini"))
diff --git a/tests/test_archives.py b/tests/test_archives.py
index c81a8a3..2c15182 100644
--- a/tests/test_archives.py
+++ b/tests/test_archives.py
@@ -21,7 +21,10 @@ 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, Settings().baseurl)
+    Settings.load_settings(
+        os.path.join(os.path.dirname(__file__), "data", "settings.ini")
+    )
+    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_doc_archives.py b/tests/test_doc_archives.py
index e925804..8635707 100644
--- a/tests/test_doc_archives.py
+++ b/tests/test_doc_archives.py
@@ -17,13 +17,20 @@ def test_parse_update_xml(monkeypatch, os_name, version, target, datafile):
 
     monkeypatch.setattr(QtArchives, "_download_update_xml", _mock)
 
-    settings = Settings()
-    qt_archives = QtArchives(os_name, "desktop", version, target, settings.baseurl)
+    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, settings.baseurl, None, ["all"], None, True
+        os_name,
+        "desktop",
+        version,
+        target,
+        Settings.baseurl,
+        None,
+        ["all"],
+        None,
+        True,
     )
     assert qt_archives_all_modules.archives is not None
 
diff --git a/tests/test_helper.py b/tests/test_helper.py
index f282c5d..ff50d5d 100644
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -51,9 +51,11 @@ def test_helper_altlink(monkeypatch):
 
 
 def test_settings(tmp_path):
-    config = helper.Settings()
-    assert config.concurrency == 3
-    assert "http://mirror.example.com" in config.blacklist
+    helper.Settings.load_settings(
+        os.path.join(os.path.dirname(__file__), "data", "settings.ini")
+    )
+    assert helper.Settings.concurrency == 3
+    assert "http://mirror.example.com" in helper.Settings.blacklist
 
 
 def mocked_iter_content(chunk_size):