| 115 | |
| 116 | @functools.lru_cache |
| 117 | def _get_packages() -> dict[str, str]: |
| 118 | try: |
| 119 | packages = _get_installer_packages() |
| 120 | except urllib.error.URLError as exc: # e.g., bad internet connection |
| 121 | if not REQUIRE_METADATA: |
| 122 | sphinx_logger.warning(f"Could not fetch package list, got: {exc}") |
| 123 | return dict() |
| 124 | raise |
| 125 | # There can be duplicates in manual and installer packages because some of the |
| 126 | # PyPI entries for installer packages are incorrect or unusable (see above), so |
| 127 | # we don't enforce that. But PyPI and manual should be disjoint: |
| 128 | dups = set(MANUAL_PACKAGES) & set(PYPI_PACKAGES) |
| 129 | assert not dups, f"Duplicates in MANUAL_PACKAGES and PYPI_PACKAGES: {sorted(dups)}" |
| 130 | # And the installer and PyPI-only should be disjoint: |
| 131 | dups = set(PYPI_PACKAGES) & set(packages) |
| 132 | assert not dups, ( |
| 133 | f"Duplicates in PYPI_PACKAGES and installer packages: {sorted(dups)}" |
| 134 | ) |
| 135 | for name in PYPI_PACKAGES | set(MANUAL_PACKAGES): |
| 136 | if name not in packages: |
| 137 | packages.append(name) |
| 138 | # Simple alphabetical order |
| 139 | packages = sorted(packages, key=lambda x: x.lower()) |
| 140 | packages = [RENAMES.get(package, package) for package in packages] |
| 141 | out = dict() |
| 142 | reasons = [] |
| 143 | for package in status_iterator( |
| 144 | packages, f"Adding {len(packages)} related software packages: " |
| 145 | ): |
| 146 | out[package] = dict() |
| 147 | try: |
| 148 | if package in MANUAL_PACKAGES: |
| 149 | md = MANUAL_PACKAGES[package] |
| 150 | else: |
| 151 | md = importlib.metadata.metadata(package) |
| 152 | except importlib.metadata.PackageNotFoundError: |
| 153 | reasons.append(f"{package}: not found, needs to be installed") |
| 154 | continue # raise a complete error later |
| 155 | else: |
| 156 | # Every project should really have this |
| 157 | do_continue = False |
| 158 | for key in ("Summary",): |
| 159 | if key not in md: |
| 160 | reasons.extend(f"{package}: missing {repr(key)}") |
| 161 | do_continue = True |
| 162 | if do_continue: |
| 163 | continue |
| 164 | # It is annoying to find the home page |
| 165 | url = None |
| 166 | if "Home-page" in md: |
| 167 | url = md["Home-page"] |
| 168 | else: |
| 169 | for prefix in ("homepage", "documentation", "user documentation"): |
| 170 | for key, val in md.items(): |
| 171 | if key == "Project-URL" and val.lower().startswith( |
| 172 | f"{prefix}, " |
| 173 | ): |
| 174 | url = val.split(", ", 1)[1] |