Repository: hanjinliu/magic-class (language: python)

Issue:
`TypeError: <class 'magicclass._gui._base.MagicTemplate'> is not a generic class` in Python 3.8
There seems to be a bug that *only* appears in Python 3.8. You can reproduce it as follows:

```bash
# Get Python 3.8
conda create -n py38 'python<3.9'
conda activate py38

# Install deps
pip install magicgui[pyqt5] magicclass

# Trigger bug
python3.8 -c 'import magicclass'
```
Result:
```
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/__init__.py", line 3, in <module>
    from .core import (
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/core.py", line 9, in <module>
    from magicclass._gui.class_gui import (
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/_gui/__init__.py", line 1, in <module>
    from ._base import BaseGui, MagicTemplate
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/magicclass/_gui/_base.py", line 814, in <module>
    class BaseGui(MagicTemplate[_W]):
  File "/opt/anaconda3/envs/py38/lib/python3.8/typing.py", line 261, in inner
    return func(*args, **kwds)
  File "/opt/anaconda3/envs/py38/lib/python3.8/typing.py", line 897, in __class_getitem__
    _check_generic(cls, params)
  File "/opt/anaconda3/envs/py38/lib/python3.8/site-packages/typing_extensions.py", line 155, in _check_generic
    raise TypeError(f"{cls} is not a generic class")
TypeError: <class 'magicclass._gui._base.MagicTemplate'> is not a generic class
```

I understand the error. It thinks that `class BaseGui(MagicTemplate[_W])` is treating `MagicTemplate` like a generic class when instead it just defines `__getitem__()` for another reason. However, I have no idea why the Python version affects this.


Discussion / hints:
Hi @multimeric , thank you for sharing the bug.
It is definitely my mistake adding a type variable to `MagicTemplate`, but I don't know why python>=3.9 did not treat it as an error...
FYI, python 3.8 is no longer supported (following NEP29). I recommend you to use python>=3.9 to get the newer updates.
I think people tend to follow the Python EOL timelines, and 3.8 is not yet end of life.
Thanks for the fix. Any chance you could publish a new patch version so I could get Python 3.8 working again?
Sure. I dropped 3.8 just recently so I think it'll not take much time.

Fix patch:
diff --git a/magicclass/_gui/_macro.py b/magicclass/_gui/_macro.py
index 8fb33c58..5444408e 100644
--- a/magicclass/_gui/_macro.py
+++ b/magicclass/_gui/_macro.py
@@ -418,7 +418,7 @@ def _action(
 
 class PropertyGroup:
     def __init__(self, parent: GuiMacro | None = None):
-        self._instances = dict[int, PropertyGroup]()
+        self._instances: dict[int, PropertyGroup] = {}
         self._parent = parent
         self._max_lines = 10000
         self._max_undo = 100
diff --git a/magicclass/types/_path.py b/magicclass/types/_path.py
index 0c214200..b0819696 100644
--- a/magicclass/types/_path.py
+++ b/magicclass/types/_path.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
-from abc import ABCMeta
 
+import sys
+from abc import ABCMeta
 import pathlib
 from typing import TYPE_CHECKING, Any, Sequence
 from typing_extensions import Annotated
@@ -27,7 +28,11 @@ def __subclasscheck__(cls, subclass: type) -> bool:
 
 class _AnnotatedMultiPathAlias(ABCMeta):
     def __getitem__(cls, filter: str) -> Self:
-        return Annotated[list[Path], {"mode": "rm", "filter": filter}]
+        if sys.version_info >= (3, 9):
+            return Annotated[list[pathlib.Path], {"mode": "rm", "filter": filter}]
+        return Annotated[
+            Any, {"mode": "rm", "filter": filter, "widget_type": "FileEdit"}
+        ]
 
 
 class _Path(pathlib.Path, metaclass=_AnnotatedPathAlias):
diff --git a/magicclass/widgets/line_runner.py b/magicclass/widgets/line_runner.py
index 837157ab..30609e02 100644
--- a/magicclass/widgets/line_runner.py
+++ b/magicclass/widgets/line_runner.py
@@ -10,8 +10,8 @@
 from magicgui.widgets.bases import ValueWidget
 from magicclass.widgets._const import FONT
 
-InjectorType = Union[Callable[[], dict[str, Any]], dict[str, Any]]
-TranslatorType = Callable[[str, dict[str, Any]], tuple[str, dict[str, Any]]]
+InjectorType = Union[Callable[[], "dict[str, Any]"], "dict[str, Any]"]
+TranslatorType = Callable[[str, "dict[str, Any]"], "tuple[str, dict[str, Any]]"]
 
 
 class HistoryStack:
diff --git a/pyproject.toml b/pyproject.toml
index ad4f43c3..72633bed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,6 +9,7 @@ classifiers = [
     "Operating System :: OS Independent",
     "Programming Language :: Python",
     "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
@@ -17,12 +18,12 @@ dynamic = ["version"]
 description = "Generate multifunctional GUIs from classes"
 readme = "README.md"
 license = { file = "LICENSE" }
-requires-python = ">=3.9"
+requires-python = ">=3.8"
 authors = [
     { name = "Hanjin Liu", email = "liuhanjin-sc@g.ecc.u-tokyo.ac.jp" },
 ]
 dependencies = [
-    "macro-kit>=0.4.5",
+    "macro-kit>=0.4.6",
     "magicgui>=0.7.0",
     "qtpy>=1.10.0",
     "superqt>=0.4.0",


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f967c598..350d2323 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,7 +18,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11"]
         platform: [ubuntu-latest, macos-latest, windows-latest]
         backend: [pyqt5]
         include:
diff --git a/magicclass/testing/_error_test.py b/magicclass/testing/_error_test.py
index 732b5314..9f9f9068 100644
--- a/magicclass/testing/_error_test.py
+++ b/magicclass/testing/_error_test.py
@@ -11,7 +11,7 @@ class GuiErrorMonitor(Sequence[Exception]):
     _instances: dict[BaseGui, GuiErrorMonitor] = {}
 
     def __init__(self, gui: BaseGui):
-        self._errors = list[Exception]()
+        self._errors: list[Exception] = []
 
     @classmethod
     def get_instance(cls, gui: BaseGui) -> GuiErrorMonitor:
diff --git a/tests/test_choices.py b/tests/test_choices.py
index 3911d951..ad24ae6b 100644
--- a/tests/test_choices.py
+++ b/tests/test_choices.py
@@ -178,7 +178,7 @@ class B:
             def _get_choices(self, w=None):
                 return [1, 2]
 
-            def _get_choices_with_name(self, w=None) -> list[tuple[str, int]]:
+            def _get_choices_with_name(self, w=None):
                 return [("a", 1), ("b", 2)]
 
         def f(self, c: OneOf[B._get_choices], d: OneOf[B._get_choices_with_name]):
@@ -204,7 +204,7 @@ class B:
             def _get_choices(self, w=None):
                 return [1, 2]
 
-            def _get_choices_with_name(self, w=None) -> list[tuple[str, int]]:
+            def _get_choices_with_name(self, w=None):
                 return [("a", 1), ("b", 2)]
 
         def f(self, c: SomeOf[B._get_choices], d: SomeOf[B._get_choices_with_name]):
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 66ddeab0..5808a2ad 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -10,8 +10,9 @@
 from magicclass.fields import widget_property
 from magicclass.types import Optional
 from magicgui import widgets
-from typing import Tuple
+import sys
 from unittest.mock import MagicMock
+import pytest
 
 def test_field_types():
     @magicclass
@@ -325,10 +326,11 @@ class A:
     assert type(ui["x_1"]) is widgets.SpinBox
     assert type(ui["y_1"]) is widgets.SpinBox
 
+@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+")
 def test_generic_and_annotated():
-    w0 = field(Tuple[int, str]).to_widget()
+    w0 = field(tuple[int, str]).to_widget()
     w1 = field(Optional[int], options={"text": "XXX", "options": {"max": 10}}).to_widget()
-    w2 = field(Optional[Tuple[int, str]]).to_widget()
+    w2 = field(Optional[tuple[int, str]]).to_widget()
 
     assert w0.widget_type == "TupleEdit"
     assert w1.widget_type == "OptionalWidget"
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 282ef650..b11e74aa 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -1,5 +1,4 @@
-from enum import auto
-from typing import Annotated
+from typing_extensions import Annotated
 from unittest.mock import MagicMock
 from magicclass import magicclass, abstractapi, field, vfield, confirm, impl_preview
 from magicclass.types import Bound
diff --git a/tests/test_types.py b/tests/test_types.py
index 76f0c6fc..d3951889 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -1,5 +1,5 @@
 from typing_extensions import Annotated
-
+import sys
 import pytest
 from magicclass import magicclass, set_options, get_function_gui
 from magicclass.types import Optional, Union, Path
@@ -69,6 +69,7 @@ def f(
     assert not opt[1].visible
     assert opt[1].min == -1
 
+@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+")
 def test_nested_annotated_in_optional():
     @magicclass
     class A: