Repository: miurahr/aqtinstall
Pull request #313 (2021-07-14 17:34:01)

Utilize dataclasses
Signed-off-by: Hiroshi Miura <miurahr@linux.com>


Patch:
diff --git a/aqt/archives.py b/aqt/archives.py
index 9326c8e..583dd8b 100644
--- a/aqt/archives.py
+++ b/aqt/archives.py
@@ -22,6 +22,7 @@
 
 import posixpath
 import xml.etree.ElementTree as ElementTree
+from dataclasses import dataclass, field
 from logging import getLogger
 from typing import Optional, Tuple
 
@@ -30,43 +31,25 @@ from aqt.helper import Settings, getUrl
 from aqt.metadata import SimpleSpec, Version
 
 
+@dataclass
 class TargetConfig:
-    def __init__(self, version, target, arch, os_name):
-        self.version = str(version)
-        self.target = target
-        self.arch = arch
-        self.os_name = os_name
-
-    def __str__(self):
-        print(
-            f"TargetConfig(version={self.version}, target={self.target}, "
-            f"arch={self.arch}, os_name={self.os_name}"
-        )
+    version: str
+    target: str
+    arch: str
+    os_name: str
 
-    def __repr__(self):
-        print(f"({self.version}, {self.target}, {self.arch}, {self.os_name})")
+    def __post_init__(self):
+        self.version = str(self.version)
 
 
+@dataclass
 class QtPackage:
-    """
-    Hold package information.
-    """
-
-    def __init__(
-        self,
-        name: str,
-        archive_url: str,
-        archive: str,
-        package_desc: str,
-        hashurl: str,
-        version: Optional[Version] = None,
-    ):
-        self.name = name
-        self.url = archive_url
-        self.archive = archive
-        self.desc = package_desc
-        self.hashurl = hashurl
-        self.version = version
+    name: str
+    archive_url: str
+    archive: str
+    package_desc: str
+    hashurl: str
+    version: Optional[Version] = field(default=None)
 
     def __repr__(self):
         v_info = f", version={self.version}" if self.version else ""
@@ -81,16 +64,12 @@ class QtPackage:
         )
 
 
+@dataclass
 class ListInfo:
-    """
-    Hold list information
-    """
-
-    def __init__(self, name, display_name, desc, virtual):
-        self.name = name
-        self.display_name = display_name
-        self.desc = desc
-        self.virtual = virtual
+    name: str
+    display_name: str
+    desc: str
+    virtual: str
 
 
 class PackagesList:
diff --git a/aqt/installer.py b/aqt/installer.py
index 89232e9..2d852cc 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -839,7 +839,7 @@ def installer(qt_archive, base_dir, command, queue, keep=False, response_timeout
     It is called through multiprocessing.Pool()
     """
     name = qt_archive.name
-    url = qt_archive.url
+    url = qt_archive.archive_url
     hashurl = qt_archive.hashurl
     archive = qt_archive.archive
     start_time = time.perf_counter()
diff --git a/aqt/metadata.py b/aqt/metadata.py
index 7bf51d0..36771e4 100644
--- a/aqt/metadata.py
+++ b/aqt/metadata.py
@@ -214,6 +214,7 @@ class ArchiveId:
         "armv7",
         "arm64_v8a",
     )
+    EXTENSIONS_REQUIRED_ANDROID_QT6 = "x86_64", "x86", "armv7", "arm64_v8a"
 
     def __init__(self, category: str, host: str, target: str, extension: str = ""):
         if category not in ArchiveId.CATEGORIES:
@@ -461,6 +462,40 @@ class ListCommand:
         ]
         return Table(head, rows)
 
+    def validate_extension(self, qt_ver: Version) -> None:
+        """
+        Checks extension, and raises CliInputError if invalid.
+
+        Rules:
+        1. On Qt6 for Android, an extension for processor architecture is required.
+        2. On any platform other than Android, or on Qt5, an extension for
+        processor architecture is forbidden.
+        3. The "wasm" extension only works on desktop targets for Qt 5.13-5.15.
+        """
+        if (
+            self.archive_id.target == "android"
+            and qt_ver.major == 6
+            and self.archive_id.extension
+            not in ArchiveId.EXTENSIONS_REQUIRED_ANDROID_QT6
+        ):
+            raise CliInputError(
+                "Qt 6 for Android requires one of the following extensions: "
+                f"{ArchiveId.EXTENSIONS_REQUIRED_ANDROID_QT6}. "
+                "Please add your extension using the `--extension` flag."
+            )
+        if self.archive_id.extension in ArchiveId.EXTENSIONS_REQUIRED_ANDROID_QT6 and (
+            self.archive_id.target != "android" or qt_ver.major != 6
+        ):
+            raise CliInputError(
+                f"The extension '{self.archive_id.extension}' is only valid for Qt 6 for Android"
+            )
+        if "wasm" in self.archive_id.extension and (
+            qt_ver not in SimpleSpec(">=5.13,<6") or self.archive_id.target != "desktop"
+        ):
+            raise CliInputError(
+                f"The extension '{self.archive_id.extension}' is only available in Qt 5.13 to 5.15 on desktop."
+            )
+
     @staticmethod
     def choose_highest_version_in_spec(
         all_tools_data: Dict[str, Dict[str, str]], simple_spec: SimpleSpec
@@ -578,6 +613,7 @@ class ListCommand:
         self, version: Version
     ) -> Tuple[List[str], List[str]]:
         """Returns [list of modules, list of architectures]"""
+        self.validate_extension(version)
         # NOTE: The url at `<base>/<host>/<target>/qt5_590/` does not exist; the real one is `qt5_590`
         patch = (
             ""
diff --git a/setup.cfg b/setup.cfg
index f34accb..d721a28 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,6 +38,7 @@ install_requires =
     py7zr>=0.15.1
     texttable
     bs4
+    dataclasses;python_version<"3.7"
 setup_requires =
     setuptools-scm[toml]>=6.0.1
     setuptools>=45.0


Test patch:
diff --git a/tests/test_archives.py b/tests/test_archives.py
index 2c15182..fb7bf73 100644
--- a/tests/test_archives.py
+++ b/tests/test_archives.py
@@ -42,7 +42,7 @@ def test_parse_update_xml(monkeypatch, os_name, version, target, datafile):
     assert qt_archives_all_modules.archives is not None
 
     # Extract all urls
-    url_list = [item.url for item in qt_archives.archives]
+    url_list = [item.archive_url for item in qt_archives.archives]
     url_all_modules_list = [item.url for item in qt_archives_all_modules.archives]
 
     # Check the difference list contains only extra modules urls for target specified
diff --git a/tests/test_doc_archives.py b/tests/test_doc_archives.py
index 8635707..8615383 100644
--- a/tests/test_doc_archives.py
+++ b/tests/test_doc_archives.py
@@ -35,8 +35,10 @@ def test_parse_update_xml(monkeypatch, os_name, version, target, datafile):
     assert qt_archives_all_modules.archives is not None
 
     # Extract all urls
-    url_list = [item.url for item in qt_archives.archives]
-    url_all_modules_list = [item.url for item in qt_archives_all_modules.archives]
+    url_list = [item.archive_url for item in qt_archives.archives]
+    url_all_modules_list = [
+        item.archive_url for item in qt_archives_all_modules.archives
+    ]
 
     # Check the difference list contains only extra modules urls for target specified
     list_diff = [item for item in url_all_modules_list if item not in url_list]
diff --git a/tests/test_list.py b/tests/test_list.py
index 45beeda..5c4b5d7 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -1,5 +1,6 @@
 import json
 import re
+import sys
 from pathlib import Path
 
 import pytest
@@ -279,3 +280,45 @@ def test_list_choose_tool_by_version(simple_spec, expected_name):
         assert item["Name"] == expected_name
     else:
         assert expected_name is None
+
+
+qt6_android_requires_ext_msg = (
+    "Qt 6 for Android requires one of the following extensions: "
+    f"{ArchiveId.EXTENSIONS_REQUIRED_ANDROID_QT6}. "
+    "Please add your extension using the `--extension` flag."
+)
+no_arm64_v8_msg = "The extension 'arm64_v8a' is only valid for Qt 6 for Android"
+no_wasm_msg = "The extension 'wasm' is only available in Qt 5.13 to 5.15 on desktop."
+
+
+@pytest.mark.parametrize(
+    "target, ext, version, expected_msg",
+    (
+        ("android", "", "6.2.0", qt6_android_requires_ext_msg),
+        ("android", "arm64_v8a", "5.13.0", no_arm64_v8_msg),
+        ("desktop", "arm64_v8a", "5.13.0", no_arm64_v8_msg),
+        ("desktop", "arm64_v8a", "6.2.0", no_arm64_v8_msg),
+        ("desktop", "wasm", "5.12.11", no_wasm_msg),  # out of range
+        ("desktop", "wasm", "6.2.0", no_wasm_msg),  # out of range
+        ("android", "wasm", "5.12.11", no_wasm_msg),  # in range, wrong target
+        ("android", "wasm", "5.14.0", no_wasm_msg),  # in range, wrong target
+        ("android", "wasm", "6.2.0", qt6_android_requires_ext_msg),
+    ),
+)
+def test_list_invalid_extensions(
+    capsys, monkeypatch, target, ext, version, expected_msg
+):
+    def _mock(_, rest_of_url: str) -> str:
+        return ""
+
+    monkeypatch.setattr(ListCommand, "fetch_http", _mock)
+
+    cat = "qt" + version[0]
+    host = "windows"
+    extension_params = ["--extension", ext] if ext else []
+    cli = Cli()
+    cli.run(["list", cat, host, target, *extension_params, "--arch", version])
+    out, err = capsys.readouterr()
+    sys.stdout.write(out)
+    sys.stderr.write(err)
+    assert expected_msg in err