Skip to content

Commit 3fc6823

Browse files
fix: fix compatibility with typing-extensions 0.4.12 (#649)
* fix: typing extensions issue * no pyside 6.7.1 * use v2 * fix workflow * skip 6.7.1 * don't raise on mutation * style(pre-commit.ci): auto fixes [...] * fix: fix nested * fix py38 * style(pre-commit.ci): auto fixes [...] * fix 6.7.1 error --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent f50a8dc commit 3fc6823

File tree

5 files changed

+81
-10
lines changed

5 files changed

+81
-10
lines changed

.github/workflows/test_and_deploy.yml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ on:
1616
jobs:
1717
test:
1818
name: Test
19-
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v1
19+
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
2020
with:
2121
os: ${{ matrix.os }}
2222
python-version: ${{ matrix.python-version }}
2323
qt: ${{ matrix.qt }}
2424
pip-install-pre-release: ${{ github.event_name == 'schedule' }}
25-
report-failures: ${{ github.event_name == 'schedule' }}
26-
secrets: inherit
25+
coverage-upload: artifact
2726
strategy:
2827
fail-fast: false
2928
matrix:
@@ -58,18 +57,24 @@ jobs:
5857

5958
test-min-reqs:
6059
name: Test min reqs
61-
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@main
60+
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
6261
with:
6362
os: ubuntu-latest
6463
python-version: ${{ matrix.python-version }}
6564
qt: pyqt5
6665
pip-install-min-reqs: true
67-
secrets: inherit
66+
coverage-upload: artifact
6867
strategy:
6968
fail-fast: false
7069
matrix:
7170
python-version: ["3.8", "3.9", "3.10", "3.11"]
7271

72+
upload_coverage:
73+
if: always()
74+
needs: [test, test-min-reqs]
75+
uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2
76+
secrets: inherit
77+
7378
test-pydantic1:
7479
name: Test pydantic1
7580
runs-on: ubuntu-latest
@@ -97,15 +102,15 @@ jobs:
97102
token: ${{ secrets.CODECOV_TOKEN }}
98103

99104
test-dependents:
100-
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v1
105+
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v2
101106
with:
102107
dependency-repo: ${{ matrix.package }}
103108
dependency-ref: ${{ matrix.package-version }}
104109
dependency-extras: ${{ matrix.package-extras || 'testing' }}
105110
host-extras: "testing"
106111
qt: pyqt5
107112
python-version: "3.10"
108-
post-install-cmd: "python -m pip install pytest-pretty lxml_html_clean" # just for napari
113+
post-install-cmd: "python -m pip install pytest-pretty lxml_html_clean" # just for napari
109114
pytest-args: ${{ matrix.pytest-args }}
110115
strategy:
111116
fail-fast: false

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ minversion = "6.0"
180180
testpaths = ["tests"]
181181
filterwarnings = [
182182
"error",
183+
"ignore:Failed to disconnect::pytestqt",
183184
"ignore::DeprecationWarning:tqdm",
184185
"ignore::DeprecationWarning:docstring_parser",
185186
"ignore:distutils Version classes are deprecated:DeprecationWarning",

src/magicgui/signature.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from __future__ import annotations
1616

1717
import inspect
18+
import typing
19+
import warnings
1820
from types import MappingProxyType
1921
from typing import TYPE_CHECKING, Any, Callable, Sequence, cast
2022

@@ -69,9 +71,65 @@ def make_annotated(annotation: Any = Any, options: dict | None = None) -> Any:
6971
f"Every item in Annotated must be castable to a dict, got: {opt!r}"
7072
) from e
7173
_options.update(_opt)
74+
75+
annotation = _make_hashable(annotation)
76+
_options = _make_hashable(_options)
7277
return Annotated[annotation, _options]
7378

7479

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+
75133
class _void:
76134
"""private sentinel."""
77135

src/magicgui/widgets/_function_gui.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def _inject_tooltips_from_docstrings(
6464
for split_key in k.split(","):
6565
doc_params[split_key.strip()] = v
6666
del doc_params[k]
67-
6867
for name, description in doc_params.items():
6968
# this is to catch potentially bad arg_name parsing in docstring_parser
7069
# if using napoleon style google docstringss

tests/test_types.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from enum import Enum
22
from pathlib import Path
3-
from typing import TYPE_CHECKING, Optional, Union
3+
from typing import TYPE_CHECKING, List, Optional, Union
44
from unittest.mock import Mock
55

66
import pytest
@@ -117,7 +117,8 @@ def widget2(fn: Union[bytes, pathlib.Path, str]):
117117

118118
def test_optional_type():
119119
@magicgui(x={"choices": ["a", "b"]})
120-
def widget(x: Optional[str] = None): ...
120+
def widget(x: Optional[str] = None):
121+
...
121122

122123
assert isinstance(widget.x, widgets.ComboBox)
123124
assert widget.x.value is None
@@ -231,3 +232,10 @@ def test_pick_widget_literal():
231232
)
232233
assert cls == widgets.RadioButtons
233234
assert set(options["choices"]) == {"a", "b"}
235+
236+
237+
def test_redundant_annotation() -> None:
238+
# just shouldn't raise
239+
@magicgui
240+
def f(a: Annotated[List[int], {"annotation": List[int]}]):
241+
pass

0 commit comments

Comments
 (0)