Repository: miurahr/aqtinstall
Pull request #401 (2021-09-17 22:08:20)

Require architecture when listing modules
This PR solves the problem in #371 by requiring that the user enter the architecture when listing modules, like this:

```bash
$ aqt list-qt mac desktop --modules 5.12.0 clang_64                     # print modules for 5.12.0
$ aqt list-qt mac desktop --spec 5.9 --modules latest clang_64          # print modules for latest 5.9
$ aqt list-qt windows desktop --modules 5.12.0  win64_mingw73           # print modules for 5.12.0
$ aqt list-qt windows desktop --spec 5.9 --modules latest win64_mingw73 # print modules for latest 5.9
```

This solution does not attempt to automatically deduce the architecture when only one architecture is available. This feature was suggested in #371, and it's definitely possible, but I have not implemented it.

It also does not attempt to implement the improved interface proposed in #371. The improved interface is a bigger job than I expected it to be, and I think it can be done independently from this PR.


Patch:
diff --git a/aqt/installer.py b/aqt/installer.py
index ed68f95..1c0cbc6 100644
--- a/aqt/installer.py
+++ b/aqt/installer.py
@@ -437,8 +437,12 @@ class Cli:
             return
         if args.target not in ArchiveId.TARGETS_FOR_HOST[args.host]:
             raise CliInputError("'{0.target}' is not a valid target for host '{0.host}'".format(args))
+        if args.modules:
+            modules_ver, modules_query = args.modules[0], tuple(args.modules)
+        else:
+            modules_ver, modules_query = None, None
 
-        for version_str in (args.modules, args.extensions, args.arch):
+        for version_str in (modules_ver, args.extensions, args.arch):
             Cli._validate_version_str(version_str, allow_latest=True, allow_empty=True)
 
         spec = None
@@ -457,7 +461,7 @@ class Cli:
             ),
             spec=spec,
             is_latest_version=args.latest_version,
-            modules_ver=args.modules,
+            modules_query=modules_query,
             extensions_ver=args.extensions,
             architectures_ver=args.arch,
         )
@@ -602,7 +606,7 @@ class Cli:
             "$ aqt list-qt mac desktop --extension wasm                   # print all wasm versions of Qt 5\n"
             '$ aqt list-qt mac desktop --spec "5.9"                       # print all versions of Qt 5.9\n'
             '$ aqt list-qt mac desktop --spec "5.9" --latest-version      # print latest Qt 5.9\n'
-            "$ aqt list-qt mac desktop --modules 5.12.0                   # print modules for 5.12.0\n"
+            "$ aqt list-qt mac desktop --modules 5.12.0 clang_64          # print modules for 5.12.0\n"
             "$ aqt list-qt mac desktop --spec 5.9 --modules latest  # print modules for latest 5.9\n"
             "$ aqt list-qt mac desktop --extensions 5.9.0                 # print choices for --extension flag\n"
             "$ aqt list-qt mac desktop --arch 5.9.9                       "
@@ -635,8 +639,10 @@ class Cli:
         output_modifier_exclusive_group.add_argument(
             "--modules",
             type=str,
-            metavar="(VERSION | latest)",
-            help='Qt version in the format of "5.X.Y", or the keyword "latest". '
+            nargs=2,
+            metavar=("(VERSION | latest)", "ARCHITECTURE"),
+            help='First arg: Qt version in the format of "5.X.Y", or the keyword "latest". '
+            'Second arg: an architecture, which may be printed with the "--arch" flag. '
             "When set, this prints all the modules available for either Qt 5.X.Y or the latest version of Qt.",
         )
         output_modifier_exclusive_group.add_argument(
diff --git a/aqt/metadata.py b/aqt/metadata.py
index accd7ed..bca3bbe 100644
--- a/aqt/metadata.py
+++ b/aqt/metadata.py
@@ -26,7 +26,7 @@ import random
 import re
 import shutil
 from logging import getLogger
-from typing import Dict, Generator, Iterable, Iterator, List, Optional, Tuple, Union
+from typing import Callable, Dict, Generator, Iterable, Iterator, List, Optional, Tuple, Union
 from xml.etree import ElementTree as ElementTree
 
 import bs4
@@ -241,21 +241,20 @@ class ArchiveId:
         """Returns True if there should be no arch attached to the module names"""
         return self.extension in ("src_doc_examples",)
 
-    def to_url(self, qt_version_no_dots: Optional[str] = None, file: str = "") -> str:
-        base = "online/qtsdkrepository/{os}{arch}/{target}/".format(
+    def to_url(self) -> str:
+        return "online/qtsdkrepository/{os}{arch}/{target}/".format(
             os=self.host,
             arch="_x86" if self.host == "windows" else "_x64",
             target=self.target,
         )
-        if not qt_version_no_dots:
-            return base
-        folder = "{category}{major}_{ver}{ext}/".format(
+
+    def to_folder(self, qt_version_no_dots: str) -> str:
+        return "{category}{major}_{ver}{ext}".format(
             category=self.category,
             major=qt_version_no_dots[0],
             ver=qt_version_no_dots,
             ext="_" + self.extension if self.extension else "",
         )
-        return base + folder + file
 
     def __str__(self) -> str:
         return "{cat}/{host}/{target}{ext}".format(
@@ -363,7 +362,7 @@ class MetadataFactory:
         *,
         spec: Optional[SimpleSpec] = None,
         is_latest_version: bool = False,
-        modules_ver: Optional[str] = None,
+        modules_query: Optional[Tuple[str, str]] = None,
         extensions_ver: Optional[str] = None,
         architectures_ver: Optional[str] = None,
         tool_name: Optional[str] = None,
@@ -376,7 +375,7 @@ class MetadataFactory:
                                     Qt that don't fit this SimpleSpec.
         :param is_latest_version:   When True, the MetadataFactory will find all versions of Qt
                                     matching filters, and only print the most recent version
-        :param modules_ver:         Version of Qt for which to list modules
+        :param modules_query:       [Version of Qt, architecture] for which to list modules
         :param extensions_ver:      Version of Qt for which to list extensions
         :param architectures_ver:   Version of Qt for which to list architectures
         :param tool_name:           Name of a tool, without architecture, ie "tools_qtcreator" or "tools_ifw"
@@ -402,9 +401,10 @@ class MetadataFactory:
         elif is_latest_version:
             self.request_type = "latest version"
             self._action = lambda: Versions(self.fetch_latest_version())
-        elif modules_ver:
+        elif modules_query:
             self.request_type = "modules"
-            self._action = lambda: self.fetch_modules(self._to_version(modules_ver))
+            version, arch = modules_query
+            self._action = lambda: self.fetch_modules(self._to_version(version), arch)
         elif extensions_ver:
             self.request_type = "extensions"
             self._action = lambda: self.fetch_extensions(self._to_version(extensions_ver))
@@ -418,11 +418,20 @@ class MetadataFactory:
     def getList(self) -> Union[List[str], Versions, ToolData]:
         return self._action()
 
-    def fetch_modules(self, version: Version) -> List[str]:
-        return self.get_modules_architectures_for_version(version=version)[0]
-
     def fetch_arches(self, version: Version) -> List[str]:
-        return self.get_modules_architectures_for_version(version=version)[1]
+        self.validate_extension(version)
+        if self.archive_id.extension == "src_doc_examples":
+            return []
+        qt_ver_str = self._get_qt_version_str(version)
+        modules = self._fetch_module_metadata(self.archive_id.to_folder(qt_ver_str))
+
+        arches = []
+        for name in modules.keys():
+            ver, arch = name.split(".")[-2:]
+            if ver == qt_ver_str:
+                arches.append(arch)
+
+        return arches
 
     def fetch_extensions(self, version: Version) -> List[str]:
         versions_extensions = MetadataFactory.get_versions_extensions(
@@ -456,28 +465,17 @@ class MetadataFactory:
         html_doc = self.fetch_http(self.archive_id.to_url())
         return list(MetadataFactory.iterate_folders(html_doc, "tools"))
 
-    def _fetch_tool_data(self, tool_name: str, keys_to_keep: Optional[Iterable[str]] = None) -> Dict[str, Dict[str, str]]:
-        # raises ArchiveDownloadError, ArchiveConnectionError
-        rest_of_url = self.archive_id.to_url() + tool_name + "/Updates.xml"
-        xml = self.fetch_http(rest_of_url)
-        modules = xml_to_modules(
-            xml,
-            predicate=MetadataFactory._has_nonempty_downloads,
-            keys_to_keep=keys_to_keep,
-        )
-        return modules
-
     def fetch_tool_modules(self, tool_name: str) -> List[str]:
-        tool_data = self._fetch_tool_data(tool_name, keys_to_keep=())
+        tool_data = self._fetch_module_metadata(tool_name)
         return list(tool_data.keys())
 
     def fetch_tool_by_simple_spec(self, tool_name: str, simple_spec: SimpleSpec) -> Optional[Dict[str, str]]:
         # Get data for all the tool modules
-        all_tools_data = self._fetch_tool_data(tool_name)
+        all_tools_data = self._fetch_module_metadata(tool_name)
         return self.choose_highest_version_in_spec(all_tools_data, simple_spec)
 
     def fetch_tool_long_listing(self, tool_name: str) -> ToolData:
-        return ToolData(self._fetch_tool_data(tool_name))
+        return ToolData(self._fetch_module_metadata(tool_name))
 
     def validate_extension(self, qt_ver: Version) -> None:
         """
@@ -609,18 +607,31 @@ class MetadataFactory:
         downloads = element.find("DownloadableArchives")
         return downloads is not None and downloads.text
 
-    def get_modules_architectures_for_version(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`
+    def _get_qt_version_str(self, version: Version) -> str:
+        """Returns a Qt version, without dots, that works in the Qt repo urls and Updates.xml files"""
+        # NOTE: The url at `<base>/<host>/<target>/qt5_590/` does not exist; the real one is `qt5_59`
         patch = (
             ""
             if version.prerelease or self.archive_id.is_preview() or version in SimpleSpec("5.9.0")
             else str(version.patch)
         )
-        qt_ver_str = "{}{}{}".format(version.major, version.minor, patch)
+        return f"{version.major}{version.minor}{patch}"
+
+    def _fetch_module_metadata(self, folder: str, predicate: Optional[Callable[[ElementTree.Element], bool]] = None):
+        rest_of_url = posixpath.join(self.archive_id.to_url(), folder, "Updates.xml")
+        xml = self.fetch_http(rest_of_url)
+        return xml_to_modules(
+            xml,
+            predicate=predicate if predicate else MetadataFactory._has_nonempty_downloads,
+        )
+
+    def fetch_modules(self, version: Version, arch: str) -> List[str]:
+        """Returns list of modules"""
+        self.validate_extension(version)
+        qt_ver_str = self._get_qt_version_str(version)
         # Example: re.compile(r"^(preview\.)?qt\.(qt5\.)?590\.(.+)$")
         pattern = re.compile(r"^(preview\.)?qt\.(qt" + str(version.major) + r"\.)?" + qt_ver_str + r"\.(.+)$")
+        modules_meta = self._fetch_module_metadata(self.archive_id.to_folder(qt_ver_str))
 
         def to_module_arch(name: str) -> Tuple[Optional[str], Optional[str]]:
             _match = pattern.match(name)
@@ -634,36 +645,12 @@ class MetadataFactory:
                 module = module[len("addons.") :]
             return module, arch
 
-        rest_of_url = self.archive_id.to_url(qt_version_no_dots=qt_ver_str, file="Updates.xml")
-        xml = self.fetch_http(rest_of_url)  # raises RequestException
-
-        # We want the names of modules, regardless of architecture:
-        modules = xml_to_modules(
-            xml,
-            predicate=MetadataFactory._has_nonempty_downloads,
-            keys_to_keep=(),  # Just want names
-        )
-
-        def naive_modules_arches(
-            names: Iterable[str],
-        ) -> Tuple[List[str], List[str]]:
-            modules_and_arches, _modules, arches = set(), set(), set()
-            for name in names:
-                # First term could be a module name or an architecture
-                first_term, arch = to_module_arch(name)
-                if first_term:
-                    modules_and_arches.add(first_term)
-                if arch:
-                    arches.add(arch)
-            for first_term in modules_and_arches:
-                if first_term not in arches:
-                    _modules.add(first_term)
-            return (
-                sorted(_modules),
-                sorted(arches),
-            )
-
-        return naive_modules_arches(modules.keys())
+        modules = set()
+        for name in modules_meta.keys():
+            module, _arch = to_module_arch(name)
+            if _arch == arch:
+                modules.add(module)
+        return sorted(modules)
 
     def describe_filters(self) -> str:
         if self.spec is None:
@@ -688,6 +675,8 @@ def suggested_follow_up(meta: MetadataFactory) -> List[str]:
         )
     elif meta.request_type in ("architectures", "modules", "extensions"):
         msg.append(f"Please use '{base_cmd}' to show versions of Qt available.")
+        if meta.request_type == "modules":
+            msg.append(f"Please use '{base_cmd} --arch <QT_VERSION>' to list valid architectures.")
 
     return msg
 
diff --git a/ci/generate_azure_pipelines_matrices.py b/ci/generate_azure_pipelines_matrices.py
index bce44e4..8000029 100644
--- a/ci/generate_azure_pipelines_matrices.py
+++ b/ci/generate_azure_pipelines_matrices.py
@@ -192,16 +192,16 @@ linux_build_jobs.extend(
             "install-doc", "6.1.0", "linux", "desktop", "gcc_64", "gcc_64", subarchives="qtdoc"
         ),
         # test for list commands
-        BuildJob('list', '5.15.2', 'linux', 'desktop', '', '', spec="<6", list_options={
+        BuildJob('list', '5.15.2', 'linux', 'desktop', 'gcc_64', '', spec="<6", list_options={
             'HAS_EXTENSIONS': "True",
         }),
-        BuildJob('list', '6.1.0', 'linux', 'android', '', '', spec=">6.0,<6.1.1", list_options={
+        BuildJob('list', '6.1.0', 'linux', 'android', 'android_armv7', '', spec=">6.0,<6.1.1", list_options={
             'HAS_EXTENSIONS': "True",
             'USE_EXTENSION': "armv7",
         }),
         # tests run on linux but query data about other platforms
-        BuildJob('list', '5.14.1', 'mac', 'ios', '', '', spec="<=5.14.1", list_options={}),
-        BuildJob('list', '5.13.1', 'windows', 'winrt', '', '', spec=">5.13.0,<5.13.2", list_options={}),
+        BuildJob('list', '5.14.1', 'mac', 'ios', 'ios', '', spec="<=5.14.1", list_options={}),
+        BuildJob('list', '5.13.1', 'windows', 'winrt', 'win64_msvc2015_winrt_x64', '', spec=">5.13.0,<5.13.2", list_options={}),
     ]
 )
 mac_build_jobs.extend(
diff --git a/ci/generate_combinations.py b/ci/generate_combinations.py
index 255e331..21c4305 100644
--- a/ci/generate_combinations.py
+++ b/ci/generate_combinations.py
@@ -100,14 +100,14 @@ def iter_qt_minor_groups(
 
 
 def iter_modules_for_qt_minor_groups(
-    host: str = "linux", target: str = "desktop"
+    host: str = "linux", target: str = "desktop", arch: str = "gcc_64"
 ) -> Generator[Dict, None, None]:
     logger.info("Fetching qt modules for {}/{}".format(host, target))
     for major, minor in tqdm(list(iter_qt_minor_groups(host, target))):
         yield {
             "qt_version": f"{major}.{minor}",
             "modules": MetadataFactory(
-                ArchiveId("qt", host, target), modules_ver=f"{major}.{minor}.0"
+                ArchiveId("qt", host, target), modules_query=(f"{major}.{minor}.0", arch)
             ).getList(),
         }
 
diff --git a/ci/steps.yml b/ci/steps.yml
index 3c1aa05..ba6fcdb 100644
--- a/ci/steps.yml
+++ b/ci/steps.yml
@@ -90,8 +90,8 @@ steps:
         aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)"                     # print all versions of Qt in SimpleSpec
         ver=$(aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)" --latest-version)  # latest Qt in SimpleSpec
         [ $ver == $(QT_VERSION) ]                                               # latest version in SPEC must be QT_VERSION
-        aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)" --modules latest    # print modules for latest in SimpleSpec
-        aqt list-qt $(HOST) $(TARGET) $ext --modules $(QT_VERSION)              # print modules for version/host/target
+        aqt list-qt $(HOST) $(TARGET) $ext --spec "$(SPEC)" --modules latest $(ARCH)    # print modules for latest in SimpleSpec
+        aqt list-qt $(HOST) $(TARGET) $ext --modules $(QT_VERSION) $(ARCH)      # print modules for version/host/target/arch
         if [[ $(HAS_EXTENSIONS) == "True" ]]; then
           aqt list-qt $(HOST) $(TARGET) --extensions $(QT_VERSION)              # print choices for --extension flag
           aqt list-qt $(HOST) $(TARGET) --spec "$(SPEC)" --extensions latest
diff --git a/docs/cli.rst b/docs/cli.rst
index b80bd8b..f9177f2 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -43,7 +43,7 @@ list-qt command
     aqt list-qt [-h | --help]
                 [--extension <extension>]
                 [--spec <specification>]
-                [--modules    (<Qt version> | latest) |
+                [--modules    (<Qt version> | latest) <architecture> |
                  --extensions (<Qt version> | latest) |
                  --arch       (<Qt version> | latest) |
                  --latest-version]
@@ -93,11 +93,12 @@ List available versions of Qt, targets, extensions, modules, and architectures.
 .. _SimpleSpec: https://python-semanticversion.readthedocs.io/en/latest/reference.html#semantic_version.SimpleSpec
 
 
-.. option:: --modules (<Qt version> | latest)
+.. option:: --modules (<Qt version> | latest) <architecture>
 
-    Qt version in the format of "5.X.Y". When set, this lists all the modules
-    available for Qt 5.X.Y with a host/target/extension, or the latest version
-    of Qt if ``latest`` is specified.
+    This flag lists all the modules available for Qt 5.X.Y with a host/target/extension/architecture
+    combination, or the latest version of Qt if ``latest`` is specified.
+    You can list available architectures by using ``aqt list-qt`` with the
+    ``--arch`` flag described below.
 
 .. option:: --arch (<Qt version> | latest)
 
@@ -591,7 +592,7 @@ Example: List available modules for latest version of Qt on macOS
 
 .. code-block:: console
 
-    aqt list-qt mac desktop --modules latest    # prints 'qtquick3d qtshadertools', etc
+    aqt list-qt mac desktop --modules latest clang_64   # prints 'qtquick3d qtshadertools', etc
 
 
 Example: List available architectures for Qt 6.1.2 on windows
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index cc1bf9b..8a8bf5a 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -151,13 +151,13 @@ Installing Modules
 Let's say we need to install some modules for Qt 5.15.2 on Windows Desktop.
 First we need to find out what the modules are called, and we can do that 
 with :ref:`aqt list-qt <list qt command>` with the ``--modules`` flag.
-Each version of Qt has a different list of modules for each host OS/ target SDK
+Each version of Qt has a different list of modules for each host OS/ target SDK/ architecture
 combination, so we will need to supply :ref:`aqt list-qt <list qt command>` with that information:
 
 .. code-block:: console
 
-    $ aqt list-qt windows desktop --modules 5.15.2
-    debug_info qtcharts qtdatavis3d qtlottie qtnetworkauth qtpurchasing qtquick3d
+    $ aqt list-qt windows desktop --modules 5.15.2 win64_mingw81
+    qtcharts qtdatavis3d qtlottie qtnetworkauth qtpurchasing qtquick3d
     qtquicktimeline qtscript qtvirtualkeyboard qtwebengine qtwebglplugin
 
 Let's say that we want to install `qtcharts` and `qtnetworkauth`. 
@@ -181,7 +181,7 @@ One way to install all modules available for Qt 5.15.2 is to send the output of
 .. code-block:: console
 
     $ aqt install-qt windows desktop 5.15.2 win64_mingw81 \
-          -m $(aqt list-qt windows desktop --modules 5.15.2)
+          -m $(aqt list-qt windows desktop --modules 5.15.2 win64_mingw81)
 
 You will need a Unix-style shell to run this command, or at least git-bash on Windows.
 The ``xargs`` equivalent to this command is an exercise left to the reader.
@@ -203,15 +203,15 @@ for Desktop on Windows, but there will be differences when we get to Qt 6.
 
 .. code-block:: console
 
-    $ aqt list-qt windows android                     # Print Qt versions available
+    $ aqt list-qt windows android                           # Print Qt versions available
     5.9.0 5.9.1 ...
     ...
     6.2.0
 
-    $ aqt list-qt windows android --arch 5.15.2       # Print architectures available
+    $ aqt list-qt windows android --arch 5.15.2             # Print architectures available
     android
 
-    $ aqt list-qt windows android --modules 5.15.2    # Print modules available
+    $ aqt list-qt windows android --modules 5.15.2 android  # Print modules available
     qtcharts qtdatavis3d qtlottie qtnetworkauth qtpurchasing qtquick3d qtquicktimeline qtscript
 
     $ aqt install-qt windows android 5.15.2 android -m qtcharts qtnetworkauth   # Install
@@ -220,12 +220,12 @@ Let's see what happens when we try to list architectures and modules for Qt 6:
 
 .. code-block:: console
 
-    $ aqt list-qt windows android --arch 6.2.0       # Print architectures available
+    $ aqt list-qt windows android --arch 6.2.0                  # Print architectures available
     Command line input error: Qt 6 for Android requires one of the following extensions:
     ('x86_64', 'x86', 'armv7', 'arm64_v8a').
     Please add your extension using the `--extension` flag.
 
-    $ aqt list-qt windows android --modules 6.2.0    # Print modules available
+    $ aqt list-qt windows android --modules 6.2.0 android_armv7     # Print modules available
     Command line input error: Qt 6 for Android requires one of the following extensions:
     ('x86_64', 'x86', 'armv7', 'arm64_v8a').
     Please add your extension using the `--extension` flag.
@@ -237,19 +237,10 @@ backwards, but that's how the Qt repo was put together.
 
 There are four architectures available, and the error message from :ref:`aqt list-qt <list qt command>`
 just told us what they are: `x86_64`, `x86`, `armv7`, and `arm64_v8a`.
-If we want to install Qt 6.2.0 for armv7, we use this command to print available modules:
-
-.. code-block:: console
-
-    $ aqt list-qt windows android --extension armv7 --modules 6.2.0
-    qt3d qt5compat qtcharts qtconnectivity qtdatavis3d qtimageformats qtlottie
-    qtmultimedia qtnetworkauth qtpositioning qtquick3d qtquicktimeline
-    qtremoteobjects qtscxml qtsensors qtserialbus qtserialport qtshadertools
-    qtvirtualkeyboard qtwebchannel qtwebsockets qtwebview
 
 We know we want to use `armv7` for the architecture, but we don't know exactly
-what value for 'architecture' we need to pass to :ref:`aqt install-qt <qt installation command>` yet, so we
-will use :ref:`aqt list-qt <list qt command>` again:
+what value for 'architecture' we need to pass to :ref:`aqt install-qt <qt installation command>`
+yet, so we will use :ref:`aqt list-qt <list qt command>` again:
 
 .. code-block:: console
 
@@ -264,7 +255,18 @@ message, instead of the obvious `android_armv7`, we would know that Qt 6.2.0
 for that architecture doesn't exist for some reason, and any attempt to install
 it with :ref:`aqt install-qt <qt installation command>` will fail.
 
-Finally, let's install Qt 6.2.0 for Android armv7 with some modules:
+If we want to install Qt 6.2.0 for armv7, we use this command to print available modules:
+
+.. code-block:: console
+
+    $ aqt list-qt windows android --extension armv7 --modules 6.2.0 android_armv7
+    qt3d qt5compat qtcharts qtconnectivity qtdatavis3d qtimageformats qtlottie
+    qtmultimedia qtnetworkauth qtpositioning qtquick3d qtquicktimeline
+    qtremoteobjects qtscxml qtsensors qtserialbus qtserialport qtshadertools
+    qtvirtualkeyboard qtwebchannel qtwebsockets qtwebview
+
+Finally, let's install Qt 6.2.0 for Android armv7 with the ``qtcharts`` and
+``qtnetworkauth`` modules:
 
 .. code-block:: console
 
@@ -292,10 +294,10 @@ We can check the architecture and modules available as before:
 
 .. code-block:: console
 
-    $ aqt list-qt windows desktop --extension wasm --arch 5.15.2     # Print architectures available
+    $ aqt list-qt windows desktop --extension wasm --arch 5.15.2        # available architectures
     wasm_32
 
-    $ aqt list-qt windows desktop --extension wasm --modules 5.15.2  # Print modules available
+    $ aqt list-qt windows desktop --extension wasm --modules 5.15.2 wasm_32   # available modules
     qtcharts qtdatavis3d qtlottie qtnetworkauth qtpurchasing qtquicktimeline qtscript
     qtvirtualkeyboard qtwebglplugin
 


Test patch:
diff --git a/tests/data/windows-5140-expect.json b/tests/data/windows-5140-expect.json
index 7982dda..3cd46e9 100644
--- a/tests/data/windows-5140-expect.json
+++ b/tests/data/windows-5140-expect.json
@@ -1,18 +1,4 @@
 {
-  "modules": [
-    "debug_info",
-    "qtcharts",
-    "qtdatavis3d",
-    "qtlottie",
-    "qtnetworkauth",
-    "qtpurchasing",
-    "qtquick3d",
-    "qtquicktimeline",
-    "qtscript",
-    "qtvirtualkeyboard",
-    "qtwebengine",
-    "qtwebglplugin"
-  ],
   "architectures": [
     "win32_mingw73",
     "win32_msvc2017",
@@ -20,6 +6,73 @@
     "win64_msvc2015_64",
     "win64_msvc2017_64"
   ],
+  "modules_by_arch": {
+    "win32_mingw73": [
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebglplugin"
+    ],
+    "win32_msvc2017": [
+      "debug_info",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebengine",
+      "qtwebglplugin"
+    ],
+    "win64_mingw73": [
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebglplugin"
+    ],
+    "win64_msvc2015_64": [
+      "debug_info",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebglplugin"
+    ],
+    "win64_msvc2017_64": [
+      "debug_info",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebengine",
+      "qtwebglplugin"
+    ]
+  },
   "modules_metadata_by_arch": {
     "win32_mingw73": [
       {
diff --git a/tests/data/windows-5150-expect.json b/tests/data/windows-5150-expect.json
index 7952ada..ee79c34 100644
--- a/tests/data/windows-5150-expect.json
+++ b/tests/data/windows-5150-expect.json
@@ -1,23 +1,76 @@
 {
-  "modules": [
-    "debug_info",
-    "qtcharts",
-    "qtdatavis3d",
-    "qtlottie",
-    "qtnetworkauth",
-    "qtpurchasing",
-    "qtquick3d",
-    "qtquicktimeline",
-    "qtscript",
-    "qtvirtualkeyboard",
-    "qtwebengine",
-    "qtwebglplugin"
-  ],
   "architectures": [
     "win32_mingw81",
     "win32_msvc2019",
     "win64_mingw81",
     "win64_msvc2015_64",
     "win64_msvc2019_64"
-  ]
+  ],
+  "modules_by_arch": {
+    "win32_mingw81": [
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebglplugin"
+    ],
+    "win32_msvc2019": [
+      "debug_info",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebengine",
+      "qtwebglplugin"
+    ],
+    "win64_mingw81": [
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebglplugin"
+    ],
+    "win64_msvc2015_64": [
+      "debug_info",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebglplugin"
+    ],
+    "win64_msvc2019_64": [
+      "debug_info",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpurchasing",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtscript",
+      "qtvirtualkeyboard",
+      "qtwebengine",
+      "qtwebglplugin"
+    ]
+  }
 }
\ No newline at end of file
diff --git a/tests/data/windows-620-expect.json b/tests/data/windows-620-expect.json
index b82a31a..fc07e88 100644
--- a/tests/data/windows-620-expect.json
+++ b/tests/data/windows-620-expect.json
@@ -1,34 +1,85 @@
 {
-  "modules": [
-    "debug_info",
-    "qt3d",
-    "qt5compat",
-    "qtactiveqt",
-    "qtcharts",
-    "qtconnectivity",
-    "qtdatavis3d",
-    "qtimageformats",
-    "qtlottie",
-    "qtmultimedia",
-    "qtnetworkauth",
-    "qtpositioning",
-    "qtquick3d",
-    "qtquicktimeline",
-    "qtremoteobjects",
-    "qtscxml",
-    "qtsensors",
-    "qtserialbus",
-    "qtserialport",
-    "qtshadertools",
-    "qtvirtualkeyboard",
-    "qtwebchannel",
-    "qtwebengine",
-    "qtwebsockets",
-    "qtwebview"
-  ],
   "architectures": [
     "win64_mingw81",
     "win64_msvc2019_64",
     "win64_msvc2019_arm64"
-  ]
+  ],
+  "modules_by_arch": {
+    "win64_mingw81": [
+      "qt3d",
+      "qtactiveqt",
+      "qtcharts",
+      "qtconnectivity",
+      "qtdatavis3d",
+      "qtimageformats",
+      "qtlottie",
+      "qtmultimedia",
+      "qtnetworkauth",
+      "qtpositioning",
+      "qtremoteobjects",
+      "qtscxml",
+      "qtsensors",
+      "qtserialbus",
+      "qtserialport",
+      "qtvirtualkeyboard",
+      "qtwebchannel",
+      "qtwebsockets",
+      "qtwebview",
+      "debug_info",
+      "qt5compat",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtshadertools"
+    ],
+    "win64_msvc2019_64": [
+      "qt3d",
+      "qtactiveqt",
+      "qtcharts",
+      "qtconnectivity",
+      "qtdatavis3d",
+      "qtimageformats",
+      "qtlottie",
+      "qtmultimedia",
+      "qtnetworkauth",
+      "qtpositioning",
+      "qtremoteobjects",
+      "qtscxml",
+      "qtsensors",
+      "qtserialbus",
+      "qtserialport",
+      "qtvirtualkeyboard",
+      "qtwebchannel",
+      "qtwebengine",
+      "qtwebsockets",
+      "qtwebview",
+      "debug_info",
+      "qt5compat",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtshadertools"
+    ],
+    "win64_msvc2019_arm64": [
+      "qt3d",
+      "qtactiveqt",
+      "qtcharts",
+      "qtdatavis3d",
+      "qtimageformats",
+      "qtlottie",
+      "qtnetworkauth",
+      "qtpositioning",
+      "qtremoteobjects",
+      "qtscxml",
+      "qtsensors",
+      "qtserialbus",
+      "qtserialport",
+      "qtvirtualkeyboard",
+      "qtwebchannel",
+      "qtwebsockets",
+      "debug_info",
+      "qt5compat",
+      "qtquick3d",
+      "qtquicktimeline",
+      "qtshadertools"
+    ]
+  }
 }
\ No newline at end of file
diff --git a/tests/test_cli.py b/tests/test_cli.py
index a839564..4d90566 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -133,7 +133,7 @@ def test_cli_invalid_version(capsys, invalid_version):
     for cmd in (
         ("install", invalid_version, "mac", "desktop"),
         ("doc", invalid_version, "mac", "desktop"),
-        ("list-qt", "mac", "desktop", "--modules", invalid_version),
+        ("list-qt", "mac", "desktop", "--arch", invalid_version),
     ):
         cli = Cli()
         assert cli.run(cmd) == 1
diff --git a/tests/test_list.py b/tests/test_list.py
index c253305..6d5a5f2 100644
--- a/tests/test_list.py
+++ b/tests/test_list.py
@@ -191,8 +191,9 @@ def test_list_architectures_and_modules(monkeypatch, version: str, extension: st
 
     monkeypatch.setattr(MetadataFactory, "fetch_http", lambda self, _: _xml)
 
-    modules = MetadataFactory(archive_id).fetch_modules(Version(version))
-    assert modules == expect["modules"]
+    for arch in expect["architectures"]:
+        modules = MetadataFactory(archive_id).fetch_modules(Version(version), arch)
+        assert modules == sorted(expect["modules_by_arch"][arch])
 
     arches = MetadataFactory(archive_id).fetch_arches(Version(version))
     assert arches == expect["architectures"]
@@ -223,10 +224,9 @@ def test_tool_modules(monkeypatch, host: str, target: str, tool_name: str):
 
 
 @pytest.fixture
-def expected_windows_desktop_5140() -> Dict[str, Set[str]]:
+def expected_windows_desktop_5140() -> Dict:
     xmlexpect = "windows-5140-expect.json"
-    expected_modules_arches = json.loads((Path(__file__).parent / "data" / xmlexpect).read_text("utf-8"))
-    return {key: set(val) for key, val in expected_modules_arches.items()}
+    return json.loads((Path(__file__).parent / "data" / xmlexpect).read_text("utf-8"))
 
 
 @pytest.mark.parametrize(
@@ -235,18 +235,22 @@ def expected_windows_desktop_5140() -> Dict[str, Set[str]]:
         ("--extensions latest", {"src_doc_examples"}),
         ("--spec 5.14 --extensions latest", {"wasm", "src_doc_examples"}),
         ("--extensions 5.14.0", {"wasm", "src_doc_examples"}),
-        ("--modules latest", "modules"),
-        ("--spec 5.14 --modules latest", "modules"),
-        ("--modules 5.14.0", "modules"),
-        ("--extension wasm --modules latest", "modules"),
-        ("--extension wasm --spec 5.14 --modules latest", "modules"),
-        ("--extension wasm --modules 5.14.0", "modules"),
-        ("--arch latest", "architectures"),
-        ("--spec 5.14 --arch latest", "architectures"),
-        ("--arch 5.14.0", "architectures"),
-        ("--extension wasm --arch latest", "architectures"),
-        ("--extension wasm --spec 5.14 --arch latest", "architectures"),
-        ("--extension wasm --arch 5.14.0", "architectures"),
+        ("--modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
+        ("--spec 5.14 --modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
+        ("--modules 5.14.0 win32_mingw73", ["modules_by_arch", "win32_mingw73"]),
+        ("--modules 5.14.0 win32_msvc2017", ["modules_by_arch", "win32_msvc2017"]),
+        ("--modules 5.14.0 win64_mingw73", ["modules_by_arch", "win64_mingw73"]),
+        ("--modules 5.14.0 win64_msvc2015_64", ["modules_by_arch", "win64_msvc2015_64"]),
+        ("--modules 5.14.0 win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
+        ("--extension wasm --modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
+        ("--extension wasm --spec 5.14 --modules latest win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
+        ("--extension wasm --modules 5.14.0 win64_msvc2017_64", ["modules_by_arch", "win64_msvc2017_64"]),
+        ("--arch latest", ["architectures"]),
+        ("--spec 5.14 --arch latest", ["architectures"]),
+        ("--arch 5.14.0", ["architectures"]),
+        ("--extension wasm --arch latest", ["architectures"]),
+        ("--extension wasm --spec 5.14 --arch latest", ["architectures"]),
+        ("--extension wasm --arch 5.14.0", ["architectures"]),
     ),
 )
 def test_list_qt_cli(
@@ -254,10 +258,20 @@ def test_list_qt_cli(
     capsys,
     expected_windows_desktop_5140: Dict[str, Set[str]],
     args: str,
-    expect: Union[Set[str], str],
+    expect: Union[Set[str], List[str]],
 ):
     htmlfile, xmlfile = "windows-desktop.html", "windows-5140-update.xml"
     version_string_to_replace = "qt5.5140"
+    if isinstance(expect, list):
+        # In this case, 'expect' is a list of keys to follow to the expected values.
+        expected_dict = expected_windows_desktop_5140
+        for key in expect:  # Follow the chain of keys to the list of values.
+            expected_dict = expected_dict[key]
+        assert isinstance(expected_dict, list)
+        expect_set = set(expected_dict)
+    else:
+        expect_set = expect
+    assert isinstance(expect_set, set)
 
     def _mock_fetch_http(_, rest_of_url: str) -> str:
         htmltext = (Path(__file__).parent / "data" / htmlfile).read_text("utf-8")
@@ -279,7 +293,6 @@ def test_list_qt_cli(
     cli.run(["list-qt", "windows", "desktop", *args.split()])
     out, err = capsys.readouterr()
     output_set = set(out.strip().split())
-    expect_set = expected_windows_desktop_5140[expect] if isinstance(expect, str) else expect
     assert output_set == expect_set
 
 
@@ -406,11 +419,10 @@ def test_list_invalid_extensions(capsys, monkeypatch, target, ext, version, expe
 
 mac_qt = ArchiveId("qt", "mac", "desktop")
 mac_wasm = ArchiveId("qt", "mac", "desktop", "wasm")
-wrong_qt_version_msg = ["Please use 'aqt list-qt mac desktop' to show versions of Qt available."]
-wrong_ext_and_version_msg = [
-    "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions.",
-    "Please use 'aqt list-qt mac desktop' to show versions of Qt available.",
-]
+wrong_tool_name_msg = "Please use 'aqt list-tool mac desktop' to check what tools are available."
+wrong_qt_version_msg = "Please use 'aqt list-qt mac desktop' to show versions of Qt available."
+wrong_ext_msg = "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions."
+wrong_arch_msg = "Please use 'aqt list-qt mac desktop --arch <QT_VERSION>' to list valid architectures."
 
 
 @pytest.mark.parametrize(
@@ -423,28 +435,28 @@ wrong_ext_and_version_msg = [
         ),
         (
             MetadataFactory(ArchiveId("tools", "mac", "desktop"), tool_name="ifw"),
-            ["Please use 'aqt list-tool mac desktop' to check what tools are available."],
+            [wrong_tool_name_msg],
         ),
         (
             MetadataFactory(mac_qt, architectures_ver="1.2.3"),
-            wrong_qt_version_msg,
+            [wrong_qt_version_msg],
         ),
         (
-            MetadataFactory(mac_qt, modules_ver="1.2.3"),
-            wrong_qt_version_msg,
+            MetadataFactory(mac_qt, modules_query=("1.2.3", "clang_64")),
+            [wrong_qt_version_msg, wrong_arch_msg],
         ),
         (
             MetadataFactory(mac_qt, extensions_ver="1.2.3"),
-            wrong_qt_version_msg,
+            [wrong_qt_version_msg],
         ),
         (
             MetadataFactory(mac_wasm),
-            ["Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions."],
+            [wrong_ext_msg],
         ),
         (
             MetadataFactory(mac_wasm, spec=SimpleSpec("<5.9")),
             [
-                "Please use 'aqt list-qt mac desktop --extensions <QT_VERSION>' to list valid extensions.",
+                wrong_ext_msg,
                 "Please use 'aqt list-qt mac desktop' to check that versions of qt exist within the spec '<5.9'.",
             ],
         ),
@@ -452,20 +464,20 @@ wrong_ext_and_version_msg = [
             MetadataFactory(ArchiveId("tools", "mac", "desktop", "wasm"), tool_name="ifw"),
             [
                 "Please use 'aqt list-tool mac desktop --extensions <QT_VERSION>' to list valid extensions.",
-                "Please use 'aqt list-tool mac desktop' to check what tools are available.",
+                wrong_tool_name_msg,
             ],
         ),
         (
             MetadataFactory(mac_wasm, architectures_ver="1.2.3"),
-            wrong_ext_and_version_msg,
+            [wrong_ext_msg, wrong_qt_version_msg],
         ),
         (
-            MetadataFactory(mac_wasm, modules_ver="1.2.3"),
-            wrong_ext_and_version_msg,
+            MetadataFactory(mac_wasm, modules_query=("1.2.3", "clang_64")),
+            [wrong_ext_msg, wrong_qt_version_msg, wrong_arch_msg],
         ),
         (
             MetadataFactory(mac_wasm, extensions_ver="1.2.3"),
-            wrong_ext_and_version_msg,
+            [wrong_ext_msg, wrong_qt_version_msg],
         ),
     ),
 )