Repository: PrettyWood/typingx (language: python)

Issue:
get_args gives incorrect with Python 3.6 in case of nested generics
The following example illustrates the issue:
```python
from typing import Collection, List, TypeVar
import typingx

T = TypeVar("T")

print(typingx.get_args(List[Collection[T]][int]))  # should be (typing.Collection[int],)
#> (typing.Collection[~T],)
```
get_args gives incorrect with Python 3.6 in case of nested generics
The following example illustrates the issue:
```python
from typing import Collection, List, TypeVar
import typingx

T = TypeVar("T")

print(typingx.get_args(List[Collection[T]][int]))  # should be (typing.Collection[int],)
#> (typing.Collection[~T],)
```


Discussion / hints:

Fix patch:
diff --git a/typingx/__init__.py b/typingx/__init__.py
index 4da9e95..beab62b 100644
--- a/typingx/__init__.py
+++ b/typingx/__init__.py
@@ -21,6 +21,7 @@
     # typing, typing_extensions or own backport
     "Any",
     "Callable",
+    "Collection",
     "Dict",
     "FrozenSet",
     "Generic",
diff --git a/typingx/typing_compat.py b/typingx/typing_compat.py
index 6b092c1..c45e65f 100644
--- a/typingx/typing_compat.py
+++ b/typingx/typing_compat.py
@@ -35,7 +35,8 @@
 elif sys.version_info[:2] == (3, 8):
 
     def T_get_args(tp: TypeLike) -> T.Tuple[T.Any, ...]:
-        if tp is T.Callable:  # fallback since it fails on MacOS
+        # fallback for plain `Callable`, `Dict`, ... since it fails on MacOS
+        if tp in T.__dict__.values():
             return ()
         return T.get_args(tp)
 
@@ -56,6 +57,17 @@ def T_get_args(tp: TypeLike) -> T.Tuple[T.Any, ...]:
 
 else:
 
+    def _replace_typevar_by_resolved(tp: T.Any, typevar_mapping: T.Dict[T.Any, T.Any]) -> T.Any:
+        args = getattr(tp, "__args__", ())
+        # Check if `tp` is itself generic
+        if args:
+            # we don't use `get_origin` to keep `typing` type and not `collections.abc` one
+            origin = getattr(tp, "__origin__", None)
+            new_args = tuple(_replace_typevar_by_resolved(t, typevar_mapping) for t in args)
+            return origin[new_args]
+        else:
+            return typevar_mapping.get(tp, tp)
+
     def T_get_args(tp: TypeLike) -> T.Tuple[T.Any, ...]:
         args = getattr(tp, "__args__", ())
         origin = getattr(tp, "__origin__", None)
@@ -66,8 +78,8 @@ def T_get_args(tp: TypeLike) -> T.Tuple[T.Any, ...]:
         if origin_parameters and origin_args:
             # in this case `args` contains only the types of generic parameters (e.g. `(float,)`)
             # and `origin_parameters` contain the generic typevars (e.g. `(~T,)`)
-            generic_to_resolved = {typevar: tp for typevar, tp in zip(origin_parameters, args)}
-            args = tuple(generic_to_resolved.get(tp, tp) for tp in origin_args)
+            typevar_mapping = {typevar: tp for typevar, tp in zip(origin_parameters, args)}
+            args = tuple(_replace_typevar_by_resolved(tp, typevar_mapping) for tp in origin_args)
 
         if getattr(origin, "_gorg", None) is T.Callable and args and args[0] is not Ellipsis:
             args = (list(args[:-1]), args[-1])


Test patch:
diff --git a/tests/test_typing_compat.py b/tests/test_typing_compat.py
index 1899b72..f21ea29 100644
--- a/tests/test_typing_compat.py
+++ b/tests/test_typing_compat.py
@@ -5,6 +5,7 @@
 from typingx import (
     Any,
     Callable,
+    Collection,
     Dict,
     FrozenSet,
     Generic,
@@ -83,6 +84,21 @@ class StrangePair(Generic[T, S]):
         (Callable[[], T][int], ([], int)),
         (Callable[[T], T][int], ([int], int)),
         (Callable[[T, float], U][int, str], ([int, float], str)),
+        (List[Collection[T]][int], (Collection[int],)),
+        (
+            Mapping[T, Sequence[U]][str, int],
+            (
+                str,
+                Sequence[int],
+            ),
+        ),
+        (
+            Mapping[str, Mapping[T, Collection[U]]][float, int],
+            (
+                str,
+                Mapping[float, Collection[int]],
+            ),
+        ),
     ],
 )
 def test_get_args(tp, expected_args):