diff --git a/CHANGELOG.md b/CHANGELOG.md index f4dde004d4c2..bd66bac41a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- Fixed `config_store` on Python 3.14, where `typing.Union`/`typing.Optional` annotations could no longer be remapped in-place via the now read-only `__args__` attribute + ### Security ## [2.8.0] - 2026-06-05 diff --git a/test/test_config_store.py b/test/test_config_store.py index 9c0179b9bbdb..1ea2bf24cd24 100644 --- a/test/test_config_store.py +++ b/test/test_config_store.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union from torch_geometric.config_store import ( class_from_dataclass, @@ -58,6 +58,18 @@ def test_map_annotation(): assert map_annotation(list[int], mapping) == list[Any] assert map_annotation(tuple[int], mapping) == tuple[Any] + # `typing.Union`/`typing.Optional` must be rebuilt without mutating + # `__args__`, which is read-only on Python >= 3.14: + assert map_annotation(Optional[int], mapping) == Optional[Any] + assert map_annotation(Union[int, str], mapping) == Union[Any, str] + assert map_annotation(List[Optional[int]], mapping) == List[Optional[Any]] + assert map_annotation(Dict[str, Optional[int]], + mapping) == Dict[str, Optional[Any]] + # When every Union member maps to the same type, the rebuilt annotation + # collapses to canonical `Any` (the old in-place mutation left a + # degenerate `Union[Any, Any]`): + assert map_annotation(Union[int, str], {int: Any, str: Any}) == Any + def test_register(): register(AddSelfLoops, group='transform') diff --git a/torch_geometric/config_store.py b/torch_geometric/config_store.py index b45a267abd41..df6cb2f105c4 100644 --- a/torch_geometric/config_store.py +++ b/torch_geometric/config_store.py @@ -1,4 +1,3 @@ -import copy import inspect import typing from collections import defaultdict @@ -170,10 +169,17 @@ def map_annotation( if type(annotation).__name__ == 'GenericAlias': # If annotated with `list[...]` or `dict[...]`: annotation = origin[args] + elif origin is Union: + # If annotated with `typing.Union[...]` or `typing.Optional[...]`. + # Rebuild instead of mutating `__args__`, which is read-only on + # Python >= 3.14 (`typing.Union` instances no longer allow + # in-place attribute assignment). + annotation = Union[args] else: - # If annotated with `typing.List[...]` or `typing.Dict[...]`: - annotation = copy.copy(annotation) - annotation.__args__ = args + # If annotated with `typing.List[...]` or `typing.Dict[...]`. + # Rebuild via `copy_with` rather than mutating `__args__`, which is + # read-only on Python >= 3.14. + annotation = annotation.copy_with(tuple(args)) return annotation