|
2 | 2 |
|
3 | 3 | import csv |
4 | 4 | import hashlib |
| 5 | +import json |
5 | 6 | import os |
6 | 7 | import stat |
7 | 8 | import sys |
8 | 9 | import tempfile |
9 | 10 | import zipfile |
| 11 | +from collections import defaultdict |
10 | 12 | from functools import cached_property |
11 | 13 | from io import StringIO |
12 | 14 | from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Sequence, Tuple, cast |
13 | 15 |
|
| 16 | +if TYPE_CHECKING: |
| 17 | + from hatchling.metadata.core import ProjectMetadata |
| 18 | + |
14 | 19 | from hatchling.__about__ import __version__ |
15 | 20 | from hatchling.builders.config import BuilderConfig |
16 | 21 | from hatchling.builders.constants import EDITABLES_REQUIREMENT |
|
26 | 31 | replace_file, |
27 | 32 | set_zip_info_mode, |
28 | 33 | ) |
| 34 | +from hatchling.builders.variant_constants import ( |
| 35 | + VALIDATION_PROPERTY_REGEX, |
| 36 | + VARIANT_DIST_INFO_FILENAME, |
| 37 | + VARIANT_INFO_DEFAULT_PRIO_KEY, |
| 38 | + VARIANT_INFO_FEATURE_KEY, |
| 39 | + VARIANT_INFO_NAMESPACE_KEY, |
| 40 | + VARIANT_INFO_PROPERTY_KEY, |
| 41 | + VARIANT_INFO_PROVIDER_DATA_KEY, |
| 42 | + VARIANT_INFO_PROVIDER_ENABLE_IF_KEY, |
| 43 | + VARIANT_INFO_PROVIDER_OPTIONAL_KEY, |
| 44 | + VARIANT_INFO_PROVIDER_PLUGIN_API_KEY, |
| 45 | + VARIANT_INFO_PROVIDER_REQUIRES_KEY, |
| 46 | + VARIANTS_JSON_SCHEMA_KEY, |
| 47 | + VARIANTS_JSON_SCHEMA_URL, |
| 48 | + VARIANTS_JSON_VARIANT_DATA_KEY, |
| 49 | +) |
29 | 50 | from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors |
30 | 51 |
|
31 | 52 | if TYPE_CHECKING: |
@@ -188,6 +209,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: |
188 | 209 | super().__init__(*args, **kwargs) |
189 | 210 |
|
190 | 211 | self.__core_metadata_constructor: Callable[..., str] | None = None |
| 212 | + self.__variants_json_constructor: Callable[..., str] | None = None |
191 | 213 | self.__shared_data: dict[str, str] | None = None |
192 | 214 | self.__shared_scripts: dict[str, str] | None = None |
193 | 215 | self.__extra_metadata: dict[str, str] | None = None |
@@ -282,6 +304,80 @@ def core_metadata_constructor(self) -> Callable[..., str]: |
282 | 304 |
|
283 | 305 | return self.__core_metadata_constructor |
284 | 306 |
|
| 307 | + @property |
| 308 | + def variants_json_constructor(self) -> Callable[..., str]: |
| 309 | + if self.__variants_json_constructor is None: |
| 310 | + def constructor(metadata: ProjectMetadata) -> str: |
| 311 | + if metadata.variant_hash is not None: |
| 312 | + data = { |
| 313 | + VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL, |
| 314 | + VARIANT_INFO_DEFAULT_PRIO_KEY: {}, |
| 315 | + VARIANT_INFO_PROVIDER_DATA_KEY: {}, |
| 316 | + VARIANTS_JSON_VARIANT_DATA_KEY: {} |
| 317 | + } |
| 318 | + |
| 319 | + # ==================== VARIANT_INFO_DEFAULT_PRIO_KEY ==================== # |
| 320 | + |
| 321 | + if (ns_prio := metadata.variant_default_priorities["namespace"]): |
| 322 | + data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_NAMESPACE_KEY] = ns_prio |
| 323 | + |
| 324 | + if (feat_prio := metadata.variant_default_priorities["feature"]): |
| 325 | + data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_FEATURE_KEY] = feat_prio |
| 326 | + |
| 327 | + if (prop_prio := metadata.variant_default_priorities["property"]): |
| 328 | + data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_PROPERTY_KEY] = prop_prio |
| 329 | + |
| 330 | + if not data[VARIANT_INFO_DEFAULT_PRIO_KEY]: |
| 331 | + # If no default priorities are set, remove the key |
| 332 | + del data[VARIANT_INFO_DEFAULT_PRIO_KEY] |
| 333 | + |
| 334 | + # ==================== VARIANT_INFO_PROVIDER_DATA_KEY ==================== # |
| 335 | + |
| 336 | + variant_providers = defaultdict(dict) |
| 337 | + for ns, plugin_conf in metadata.variant_plugins.items(): |
| 338 | + variant_providers[ns][VARIANT_INFO_PROVIDER_REQUIRES_KEY] = plugin_conf.get("requires", []) |
| 339 | + |
| 340 | + if (enable_if := plugin_conf.get("enable_if", None)) is not None: |
| 341 | + variant_providers[ns][VARIANT_INFO_PROVIDER_ENABLE_IF_KEY] = enable_if |
| 342 | + |
| 343 | + if plugin_conf.get("optional", False): |
| 344 | + variant_providers[ns][VARIANT_INFO_PROVIDER_OPTIONAL_KEY] = True |
| 345 | + |
| 346 | + if (plugin_api := plugin_conf.get("plugin_api", None)) is not None: |
| 347 | + variant_providers[ns][VARIANT_INFO_PROVIDER_PLUGIN_API_KEY] = plugin_api |
| 348 | + |
| 349 | + data[VARIANT_INFO_PROVIDER_DATA_KEY] = variant_providers |
| 350 | + |
| 351 | + # ==================== VARIANTS_JSON_VARIANT_DATA_KEY ==================== # |
| 352 | + |
| 353 | + variant_data = defaultdict(lambda: defaultdict(set)) |
| 354 | + for vprop_str in metadata.variant_properties: |
| 355 | + match = VALIDATION_PROPERTY_REGEX.match(vprop_str) |
| 356 | + if not match: |
| 357 | + raise ValueError( |
| 358 | + f"Invalid variant property '{vprop_str}' in variant {metadata.variant_hash}" |
| 359 | + ) |
| 360 | + namespace = match.group('namespace') |
| 361 | + feature = match.group('feature') |
| 362 | + value = match.group('value') |
| 363 | + variant_data[namespace][feature].add(value) |
| 364 | + data[VARIANTS_JSON_VARIANT_DATA_KEY][metadata.variant_hash] = variant_data |
| 365 | + |
| 366 | + def preprocess(data): |
| 367 | + """Preprocess the data to ensure it is JSON serializable.""" |
| 368 | + if isinstance(data, (defaultdict, dict)): |
| 369 | + return {k: preprocess(v) for k, v in data.items()} |
| 370 | + if isinstance(data, set): |
| 371 | + return list(data) |
| 372 | + return data |
| 373 | + |
| 374 | + return json.dumps( |
| 375 | + preprocess(data), indent=4, sort_keys=True, ensure_ascii=False |
| 376 | + ) |
| 377 | + return '' |
| 378 | + self.__variants_json_constructor = constructor |
| 379 | + return self.__variants_json_constructor |
| 380 | + |
285 | 381 | @property |
286 | 382 | def shared_data(self) -> dict[str, str]: |
287 | 383 | if self.__shared_data is None: |
@@ -710,6 +806,11 @@ def write_project_metadata( |
710 | 806 | 'METADATA', self.config.core_metadata_constructor(self.metadata, extra_dependencies=extra_dependencies) |
711 | 807 | ) |
712 | 808 | records.write(record) |
| 809 | + record = archive.write_metadata( |
| 810 | + VARIANT_DIST_INFO_FILENAME, |
| 811 | + self.config.variants_json_constructor(self.metadata), |
| 812 | + ) |
| 813 | + records.write(record) |
713 | 814 |
|
714 | 815 | def add_licenses(self, archive: WheelArchive, records: RecordFile) -> None: |
715 | 816 | for relative_path in self.metadata.core.license_files: |
|
0 commit comments