|
15 | 15 | from __future__ import annotations
|
16 | 16 |
|
17 | 17 | import inspect
|
| 18 | +import typing |
| 19 | +import warnings |
18 | 20 | from types import MappingProxyType
|
19 | 21 | from typing import TYPE_CHECKING, Any, Callable, Sequence, cast
|
20 | 22 |
|
@@ -69,9 +71,65 @@ def make_annotated(annotation: Any = Any, options: dict | None = None) -> Any:
|
69 | 71 | f"Every item in Annotated must be castable to a dict, got: {opt!r}"
|
70 | 72 | ) from e
|
71 | 73 | _options.update(_opt)
|
| 74 | + |
| 75 | + annotation = _make_hashable(annotation) |
| 76 | + _options = _make_hashable(_options) |
72 | 77 | return Annotated[annotation, _options]
|
73 | 78 |
|
74 | 79 |
|
| 80 | +# this is a stupid hack to deal with something that changed in typing-extensions |
| 81 | +# v4.12.0 (and probably python 3.13), where all items in Annotated must now be hashable |
| 82 | +# The PR that necessitated this was https://github.com/python/typing_extensions/pull/392 |
| 83 | + |
| 84 | + |
| 85 | +class hashabledict(dict): |
| 86 | + """Hashable immutable dict.""" |
| 87 | + |
| 88 | + def __hash__(self) -> int: # type: ignore # noqa: D105 |
| 89 | + # we don't actually want to hash the contents, just the object itself. |
| 90 | + return id(self) |
| 91 | + |
| 92 | + def __setitem__(self, key: Any, value: Any) -> None: # noqa: D105 |
| 93 | + warnings.warn( |
| 94 | + "Mutating magicgui Annotation metadata after creation is not supported." |
| 95 | + "This will raise an error in a future version.", |
| 96 | + stacklevel=2, |
| 97 | + ) |
| 98 | + super().__setitem__(key, value) |
| 99 | + |
| 100 | + def __delitem__(self, key: Any) -> None: # noqa: D105 |
| 101 | + raise TypeError("hashabledict is immutable") |
| 102 | + |
| 103 | + |
| 104 | +def _hashable(obj: Any) -> bool: |
| 105 | + try: |
| 106 | + hash(obj) |
| 107 | + return True |
| 108 | + except TypeError: |
| 109 | + return False |
| 110 | + |
| 111 | + |
| 112 | +def _make_hashable(obj: Any) -> Any: |
| 113 | + if _hashable(obj): |
| 114 | + return obj |
| 115 | + if isinstance(obj, dict): |
| 116 | + return hashabledict({k: _make_hashable(v) for k, v in obj.items()}) |
| 117 | + if isinstance(obj, (list, tuple)): |
| 118 | + return tuple(_make_hashable(v) for v in obj) |
| 119 | + if isinstance(obj, set): |
| 120 | + return frozenset(_make_hashable(v) for v in obj) |
| 121 | + if (args := get_args(obj)) and (not _hashable(args)): |
| 122 | + try: |
| 123 | + obj = get_origin(obj)[_make_hashable(args)] |
| 124 | + except TypeError: |
| 125 | + # python 3.8 hack |
| 126 | + if obj.__module__ == "typing" and hasattr(obj, "_name"): |
| 127 | + generic = getattr(typing, obj._name) |
| 128 | + return generic[_make_hashable(args)] |
| 129 | + raise |
| 130 | + return obj |
| 131 | + |
| 132 | + |
75 | 133 | class _void:
|
76 | 134 | """private sentinel."""
|
77 | 135 |
|
|
0 commit comments